step 5.2: done
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
// Service-level tests for inventoryService: list, equip, unequip.
|
||||
// Uses the same fresh-DB pattern as hunt-service.test.js.
|
||||
const { describe, it, before, after } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const { setup, teardown, cleanup } = require('../helpers');
|
||||
|
||||
const NOW = 1_700_000_000_000;
|
||||
|
||||
describe('inventoryService', () => {
|
||||
let db, listInventory, equipItem, unequipItem, createCharacter, userSeq = 0;
|
||||
|
||||
before(async () => {
|
||||
await setup();
|
||||
db = require('../../src/db').getDb();
|
||||
({ listInventory, equipItem, unequipItem } = require('../../src/services/inventoryService'));
|
||||
({ createCharacter } = require('../../src/services/characterService'));
|
||||
});
|
||||
|
||||
after(async () => { await teardown(); cleanup(); });
|
||||
|
||||
function makeHero(overrides = {}) {
|
||||
userSeq += 1;
|
||||
const uid = db.prepare('INSERT INTO users (username, password_hash, created_at) VALUES (?, ?, ?)')
|
||||
.run(`u${userSeq}`, 'x', NOW).lastInsertRowid;
|
||||
const { id } = createCharacter(Number(uid), `Hero${userSeq}`, 'warrior');
|
||||
const cols = Object.keys(overrides);
|
||||
if (cols.length) {
|
||||
db.prepare(`UPDATE characters SET ${cols.map((c) => `${c} = ?`).join(', ')} WHERE id = ?`)
|
||||
.run(...Object.values(overrides), id);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
/** 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);
|
||||
}
|
||||
|
||||
it('returns empty inventory for a fresh character', () => {
|
||||
const id = makeHero();
|
||||
const result = listInventory(id);
|
||||
assert.equal(result.ok !== undefined || true, true); // service returns raw obj
|
||||
assert.equal(result.items.length, 0);
|
||||
assert.equal(result.character.name, `Hero${userSeq}`);
|
||||
});
|
||||
|
||||
it('equips a weapon and updates derived stats', () => {
|
||||
const id = makeHero();
|
||||
grantItem(id, 1); // Rusty Sword (weapon, +3 attack)
|
||||
const result = equipItem(id, 1, 'weapon');
|
||||
assert.equal(result.items.find((i) => i.itemId === 1)?.equipped, true);
|
||||
assert.equal(result.items.find((i) => i.itemId === 1)?.equipped_slot, 'weapon');
|
||||
|
||||
// Derived stats should reflect the +3 attack bonus
|
||||
assert.ok(result.derived.attack >= 7, `attack should include bonus (got ${result.derived.attack})`);
|
||||
});
|
||||
|
||||
it('equips armor and trinket independently', () => {
|
||||
const id = makeHero();
|
||||
grantItem(id, 8); // Leather Jerkin (armor, +3 def)
|
||||
grantItem(id, 11); // Wooden Charm (trinket, +1 vit)
|
||||
equipItem(id, 8, 'armor');
|
||||
equipItem(id, 11, 'trinket');
|
||||
|
||||
const result = listInventory(id);
|
||||
const armorItem = result.items.find((i) => i.itemId === 8);
|
||||
const trinketItem = result.items.find((i) => i.itemId === 11);
|
||||
assert.equal(armorItem?.equipped, true);
|
||||
assert.equal(armorItem?.equipped_slot, 'armor');
|
||||
assert.equal(trinketItem?.equipped, true);
|
||||
assert.equal(trinketItem?.equipped_slot, 'trinket');
|
||||
});
|
||||
|
||||
it('auto-unequips the old item when equipping to an occupied slot', () => {
|
||||
const id = makeHero({ level: 5, str: 8, agi: 4, int: 2, vit: 8 });
|
||||
grantItem(id, 1); // Rusty Sword
|
||||
grantItem(id, 4); // Iron Blade (requires level 5)
|
||||
equipItem(id, 1, 'weapon');
|
||||
equipItem(id, 4, 'weapon');
|
||||
|
||||
const result = listInventory(id);
|
||||
// After equipping item 4, item 1 should be unequipped (only one per slot).
|
||||
// The equipped item 4 now occupies the weapon slot; item 1 is in inventory but not equipped.
|
||||
const equippedItem = result.items.find((i) => i.equipped);
|
||||
assert.ok(equippedItem, 'should have one equipped item');
|
||||
assert.equal(equippedItem.itemId, 4);
|
||||
// Item 1 should still be in inventory but unequipped
|
||||
const item1 = result.items.find((i) => i.itemId === 1);
|
||||
assert.ok(item1, 'item 1 should still be in inventory');
|
||||
assert.equal(item1.equipped, false);
|
||||
});
|
||||
|
||||
it('unequips an item from a slot', () => {
|
||||
const id = makeHero();
|
||||
grantItem(id, 1);
|
||||
equipItem(id, 1, 'weapon');
|
||||
const result = unequipItem(id, 'weapon');
|
||||
const item1 = result.items.find((i) => i.itemId === 1);
|
||||
assert.ok(item1, 'item 1 should still be in inventory after unequip');
|
||||
assert.equal(item1.equipped, false);
|
||||
assert.equal(item1.equipped_slot, null);
|
||||
});
|
||||
|
||||
it('rejects equipping a non-existent item', () => {
|
||||
const id = makeHero();
|
||||
assert.throws(() => equipItem(id, 99999, 'weapon'), (e) => e.code === 'NOT_FOUND');
|
||||
});
|
||||
|
||||
it('rejects equipping wrong item type to slot', () => {
|
||||
const id = makeHero();
|
||||
// Item 14 = Minor Healing Potion (consumable)
|
||||
assert.throws(() => equipItem(id, 14, 'weapon'), (e) => e.code === 'VALIDATION_ERROR');
|
||||
});
|
||||
|
||||
it('rejects equipping an item the character is too low level for', () => {
|
||||
const id = makeHero();
|
||||
// Item 7 = Emberforged Greatsword (rare, min_level 10)
|
||||
assert.throws(() => equipItem(id, 7, 'weapon'), (e) => e.code === 'LEVEL_TOO_LOW');
|
||||
});
|
||||
|
||||
it('rejects unequip when slot is empty', () => {
|
||||
const id = makeHero();
|
||||
assert.throws(() => unequipItem(id, 'weapon'), (e) => e.code === 'NOT_FOUND');
|
||||
});
|
||||
|
||||
it('rejects invalid slot names', () => {
|
||||
const id = makeHero();
|
||||
assert.throws(() => equipItem(id, 1, 'head'), (e) => e.code === 'VALIDATION_ERROR');
|
||||
assert.throws(() => unequipItem(id, 'boots'), (e) => e.code === 'VALIDATION_ERROR');
|
||||
});
|
||||
|
||||
it('rejects equipping an item not in the character inventory', () => {
|
||||
const id = makeHero();
|
||||
// Item 1 exists but was never acquired by this character
|
||||
assert.throws(() => equipItem(id, 1, 'weapon'), (e) => e.code === 'NOT_FOUND');
|
||||
});
|
||||
|
||||
it('updates derived stats when equipping armor (defense bonus)', () => {
|
||||
const id = makeHero();
|
||||
grantItem(id, 8); // Leather Jerkin (armor, +3 defense)
|
||||
const baseline = listInventory(id);
|
||||
const baseDefense = baseline.derived.defense;
|
||||
|
||||
equipItem(id, 8, 'armor');
|
||||
const result = listInventory(id);
|
||||
|
||||
// Warrior gets +10% defense, so: (floor(vit*1.5) + 3) * 1.1
|
||||
assert.ok(result.derived.defense > baseDefense,
|
||||
`defense should increase with armor (was ${baseDefense}, now ${result.derived.defense})`);
|
||||
});
|
||||
|
||||
it('updates derived stats when equipping trinket (vit bonus)', () => {
|
||||
const id = makeHero();
|
||||
grantItem(id, 11); // Wooden Charm (trinket, +1 vit)
|
||||
const baseline = listInventory(id);
|
||||
const baseMaxHp = baseline.derived.maxHp;
|
||||
|
||||
equipItem(id, 11, 'trinket');
|
||||
const result = listInventory(id);
|
||||
|
||||
// +1 vit = +10 max HP
|
||||
assert.ok(result.derived.maxHp > baseMaxHp,
|
||||
`maxHp should increase with vit bonus (was ${baseMaxHp}, now ${result.derived.maxHp})`);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user