Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/apps/web/app/services/admin/admin-services.integration.test.ts
T
2026-07-08 16:15:37 +02:00

115 lines
5.1 KiB
TypeScript

/**
* AK-P19 — DB-backed integration test for the admin services.
*
* Gated on ADMIN_IT_DATABASE_URL (like the RLS gate tests): it runs the real
* service code through a real Prisma client against a real Postgres with the
* admin.prisma schema pushed, so it catches query/schema mismatches the
* fake-client unit tests cannot. Skipped automatically when the env var is
* unset, so normal `pnpm test` / CI stay green without a database.
*
* To run locally:
* prisma db push --url <url>
* ADMIN_IT_DATABASE_URL=<url> node scripts/vitest-cli-compat.mjs run \
* apps/web/app/services/admin/admin-services.integration.test.ts
*/
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { createDbClient, type DbClient } from "@hivegrid/db/client.server";
import { getDefaultProductBlueprint, getProductBlueprint } from "@hivegrid/entitlements";
import { createTenantNote, deleteTenantNote, getTenantNoteRows, type TenantNotesDeps } from "./tenant-notes.server";
import {
getTenantConnectorRows,
setTenantFeatureConnector,
type ConnectorServiceDeps,
} from "./subscription-assembly.server";
import {
assignSupportTicket,
postSupportInternalNote,
postSupportReply,
setSupportTicketStatus,
type SupportInboxDeps,
} from "./support-inbox.server";
const IT_URL = process.env.ADMIN_IT_DATABASE_URL;
const gated = IT_URL ? describe : describe.skip;
const noopRecord = async () => {};
const realBlueprintReader: ConnectorServiceDeps["readBlueprint"] = (blueprintId) =>
(blueprintId ? getProductBlueprint(blueprintId) : undefined)?.features ?? getDefaultProductBlueprint().features;
gated("admin services against a real database", () => {
let db: DbClient;
let tenantId: string;
// Unique slug per run so repeated local runs don't collide on the unique index.
const slug = `akp19-it-${Date.now().toString(36)}`;
beforeAll(async () => {
db = createDbClient({ connectionString: IT_URL });
const tenant = await db.tenant.create({ data: { slug, name: "AK-P19 IT Tenant" } });
tenantId = tenant.id;
});
afterAll(async () => {
if (db && tenantId) {
await db.adminTenantNote.deleteMany({ where: { tenantId } });
await db.tenantFeatureOverride.deleteMany({ where: { tenantId } });
await db.supportTicket.deleteMany({ where: { tenantId } }); // cascades to messages
await db.tenant.delete({ where: { id: tenantId } });
await db.$disconnect();
}
});
it("creates, lists and deletes a tenant note", async () => {
const deps: TenantNotesDeps = { db, recordAction: noopRecord };
await createTenantNote(
{ tenantId, noteText: "Zahlungsziel verlaengert", category: "billing", pinned: true, authorUserId: null },
deps
);
const rows = await getTenantNoteRows(tenantId, deps);
expect(rows).toHaveLength(1);
expect(rows[0]).toMatchObject({ category: "billing", pinned: true, noteText: "Zahlungsziel verlaengert" });
await deleteTenantNote({ noteId: rows[0]!.id, tenantId, actorUserId: null }, deps);
expect(await getTenantNoteRows(tenantId, deps)).toHaveLength(0);
});
it("toggles a feature connector override and clears it on inherit", async () => {
const deps: ConnectorServiceDeps = {
db,
invalidateCache: () => {},
readBlueprint: realBlueprintReader,
recordAction: noopRecord,
};
await setTenantFeatureConnector({ tenantId, featureKey: "files", state: "disabled", reason: "Pilot ohne Files", actorUserId: null }, deps);
const disabled = await getTenantConnectorRows(tenantId, deps);
expect(disabled.find((r) => r.featureKey === "files")?.overrideState).toBe("disabled");
await setTenantFeatureConnector({ tenantId, featureKey: "files", state: "inherit", reason: "Zurueck auf Plan-Default", actorUserId: null }, deps);
const inherited = await getTenantConnectorRows(tenantId, deps);
expect(inherited.find((r) => r.featureKey === "files")?.overrideState).toBeNull();
});
it("runs the support-inbox thread: reply, internal note, status and assignment", async () => {
const ticket = await db.supportTicket.create({
data: { tenantId, source: "in_app_feedback_widget", subject: "Login kaputt", message: "Ich komme nicht rein" },
});
const deps: SupportInboxDeps = { db, recordAction: noopRecord };
await postSupportReply({ ticketId: ticket.id, body: "Wir schauen es uns an", authorUserId: null }, deps);
await postSupportInternalNote({ ticketId: ticket.id, body: "Vermutlich MFA-Reset noetig", authorUserId: null }, deps);
await setSupportTicketStatus({ ticketId: ticket.id, status: "closed", actorUserId: null }, deps);
await assignSupportTicket({ ticketId: ticket.id, assigneeUserId: null, actorUserId: null }, deps);
const messages = await db.supportTicketMessage.findMany({
where: { supportTicketId: ticket.id },
orderBy: { createdAt: "asc" },
});
expect(messages).toHaveLength(2);
expect(messages.map((m) => m.internalNote)).toEqual([false, true]);
const updated = await db.supportTicket.findUniqueOrThrow({ where: { id: ticket.id } });
expect(updated.status).toBe("closed");
});
});