115 lines
5.1 KiB
TypeScript
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");
|
|
});
|
|
});
|