step 6.3: done

This commit is contained in:
bepis
2026-10-21 01:10:23 +11:00
parent 2930c70010
commit cf5c4616d8
4 changed files with 91 additions and 22 deletions
+58
View File
@@ -236,6 +236,39 @@ describe('shop', () => {
assert.equal(body.error.code, 'VALIDATION_ERROR');
});
it('concurrent double-buy: only one succeeds when gold covers exactly one', async () => {
const { cookie, characterId } = await setupHero({ gold: 20 });
// Rusty Sword costs 20 gold — exactly enough for one.
// Send two buy requests concurrently.
const [resA, resB] = await Promise.all([
fetch(`${baseUrl}/api/shop/buy`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...authHeaders(cookie) },
body: JSON.stringify({ itemId: 1, quantity: 1 }),
}),
fetch(`${baseUrl}/api/shop/buy`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...authHeaders(cookie) },
body: JSON.stringify({ itemId: 1, quantity: 1 }),
}),
]);
const bodyA = await resA.json();
const bodyB = await resB.json();
// Exactly one should succeed, the other should fail with INSUFFICIENT_GOLD.
const success = [bodyA, bodyB].filter((b) => b.ok === true);
const failures = [bodyA, bodyB].filter((b) => b.ok === false);
assert.equal(success.length, 1, 'exactly one buy should succeed');
assert.equal(failures.length, 1, 'exactly one buy should fail');
assert.equal(failures[0].error.code, 'INSUFFICIENT_GOLD');
// Gold should be 0 after one successful buy.
const winner = success[0];
assert.equal(winner.data.character.gold, 0);
});
// ─── POST /api/shop/sell ──────────────────────────────────────────────
it('sells a material and increases gold', async () => {
@@ -336,6 +369,31 @@ describe('shop', () => {
assert.equal(body.error.code, 'NOT_FOUND');
});
// ─── Sell equipped item rejection ─────────────────────────────────────
it('rejects selling an equipped item', async () => {
const { cookie, characterId } = await setupHero({ gold: 100 });
// Grant and equip Rusty Sword (item 1, weapon)
grantItem(characterId, 1, 1);
const db = require('../../src/db').getDb();
db.prepare('UPDATE inventory SET equipped_slot = ? WHERE character_id = ? AND item_id = ?').run('weapon', characterId, 1);
// Try to sell the equipped Rusty Sword
const res = await fetch(`${baseUrl}/api/shop/sell`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...authHeaders(cookie) },
body: JSON.stringify({ itemId: 1, quantity: 1 }),
});
const body = await res.json();
assert.equal(res.status, 400);
assert.equal(body.ok, false);
assert.equal(body.error.code, 'VALIDATION_ERROR');
assert.ok(body.error.message.includes('Unequip'), 'should mention unequipping');
});
// ─── Round-trip ───────────────────────────────────────────────────────
it('buy and sell round-trip: buy then sell loses gold (spread)', async () => {