Files
healer-man/server/boss-card-cloud-save.test.mjs
T

246 lines
12 KiB
JavaScript

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 });
}
});