227 lines
9.5 KiB
JavaScript
227 lines
9.5 KiB
JavaScript
// Step 10.10 — Rest endpoint: POST /api/character/rest
|
|
// Costs level*5 gold, heals to full max HP.
|
|
// Errors: NOTHING_TO_HEAL (422) if HP already full, INSUFFICIENT_GOLD (422).
|
|
const { describe, it, before, after } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const { setup, teardown, cleanup } = require('../helpers');
|
|
|
|
describe('Step 10.10 — Rest endpoint', () => {
|
|
let baseUrl;
|
|
let loginCookie;
|
|
|
|
function registerAndLogin(username) {
|
|
return fetch(`${baseUrl}/api/auth/register`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ username, password: 'password123' }),
|
|
}).then(() => fetch(`${baseUrl}/api/auth/login`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ username, password: 'password123' }),
|
|
}));
|
|
}
|
|
|
|
function createHero(cookie, name) {
|
|
return fetch(`${baseUrl}/api/character`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', Cookie: cookie },
|
|
body: JSON.stringify({ name, class: 'warrior' }),
|
|
});
|
|
}
|
|
|
|
function getCharacter(cookie) {
|
|
return fetch(`${baseUrl}/api/character`, {
|
|
method: 'GET',
|
|
headers: { Cookie: cookie },
|
|
});
|
|
}
|
|
|
|
function rest(cookie) {
|
|
return fetch(`${baseUrl}/api/character/rest`, {
|
|
method: 'POST',
|
|
headers: { Cookie: cookie },
|
|
});
|
|
}
|
|
|
|
function setupHero(username, name, hp, gold) {
|
|
return registerAndLogin(username)
|
|
.then((res) => {
|
|
loginCookie = res.headers.get('set-cookie');
|
|
return createHero(loginCookie, name);
|
|
})
|
|
.then(() => {
|
|
const db = require('../../src/db').getDb();
|
|
const userId = Number(db.prepare('SELECT id FROM users WHERE username = ?').get(username).id);
|
|
const char = db.prepare('SELECT id FROM characters WHERE user_id = ?').get(userId);
|
|
db.prepare('UPDATE characters SET hp_current = ?, gold = ? WHERE id = ?').run(hp, gold, char.id);
|
|
});
|
|
}
|
|
|
|
before(async () => {
|
|
baseUrl = await setup();
|
|
});
|
|
|
|
after(async () => {
|
|
const { resetNow } = require('../../src/utils/time');
|
|
resetNow();
|
|
await teardown();
|
|
cleanup();
|
|
});
|
|
|
|
it('rest heals to full max HP and deducts level*5 gold', async () => {
|
|
await setupHero('rest1', 'RestHero1', 10, 100);
|
|
|
|
const charRes = await getCharacter(loginCookie);
|
|
assert.equal(charRes.status, 200);
|
|
const charData = await charRes.json();
|
|
const level = charData.data.character.level;
|
|
const maxHp = charData.data.derived.maxHp;
|
|
const goldBefore = charData.data.character.gold;
|
|
const cost = level * 5;
|
|
|
|
const restRes = await rest(loginCookie);
|
|
assert.equal(restRes.status, 200);
|
|
const restData = await restRes.json();
|
|
assert.equal(restData.ok, true);
|
|
|
|
// HP should be at max
|
|
assert.equal(restData.data.character.hp_current, maxHp);
|
|
// Gold should be reduced by cost
|
|
assert.equal(restData.data.character.gold, goldBefore - cost);
|
|
// Character data and derived included
|
|
assert.ok(restData.data.character, 'response includes updated character');
|
|
assert.ok(restData.data.derived, 'response includes updated derived stats');
|
|
|
|
// Verify in DB
|
|
const db = require('../../src/db').getDb();
|
|
const userId = Number(db.prepare('SELECT id FROM users WHERE username = ?').get('rest1').id);
|
|
const char = db.prepare('SELECT hp_current, gold FROM characters WHERE user_id = ?').get(userId);
|
|
assert.equal(char.hp_current, maxHp);
|
|
assert.equal(char.gold, goldBefore - cost);
|
|
});
|
|
|
|
it('rest at full HP returns 422 NOTHING_TO_HEAL and gold unchanged', async () => {
|
|
// Create a hero at level 5 (maxHp = 50 + 8*10 + 5*5 = 165), set hp to max
|
|
await setupHero('rest2', 'RestHero2', 10, 100);
|
|
// Boost to level 5 so maxHp is higher, then set hp to match maxHp
|
|
const db = require('../../src/db').getDb();
|
|
const userId = Number(db.prepare('SELECT id FROM users WHERE username = ?').get('rest2').id);
|
|
const char = db.prepare('SELECT id FROM characters WHERE user_id = ?').get(userId);
|
|
db.prepare('UPDATE characters SET level = 5, hp_current = 165 WHERE id = ?').run(char.id);
|
|
|
|
const charRes = await getCharacter(loginCookie);
|
|
assert.equal(charRes.status, 200);
|
|
const charData = await charRes.json();
|
|
const goldBefore = charData.data.character.gold;
|
|
|
|
const restRes = await rest(loginCookie);
|
|
assert.equal(restRes.status, 422);
|
|
const restData = await restRes.json();
|
|
assert.equal(restData.ok, false);
|
|
assert.equal(restData.error.code, 'NOTHING_TO_HEAL');
|
|
|
|
// Gold should be unchanged
|
|
assert.equal(goldBefore, charData.data.character.gold);
|
|
|
|
// Verify in DB
|
|
const db2 = require('../../src/db').getDb();
|
|
const charAfter = db2.prepare('SELECT hp_current, gold FROM characters WHERE id = ?').get(char.id);
|
|
assert.equal(charAfter.hp_current, 165);
|
|
assert.equal(charAfter.gold, 100);
|
|
});
|
|
|
|
it('rest with insufficient gold returns 422 INSUFFICIENT_GOLD and HP unchanged', async () => {
|
|
// Level 5 hero costs 25 gold to rest; give only 5 gold
|
|
await setupHero('rest3', 'RestHero3', 10, 5);
|
|
const db = require('../../src/db').getDb();
|
|
const userId = Number(db.prepare('SELECT id FROM users WHERE username = ?').get('rest3').id);
|
|
const char = db.prepare('SELECT id FROM characters WHERE user_id = ?').get(userId);
|
|
db.prepare('UPDATE characters SET level = 5, hp_current = 10, gold = 5 WHERE id = ?').run(char.id);
|
|
|
|
const charRes = await getCharacter(loginCookie);
|
|
assert.equal(charRes.status, 200);
|
|
const charData = await charRes.json();
|
|
const hpBefore = charData.data.character.hp_current;
|
|
|
|
const restRes = await rest(loginCookie);
|
|
assert.equal(restRes.status, 422);
|
|
const restData = await restRes.json();
|
|
assert.equal(restData.ok, false);
|
|
assert.equal(restData.error.code, 'INSUFFICIENT_GOLD');
|
|
|
|
// HP should be unchanged
|
|
assert.equal(hpBefore, charData.data.character.hp_current);
|
|
|
|
// Verify in DB
|
|
const db2 = require('../../src/db').getDb();
|
|
const charAfter = db2.prepare('SELECT hp_current, gold FROM characters WHERE id = ?').get(char.id);
|
|
assert.equal(charAfter.hp_current, 10);
|
|
assert.equal(charAfter.gold, 5);
|
|
});
|
|
|
|
it('rest on another user\'s character is rejected (cross-user isolation)', async () => {
|
|
// Create a second user (the "victim")
|
|
const victimRes = await registerAndLogin('rest4victim');
|
|
const victimCookie = victimRes.headers.get('set-cookie');
|
|
await createHero(victimCookie, 'VictimHero');
|
|
const db = require('../../src/db').getDb();
|
|
const victimUserId = Number(db.prepare('SELECT id FROM users WHERE username = ?').get('rest4victim').id);
|
|
const victimChar = db.prepare('SELECT id FROM characters WHERE user_id = ?').get(victimUserId);
|
|
db.prepare('UPDATE characters SET hp_current = 10, gold = 100 WHERE id = ?').run(victimChar.id);
|
|
|
|
// Also set up rest1 with enough gold to succeed if it worked on their own char
|
|
const r1 = await registerAndLogin('rest1');
|
|
loginCookie = r1.headers.get('set-cookie');
|
|
await createHero(loginCookie, 'RestHero1');
|
|
const r1UserId = Number(db.prepare('SELECT id FROM users WHERE username = ?').get('rest1').id);
|
|
const r1Char = db.prepare('SELECT id FROM characters WHERE user_id = ?').get(r1UserId);
|
|
db.prepare('UPDATE characters SET hp_current = 10, gold = 100 WHERE id = ?').run(r1Char.id);
|
|
|
|
// Try to rest with rest1's session — should succeed for rest1, NOT affect victim
|
|
const restRes = await rest(loginCookie);
|
|
assert.equal(restRes.status, 200);
|
|
|
|
// Victim's HP should be unchanged (rest1 can't rest on victim)
|
|
const db2 = require('../../src/db').getDb();
|
|
const charAfter = db2.prepare('SELECT hp_current FROM characters WHERE id = ?').get(victimChar.id);
|
|
assert.equal(charAfter.hp_current, 10);
|
|
});
|
|
|
|
it('rest cost scales with level: level 1 costs 5g, level 5 costs 25g', async () => {
|
|
// Level 1 hero — verify cost = 1 * 5 = 5
|
|
await setupHero('rest5a', 'RestHero5A', 10, 50);
|
|
const charResA = await getCharacter(loginCookie);
|
|
assert.equal(charResA.status, 200);
|
|
const charDataA = await charResA.json();
|
|
assert.ok(charDataA.data, 'response must have data');
|
|
assert.ok(charDataA.data.character, 'response data must have character');
|
|
const goldBeforeA = charDataA.data.character.gold;
|
|
|
|
const restResA = await rest(loginCookie);
|
|
assert.equal(restResA.status, 200);
|
|
const restDataA = await restResA.json();
|
|
assert.equal(restDataA.data.character.gold, goldBeforeA - 5);
|
|
|
|
// Level 5 hero — verify cost = 5 * 5 = 25
|
|
const resB = await registerAndLogin('rest5b');
|
|
loginCookie = resB.headers.get('set-cookie');
|
|
await createHero(loginCookie, 'RestHero5B');
|
|
const db = require('../../src/db').getDb();
|
|
const userIdB = Number(db.prepare('SELECT id FROM users WHERE username = ?').get('rest5b').id);
|
|
const charB = db.prepare('SELECT id FROM characters WHERE user_id = ?').get(userIdB);
|
|
db.prepare('UPDATE characters SET level = 5, hp_current = 10, gold = 50 WHERE id = ?').run(charB.id);
|
|
|
|
const charResB = await getCharacter(loginCookie);
|
|
assert.equal(charResB.status, 200);
|
|
const charDataB = await charResB.json();
|
|
assert.ok(charDataB.data, 'response must have data');
|
|
assert.ok(charDataB.data.character, 'response data must have character');
|
|
const goldBeforeB = charDataB.data.character.gold;
|
|
|
|
const restResB = await rest(loginCookie);
|
|
assert.equal(restResB.status, 200);
|
|
const restDataB = await restResB.json();
|
|
assert.equal(restDataB.data.character.gold, goldBeforeB - 25);
|
|
});
|
|
});
|