170 lines
6.7 KiB
JavaScript
170 lines
6.7 KiB
JavaScript
// Integration tests for step 10.13: Starter kit on character creation.
|
|
const { describe, it, before, after } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const { setup, teardown, cleanup } = require('../helpers');
|
|
|
|
describe('step-10.13 starter kit', () => {
|
|
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 }. */
|
|
async function createHero(cookie, name, classKey) {
|
|
const res = await fetch(`${baseUrl}/api/character`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', Cookie: cookie },
|
|
body: JSON.stringify({ name, class: classKey }),
|
|
});
|
|
assert.equal(res.status, 201, `createHero failed for ${name}: ${res.status}`);
|
|
const { data } = await res.json();
|
|
return { characterId: data.character.id, cookie };
|
|
}
|
|
|
|
/** 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;
|
|
}
|
|
|
|
// --- warrior tests ---
|
|
|
|
it('warrior starts with Rusty Sword equipped + 2 Minor Healing Potions', async () => {
|
|
const cookie = await register(`s13w${Date.now()}`, 'pass1234');
|
|
const { characterId } = await createHero(cookie, 'S13Warrior', 'warrior');
|
|
|
|
// Inventory: exactly 2 rows (weapon + potion stack).
|
|
const rows = db.prepare(
|
|
'SELECT item_id, quantity, equipped_slot FROM inventory WHERE character_id = ? ORDER BY item_id'
|
|
).all(characterId);
|
|
assert.equal(rows.length, 2, `expected 2 inventory rows, got ${rows.length}`);
|
|
|
|
// Weapon is Rusty Sword (item 1), equipped in weapon slot.
|
|
const weapon = rows.find(r => r.equipped_slot === 'weapon');
|
|
assert.ok(weapon, 'expected an equipped weapon row');
|
|
assert.equal(weapon.item_id, 1, 'warrior weapon should be Rusty Sword (item 1)');
|
|
|
|
// Potions: one stack row, quantity 2.
|
|
const potions = rows.find(r => r.item_id === 14);
|
|
assert.ok(potions, 'expected a potion row');
|
|
assert.equal(potions.quantity, 2, 'should have 2 Minor Healing Potions');
|
|
|
|
// Derived attack includes the Rusty Sword bonus (+3 bonus_attack).
|
|
// Warrior: 5 + 8*2 + 1 + 3 = 25
|
|
const profile = await getCharacter(cookie);
|
|
assert.equal(profile.derived.attack, 25,
|
|
`warrior attack should be 25 (5 + 8*2 + 1 + 3): got ${profile.derived.attack}`);
|
|
});
|
|
|
|
// --- mage tests ---
|
|
|
|
it('mage starts with Apprentice Staff equipped + 2 Minor Healing Potions', async () => {
|
|
const cookie = await register(`s13m${Date.now()}`, 'pass1234');
|
|
const { characterId } = await createHero(cookie, 'S13Mage', 'mage');
|
|
|
|
const rows = db.prepare(
|
|
'SELECT item_id, quantity, equipped_slot FROM inventory WHERE character_id = ? ORDER BY item_id'
|
|
).all(characterId);
|
|
assert.equal(rows.length, 2, `expected 2 inventory rows, got ${rows.length}`);
|
|
|
|
const weapon = rows.find(r => r.equipped_slot === 'weapon');
|
|
assert.ok(weapon, 'expected an equipped weapon row');
|
|
assert.equal(weapon.item_id, 2, 'mage weapon should be Apprentice Staff (item 2)');
|
|
|
|
const potions = rows.find(r => r.item_id === 14);
|
|
assert.ok(potions, 'expected a potion row');
|
|
assert.equal(potions.quantity, 2, 'should have 2 Minor Healing Potions');
|
|
|
|
// Derived attack includes the Apprentice Staff bonus (+1 int → primary goes 9→10).
|
|
// Mage: 5 + 10*2 + 1 + 0 = 26
|
|
const profile = await getCharacter(cookie);
|
|
assert.equal(profile.derived.attack, 26,
|
|
`mage attack should be 26 (5 + 10*2 + 1 + 0): got ${profile.derived.attack}`);
|
|
});
|
|
|
|
// --- ranger tests ---
|
|
|
|
it('ranger starts with Hunters Shortbow equipped + 2 Minor Healing Potions', async () => {
|
|
const cookie = await register(`s13r${Date.now()}`, 'pass1234');
|
|
const { characterId } = await createHero(cookie, 'S13Ranger', 'ranger');
|
|
|
|
const rows = db.prepare(
|
|
'SELECT item_id, quantity, equipped_slot FROM inventory WHERE character_id = ? ORDER BY item_id'
|
|
).all(characterId);
|
|
assert.equal(rows.length, 2, `expected 2 inventory rows, got ${rows.length}`);
|
|
|
|
const weapon = rows.find(r => r.equipped_slot === 'weapon');
|
|
assert.ok(weapon, 'expected an equipped weapon row');
|
|
assert.equal(weapon.item_id, 3, 'ranger weapon should be Hunters Shortbow (item 3)');
|
|
|
|
const potions = rows.find(r => r.item_id === 14);
|
|
assert.ok(potions, 'expected a potion row');
|
|
assert.equal(potions.quantity, 2, 'should have 2 Minor Healing Potions');
|
|
|
|
// Derived attack includes the Shortbow bonus (+2 agi → primary goes 9→11).
|
|
// Ranger: 5 + 11*2 + 1 + 0 = 28
|
|
const profile = await getCharacter(cookie);
|
|
assert.equal(profile.derived.attack, 28,
|
|
`ranger attack should be 28 (5 + 11*2 + 1 + 0): got ${profile.derived.attack}`);
|
|
});
|
|
|
|
// --- failure path: duplicate name blocked ---
|
|
|
|
it('duplicate hero name is still rejected despite starter kit', async () => {
|
|
const cookie = await register(`s13x${Date.now()}`, 'pass1234');
|
|
const { characterId } = await createHero(cookie, 'S13X', 'warrior');
|
|
|
|
const res = await fetch(`${baseUrl}/api/character`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', Cookie: cookie },
|
|
body: JSON.stringify({ name: 'S13X', class: 'mage' }),
|
|
});
|
|
assert.equal(res.status, 409);
|
|
const body = await res.json();
|
|
assert.equal(body.ok, false);
|
|
assert.equal(body.error.code, 'HERO_EXISTS');
|
|
|
|
// The first hero should have its starter kit intact (2 rows).
|
|
const rows = db.prepare(
|
|
'SELECT COUNT(*) as cnt FROM inventory WHERE character_id = ?'
|
|
).get(characterId);
|
|
assert.equal(rows.cnt, 2, 'starter kit should have 2 inventory rows');
|
|
});
|
|
});
|