import { getPayload, Payload } from "payload"; import config from "@/payload.config"; import { afterAll, beforeAll, describe, expect, it, vi, type MockInstance } from "vitest"; import type { ArmaCommand, SupplyBox, User } from "@/payload-types"; import { queueCrateCommand } from "@/app/(frontend)/logistics/supply-boxes/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 = `sbx-${Date.now().toString(36)}`; // --------------------------------------------------------------------------- // Fixture tracking for cleanup // --------------------------------------------------------------------------- const userIds: number[] = []; const roleIds: number[] = []; const syncEventIds: number[] = []; const boxIds: number[] = []; const commandIds: number[] = []; let serverId: number; let commandUser: User; let plainUser: User; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- async function makeUser(label: string, roleDocIds?: number[]): Promise { 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; } // Creates a supply box directly (the crate-sync handler is covered by // supply-box-sync.int.spec.ts) with a real arma-sync-events row as the // required sourceEvent provenance anchor. async function makeBox(boxId: string): Promise { const raw = await payload.create({ collection: "arma-sync-events", data: { eventId: `${RUN}-evt-${boxId}`, server: serverId, type: "fixture", occurredAt: new Date().toISOString(), payload: [], }, overrideAccess: true, depth: 0, }); syncEventIds.push(raw.id); const box = (await payload.create({ collection: "supply-boxes", data: { boxId, sessionId: RUN, server: serverId, boxClass: "Box_NATO_Wps_F", displayName: "Ammo Crate", location: { x: 100, y: 200, z: 0 }, contents: [{ class: "Item_1", count: 10 }], lastSyncedAt: new Date().toISOString(), status: "active", sourceEvent: raw.id, }, overrideAccess: true, depth: 0, })) as unknown as SupplyBox; boxIds.push(box.id); return box; } async function findCommand(commandId: string): Promise { const found = await payload.find({ collection: "arma-commands", where: { commandId: { equals: commandId } }, limit: 1, depth: 0, overrideAccess: true, }); return (found.docs[0] as ArmaCommand | undefined) ?? null; } // --------------------------------------------------------------------------- // Setup / teardown // --------------------------------------------------------------------------- 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); commandUser = await makeUser("command", [superRole.id]); plainUser = await makeUser("plain"); const server = await payload.create({ collection: "game-servers", data: { serverId: `${RUN}-srv`, name: `${RUN} Server`, status: "online" }, overrideAccess: true, depth: 0, }); serverId = server.id; }); afterAll(async () => { if (!payload) return; for (const id of commandIds) { await payload.delete({ collection: "arma-commands", id, overrideAccess: true }).catch(() => {}); } for (const id of boxIds) { await payload.delete({ collection: "supply-boxes", id, overrideAccess: true }).catch(() => {}); } for (const id of syncEventIds) { await payload.delete({ collection: "arma-sync-events", 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(() => {}); } if (serverId) { await payload.delete({ collection: "game-servers", id: serverId, overrideAccess: true }).catch(() => {}); } }); // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- describe("supply box crate command actions", () => { describe("queueCrateCommand", () => { it("queues a crate.apply command with the contract payload for a qualified user", async () => { authSpy.mockResolvedValue({ user: commandUser }); const box = await makeBox(`box:${RUN}:apply`); const result = await queueCrateCommand(box.boxId, "apply", [ [ ["Item_1", 10], ["Item_2", 5], ], ]); expect(result.success).toBe(true); expect(result.data?.status).toBe("queued"); expect(result.data?.commandId).toMatch(/^crate-[0-9a-f]{24}$/); const cmd = await findCommand(result.data!.commandId); expect(cmd).not.toBeNull(); expect(cmd?.type).toBe("crate.apply"); expect(cmd?.server).toBe(serverId); expect(cmd?.status).toBe("queued"); expect(cmd?.payload).toEqual([ box.boxId, [ ["Item_1", 10], ["Item_2", 5], ], ]); commandIds.push(cmd!.id); }); it("rejects a user without the logistics qualification with a forbidden error", async () => { authSpy.mockResolvedValue({ user: plainUser }); const box = await makeBox(`box:${RUN}:forbidden`); const result = await queueCrateCommand(box.boxId, "despawn", []); expect(result.success).toBe(false); expect(result.error).toContain("forbidden"); }); it("maps an unknown box id to a not-found error", async () => { authSpy.mockResolvedValue({ user: commandUser }); const result = await queueCrateCommand(`box:${RUN}:missing`, "despawn", []); expect(result.success).toBe(false); expect(result.error).toContain("not found"); }); it("mints a unique command id per queue", async () => { authSpy.mockResolvedValue({ user: commandUser }); const box = await makeBox(`box:${RUN}:unique`); const first = await queueCrateCommand(box.boxId, "despawn", []); const second = await queueCrateCommand(box.boxId, "despawn", []); expect(first.success).toBe(true); expect(second.success).toBe(true); expect(first.data?.commandId).not.toBe(second.data?.commandId); const firstCmd = await findCommand(first.data!.commandId); const secondCmd = await findCommand(second.data!.commandId); expect(firstCmd).not.toBeNull(); expect(secondCmd).not.toBeNull(); expect(firstCmd?.id).not.toBe(secondCmd?.id); expect(firstCmd?.payload).toEqual([box.boxId]); expect(secondCmd?.payload).toEqual([box.boxId]); commandIds.push(firstCmd!.id, secondCmd!.id); }); }); });