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>
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
// 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 };
|
||||
Reference in New Issue
Block a user