// Shared fixtures for the scripts/test-stammdaten-*.ts tests (lane L1 Stammdaten). // Creates isolated zz-test tenants with the raw owner client and builds ServiceCtx objects with // the permission sets of the standard roles (src/server/rbac.ts ROLE_DEFS). Not a test itself // (the runner only picks up files named test-*.ts). import "dotenv/config"; import { prisma, dbForTenant } from "../src/server/db"; import { ROLE_DEFS, type RoleKey } from "../src/server/rbac"; import { ServiceError, type ServiceCtx } from "../src/server/services/context"; export function checker(title: string) { let failures = 0; const ok = (cond: unknown, msg: string) => { console.log(`${cond ? "✓" : "✗ FEHLER"} ${msg}`); if (!cond) failures++; }; /** Expect `fn` to reject with a ServiceError of `code` (and optionally `reason`). */ const expectServiceError = async (fn: () => Promise, code: ServiceError["code"], msg: string, reason?: string) => { try { await fn(); ok(false, `${msg} — kein Fehler geworfen`); } catch (err) { const e = err as ServiceError; const actualReason = (e.details as { reason?: string } | undefined)?.reason; const match = e instanceof ServiceError && e.code === code && (!reason || actualReason === reason); ok(match, `${msg}${match ? "" : ` — erhalten: ${e?.name}/${e?.code ?? ""}/${actualReason ?? ""} ${e?.message ?? ""}`}`); } }; /** Expect `fn` to reject with any error whose name matches (e.g. ZodError). */ const expectErrorName = async (fn: () => Promise, name: string, msg: string) => { try { await fn(); ok(false, `${msg} — kein Fehler geworfen`); } catch (err) { ok((err as Error)?.name === name, `${msg}${(err as Error)?.name === name ? "" : ` — erhalten: ${(err as Error)?.name} ${(err as Error)?.message}`}`); } }; const finish = () => { console.log(failures === 0 ? `\nOK — ${title}: alle Prüfungen erfüllt.` : `\n${failures} FEHLER in ${title}.`); return failures; }; return { ok, expectServiceError, expectErrorName, finish, get failures() { return failures; } }; } const MODELS_IN_DELETE_ORDER = [ "signature", "report", "photo", "activityNote", "voiceNote", "materialUsage", "timeEntry", "workSession", "materialPlan", "checklistItem", "photoRequirement", "workOrderStatusChange", "workOrderAssignee", "document", "syncOperation", "notification", "aiGeneration", "workOrder", "importJob", "site", "contact", "customer", "teamMember", "team", "checklistTemplate", "orderType", "numberSequence", "auditLog", ] as const; export async function cleanupTenants(slugs: string[], emailDomain: string) { const tenants = await prisma.tenant.findMany({ where: { slug: { in: slugs } }, select: { id: true } }); const ids = tenants.map((t) => t.id); if (ids.length) { const client = prisma as unknown as Record Promise }>; for (const model of MODELS_IN_DELETE_ORDER) await client[model].deleteMany({ where: { tenantId: { in: ids } } }); await prisma.userRole.deleteMany({ where: { user: { tenantId: { in: ids } } } }); await prisma.user.deleteMany({ where: { tenantId: { in: ids } } }); await prisma.tenantModule.deleteMany({ where: { tenantId: { in: ids } } }); await prisma.tenantSettings.deleteMany({ where: { tenantId: { in: ids } } }); await prisma.role.deleteMany({ where: { tenantId: { in: ids } } }); await prisma.tenant.deleteMany({ where: { id: { in: ids } } }); } await prisma.identity.deleteMany({ where: { email: { endsWith: `@${emailDomain}` }, memberships: { none: {} } } }); } export async function createTenant(slug: string, name: string) { return prisma.tenant.create({ data: { slug, name } }); } export async function createUser(tenantId: string, email: string, name: string) { const identity = await prisma.identity.upsert({ where: { email }, update: {}, create: { email, passwordHash: "x" } }); return prisma.user.create({ data: { tenantId, identityId: identity.id, email, name, status: "ACTIVE" } }); } /** ServiceCtx with the permission set of a standard role (plus/minus adjustments). */ export function ctxFor(tenantId: string, userId: string, role: RoleKey, adjust: { add?: string[]; remove?: string[] } = {}): ServiceCtx { const perms = new Set(ROLE_DEFS[role].permissions); for (const p of adjust.add ?? []) perms.add(p); for (const p of adjust.remove ?? []) perms.delete(p); return { db: dbForTenant(tenantId), tenantId, userId, permissions: perms }; } let orderSeq = 0; export async function createWorkOrder( tenantId: string, data: { customerId: string; siteId?: string | null; assignedTeamId?: string | null; status?: string; title?: string; followUpWork?: string | null; plannedStart?: Date }, ) { orderSeq++; return prisma.workOrder.create({ data: { tenantId, number: `ZZ-${Date.now().toString(36)}-${orderSeq}`, customerId: data.customerId, siteId: data.siteId ?? null, assignedTeamId: data.assignedTeamId ?? null, status: (data.status ?? "assigned") as never, title: data.title ?? `Testauftrag ${orderSeq}`, followUpWork: data.followUpWork ?? null, plannedStart: data.plannedStart, }, }); } export async function createTeamWithMember(tenantId: string, name: string, memberUserId: string | null, leaderUserId: string | null = null) { const team = await prisma.team.create({ data: { tenantId, name, leaderUserId } }); if (memberUserId) await prisma.teamMember.create({ data: { tenantId, teamId: team.id, userId: memberUserId, validFrom: new Date(Date.now() - 86_400_000) } }); return team; } export async function disconnect() { await prisma.$disconnect(); } export { prisma };