// Integration tests for step 10.12: Equipment bonuses in combat, profile, and potions. // ONE shared getEquipmentBonuses() function powers all of these. const { describe, it, before, after } = require('node:test'); const assert = require('node:assert/strict'); const { setup, teardown, cleanup } = require('../helpers'); describe('step-10.12 equipment bonuses', () => { let baseUrl, db; const NOW = 1_700_000_000_000; let userSeq = 0; before(async () => { baseUrl = await setup(); db = require('../../src/db').getDb(); }); after(async () => { teardown(); cleanup(); }); // --- helpers --- /** Register a user and return the Set-Cookie header value. */ async function register(username, password) { const res = await fetch(`${baseUrl}/api/auth/register`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }), }); const setCookie = res.headers.get('set-cookie') || ''; assert.equal(res.status, 201); return setCookie; } /** Login and return the Set-Cookie header value. */ async function login(username, password) { const res = await fetch(`${baseUrl}/api/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }), }); const setCookie = res.headers.get('set-cookie') || ''; assert.equal(res.status, 200); return setCookie; } /** Create a hero and return { cookie, characterId }. Uses unique names per test. */ async function createHero(cookie, idx) { const name = `E12${idx}`; const res = await fetch(`${baseUrl}/api/character`, { method: 'POST', headers: { 'Content-Type': 'application/json', Cookie: cookie }, body: JSON.stringify({ name, class: 'warrior' }), }); assert.equal(res.status, 201, `createHero failed for ${name}: ${res.status}`); const { data } = await res.json(); return { characterId: data.character.id, cookie }; } /** Grant an item to a character's inventory (bypasses hunting). */ function grantItem(characterId, itemId, quantity = 1) { db.prepare( 'INSERT INTO inventory (character_id, item_id, quantity, equipped_slot, acquired_at) VALUES (?, ?, ?, NULL, ?)' ).run(characterId, itemId, quantity, NOW); } /** Equip an item by calling the HTTP endpoint. */ async function httpEquip(cookie, itemId, slot) { const res = await fetch(`${baseUrl}/api/inventory/equip`, { method: 'POST', headers: { 'Content-Type': 'application/json', Cookie: cookie }, body: JSON.stringify({ itemId, slot }), }); assert.equal(res.status, 200); return res.json(); } /** Unequip an item by calling the HTTP endpoint. */ async function httpUnequip(cookie, slot) { const res = await fetch(`${baseUrl}/api/inventory/unequip`, { method: 'POST', headers: { 'Content-Type': 'application/json', Cookie: cookie }, body: JSON.stringify({ slot }), }); assert.equal(res.status, 200); return res.json(); } /** GET /api/character and return { character, derived }. */ async function getCharacter(cookie) { const res = await fetch(`${baseUrl}/api/character`, { headers: { Cookie: cookie }, }); assert.equal(res.status, 200); const { data } = await res.json(); return data; } // --- tests --- it('GET /api/character derived stats include equipment bonuses', async () => { const cookie = await register(`e1012a${Date.now()}`, 'pass1234'); const { characterId } = await createHero(cookie, 1); // Baseline: no equipment. const before = await getCharacter(cookie); const baseDefense = before.derived.defense; const baseAttack = before.derived.attack; // Equip Rusty Sword (item 1, +3 attack) and Leather Jerkin (item 8, +3 defense). grantItem(characterId, 1); grantItem(characterId, 8); await httpEquip(cookie, 1, 'weapon'); await httpEquip(cookie, 8, 'armor'); const after = await getCharacter(cookie); // Attack should include +3 from weapon. assert.equal(after.derived.attack, baseAttack + 3, `derived.attack should include +3 weapon bonus: was ${baseAttack}, now ${after.derived.attack}`); // Defense should include +3 from armor. assert.equal(after.derived.defense, baseDefense + 3, `derived.defense should include +3 armor bonus: was ${baseDefense}, now ${after.derived.defense}`); }); it('equipment bonus_vit raises max HP exactly once', async () => { const cookie = await register(`e1012b${Date.now()}`, 'pass1234'); const { characterId } = await createHero(cookie, 2); // Baseline max HP (warrior, vit=8, level 1): 50 + 8*10 + 1*5 = 135. const before = await getCharacter(cookie); const baseMaxHp = before.derived.maxHp; // Equip Wooden Charm (item 11, trinket, +1 vit). grantItem(characterId, 11); await httpEquip(cookie, 11, 'trinket'); const after = await getCharacter(cookie); // +1 vit → +10 max HP. assert.equal(after.derived.maxHp, baseMaxHp + 10, `maxHp should increase by +10 for +1 vit: was ${baseMaxHp}, now ${after.derived.maxHp}`); }); it('potion heal caps at equipment-inclusive maxHp', async () => { const cookie = await register(`e1012c${Date.now()}`, 'pass1234'); const { characterId } = await createHero(cookie, 3); // Equip Wooden Charm (+1 vit) → maxHp = 135 + 10 = 145. grantItem(characterId, 11); await httpEquip(cookie, 11, 'trinket'); const before = await getCharacter(cookie); const equipMaxHp = before.derived.maxHp; // Set HP to 60. db.prepare('UPDATE characters SET hp_current = 60 WHERE id = ?').run(characterId); // Grant and use Minor Healing Potion (item 14, heal:50). grantItem(characterId, 14, 3); const res = await fetch(`${baseUrl}/api/inventory/use`, { method: 'POST', headers: { 'Content-Type': 'application/json', Cookie: cookie }, body: JSON.stringify({ itemId: 14 }), }); assert.equal(res.status, 200); // HP should be capped at equipment-inclusive maxHp. const afterMaxHp = (await getCharacter(cookie)).derived.maxHp; assert.equal(afterMaxHp, equipMaxHp, 'maxHp should be unchanged after potion'); const hp = db.prepare('SELECT hp_current FROM characters WHERE id = ?').get(characterId).hp_current; assert.equal(hp, Math.min(60 + 50, equipMaxHp), `potion heal should cap at equipment-inclusive maxHp (${equipMaxHp})`); }); it('unequipping removes equipment bonuses from derived stats', async () => { const cookie = await register(`e1012d${Date.now()}`, 'pass1234'); const { characterId } = await createHero(cookie, 4); // Equip Rusty Sword (+3 attack). grantItem(characterId, 1); await httpEquip(cookie, 1, 'weapon'); const withEquip = await getCharacter(cookie); const equippedAttack = withEquip.derived.attack; // Unequip. await httpUnequip(cookie, 'weapon'); const withoutEquip = await getCharacter(cookie); // Attack should drop back to the baseline. assert.equal(withoutEquip.derived.attack, equippedAttack - 3, `attack should drop by 3 after unequipping weapon: was ${equippedAttack}, now ${withoutEquip.derived.attack}`); }); });