1
0
Fork 0
polaris-task-force/tests/int/operation-reservation-surfaces.int.spec.ts

201 lines
8 KiB
TypeScript

import { getPayload, Payload } from "payload";
import config from "@/payload.config";
import { afterAll, beforeAll, describe, expect, it, vi, type MockInstance } from "vitest";
import type { OperationReservation, User } from "@/payload-types";
import { getReservationList } from "@/app/(frontend)/operations/reservations/page";
import { getReservationDetail } from "@/app/(frontend)/operations/reservations/[id]/page";
import { createReservation, cancelReservation } from "@/app/(frontend)/operations/reservations/actions";
// Mock next/headers so the action's authenticate() can run outside a request.
vi.mock("next/headers", () => ({
headers: async () => new Headers(),
}));
let payload: Payload;
let authSpy: MockInstance;
const RUN = `res-surfaces-${Date.now().toString(36)}`;
const reservationIds: number[] = [];
const userIds: number[] = [];
const roleIds: number[] = [];
let superUser: User;
let plainUser: User;
let soldierOne: User;
function relId(value: unknown): number {
return typeof value === "object" && value !== null
? (value as { id: number }).id
: (value as number);
}
async function makeUser(label: string, roleDocIds?: number[]): Promise<User> {
const user = (await payload.create({
collection: "users",
data: {
username: `${RUN}-${label}`,
discordUsername: `${RUN}-${label}-discord`,
displayName: `${RUN} ${label}`,
steamId: `${RUN}-steam-${label}`,
password: "Test1234",
roles: ["user"],
...(roleDocIds && roleDocIds.length > 0 ? { roleDocs: roleDocIds } : {}),
},
overrideAccess: true,
depth: 0,
})) as unknown as User;
userIds.push(user.id);
return user;
}
/** Create a minimal (personnel-only) reservation that succeeds without fixtures. */
async function createReservedReservation(key: string, operationId: string): Promise<number> {
const result = await createReservation({
reservationKey: key,
operationId,
actorId: superUser.id,
personnel: [{ userId: soldierOne.id, slot: "Team Lead" }],
expiresAt: new Date(Date.now() + 3_600_000).toISOString(),
});
expect(result.success).toBe(true);
const id = result.data!.id;
reservationIds.push(id);
return id;
}
beforeAll(async () => {
const payloadConfig = await config;
payload = await getPayload({ config: payloadConfig });
authSpy = vi.spyOn(payload, "auth");
const superRole = await payload.create({
collection: "roles",
data: { name: `${RUN} Command`, slug: `${RUN}-command`, isSuperuser: true },
overrideAccess: true,
depth: 0,
});
roleIds.push(superRole.id);
superUser = await makeUser("command", [superRole.id]);
plainUser = await makeUser("plain");
soldierOne = await makeUser("soldier1");
});
afterAll(async () => {
if (!payload) return;
for (const id of reservationIds) {
await payload.delete({ collection: "operation-reservations", id, overrideAccess: true }).catch(() => {});
}
for (const id of userIds) {
await payload.delete({ collection: "users", id, overrideAccess: true }).catch(() => {});
}
for (const id of roleIds) {
await payload.delete({ collection: "roles", id, overrideAccess: true }).catch(() => {});
}
});
describe("reservation surfaces: getReservationList", () => {
it("returns rows and capability true for a permitted viewer", async () => {
authSpy.mockResolvedValue({ user: superUser });
await createReservedReservation(`${RUN}-list-1`, `${RUN}-op-a`);
const data = await getReservationList(payload, { id: superUser.id }, {}, 1);
expect(data.capability).toBe(true);
expect(data.rows.length).toBeGreaterThan(0);
expect(data.rows.some((r) => r.reservationKey === `${RUN}-list-1`)).toBe(true);
const listed = data.rows.find((r) => r.reservationKey === `${RUN}-list-1`);
expect(listed?.status).toBe("reserved");
expect(listed?.expiresAt).toBeTruthy();
});
it("returns empty rows and capability false for a plain user (permission-denied shape)", async () => {
authSpy.mockResolvedValue({ user: plainUser });
const data = await getReservationList(payload, { id: plainUser.id }, {}, 1);
expect(data.capability).toBe(false);
expect(data.rows).toHaveLength(0);
});
it("narrows rows by status", async () => {
authSpy.mockResolvedValue({ user: superUser });
await createReservedReservation(`${RUN}-filter-r`, `${RUN}-op-filter-r`);
const cancelledId = await createReservedReservation(`${RUN}-filter-c`, `${RUN}-op-filter-c`);
await cancelReservation({ reservationId: cancelledId });
const reserved = await getReservationList(payload, { id: superUser.id }, { status: "reserved" }, 1);
const reservedRows = reserved.rows.filter((r) => r.reservationKey === `${RUN}-filter-r`);
expect(reservedRows).toHaveLength(1);
expect(reservedRows[0].status).toBe("reserved");
const cancelled = await getReservationList(payload, { id: superUser.id }, { status: "cancelled" }, 1);
const cancelledRows = cancelled.rows.filter((r) => r.reservationKey === `${RUN}-filter-c`);
expect(cancelledRows).toHaveLength(1);
expect(cancelledRows[0].status).toBe("cancelled");
});
it("narrows rows by operationId", async () => {
authSpy.mockResolvedValue({ user: superUser });
await createReservedReservation(`${RUN}-opid-1`, `${RUN}-op-specific`);
const data = await getReservationList(payload, { id: superUser.id }, { operationId: `${RUN}-op-specific` }, 1);
expect(data.rows).toHaveLength(1);
expect(data.rows[0].operationId).toBe(`${RUN}-op-specific`);
});
});
describe("reservation surfaces: getReservationDetail", () => {
it("returns the full reservation and canCancel when reserved and permitted", async () => {
authSpy.mockResolvedValue({ user: superUser });
const id = await createReservedReservation(`${RUN}-detail-1`, `${RUN}-op-detail`);
const detail = await getReservationDetail(payload, { id: superUser.id }, String(id));
expect(detail.reservation).not.toBeNull();
expect(detail.reservation?.id).toBe(id);
expect(detail.reservation?.status).toBe("reserved");
expect(detail.reservation?.personnel).toHaveLength(1);
expect(relId(detail.reservation?.personnel?.[0]?.user)).toBe(soldierOne.id);
expect(detail.canCancel).toBe(true);
});
it("reports a not-found reservation for an unknown id", async () => {
authSpy.mockResolvedValue({ user: superUser });
const detail = await getReservationDetail(payload, { id: superUser.id }, "999999999");
expect(detail.reservation).toBeNull();
expect(detail.canCancel).toBe(false);
});
it("does not allow cancel for a plain user even on a reserved row", async () => {
authSpy.mockResolvedValue({ user: superUser });
const id = await createReservedReservation(`${RUN}-detail-2`, `${RUN}-op-detail-2`);
const detail = await getReservationDetail(payload, { id: plainUser.id }, String(id));
expect(detail.reservation).not.toBeNull();
expect(detail.canCancel).toBe(false);
});
});
describe("reservation surfaces: cancelReservation integration", () => {
it("cancels a reserved reservation exactly once; second cancel is a clean no-op", async () => {
authSpy.mockResolvedValue({ user: superUser });
const id = await createReservedReservation(`${RUN}-cancel-int`, `${RUN}-op-cancel-int`);
const first = await cancelReservation({ reservationId: id }, { reason: "Cancelled by user" });
expect(first.success).toBe(true);
expect(first.data?.status).toBe("cancelled");
expect(first.data?.settledAt).toBeTruthy();
const second = await cancelReservation({ reservationId: id });
expect(second.success).toBe(true);
expect(second.data?.status).toBe("cancelled");
expect(second.data?.settledAt).toBe(first.data?.settledAt);
});
it("denies cancel for a user without operation-reservations:update", async () => {
authSpy.mockResolvedValue({ user: superUser });
const id = await createReservedReservation(`${RUN}-cancel-denied`, `${RUN}-op-cancel-denied`);
authSpy.mockResolvedValue({ user: plainUser });
const denied = await cancelReservation({ reservationId: id });
expect(denied.success).toBe(false);
expect(denied.error).toContain("forbidden");
});
});