Merge current features with security hardening and automatic safe initialization
This commit is contained in:
+29
-6
@@ -23,6 +23,7 @@ function loader(overrides = {}) {
|
||||
}).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() {} };
|
||||
@@ -38,6 +39,7 @@ function loader(overrides = {}) {
|
||||
}
|
||||
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 = [];
|
||||
@@ -255,14 +257,14 @@ test('participant secrets are removed from props delivered to unauthorized brows
|
||||
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(); const { newToken, hashToken } = load('src/lib/tokens.ts');
|
||||
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.notdienstAlert.create({ data: { kitaId: kitaA.id, assignmentId: assignment.id, parentUserId: parentA.id, confirmationToken: hashToken(token), triggeredAt: new Date(Date.now() - 48 * 3600000) } });
|
||||
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.notdienstAlert.update({ where: { id: alert.id }, data: { triggeredAt: new Date() } });
|
||||
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);
|
||||
@@ -353,7 +355,7 @@ test('upload quota holds under concurrent writes and accepted MIME comes from fi
|
||||
|
||||
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,31);
|
||||
assert.equal(row.count,41);
|
||||
});
|
||||
test('role guard authenticates once and keeps role validation on that session', async () => {
|
||||
let calls = 0;
|
||||
@@ -370,9 +372,30 @@ test('parallel alarm triggers send one set of messages and skip unverified recip
|
||||
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.notdienstAlert.findMany({ where: { assignmentId: assignment.id } });
|
||||
const alerts = await prisma.notdienstAlarmRecipient.findMany({ where: { alarm: { assignmentId: assignment.id } } });
|
||||
assert.ok(alerts.length > 0);
|
||||
assert.equal(alerts.some(alert => alert.parentUserId === unverified.id),false);
|
||||
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' } }));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user