118 lines
4.2 KiB
TypeScript
118 lines
4.2 KiB
TypeScript
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<GameServer> => {
|
|
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<GameServer> =>
|
|
payload.findByID({
|
|
collection: "game-servers",
|
|
id,
|
|
depth: 0,
|
|
overrideAccess: true,
|
|
}) as unknown as Promise<GameServer>;
|
|
|
|
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([]);
|
|
});
|
|
});
|