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>
141 lines
5.7 KiB
TypeScript
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 };
|