Files
craftvia/scripts/lib-stammdaten-fixtures.ts
T
msolarczekandClaude Opus 5 6423351035 L1 Stammdaten: Tests für Dubletten, Kunden, Objekte, Teams und Dokumente
Kernlogik, Mandantentrennung (Mandant B liest/ändert nichts von A) und
Rollen/Scope (Monteur ohne Zuweisung → not_found/forbidden).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 12:26:27 +02:00

141 lines
5.7 KiB
TypeScript

// 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<unknown>, 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<unknown>, 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<string, { deleteMany: (a: unknown) => Promise<unknown> }>;
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<string>(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 };