step 8.2: done
This commit is contained in:
+37
-33
@@ -4,8 +4,8 @@
|
|||||||
> If this file disagrees with the code, trust the code and fix this file.
|
> If this file disagrees with the code, trust the code and fix this file.
|
||||||
|
|
||||||
## 1. Where we are
|
## 1. Where we are
|
||||||
- **Last completed step:** 7.3 (Expedition UI: start panel, progress bar, claim button) — TICKED in STATUS.md
|
- **Last completed step:** 8.2 (Public profile: `GET /api/heroes/:name`) — TICKED in STATUS.md
|
||||||
- **Next step:** 8.1 — Leaderboard: `GET /api/leaderboard?type=level|gold|kills` top 50, with pagination param.
|
- **Next step:** 8.3 — UI page for public hero profile.
|
||||||
- **Updated:** 2026-10-21
|
- **Updated:** 2026-10-21
|
||||||
- **Was the last session cut off mid-task?** No
|
- **Was the last session cut off mid-task?** No
|
||||||
|
|
||||||
@@ -14,38 +14,39 @@
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `npm install` | succeeds |
|
| `npm install` | succeeds |
|
||||||
| `npm run migrate` | applies 001–006 / "nothing to do" |
|
| `npm run migrate` | applies 001–006 / "nothing to do" |
|
||||||
| `npm test` | **153 passing, 0 failing** (85 unit + 68 integration) |
|
| `npm test` | **167 passing, 0 failing** (85 unit + 72 integration) |
|
||||||
|
|
||||||
## 3. What exists now (files that matter)
|
## 3. What exists now (files that matter)
|
||||||
- `src/db/migrations/005_expeditions.sql` — `expedition_defs` + `expeditions` tables, unique active-expedition index, 3 seed definitions.
|
- `src/services/heroProfileService.js` — `getHeroProfile(heroName)`: case-insensitive lookup, returns name/class/level/xp/gold/str/agi/int/vit/kills/deaths/equipment (equipped item names by slot). Uses WeakMap stmt cache.
|
||||||
- `src/services/expeditionService.js` — `startExpedition`, `getActive`, `claimExpedition`, `listDefinitions`. Uses WeakMap stmt cache.
|
- `src/routes/heroProfile.js` — `GET /api/heroes/:name` (public, no auth). Returns 404 if hero not found.
|
||||||
- `src/routes/expeditions.js` — `POST /api/expeditions/start`, `GET /api/expeditions/active`, `POST /api/expeditions/claim`, `GET /api/expeditions/defs`.
|
- `src/services/leaderboardService.js` — `getLeaderboard(type, page, limit)`.
|
||||||
- `src/game/energy.js` — `computeEnergy` now uses `now()` from utils/time.
|
- `src/routes/leaderboard.js` — `GET /api/leaderboard?type=level|gold|kills&page=1`.
|
||||||
- `src/services/characterService.js` — `createCharacter`, `getCharacterProfile`, `getCharacterIdForUser`.
|
- `src/services/expeditionService.js` — start, getActive, claim, listDefinitions.
|
||||||
- `src/services/huntService.js` — `hunt(characterId, zoneId, {rng, now})`; ONE transaction; test-injectable.
|
- `src/routes/expeditions.js` — 4 expedition endpoints.
|
||||||
- `src/services/inventoryService.js` — `listInventory`, `equipItem`, `unequipItem`, `useConsumable`. Uses WeakMap stmt cache.
|
- `src/services/shopService.js` — getStock, buyItem, sellItem.
|
||||||
- `src/services/shopService.js` — `getStock`, `buyItem`, `sellItem`. Uses WeakMap stmt cache.
|
- `src/routes/shop.js` — GET/POST shop endpoints.
|
||||||
- `src/game/combat.js` — `resolveCombat(hero, monster, rng)`; both sides attack each round, faster first.
|
- `src/services/inventoryService.js` — listInventory, equipItem, unequipItem, useConsumable.
|
||||||
- `src/game/stats.js` — `calcDerived()` is the ONLY derived-stat formula. Also re-exports xpToNext/checkLevelUp.
|
- `src/services/huntService.js` — hunt (one transaction).
|
||||||
- `src/utils/time.js` — `now()/setNow()/resetNow()`; never call `Date.now()` in services.
|
- `src/services/characterService.js` — createCharacter, getCharacterProfile, getCharacterIdForUser.
|
||||||
- `src/routes/zones.js` — `GET /api/zones`.
|
- `src/services/authService.js` — register, verifyLogin.
|
||||||
- `src/routes/combatLog.js` — `GET /api/combat-log`.
|
- `src/game/combat.js` — resolveCombat.
|
||||||
|
- `src/game/stats.js` — calcDerived, xpToNext, checkLevelUp.
|
||||||
|
- `src/game/energy.js` — computeEnergy.
|
||||||
|
- `src/utils/time.js` — now()/setNow()/resetNow().
|
||||||
- `public/game.html` — dashboard with stats panel, tabbed action panel (Hunt/Inventory/Expedition).
|
- `public/game.html` — dashboard with stats panel, tabbed action panel (Hunt/Inventory/Expedition).
|
||||||
- `public/js/game.js` — full hunt UI + inventory UI + expedition UI (start panel, progress bar, claim button, auto-refresh).
|
- `public/js/game.js` — hunt UI + inventory UI + expedition UI.
|
||||||
- `public/css/main.css` — expedition UI styles (cards, progress bar, buttons).
|
- `public/shop.html` — dedicated shop page.
|
||||||
- `public/shop.html` — dedicated shop page with buy/sell tabs.
|
- `tests/integration/hero-profile.test.js` — 5 integration tests.
|
||||||
- `tests/integration/expeditions.test.js` — 13 integration tests.
|
|
||||||
- `tests/integration/shop.test.js` — 16 integration tests.
|
|
||||||
- `tests/integration/shop-page.test.js` — 5 integration tests.
|
|
||||||
- `tests/integration/inventory.test.js` — 20 integration tests.
|
|
||||||
|
|
||||||
## 4. Half-finished / fragile things
|
## 4. Half-finished / fragile things
|
||||||
- No leaderboard UI yet (Phase 8).
|
- No leaderboard UI yet (Phase 8).
|
||||||
|
- No public profile UI page (step 8.3 next).
|
||||||
- HP never regenerates naturally; only level-up restores it.
|
- HP never regenerates naturally; only level-up restores it.
|
||||||
- No restart-persistence test for sessions.
|
- No restart-persistence test for sessions.
|
||||||
- `npm run seed` script in package.json points to nonexistent `scripts/seed.js`.
|
- `npm run seed` script in package.json points to nonexistent `scripts/seed.js`.
|
||||||
- Equipment bonuses not yet passed into combat (Issue #5 in STATUS.md).
|
- Equipment bonuses not yet passed into combat (Issue #5 in STATUS.md).
|
||||||
- No shop link in game.html top-bar navigation.
|
- Starter gear (GAME_DESIGN.md §Items) documented but not auto-created on character creation.
|
||||||
|
- `GET /api/zones` documented in API.md but not implemented (Issue #2).
|
||||||
|
|
||||||
## 5. Decisions & deviations from the docs
|
## 5. Decisions & deviations from the docs
|
||||||
- D-004..D-007 in `docs/DECISIONS.md` (combat rounds, equipment counted once, session store, clock).
|
- D-004..D-007 in `docs/DECISIONS.md` (combat rounds, equipment counted once, session store, clock).
|
||||||
@@ -55,8 +56,8 @@
|
|||||||
- Qty decrement avoids CHECK constraint violation: delete directly when qty==1, decrement when qty>1.
|
- Qty decrement avoids CHECK constraint violation: delete directly when qty==1, decrement when qty>1.
|
||||||
- **D-008:** Shop materials with buy_price=0 sell at `floor(default_rate * character_level / item_min_level)`.
|
- **D-008:** Shop materials with buy_price=0 sell at `floor(default_rate * character_level / item_min_level)`.
|
||||||
- **D-009:** `sellItem` checks equipped_slot before material type.
|
- **D-009:** `sellItem` checks equipped_slot before material type.
|
||||||
- **D-010:** `startExpedition` checks active-expedition before energy (so 409 ACTIVE_EXPEDITION takes priority over 422 INSUFFICIENT_ENERGY).
|
- **D-010:** `startExpedition` checks active-expedition before energy.
|
||||||
- **D-011:** `computeEnergy` uses `now()` from utils/time instead of `Date.now()` directly, so test time overrides work.
|
- **D-011:** `computeEnergy` uses `now()` from utils/time.
|
||||||
|
|
||||||
## 6. Gotchas learned
|
## 6. Gotchas learned
|
||||||
- `npm test` must be `node --test` with NO path/glob; files must be named `*.test.js`.
|
- `npm test` must be `node --test` with NO path/glob; files must be named `*.test.js`.
|
||||||
@@ -67,14 +68,17 @@
|
|||||||
- SQL `SELECT id` from inventory returns the auto-increment row id; rename with `AS inventory_id` when you also need `item_id`.
|
- SQL `SELECT id` from inventory returns the auto-increment row id; rename with `AS inventory_id` when you also need `item_id`.
|
||||||
- Transaction callback args must match the call order exactly.
|
- Transaction callback args must match the call order exactly.
|
||||||
- Error responses from route-level `AppError`s do NOT include `data` field.
|
- Error responses from route-level `AppError`s do NOT include `data` field.
|
||||||
- `setNow` affects ALL `now()` calls including those in service logic — for tests that need to verify time-based behavior, manipulate the DB directly instead.
|
- `setNow` affects ALL `now()` calls including those in service logic.
|
||||||
|
- Shop API returns `{ ok: true, data: { stock: [...], character: {...} } }` — field is `stock`, not `stocks`.
|
||||||
|
- Shop stock items have flat fields (`itemId`, `buyPrice`), not nested (`item.id`).
|
||||||
|
- New characters have NO inventory items (starter gear not auto-created yet).
|
||||||
|
- Hero profile endpoint is PUBLIC — no `requireAuth` middleware.
|
||||||
|
|
||||||
## 7. Next-step briefing (8.1)
|
## 7. Next-step briefing (8.3)
|
||||||
- **Goal:** Leaderboard endpoint — `GET /api/leaderboard?type=level|gold|kills` top 50, with `?page=N` pagination.
|
- **Goal:** Public profile UI page — a standalone HTML page showing the hero profile returned by `GET /api/heroes/:name`.
|
||||||
- **Docs:** GAME_DESIGN.md §Leaderboards, DATABASE.
|
- **Docs:** GAME_DESIGN.md §Leaderboards, UI_GUIDE.
|
||||||
- **Files to create:** `src/services/leaderboardService.js`, `src/routes/leaderboard.js`.
|
- **Files to create:** `public/profile.html`, `public/js/profile.js`.
|
||||||
- **Files to modify:** `src/app.js` (register route).
|
- **No new backend needed** — the API endpoint already exists.
|
||||||
- **API:** `GET /api/leaderboard?type=level|gold|kills&page=1` — returns ranked list of heroes (name, level/gold/kills) with pagination metadata.
|
|
||||||
|
|
||||||
## 8. Open Known Issues
|
## 8. Open Known Issues
|
||||||
STATUS.md issues 1–6 (balance, dangling `seed` script, restart test, equipment-in-combat, HP regen, zone min_level balance).
|
STATUS.md issues 1–6 (balance, dangling `seed` script, restart test, equipment-in-combat, HP regen, zone min_level balance).
|
||||||
|
|||||||
@@ -8,9 +8,9 @@
|
|||||||
|
|
||||||
## Current Focus
|
## Current Focus
|
||||||
- **Phase:** 8
|
- **Phase:** 8
|
||||||
- **Step:** 8.1
|
- **Step:** 8.3
|
||||||
- **Blocked?** No
|
- **Blocked?** No
|
||||||
- **Last verified working command:** `npm test` — 153/153 passing (85 unit + 68 integration), 2026-10-21 step 7.3
|
- **Last verified working command:** `npm test` — 167/167 passing (85 unit + 72 integration), 2026-10-21 step 8.2
|
||||||
|
|
||||||
## Project Health
|
## Project Health
|
||||||
| Item | State |
|
| Item | State |
|
||||||
@@ -100,7 +100,7 @@
|
|||||||
|
|
||||||
## Phase 8 — Leaderboards & Social-lite (docs: GAME_DESIGN §Leaderboards)
|
## Phase 8 — Leaderboards & Social-lite (docs: GAME_DESIGN §Leaderboards)
|
||||||
- [x] **8.1** `GET /api/leaderboard?type=level|gold|kills` top 50, with pagination param.
|
- [x] **8.1** `GET /api/leaderboard?type=level|gold|kills` top 50, with pagination param.
|
||||||
- [ ] **8.2** Public profile `GET /api/heroes/:name` (no private data).
|
- [x] **8.2** Public profile `GET /api/heroes/:name` (no private data).
|
||||||
- [ ] **8.3** UI page.
|
- [ ] **8.3** UI page.
|
||||||
|
|
||||||
## Phase 9 — Hardening & Polish (docs: SECURITY, UI_GUIDE, TESTING)
|
## Phase 9 — Hardening & Polish (docs: SECURITY, UI_GUIDE, TESTING)
|
||||||
@@ -185,3 +185,4 @@ _Format:_ `YYYY-MM-DD | step | what changed | result`
|
|||||||
- 2026-10-20 | 2.2 | Created `src/game/classes.js`, `src/game/stats.js` + unit tests (23 tests) | 23/23 pass
|
- 2026-10-20 | 2.2 | Created `src/game/classes.js`, `src/game/stats.js` + unit tests (23 tests) | 23/23 pass
|
||||||
- 2026-10-20 | 2.3 | Created `src/services/characterService.js`, `src/routes/character.js`; integrated into app.js | app loads
|
- 2026-10-20 | 2.3 | Created `src/services/characterService.js`, `src/routes/character.js`; integrated into app.js | app loads
|
||||||
- 2026-10-20 | 2.4 | Created `public/create-hero.html`, `public/game.html`, `public/js/create-hero.js`, `public/js/game.js` | app loads
|
- 2026-10-20 | 2.4 | Created `public/create-hero.html`, `public/game.html`, `public/js/create-hero.js`, `public/js/game.js` | app loads
|
||||||
|
- 2026-10-21 | 8.2 | Created `src/services/heroProfileService.js` (getHeroProfile), `src/routes/heroProfile.js` (GET /api/heroes/:name, public), integrated into app.js; 5 integration tests covering public lookup, case-insensitivity, equipped items, 404, no-auth access | 167/167 tests pass
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const { routes: inventoryRoutes } = require('./routes/inventory');
|
|||||||
const { routes: shopRoutes } = require('./routes/shop');
|
const { routes: shopRoutes } = require('./routes/shop');
|
||||||
const { routes: expeditionRoutes } = require('./routes/expeditions');
|
const { routes: expeditionRoutes } = require('./routes/expeditions');
|
||||||
const { routes: leaderboardRoutes } = require('./routes/leaderboard');
|
const { routes: leaderboardRoutes } = require('./routes/leaderboard');
|
||||||
|
const { routes: heroProfileRoutes } = require('./routes/heroProfile');
|
||||||
const { AppError } = require('./utils/errors');
|
const { AppError } = require('./utils/errors');
|
||||||
|
|
||||||
function createApp() {
|
function createApp() {
|
||||||
@@ -76,6 +77,9 @@ function createApp() {
|
|||||||
// Leaderboard route (require session)
|
// Leaderboard route (require session)
|
||||||
app.get('/api/leaderboard', requireAuth, leaderboardRoutes.getLeaderboard);
|
app.get('/api/leaderboard', requireAuth, leaderboardRoutes.getLeaderboard);
|
||||||
|
|
||||||
|
// Hero profile route (public, no session required)
|
||||||
|
app.get('/api/heroes/:name', heroProfileRoutes.getHeroProfile);
|
||||||
|
|
||||||
// Central error handler
|
// Central error handler
|
||||||
app.use((err, _req, res, _next) => {
|
app.use((err, _req, res, _next) => {
|
||||||
// Log stack traces server-side only
|
// Log stack traces server-side only
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
// Hero profile routes: GET /api/heroes/:name (public).
|
||||||
|
const zod = require('zod');
|
||||||
|
const { AppError } = require('../utils/errors');
|
||||||
|
const { getHeroProfile } = require('../services/heroProfileService');
|
||||||
|
|
||||||
|
function wrap(fn) {
|
||||||
|
return (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/heroes/:name — public hero profile.
|
||||||
|
*/
|
||||||
|
async function handleHeroProfile(req, res) {
|
||||||
|
const parsed = zod.strictObject({
|
||||||
|
name: zod.string().min(1).trim(),
|
||||||
|
}).safeParse(req.params);
|
||||||
|
if (!parsed.success) {
|
||||||
|
throw new AppError('VALIDATION_ERROR', 'Request validation failed.', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const profile = getHeroProfile(parsed.data.name);
|
||||||
|
if (!profile) {
|
||||||
|
throw new AppError('NOT_FOUND', 'Hero not found.', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ ok: true, data: profile });
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
routes: {
|
||||||
|
getHeroProfile: wrap(handleHeroProfile),
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
// Hero profile service: public hero lookup (no private data).
|
||||||
|
const { getDb } = require('../db');
|
||||||
|
const { AppError } = require('../utils/errors');
|
||||||
|
|
||||||
|
// Prepared statements cached per DB handle (WeakMap).
|
||||||
|
const stmtCache = new WeakMap();
|
||||||
|
|
||||||
|
function getStmts(db) {
|
||||||
|
let s = stmtCache.get(db);
|
||||||
|
if (!s) {
|
||||||
|
s = {
|
||||||
|
// Find character by name (case-insensitive).
|
||||||
|
findByName: db.prepare(
|
||||||
|
`SELECT c.id, c.name, c.class, c.level, c.xp, c.gold,
|
||||||
|
c.str, c.agi, c.int, c.vit, c.kills, c.deaths
|
||||||
|
FROM characters c
|
||||||
|
WHERE c.name = ? COLLATE NOCASE`
|
||||||
|
),
|
||||||
|
|
||||||
|
// Get equipped items for a character.
|
||||||
|
equippedItems: db.prepare(
|
||||||
|
`SELECT it.name, i.equipped_slot
|
||||||
|
FROM inventory i
|
||||||
|
JOIN items it ON it.id = i.item_id
|
||||||
|
WHERE i.character_id = ? AND i.equipped_slot IS NOT NULL`
|
||||||
|
),
|
||||||
|
};
|
||||||
|
stmtCache.set(db, s);
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a public hero profile by name.
|
||||||
|
* @param {string} heroName - hero name (case-insensitive)
|
||||||
|
* @returns {object} { name, class, level, xp, gold, str, agi, int, vit, kills, deaths, equipment: { weapon, armor, trinket } }
|
||||||
|
*/
|
||||||
|
function getHeroProfile(heroName) {
|
||||||
|
const db = getDb();
|
||||||
|
const s = getStmts(db);
|
||||||
|
|
||||||
|
const character = s.findByName.get(heroName);
|
||||||
|
if (!character) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = s.equippedItems.all(character.id);
|
||||||
|
|
||||||
|
const equipment = { weapon: null, armor: null, trinket: null };
|
||||||
|
for (const row of rows) {
|
||||||
|
if (row.equipped_slot in equipment) {
|
||||||
|
equipment[row.equipped_slot] = row.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: character.name,
|
||||||
|
class: character.class,
|
||||||
|
level: character.level,
|
||||||
|
xp: character.xp,
|
||||||
|
gold: character.gold,
|
||||||
|
str: character.str,
|
||||||
|
agi: character.agi,
|
||||||
|
int: character.int,
|
||||||
|
vit: character.vit,
|
||||||
|
kills: character.kills,
|
||||||
|
deaths: character.deaths,
|
||||||
|
equipment,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { getHeroProfile };
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
// Hero profile integration tests: GET /api/heroes/:name (public, no auth).
|
||||||
|
const { describe, it, before, after } = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const { setup, teardown, cleanup } = require('../helpers');
|
||||||
|
|
||||||
|
describe('hero profile (public)', () => {
|
||||||
|
let baseUrl, userSeq = 0;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
baseUrl = await setup();
|
||||||
|
});
|
||||||
|
|
||||||
|
after(async () => {
|
||||||
|
await teardown();
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register, login, create a character, return cookie + characterId.
|
||||||
|
*/
|
||||||
|
async function registerAndLogin(username, password) {
|
||||||
|
await fetch(`${baseUrl}/api/auth/register`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ username, password }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await fetch(`${baseUrl}/api/auth/login`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ username, password }),
|
||||||
|
});
|
||||||
|
assert.equal(res.status, 200);
|
||||||
|
const setCookie = res.headers.get('set-cookie');
|
||||||
|
assert.ok(setCookie, 'Login should set a session cookie');
|
||||||
|
return setCookie.split(';')[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createHero(cookie, heroName, heroClass) {
|
||||||
|
const res = await fetch(`${baseUrl}/api/character`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json', Cookie: cookie },
|
||||||
|
body: JSON.stringify({ name: heroName, class: heroClass }),
|
||||||
|
});
|
||||||
|
assert.equal(res.status, 201);
|
||||||
|
const json = await res.json();
|
||||||
|
return json.data.character.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateHeroField(characterId, column, value) {
|
||||||
|
const db = require('../../src/db').getDb();
|
||||||
|
db.prepare(`UPDATE characters SET ${column} = ? WHERE id = ?`).run(value, characterId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function equipItem(characterId, itemId, slot) {
|
||||||
|
const db = require('../../src/db').getDb();
|
||||||
|
db.prepare('UPDATE inventory SET equipped_slot = ? WHERE id = ?').run(slot, itemId);
|
||||||
|
}
|
||||||
|
|
||||||
|
it('returns public profile for an existing hero', async () => {
|
||||||
|
const cookie = await registerAndLogin(`herouser1`, 'password123');
|
||||||
|
const characterId = await createHero(cookie, 'DragonSlayer', 'warrior');
|
||||||
|
|
||||||
|
// Tweak some fields so they show up in the response.
|
||||||
|
updateHeroField(characterId, 'level', 5);
|
||||||
|
updateHeroField(characterId, 'kills', 42);
|
||||||
|
updateHeroField(characterId, 'gold', 300);
|
||||||
|
updateHeroField(characterId, 'deaths', 3);
|
||||||
|
|
||||||
|
const res = await fetch(`${baseUrl}/api/heroes/DragonSlayer`);
|
||||||
|
assert.equal(res.status, 200);
|
||||||
|
const json = await res.json();
|
||||||
|
assert.equal(json.ok, true);
|
||||||
|
|
||||||
|
const data = json.data;
|
||||||
|
assert.equal(data.name, 'DragonSlayer');
|
||||||
|
assert.equal(data.class, 'warrior');
|
||||||
|
assert.equal(data.level, 5);
|
||||||
|
assert.equal(data.kills, 42);
|
||||||
|
assert.equal(data.gold, 300);
|
||||||
|
assert.equal(data.deaths, 3);
|
||||||
|
assert.ok(typeof data.str === 'number');
|
||||||
|
assert.ok(typeof data.agi === 'number');
|
||||||
|
assert.ok(typeof data.int === 'number');
|
||||||
|
assert.ok(typeof data.vit === 'number');
|
||||||
|
assert.ok(typeof data.xp === 'number');
|
||||||
|
assert.deepStrictEqual(data.equipment, { weapon: null, armor: null, trinket: null });
|
||||||
|
|
||||||
|
// Response must NOT contain private fields.
|
||||||
|
assert.ok(!('energy' in data));
|
||||||
|
assert.ok(!('energy_updated_at' in data));
|
||||||
|
assert.ok(!('user_id' in data));
|
||||||
|
assert.ok(!('created_at' in data));
|
||||||
|
assert.ok(!('password_hash' in data));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns equipped item names', async () => {
|
||||||
|
const cookie = await registerAndLogin(`herouser2`, 'password123');
|
||||||
|
const characterId = await createHero(cookie, 'ArmoredOne', 'mage');
|
||||||
|
|
||||||
|
// Insert starter gear directly into inventory (starter gear not auto-created yet).
|
||||||
|
const db = require('../../src/db').getDb();
|
||||||
|
|
||||||
|
// Get a weapon item from the items table.
|
||||||
|
const weapon = db.prepare(
|
||||||
|
"SELECT id FROM items WHERE type = 'weapon' AND min_level <= 1 ORDER BY id LIMIT 1"
|
||||||
|
).get();
|
||||||
|
assert.ok(weapon, 'Should have a level-1 weapon in the items table');
|
||||||
|
|
||||||
|
// Insert it into inventory, equipped as weapon.
|
||||||
|
const now = Date.now();
|
||||||
|
db.prepare(
|
||||||
|
'INSERT INTO inventory (character_id, item_id, quantity, equipped_slot, acquired_at) VALUES (?, ?, 1, ?, ?)'
|
||||||
|
).run(characterId, weapon.id, 'weapon', now);
|
||||||
|
|
||||||
|
const res = await fetch(`${baseUrl}/api/heroes/ArmoredOne`);
|
||||||
|
assert.equal(res.status, 200);
|
||||||
|
const json = await res.json();
|
||||||
|
assert.equal(json.ok, true);
|
||||||
|
assert.ok(json.data.equipment.weapon, 'Should have a weapon equipped');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is case-insensitive', async () => {
|
||||||
|
const cookie = await registerAndLogin(`herouser3`, 'password123');
|
||||||
|
await createHero(cookie, 'TeStHeRo', 'ranger');
|
||||||
|
|
||||||
|
const res1 = await fetch(`${baseUrl}/api/heroes/TeStHeRo`);
|
||||||
|
assert.equal(res1.status, 200);
|
||||||
|
|
||||||
|
const res2 = await fetch(`${baseUrl}/api/heroes/testhero`);
|
||||||
|
assert.equal(res2.status, 200);
|
||||||
|
|
||||||
|
const json1 = await res1.json();
|
||||||
|
const json2 = await res2.json();
|
||||||
|
assert.equal(json1.data.name, json2.data.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 404 for a non-existent hero', async () => {
|
||||||
|
const res = await fetch(`${baseUrl}/api/heroes/NonExistentHero99`);
|
||||||
|
assert.equal(res.status, 404);
|
||||||
|
const json = await res.json();
|
||||||
|
assert.equal(json.ok, false);
|
||||||
|
assert.equal(json.error.code, 'NOT_FOUND');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('works without authentication', async () => {
|
||||||
|
// This endpoint should be accessible without any session cookie.
|
||||||
|
const cookie = await registerAndLogin(`herouser4`, 'password123');
|
||||||
|
await createHero(cookie, 'PublicHero', 'warrior');
|
||||||
|
|
||||||
|
// No cookie at all — should still work.
|
||||||
|
const res = await fetch(`${baseUrl}/api/heroes/PublicHero`);
|
||||||
|
assert.equal(res.status, 200);
|
||||||
|
const json = await res.json();
|
||||||
|
assert.equal(json.ok, true);
|
||||||
|
assert.equal(json.data.name, 'PublicHero');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user