// Shared fixture of the L15 tests (scripts/test-testphase-*.ts): zz trial tenants, platform admins, // mail capture, zip reader and a complete tenant purge. Not a test itself (runner only picks up // scripts/test-*.ts at top level). import { inflateRawSync } from "node:zlib"; import { prisma, dbForTenant } from "../../src/server/db"; import { ROLE_DEFS, type RoleKey } from "../../src/server/rbac"; import type { ServiceCtx } from "../../src/server/services/context"; import { offboardTenant } from "../../src/server/dsgvo/deletion"; import type { enqueueMail } from "../../src/server/mail/service"; import { provisionTrialTenant } from "../../src/server/services/trial/provision"; export const DOMAIN = "@zz-l15.test"; export const SLUG_PREFIX = "zz-l15"; export let failures = 0; export const ok = (cond: boolean, msg: string) => { console.log(`${cond ? "✓" : "✗ FEHLER"} ${msg}`); if (!cond) failures++; }; /** Expects `fn` to throw an error with the given `code` (ServiceError) — or any error when code is "*". */ export async function expectCode(fn: () => Promise, code: string, msg: string) { try { await fn(); ok(false, `${msg} — kein Fehler (erwartet ${code})`); } catch (err) { const actual = (err as { code?: string }).code; const pass = code === "*" || actual === code; ok(pass, `${msg}${pass ? "" : ` — Code ${actual ?? (err as Error).message}`}`); } } export function ctxFor(tenantId: string, userId: string, role: RoleKey): ServiceCtx { return { db: dbForTenant(tenantId), tenantId, userId, permissions: new Set(ROLE_DEFS[role].permissions) }; } type MailInput = Parameters[0]; export function captureMail() { const sent: MailInput[] = []; const fn = (async (input: MailInput) => { sent.push(input); return { status: "queued", mailLogId: `zz-${sent.length}` }; }) as typeof enqueueMail; return { sent, fn }; } export async function platformAdmin(role: "full" | "readonly" = "full") { return prisma.platformAdmin.create({ data: { email: `platform-${role}-${Math.random().toString(36).slice(2, 8)}${DOMAIN}`, passwordHash: "x", name: `ZZ Platform ${role}`, role }, }); } /** Trial tenant through the real provisioning (slug zz-l15-). */ export async function trialTenant(name: string, endDateKey: string, opts: { sampleData?: boolean } = {}) { const slugPart = name.toLowerCase().replace(/[^a-z0-9]+/g, "-"); return provisionTrialTenant({ companyName: `ZZ L15 ${name}`, admin: { name: `Admin ${name}`, email: `admin-${slugPart}${DOMAIN}`, passwordHash: "x" }, endDateKey, sampleData: opts.sampleData ?? false, source: "platform", }); } export async function addMember(tenantId: string, local: string, role: RoleKey) { const email = `${local}${DOMAIN}`; const identity = await prisma.identity.upsert({ where: { email }, update: {}, create: { email, passwordHash: "x" } }); const roleRow = await prisma.role.findUniqueOrThrow({ where: { tenantId_key: { tenantId, key: role } } }); return prisma.user.create({ data: { tenantId, identityId: identity.id, email, name: local, userRoles: { create: [{ roleId: roleRow.id }] } } }); } /** Removes a test tenant completely (offboarding of all tenant tables + rows around the tenant). */ export async function purgeTenant(tenantId: string) { const exists = await prisma.tenant.findUnique({ where: { id: tenantId }, select: { id: true } }); if (!exists) return; await offboardTenant(tenantId, { reason: "zz-test-cleanup", purgeFiles: false }); await prisma.auditLog.deleteMany({ where: { tenantId } }); await prisma.mailLog.deleteMany({ where: { tenantId } }); await prisma.deletionCertificate.deleteMany({ where: { tenantId } }); await prisma.trialSignup.deleteMany({ where: { provisionedTenantId: tenantId } }); await prisma.tenant.delete({ where: { id: tenantId } }); } export async function cleanupL15() { const tenants = await prisma.tenant.findMany({ where: { slug: { startsWith: SLUG_PREFIX } }, select: { id: true } }); for (const t of tenants) await purgeTenant(t.id); await prisma.trialSignup.deleteMany({ where: { email: { endsWith: DOMAIN } } }); await prisma.identity.deleteMany({ where: { email: { endsWith: DOMAIN }, memberships: { none: {} } } }); await prisma.platformAdmin.deleteMany({ where: { email: { endsWith: DOMAIN } } }); } /** Minimal ZIP reader for the export tests (deflate entries written by src/server/backup/zip.ts). */ export function readZip(buf: Buffer): Map { const out = new Map(); let offset = 0; while (offset + 30 <= buf.length && buf.readUInt32LE(offset) === 0x04034b50) { const method = buf.readUInt16LE(offset + 8); const compressed = buf.readUInt32LE(offset + 18); const nameLen = buf.readUInt16LE(offset + 26); const extraLen = buf.readUInt16LE(offset + 28); const name = buf.subarray(offset + 30, offset + 30 + nameLen).toString("utf8"); const start = offset + 30 + nameLen + extraLen; const data = buf.subarray(start, start + compressed); out.set(name, method === 8 ? inflateRawSync(data) : Buffer.from(data)); offset = start + compressed; } return out; }