197 lines
7.1 KiB
JavaScript
197 lines
7.1 KiB
JavaScript
// 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}`);
|
|
});
|
|
});
|