Merge main with notdienst planning and notification improvements

This commit is contained in:
t.indorf
2026-10-12 15:37:10 +02:00
30 changed files with 2848 additions and 644 deletions
+30 -1
View File
@@ -163,6 +163,7 @@ test('database blocks cross-tenant child, user, RSVP and announcement links', as
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');
});
@@ -304,6 +305,7 @@ test('changing a parent email revokes sessions and requires fresh mailbox activa
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);
@@ -313,6 +315,33 @@ test('changing a parent email revokes sessions and requires fresh mailbox activa
assert.equal((await accept({ attempt: 0 }, form(renewedInvite))).errors,undefined);
assert.ok((await prisma.user.findUnique({ where: { id: parentA.id } })).emailVerifiedAt);
});
test('family admins keep their role when a changed mailbox is invited and activated', async () => {
const family = await prisma.family.create({ data: { kitaId: kitaA.id, name: 'Admin family' } });
const child = await prisma.child.create({ data: { kitaId: kitaA.id, familyId: family.id, firstName: 'Admin', lastName: 'Child' } });
const user = await prisma.user.create({ data: { email: `family-admin.${prefix}@example.invalid`, firstName: 'Family', lastName: 'Admin',
role: 'ADMIN', kitaId: kitaA.id, familyId: family.id, passwordHash: passHash,
emailVerifiedAt: new Date(), privacyPolicyAcceptedAt: new Date() } });
try {
const load = loader({ '@/auth': { auth: async () => session(adminA), signIn: async () => {} }, '@/lib/mail': emailMock });
const actions = load('src/app/dashboard/families/actions.ts');
const updated = await actions.updateFamilyAction({ familyId: family.id, familyName: 'Family A',
parents: [{ id: user.id, firstName: 'Family', lastName: 'Admin', email: `changed-family-admin.${prefix}@example.invalid` }],
children: [{ id: child.id, firstName: 'A', lastName: 'Test' }] });
assert.equal(updated.success,true,updated.error); assert.equal(updated.warning,undefined);
assert.equal((await prisma.user.findUnique({ where: { id: user.id } })).role,'ADMIN');
await prisma.invitation.update({ where: { userId: user.id }, data: { sentAt: new Date(Date.now() - 61_000) } });
assert.equal((await actions.resendParentInvite(user.id)).success,true);
const token = emails.at(-1).react.props.inviteLink.split('/').at(-1);
const accepted = await load('src/app/invite/[token]/actions.ts').acceptInviteAction({ attempt: 0 }, fields({ token,
password: 'Family-admin-new-password', confirmPassword: 'Family-admin-new-password', acceptPrivacyPolicy: 'on' }));
assert.equal(accepted.errors,undefined);
const activated = await prisma.user.findUnique({ where: { id: user.id } });
assert.equal(activated.role,'ADMIN'); assert.ok(activated.emailVerifiedAt);
} finally {
await prisma.verificationToken.deleteMany({ where: { identifier: user.id } });
await prisma.family.delete({ where: { id: family.id } });
}
});
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: '' } });
@@ -355,7 +384,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,44);
assert.equal(row.count,45);
});
test('role guard authenticates once and keeps role validation on that session', async () => {
let calls = 0;