see comments

This commit is contained in:
Rajpreet0 2026-09-15 13:53:27 +02:00
parent a26e2a6245
commit 94e632681e
4 changed files with 98 additions and 51 deletions

View file

@ -4,9 +4,8 @@ vi.mock("@/src/lib/prisma", () => ({
prisma: {
course: {
findMany: vi.fn(),
updateMany: vi.fn(),
updateManyAndReturn: vi.fn(),
},
$transaction: vi.fn(),
},
}));
@ -14,8 +13,10 @@ vi.mock("@/src/lib/auth/getUser", () => ({
getUserData: vi.fn(),
isManager: (u: { role: string }) => u?.role === "manager",
isEmployee: (u: { role: string }) => u?.role === "employee",
getEffectiveManagerId: (u: { role: string; id: string; managerId?: string }) =>
u?.role === "manager" ? u.id : (u?.managerId ?? null),
// Spiegelt die echte Implementierung: für Employees ist der Manager
// EmployeeData.createdBy — nicht etwa ein managerId-Feld, das es nicht gibt.
getEffectiveManagerId: (u: { role: string; id: string; createdBy?: string | null }) =>
u?.role === "manager" ? u.id : (u?.createdBy ?? null),
}));
vi.mock("next/cache", () => ({
@ -30,8 +31,7 @@ import {
} from "@/src/modules/courses/actions/publish-drafts-actions";
const findMany = vi.mocked(prisma.course.findMany);
const updateMany = vi.mocked(prisma.course.updateMany);
const transaction = vi.mocked(prisma.$transaction);
const updateManyAndReturn = vi.mocked(prisma.course.updateManyAndReturn);
const mockGetUserData = vi.mocked(getUserData);
const MANAGER = { id: "mgr-1", role: "manager" };
@ -61,17 +61,13 @@ const completeDraft = (overrides: Record<string, unknown> = {}) => ({
...overrides,
});
/** $transaction mit Callback ausführen, als liefe sie gegen die echte DB. */
const runTransaction = (updatedIds: string[]) => {
transaction.mockImplementationOnce(async (cb: unknown) => {
const tx = {
course: {
updateMany: vi.fn().mockResolvedValue({ count: updatedIds.length }),
findMany: vi.fn().mockResolvedValue(updatedIds.map((id) => ({ id }))),
},
};
return (cb as (t: typeof tx) => Promise<string[]>)(tx);
});
/**
* Simuliert updateManyAndReturn: die DB gibt nur die Zeilen zurück, die dieses
* Statement tatsächlich geändert hat. Wer nicht in updatedIds steht, wurde
* zwischenzeitlich von anderer Seite verändert und bleibt unberührt.
*/
const mockUpdateReturning = (updatedIds: string[]) => {
updateManyAndReturn.mockResolvedValueOnce(updatedIds.map((id) => ({ id })) as never);
};
beforeEach(() => {
@ -90,7 +86,7 @@ describe("getPublishableDrafts", () => {
mockGetUserData.mockResolvedValueOnce({
id: "emp-1",
role: "employee",
managerId: "mgr-1",
createdBy: "mgr-1",
permissions: { courses: { edit: false } },
} as never);
@ -185,7 +181,7 @@ describe("publishDraftCourses", () => {
mockGetUserData.mockResolvedValueOnce(null as never);
const result = await publishDraftCourses(["c1"]);
expect(result.success).toBe(false);
expect(transaction).not.toHaveBeenCalled();
expect(updateManyAndReturn).not.toHaveBeenCalled();
});
it("lehnt eine leere Auswahl ab", async () => {
@ -212,7 +208,7 @@ describe("publishDraftCourses", () => {
it("veröffentlicht vollständige Entwürfe", async () => {
mockGetUserData.mockResolvedValueOnce(MANAGER as never);
findMany.mockResolvedValueOnce([completeDraft()] as never);
runTransaction(["c1"]);
mockUpdateReturning(["c1"]);
const result = await publishDraftCourses(["c1"]);
@ -229,7 +225,7 @@ describe("publishDraftCourses", () => {
completeDraft(),
completeDraft({ id: "c2", name: "Zumba", room: "" }),
] as never);
runTransaction(["c1"]);
mockUpdateReturning(["c1"]);
const result = await publishDraftCourses(["c1", "c2"]);
@ -242,10 +238,47 @@ describe("publishDraftCourses", () => {
expect(result.skipped[0].reason).toContain("Raum");
});
it("meldet nur Kurse als veröffentlicht, die dieses Update wirklich geändert hat", async () => {
mockGetUserData.mockResolvedValueOnce(MANAGER as never);
findMany.mockResolvedValueOnce([
completeDraft(),
completeDraft({ id: "c2", name: "Pilates" }),
] as never);
// c2 wurde zwischen Prüfung und Update von anderer Seite verändert (z.B.
// parallel veröffentlicht oder abgesagt) — das Update erfasst nur c1.
mockUpdateReturning(["c1"]);
const result = await publishDraftCourses(["c1", "c2"]);
expect(result.success).toBe(true);
if (!result.success) return;
expect(result.published).toBe(1);
expect(result.publishedIds).toEqual(["c1"]);
// c2 darf weder in publishedIds stehen noch mitgezählt werden.
expect(result.publishedIds).not.toContain("c2");
expect(result.vanished).toBe(1);
});
it("veröffentlicht nur Kurse, die noch den Status draft haben", async () => {
mockGetUserData.mockResolvedValueOnce(MANAGER as never);
findMany.mockResolvedValueOnce([completeDraft()] as never);
mockUpdateReturning(["c1"]);
await publishDraftCourses(["c1"]);
// Ohne status: "draft" im where könnte ein zwischenzeitlich abgesagter
// Kurs wieder live gehen.
expect(updateManyAndReturn.mock.calls[0][0]?.where).toEqual({
id: { in: ["c1"] },
createdBy: "mgr-1",
status: "draft",
});
});
it("zählt IDs, die die Abfrage nicht zurückgibt, als verschwunden", async () => {
mockGetUserData.mockResolvedValueOnce(MANAGER as never);
findMany.mockResolvedValueOnce([completeDraft()] as never);
runTransaction(["c1"]);
mockUpdateReturning(["c1"]);
const result = await publishDraftCourses(["c1", "geloescht", "fremd"]);
@ -263,19 +296,18 @@ describe("publishDraftCourses", () => {
expect(result.success).toBe(true);
if (!result.success) return;
expect(result.published).toBe(0);
expect(transaction).not.toHaveBeenCalled();
expect(updateMany).not.toHaveBeenCalled();
expect(updateManyAndReturn).not.toHaveBeenCalled();
});
it("erlaubt Employees mit edit-Berechtigung das Veröffentlichen im Namen des Managers", async () => {
mockGetUserData.mockResolvedValueOnce({
id: "emp-1",
role: "employee",
managerId: "mgr-1",
createdBy: "mgr-1",
permissions: { courses: { edit: true } },
} as never);
findMany.mockResolvedValueOnce([completeDraft()] as never);
runTransaction(["c1"]);
mockUpdateReturning(["c1"]);
const result = await publishDraftCourses(["c1"]);