600 lines
19 KiB
JavaScript
600 lines
19 KiB
JavaScript
/* eslint-disable @typescript-eslint/no-require-imports -- Test real server code with isolated request and transport boundaries. */
|
|
const { test, before, after, beforeEach } = require("node:test");
|
|
const assert = require("node:assert/strict");
|
|
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
const vm = require("node:vm");
|
|
const ts = require("typescript");
|
|
const { PrismaClient } = require("@prisma/client");
|
|
const { randomUUID } = require("node:crypto");
|
|
let prisma,
|
|
current,
|
|
f,
|
|
change,
|
|
changeAction,
|
|
remove,
|
|
support,
|
|
updateFamily,
|
|
alarms,
|
|
alarmMail,
|
|
staff,
|
|
staffMail,
|
|
assign;
|
|
const emails = [];
|
|
const tenants = [],
|
|
looseUsers = [];
|
|
function loader(overrides = {}) {
|
|
const cache = new Map();
|
|
function load(file) {
|
|
file = path.resolve(file);
|
|
if (cache.has(file)) return cache.get(file).exports;
|
|
const mod = { exports: {} };
|
|
cache.set(file, mod);
|
|
const code = ts.transpileModule(fs.readFileSync(file, "utf8"), {
|
|
compilerOptions: {
|
|
target: ts.ScriptTarget.ES2022,
|
|
module: ts.ModuleKind.CommonJS,
|
|
jsx: ts.JsxEmit.ReactJSX,
|
|
esModuleInterop: true,
|
|
},
|
|
fileName: file,
|
|
}).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() {} };
|
|
if (spec === "next/navigation")
|
|
return {
|
|
redirect(url) {
|
|
throw new Error(`REDIRECT:${url}`);
|
|
},
|
|
notFound() {
|
|
throw new Error("NOT_FOUND");
|
|
},
|
|
};
|
|
if (spec.startsWith("@/") || spec.startsWith(".")) {
|
|
const target = spec.startsWith("@/")
|
|
? path.resolve("src", spec.slice(2))
|
|
: path.resolve(path.dirname(file), spec);
|
|
for (const suffix of [".ts", ".tsx", "/index.ts"])
|
|
if (fs.existsSync(target + suffix)) return load(target + suffix);
|
|
}
|
|
return require(spec);
|
|
};
|
|
vm.runInThisContext(`(function(require,module,exports){${code}\n})`, {
|
|
filename: file,
|
|
})(req, mod, mod.exports);
|
|
return mod.exports;
|
|
}
|
|
return load;
|
|
}
|
|
|
|
before(async () => {
|
|
const raw = process.env.TEST_DATABASE_URL;
|
|
assert.ok(
|
|
raw,
|
|
"TEST_DATABASE_URL muss ausdrücklich auf die separate lokale kita_roles_test zeigen.",
|
|
);
|
|
const url = new URL(raw);
|
|
assert.ok(["127.0.0.1", "localhost"].includes(url.hostname));
|
|
assert.equal(url.pathname, "/kita_roles_test");
|
|
prisma = new PrismaClient({ datasourceUrl: raw });
|
|
const load = loader({
|
|
"@/lib/auth-utils": {
|
|
requireKitaSession: async () => ({ user: current }),
|
|
requireRole: async (roles) => {
|
|
assert.ok(roles.includes(current.role));
|
|
return { user: current };
|
|
},
|
|
},
|
|
"@/lib/mail": {
|
|
getAppEmailConfigError: () => null,
|
|
sendAppEmail: async (input) => {
|
|
emails.push(input);
|
|
return { success: true };
|
|
},
|
|
},
|
|
});
|
|
change = load("src/lib/kita-role-management.ts").changeKitaRole;
|
|
changeAction = load(
|
|
"src/app/dashboard/zugriffsrechte/actions.ts",
|
|
).changeKitaRoleAction;
|
|
remove = load("src/app/dashboard/profil/actions.ts").deleteMyAccount;
|
|
support = load("src/app/admin/actions.ts");
|
|
updateFamily = load(
|
|
"src/app/dashboard/families/actions.ts",
|
|
).updateFamilyAction;
|
|
alarms = load("src/lib/alarms/service.ts");
|
|
alarmMail = load("src/lib/alarms/email.ts");
|
|
staff = load("src/lib/staff/service.ts");
|
|
staffMail = load("src/lib/staff/email.ts");
|
|
assign = load(
|
|
"src/app/dashboard/notdienst/plan/actions.ts",
|
|
).updateAssignmentAction;
|
|
process.env.AUTH_URL = "http://127.0.0.1:3008";
|
|
});
|
|
async function user(role, extra = {}, kitaId = f.kita.id) {
|
|
return prisma.user.create({
|
|
data: {
|
|
kitaId,
|
|
email: `${randomUUID()}@example.invalid`,
|
|
firstName: "Test",
|
|
lastName: role,
|
|
passwordHash: "test-only",
|
|
emailVerifiedAt: new Date(),
|
|
privacyPolicyAcceptedAt: new Date(),
|
|
role,
|
|
...extra,
|
|
},
|
|
});
|
|
}
|
|
const actor = (u) => ({
|
|
id: u.id,
|
|
kitaId: u.kitaId,
|
|
sessionVersion: u.sessionVersion,
|
|
});
|
|
const changeTo = (u, role, by = f.admin) =>
|
|
change(actor(by), { userId: u.id, role, expectedRole: u.role });
|
|
beforeEach(async () => {
|
|
const kita = await prisma.kita.create({
|
|
data: { name: "Rollen-Test", slug: `roles-${randomUUID()}` },
|
|
});
|
|
tenants.push(kita.id);
|
|
f = { kita };
|
|
f.admin = await user("ADMIN");
|
|
f.family = await prisma.family.create({
|
|
data: { kitaId: kita.id, name: "Beispiel" },
|
|
});
|
|
f.parent = await user("ELTERN", { familyId: f.family.id });
|
|
f.child = await prisma.child.create({
|
|
data: {
|
|
kitaId: kita.id,
|
|
familyId: f.family.id,
|
|
firstName: "Kind",
|
|
lastName: "Test",
|
|
},
|
|
});
|
|
current = f.admin;
|
|
emails.length = 0;
|
|
});
|
|
after(async () => {
|
|
if (!prisma) return;
|
|
await prisma.kita.deleteMany({ where: { id: { in: tenants } } });
|
|
await prisma.user.deleteMany({ where: { id: { in: looseUsers } } });
|
|
await prisma.auditLog.deleteMany({
|
|
where: { targetKitaId: { in: tenants } },
|
|
});
|
|
await prisma.$disconnect();
|
|
});
|
|
|
|
test("Eltern werden Koordination und Kita-Admin; Haushalt und Kind bleiben erhalten", async () => {
|
|
assert.equal(
|
|
(
|
|
await changeAction({
|
|
userId: f.parent.id,
|
|
expectedRole: "ELTERN",
|
|
role: "KOORDINATOR",
|
|
})
|
|
).success,
|
|
true,
|
|
);
|
|
const coord = await prisma.user.findUniqueOrThrow({
|
|
where: { id: f.parent.id },
|
|
});
|
|
await changeTo(coord, "ADMIN");
|
|
const updated = await prisma.user.findUniqueOrThrow({
|
|
where: { id: coord.id },
|
|
});
|
|
assert.equal(updated.familyId, f.family.id);
|
|
assert.equal(updated.sessionVersion, coord.sessionVersion);
|
|
assert.equal(
|
|
await prisma.child.count({ where: { familyId: f.family.id } }),
|
|
1,
|
|
);
|
|
});
|
|
test("Rollenänderung protokolliert Akteur, Kita und vorherige/neue Rolle genau einmal", async () => {
|
|
await changeTo(f.parent, "ADMIN");
|
|
const jobs = await prisma.auditLog.findMany({
|
|
where: { targetKitaId: f.kita.id, action: "USER_ROLE_CHANGE" },
|
|
});
|
|
assert.equal(jobs.length, 1);
|
|
assert.equal(jobs[0].actorUserId, f.admin.id);
|
|
assert.deepEqual(jobs[0].metadata, {
|
|
previousRole: "ELTERN",
|
|
newRole: "ADMIN",
|
|
});
|
|
assert.equal(
|
|
(await changeTo({ ...f.parent, role: "ADMIN" }, "ADMIN")).changed,
|
|
false,
|
|
);
|
|
assert.equal(
|
|
await prisma.auditLog.count({
|
|
where: { targetKitaId: f.kita.id, action: "USER_ROLE_CHANGE" },
|
|
}),
|
|
1,
|
|
);
|
|
});
|
|
test("Konto ohne Haushalt darf Koordination werden, aber nicht Eltern oder Fachkraft", async () => {
|
|
const person = await user("KOORDINATOR");
|
|
await assert.rejects(changeTo(person, "ELTERN"), /Haushalt/);
|
|
await assert.rejects(changeTo(person, "ERZIEHER"), /Personalprofil/);
|
|
assert.equal((await changeTo(person, "ADMIN")).changed, true);
|
|
});
|
|
test("Superadmin-Rolle kann weder vergeben noch verändert werden", async () => {
|
|
await assert.rejects(changeTo(f.parent, "SUPERADMIN"), /Ungültige/);
|
|
const root = await user("SUPERADMIN");
|
|
await assert.rejects(
|
|
change(actor(f.admin), {
|
|
userId: root.id,
|
|
expectedRole: "ADMIN",
|
|
role: "KOORDINATOR",
|
|
}),
|
|
/nicht geändert/,
|
|
);
|
|
});
|
|
test("Fremde Kita ist bei Zielkonto und Akteur ausgeschlossen", async () => {
|
|
const foreign = await prisma.kita.create({
|
|
data: { name: "Andere", slug: `roles-${randomUUID()}` },
|
|
});
|
|
tenants.push(foreign.id);
|
|
const outsider = await user("ADMIN", {}, foreign.id);
|
|
await assert.rejects(changeTo(outsider, "KOORDINATOR"), /nicht geändert/);
|
|
await assert.rejects(
|
|
change(
|
|
{ ...actor(f.admin), kitaId: foreign.id },
|
|
{ userId: outsider.id, expectedRole: "ADMIN", role: "KOORDINATOR" },
|
|
),
|
|
/nicht mehr ändern/,
|
|
);
|
|
});
|
|
test("Eltern, gesperrte Admins und veraltete Sessions dürfen keine Rollen ändern", async () => {
|
|
await assert.rejects(
|
|
change(actor(f.parent), {
|
|
userId: f.admin.id,
|
|
expectedRole: "ADMIN",
|
|
role: "KOORDINATOR",
|
|
}),
|
|
/nicht mehr ändern/,
|
|
);
|
|
await assert.rejects(
|
|
change(
|
|
{ ...actor(f.admin), sessionVersion: 9 },
|
|
{ userId: f.parent.id, expectedRole: "ELTERN", role: "ADMIN" },
|
|
),
|
|
/nicht mehr ändern/,
|
|
);
|
|
await prisma.user.update({
|
|
where: { id: f.admin.id },
|
|
data: { disabledAt: new Date() },
|
|
});
|
|
await assert.rejects(changeTo(f.parent, "ADMIN"), /nicht mehr ändern/);
|
|
});
|
|
test("Veralteter Dialog überschreibt keine zwischenzeitliche Rollenänderung", async () => {
|
|
await changeTo(f.parent, "KOORDINATOR");
|
|
await assert.rejects(changeTo(f.parent, "ADMIN"), /inzwischen geändert/);
|
|
assert.equal(
|
|
(await prisma.user.findUnique({ where: { id: f.parent.id } })).role,
|
|
"KOORDINATOR",
|
|
);
|
|
});
|
|
test("Letzter Admin kann seine Rolle nicht abgeben", async () => {
|
|
await assert.rejects(
|
|
changeTo(f.admin, "KOORDINATOR"),
|
|
/Mindestens ein aktiver/,
|
|
);
|
|
});
|
|
for (const [label, extra] of [
|
|
["offene Einladung", { emailVerifiedAt: null, passwordHash: "" }],
|
|
["deaktiviert", { disabledAt: new Date() }],
|
|
["gesperrt", { lockedUntil: new Date("2099-01-01") }],
|
|
["ohne Zustimmung", { privacyPolicyAcceptedAt: null }],
|
|
]) {
|
|
test(`Ein weiterer Admin ${label} reicht nicht als Ersatz`, async () => {
|
|
await user("ADMIN", extra);
|
|
await assert.rejects(
|
|
changeTo(f.admin, "KOORDINATOR"),
|
|
/Mindestens ein aktiver/,
|
|
);
|
|
});
|
|
}
|
|
test("Mit zweitem aktivierten Admin ist eigene Herabstufung möglich und stale Admin-Rechte sind unwirksam", async () => {
|
|
await user("ADMIN");
|
|
assert.equal((await changeTo(f.admin, "KOORDINATOR")).ownRoleChanged, true);
|
|
await assert.rejects(changeTo(f.parent, "ADMIN"), /nicht mehr ändern/);
|
|
});
|
|
test("Parallele Herabstufung zweier Admins lässt genau einen Admin bestehen", async () => {
|
|
const second = await user("ADMIN");
|
|
const results = await Promise.allSettled([
|
|
changeTo(f.admin, "KOORDINATOR", f.admin),
|
|
changeTo(second, "KOORDINATOR", second),
|
|
]);
|
|
assert.equal(results.filter((r) => r.status === "fulfilled").length, 1);
|
|
assert.equal(
|
|
await prisma.user.count({ where: { kitaId: f.kita.id, role: "ADMIN" } }),
|
|
1,
|
|
);
|
|
});
|
|
test("Eigene Kontolöschung ist für letzten Admin gesperrt", async () => {
|
|
assert.match((await remove()).error, /Mindestens ein aktiver/);
|
|
assert.ok(await prisma.user.findUnique({ where: { id: f.admin.id } }));
|
|
});
|
|
test("Eigene Kontolöschung mit zweitem aktivem Admin funktioniert weiterhin", async () => {
|
|
await user("ADMIN");
|
|
assert.equal((await remove()).success, true);
|
|
assert.equal(
|
|
await prisma.user.findUnique({ where: { id: f.admin.id } }),
|
|
null,
|
|
);
|
|
});
|
|
test("Superadmin kann den letzten Kita-Admin weder löschen noch sperren", async () => {
|
|
current = await user("SUPERADMIN", {}, null);
|
|
looseUsers.push(current.id);
|
|
current.authenticatedAt = Date.now();
|
|
assert.match(
|
|
(
|
|
await support.deleteUser({
|
|
userId: f.admin.id,
|
|
confirmationEmail: f.admin.email,
|
|
})
|
|
).error,
|
|
/Mindestens ein aktiver/,
|
|
);
|
|
assert.match(
|
|
(await support.lockUser(f.admin.id)).error,
|
|
/Mindestens ein aktiver/,
|
|
);
|
|
});
|
|
test("Support darf zusätzlichen Admin sperren und löschen", async () => {
|
|
const second = await user("ADMIN");
|
|
current = await user("SUPERADMIN", {}, null);
|
|
looseUsers.push(current.id);
|
|
current.authenticatedAt = Date.now();
|
|
assert.equal((await support.lockUser(second.id)).ok, true);
|
|
assert.equal(
|
|
(
|
|
await support.deleteUser({
|
|
userId: second.id,
|
|
confirmationEmail: second.email,
|
|
})
|
|
).ok,
|
|
true,
|
|
);
|
|
});
|
|
test("Gleichzeitige Admin-Löschung und Herabstufung lässt einen Admin bestehen", async () => {
|
|
const second = await user("ADMIN");
|
|
const results = await Promise.allSettled([
|
|
remove(),
|
|
changeTo(second, "KOORDINATOR"),
|
|
]);
|
|
const successful =
|
|
results.filter((r) => r.status === "fulfilled" && r.value.success).length +
|
|
results.filter((r) => r.status === "fulfilled" && r.value.changed).length;
|
|
assert.equal(successful, 1);
|
|
assert.equal(
|
|
await prisma.user.count({ where: { kitaId: f.kita.id, role: "ADMIN" } }),
|
|
1,
|
|
);
|
|
});
|
|
test("E-Mail-Änderung darf letzten aktivierten Admin nicht deaktivieren", async () => {
|
|
await prisma.user.update({
|
|
where: { id: f.admin.id },
|
|
data: { familyId: f.family.id },
|
|
});
|
|
const payload = {
|
|
familyId: f.family.id,
|
|
familyName: f.family.name,
|
|
parents: [
|
|
f.parent,
|
|
{ ...f.admin, email: `${randomUUID()}@example.invalid` },
|
|
],
|
|
children: [f.child],
|
|
};
|
|
assert.match((await updateFamily(payload)).error, /Mindestens ein aktiver/);
|
|
assert.equal(
|
|
(await prisma.user.findUnique({ where: { id: f.admin.id } })).email,
|
|
f.admin.email,
|
|
);
|
|
});
|
|
|
|
test("Fachkraft wird Admin; Personalprofil bleibt bearbeitbar und eigene Mailpräferenz erhalten", async () => {
|
|
const person = await user("ERZIEHER");
|
|
const educator = await prisma.educator.create({
|
|
data: { kitaId: f.kita.id, firstName: "Team", lastName: "Test" },
|
|
});
|
|
const profile = await prisma.staffProfile.create({
|
|
data: {
|
|
kitaId: f.kita.id,
|
|
educatorId: educator.id,
|
|
userId: person.id,
|
|
employmentStart: new Date("2030-01-01"),
|
|
trackingStart: new Date("2030-01-01"),
|
|
},
|
|
});
|
|
await assert.rejects(changeTo(person, "KOORDINATOR"), /Personalprofil/);
|
|
await changeTo(person, "ADMIN");
|
|
await staff.executeStaffCommand(
|
|
{ id: person.id, kitaId: f.kita.id, role: "ADMIN" },
|
|
{ command: "emailPreference", data: { enabled: false } },
|
|
);
|
|
await staff.executeStaffCommand(
|
|
{ id: f.admin.id, kitaId: f.kita.id, role: "ADMIN" },
|
|
{
|
|
command: "educatorUpdate",
|
|
data: {
|
|
educatorId: educator.id,
|
|
firstName: "Team",
|
|
lastName: "Geändert",
|
|
active: true,
|
|
email: person.email,
|
|
},
|
|
},
|
|
);
|
|
assert.equal(
|
|
(await prisma.staffProfile.findUnique({ where: { id: profile.id } }))
|
|
.userId,
|
|
person.id,
|
|
);
|
|
assert.equal(
|
|
(await prisma.user.findUnique({ where: { id: person.id } }))
|
|
.staffDecisionEmails,
|
|
false,
|
|
);
|
|
await changeTo({ ...person, role: "ADMIN" }, "ERZIEHER");
|
|
});
|
|
test("Offene Fachkraft-Einladung lässt sich auch nach Admin-Ernennung erneut senden", async () => {
|
|
const person = await user("ERZIEHER", {
|
|
emailVerifiedAt: null,
|
|
passwordHash: "",
|
|
});
|
|
const educator = await prisma.educator.create({
|
|
data: { kitaId: f.kita.id, firstName: "Team", lastName: "Test" },
|
|
});
|
|
await prisma.staffProfile.create({
|
|
data: {
|
|
kitaId: f.kita.id,
|
|
educatorId: educator.id,
|
|
userId: person.id,
|
|
employmentStart: new Date("2030-01-01"),
|
|
trackingStart: new Date("2030-01-01"),
|
|
},
|
|
});
|
|
await changeTo(person, "ADMIN");
|
|
const queued = await staff.executeStaffCommand(
|
|
{ id: f.admin.id, kitaId: f.kita.id, role: "ADMIN" },
|
|
{ command: "educatorInvite", data: { educatorId: educator.id } },
|
|
);
|
|
assert.equal(
|
|
(
|
|
await staffMail.deliverStaffEmails(queued.emailIds, async () => ({
|
|
success: true,
|
|
}))
|
|
).sent,
|
|
1,
|
|
);
|
|
});
|
|
for (const role of ["ADMIN", "KOORDINATOR"])
|
|
test(`Haushaltsalarme erreichen und akzeptieren Eltern mit Rolle ${role}`, async () => {
|
|
await changeTo(f.parent, role);
|
|
const plan = await prisma.notdienstPlan.create({
|
|
data: {
|
|
kitaId: f.kita.id,
|
|
year: 2030,
|
|
month: 11,
|
|
status: "PUBLISHED",
|
|
assignments: {
|
|
create: {
|
|
kitaId: f.kita.id,
|
|
childId: f.child.id,
|
|
date: new Date("2030-11-04"),
|
|
},
|
|
},
|
|
},
|
|
include: { assignments: true },
|
|
});
|
|
const job = await alarms.executeAlarmCommand(
|
|
{ id: f.admin.id, kitaId: f.kita.id, role: "ADMIN" },
|
|
"create",
|
|
{ assignmentId: plan.assignments[0].id, wholeDay: true },
|
|
);
|
|
const request = await prisma.notdienstAlarmEmail.findFirstOrThrow({
|
|
where: { alarmId: job.alarmId, userId: f.parent.id, kind: "REQUEST" },
|
|
});
|
|
const token = request.path.split("/").pop();
|
|
assert.ok(await alarms.getPublicAlarm(token));
|
|
assert.equal(
|
|
(
|
|
await alarmMail.deliverAlarmEmails([request.id], async () => ({
|
|
success: true,
|
|
}))
|
|
).sent,
|
|
1,
|
|
);
|
|
await alarms.respondToAlarm(token, true);
|
|
assert.equal(
|
|
(await prisma.notdienstAlarm.findUnique({ where: { id: job.alarmId } }))
|
|
.status,
|
|
"CONFIRMED",
|
|
);
|
|
});
|
|
test("Bereits ausgestellter Haushalts-Alarmlink bleibt nach Ernennung gültig", async () => {
|
|
const plan = await prisma.notdienstPlan.create({
|
|
data: {
|
|
kitaId: f.kita.id,
|
|
year: 2030,
|
|
month: 11,
|
|
status: "PUBLISHED",
|
|
assignments: {
|
|
create: {
|
|
kitaId: f.kita.id,
|
|
childId: f.child.id,
|
|
date: new Date("2030-11-04"),
|
|
},
|
|
},
|
|
},
|
|
include: { assignments: true },
|
|
});
|
|
const job = await alarms.executeAlarmCommand(
|
|
{ id: f.admin.id, kitaId: f.kita.id, role: "ADMIN" },
|
|
"create",
|
|
{ assignmentId: plan.assignments[0].id, wholeDay: true },
|
|
);
|
|
const request = await prisma.notdienstAlarmEmail.findFirstOrThrow({
|
|
where: { alarmId: job.alarmId, userId: f.parent.id, kind: "REQUEST" },
|
|
});
|
|
await changeTo(f.parent, "ADMIN");
|
|
assert.ok(await alarms.getPublicAlarm(request.path.split("/").pop()));
|
|
});
|
|
|
|
test("Letzter Admin im Ein-Personen-Haushalt bleibt auch vor kaskadierender Familienlöschung geschützt", async () => {
|
|
await prisma.user.update({
|
|
where: { id: f.parent.id },
|
|
data: { role: "ADMIN" },
|
|
});
|
|
await prisma.user.update({
|
|
where: { id: f.admin.id },
|
|
data: { role: "KOORDINATOR" },
|
|
});
|
|
current = { ...f.parent, role: "ADMIN" };
|
|
assert.match((await remove()).error, /Mindestens ein aktiver/);
|
|
assert.ok(await prisma.family.findUnique({ where: { id: f.family.id } }));
|
|
assert.ok(await prisma.child.findUnique({ where: { id: f.child.id } }));
|
|
});
|
|
|
|
test("Veröffentlichte Umteilung informiert auch den Admin im betroffenen Elternhaushalt", async () => {
|
|
await changeTo(f.parent, "ADMIN");
|
|
const family = await prisma.family.create({
|
|
data: { kitaId: f.kita.id, name: "Ersatz" },
|
|
});
|
|
const parent = await user("ELTERN", { familyId: family.id });
|
|
const child = await prisma.child.create({
|
|
data: {
|
|
kitaId: f.kita.id,
|
|
familyId: family.id,
|
|
firstName: "Ersatz",
|
|
lastName: "Test",
|
|
},
|
|
});
|
|
const plan = await prisma.notdienstPlan.create({
|
|
data: {
|
|
kitaId: f.kita.id,
|
|
year: 2030,
|
|
month: 11,
|
|
status: "PUBLISHED",
|
|
assignments: {
|
|
create: {
|
|
kitaId: f.kita.id,
|
|
childId: f.child.id,
|
|
date: new Date("2030-11-04"),
|
|
},
|
|
},
|
|
},
|
|
});
|
|
const result = await assign(plan.id, "2030-11-04", child.id);
|
|
assert.equal(result.success, true);
|
|
assert.ok(emails.some((email) => [email.to].flat().includes(f.parent.email)));
|
|
assert.ok(emails.some((email) => [email.to].flat().includes(parent.email)));
|
|
});
|