// 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); }); });