import assert from "node:assert/strict"; import { mkdtempSync, mkdirSync, rmSync } from "node:fs"; import os from "node:os"; import path from "node:path"; import { randomBytes } from "node:crypto"; import test from "node:test"; import { createGameServer } from "./server.mjs"; function fixtureUsername() { return `card${process.pid.toString(36)}${randomBytes(4).toString("hex")}`; } async function startServer(dataDir) { const root = path.join(dataDir, "http"); const staticDir = path.join(root, "static"); const contentDir = path.join(root, "content"); mkdirSync(staticDir, { recursive: true }); mkdirSync(contentDir, { recursive: true }); const runtime = createGameServer({ staticDir, contentDir, dataDir, enableShelvedModes: true, corsOrigins: "https://native-test.invalid", }); await new Promise((resolve) => runtime.server.listen(0, "127.0.0.1", resolve)); return { runtime, origin: `http://127.0.0.1:${runtime.server.address().port}` }; } async function stopServer(runtime) { if (!runtime?.server.listening) return; await new Promise((resolve, reject) => { runtime.server.close((error) => error ? reject(error) : resolve()); }); } async function request(origin, route, { method = "GET", token, body } = {}) { const response = await fetch(`${origin}${route}`, { method, headers: { ...(token ? { Authorization: `Bearer ${token}` } : {}), ...(body === undefined ? {} : { "Content-Type": "application/json" }), }, ...(body === undefined ? {} : { body: JSON.stringify(body) }), }); return { status: response.status, body: await response.json() }; } async function register(origin, username, password) { const result = await request(origin, "/api/auth/register", { method: "POST", body: { username, password }, }); assert.equal(result.status, 201); return result.body; } test("cloud card PATCH checkpoints atomically per authenticated account and legacy PUT cannot erase cards", async () => { const tempDir = mkdtempSync(path.join(os.tmpdir(), "healer-boss-card-checkpoint-")); const firstPassword = `A${randomBytes(18).toString("base64url")}`; const secondPassword = `B${randomBytes(18).toString("base64url")}`; const firstUsername = fixtureUsername(); const secondUsername = fixtureUsername(); let runtime; try { let service = await startServer(tempDir); runtime = service.runtime; const unauthenticated = await request(service.origin, "/api/cloud-save", { method: "PATCH", body: { expectedRevision: 0, bossCards: {} }, }); assert.equal(unauthenticated.status, 401); const preflight = await fetch(`${service.origin}/api/cloud-save`, { method: "OPTIONS", headers: { Origin: "https://native-test.invalid", "Access-Control-Request-Method": "PATCH", "Access-Control-Request-Headers": "authorization,content-type", }, }); assert.equal(preflight.status, 204); assert.equal(preflight.headers.get("access-control-allow-origin"), "https://native-test.invalid"); assert.match(preflight.headers.get("access-control-allow-methods"), /\bPATCH\b/); const unauthorizedMalformed = await request(service.origin, "/api/cloud-save", { method: "PATCH", token: "not-a-valid-session-token", body: { expectedRevision: 0 }, }); assert.equal(unauthorizedMalformed.status, 401); const firstAccount = await register(service.origin, firstUsername, firstPassword); const secondAccount = await register(service.origin, secondUsername, secondPassword); assert.notEqual(firstAccount.account.id, secondAccount.account.id); assert.ok(firstAccount.token); assert.ok(secondAccount.token); const missingCards = await request(service.origin, "/api/cloud-save", { method: "PATCH", token: firstAccount.token, body: { expectedRevision: 0 }, }); assert.equal(missingCards.status, 400); assert.equal(missingCards.body.code, "invalid_boss_cards"); const initialFirst = await request(service.origin, "/api/cloud-save", { token: firstAccount.token }); const initialSecond = await request(service.origin, "/api/cloud-save", { token: secondAccount.token }); assert.equal(initialFirst.body.revision, 0); assert.deepEqual(initialFirst.body.data, { version: 1, characters: [] }); assert.equal(initialSecond.body.revision, 0); const rawCards = JSON.parse( '{"counts":{"__proto__":{"normal":9},"wow:55":{"normal":2.9,"rare":0,"epic":1.7,"legendary":9007199254740992,"unknown":50},"":{"normal":7}},"lastClaims":{"__proto__":"200:1","spawn:a":"100:2","ignored":42}}', ); const firstCheckpoint = await request(service.origin, "/api/cloud-save", { method: "PATCH", token: firstAccount.token, body: { accountId: secondAccount.account.id, expectedRevision: 0, bossCards: rawCards }, }); assert.equal(firstCheckpoint.status, 200); assert.equal(firstCheckpoint.body.revision, 1); assert.deepEqual(firstCheckpoint.body.data.version, 1); assert.deepEqual(firstCheckpoint.body.data.characters, []); assert.equal(firstCheckpoint.body.data.bossCards.counts["wow:55"].normal, 2); assert.equal(firstCheckpoint.body.data.bossCards.counts["wow:55"].epic, 1); assert.equal(firstCheckpoint.body.data.bossCards.counts["wow:55"].legendary, Number.MAX_SAFE_INTEGER); assert.equal(firstCheckpoint.body.data.bossCards.counts["wow:55"].rare, undefined); assert.equal(firstCheckpoint.body.data.bossCards.counts["wow:55"].unknown, undefined); assert.equal(firstCheckpoint.body.data.bossCards.counts[""], undefined); assert.equal(firstCheckpoint.body.data.bossCards.counts["__proto__"].normal, 9); assert.equal(firstCheckpoint.body.data.bossCards.lastClaims["__proto__"], "200:1"); assert.equal(firstCheckpoint.body.data.bossCards.lastClaims["spawn:a"], "100:2"); assert.equal(firstCheckpoint.body.data.bossCards.lastClaims.ignored, undefined); const conflict = await request(service.origin, "/api/cloud-save", { method: "PATCH", token: firstAccount.token, body: { expectedRevision: 0, bossCards: { counts: { "wow:55": { normal: 50 } } } }, }); assert.equal(conflict.status, 409); assert.equal(conflict.body.code, "revision_conflict"); const unchangedAfterConflict = await request(service.origin, "/api/cloud-save", { token: firstAccount.token }); assert.deepEqual(unchangedAfterConflict.body, firstCheckpoint.body); const invalidRevision = await request(service.origin, "/api/cloud-save", { method: "PATCH", token: firstAccount.token, body: { expectedRevision: 1.5, bossCards: {} }, }); assert.equal(invalidRevision.status, 400); assert.equal(invalidRevision.body.code, "invalid_revision"); const retried = await request(service.origin, "/api/cloud-save", { method: "PATCH", token: firstAccount.token, body: { expectedRevision: 1, bossCards: { counts: { "wow:55": { normal: 5, rare: 1 }, "wow:new": { epic: 1 } }, lastClaims: { "spawn:a": "100:1", "spawn:b": "alpha" }, }, }, }); assert.equal(retried.status, 200); assert.equal(retried.body.revision, 2); assert.equal(retried.body.data.bossCards.counts["wow:55"].normal, 5); assert.equal(retried.body.data.bossCards.counts["wow:55"].rare, 1); assert.equal(retried.body.data.bossCards.counts["wow:new"].epic, 1); assert.equal(retried.body.data.bossCards.lastClaims["spawn:a"], "100:2"); const otherCheckpoint = await request(service.origin, "/api/cloud-save", { method: "PATCH", token: secondAccount.token, body: { expectedRevision: 0, bossCards: { counts: { "rom:9": { rare: 3 } } } }, }); assert.equal(otherCheckpoint.status, 200); assert.equal(otherCheckpoint.body.revision, 1); assert.deepEqual(Object.keys(otherCheckpoint.body.data.bossCards.counts), ["rom:9"]); const firstStillIsolated = await request(service.origin, "/api/cloud-save", { token: firstAccount.token }); assert.equal(firstStillIsolated.body.revision, 2); assert.equal(firstStillIsolated.body.data.bossCards.counts["rom:9"], undefined); const stalePut = await request(service.origin, "/api/cloud-save", { method: "PUT", token: firstAccount.token, body: { data: { version: 2, characters: [{ id: "character-a" }], uiPreferences: { theme: "night" }, futureField: { preservedDuringThisWrite: true }, bossCards: { counts: { "wow:55": { normal: 1 }, "wow:legacy": { legendary: 1 } }, lastClaims: { "spawn:a": "99:9", "spawn:c": "claim-c" }, }, }, }, }); assert.equal(stalePut.status, 200); assert.equal(stalePut.body.revision, 3); const stalePutExpectedData = JSON.parse( '{"version":2,"characters":[{"id":"character-a"}],"uiPreferences":{"theme":"night"},"futureField":{"preservedDuringThisWrite":true},"bossCards":{"counts":{"__proto__":{"normal":9},"wow:55":{"normal":5,"rare":1,"epic":1,"legendary":9007199254740991},"wow:new":{"epic":1},"wow:legacy":{"legendary":1}},"lastClaims":{"__proto__":"200:1","spawn:a":"100:2","spawn:b":"alpha","spawn:c":"claim-c"}}}', ); assert.deepEqual(stalePut.body.data, stalePutExpectedData); assert.equal(stalePut.body.data.bossCards.counts["wow:55"].normal, 5); assert.equal(stalePut.body.data.bossCards.counts["wow:55"].rare, 1); assert.equal(stalePut.body.data.bossCards.counts["wow:legacy"].legendary, 1); assert.equal(stalePut.body.data.bossCards.lastClaims["spawn:a"], "100:2"); const omittedCardPut = await request(service.origin, "/api/cloud-save", { method: "PUT", token: firstAccount.token, body: { data: { version: 3, characters: [], custom: "legacy replacement" } }, }); assert.equal(omittedCardPut.status, 200); assert.equal(omittedCardPut.body.revision, 4); assert.deepEqual(omittedCardPut.body.data, { version: 3, characters: [], custom: "legacy replacement", bossCards: stalePut.body.data.bossCards, }); await stopServer(runtime); runtime = null; service = await startServer(tempDir); runtime = service.runtime; const firstLogin = await request(service.origin, "/api/auth/login", { method: "POST", body: { username: firstUsername, password: firstPassword }, }); const secondLogin = await request(service.origin, "/api/auth/login", { method: "POST", body: { username: secondUsername, password: secondPassword }, }); assert.equal(firstLogin.status, 200); assert.equal(secondLogin.status, 200); const reopenedFirst = await request(service.origin, "/api/cloud-save", { token: firstLogin.body.token }); const reopenedSecond = await request(service.origin, "/api/cloud-save", { token: secondLogin.body.token }); assert.deepEqual(reopenedFirst.body.data, omittedCardPut.body.data); assert.deepEqual(reopenedSecond.body.data.bossCards.counts, { "rom:9": { rare: 3 } }); const concurrent = await Promise.all([0, 1].map(() => request(service.origin, "/api/cloud-save", { method: "PATCH", token: firstLogin.body.token, body: { expectedRevision: 4, bossCards: stalePut.body.data.bossCards }, }))); assert.deepEqual(concurrent.map((result) => result.status).sort(), [200, 409]); assert.equal(concurrent.find((result) => result.status === 409).body.code, "revision_conflict"); const afterConcurrentCheckpoint = await request(service.origin, "/api/cloud-save", { token: firstLogin.body.token }); assert.equal(afterConcurrentCheckpoint.body.revision, 5); assert.deepEqual(afterConcurrentCheckpoint.body.data, omittedCardPut.body.data); const duplicateCheckpoint = await request(service.origin, "/api/cloud-save", { method: "PATCH", token: firstLogin.body.token, body: { expectedRevision: 5, bossCards: stalePut.body.data.bossCards }, }); assert.equal(duplicateCheckpoint.status, 200); assert.equal(duplicateCheckpoint.body.revision, 6); assert.deepEqual(duplicateCheckpoint.body.data.bossCards, omittedCardPut.body.data.bossCards); } finally { await stopServer(runtime); rmSync(tempDir, { recursive: true, force: true }); } });