Files
healer-man/server/database.test.mjs
T
2026-08-20 14:05:50 -04:00

182 lines
8.7 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 { DatabaseSync } from "node:sqlite";
import { GameDatabaseError, openGameDatabase } from "./database.mjs";
function withDatabase(run) {
const directory = mkdtempSync(path.join(os.tmpdir(), "healer-man-db-test-"));
const database = openGameDatabase({ dataDir: directory, sessionTtlDays: 1 });
return Promise.resolve(run(database)).finally(() => {
database.close();
rmSync(directory, { recursive: true, force: true });
});
}
test("registers, authenticates, and revokes an account session", () => withDatabase(async (database) => {
const registered = await database.register("CaveHealer", "serpent-pass");
assert.equal(registered.account.username, "CaveHealer");
assert.equal(database.authenticate(registered.token).id, registered.account.id);
const loggedIn = await database.login("cavehealer", "serpent-pass");
assert.equal(loggedIn.account.id, registered.account.id);
database.logout(loggedIn.token);
assert.throws(() => database.authenticate(loggedIn.token), (error) => (
error instanceof GameDatabaseError && error.status === 401
));
}));
test("rejects duplicate usernames and incorrect passwords", () => withDatabase(async (database) => {
await database.register("PartyLead", "eight-plus");
await assert.rejects(database.register("partylead", "another-pass"), (error) => (
error instanceof GameDatabaseError && error.status === 409
));
await assert.rejects(database.login("PartyLead", "wrong-pass"), (error) => (
error instanceof GameDatabaseError && error.status === 401
));
}));
test("persists opaque cloud roster data with increasing revisions", () => withDatabase(async (database) => {
const registered = await database.register("CloudPriest", "cloud-pass");
assert.equal(database.getCloudSave(registered.account.id).revision, 0);
const first = database.putCloudSave(registered.account.id, {
version: 1,
characters: [{ id: "character-one", name: "Mendwell" }],
});
const second = database.putCloudSave(registered.account.id, {
version: 1,
characters: [{ id: "character-one", name: "Mendbetter" }],
});
assert.equal(first.revision, 1);
assert.equal(second.revision, 2);
assert.equal(database.getCloudSave(registered.account.id).data.characters[0].name, "Mendbetter");
}));
test("derives GM authority from the hardcoded Phenom account", () => withDatabase(async (database) => {
const gm = await database.register("Phenom", "storm-admin");
const player = await database.register("Ordinary", "storm-player");
assert.deepEqual(gm.account.roles, ["gm"]);
assert.deepEqual((await database.login("pHeNoM", "storm-admin")).account.roles, ["gm"]);
assert.equal(database.isGameMaster(database.authenticate(gm.token)), true);
assert.equal(database.isGameMaster(database.authenticate(player.token)), false);
assert.equal(database.isGameMaster({ ...player.account, roles: ["gm"] }), false);
}));
test("recovers Manastorm configuration after restart and falls back safely from corrupt data", async () => {
const directory = mkdtempSync(path.join(os.tmpdir(), "healer-man-db-restart-test-"));
const databasePath = path.join(directory, "game.db");
try {
const first = openGameDatabase({ databasePath });
const gm = await first.register("Phenom", "storm-admin");
const current = first.getManastormConfig();
first.putManastormConfig(first.authenticate(gm.token), 0, {
...current.config,
global: { ...current.config.global, enabled: false },
});
first.close();
const restarted = openGameDatabase({ databasePath });
assert.equal(restarted.getManastormConfig().revision, 1);
assert.equal(restarted.getManastormConfig().config.global.enabled, false);
restarted.close();
const raw = new DatabaseSync(databasePath);
raw.prepare("UPDATE manastorm_admin_config SET config_json = ? WHERE id = 1").run("{corrupt");
raw.close();
const recovered = openGameDatabase({ databasePath });
assert.equal(recovered.getManastormConfig().revision, 0);
assert.deepEqual(recovered.getManastormConfig().config.bosses, {});
recovered.close();
} finally {
rmSync(directory, { recursive: true, force: true });
}
});
test("persists revisioned Manastorm GM configuration and rejects stale or unauthorized writes", () => withDatabase(async (database) => {
const gm = await database.register("Phenom", "storm-admin");
const player = await database.register("Ordinary", "storm-player");
const gmAccount = database.authenticate(gm.token);
const playerAccount = database.authenticate(player.token);
const initial = database.getManastormConfig();
assert.equal(initial.revision, 0);
assert.deepEqual(initial.config.bosses, {});
const configured = {
...initial.config,
global: { ...initial.config.global, affixesEnabled: false },
};
const saved = database.putManastormConfig(gmAccount, 0, configured);
assert.equal(saved.revision, 1);
assert.equal(saved.config.global.affixesEnabled, false);
assert.equal(database.getManastormConfig().updatedBy, "Phenom");
assert.throws(() => database.putManastormConfig(gmAccount, 0, configured), (error) => (
error instanceof GameDatabaseError && error.status === 409
));
assert.throws(() => database.putManastormConfig(playerAccount, 1, configured), (error) => (
error instanceof GameDatabaseError && error.status === 403
));
assert.throws(() => database.putManastormConfig(gmAccount, 1, {
...configured,
bosses: { "boss:incomplete": { status: "ready" } },
}), (error) => error instanceof GameDatabaseError && error.status === 400);
}));
test("creates online groups through invitations and transfers leadership when the leader leaves", () => withDatabase(async (database) => {
const leaderAuth = await database.register("GroupLeader", "group-pass");
const healerAuth = await database.register("GroupHealer", "healer-pass");
const leader = database.authenticate(leaderAuth.token);
const healer = database.authenticate(healerAuth.token);
const invitation = database.inviteOnlineGroupMember(leader, "grouphealer");
assert.equal(invitation.state.group.members.length, 1);
const incoming = database.onlineGroupState(healer.id).invitations;
assert.equal(incoming.length, 1);
assert.equal(incoming[0].inviterUsername, "GroupLeader");
const accepted = database.respondToOnlineGroupInvite(healer.id, incoming[0].id, true);
assert.equal(accepted.group.members.length, 2);
assert.equal(accepted.group.leaderAccountId, leader.id);
database.leaveOnlineGroup(leader.id);
const transferred = database.onlineGroupState(healer.id);
assert.equal(transferred.group.leaderAccountId, healer.id);
assert.deepEqual(transferred.group.members.map((member) => member.username), ["GroupHealer"]);
}));
test("launches players-only and AI-filled group activities only when every member is ready", () => withDatabase(async (database) => {
const leaderAuth = await database.register("ReadyLeader", "group-pass");
const memberAuth = await database.register("ReadyMember", "member-pass");
const leader = database.authenticate(leaderAuth.token);
const member = database.authenticate(memberAuth.token);
database.inviteOnlineGroupMember(leader, member.username);
const invite = database.onlineGroupState(member.id).invitations[0];
database.respondToOnlineGroupInvite(member.id, invite.id, true);
database.updateOnlineGroupMember(leader.id, {
id: "leader-character", name: "Leadwell", classId: "warrior", raceId: "human", gender: "male", level: 20,
}, "tank");
assert.throws(() => database.startOnlineGroupActivity(leader.id, {
type: "dungeon", contentId: "wailing-caverns", fillWithAi: true, partySize: 5,
}), (error) => error instanceof GameDatabaseError && error.code === "group_not_ready");
database.updateOnlineGroupMember(member.id, {
id: "member-character", name: "Mendwell", classId: "priest", raceId: "human", gender: "female", level: 20,
}, "healer");
const playersOnly = database.startOnlineGroupActivity(leader.id, {
type: "dungeon", contentId: "wailing-caverns", fillWithAi: false, partySize: 5,
});
assert.equal(playersOnly.group.activity.partySize, 2);
assert.equal(playersOnly.group.activity.fillWithAi, false);
const filled = database.startOnlineGroupActivity(leader.id, {
type: "manastorm", contentId: "manastorm", fillWithAi: true, partySize: 5, startingLevel: 10,
});
assert.equal(filled.group.activity.partySize, 5);
assert.equal(filled.group.activity.startingLevel, 10);
assert.throws(() => database.startOnlineGroupActivity(member.id, {
type: "dungeon", contentId: "wailing-caverns",
}), (error) => error instanceof GameDatabaseError && error.status === 403);
}));