step 10.10: wip (not ticked)
This commit is contained in:
@@ -0,0 +1,226 @@
|
||||
// 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user