step 6.2: done
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
// 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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user