// 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'); }); });