259 lines
No EOL
8.6 KiB
TypeScript
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);
|
|
});
|
|
}); |