Merge lane/testphase in feature/craftvia-mvp
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> # Conflicts: # prisma/schema.prisma # scripts/test-e2e-tenant-isolation.ts # src/server/backup/topology.ts # src/server/db.ts # src/server/dsgvo/pii-fields.ts
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
// 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<unknown>, 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<string>(ROLE_DEFS[role].permissions) };
|
||||
}
|
||||
|
||||
type MailInput = Parameters<typeof enqueueMail>[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-<name>). */
|
||||
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<string, Buffer> {
|
||||
const out = new Map<string, Buffer>();
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user