Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/apps/web/app/utils/services/adminDashboardService.test.ts
T

245 lines
9.0 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
import { getAdminActiveUserKpiCounts, getAdminTenantKpiCounts } from "@hivegrid/db/kpi.server";
import { StatChange } from "~/application/dtos/stats/StatChange";
import { getDbClient } from "@hivegrid/db/client.server";
import { listAuditEvents } from "@hivegrid/db/audit-events-query.server";
import { getAdminDashboardOverview, getAdminDashboardStats } from "./adminDashboardService";
import { getMrr } from "./.server/subscriptionService";
import { adminGetAllTenantsIdsAndNames } from "../db/tenants.db.server";
import { getRlsExecutionContext, runWithRlsContext } from "@hivegrid/db/rls-context.server";
import { getPlatformOutboxCounts } from "@hivegrid/db/admin-dashboard.server";
vi.mock("@hivegrid/db/kpi.server", () => ({
getAdminActiveUserKpiCounts: vi.fn(),
getAdminTenantKpiCounts: vi.fn(),
}));
vi.mock("@hivegrid/db/client.server", () => ({
getDbClient: vi.fn(),
}));
vi.mock("@hivegrid/db/audit-events-query.server", () => ({
listAuditEvents: vi.fn(),
}));
vi.mock("@hivegrid/db/admin-dashboard.server", () => ({
getPlatformOutboxCounts: vi.fn(),
}));
vi.mock("./.server/subscriptionService", () => ({
getMrr: vi.fn(),
}));
vi.mock("../db/tenants.db.server", () => ({
adminGetAllTenantsIdsAndNames: vi.fn(),
}));
const adminTenants = ["tenant_1", "tenant_2", "tenant_3", "tenant_4", "tenant_5"].map((id) => ({
id,
name: id,
slug: id,
}));
const translations: Record<string, string> = {
"admin.kpi.accounts.name": "Translated accounts",
"admin.kpi.accounts.hint": "Translated total accounts",
"admin.kpi.mrr.name": "Translated MRR",
"admin.kpi.mrr.hint": "Translated monthly revenue",
"admin.kpi.mrr.previousStat": "{{0}} translated accounts",
"admin.kpi.activeUsers.name": "Translated MAU",
"admin.kpi.activeUsers.hint": "Translated monthly active users",
};
function t(key: string) {
return translations[key] ?? key;
}
describe("getAdminDashboardStats", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(adminGetAllTenantsIdsAndNames).mockResolvedValue(adminTenants);
vi.mocked(getMrr).mockResolvedValue({ total: 1234.5, count: 7 });
});
it("builds translated KPI stats from package counts and forwards the selected period", async () => {
const gte = new Date("2026-01-01T00:00:00.000Z");
vi.mocked(getAdminTenantKpiCounts).mockResolvedValue({ added: 3, total: 10 });
vi.mocked(getAdminActiveUserKpiCounts).mockResolvedValue({ added: 2, total: 8 });
const stats = await getAdminDashboardStats({ t, gte });
expect(getAdminTenantKpiCounts).toHaveBeenCalledTimes(1);
expect(getAdminTenantKpiCounts).toHaveBeenCalledWith({ gte });
expect(getAdminActiveUserKpiCounts).toHaveBeenCalledTimes(1);
expect(getAdminActiveUserKpiCounts).toHaveBeenCalledWith({ gte });
expect(getMrr).toHaveBeenCalledTimes(1);
expect(getMrr).toHaveBeenCalledWith(
"eur",
adminTenants.map((tenant) => tenant.id)
);
expect(stats).toEqual([
{
name: "Translated accounts",
hint: "Translated total accounts",
stat: "10",
previousStat: "7",
change: "+3",
changeType: StatChange.Increase,
path: "/accounts",
},
{
name: "Translated MRR",
hint: "Translated monthly revenue",
stat: "€1,234.50",
previousStat: "7 translated accounts",
},
{
name: "Translated MAU",
hint: "Translated monthly active users",
stat: "8",
previousStat: "6",
change: "+2",
changeType: StatChange.Increase,
},
]);
});
it("keeps zero additions visible with equal change type and missing translations as key text", async () => {
vi.mocked(getAdminTenantKpiCounts).mockResolvedValue({ added: 0, total: 5 });
vi.mocked(getAdminActiveUserKpiCounts).mockResolvedValue({ added: 0, total: 0 });
vi.mocked(getMrr).mockResolvedValue({ total: 0, count: 0 });
const stats = await getAdminDashboardStats({ t: (key) => key });
expect(getAdminTenantKpiCounts).toHaveBeenCalledWith({ gte: undefined });
expect(getAdminActiveUserKpiCounts).toHaveBeenCalledWith({ gte: undefined });
expect(stats[0]).toMatchObject({
name: "admin.kpi.accounts.name",
hint: "admin.kpi.accounts.hint",
stat: "5",
previousStat: "5",
change: "+0",
changeType: StatChange.Equal,
path: "/accounts",
});
expect(stats[1]).toMatchObject({
name: "admin.kpi.mrr.name",
hint: "admin.kpi.mrr.hint",
stat: "€0.00",
previousStat: "admin.kpi.mrr.previousStat",
});
expect(stats[2]).toMatchObject({
name: "admin.kpi.activeUsers.name",
hint: "admin.kpi.activeUsers.hint",
stat: "0",
previousStat: "0",
change: "+0",
changeType: StatChange.Increase,
});
});
});
describe("getAdminDashboardOverview", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(adminGetAllTenantsIdsAndNames).mockResolvedValue(adminTenants);
vi.stubEnv("BULLMQ_REDIS_URL", "redis://localhost:6379/0");
vi.stubEnv("MEILI_URL", "http://localhost:7700");
vi.stubEnv("MEILI_API_KEY", "devkey");
vi.stubEnv("LAGO_API_URL", "http://lago.local");
vi.stubEnv("LAGO_API_KEY", "lago-key");
vi.stubEnv("MOLLIE_API_KEY", "mollie-key");
vi.stubEnv("MOLLIE_PROFILE_ID", "profile-id");
vi.stubEnv("SENTRY_ENVIRONMENT", "staging");
vi.stubEnv("SENTRY_RELEASE", "abc123");
vi.stubEnv("SENTRY_DSN", "https://example.invalid/1");
vi.mocked(listAuditEvents).mockResolvedValue({
rows: [
{
id: "audit-1",
createdAt: new Date("2026-07-01T10:00:00.000Z"),
summaryText: "Tenant aktualisiert",
action: "tenant.update",
actorEmail: "ops@example.test",
targetType: "Tenant",
sourceModule: "admin",
actorType: "user",
outcome: "success",
},
],
total: 1,
take: 5,
skip: 0,
} as never);
vi.mocked(getPlatformOutboxCounts).mockResolvedValue({ pendingOrProcessing: 5, failed: 6 });
});
it("builds billing, system, deploy, and audit overview signals", async () => {
vi.mocked(getDbClient).mockReturnValue(createDashboardClient({
failedWebhookCount: 4,
}) as never);
const overview = await runWithRlsContext({ userId: "operator_1" }, () => getAdminDashboardOverview());
expect(overview.billing).toMatchObject({
openIssueCount: 6,
pastDueCount: 2,
frozenCount: 1,
missingProviderCount: 3,
});
expect(overview.billing.signals).toEqual(
expect.arrayContaining([
expect.objectContaining({ id: "billing-issues", value: "6", tone: "warning", href: "/system/billing" }),
expect.objectContaining({ id: "billing-past-due", value: "2", tone: "danger", href: "/system/billing" }),
])
);
expect(overview.system.signals).toEqual(
expect.arrayContaining([
expect.objectContaining({ id: "redis", value: "OK", tone: "success" }),
expect.objectContaining({ id: "webhooks", value: "4", tone: "warning", href: "/system/webhooks" }),
expect.objectContaining({ id: "outbox", value: "5/6", tone: "danger", href: "/events" }),
])
);
expect(overview.deploy).toMatchObject({ environment: "staging", release: "abc123" });
expect(overview.recentEvents).toEqual([
expect.objectContaining({
id: "audit-1",
label: "Tenant aktualisiert",
detail: "ops@example.test · Tenant",
timestamp: "2026-07-01T10:00:00.000Z",
href: "/audit",
}),
]);
expect(listAuditEvents).toHaveBeenCalledWith({ take: 5, skip: 0 });
expect(getPlatformOutboxCounts).toHaveBeenCalledWith(undefined, {
allowMarkedLocalDatabaseFallback: false,
});
});
});
function createDashboardClient({
failedWebhookCount,
}: {
failedWebhookCount: number;
}) {
const subscriptions = new Map([
["tenant_1", { status: "past_due", pastDueSince: null, frozenAt: null, planCode: "pro", lagoCustomerId: null, mollieCustomerId: "m_1" }],
["tenant_2", { status: "trial", pastDueSince: new Date(), frozenAt: null, planCode: null, lagoCustomerId: "l_2", mollieCustomerId: "m_2" }],
["tenant_3", { status: "frozen", pastDueSince: null, frozenAt: new Date(), planCode: "pro", lagoCustomerId: "l_3", mollieCustomerId: null }],
["tenant_4", { status: "active", pastDueSince: null, frozenAt: null, planCode: "pro", lagoCustomerId: null, mollieCustomerId: "m_4" }],
["tenant_5", { status: "trial", pastDueSince: null, frozenAt: null, planCode: null, lagoCustomerId: "l_5", mollieCustomerId: "m_5" }],
]);
const tenantSubscriptionFindUnique = vi.fn(async ({ where }: { where: { tenantId: string } }) => {
expect(getRlsExecutionContext()).toMatchObject({ userId: "operator_1", tenantId: where.tenantId });
return subscriptions.get(where.tenantId) ?? null;
});
return {
tenantSubscription: {
findUnique: tenantSubscriptionFindUnique,
},
eventWebhookAttempt: {
count: vi.fn().mockResolvedValue(failedWebhookCount),
},
};
}