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.deepEqual(database.listDungeonReports().openCounts, { "forsaken-abbey": 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("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 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 }); } });