Files
healer-man/server/dungeon-reports.test.mjs
T
2026-09-20 18:08:43 -04:00

174 lines
12 KiB
JavaScript

import assert from "node:assert/strict";
import { mkdtempSync, rmSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import { openGameDatabase } from "./database.mjs";
import { createGameServer } from "./server.mjs";
async function withDatabase(run) {
const directory = mkdtempSync(path.join(os.tmpdir(), "healer-man-reports-test-"));
const database = openGameDatabase({ dataDir: directory });
try { await run(database); } finally { database.close(); rmSync(directory, { recursive: true, force: true }); }
}
test("validates comments and dungeon IDs and stamps new reports as Submitted", () => withDatabase(async (database) => {
assert.deepEqual(database.listDungeonReports(), { reports: [], openCounts: {} });
for (const comment of ["", " \n ", "x".repeat(501), null, 123]) {
assert.throws(() => database.submitDungeonReport(null, { dungeonId: "stormwind-stockade", comment }), { code: "invalid_report_comment" });
}
assert.throws(() => database.submitDungeonReport(null, { dungeonId: "fake", comment: "Broken" }), { code: "invalid_dungeon" });
const account = (await database.register("Reporter", "report-pass")).account;
const report = database.submitDungeonReport(account, {
dungeonId: "stormwind-stockade", comment: "x".repeat(500), status: "Closed", submittedBy: "Phenom",
});
assert.equal(report.comment.length, 500);
assert.equal(report.status, "Submitted");
assert.equal(report.submittedBy, "Reporter");
assert.equal(database.submitDungeonReport(null, { dungeonId: "forsaken-abbey", comment: " Door is stuck\r\nAfter a wipe " }).comment, "Door is stuck\nAfter a wipe");
assert.equal(database.submitDungeonReport(null, { dungeonId: "bite-sized-stormwind-stockade", comment: "Wall issue" }).status, "Submitted");
assert.equal(database.submitDungeonReport(null, { dungeonId: "randomized-bite-sized-stormwind-stockade", comment: "Random mob issue" }).status, "Submitted");
assert.deepEqual(database.listDungeonReports().openCounts, {
"bite-sized-stormwind-stockade": 1,
"forsaken-abbey": 1,
"randomized-bite-sized-stormwind-stockade": 1,
"stormwind-stockade": 1,
});
}));
test("enforces GM updates, counts all open statuses, supports reopening, and prevents stale changes", () => withDatabase(async (database) => {
const gm = (await database.register("Phenom", "report-admin")).account;
const player = (await database.register("Tester", "report-player")).account;
let report = database.submitDungeonReport(player, { dungeonId: "stormwind-stockade", comment: "Gate is stuck" });
database.submitDungeonReport(player, { dungeonId: "wailing-caverns", comment: "Boss stuck" });
assert.throws(() => database.updateDungeonReport({ ...player, roles: ["gm"] }, report.id, { status: "Closed", expectedRevision: 0 }), { status: 403 });
assert.throws(() => database.updateDungeonReport(gm, report.id, { status: "Other", expectedRevision: 0 }), { code: "invalid_report_status" });
assert.throws(() => database.updateDungeonReport(gm, report.id, { status: "Closed" }), { code: "invalid_revision" });
assert.throws(() => database.updateDungeonReport(gm, "missing", { status: "Closed", expectedRevision: 0 }), { status: 404 });
for (const status of ["In-Progress", "Testing Fixes"]) {
report = database.updateDungeonReport(gm, report.id, { status, expectedRevision: report.revision });
assert.equal(report.updatedBy, "Phenom");
assert.equal(database.listDungeonReports().openCounts[report.dungeonId], 1);
}
assert.throws(() => database.updateDungeonReport(gm, report.id, { status: "Closed", expectedRevision: 0 }), { status: 409 });
report = database.updateDungeonReport(gm, report.id, { status: "Closed", expectedRevision: report.revision });
assert.deepEqual(database.listDungeonReports().openCounts, { "wailing-caverns": 1 });
assert.equal(database.listDungeonReports().reports.length, 2);
const filtered = database.listDungeonReports({ status: "Closed", dungeonId: report.dungeonId });
assert.equal(filtered.reports.length, 1);
assert.deepEqual(filtered.openCounts, { "wailing-caverns": 1 });
report = database.updateDungeonReport(gm, report.id, { status: "Submitted", expectedRevision: report.revision });
assert.equal(database.listDungeonReports().openCounts[report.dungeonId], 1);
}));
test("persists reports and status changes across database restarts", async () => {
const directory = mkdtempSync(path.join(os.tmpdir(), "healer-man-report-restart-"));
let database;
try {
database = openGameDatabase({ dataDir: directory });
const gm = (await database.register("Phenom", "report-admin")).account;
const report = database.submitDungeonReport(null, { dungeonId: "stormwind-stockade", comment: "Entrance issue" });
const updated = database.updateDungeonReport(gm, report.id, { status: "Testing Fixes", expectedRevision: 0 });
database.close();
database = openGameDatabase({ dataDir: directory });
assert.deepEqual(database.listDungeonReports().reports, [updated]);
assert.equal(database.listDungeonReports().openCounts[report.dungeonId], 1);
} finally { database?.close(); rmSync(directory, { recursive: true, force: true }); }
});
test("persists boss readiness and structured boss reports without changing ordinary reports", async () => {
const directory = mkdtempSync(path.join(os.tmpdir(), "healer-man-boss-feedback-"));
let database;
try {
database = openGameDatabase({ dataDir: directory });
const boss = { dungeonId: "wailing-caverns", bossKey: "wailing-caverns/lady-anacondra", bossName: "Lady Anacondra" };
for (const status of ["ready", "needs-testing", "not-ready"]) {
assert.equal(database.saveBossTestingReview(null, { ...boss, status }).status, status);
}
assert.throws(() => database.saveBossTestingReview(null, { ...boss, status: "Closed" }), { code: "invalid_boss_status" });
assert.throws(() => database.saveBossTestingReview(null, { ...boss, bossKey: "other/boss", status: "ready" }), { code: "invalid_boss" });
const report = database.submitDungeonReport(null, { ...boss, comment: "x".repeat(500) });
assert.equal(report.bossKey, boss.bossKey);
assert.equal(report.bossName, boss.bossName);
const reviews = database.listBossTestingReviews();
database.close();
database = openGameDatabase({ dataDir: directory });
assert.deepEqual(database.listBossTestingReviews(), reviews);
assert.deepEqual(database.listDungeonReports().reports, [report]);
const ordinary = database.submitDungeonReport(null, { dungeonId: boss.dungeonId, comment: "Door issue" });
assert.equal(ordinary.bossKey, undefined);
} finally { database?.close(); rmSync(directory, { recursive: true, force: true }); }
});
test("persists randomized mob and boss readiness and requires a server-authenticated GM", () => withDatabase(async (database) => {
const gm = (await database.register("Phenom", "readiness-admin")).account;
const player = (await database.register("ReadinessPlayer", "readiness-player")).account;
const creature = {
creatureKey: "wow:mob:3868",
dungeonId: "randomized-bite-sized-stormwind-stockade",
creatureName: "Defias Convict",
creatureKind: "mob",
};
assert.throws(() => database.saveRandomizedCreatureReadiness(player, { ...creature, status: "ready" }), { status: 403 });
for (const status of ["ready", "needs-testing", "not-ready"]) {
assert.equal(database.saveRandomizedCreatureReadiness(gm, { ...creature, status }).status, status);
}
assert.throws(() => database.saveRandomizedCreatureReadiness(gm, { ...creature, status: "Closed" }), { code: "invalid_creature_status" });
assert.throws(() => database.saveRandomizedCreatureReadiness(gm, { ...creature, creatureKind: "pet", status: "ready" }), { code: "invalid_creature" });
assert.deepEqual(database.listRandomizedCreatureReadiness().reviews[0], {
...creature,
status: "not-ready",
reviewedBy: "Phenom",
reviewedAt: database.listRandomizedCreatureReadiness().reviews[0].reviewedAt,
});
}));
test("serves public reports and GM-only status API with shelved modes disabled", async () => {
const directory = mkdtempSync(path.join(os.tmpdir(), "healer-man-report-api-"));
const runtime = createGameServer({ dataDir: directory, enableShelvedModes: false });
await new Promise((resolve) => runtime.server.listen(0, "127.0.0.1", resolve));
const origin = `http://127.0.0.1:${runtime.server.address().port}`;
const request = (route, method = "GET", body, token) => fetch(`${origin}/api/dungeon-reports${route}`, {
method, headers: { "Content-Type": "application/json", ...(token ? { Authorization: `Bearer ${token}` } : {}) },
...(body === undefined ? {} : { body: JSON.stringify(body) }),
});
try {
const player = await runtime.database.register("ApiPlayer", "report-player");
const boss = { dungeonId: "wailing-caverns", bossKey: "wailing-caverns/lady-anacondra", bossName: "Lady Anacondra", status: "ready" };
const saved = await fetch(`${origin}/api/boss-testing/reviews`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(boss) });
assert.equal(saved.status, 200);
const labels = await (await fetch(`${origin}/api/boss-testing/reviews`)).json();
assert.equal(labels.reviews[0].status, "ready");
const gm = await runtime.database.register("Phenom", "report-admin");
const creature = { creatureKey: "wow:mob:3868", dungeonId: "randomized-bite-sized-stormwind-stockade",
creatureName: "Defias Convict", creatureKind: "mob", status: "ready" };
const playerDenied = await fetch(`${origin}/api/gm/randomized-creature-readiness`, { method: "PUT",
headers: { "Content-Type": "application/json", Authorization: `Bearer ${player.token}` }, body: JSON.stringify(creature) });
assert.equal(playerDenied.status, 403);
const readinessSaved = await fetch(`${origin}/api/gm/randomized-creature-readiness`, { method: "PUT",
headers: { "Content-Type": "application/json", Authorization: `Bearer ${gm.token}` }, body: JSON.stringify(creature) });
assert.equal(readinessSaved.status, 200);
const readiness = await (await fetch(`${origin}/api/randomized-creature-readiness`)).json();
assert.equal(readiness.reviews[0].status, "ready");
const created = await request("", "POST", { dungeonId: "stormwind-stockade", comment: "Issue via API" }, player.token);
assert.equal(created.status, 201);
const { report } = await created.json();
assert.equal((await request("", "POST", { dungeonId: "wailing-caverns", comment: "Guest issue" })).status, 201);
assert.equal((await request(`/${report.id}`, "PATCH", { status: "Closed", expectedRevision: 0 })).status, 401);
assert.equal((await request(`/${report.id}`, "PATCH", { status: "Closed", expectedRevision: 0 }, player.token)).status, 403);
assert.equal((await request("", "POST", { dungeonId: "stormwind-stockade", comment: "x".repeat(501) })).status, 400);
const closed = await request(`/${report.id}`, "PATCH", { status: "Closed", expectedRevision: 0 }, gm.token);
assert.equal(closed.status, 200);
assert.equal((await closed.json()).report.status, "Closed");
const all = await (await request("")).json();
assert.equal(all.reports.length, 2);
assert.deepEqual(all.openCounts, { "wailing-caverns": 1 });
assert.equal((await (await request("?status=Closed&dungeon=stormwind-stockade")).json()).reports.length, 1);
assert.equal((await request("?status=Bad")).status, 400);
assert.equal((await request("?dungeon=Bad")).status, 400);
} finally {
await new Promise((resolve) => runtime.server.close(resolve));
rmSync(directory, { recursive: true, force: true });
}
});