1
0
Fork 0
polaris-task-force/tests/int/supply-box-sync.int.spec.ts

259 lines
No EOL
8.6 KiB
TypeScript

import { getPayload, Payload } from "payload";
import config from "@/payload.config";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import type { GameServer, SupplyBox } from "@/payload-types";
import { processBoxEvent, sessionIdFromBoxId } from "@/lib/arma-bridge/crates";
let payload: Payload;
const RUN = `t5-${Date.now().toString(36)}`;
describe("Supply box crate sync", () => {
let serverId: number | null = null;
const syncEventIds: number[] = [];
const opEventIds: number[] = [];
const boxIds: number[] = [];
beforeAll(async () => {
const payloadConfig = await config;
payload = await getPayload({ config: payloadConfig });
const server = (await payload.create({
collection: "game-servers",
data: {
serverId: `${RUN}-srv`,
name: `${RUN} Server`,
status: "online",
},
overrideAccess: true,
depth: 0,
})) as unknown as GameServer;
serverId = server.id;
});
afterAll(async () => {
if (!payload) return;
for (const id of boxIds) {
await payload.delete({ collection: "supply-boxes", id, overrideAccess: true }).catch(() => {});
}
for (const id of opEventIds) {
await payload.delete({ collection: "operation-events", id, overrideAccess: true }).catch(() => {});
}
for (const id of syncEventIds) {
await payload.delete({ collection: "arma-sync-events", id, overrideAccess: true }).catch(() => {});
}
if (serverId) {
await payload.delete({ collection: "game-servers", id: serverId, overrideAccess: true }).catch(() => {});
}
});
// Creates a raw arma-sync-events row as a provenance anchor (the supply-boxes
// sourceEvent relationship requires a real row id) with a neutral type so the
// afterChange hook does not process it as a box event, then runs the handler
// directly against it.
async function emitRaw(eventId: string, type: string, eventPayload: unknown[]): Promise<number> {
const syncDoc = await payload.create({
collection: "arma-sync-events",
data: {
eventId,
server: serverId!,
type,
occurredAt: new Date().toISOString(),
payload: eventPayload,
},
overrideAccess: true,
depth: 0,
});
syncEventIds.push(syncDoc.id);
const opEvents = await payload.find({
collection: "operation-events",
where: { sourceEvent: { equals: syncDoc.id } },
limit: 1,
depth: 0,
overrideAccess: true,
});
for (const doc of opEvents.docs) opEventIds.push(doc.id);
return syncDoc.id;
}
async function runBoxEvent(eventId: string, type: string, eventPayload: unknown[]): Promise<number> {
const rawId = await emitRaw(eventId, "fixture", []);
await processBoxEvent(payload, {
id: rawId,
eventId,
server: serverId!,
type,
occurredAt: new Date().toISOString(),
payload: eventPayload,
});
return rawId;
}
async function fetchBox(boxId: string): Promise<SupplyBox | null> {
const found = await payload.find({
collection: "supply-boxes",
where: { boxId: { equals: boxId } },
limit: 1,
depth: 0,
overrideAccess: true,
});
if (found.docs.length === 0) return null;
boxIds.push(found.docs[0].id);
return found.docs[0] as unknown as SupplyBox;
}
it("registers a box with provenance from a box.register event", async () => {
const boxId = `box:${RUN}-reg:1`;
const rawId = await runBoxEvent(`${RUN}-evt-reg`, "box.register", [
boxId,
"Box_NATO_Wps_F",
"Ammo Crate",
100,
200,
0,
]);
const box = await fetchBox(boxId);
expect(box).not.toBeNull();
expect(box!.boxClass).toBe("Box_NATO_Wps_F");
expect(box!.displayName).toBe("Ammo Crate");
expect(box!.location).toEqual({ x: 100, y: 200, z: 0 });
expect(box!.server).toBe(serverId);
expect(box!.status).toBe("active");
expect(box!.sessionId).toBe(`${RUN}-reg`);
expect(box!.sourceEvent).toBe(rawId);
expect(box!.lastSyncedAt).toBeTruthy();
});
it("updates contents and lastSyncedAt from a box.sync event", async () => {
const boxId = `box:${RUN}-sync:1`;
await runBoxEvent(`${RUN}-evt-sync-reg`, "box.register", [boxId, "Box_NATO_Wps_F", "Ammo Crate", 1, 2, 3]);
const before = await fetchBox(boxId);
expect(before).not.toBeNull();
await runBoxEvent(`${RUN}-evt-sync`, "box.sync", [
boxId,
[
["Item_1", 10],
["Item_2", 5],
],
]);
const after = await fetchBox(boxId);
expect(after).not.toBeNull();
expect(after!.contents).toEqual([
{ class: "Item_1", count: 10 },
{ class: "Item_2", count: 5 },
]);
expect(new Date(after!.lastSyncedAt).getTime()).toBeGreaterThanOrEqual(
new Date(before!.lastSyncedAt).getTime(),
);
});
it("is idempotent when the same box event is applied twice", async () => {
const boxId = `box:${RUN}-idem:1`;
const payloadArgs = [boxId, "Box_NATO_Wps_F", "Ammo Crate", 1, 2, 3] as unknown[];
await runBoxEvent(`${RUN}-evt-idem-a`, "box.register", payloadArgs);
await runBoxEvent(`${RUN}-evt-idem-b`, "box.register", payloadArgs);
const found = await payload.find({
collection: "supply-boxes",
where: { boxId: { equals: boxId } },
limit: 10,
depth: 0,
overrideAccess: true,
});
expect(found.docs).toHaveLength(1);
for (const doc of found.docs) boxIds.push(doc.id);
});
it("no-ops on malformed payloads without throwing", async () => {
await runBoxEvent(`${RUN}-evt-malformed-1`, "box.sync", "not-an-array" as unknown as unknown[]);
await runBoxEvent(`${RUN}-evt-malformed-2`, "box.register", []);
await runBoxEvent(`${RUN}-evt-malformed-3`, "box.register", ["not-a-box-id", "Box_NATO_Wps_F", "X", 1, 2, 3]);
const found = await payload.find({
collection: "supply-boxes",
where: { boxId: { like: `${RUN}-malformed` } },
limit: 10,
depth: 0,
overrideAccess: true,
});
expect(found.docs).toHaveLength(0);
});
it("marks a box deleted and reactivates it on a later sync", async () => {
const boxId = `box:${RUN}-del:1`;
await runBoxEvent(`${RUN}-evt-del-reg`, "box.register", [boxId, "Box_NATO_Wps_F", "Ammo Crate", 1, 2, 3]);
await runBoxEvent(`${RUN}-evt-del`, "box.deleted", [boxId]);
const deleted = await fetchBox(boxId);
expect(deleted).not.toBeNull();
expect(deleted!.status).toBe("deleted");
await runBoxEvent(`${RUN}-evt-del-sync`, "box.sync", [boxId, [["Item_1", 3]]]);
const reactivated = await fetchBox(boxId);
expect(reactivated).not.toBeNull();
expect(reactivated!.status).toBe("active");
expect(reactivated!.contents).toEqual([{ class: "Item_1", count: 3 }]);
});
it("no-ops when the raw event has no server", async () => {
const rawId = await emitRaw(`${RUN}-evt-null-server`, "fixture", []);
await processBoxEvent(payload, {
id: rawId,
eventId: `${RUN}-evt-null-server`,
server: null,
type: "box.register",
occurredAt: new Date().toISOString(),
payload: [`box:${RUN}-null:1`, "Box_NATO_Wps_F", "X", 1, 2, 3],
});
const found = await payload.find({
collection: "supply-boxes",
where: { boxId: { equals: `box:${RUN}-null:1` } },
limit: 1,
depth: 0,
overrideAccess: true,
});
expect(found.docs).toHaveLength(0);
});
it("extracts the session id from the box id", () => {
expect(sessionIdFromBoxId("box:Altis-20260921120000:42")).toBe("Altis-20260921120000");
expect(sessionIdFromBoxId("box:Altis:1")).toBe("Altis");
expect(sessionIdFromBoxId("not-a-box-id")).toBeNull();
expect(sessionIdFromBoxId("box:Altis-1:abc")).toBeNull();
expect(sessionIdFromBoxId("box::1")).toBeNull();
});
it("wires crate sync through the afterChange hook", async () => {
const boxId = `box:${RUN}-hook:1`;
const raw = await payload.create({
collection: "arma-sync-events",
data: {
eventId: `${RUN}-evt-hook`,
server: serverId!,
type: "box.register",
occurredAt: new Date().toISOString(),
payload: [boxId, "Box_NATO_Wps_F", "Hook Crate", 10, 20, 30],
},
overrideAccess: true,
depth: 0,
});
syncEventIds.push(raw.id);
const opEvents = await payload.find({
collection: "operation-events",
where: { sourceEvent: { equals: raw.id } },
limit: 1,
depth: 0,
overrideAccess: true,
});
for (const doc of opEvents.docs) opEventIds.push(doc.id);
const box = await fetchBox(boxId);
expect(box).not.toBeNull();
expect(box!.boxClass).toBe("Box_NATO_Wps_F");
expect(box!.sourceEvent).toBe(raw.id);
});
});