787 lines
26 KiB
TypeScript
787 lines
26 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { mock, test } from "node:test";
|
|
import { createRequire } from "node:module";
|
|
import { resolve } from "node:path";
|
|
import { randomUUID } from "node:crypto";
|
|
import { readFile } from "node:fs/promises";
|
|
import { PrismaClient, UserRole } from "@prisma/client";
|
|
import { hashToken } from "../src/lib/tokens";
|
|
import { absenceDate, absenceToday } from "../src/lib/absence-policy";
|
|
|
|
test(
|
|
"Familieneinladungen und Abwesenheiten gegen isoliertes 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));
|
|
assert.ok(
|
|
/test/.test(url.pathname),
|
|
"Nur ausdrücklich bezeichnete Testdatenbanken",
|
|
);
|
|
process.env.DATABASE_URL = url.toString();
|
|
process.env.NEXT_PUBLIC_SITE_URL = "http://127.0.0.1:3010";
|
|
const db = new PrismaClient();
|
|
const suffix = randomUUID();
|
|
const kitaId = `flow-test-${suffix}`,
|
|
otherKitaId = `other-flow-${suffix}`;
|
|
let actor: {
|
|
id: string;
|
|
kitaId: string;
|
|
familyId: string | null;
|
|
role: UserRole;
|
|
};
|
|
const deliveries: { to: string; key: string | undefined }[] = [];
|
|
const fail = new Set<string>();
|
|
let throwTransport = false;
|
|
const refreshed: string[] = [];
|
|
const moduleMock = mock as typeof mock & {
|
|
module: (
|
|
name: string,
|
|
options: { namedExports: Record<string, unknown> },
|
|
) => unknown;
|
|
};
|
|
moduleMock.module("../src/lib/auth-utils.ts", {
|
|
namedExports: {
|
|
requireKitaSession: async () => ({ user: actor }),
|
|
requireRole: async (roles: UserRole[]) => {
|
|
assert.ok(roles.includes(actor.role), "Rolle muss erlaubt sein");
|
|
return { user: actor };
|
|
},
|
|
},
|
|
});
|
|
moduleMock.module("next/cache", {
|
|
namedExports: { revalidatePath: (path: string) => refreshed.push(path) },
|
|
});
|
|
moduleMock.module("../src/auth.ts", {
|
|
namedExports: { signIn: async () => {} },
|
|
});
|
|
moduleMock.module("../src/lib/mail.ts", {
|
|
namedExports: {
|
|
sendAppEmail: async (input: {
|
|
to: string;
|
|
idempotencyKey?: string;
|
|
}) => {
|
|
deliveries.push({ to: input.to, key: input.idempotencyKey });
|
|
if (throwTransport) throw new Error("Simulierter Transportfehler");
|
|
return fail.has(input.to)
|
|
? { success: false, error: "Simulierter Mailfehler" }
|
|
: { success: true, id: "test-only" };
|
|
},
|
|
getAppEmailConfigError: () => "Test ohne echte Mail-Konfiguration",
|
|
},
|
|
});
|
|
const load = createRequire(
|
|
resolve("tests/family-absence-flows.integration.test.ts"),
|
|
);
|
|
const familyActions = load(
|
|
"../src/app/dashboard/families/actions",
|
|
) as typeof import("../src/app/dashboard/families/actions");
|
|
const absences = load(
|
|
"../src/actions/absences",
|
|
) as typeof import("../src/actions/absences");
|
|
const { acceptInviteAction } = load(
|
|
"../src/app/invite/[token]/actions",
|
|
) as typeof import("../src/app/invite/[token]/actions");
|
|
const { prisma } = load(
|
|
"../src/lib/prisma",
|
|
) as typeof import("../src/lib/prisma");
|
|
const today = absenceToday();
|
|
const day = (offset: number) =>
|
|
new Date(absenceDate(today).getTime() + offset * 86_400_000)
|
|
.toISOString()
|
|
.slice(0, 10);
|
|
const actorOf = (user: {
|
|
id: string;
|
|
kitaId: string | null;
|
|
familyId: string | null;
|
|
role: UserRole;
|
|
}) => ({ ...user, kitaId: user.kitaId! });
|
|
const createUser = (
|
|
role: UserRole,
|
|
tenant = kitaId,
|
|
familyId: string | null = null,
|
|
) =>
|
|
db.user.create({
|
|
data: {
|
|
kitaId: tenant,
|
|
familyId,
|
|
role,
|
|
email: `${randomUUID()}@example.test`,
|
|
passwordHash: "",
|
|
firstName: "Test",
|
|
lastName: role,
|
|
privacyPolicyAcceptedAt: new Date(),
|
|
},
|
|
});
|
|
const form = (name: string, emails: string[]) => {
|
|
const data = new FormData();
|
|
for (const [key, value] of Object.entries({
|
|
familyName: name,
|
|
parent1FirstName: "Eva",
|
|
parent1LastName: "Test",
|
|
parent1Email: emails[0],
|
|
childCount: "1",
|
|
childFirstName_0: "Kind",
|
|
childLastName_0: "Test",
|
|
}))
|
|
data.set(key, value);
|
|
if (emails[1])
|
|
for (const [key, value] of Object.entries({
|
|
parent2FirstName: "Jan",
|
|
parent2LastName: "Test",
|
|
parent2Email: emails[1],
|
|
}))
|
|
data.set(key, value);
|
|
return data;
|
|
};
|
|
const createAbsence = (
|
|
childId: string,
|
|
start: number,
|
|
end = start,
|
|
tenant = kitaId,
|
|
) =>
|
|
db.absence.create({
|
|
data: {
|
|
kitaId: tenant,
|
|
childId,
|
|
startDate: absenceDate(day(start)),
|
|
endDate: absenceDate(day(end)),
|
|
reason: "ILLNESS",
|
|
},
|
|
});
|
|
const identity = (row: { id: string; updatedAt: Date }) => ({
|
|
id: row.id,
|
|
updatedAt: row.updatedAt.toISOString(),
|
|
});
|
|
try {
|
|
await db.kita.createMany({
|
|
data: [
|
|
{ id: kitaId, name: "Ablauftest", slug: kitaId },
|
|
{ id: otherKitaId, name: "Andere Kita", slug: otherKitaId },
|
|
],
|
|
});
|
|
const admin = await createUser("ADMIN");
|
|
actor = actorOf(admin);
|
|
const emails = [
|
|
`failure-${suffix}@example.test`,
|
|
`success-${suffix}@example.test`,
|
|
];
|
|
fail.add(emails[0]);
|
|
let invitedUsers: Awaited<ReturnType<typeof db.user.findMany>> = [];
|
|
|
|
await t.test(
|
|
"Mailfehler lässt Familie erfolgreich gespeichert; alle Empfänger werden versucht",
|
|
async () => {
|
|
const result = await familyActions.addFamilyAction(
|
|
{},
|
|
form("Eingeladen", emails),
|
|
);
|
|
assert.equal(result.success, true);
|
|
assert.ok(result.warning);
|
|
assert.equal(result.errors, undefined);
|
|
assert.equal(
|
|
await db.family.count({ where: { kitaId, name: "Eingeladen" } }),
|
|
1,
|
|
);
|
|
assert.ok(refreshed.includes("/dashboard/families"));
|
|
invitedUsers = await db.user.findMany({
|
|
where: { email: { in: emails } },
|
|
});
|
|
assert.equal(invitedUsers.length, 2);
|
|
const jobs = await db.invitation.findMany({
|
|
where: { userId: { in: invitedUsers.map((user) => user.id) } },
|
|
});
|
|
assert.equal(jobs.length, 2);
|
|
assert.equal(
|
|
jobs.find((job) => job.email === emails[0])!.failed,
|
|
true,
|
|
);
|
|
assert.ok(jobs.find((job) => job.email === emails[1])!.sentAt);
|
|
assert.equal(deliveries.length, 2);
|
|
const repeated = await familyActions.addFamilyAction(
|
|
{},
|
|
form("Eingeladen", emails),
|
|
);
|
|
assert.ok(repeated.errors);
|
|
assert.equal(
|
|
await db.family.count({ where: { kitaId, name: "Eingeladen" } }),
|
|
1,
|
|
);
|
|
},
|
|
);
|
|
|
|
await t.test(
|
|
"Gezielte Wiederholung behält den gültigen fehlgeschlagenen Link und erstellt keine Person erneut",
|
|
async () => {
|
|
const user = invitedUsers.find((user) => user.email === emails[0])!;
|
|
const before = await db.invitation.findUniqueOrThrow({
|
|
where: { userId: user.id },
|
|
});
|
|
fail.clear();
|
|
const result = await familyActions.resendFamilyInvitationAction(
|
|
user.id,
|
|
);
|
|
assert.equal(result.success, true);
|
|
assert.equal(result.warning, undefined);
|
|
const after = await db.invitation.findUniqueOrThrow({
|
|
where: { userId: user.id },
|
|
});
|
|
assert.equal(after.token, before.token);
|
|
assert.equal(after.attempts, 2);
|
|
assert.equal(after.failed, false);
|
|
assert.ok(after.sentAt);
|
|
assert.equal(deliveries.length, 3);
|
|
assert.equal(deliveries[2].key, deliveries[0].key);
|
|
assert.equal(
|
|
await db.user.count({ where: { email: user.email } }),
|
|
1,
|
|
);
|
|
const again = await familyActions.resendFamilyInvitationAction(
|
|
user.id,
|
|
);
|
|
assert.ok(again.error);
|
|
assert.equal(deliveries.length, 3);
|
|
},
|
|
);
|
|
|
|
await t.test(
|
|
"Abgelaufener Link wird erneuert und alter Token ungültig",
|
|
async () => {
|
|
const user = invitedUsers[0];
|
|
const before = await db.invitation.update({
|
|
where: { userId: user.id },
|
|
data: {
|
|
expiresAt: absenceDate(day(-1)),
|
|
sentAt: absenceDate(day(-2)),
|
|
},
|
|
});
|
|
await db.verificationToken.update({
|
|
where: { token: hashToken(before.token) },
|
|
data: { expires: absenceDate(day(-1)) },
|
|
});
|
|
assert.equal(
|
|
(await familyActions.resendFamilyInvitationAction(user.id)).success,
|
|
true,
|
|
);
|
|
const after = await db.invitation.findUniqueOrThrow({
|
|
where: { userId: user.id },
|
|
});
|
|
assert.notEqual(after.token, before.token);
|
|
assert.ok(after.expiresAt > new Date());
|
|
assert.equal(
|
|
await db.verificationToken.findUnique({
|
|
where: { token: hashToken(before.token) },
|
|
}),
|
|
null,
|
|
);
|
|
},
|
|
);
|
|
|
|
await t.test(
|
|
"Aktivierte und kita-fremde Konten werden nicht angeschrieben",
|
|
async () => {
|
|
const user = invitedUsers[0];
|
|
await db.user.update({
|
|
where: { id: user.id },
|
|
data: { emailVerifiedAt: new Date() },
|
|
});
|
|
const before = deliveries.length;
|
|
assert.ok(
|
|
(await familyActions.resendFamilyInvitationAction(user.id)).error,
|
|
);
|
|
const foreignFamily = await db.family.create({
|
|
data: { kitaId: otherKitaId, name: "Fremd" },
|
|
});
|
|
const foreign = await createUser(
|
|
"ELTERN",
|
|
otherKitaId,
|
|
foreignFamily.id,
|
|
);
|
|
assert.ok(
|
|
(await familyActions.resendFamilyInvitationAction(foreign.id))
|
|
.error,
|
|
);
|
|
assert.equal(deliveries.length, before);
|
|
},
|
|
);
|
|
|
|
await t.test(
|
|
"Hinzufügen eines Elternteils bei Mailfehler ist abgeschlossen und wiederholbar",
|
|
async () => {
|
|
const first = `${randomUUID()}@example.test`;
|
|
assert.equal(
|
|
(await familyActions.addFamilyAction({}, form("Ergänzen", [first])))
|
|
.success,
|
|
true,
|
|
);
|
|
const family = await db.family.findFirstOrThrow({
|
|
where: { kitaId, name: "Ergänzen" },
|
|
include: { users: true, children: true },
|
|
});
|
|
const second = `${randomUUID()}@example.test`;
|
|
fail.add(second);
|
|
const result = await familyActions.updateFamilyAction({
|
|
familyId: family.id,
|
|
familyName: family.name,
|
|
parents: family.users,
|
|
children: family.children,
|
|
newParents: [{ firstName: "Neu", lastName: "Test", email: second }],
|
|
});
|
|
assert.equal(result.success, true);
|
|
assert.ok(result.warning);
|
|
assert.equal(result.error, undefined);
|
|
assert.equal(
|
|
await db.user.count({ where: { familyId: family.id } }),
|
|
2,
|
|
);
|
|
fail.clear();
|
|
const parent = await db.user.findUniqueOrThrow({
|
|
where: { email: second },
|
|
});
|
|
assert.equal(
|
|
(await familyActions.resendFamilyInvitationAction(parent.id))
|
|
.success,
|
|
true,
|
|
);
|
|
},
|
|
);
|
|
|
|
await t.test(
|
|
"Adresskorrektur eines offenen Kontos erneuert die Einladung für die neue Adresse",
|
|
async () => {
|
|
const family = await db.family.findFirstOrThrow({
|
|
where: { kitaId, name: "Eingeladen" },
|
|
include: { users: true, children: true },
|
|
});
|
|
const pending = family.users.find((user) => !user.emailVerifiedAt)!;
|
|
const old = await db.invitation.findUniqueOrThrow({
|
|
where: { userId: pending.id },
|
|
});
|
|
const email = `${randomUUID()}@example.test`;
|
|
const result = await familyActions.updateFamilyAction({
|
|
familyId: family.id,
|
|
familyName: family.name,
|
|
parents: family.users.map((user) => ({
|
|
...user,
|
|
email: user.id === pending.id ? email : user.email,
|
|
})),
|
|
children: family.children,
|
|
});
|
|
assert.equal(result.success, true);
|
|
const renewed = await db.invitation.findUniqueOrThrow({
|
|
where: { userId: pending.id },
|
|
});
|
|
assert.equal(renewed.email, email);
|
|
assert.notEqual(renewed.token, old.token);
|
|
assert.equal(
|
|
await db.verificationToken.findUnique({
|
|
where: { token: hashToken(old.token) },
|
|
}),
|
|
null,
|
|
);
|
|
assert.equal(deliveries.at(-1)!.to, email);
|
|
},
|
|
);
|
|
|
|
await t.test(
|
|
"Transportausnahme wird gespeichert statt den Familienvorgang zurückzuwerfen",
|
|
async () => {
|
|
throwTransport = true;
|
|
const result = await familyActions.addFamilyAction(
|
|
{},
|
|
form("Transportfehler", [`${randomUUID()}@example.test`]),
|
|
);
|
|
assert.equal(result.success, true);
|
|
assert.ok(result.warning);
|
|
throwTransport = false;
|
|
},
|
|
);
|
|
|
|
await t.test(
|
|
"Alte Einladungen werden übernommen; paralleles erneutes Senden verschickt nur einmal und Aktivierung schließt den Status ab",
|
|
async () => {
|
|
const household = await db.family.create({
|
|
data: { kitaId, name: "Alte Einladung" },
|
|
});
|
|
const user = await createUser("ELTERN", kitaId, household.id);
|
|
const oldToken = randomUUID();
|
|
await db.verificationToken.create({
|
|
data: {
|
|
identifier: user.id,
|
|
token: hashToken(oldToken),
|
|
expires: absenceDate(day(-1)),
|
|
},
|
|
});
|
|
await db.invitation.create({
|
|
data: {
|
|
kitaId,
|
|
email: user.email,
|
|
token: oldToken,
|
|
expiresAt: absenceDate(day(-1)),
|
|
},
|
|
});
|
|
const before = deliveries.length;
|
|
const results = await Promise.all([
|
|
familyActions.resendFamilyInvitationAction(user.id),
|
|
familyActions.resendFamilyInvitationAction(user.id),
|
|
]);
|
|
assert.ok(results.some((result) => result.success));
|
|
assert.equal(deliveries.length, before + 1);
|
|
assert.equal(
|
|
await db.invitation.count({ where: { userId: user.id } }),
|
|
1,
|
|
);
|
|
const job = await db.invitation.findUniqueOrThrow({
|
|
where: { userId: user.id },
|
|
});
|
|
assert.equal(
|
|
await db.verificationToken.findUnique({
|
|
where: { token: hashToken(oldToken) },
|
|
}),
|
|
null,
|
|
);
|
|
const activation = new FormData();
|
|
activation.set("token", job.token);
|
|
activation.set("password", "Test-only-password-2026");
|
|
activation.set("confirmPassword", "Test-only-password-2026");
|
|
activation.set("acceptPrivacyPolicy", "on");
|
|
await acceptInviteAction({ attempt: 0 }, activation);
|
|
assert.ok(
|
|
(await db.user.findUniqueOrThrow({ where: { id: user.id } }))
|
|
.emailVerifiedAt,
|
|
);
|
|
assert.equal(
|
|
(
|
|
await db.invitation.findUniqueOrThrow({
|
|
where: { userId: user.id },
|
|
})
|
|
).status,
|
|
"ACCEPTED",
|
|
);
|
|
assert.equal(
|
|
await db.verificationToken.findUnique({
|
|
where: { token: hashToken(job.token) },
|
|
}),
|
|
null,
|
|
);
|
|
},
|
|
);
|
|
|
|
const ownFamily = await db.family.create({
|
|
data: { kitaId, name: "Eigener Haushalt" },
|
|
});
|
|
const otherFamily = await db.family.create({
|
|
data: { kitaId, name: "Anderer Haushalt" },
|
|
});
|
|
const parent = await createUser("ELTERN", kitaId, ownFamily.id);
|
|
const child = await db.child.create({
|
|
data: {
|
|
kitaId,
|
|
familyId: ownFamily.id,
|
|
firstName: "Eigen",
|
|
lastName: "Kind",
|
|
},
|
|
});
|
|
const otherChild = await db.child.create({
|
|
data: {
|
|
kitaId,
|
|
familyId: otherFamily.id,
|
|
firstName: "Fremd",
|
|
lastName: "Kind",
|
|
},
|
|
});
|
|
actor = actorOf(parent);
|
|
let future = await createAbsence(child.id, 3, 4);
|
|
|
|
await t.test(
|
|
"Eltern bearbeiten zukünftige Meldungen und die eigene Tagesgrenze wird eingehalten",
|
|
async () => {
|
|
const result = await absences.updateAbsence({
|
|
...identity(future),
|
|
startDate: day(5),
|
|
endDate: day(6),
|
|
reason: "VACATION",
|
|
note: "Korrigiert",
|
|
});
|
|
assert.equal(result.success, true);
|
|
future = await db.absence.findUniqueOrThrow({
|
|
where: { id: future.id },
|
|
});
|
|
assert.equal(future.startDate.toISOString().slice(0, 10), day(5));
|
|
assert.equal(future.note, "Korrigiert");
|
|
assert.ok(refreshed.includes("/dashboard/abwesenheiten"));
|
|
assert.ok(
|
|
(
|
|
await absences.updateAbsence({
|
|
...identity(future),
|
|
startDate: today,
|
|
endDate: day(6),
|
|
reason: "VACATION",
|
|
})
|
|
).error,
|
|
);
|
|
},
|
|
);
|
|
|
|
await t.test(
|
|
"Begonnene Mehrtagesmeldungen bleiben für Eltern vollständig unveränderbar",
|
|
async () => {
|
|
const running = await createAbsence(child.id, -1, 1);
|
|
assert.ok(
|
|
(
|
|
await absences.updateAbsence({
|
|
...identity(running),
|
|
startDate: day(1),
|
|
endDate: day(2),
|
|
reason: "OTHER",
|
|
})
|
|
).error,
|
|
);
|
|
assert.ok((await absences.deleteAbsence(identity(running))).error);
|
|
assert.equal(
|
|
await db.absence.count({ where: { id: running.id } }),
|
|
1,
|
|
);
|
|
},
|
|
);
|
|
|
|
await t.test(
|
|
"Andere Haushalte und Kitas bleiben bei Änderungen und Rücknahmen geschützt",
|
|
async () => {
|
|
const other = await createAbsence(otherChild.id, 10);
|
|
assert.ok((await absences.deleteAbsence(identity(other))).error);
|
|
assert.ok(
|
|
(
|
|
await absences.updateAbsence({
|
|
...identity(other),
|
|
startDate: day(11),
|
|
endDate: day(11),
|
|
reason: "OTHER",
|
|
})
|
|
).error,
|
|
);
|
|
actor = { ...actor, kitaId: otherKitaId };
|
|
assert.ok((await absences.deleteAbsence(identity(future))).error);
|
|
actor = actorOf(parent);
|
|
await assert.rejects(() =>
|
|
absences.getDailyAbsences({ date: today }),
|
|
);
|
|
},
|
|
);
|
|
|
|
await t.test(
|
|
"Ungültige Daten, Überschneidungen und veraltete Bearbeitungsstände werden abgewiesen",
|
|
async () => {
|
|
await createAbsence(child.id, 8, 9);
|
|
assert.ok(
|
|
(
|
|
await absences.updateAbsence({
|
|
...identity(future),
|
|
startDate: day(8),
|
|
endDate: day(8),
|
|
reason: "OTHER",
|
|
})
|
|
).error,
|
|
);
|
|
assert.ok(
|
|
(
|
|
await absences.updateAbsence({
|
|
...identity(future),
|
|
startDate: "2026-02-30",
|
|
endDate: day(8),
|
|
reason: "OTHER",
|
|
})
|
|
).error,
|
|
);
|
|
assert.ok(
|
|
(
|
|
await absences.updateAbsence({
|
|
...identity(future),
|
|
startDate: day(7),
|
|
endDate: day(6),
|
|
reason: "OTHER",
|
|
})
|
|
).error,
|
|
);
|
|
assert.ok(
|
|
(
|
|
await absences.deleteAbsence({
|
|
id: future.id,
|
|
updatedAt: "2000-01-01T00:00:00.000Z",
|
|
})
|
|
).error,
|
|
);
|
|
assert.equal(await db.absence.count({ where: { id: future.id } }), 1);
|
|
},
|
|
);
|
|
|
|
await t.test(
|
|
"Gleichzeitige Abmeldungen erzeugen höchstens eine überlappende Meldung",
|
|
async () => {
|
|
const payload = {
|
|
childId: child.id,
|
|
startDate: day(20),
|
|
endDate: day(22),
|
|
reason: "ILLNESS",
|
|
};
|
|
const results = await Promise.all([
|
|
absences.reportAbsence(payload),
|
|
absences.reportAbsence(payload),
|
|
]);
|
|
assert.equal(results.filter((result) => result.success).length, 1);
|
|
assert.equal(results.filter((result) => result.error).length, 1);
|
|
assert.equal(
|
|
await db.absence.count({
|
|
where: { childId: child.id, startDate: absenceDate(day(20)) },
|
|
}),
|
|
1,
|
|
);
|
|
assert.ok(
|
|
(
|
|
await absences.reportAbsence({
|
|
...payload,
|
|
startDate: day(-2),
|
|
endDate: day(-2),
|
|
})
|
|
).error,
|
|
);
|
|
assert.ok(
|
|
(
|
|
await absences.reportAbsence({
|
|
...payload,
|
|
childId: otherChild.id,
|
|
})
|
|
).error,
|
|
);
|
|
},
|
|
);
|
|
|
|
await t.test(
|
|
"Kita-Team korrigiert laufende Meldungen ohne eigenen Haushalt; Eltern ziehen zukünftige zurück",
|
|
async () => {
|
|
const running = await db.absence.findFirstOrThrow({
|
|
where: { childId: child.id, startDate: absenceDate(day(-1)) },
|
|
});
|
|
for (const role of [
|
|
"ADMIN",
|
|
"KOORDINATOR",
|
|
"ERZIEHER",
|
|
] as UserRole[]) {
|
|
const team = role === "ADMIN" ? admin : await createUser(role);
|
|
actor = actorOf(team);
|
|
const latest = await db.absence.findUniqueOrThrow({
|
|
where: { id: running.id },
|
|
});
|
|
assert.equal(
|
|
(
|
|
await absences.updateAbsence({
|
|
...identity(latest),
|
|
startDate: day(-1),
|
|
endDate: day(2),
|
|
reason: "OTHER",
|
|
note: role,
|
|
})
|
|
).success,
|
|
true,
|
|
);
|
|
assert.equal(
|
|
(await absences.getDailyAbsences({ date: today })).absences![0]
|
|
.startDate,
|
|
day(-1),
|
|
);
|
|
}
|
|
actor = actorOf(parent);
|
|
assert.equal(
|
|
(await absences.deleteAbsence(identity(future))).success,
|
|
true,
|
|
);
|
|
assert.equal(
|
|
await db.absence.findUnique({ where: { id: future.id } }),
|
|
null,
|
|
);
|
|
},
|
|
);
|
|
|
|
await t.test(
|
|
"Ein veraltetes Rollen-Token verleiht keine Teamrechte",
|
|
async () => {
|
|
actor = { ...actorOf(parent), role: "ADMIN" };
|
|
const running = await db.absence.findFirstOrThrow({
|
|
where: { childId: child.id, startDate: absenceDate(day(-1)) },
|
|
});
|
|
assert.ok((await absences.deleteAbsence(identity(running))).error);
|
|
assert.ok(
|
|
(
|
|
await familyActions.resendFamilyInvitationAction(
|
|
invitedUsers[1].id,
|
|
)
|
|
).error,
|
|
);
|
|
},
|
|
);
|
|
await t.test(
|
|
"Additives Upgrade funktioniert auf leerem und bestehendem Schema und ist wiederholbar",
|
|
async () => {
|
|
const sql = await readFile(
|
|
resolve("deploy/sql/familieneinladungen.sql"),
|
|
"utf8",
|
|
);
|
|
const schema = `upgrade_test_${suffix.replaceAll("-", "")}`;
|
|
await db.$executeRawUnsafe(`CREATE SCHEMA "${schema}"`);
|
|
try {
|
|
await db.$transaction(async (tx) => {
|
|
await tx.$executeRawUnsafe(
|
|
`SET LOCAL search_path TO "${schema}"`,
|
|
);
|
|
await tx.$executeRawUnsafe(sql);
|
|
const fresh = await tx.$queryRawUnsafe<
|
|
{ relation: string | null }[]
|
|
>("SELECT to_regclass('invitations')::text AS relation");
|
|
assert.equal(fresh[0].relation, null);
|
|
await tx.$executeRawUnsafe(
|
|
'CREATE TABLE "users" ("id" TEXT PRIMARY KEY)',
|
|
);
|
|
await tx.$executeRawUnsafe(
|
|
'CREATE TABLE "invitations" ("id" TEXT PRIMARY KEY, "token" TEXT NOT NULL)',
|
|
);
|
|
await tx.$executeRawUnsafe(
|
|
"INSERT INTO \"invitations\" (\"id\", \"token\") VALUES ('a', 'old-a'), ('b', 'old-b')",
|
|
);
|
|
await tx.$executeRawUnsafe(sql);
|
|
await tx.$executeRawUnsafe(sql);
|
|
const rows = await tx.$queryRawUnsafe<
|
|
{
|
|
token: string;
|
|
userId: string | null;
|
|
failed: boolean;
|
|
attempts: number;
|
|
}[]
|
|
>(
|
|
'SELECT "token", "userId", "failed", "attempts" FROM "invitations" ORDER BY "id"',
|
|
);
|
|
assert.deepEqual(rows, [
|
|
{ token: "old-a", userId: null, failed: false, attempts: 0 },
|
|
{ token: "old-b", userId: null, failed: false, attempts: 0 },
|
|
]);
|
|
});
|
|
} finally {
|
|
await db.$executeRawUnsafe(`DROP SCHEMA "${schema}" CASCADE`);
|
|
}
|
|
},
|
|
);
|
|
} finally {
|
|
const users = await db.user.findMany({
|
|
where: { kitaId: { in: [kitaId, otherKitaId] } },
|
|
select: { id: true },
|
|
});
|
|
await db.verificationToken.deleteMany({
|
|
where: { identifier: { in: users.map((user) => user.id) } },
|
|
});
|
|
await db.kita.deleteMany({
|
|
where: { id: { in: [kitaId, otherKitaId] } },
|
|
});
|
|
await Promise.all([db.$disconnect(), prisma.$disconnect()]);
|
|
}
|
|
},
|
|
);
|