import { getPayload, Payload } from "payload"; import config from "@/payload.config"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import type { GameStructure, GameVehicle, OperationReservation, Resource, Structure, User, } from "@/payload-types"; import { listReservationOptions } from "@/lib/operations/reservationOptions"; import { createAccount } from "@/lib/banking"; import { storedAmount } from "@/lib/base/storage"; let payload: Payload; const RUN = `res-options-${Date.now().toString(36)}`; // --------------------------------------------------------------------------- // Fixture tracking for cleanup // --------------------------------------------------------------------------- const reservationIds: number[] = []; const userIds: number[] = []; const roleIds: number[] = []; const accountIds: number[] = []; const gameStructureIds: number[] = []; const blueprintIds: number[] = []; const gameVehicleIds: number[] = []; const vehicleBlueprintIds: number[] = []; const resourceIds: number[] = []; const missionIds: number[] = []; const campaignIds: number[] = []; const mapIds: number[] = []; let commandUser: User; let plainUser: User; let mapId: number; let campaignId: number; let resourceAId: number; let resourceBId: number; let normalBlueprintId: number; let vehicleBlueprintId: number; const LEXICAL_EMPTY = { root: { children: [{ text: "" }], direction: null, format: "" as const, indent: 0, type: "text", version: 1, }, }; 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 { const user = (await payload.create({ collection: "users", data: { username: `${RUN}-${label}-${Math.random().toString(36).slice(2, 8)}`, 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; } async function makeMission( status: "Ready" | "Scheduled" | "Completed" | "Cancelled", visibility: "unit" | "leadership", ): Promise { const mission = await payload.create({ collection: "missions", data: { name: `${RUN} ${status} ${visibility}`, codeName: `${RUN}-${status}-${visibility}`, summary: "Options test mission", operationType: "main", classification: { map: mapId, missionType: "PvE", campaign: campaignId, startDateTime: new Date(Date.now() + 86_400_000).toISOString(), estimatedDuration: 60, }, ownershipAndStatus: { authors: [commandUser.id], status, visibility, }, missionRoles: { maxPlayers: 16 }, gameDetails: { serverDetails: { serverIp: "127.0.0.1", serverPort: 2302 } }, briefing: [], }, overrideAccess: true, depth: 0, }); missionIds.push(mission.id); return mission.id; } async function makeOrigin(stock: { resourceId: number; amount: number }[]): Promise { const site = await payload.create({ collection: "game-structures", data: { name: `${RUN} origin ${gameStructureIds.length}`, type: normalBlueprintId, map: mapId, coordinates: [100 + gameStructureIds.length, 100], constructionStatus: "complete", storedResources: stock.map((s, i) => ({ resource: s.resourceId, amount: s.amount, gridX: 0, gridY: i, rotated: false, })), }, overrideAccess: true, depth: 0, }); gameStructureIds.push(site.id); return site.id; } async function makeVehicle(status: "idle" | "assigned"): Promise { const vehicle = await payload.create({ collection: "game-vehicles", data: { name: `${RUN} vic ${gameVehicleIds.length}`, type: vehicleBlueprintId, deployedAt: gameStructureIds[0], status, currentFuel: 800, currentHealth: 100, }, overrideAccess: true, depth: 0, }); gameVehicleIds.push(vehicle.id); return vehicle.id; } // --------------------------------------------------------------------------- // Setup / teardown // --------------------------------------------------------------------------- beforeAll(async () => { const payloadConfig = await config; payload = await getPayload({ config: payloadConfig }); 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 map = await payload.create({ collection: "maps", data: { name: `${RUN} Map`, worldSizeWidth: 8192, worldSizeHeight: 8192, basemapMode: "image" }, overrideAccess: true, depth: 0, }); mapId = map.id; mapIds.push(map.id); const campaign = await payload.create({ collection: "campaigns", data: { name: `${RUN} Campaign`, summary: "Options test campaign", status: "concept", campaignMode: "custom", }, overrideAccess: true, depth: 0, }); campaignId = campaign.id; campaignIds.push(campaign.id); const resourceA = await payload.create({ collection: "resources", data: { name: `${RUN} Alpha`, codeName: `${RUN}-alpha`, approvalStatus: "in_progress", type: "physical", baseValue: 1, rarity: "common", unitOfMeasure: "kg", massPerUnit: 1, gridWidth: 1, gridHeight: 1, }, overrideAccess: true, depth: 0, }); resourceAId = resourceA.id; resourceIds.push(resourceA.id); const resourceB = await payload.create({ collection: "resources", data: { name: `${RUN} Beta`, codeName: `${RUN}-beta`, approvalStatus: "in_progress", type: "physical", baseValue: 1, rarity: "common", unitOfMeasure: "kg", massPerUnit: 1, gridWidth: 1, gridHeight: 1, }, overrideAccess: true, depth: 0, }); resourceBId = resourceB.id; resourceIds.push(resourceB.id); const normalBlueprint = await payload.create({ collection: "structures", data: { name: `${RUN} Depot`, codeName: `${RUN}-depot`, approvalStatus: "in_progress", description: LEXICAL_EMPTY as unknown as Structure["description"], category: "logistics", materials: [{ resource: resourceAId, amount: 1 }], constructionDurationMinutes: 1, terrainType: "land", maxHealth: 100, }, overrideAccess: true, depth: 0, }); normalBlueprintId = normalBlueprint.id; blueprintIds.push(normalBlueprint.id); const vehicleBlueprint = await payload.create({ collection: "vehicles", data: { name: `${RUN} Truck`, approvalStatus: "in_progress", transportMode: "ground", maxSpeedOnRoad: 60, fuel: { fuelType: resourceAId, fuelCapacity: 1000, fuelConsumptionRate: 0.01 }, }, overrideAccess: true, depth: 0, }); vehicleBlueprintId = vehicleBlueprint.id; vehicleBlueprintIds.push(vehicleBlueprint.id); }); afterAll(async () => { if (!payload) return; for (const id of reservationIds) { await payload.delete({ collection: "operation-reservations", id, overrideAccess: true }).catch(() => {}); } for (const id of gameVehicleIds) { await payload.delete({ collection: "game-vehicles", id, overrideAccess: true }).catch(() => {}); } for (const id of gameStructureIds) { await payload.delete({ collection: "game-structures", id, overrideAccess: true }).catch(() => {}); } for (const id of vehicleBlueprintIds) { await payload.delete({ collection: "vehicles", id, overrideAccess: true }).catch(() => {}); } for (const id of blueprintIds) { await payload.delete({ collection: "structures", id, overrideAccess: true }).catch(() => {}); } for (const id of resourceIds) { await payload.delete({ collection: "resources", id, overrideAccess: true }).catch(() => {}); } for (const id of missionIds) { await payload.delete({ collection: "missions", id, overrideAccess: true }).catch(() => {}); } for (const id of campaignIds) { await payload.delete({ collection: "campaigns", id, overrideAccess: true }).catch(() => {}); } for (const id of mapIds) { await payload.delete({ collection: "maps", id, overrideAccess: true }).catch(() => {}); } for (const id of accountIds) { const txns = await payload.find({ collection: "bank-transactions", where: { or: [{ fromAccount: { equals: id } }, { toAccount: { equals: id } }] }, limit: 100, depth: 0, overrideAccess: true, }); for (const txn of txns.docs) { await payload.delete({ collection: "bank-transactions", id: txn.id, overrideAccess: true }).catch(() => {}); } await payload.delete({ collection: "bank-accounts", 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(() => {}); } }); // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- describe("listReservationOptions", () => { it("returns only Ready/Scheduled unit-visible missions, sorted by start", async () => { const scheduledId = await makeMission("Scheduled", "unit"); const readyId = await makeMission("Ready", "unit"); const completedId = await makeMission("Completed", "unit"); const leadershipId = await makeMission("Scheduled", "leadership"); const options = await listReservationOptions(payload, commandUser); const ids = options.upcomingMissions.map((m) => m.id); expect(ids).toContain(scheduledId); expect(ids).toContain(readyId); expect(ids).not.toContain(completedId); expect(ids).not.toContain(leadershipId); }); it("excludes non-idle vehicles and vehicles already reserved", async () => { const idleId = await makeVehicle("idle"); const assignedId = await makeVehicle("assigned"); const reservedId = await makeVehicle("idle"); // Reserve the second idle vehicle via an active (reserved) reservation row. const row = await payload.create({ collection: "operation-reservations", data: { reservationKey: `${RUN}-reserved-veh`, operationId: `${RUN}-op`, status: "reserved", vehicles: [{ vehicle: reservedId }], createdBy: commandUser.id, }, overrideAccess: true, depth: 0, }); reservationIds.push(row.id); const options = await listReservationOptions(payload, commandUser); const vehicleIds = options.vehicles.map((v) => v.id); expect(vehicleIds).toContain(idleId); expect(vehicleIds).not.toContain(assignedId); expect(vehicleIds).not.toContain(reservedId); }); it("reflects storedAmount per origin in stockByOrigin", async () => { const originId = await makeOrigin([ { resourceId: resourceAId, amount: 100 }, { resourceId: resourceBId, amount: 50 }, ]); const options = await listReservationOptions(payload, commandUser); const stock = options.stockByOrigin[String(originId)]; expect(stock).toHaveLength(2); const origin = (await payload .findByID({ collection: "game-structures", id: originId, depth: 0, overrideAccess: true }) .catch(() => null)) as unknown as (GameStructure & { storedResources?: unknown[]; voidStorage?: unknown[]; }) | null; const entries = [ ...(origin?.storedResources ?? []), ...(origin?.voidStorage ?? []), ] as Array<{ resource: { id: number }; amount: number }>; const amountA = storedAmount(entries as never, resourceAId); const amountB = storedAmount(entries as never, resourceBId); expect(amountA).toBe(100); expect(amountB).toBe(50); const lineA = stock!.find((s) => s.resourceId === resourceAId); const lineB = stock!.find((s) => s.resourceId === resourceBId); expect(lineA?.amount).toBe(amountA); expect(lineB?.amount).toBe(amountB); expect(lineA?.resourceName).toBeTruthy(); expect(lineB?.resourceName).toBeTruthy(); }); it("caps the roster pool at 200 users", async () => { for (let i = 0; i < 205; i++) { await makeUser(`roster${i}`); } const options = await listReservationOptions(payload, commandUser); expect(options.rosterUsers.length).toBe(200); // Every entry carries a display name and id; nothing else leaks. for (const user of options.rosterUsers) { expect(typeof user.id).toBe("number"); expect(user.displayName.length).toBeGreaterThan(0); } }, 120000); it("scopes budget accounts to treasury + this user's personal accounts, no balances", async () => { const treasury = await createAccount(payload, { name: `${RUN} Treasury`, accountType: "treasury" }); accountIds.push(treasury.id); const commandPersonal = await createAccount(payload, { name: `${RUN} Command Personal`, accountType: "personal", ownerUserId: commandUser.id, }); accountIds.push(commandPersonal.id); const plainPersonal = await createAccount(payload, { name: `${RUN} Plain Personal`, accountType: "personal", ownerUserId: plainUser.id, }); accountIds.push(plainPersonal.id); const options = await listReservationOptions(payload, commandUser); const ids = options.budgetAccounts.map((a) => a.id); expect(ids).toContain(treasury.id); expect(ids).toContain(commandPersonal.id); // Another user's personal account is out of scope. expect(ids).not.toContain(plainPersonal.id); // Only id, name, and type are exposed - balances never leak. for (const account of options.budgetAccounts) { const keys = Object.keys(account).sort(); expect(keys).toEqual(["id", "name", "type"]); } }); });