416 lines
13 KiB
TypeScript
416 lines
13 KiB
TypeScript
import { strict as assert } from "node:assert";
|
|
import { mock, test } from "node:test";
|
|
import { createRequire } from "node:module";
|
|
import { resolve } from "node:path";
|
|
import { randomUUID } from "node:crypto";
|
|
import {
|
|
AnnouncementAudience,
|
|
EventParticipationStatus as S,
|
|
PrismaClient,
|
|
RsvpMode,
|
|
TerminStatus,
|
|
TerminType,
|
|
UserRole,
|
|
} from "@prisma/client";
|
|
|
|
// No database work without an explicitly supplied local test database.
|
|
test(
|
|
"Elternabläufe gegen PostgreSQL",
|
|
{ skip: !process.env.TEST_DATABASE_URL },
|
|
async (t) => {
|
|
const url = new URL(process.env.TEST_DATABASE_URL!);
|
|
assert.ok(
|
|
["127.0.0.1", "localhost"].includes(url.hostname),
|
|
"Nur lokale Testdatenbanken sind erlaubt",
|
|
);
|
|
assert.ok(
|
|
/test|parent_flows/.test(url.pathname),
|
|
"Der Datenbankname muss ausdrücklich eine Testdatenbank kennzeichnen",
|
|
);
|
|
process.env.DATABASE_URL = url.toString();
|
|
const db = new PrismaClient();
|
|
const id = randomUUID();
|
|
const kitaId = `parent-test-${id}`;
|
|
const otherKitaId = `other-test-${id}`;
|
|
let user = {
|
|
id: `parent-a-${id}`,
|
|
kitaId,
|
|
familyId: `family-${id}`,
|
|
role: UserRole.ELTERN as UserRole,
|
|
};
|
|
const moduleMock = mock as typeof mock & {
|
|
module: (
|
|
specifier: string,
|
|
options: { namedExports: Record<string, unknown> },
|
|
) => unknown;
|
|
};
|
|
moduleMock.module("../src/lib/auth-utils.ts", {
|
|
namedExports: {
|
|
requireKitaSession: async () => ({ user }),
|
|
requireRole: async (roles: UserRole[]) => {
|
|
assert.ok(roles.includes(user.role));
|
|
return { user };
|
|
},
|
|
},
|
|
});
|
|
moduleMock.module("../src/auth.ts", {
|
|
namedExports: { auth: async () => ({ user }) },
|
|
});
|
|
moduleMock.module("next/cache", {
|
|
namedExports: { revalidatePath: () => {} },
|
|
});
|
|
const loadAfterMocks = createRequire(
|
|
resolve("tests/parent-flows.integration.test.ts"),
|
|
);
|
|
const { submitEventRsvp } = loadAfterMocks(
|
|
"../src/actions/rsvp",
|
|
) as typeof import("../src/actions/rsvp");
|
|
const { updateTerminAdmin } = loadAfterMocks(
|
|
"../src/app/dashboard/kalender/actions",
|
|
) as typeof import("../src/app/dashboard/kalender/actions");
|
|
const { createAnnouncement, markAnnouncementRead } = loadAfterMocks(
|
|
"../src/actions/announcements",
|
|
) as typeof import("../src/actions/announcements");
|
|
const { GET: getAttachment } = loadAfterMocks(
|
|
"../src/app/api/announcements/attachments/[id]/route",
|
|
) as typeof import("../src/app/api/announcements/attachments/[id]/route");
|
|
const familyId = user.familyId;
|
|
const startDate = new Date(Date.now() + 86400000 * 3);
|
|
const endDate = new Date(startDate.getTime() + 3600000);
|
|
const eventBase = {
|
|
kitaId,
|
|
title: "Testtermin",
|
|
type: TerminType.SONSTIGES,
|
|
status: TerminStatus.CONFIRMED,
|
|
startDate,
|
|
endDate,
|
|
requiresRsvp: true,
|
|
};
|
|
const updateBase = {
|
|
title: "Testtermin geändert",
|
|
type: TerminType.SONSTIGES,
|
|
startDate: startDate.toISOString(),
|
|
endDate: endDate.toISOString(),
|
|
};
|
|
let teacherAnnouncementId = "";
|
|
try {
|
|
await db.kita.create({
|
|
data: { id: kitaId, name: "Elternabläufe Test", slug: kitaId },
|
|
});
|
|
await db.kita.create({
|
|
data: { id: otherKitaId, name: "Andere Kita", slug: otherKitaId },
|
|
});
|
|
await db.family.create({
|
|
data: { id: familyId, kitaId, name: "Testfamilie" },
|
|
});
|
|
await db.family.create({
|
|
data: {
|
|
id: `other-family-${id}`,
|
|
kitaId: otherKitaId,
|
|
name: "Fremde Familie",
|
|
},
|
|
});
|
|
await db.user.create({
|
|
data: {
|
|
id: user.id,
|
|
kitaId,
|
|
familyId,
|
|
email: `${id}@example.test`,
|
|
passwordHash: "test",
|
|
firstName: "Test",
|
|
lastName: "Eltern",
|
|
},
|
|
});
|
|
const children = await Promise.all(
|
|
["a", "b"].map((suffix) =>
|
|
db.child.create({
|
|
data: {
|
|
id: `${suffix}-${id}`,
|
|
kitaId,
|
|
familyId,
|
|
firstName: suffix,
|
|
lastName: "Test",
|
|
},
|
|
}),
|
|
),
|
|
);
|
|
const foreign = await db.child.create({
|
|
data: {
|
|
kitaId: otherKitaId,
|
|
familyId: `other-family-${id}`,
|
|
firstName: "Fremd",
|
|
lastName: "Test",
|
|
},
|
|
});
|
|
const inactive = await db.child.create({
|
|
data: {
|
|
kitaId,
|
|
familyId,
|
|
firstName: "Inaktiv",
|
|
lastName: "Test",
|
|
active: false,
|
|
},
|
|
});
|
|
const familyEvent = await db.termin.create({
|
|
data: { ...eventBase, rsvpMode: RsvpMode.FAMILY },
|
|
});
|
|
const childEvent = await db.termin.create({
|
|
data: {
|
|
...eventBase,
|
|
rsvpMode: RsvpMode.CHILD,
|
|
mitbringselListEnabled: true,
|
|
participantInfo: "Nur Teilnehmende",
|
|
},
|
|
});
|
|
const familyPayload = {
|
|
eventId: familyEvent.id,
|
|
mode: RsvpMode.FAMILY,
|
|
status: S.ATTENDING,
|
|
adultCount: 2,
|
|
};
|
|
const childPayload = {
|
|
eventId: childEvent.id,
|
|
mode: RsvpMode.CHILD,
|
|
responses: [
|
|
{ childId: children[0].id, status: S.ATTENDING },
|
|
{ childId: children[1].id, status: S.NOT_ATTENDING },
|
|
],
|
|
};
|
|
|
|
await t.test(
|
|
"Zwei Elternkonten ändern eine gemeinsame Familienantwort",
|
|
async () => {
|
|
assert.equal((await submitEventRsvp(familyPayload)).success, true);
|
|
user = { ...user, id: `parent-b-${id}` };
|
|
assert.equal(
|
|
(await submitEventRsvp({ ...familyPayload, adultCount: 3 }))
|
|
.success,
|
|
true,
|
|
);
|
|
assert.equal(
|
|
await db.familyEventParticipation.count({
|
|
where: { eventId: familyEvent.id },
|
|
}),
|
|
1,
|
|
);
|
|
assert.equal(
|
|
(
|
|
await db.familyEventParticipation.findFirstOrThrow({
|
|
where: { eventId: familyEvent.id },
|
|
})
|
|
).adultCount,
|
|
3,
|
|
);
|
|
assert.ok(
|
|
(await submitEventRsvp({ ...familyPayload, adultCount: 0 })).error,
|
|
);
|
|
assert.equal(
|
|
(
|
|
await submitEventRsvp({
|
|
...familyPayload,
|
|
status: S.NOT_ATTENDING,
|
|
adultCount: 0,
|
|
})
|
|
).success,
|
|
true,
|
|
);
|
|
assert.equal(
|
|
(
|
|
await db.familyEventParticipation.findFirstOrThrow({
|
|
where: { eventId: familyEvent.id },
|
|
})
|
|
).adultCount,
|
|
0,
|
|
);
|
|
},
|
|
);
|
|
await t.test(
|
|
"Alle Kinder werden gemeinsam gespeichert, fremde/inaktive Kinder abgewiesen",
|
|
async () => {
|
|
assert.equal((await submitEventRsvp(childPayload)).success, true);
|
|
for (const childId of [foreign.id, inactive.id]) {
|
|
assert.ok(
|
|
(
|
|
await submitEventRsvp({
|
|
...childPayload,
|
|
responses: [
|
|
{ childId: children[0].id, status: S.NOT_ATTENDING },
|
|
{ childId, status: S.ATTENDING },
|
|
],
|
|
})
|
|
).error,
|
|
);
|
|
}
|
|
assert.ok(
|
|
(
|
|
await submitEventRsvp({
|
|
...childPayload,
|
|
responses: [childPayload.responses[0]],
|
|
})
|
|
).error,
|
|
);
|
|
const rows = await db.eventParticipation.findMany({
|
|
where: { eventId: childEvent.id },
|
|
orderBy: { childId: "asc" },
|
|
});
|
|
assert.equal(rows.length, 2);
|
|
assert.equal(rows[0].status, S.ATTENDING);
|
|
assert.equal(rows[1].status, S.NOT_ATTENDING);
|
|
},
|
|
);
|
|
await t.test(
|
|
"Falsche Antwortart und fremde Kita können nicht beschrieben werden",
|
|
async () => {
|
|
assert.ok(
|
|
(
|
|
await submitEventRsvp({
|
|
...familyPayload,
|
|
eventId: childEvent.id,
|
|
})
|
|
).error,
|
|
);
|
|
user = { ...user, kitaId: otherKitaId };
|
|
assert.ok((await submitEventRsvp(familyPayload)).error);
|
|
user = { ...user, kitaId };
|
|
},
|
|
);
|
|
await t.test(
|
|
"Frist, Absage und deaktiviertes Terminmodul verhindern Antworten",
|
|
async () => {
|
|
await db.termin.update({
|
|
where: { id: familyEvent.id },
|
|
data: { rsvpDeadline: new Date(Date.now() - 1000) },
|
|
});
|
|
assert.ok((await submitEventRsvp(familyPayload)).error);
|
|
await db.termin.update({
|
|
where: { id: familyEvent.id },
|
|
data: { rsvpDeadline: null, status: TerminStatus.CANCELLED },
|
|
});
|
|
assert.ok((await submitEventRsvp(familyPayload)).error);
|
|
await db.termin.update({
|
|
where: { id: familyEvent.id },
|
|
data: { status: TerminStatus.CONFIRMED },
|
|
});
|
|
await db.kita.update({
|
|
where: { id: kitaId },
|
|
data: { terminModuleEnabled: false },
|
|
});
|
|
assert.ok((await submitEventRsvp(familyPayload)).error);
|
|
await db.kita.update({
|
|
where: { id: kitaId },
|
|
data: { terminModuleEnabled: true },
|
|
});
|
|
},
|
|
);
|
|
await t.test(
|
|
"Normale Terminänderung erhält RSVP-Felder; Antwortart ist nach Antwort gesperrt",
|
|
async () => {
|
|
user = { ...user, role: UserRole.ADMIN };
|
|
assert.equal(
|
|
(await updateTerminAdmin(childEvent.id, updateBase)).success,
|
|
true,
|
|
);
|
|
const event = await db.termin.findUniqueOrThrow({
|
|
where: { id: childEvent.id },
|
|
});
|
|
assert.equal(event.requiresRsvp, true);
|
|
assert.equal(event.mitbringselListEnabled, true);
|
|
assert.equal(event.participantInfo, "Nur Teilnehmende");
|
|
assert.ok(
|
|
(
|
|
await updateTerminAdmin(childEvent.id, {
|
|
...updateBase,
|
|
rsvpMode: RsvpMode.FAMILY,
|
|
})
|
|
).error,
|
|
);
|
|
},
|
|
);
|
|
await t.test(
|
|
"Gleichzeitige erste Antwort und Modusänderung bleiben widerspruchsfrei",
|
|
async () => {
|
|
const event = await db.termin.create({
|
|
data: { ...eventBase, rsvpMode: RsvpMode.FAMILY },
|
|
});
|
|
await Promise.all([
|
|
submitEventRsvp({ ...familyPayload, eventId: event.id }),
|
|
updateTerminAdmin(event.id, {
|
|
...updateBase,
|
|
rsvpMode: RsvpMode.CHILD,
|
|
}),
|
|
]);
|
|
const stored = await db.termin.findUniqueOrThrow({
|
|
where: { id: event.id },
|
|
include: { familyParticipations: true },
|
|
});
|
|
assert.ok(
|
|
stored.rsvpMode === RsvpMode.FAMILY
|
|
? stored.familyParticipations.length === 1
|
|
: stored.familyParticipations.length === 0,
|
|
);
|
|
},
|
|
);
|
|
await t.test(
|
|
"Empfängergruppe wird auch ohne Mail gespeichert",
|
|
async () => {
|
|
user = { ...user, id: `parent-a-${id}`, role: UserRole.ADMIN };
|
|
const form = new FormData();
|
|
form.set("title", "Nur ErzieherInnen");
|
|
form.set("content", "Interne Teammitteilung");
|
|
form.set("audience", "ERZIEHER");
|
|
assert.equal((await createAnnouncement(form)).success, true);
|
|
const announcement = await db.announcement.findFirstOrThrow({
|
|
where: { kitaId, title: "Nur ErzieherInnen" },
|
|
});
|
|
assert.equal(announcement.audience, AnnouncementAudience.ERZIEHER);
|
|
teacherAnnouncementId = announcement.id;
|
|
await db.attachment.create({
|
|
data: {
|
|
id: `teacher-attachment-${id}`,
|
|
announcementId: announcement.id,
|
|
fileName: "team.pdf",
|
|
fileUrl: `/api/announcements/attachments/teacher-attachment-${id}`,
|
|
fileType: "application/pdf",
|
|
},
|
|
});
|
|
},
|
|
);
|
|
await t.test(
|
|
"Eltern können Teammitteilungen weder als gelesen markieren noch Anhänge abrufen",
|
|
async () => {
|
|
user = { ...user, role: UserRole.ELTERN };
|
|
assert.ok((await markAnnouncementRead(teacherAnnouncementId)).error);
|
|
const response = await getAttachment(
|
|
new Request("http://localhost/test"),
|
|
{ params: Promise.resolve({ id: `teacher-attachment-${id}` }) },
|
|
);
|
|
assert.equal(response.status, 404);
|
|
assert.equal(
|
|
await db.announcementRead.count({
|
|
where: { announcementId: teacherAnnouncementId },
|
|
}),
|
|
0,
|
|
);
|
|
user = { ...user, role: UserRole.ERZIEHER };
|
|
assert.equal(
|
|
(await markAnnouncementRead(teacherAnnouncementId)).success,
|
|
true,
|
|
);
|
|
user = { ...user, kitaId: otherKitaId };
|
|
assert.ok((await markAnnouncementRead(teacherAnnouncementId)).error);
|
|
},
|
|
);
|
|
} finally {
|
|
await db.kita.deleteMany({
|
|
where: { id: { in: [kitaId, otherKitaId] } },
|
|
});
|
|
await db.$disconnect();
|
|
const { prisma } = loadAfterMocks(
|
|
"../src/lib/prisma",
|
|
) as typeof import("../src/lib/prisma");
|
|
await prisma.$disconnect();
|
|
mock.restoreAll();
|
|
}
|
|
},
|
|
);
|