mirror of
https://github.com/Since-3/Kuma.git
synced 2026-10-12 20:51:18 +00:00
184 lines
6.9 KiB
TypeScript
184 lines
6.9 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
import {
|
|
assignSpots,
|
|
extendSpotHolds,
|
|
getCourseSpots,
|
|
getHeldSpotIds,
|
|
holdSpot,
|
|
spotHolderKey,
|
|
} from "@/src/modules/courses/lib/course-spots";
|
|
import type { FloorPlanItem } from "@/src/modules/rooms/lib/floor-plan";
|
|
|
|
const db = {
|
|
room: { findUnique: vi.fn() },
|
|
courseBooking: { findMany: vi.fn(), findFirst: vi.fn() },
|
|
courseSpotHold: {
|
|
findMany: vi.fn(),
|
|
findUnique: vi.fn(),
|
|
deleteMany: vi.fn(),
|
|
create: vi.fn(),
|
|
updateMany: vi.fn(),
|
|
},
|
|
};
|
|
|
|
const spot = (id: string, label: string): FloorPlanItem => ({
|
|
id,
|
|
type: "reformer",
|
|
x: 1,
|
|
y: 1.5,
|
|
rotation: 0,
|
|
label,
|
|
});
|
|
const spots = [spot("s1", "1"), spot("s2", "2"), spot("s3", "3")];
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
db.courseBooking.findMany.mockResolvedValue([]);
|
|
db.courseBooking.findFirst.mockResolvedValue(null);
|
|
db.courseSpotHold.findMany.mockResolvedValue([]);
|
|
db.courseSpotHold.deleteMany.mockResolvedValue({ count: 0 });
|
|
db.courseSpotHold.create.mockResolvedValue({ id: "hold-1" });
|
|
});
|
|
|
|
describe("getCourseSpots", () => {
|
|
it("liest den Raum gar nicht erst, wenn der Kurs keine Platzwahl hat", async () => {
|
|
expect(await getCourseSpots(db as never, { spotBooking: false, room: "room-1" })).toBeNull();
|
|
expect(db.room.findUnique).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("behandelt einen Raum ohne Geräte wie einen Kurs ohne Platzwahl", async () => {
|
|
db.room.findUnique.mockResolvedValue({ floorPlan: null });
|
|
expect(await getCourseSpots(db as never, { spotBooking: true, room: "room-1" })).toBeNull();
|
|
});
|
|
|
|
it("liefert die buchbaren Geräte des Raums", async () => {
|
|
db.room.findUnique.mockResolvedValue({
|
|
floorPlan: { width: 8, depth: 6, items: [spot("s2", "2"), spot("s1", "1")] },
|
|
});
|
|
const result = await getCourseSpots(db as never, { spotBooking: true, room: "room-1" });
|
|
expect(result?.map((s) => s.id)).toEqual(["s1", "s2"]);
|
|
});
|
|
});
|
|
|
|
describe("assignSpots", () => {
|
|
const assign = (params: { count: number; requestedSpotIds: string[]; holdIds?: string[] }) =>
|
|
assignSpots(db as never, { courseId: "c1", spots, holdIds: [], ...params });
|
|
|
|
it("gibt die Wunschplätze, wenn sie frei sind", async () => {
|
|
const result = await assign({ count: 2, requestedSpotIds: ["s3", "s2"], holdIds: ["h"] });
|
|
expect(result).toEqual([
|
|
{ spotId: "s2", spotLabel: "Reformer 2", changed: false },
|
|
{ spotId: "s3", spotLabel: "Reformer 3", changed: false },
|
|
]);
|
|
});
|
|
|
|
it("weicht auf einen freien Platz aus, den niemand gerade im Checkout hält", async () => {
|
|
db.courseBooking.findMany.mockResolvedValue([{ spotIds: ["s1"] }]);
|
|
db.courseSpotHold.findMany.mockResolvedValue([{ spotId: "s2" }]);
|
|
|
|
const result = await assign({ count: 1, requestedSpotIds: ["s1"], holdIds: ["hold-1"] });
|
|
|
|
expect(result).toEqual([{ spotId: "s3", spotLabel: "Reformer 3", changed: true }]);
|
|
// Die eigenen Holds zählen nicht als "von jemand anderem gehalten".
|
|
expect(db.courseSpotHold.findMany.mock.calls[0][0].where.id).toEqual({ notIn: ["hold-1"] });
|
|
});
|
|
|
|
it("behält freie Wunschplätze und füllt nur den fehlenden auf", async () => {
|
|
db.courseBooking.findMany.mockResolvedValue([{ spotIds: ["s1"] }]);
|
|
|
|
const result = await assign({ count: 2, requestedSpotIds: ["s1", "s2"] });
|
|
|
|
expect(result?.map((s) => [s.spotId, s.changed])).toEqual([
|
|
["s2", false],
|
|
["s3", true],
|
|
]);
|
|
});
|
|
|
|
it("nimmt notfalls einen gehaltenen Platz - bezahlt schlägt Checkout", async () => {
|
|
db.courseBooking.findMany.mockResolvedValue([{ spotIds: ["s1", "s3"] }]);
|
|
db.courseSpotHold.findMany.mockResolvedValue([{ spotId: "s2" }]);
|
|
|
|
const result = await assign({ count: 1, requestedSpotIds: ["s1"] });
|
|
expect(result?.[0].spotId).toBe("s2");
|
|
});
|
|
|
|
it("liefert null, wenn nicht genug Plätze frei sind", async () => {
|
|
db.courseBooking.findMany.mockResolvedValue([{ spotIds: ["s1", "s2"] }]);
|
|
expect(await assign({ count: 2, requestedSpotIds: ["s3"] })).toBeNull();
|
|
});
|
|
|
|
it("weist bei alten Sessions ohne Wunschplatz die ersten freien zu", async () => {
|
|
const result = await assign({ count: 1, requestedSpotIds: [] });
|
|
expect(result).toEqual([{ spotId: "s1", spotLabel: "Reformer 1", changed: false }]);
|
|
});
|
|
});
|
|
|
|
describe("holdSpot", () => {
|
|
const params = {
|
|
courseId: "c1",
|
|
spotId: "s1",
|
|
holderKey: spotHolderKey({ userId: "u1" }),
|
|
expiresAt: new Date(Date.now() + 30 * 60_000),
|
|
};
|
|
|
|
it("räumt nur abgelaufene Holds weg - eigene Holds anderer Checkouts bleiben", async () => {
|
|
const result = await holdSpot(db as never, params);
|
|
|
|
expect(result).toEqual({ held: true, holdId: "hold-1" });
|
|
const where = db.courseSpotHold.deleteMany.mock.calls[0][0].where;
|
|
expect(where).toEqual({ courseId: "c1", expiresAt: { lte: expect.any(Date) } });
|
|
});
|
|
|
|
it("hält keinen Platz, der schon bezahlt ist", async () => {
|
|
db.courseBooking.findFirst.mockResolvedValue({ id: "b1" });
|
|
const result = await holdSpot(db as never, params);
|
|
expect(result.held).toBe(false);
|
|
expect(db.courseSpotHold.create).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("verliert gegen einen gleichzeitigen Checkout für denselben Platz", async () => {
|
|
db.courseSpotHold.create.mockRejectedValue(
|
|
Object.assign(new Error("Unique constraint failed"), { code: "P2002" })
|
|
);
|
|
db.courseSpotHold.findUnique.mockResolvedValue({ holderKey: "user:jemand-anderes" });
|
|
const result = await holdSpot(db as never, params);
|
|
expect(result).toMatchObject({
|
|
held: false,
|
|
reason: expect.stringContaining("jemand anderem"),
|
|
});
|
|
});
|
|
|
|
it("sagt der Person, dass ihre eigene Zahlung für den Platz noch läuft", async () => {
|
|
db.courseSpotHold.create.mockRejectedValue(
|
|
Object.assign(new Error("Unique constraint failed"), { code: "P2002" })
|
|
);
|
|
db.courseSpotHold.findUnique.mockResolvedValue({ holderKey: "user:u1" });
|
|
const result = await holdSpot(db as never, params);
|
|
expect(result).toMatchObject({ held: false, reason: expect.stringContaining("Ihre Zahlung") });
|
|
});
|
|
});
|
|
|
|
describe("Hold-Status", () => {
|
|
it("markiert einen verlängerten Hold als abgeschlossenen Checkout", async () => {
|
|
const until = new Date(Date.now() + 14 * 86_400_000);
|
|
await extendSpotHolds(db as never, ["hold-1"], until);
|
|
expect(db.courseSpotHold.updateMany).toHaveBeenCalledWith({
|
|
where: { id: { in: ["hold-1"] } },
|
|
data: { expiresAt: until, status: "awaiting_payment" },
|
|
});
|
|
});
|
|
|
|
it("blendet für die Person nur ihre offenen Checkouts aus", async () => {
|
|
await getHeldSpotIds(db as never, "c1", { exceptHolderKey: "user:u1" });
|
|
expect(db.courseSpotHold.findMany.mock.calls[0][0].where.NOT).toEqual({
|
|
holderKey: "user:u1",
|
|
status: "checkout",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("spotHolderKey", () => {
|
|
it("normalisiert Gast-E-Mails, damit derselbe Gast seinen alten Hold ersetzt", () => {
|
|
expect(spotHolderKey({ guestEmail: " Gast@Example.com " })).toBe("guest:gast@example.com");
|
|
});
|
|
});
|