feat: add parent announcements and family or child event responses
This commit is contained in:
@@ -0,0 +1,415 @@
|
||||
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();
|
||||
}
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user