forked from phenom/healer-man
174 lines
12 KiB
JavaScript
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 });
|
|
}
|
|
});
|