// Shop page integration tests: verify page loads and requires auth. const { describe, it, before, after } = require('node:test'); const assert = require('node:assert/strict'); const { setup, teardown, cleanup } = require('../helpers'); /** * Register a user and log them in, returning the session cookie string. */ async function registerAndLogin(baseUrl, 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 json = await res.json(); assert.equal(json.ok, true); const setCookie = res.headers.get('set-cookie'); assert.ok(setCookie, 'Login should set a session cookie'); return setCookie.split(';')[0]; } function authHeaders(cookie) { return { Cookie: cookie }; } describe('shop page', () => { let baseUrl, userSeq = 0; before(async () => { baseUrl = await setup(); }); after(async () => { await teardown(); cleanup(); }); /** * Register, login, create a character, optionally tweak DB fields. */ async function setupHero(overrides = {}) { userSeq += 1; const username = `shoppu${userSeq}`; const cookie = await registerAndLogin(baseUrl, username, 'password123'); const charRes = await fetch(`${baseUrl}/api/character`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...authHeaders(cookie) }, body: JSON.stringify({ name: `ShopTest${userSeq}`, class: 'warrior' }), }); assert.equal(charRes.status, 201); const charJson = await charRes.json(); const characterId = charJson.data.character.id; const db = require('../../src/db').getDb(); const cols = Object.keys(overrides); if (cols.length) { db.prepare(`UPDATE characters SET ${cols.map((c) => `${c} = ?`).join(', ')} WHERE id = ?`) .run(...Object.values(overrides), characterId); } return { cookie, characterId }; } /** Grant an item to a character's inventory. */ function grantItem(characterId, itemId, quantity = 1) { const db = require('../../src/db').getDb(); const NOW = 1_700_000_000_000; db.prepare( 'INSERT INTO inventory (character_id, item_id, quantity, equipped_slot, acquired_at) VALUES (?, ?, ?, NULL, ?)' ).run(characterId, itemId, quantity, NOW); } // ─── Shop page loads ────────────────────────────────────────────────── it('GET /shop returns stock with character info', async () => { const { cookie } = await setupHero({ gold: 500 }); const res = await fetch(`${baseUrl}/api/shop`, { headers: authHeaders(cookie) }); const body = await res.json(); assert.equal(res.status, 200); assert.equal(body.ok, true); assert.ok(Array.isArray(body.data.stock)); assert.ok(body.data.character, 'should include character info'); assert.equal(body.data.character.gold, 500); assert.equal(body.data.character.level, 1); }); // ─── Level gating ───────────────────────────────────────────────────── it('filters high-level items from shop stock (levelOk)', async () => { const { cookie } = await setupHero({ gold: 9999, level: 1 }); const res = await fetch(`${baseUrl}/api/shop`, { headers: authHeaders(cookie) }); const body = await res.json(); // All purchasable items should be level-Ok for level-1 hero const purchasable = body.data.stock.filter((i) => i.buyPrice > 0); for (const item of purchasable) { assert.equal(item.levelOk, true, `${item.name} should be levelOk for level-1 hero`); } }); it('shows levelOk=false for high-level items when visible', async () => { // Use a level-5 hero; high-level items (Lv.10+) should be levelOk=false const { cookie } = await setupHero({ gold: 9999, level: 5 }); const res = await fetch(`${baseUrl}/api/shop`, { headers: authHeaders(cookie) }); const body = await res.json(); // Items requiring level 10+ should NOT appear for a level-5 hero // (the backend filters them out entirely via WHERE clause) const highLevelItems = body.data.stock.filter((i) => i.levelReq >= 10); assert.equal(highLevelItems.length, 0); }); // ─── Sell pricing ───────────────────────────────────────────────────── it('includes sellPrice for materials', async () => { const { cookie } = await setupHero({ gold: 100, level: 5 }); const res = await fetch(`${baseUrl}/api/shop`, { headers: authHeaders(cookie) }); const body = await res.json(); const materials = body.data.stock.filter((i) => i.buyPrice === 0); assert.ok(materials.length > 0, 'should have materials'); for (const mat of materials) { assert.ok(typeof mat.sellPrice === 'number', `${mat.name} should have sellPrice`); assert.ok(mat.sellPrice >= 1, `${mat.name} sellPrice >= 1`); } }); // ─── Buy/sell round-trip ────────────────────────────────────────────── it('buy then sell: gold decreases (spread)', async () => { const { cookie, characterId } = await setupHero({ gold: 500 }); // Buy Minor Healing Potion (item 14, buy_price 15) const buyRes = await fetch(`${baseUrl}/api/shop/buy`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...authHeaders(cookie) }, body: JSON.stringify({ itemId: 14, quantity: 1 }), }); const buyBody = await buyRes.json(); assert.equal(buyRes.status, 200); assert.equal(buyBody.ok, true); const afterBuyGold = buyBody.data.character.gold; // 500 - 15 = 485 assert.equal(afterBuyGold, 485); // Sell Rat Tail (item 17, common material, min_level 1) — must grant first grantItem(characterId, 17, 1); const sellRes = await fetch(`${baseUrl}/api/shop/sell`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...authHeaders(cookie) }, body: JSON.stringify({ itemId: 17, quantity: 1 }), }); const sellBody = await sellRes.json(); assert.equal(sellRes.status, 200); assert.equal(sellBody.ok, true); const afterSellGold = sellBody.data.character.gold; // Common material at level 1: floor(2 * 1 / 1) = 2 assert.equal(afterSellGold, 487); assert.ok(afterSellGold < 500, 'round-trip should lose gold'); }); });