/* eslint-disable @typescript-eslint/no-require-imports -- CommonJS is required by the isolated TypeScript module loader. */
const { test, before, after } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const vm = require('node:vm');
const ts = require('typescript');
const { PrismaClient, UserRole } = require('@prisma/client');
const { hash, compare } = require('bcryptjs');
const { randomUUID } = require('node:crypto');
// Execute the actual application code. Only the Next request/UI boundary and
// outgoing email transport are replaced. Database queries and bcrypt are real.
function loader(overrides = {}) {
const cache = new Map();
function load(file) {
file = path.resolve(file);
if (cache.has(file)) return cache.get(file).exports;
const mod = { exports: {} }; cache.set(file, mod);
const code = ts.transpileModule(fs.readFileSync(file, 'utf8'), {
compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS, jsx: ts.JsxEmit.ReactJSX, esModuleInterop: true },
fileName: file,
}).outputText;
const req = spec => {
if (Object.hasOwn(overrides, spec)) return overrides[spec];
if (spec === 'server-only') return {};
if (spec === '@/lib/prisma') return { prisma };
if (spec === 'next/headers') return { headers: async () => new Headers() };
if (spec === 'next/cache') return { revalidatePath() {} };
if (spec === 'next/navigation') return { redirect(url) { throw new Error(`REDIRECT:${url}`); }, notFound() { throw new Error('NOT_FOUND'); } };
if (spec.startsWith('@/') || spec.startsWith('.')) {
const target = spec.startsWith('@/') ? path.resolve('src', spec.slice(2)) : path.resolve(path.dirname(file), spec);
for (const suffix of ['.ts', '.tsx', '/index.ts']) if (fs.existsSync(target + suffix)) return load(target + suffix);
}
return require(spec);
};
vm.runInThisContext(`(function(require,module,exports){${code}\n})`, { filename: file })(req, mod, mod.exports);
return mod.exports;
}
return load;
}
process.env.AUTH_URL = 'http://127.0.0.1:3008';
let prisma, kitaA, kitaB, familyA, familyB, parentA, parentB, adminA, founder, superadmin, childA, childB, passHash;
const prefix = `security-${randomUUID()}`;
const emails = [];
const session = user => ({ user: { id: user.id, kitaId: user.kitaId, familyId: user.familyId, role: user.role, email: user.email, sessionVersion: user.sessionVersion, authenticatedAt: Date.now() } });
const fields = data => { const form = new FormData(); for (const [key, value] of Object.entries(data)) form.set(key, value); return form; };
const emailMock = { getAppEmailConfigError: () => null, sendAppEmail: async input => { emails.push(input); return { success: true }; } };
const noAuth = { auth: async () => null, signIn: async () => {} };
let authConfig;
before(async () => {
const url = process.env.SECURITY_TEST_DATABASE_URL;
if (!url || !['127.0.0.1','localhost'].includes(new URL(url).hostname) || !new URL(url).pathname.endsWith('/kita_security_test')) {
throw new Error('Set SECURITY_TEST_DATABASE_URL to the dedicated local kita_security_test database; never use application data.');
}
prisma = new PrismaClient({ datasourceUrl: url });
passHash = await hash('Safe-test-password', 12);
kitaA = await prisma.kita.create({ data: { name: prefix + '-A', slug: prefix + '-a' } });
kitaB = await prisma.kita.create({ data: { name: prefix + '-B', slug: prefix + '-b' } });
familyA = await prisma.family.create({ data: { kitaId: kitaA.id, name: 'A' } });
familyB = await prisma.family.create({ data: { kitaId: kitaB.id, name: 'B' } });
const makeUser = (label, role, kitaId, familyId) => prisma.user.create({ data: { email: `${label}.${prefix}@example.invalid`, firstName: label, lastName: 'Test', role, kitaId, familyId, passwordHash: passHash, emailVerifiedAt: new Date(), privacyPolicyAcceptedAt: new Date() } });
parentA = await makeUser('parent-a', UserRole.ELTERN, kitaA.id, familyA.id);
parentB = await makeUser('parent-b', UserRole.ELTERN, kitaB.id, familyB.id);
adminA = await makeUser('admin-a', UserRole.ADMIN, kitaA.id, null);
founder = await makeUser('founder', UserRole.ADMIN, null, null);
superadmin = await makeUser('superadmin', UserRole.SUPERADMIN, null, null);
childA = await prisma.child.create({ data: { kitaId: kitaA.id, familyId: familyA.id, firstName: 'A', lastName: 'Test' } });
childB = await prisma.child.create({ data: { kitaId: kitaB.id, familyId: familyB.id, firstName: 'B', lastName: 'Test' } });
loader({ 'next-auth': config => { authConfig = config; return {}; }, 'next-auth/providers/credentials': options => options })('src/auth.ts');
});
after(async () => {
if (!prisma) return;
await prisma.verificationToken.deleteMany({ where: { identifier: { in: [parentA?.id, parentB?.id].filter(Boolean) } } });
await prisma.pendingRegistration.deleteMany({ where: { email: { contains: prefix } } });
await prisma.kita.deleteMany({ where: { slug: { startsWith: prefix } } });
await prisma.user.deleteMany({ where: { email: { contains: prefix } } });
await prisma.auditLog.deleteMany({ where: { actorEmail: { contains: prefix } } });
await prisma.$disconnect();
});
test('bcrypt limit counts UTF-8 bytes, preventing equivalent long passwords', async () => {
const { passwordSchema } = loader()('src/lib/password.ts');
assert.equal(passwordSchema.safeParse('😀'.repeat(18)).success, true);
assert.equal(passwordSchema.safeParse('😀'.repeat(18) + 'A').success, false);
assert.equal(passwordSchema.safeParse('a'.repeat(73)).success, false);
assert.equal(await compare('😀'.repeat(18) + 'B', await hash('😀'.repeat(18) + 'A', 4)), true);
});
test('redirects reject external, backslash and control-character destinations', () => {
const { safeCallbackUrl } = loader()('src/lib/safe-redirect.ts');
for (const bad of ['//evil.invalid','/\\evil.invalid','https://evil.invalid','/\nevil']) assert.equal(safeCallbackUrl(bad), '/dashboard');
assert.equal(safeCallbackUrl('/admin?view=1'), '/admin?view=1');
});
test('invalid calendar dates and planning months are rejected', () => {
const { dateOnlySchema, validPlanMonth } = loader()('src/lib/date-schema.ts');
for (const bad of ['2026-02-31','2026-13-01','9999-99-99','garbage']) assert.equal(dateOnlySchema.safeParse(bad).success, false);
assert.equal(dateOnlySchema.safeParse('2028-02-29').success, true);
assert.equal(validPlanMonth(2026, 13), false); assert.equal(validPlanMonth(2026.5, 1), false);
});
test('upload checks reject HTML disguised as JPG and path traversal', () => {
const files = loader()('src/lib/attachments.ts');
assert.equal(files.detectAttachmentType(Buffer.from('')), null);
assert.throws(() => files.attachmentPath('../../etc/passwd'));
assert.equal(files.attachmentExtensionMatches('attack.jpg', 'application/pdf'), false);
assert.match(files.attachmentDisposition('a\r\nX-Injected: yes.pdf'), /^attachment;/);
assert.doesNotMatch(files.attachmentDisposition('a\r\nX-Injected: yes.pdf'), /[\r\n]/);
});
test('rate limiting is atomic under concurrent requests and expires correctly', async () => {
const { rateLimit } = loader()('src/lib/rate-limit.ts');
const results = await Promise.all(Array.from({ length: 20 }, () => rateLimit(prefix + ':parallel', 5, 60)));
assert.equal(results.filter(result => result.allowed).length, 5);
assert.equal((await rateLimit(prefix + ':parallel', 5, 60)).allowed, false);
const { createHash } = require('node:crypto');
await prisma.rateLimitBucket.update({ where: { key: createHash('sha256').update(prefix + ':parallel').digest('hex') }, data: { resetAt: new Date(0) } });
assert.equal((await rateLimit(prefix + ':parallel', 5, 60)).allowed, true);
});
test('JWT callback rejects deleted, malformed and old-format sessions', async () => {
assert.equal(await authConfig.callbacks.jwt({ token: {} }), null);
assert.equal(await authConfig.callbacks.jwt({ token: { id: parentA.id } }), null);
assert.equal(await authConfig.callbacks.jwt({ token: { id: 'missing-' + prefix, sessionVersion: 0, authenticatedAt: Date.now() } }), null);
});
test('administrative lock and unlock permanently revoke existing sessions', async () => {
const admitted = await authConfig.providers[0].authorize({ email: parentA.email, password: 'Safe-test-password' });
assert.equal(admitted.id, parentA.id);
const token = await authConfig.callbacks.jwt({ token: {}, user: admitted });
const admin = loader({ '@/auth': { auth: async () => session(superadmin) } })('src/app/admin/actions.ts');
assert.equal((await admin.lockUser(parentA.id)).ok, true);
assert.equal(await authConfig.callbacks.jwt({ token: { ...token } }), null);
assert.equal(await authConfig.providers[0].authorize({ email: parentA.email, password: 'Safe-test-password' }), null);
assert.equal((await admin.unlockUser(parentA.id)).ok, true);
assert.equal(await authConfig.callbacks.jwt({ token: { ...token } }), null);
const relogin = await authConfig.providers[0].authorize({ email: parentA.email, password: 'Safe-test-password' });
assert.equal(relogin.sessionVersion, token.sessionVersion + 1);
parentA = await prisma.user.findUnique({ where: { id: parentA.id } });
});
test('parallel incorrect passwords cannot lose failed-login increments', async () => {
await Promise.all(Array.from({ length: 8 }, () => authConfig.providers[0].authorize({ email: parentB.email, password: 'wrong' })));
const user = await prisma.user.findUnique({ where: { id: parentB.id } });
assert.equal(user.failedLoginAttempts, 8);
await prisma.user.update({ where: { id: parentB.id }, data: { failedLoginAttempts: 0 } });
});
test('normal roles require consent and tenant; TEST_MODE never creates a fake identity', async () => {
await prisma.user.update({ where: { id: adminA.id }, data: { privacyPolicyAcceptedAt: null } });
const util = loader({ '@/auth': { auth: async () => session(adminA) } })('src/lib/auth-utils.ts');
await assert.rejects(util.requireRole([UserRole.ADMIN]), /REDIRECT:\/onboarding\/consent/);
assert.equal(await util.getKitaSession(), null);
await prisma.user.update({ where: { id: adminA.id }, data: { privacyPolicyAcceptedAt: new Date() } });
const old = process.env.TEST_MODE; process.env.TEST_MODE = 'true';
try { await assert.rejects(loader({ '@/auth': noAuth })('src/lib/auth-utils.ts').requireRole([UserRole.SUPERADMIN]), /REDIRECT:\/login/); }
finally { if (old === undefined) delete process.env.TEST_MODE; else process.env.TEST_MODE = old; }
});
test('onboarding denies missing IDs, global admins and unverified founders', async () => {
const payload = fields({ kitaName: 'Test', notdienstMinPerChildPerMonth: '1' });
for (const bad of [null, { user: { role: UserRole.ADMIN } }, session(superadmin)]) {
const action = loader({ '@/auth': { auth: async () => bad } })('src/app/onboarding/actions.ts').completeOnboardingAction;
await assert.rejects(action({ attempt: 0 }, payload), /REDIRECT:/);
}
await prisma.user.update({ where: { id: founder.id }, data: { emailVerifiedAt: null } });
await assert.rejects(loader({ '@/auth': { auth: async () => session(founder) } })('src/app/onboarding/actions.ts').completeOnboardingAction({ attempt: 0 }, payload), /REDIRECT:\/register/);
await prisma.user.update({ where: { id: founder.id }, data: { emailVerifiedAt: new Date() } });
});
test('database blocks cross-tenant child, user, RSVP and announcement links', async () => {
await assert.rejects(prisma.child.create({ data: { firstName: 'Wrong', lastName: 'Tenant', kitaId: kitaA.id, familyId: familyB.id } }), error => error.code === 'P2003');
await assert.rejects(prisma.user.update({ where: { id: parentA.id }, data: { familyId: familyB.id } }), error => error.code === 'P2003');
await assert.rejects(prisma.announcement.create({ data: { kitaId: kitaA.id, authorId: parentB.id, title: 'Wrong', content: 'Wrong' } }), error => error.code === 'P2003');
await assert.rejects(prisma.invitation.create({ data: { kitaId: kitaA.id, userId: parentB.id, email: 'cross-' + prefix + '@example.invalid', token: randomUUID(), expiresAt: new Date(Date.now() + 60000), invitedById: adminA.id } }), error => error.code === 'P2003');
const event = await prisma.termin.create({ data: { kitaId: kitaA.id, title: 'Event', type: 'SONSTIGES', status: 'CONFIRMED', startDate: new Date(), endDate: new Date() } });
await assert.rejects(prisma.eventParticipation.create({ data: { kitaId: kitaA.id, eventId: event.id, childId: childB.id, status: 'ATTENDING' } }), error => error.code === 'P2003');
});
test('download route rejects foreign attachments and requires consent', async () => {
const { writeFile, mkdir } = require('node:fs/promises');
const files = loader()('src/lib/attachments.ts');
const id = randomUUID();
const announcement = await prisma.announcement.create({ data: { kitaId: kitaB.id, authorId: parentB.id, title: 'Private', content: 'Private', attachments: { create: { id, fileName: 'unsafe.html', fileType: 'text/html', fileUrl: '/api/' + id } } } });
await mkdir(files.attachmentDirectory(), { recursive: true });
await writeFile(files.attachmentPath(id), '');
try {
const load = user => loader({ '@/auth': { auth: async () => session(user) } })('src/app/api/announcements/attachments/[id]/route.ts').GET(new Request('http://localhost'), { params: Promise.resolve({ id }) });
assert.equal((await load(parentA)).status, 404);
const response = await load(parentB);
assert.equal(response.status, 200); assert.equal(response.headers.get('content-type'), 'application/octet-stream');
assert.match(response.headers.get('content-disposition'), /^attachment;/);
assert.equal(response.headers.get('x-content-type-options'), 'nosniff'); assert.equal(response.headers.get('cache-control'), 'private, no-store');
await prisma.user.update({ where: { id: parentB.id }, data: { privacyPolicyAcceptedAt: null } });
assert.equal((await load(parentB)).status, 401);
} finally {
await files.removeAttachmentFiles([id]);
await prisma.user.update({ where: { id: parentB.id }, data: { privacyPolicyAcceptedAt: new Date() } });
await prisma.announcement.delete({ where: { id: announcement.id } });
}
});
test('registration has no user before mail verification and token can be consumed once', async () => {
const email = `registration.${prefix}@example.invalid`;
const load = loader({ '@/auth': noAuth, '@/lib/mail': emailMock });
const registration = load('src/app/register/actions.ts');
const result = await registration.registerAction({ attempt: 0 }, fields({ email, firstName: 'Founder', lastName: 'Test', acceptPrivacyPolicy: 'on' }));
assert.ok(result.message); assert.equal(await prisma.user.findUnique({ where: { email } }), null);
const message = emails.at(-1); const link = message.react.props.children[1].props.href; const token = link.split('/').at(-1);
const draft = await prisma.pendingRegistration.findUnique({ where: { email } });
assert.notEqual(draft.tokenHash, token);
const verify = load('src/app/register/verify/[token]/actions.ts').verifyRegistrationAction;
const form = () => fields({ token, password: 'Owner-chosen-password', confirmPassword: 'Owner-chosen-password', acceptPrivacyPolicy: 'on' });
const results = await Promise.all([verify({}, form()), verify({}, form())]);
assert.equal(results.filter(result => !result.error).length, 1);
const user = await prisma.user.findUnique({ where: { email } }); assert.ok(user.emailVerifiedAt); assert.equal(user.kitaId, null);
assert.equal(await compare('Owner-chosen-password', user.passwordHash), true);
assert.ok((await verify({}, form())).error);
});
test('invite activation uses a hashed expiring token and rejects concurrent reuse', async () => {
const load = loader({ '@/auth': noAuth }); const { newToken, hashToken } = load('src/lib/tokens.ts');
const token = newToken(); const email = `invite.${prefix}@example.invalid`;
const user = await prisma.user.create({ data: { email, firstName: 'Invited', lastName: 'Test', kitaId: kitaA.id, familyId: familyA.id, passwordHash: '' } });
await prisma.verificationToken.create({ data: { token: hashToken(token), identifier: user.id, expires: new Date(Date.now() + 60_000) } });
const accept = load('src/app/invite/[token]/actions.ts').acceptInviteAction;
const form = () => fields({ token, password: 'Invite-test-password', confirmPassword: 'Invite-test-password', acceptPrivacyPolicy: 'on' });
const results = await Promise.all([accept({ attempt: 0 }, form()), accept({ attempt: 0 }, form())]);
assert.equal(results.filter(result => !result.errors).length, 1);
assert.equal(await prisma.verificationToken.findUnique({ where: { token: hashToken(token) } }), null);
});
test('household creation is atomic for parallel submissions', async () => {
const action = loader({ '@/auth': { auth: async () => session(adminA) } })('src/app/dashboard/profil/actions.ts').createMyFamily;
const before = await prisma.family.count({ where: { kitaId: kitaA.id } });
const results = await Promise.all([action({ familyName: 'Test household' }), action({ familyName: 'Test household' })]);
assert.equal(results.filter(result => result.success).length, 1);
assert.equal(await prisma.family.count({ where: { kitaId: kitaA.id } }), before + 1);
});
test('bulk email transport never exposes another recipient in To', async () => {
const sent = [];
const oldKey = process.env.RESEND_API_KEY; const oldFrom = process.env.EMAIL_FROM;
process.env.RESEND_API_KEY = 'test-only'; process.env.EMAIL_FROM = 'test@example.invalid';
try {
const mail = loader({ resend: { Resend: class { emails = { send: async input => { sent.push(input); return { data: { id: 'test' } }; } }; } } })('src/lib/mail.ts');
assert.equal((await mail.sendAppEmail({ to: ['a@example.invalid','b@example.invalid','a@example.invalid'], subject: 'Test', react: null })).success, true);
assert.deepEqual(sent.map(input => input.to), ['a@example.invalid','b@example.invalid']);
} finally {
if (oldKey === undefined) delete process.env.RESEND_API_KEY; else process.env.RESEND_API_KEY = oldKey;
if (oldFrom === undefined) delete process.env.EMAIL_FROM; else process.env.EMAIL_FROM = oldFrom;
}
});
test('read receipts cannot link users and announcements across tenants', async () => {
const announcement = await prisma.announcement.create({ data: { kitaId: kitaA.id, authorId: parentA.id, title: 'Tenant A', content: 'Private' } });
await assert.rejects(prisma.announcementRead.create({ data: { kitaId: kitaA.id, userId: parentB.id, announcementId: announcement.id } }), error => error.code === 'P2003');
await assert.rejects(prisma.user.update({ where: { id: parentA.id }, data: { kitaId: null } }), error => error.message.includes('users_family_requires_tenant'));
});
test('participant secrets are removed from props delivered to unauthorized browsers', async () => {
const secret = 'CONFIDENTIAL-PARTICIPANT-' + prefix;
const event = await prisma.termin.create({ data: { kitaId: kitaA.id, title: 'Private details', type: 'SONSTIGES', status: 'CONFIRMED', requiresRsvp: true,
participantInfo: secret, startDate: new Date(Date.now() + 86400000), endDate: new Date(Date.now() + 86400000 + 3600000) } });
const mockComponents = { './_components/admin-termin-modal': { AdminTerminModal: () => null },
'./_components/pending-anfragen': { PendingAnfragen: () => null },
'./_components/termin-request-modal': { TerminRequestModal: () => null },
'./_components/calendar-overview-client': { CalendarOverviewClient: () => null } };
const page = user => loader({ ...mockComponents, '@/auth': { auth: async () => session(user) } })('src/app/dashboard/kalender/page.tsx').default({ searchParams: Promise.resolve({}) });
assert.equal(JSON.stringify(await page(parentA)).includes(secret), false);
await prisma.eventParticipation.create({ data: { kitaId: kitaA.id, childId: childA.id, eventId: event.id, status: 'ATTENDING' } });
assert.equal(JSON.stringify(await page(parentA)).includes(secret), true);
assert.equal(JSON.stringify(await page(parentB)).includes(secret), false);
});
test('alarm links expire and concurrent confirmations consume the token only once', async () => {
const load = loader({ '@/lib/mail': emailMock }); const { newToken, hashToken } = load('src/lib/tokens.ts');
const plan = await prisma.notdienstPlan.create({ data: { kitaId: kitaA.id, year: 2029, month: 1, status: 'PUBLISHED' } });
const assignment = await prisma.notdienstAssignment.create({ data: { kitaId: kitaA.id, planId: plan.id, childId: childA.id, date: new Date(Date.now() + 86400000) } });
const token = newToken();
const alert = await prisma.notdienstAlarm.create({ data: { kitaId: kitaA.id, assignmentId: assignment.id, activeAssignmentId: assignment.id, familyId: familyA.id, familyName: 'A', childName: 'A Test', date: assignment.date, expiresAt: new Date(Date.now() - 1000), deadline: new Date(Date.now() + 3600000), recipients: { create: { userId: parentA.id, token: hashToken(token), sessionVersion: parentA.sessionVersion } } } });
const confirm = load('src/app/alert/[token]/actions.ts').confirmAlertAction;
assert.ok((await confirm(token)).error);
await prisma.notdienstAlarm.update({ where: { id: alert.id }, data: { expiresAt: new Date(Date.now() + 86400000) } });
const results = await Promise.all([confirm(token), confirm(token)]);
assert.equal(results.filter(result => result.success).length, 1);
assert.ok((await confirm(token)).error);
});
test('parallel swap creation and acceptance cannot duplicate or undo a swap', async () => {
const otherFamily = await prisma.family.create({ data: { kitaId: kitaA.id, name: 'Swap partner' } });
const partner = await prisma.user.create({ data: { email: `partner.${prefix}@example.invalid`, firstName: 'Partner', lastName: 'Test', kitaId: kitaA.id,
familyId: otherFamily.id, role: 'ELTERN', passwordHash: passHash, emailVerifiedAt: new Date(), privacyPolicyAcceptedAt: new Date() } });
const partnerChild = await prisma.child.create({ data: { kitaId: kitaA.id, familyId: otherFamily.id, firstName: 'Partner', lastName: 'Test' } });
const plan = await prisma.notdienstPlan.create({ data: { kitaId: kitaA.id, year: 2029, month: 2, status: 'PUBLISHED' } });
const offered = await prisma.notdienstAssignment.create({ data: { kitaId: kitaA.id, planId: plan.id, childId: childA.id, date: new Date(Date.now() + 2 * 86400000) } });
const target = await prisma.notdienstAssignment.create({ data: { kitaId: kitaA.id, planId: plan.id, childId: partnerChild.id, date: new Date(Date.now() + 3 * 86400000) } });
const actions = user => loader({ '@/lib/mail': emailMock, '@/auth': { auth: async () => session(user) } })('src/app/dashboard/kalender/actions.ts');
const requester = actions(parentA); const recipient = actions(partner);
const payload = { targetAssignmentId: target.id, offeredAssignmentId: offered.id };
const created = await Promise.all([requester.createNotdienstSwapRequest(payload), requester.createNotdienstSwapRequest(payload)]);
assert.equal(created.filter(result => result.success).length, 1);
const request = await prisma.notdienstSwapRequest.findFirst({ where: { kitaId: kitaA.id, targetAssignmentId: target.id } });
assert.ok((await actions(parentB).acceptNotdienstSwapRequest(request.id)).error);
const accepted = await Promise.all([recipient.acceptNotdienstSwapRequest(request.id), recipient.acceptNotdienstSwapRequest(request.id)]);
assert.equal(accepted.filter(result => result.success).length, 1);
assert.equal((await prisma.notdienstAssignment.findUnique({ where: { id: target.id } })).childId, childA.id);
assert.equal((await prisma.notdienstAssignment.findUnique({ where: { id: offered.id } })).childId, partnerChild.id);
assert.ok((await requester.cancelNotdienstSwapRequest(request.id)).error);
});
test('changing a parent email revokes sessions and requires fresh mailbox activation', async () => {
const newEmail = `changed.${prefix}@example.invalid`;
const load = loader({ '@/auth': { auth: async () => session(adminA), signIn: async () => {} }, '@/lib/mail': emailMock });
const actions = load('src/app/dashboard/families/actions.ts');
const oldToken = await authConfig.callbacks.jwt({ token: { id: parentA.id, sessionVersion: parentA.sessionVersion, authenticatedAt: Date.now() } });
assert.equal((await actions.updateFamilyAction({ familyId: familyA.id, familyName: 'Family A',
parents: [{ id: parentA.id, firstName: 'Parent', lastName: 'Test', email: newEmail }],
children: [{ id: childA.id, firstName: 'A', lastName: 'Test' }] })).success, true);
const changed = await prisma.user.findUnique({ where: { id: parentA.id } });
assert.equal(changed.emailVerifiedAt,null); assert.equal(changed.passwordHash,'');
assert.equal(await authConfig.callbacks.jwt({ token: oldToken }),null);
assert.equal(await authConfig.providers[0].authorize({ email: newEmail, password: 'Safe-test-password' }),null);
const initialInvite = emails.at(-1).react.props.inviteLink.split('/').at(-1);
await prisma.invitation.update({ where: { userId: parentA.id }, data: { sentAt: new Date(Date.now() - 61_000) } });
assert.equal((await actions.resendParentInvite(parentA.id)).success,true);
const renewedInvite = emails.at(-1).react.props.inviteLink.split('/').at(-1);
assert.notEqual(initialInvite,renewedInvite);
const accept = load('src/app/invite/[token]/actions.ts').acceptInviteAction;
const form = token => fields({ token, password: 'New-owner-password', confirmPassword: 'New-owner-password', acceptPrivacyPolicy: 'on' });
assert.ok((await accept({ attempt: 0 }, form(initialInvite))).errors);
assert.equal((await accept({ attempt: 0 }, form(renewedInvite))).errors,undefined);
assert.ok((await prisma.user.findUnique({ where: { id: parentA.id } })).emailVerifiedAt);
});
test('resending an invite cannot issue an active link to an obsolete mailbox', async () => {
const user = await prisma.user.create({ data: { email: `resend-race.${prefix}@example.invalid`, firstName: 'Race', lastName: 'Test',
role: 'ELTERN', kitaId: kitaA.id, familyId: familyA.id, passwordHash: '' } });
// Interleave a committed address change after the resend's initial lookup.
const guardedPrisma = new Proxy(prisma, { get(target, property) {
if (property === '$transaction') return async operation => {
await prisma.user.update({ where: { id: user.id }, data: { email: `new-resend-race.${prefix}@example.invalid` } });
return prisma.$transaction(operation);
};
const value = target[property]; return typeof value === 'function' ? value.bind(target) : value;
} });
const load = loader({ '@/auth': { auth: async () => session(adminA) }, '@/lib/mail': emailMock, '@/lib/prisma': { prisma: guardedPrisma } });
const before = emails.length;
assert.ok((await load('src/app/dashboard/families/actions.ts').resendParentInvite(user.id)).error);
assert.equal(emails.length,before);
assert.equal(await prisma.verificationToken.count({ where: { identifier: user.id } }),0);
});
test('upload quota holds under concurrent writes and accepted MIME comes from file bytes', async () => {
const bytes = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aXZQAAAAASUVORK5CYII=', 'base64');
const oldQuota = process.env.UPLOAD_TENANT_QUOTA_BYTES;
process.env.UPLOAD_TENANT_QUOTA_BYTES = String(bytes.length * 2);
const load = loader({ '@/auth': { auth: async () => session(adminA) }, '@/lib/mail': emailMock });
const create = load('src/actions/announcements.ts').createAnnouncement;
const form = () => { const result = fields({ title: 'Quota test', content: 'Safe contents', audience: 'ALLE' }); result.append('attachments',new File([bytes],'test.png',{ type: 'image/png' })); return result; };
try {
const results = await Promise.all([create(form()), create(form()), create(form())]);
assert.equal(results.filter(result => result.success).length,2);
const used = await prisma.attachment.aggregate({ where: { announcement: { kitaId: kitaA.id } }, _sum: { sizeBytes: true } });
assert.equal(used._sum.sizeBytes,bytes.length * 2);
const bad = fields({ title: 'Quota bad', content: 'Unsafe contents' });
bad.append('attachments',new File(['bad'],'bad.jpg',{ type: 'text/html' }));
assert.ok((await create(bad)).error);
} finally {
const files = await prisma.attachment.findMany({ where: { announcement: { kitaId: kitaA.id, title: 'Quota test' } }, select: { id: true } });
await load('src/lib/attachments.ts').removeAttachmentFiles(files.map(file => file.id));
await prisma.announcement.deleteMany({ where: { kitaId: kitaA.id, title: 'Quota test' } });
if (oldQuota === undefined) delete process.env.UPLOAD_TENANT_QUOTA_BYTES; else process.env.UPLOAD_TENANT_QUOTA_BYTES = oldQuota;
}
});
test('all supplemental database tenant constraints are present', async () => {
const [row] = await prisma.$queryRaw`SELECT count(*)::int AS count FROM pg_constraint WHERE conname LIKE '%_tenant_fkey'`;
assert.equal(row.count,42);
});
test('role guard authenticates once and keeps role validation on that session', async () => {
let calls = 0;
const util = loader({ '@/auth': { auth: async () => { calls++; return session(calls === 1 ? adminA : parentB); } } })('src/lib/auth-utils.ts');
assert.equal((await util.requireRole([UserRole.ADMIN])).user.id,adminA.id);
assert.equal(calls,1);
});
test('parallel alarm triggers send one set of messages and skip unverified recipients', async () => {
const unverified = await prisma.user.create({ data: { email: `unverified.${prefix}@example.invalid`, firstName: 'Unverified', lastName: 'Test', role: 'ELTERN',
kitaId: kitaA.id, familyId: familyA.id, passwordHash: '', emailVerifiedAt: null } });
const plan = await prisma.notdienstPlan.create({ data: { kitaId: kitaA.id, year: 2029, month: 3, status: 'PUBLISHED' } });
const assignment = await prisma.notdienstAssignment.create({ data: { kitaId: kitaA.id, planId: plan.id, childId: childA.id, date: new Date(Date.now() + 86400000) } });
const trigger = loader({ '@/auth': { auth: async () => session(adminA) }, '@/lib/mail': emailMock })('src/app/dashboard/actions.ts').triggerAlertAction;
const before = emails.length;
const results = await Promise.all([trigger(assignment.id),trigger(assignment.id)]);
assert.equal(results.filter(result => result.success).length,1);
const alerts = await prisma.notdienstAlarmRecipient.findMany({ where: { alarm: { assignmentId: assignment.id } } });
assert.ok(alerts.length > 0);
assert.equal(alerts.some(alert => alert.userId === unverified.id),false);
assert.equal(emails.slice(before).some(email => email.to === unverified.email),false);
assert.equal(emails.length - before,alerts.length);
});
test('new alarm links reject stored hashes and stay invalid after account unlock', async () => {
const load = loader(); const { newToken, hashToken } = load('src/lib/tokens.ts');
const { getPublicAlarm } = load('src/lib/alarms/service.ts');
const user = await prisma.user.create({ data: { email: `alarm-version.${prefix}@example.invalid`, firstName: 'Parent', lastName: 'Test', role: 'ELTERN', kitaId: kitaA.id, familyId: familyA.id, passwordHash: passHash, emailVerifiedAt: new Date(), privacyPolicyAcceptedAt: new Date() } });
const token = newToken();
await prisma.notdienstAlarm.create({ data: { kitaId: kitaA.id, familyId: familyA.id, familyName: 'A', childName: 'A', date: new Date(), expiresAt: new Date(Date.now() + 86400000), deadline: new Date(Date.now() + 3600000), recipients: { create: { userId: user.id, token: hashToken(token), sessionVersion: user.sessionVersion } } } });
assert.ok(await getPublicAlarm(token));
assert.equal(await getPublicAlarm(hashToken(token)), null);
await prisma.user.update({ where: { id: user.id }, data: { disabledAt: new Date(), sessionVersion: { increment: 1 } } });
assert.equal(await getPublicAlarm(token), null);
await prisma.user.update({ where: { id: user.id }, data: { disabledAt: null } });
assert.equal(await getPublicAlarm(token), null);
});
test('new staff and household event relationships reject foreign tenants in the database', async () => {
const educator = await prisma.educator.create({ data: { kitaId: kitaA.id, firstName: 'Staff', lastName: 'Test' } });
await assert.rejects(prisma.staffProfile.create({ data: { kitaId: kitaB.id, educatorId: educator.id, employmentStart: new Date(), trackingStart: new Date() } }));
const event = await prisma.termin.create({ data: { kitaId: kitaA.id, title: 'Tenant event', type: 'SONSTIGES', status: 'CONFIRMED', startDate: new Date(), endDate: new Date(Date.now() + 3600000), createdById: adminA.id } });
await assert.rejects(prisma.familyEventParticipation.create({ data: { kitaId: kitaA.id, eventId: event.id, familyId: familyB.id, status: 'ATTENDING' } }));
});