import { getPayload, Payload } from "payload"; import config from "@/payload.config"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import type { GameServer } from "@/payload-types"; import { markStaleServersOffline, SERVER_OFFLINE_AFTER_MS } from "@/lib/arma-bridge/presence"; let payload: Payload; const RUN = `srv-${Date.now().toString(36)}`; describe("Server presence sweep", () => { let now: Date; const serverIds: number[] = []; beforeAll(async () => { const payloadConfig = await config; payload = await getPayload({ config: payloadConfig }); now = new Date(); now.setMilliseconds(0); }); afterAll(async () => { for (const id of serverIds) { await payload.delete({ collection: "game-servers", id, overrideAccess: true }).catch(() => {}); } const events = await payload .find({ collection: "game-event-logs", where: { and: [{ type: { equals: "server:offline" } }, { targetId: { in: serverIds } }] }, limit: 50, depth: 0, overrideAccess: true, }) .catch(() => null); for (const event of events?.docs ?? []) { await payload .delete({ collection: "game-event-logs", id: event.id, overrideAccess: true }) .catch(() => {}); } }); const makeServer = async ( label: string, data: { status?: GameServer["status"]; lastSeenAt?: Date }, ): Promise => { const server = (await payload.create({ collection: "game-servers", data: { serverId: `${RUN}-${label}`, name: `${RUN} ${label}`, status: data.status ?? "online", lastSeenAt: data.lastSeenAt ? data.lastSeenAt.toISOString() : undefined, }, overrideAccess: true, depth: 0, })) as unknown as GameServer; serverIds.push(server.id); return server; }; const fetchServer = (id: number): Promise => payload.findByID({ collection: "game-servers", id, depth: 0, overrideAccess: true, }) as unknown as Promise; it("flips online servers with no recent heartbeat to offline and leaves the rest alone", async () => { const fresh = await makeServer("fresh", { lastSeenAt: new Date(now.getTime() - 60_000) }); const stale = await makeServer("stale", { lastSeenAt: new Date(now.getTime() - (SERVER_OFFLINE_AFTER_MS + 1_000)), }); const neverSeen = await makeServer("never-seen", {}); const offlineStale = await makeServer("offline-stale", { status: "offline", lastSeenAt: new Date(now.getTime() - 3_600_000), }); const boundary = await makeServer("boundary", { lastSeenAt: new Date(now.getTime() - SERVER_OFFLINE_AFTER_MS), }); // Scope to this run's fixtures so the sweep never flips other servers in the // shared dev database (stale real servers would be flipped legitimately). const result = await markStaleServersOffline(payload, now, { serverIds }); expect(result.markedOffline).toBe(2); expect(result.servers.map((s) => s.id).sort()).toEqual([stale.id, neverSeen.id].sort()); expect(result.servers.find((s) => s.id === stale.id)?.serverId).toBe(`${RUN}-stale`); expect((await fetchServer(fresh.id)).status).toBe("online"); expect((await fetchServer(stale.id)).status).toBe("offline"); expect((await fetchServer(neverSeen.id)).status).toBe("offline"); expect((await fetchServer(offlineStale.id)).status).toBe("offline"); // Exactly at the cutoff the server is still within its heartbeat window. expect((await fetchServer(boundary.id)).status).toBe("online"); }); it("logs a server:offline event per flipped server", async () => { const events = await payload.find({ collection: "game-event-logs", where: { and: [{ type: { equals: "server:offline" } }, { targetId: { in: serverIds } }], }, limit: 10, depth: 0, overrideAccess: true, }); expect(events.docs).toHaveLength(2); }); it("is idempotent — a second sweep finds nothing left to flip", async () => { const result = await markStaleServersOffline(payload, now, { serverIds }); expect(result.markedOffline).toBe(0); expect(result.servers).toEqual([]); }); });