Files
Emberfall/tests/integration/expeditions.test.js
T
2026-10-22 22:22:44 +11:00

618 lines
26 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Expedition integration tests: POST /api/expeditions/start, GET /api/expeditions/active, POST /api/expeditions/claim.
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');
const cookie = setCookie.split(';')[0];
return cookie;
}
function authHeaders(cookie) {
return { Cookie: cookie };
}
describe('expeditions', () => {
let baseUrl, userSeq = 0;
before(async () => {
baseUrl = await setup();
});
after(async () => {
await teardown(); cleanup();
});
/**
* Register, login, create a character, optionally tweak DB fields, return cookie + characterId.
*/
async function setupHero(overrides = {}) {
userSeq += 1;
const username = `expu${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: `ExpHero${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 };
}
// ─── POST /api/expeditions/start ──────────────────────────────────────
it('starts a scout expedition (10 min, defId=1)', async () => {
const { cookie, characterId } = await setupHero({ gold: 500, energy: 50 });
const res = await fetch(`${baseUrl}/api/expeditions/start`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...authHeaders(cookie) },
body: JSON.stringify({ defId: 1 }),
});
const body = await res.json();
assert.equal(res.status, 200);
assert.equal(body.ok, true);
assert.ok(body.data.expedition, 'should return expedition object');
assert.equal(body.data.definition.slug, 'scout-old-road');
assert.equal(body.data.definition.energyCost, 10);
// Energy should be deducted
assert.equal(body.data.character.energy, 40);
});
it('rejects start when another expedition is already active', async () => {
const { cookie, characterId } = await setupHero({ gold: 500, energy: 80, level: 3 });
// Start first expedition (scout, defId=1).
const res1 = await fetch(`${baseUrl}/api/expeditions/start`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...authHeaders(cookie) },
body: JSON.stringify({ defId: 1 }),
});
assert.equal(res1.status, 200);
// Try to start a second expedition (raid, defId=2).
const res2 = await fetch(`${baseUrl}/api/expeditions/start`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...authHeaders(cookie) },
body: JSON.stringify({ defId: 2 }),
});
const body2 = await res2.json();
assert.equal(res2.status, 409);
assert.equal(body2.ok, false);
assert.equal(body2.error.code, 'ACTIVE_EXPEDITION');
});
it('rejects start when energy is insufficient', async () => {
const { cookie } = await setupHero({ gold: 500, energy: 10, level: 3 });
// Raid costs 30 energy.
const res = await fetch(`${baseUrl}/api/expeditions/start`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...authHeaders(cookie) },
body: JSON.stringify({ defId: 2 }),
});
const body = await res.json();
assert.equal(res.status, 422);
assert.equal(body.ok, false);
assert.equal(body.error.code, 'INSUFFICIENT_ENERGY');
});
it('rejects start when character level is too low', async () => {
const { cookie } = await setupHero({ gold: 500, energy: 100, level: 1 });
// Delve requires level 5.
const res = await fetch(`${baseUrl}/api/expeditions/start`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...authHeaders(cookie) },
body: JSON.stringify({ defId: 3 }),
});
const body = await res.json();
assert.equal(res.status, 422);
assert.equal(body.ok, false);
assert.equal(body.error.code, 'LEVEL_TOO_LOW');
});
it('rejects start with invalid defId', async () => {
const { cookie } = await setupHero({ gold: 500, energy: 100 });
const res = await fetch(`${baseUrl}/api/expeditions/start`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...authHeaders(cookie) },
body: JSON.stringify({ defId: 999 }),
});
const body = await res.json();
assert.equal(res.status, 404);
assert.equal(body.ok, false);
assert.equal(body.error.code, 'NOT_FOUND');
});
// ─── GET /api/expeditions/active ──────────────────────────────────────
it('returns null when no active expedition', async () => {
const { cookie } = await setupHero();
const res = await fetch(`${baseUrl}/api/expeditions/active`, {
headers: authHeaders(cookie),
});
const body = await res.json();
assert.equal(res.status, 200);
assert.equal(body.ok, true);
assert.equal(body.data, null);
});
it('returns active expedition with timeRemaining', async () => {
const { cookie } = await setupHero({ energy: 80 });
// Start an expedition first.
await fetch(`${baseUrl}/api/expeditions/start`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...authHeaders(cookie) },
body: JSON.stringify({ defId: 1 }),
});
const res = await fetch(`${baseUrl}/api/expeditions/active`, {
headers: authHeaders(cookie),
});
const body = await res.json();
assert.equal(res.status, 200);
assert.equal(body.ok, true);
assert.ok(body.data, 'should return expedition data');
assert.ok(body.data.timeRemaining > 0, 'should have positive timeRemaining');
assert.equal(body.data.isFinished, false);
assert.equal(body.data.definition.slug, 'scout-old-road');
});
// ─── POST /api/expeditions/claim ──────────────────────────────────────
it('rejects claim when expedition is not finished', async () => {
const { cookie, characterId } = await setupHero({ energy: 80 });
// Start a 10-min expedition (not finished yet).
const startRes = await fetch(`${baseUrl}/api/expeditions/start`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...authHeaders(cookie) },
body: JSON.stringify({ defId: 1 }),
});
const startBody = await startRes.json();
const expeditionId = startBody.data.expedition.id;
// Try to claim immediately — should fail.
const res = await fetch(`${baseUrl}/api/expeditions/claim`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...authHeaders(cookie) },
body: JSON.stringify({ expeditionId }),
});
const body = await res.json();
assert.equal(res.status, 409);
assert.equal(body.ok, false);
assert.equal(body.error.code, 'NOT_FINISHED');
});
it('grants XP and gold when expedition is claimed', async () => {
const { cookie, characterId } = await setupHero({ energy: 80, level: 1, xp: 10, gold: 50 });
// Start a scout expedition normally.
const startRes = await fetch(`${baseUrl}/api/expeditions/start`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...authHeaders(cookie) },
body: JSON.stringify({ defId: 1 }),
});
const startBody = await startRes.json();
const expeditionId = startBody.data.expedition.id;
// Fast-forward time past the 10-min scout duration by directly setting ends_at in DB.
// (setNow can't work because it also shifts ends_at.)
const db = require('../../src/db').getDb();
const pastTime = Date.now() - 600000; // 10 min ago
db.prepare('UPDATE expeditions SET ends_at = ? WHERE id = ?').run(pastTime, expeditionId);
// Claim it.
const res = await fetch(`${baseUrl}/api/expeditions/claim`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...authHeaders(cookie) },
body: JSON.stringify({ expeditionId }),
});
const body = await res.json();
assert.equal(res.status, 200);
assert.equal(body.ok, true);
assert.equal(body.data.xpGained, 30); // Scout XP reward
assert.ok(body.data.goldGained >= 5 && body.data.goldGained <= 15, 'gold should be in range [5,15]');
// Verify character was updated — XP is ADDED, not overwritten.
const ch = db.prepare('SELECT xp, gold, level, unspent_points FROM characters WHERE id = ?').get(characterId);
assert.equal(ch.xp, 40, 'XP should be 10 + 30 = 40 (added, not overwritten)');
assert.equal(ch.gold, 50 + body.data.goldGained, 'gold should increase');
assert.equal(ch.level, 1, 'level should still be 1 (40 < 100 xpToNext)');
assert.equal(ch.unspent_points, 0, 'no new stat points at level 1');
});
it('10.2: claim with level-up → correct XP remainder, unspent_points, and full heal', async () => {
const { cookie, characterId } = await setupHero({ energy: 80, level: 1, xp: 80, gold: 50 });
// Scout gives 30 XP. 80 + 30 = 110 >= xpToNext(1)=100 → level up to 2, xp remainder = 10, +3 points.
const startRes = await fetch(`${baseUrl}/api/expeditions/start`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...authHeaders(cookie) },
body: JSON.stringify({ defId: 1 }),
});
const startBody = await startRes.json();
const expeditionId = startBody.data.expedition.id;
const db = require('../../src/db').getDb();
db.prepare('UPDATE expeditions SET ends_at = ? WHERE id = ?').run(Date.now() - 600000, expeditionId);
const res = await fetch(`${baseUrl}/api/expeditions/claim`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...authHeaders(cookie) },
body: JSON.stringify({ expeditionId }),
});
const body = await res.json();
assert.equal(res.status, 200);
assert.equal(body.ok, true);
assert.equal(body.data.xpGained, 30);
// Verify DB state.
const ch = db.prepare('SELECT xp, level, unspent_points, hp_current FROM characters WHERE id = ?').get(characterId);
assert.equal(ch.level, 2, 'should have leveled up to 2');
assert.equal(ch.xp, 10, 'XP remainder should be 110 - 100 = 10');
assert.equal(ch.unspent_points, 3, 'should gain 3 stat points on level-up');
assert.ok(ch.hp_current >= 100, 'HP should be healed to new maxHp on level-up');
});
it('10.2: claim with two-level span → both levels gained, +6 points', async () => {
const { cookie, characterId } = await setupHero({ energy: 80, level: 1, xp: 95, gold: 50 });
// 95 + 30 = 125. xpToNext(1)=100 → level 2, remainder=25. xpToNext(2)=282 → 25 < 282, only 1 level.
// Need more XP. Use raid (defId=2, xp=100). 95 + 100 = 195.
// xpToNext(1)=100 → level 2, remainder=95. xpToNext(2)=282 → 95 < 282, only 1 level.
// Use delve (defId=3, xp=250). 95 + 250 = 345.
// xpToNext(1)=100 → level 2, remainder=245. xpToNext(2)=282 → 245 < 282, only 1 level.
// Need xpToNext(1)+xpToNext(2)=100+282=382. Use 345 → still only 1 level.
// Let's use xp=95 + delves 250 = 345. 345 >= 100+282=382? No.
// Use xp=95 + 250 = 345. 345-100=245 (level 2). 245 < 282. Only 1 level.
// Use xp=99 + delve 250 = 349. 349-100=249 (level 2). 249 < 282. Still 1 level.
// Need 383 for 2 levels. xp=1 + 250 = 251. 251-100=151 (level 2). 151 < 282.
// xp=99 + 300? No expedition gives 300. Let's use xp=99 + delve(250) = 349.
// Hmm, 349-100=249, 249 < 282. Only 1 level.
// Use level 2, xp=200 + delve(250) = 450. 450-282=168 (level 3). 168 < 562. 1 level.
// level 1, xp=99 + delve(250) = 349. 349-100=249. 249 < 282.
// level 1, xp=99 + 284? No expedition gives 284.
// Actually let's just use a direct DB injection for the expedition reward.
// We can't change the XP reward from the DB easily. Let's use a different approach:
// Set hero to level 1, xp=99. Start a delve (xp=250). 99+250=349.
// 349 >= 100 → level 2, remainder 249. 249 < 282 → stop. Only 1 level.
// To get 2 levels: need xpGained >= xpToNext(level) + xpToNext(level+1).
// At level 1: 100 + 282 = 382. No single expedition gives 382.
// Let's just set the hero's XP manually after setup to test the logic.
// Actually, we can set the expedition ends_at in DB and claim. The XP reward comes from the def.
// Let's use a different approach: set hero to level 1, xp=0, gold=50, energy=80.
// Start delve (xp=250). 0+250=250. 250>=100 → level 2, remainder=150. 150<282. 1 level.
// level 1, xp=99 + 250 = 349. 349-100=249. 249<282. 1 level.
// We need 383. Let's just manually set the expedition's xp_reward via DB for this test.
// Or better: just set hero to level 1, xp=99, and manually bump the expedition reward.
// Simplest: use level 1, xp=99, and set expedition xp_reward to 400 temporarily.
// Actually, let's just set the character's xp to 99 + start a delve(250) = 349.
// 349 >= 100+282=382? No. So we can't get 2 levels with existing defs.
// Let's just test 1 level-up and accept that 2-level is a future test.
// Actually wait: we can directly inject the expedition row with a custom xp_reward.
// No, that's fragile. Let's just set xp=99, start delve(250), and verify 1 level-up.
// For 2 levels, we'd need a higher XP expedition or to modify def data.
// Let's test 2 levels by injecting a custom expedition row.
const db2 = require('../../src/db').getDb();
// Insert a custom expedition row with high XP reward to test multi-level.
db2.prepare('DELETE FROM expeditions WHERE character_id = ?').run(characterId);
db2.prepare('INSERT INTO expeditions (character_id, def_id, started_at, ends_at) VALUES (?, 3, ?, ?)')
.run(characterId, Date.now() - 700000, Date.now() - 600000);
// Override the xp_reward for this test by patching the def temporarily.
const oldXp = db2.prepare('SELECT xp_reward FROM expedition_defs WHERE id = 3').get().xp_reward;
db2.prepare('UPDATE expedition_defs SET xp_reward = 400 WHERE id = 3').run();
const res2 = await fetch(`${baseUrl}/api/expeditions/claim`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...authHeaders(cookie) },
body: JSON.stringify({ expeditionId: db2.prepare('SELECT id FROM expeditions WHERE character_id = ? AND claimed_at IS NULL').get(characterId).id }),
});
const body2 = await res2.json();
assert.equal(res2.status, 200);
assert.equal(body2.ok, true);
const ch2 = db2.prepare('SELECT xp, level, unspent_points FROM characters WHERE id = ?').get(characterId);
assert.equal(ch2.level, 3, 'should have leveled up twice (1→3)');
assert.equal(ch2.unspent_points, 6, 'should gain 6 stat points (2 levels × 3)');
// Restore original xp_reward.
db2.prepare('UPDATE expedition_defs SET xp_reward = ? WHERE id = 3').run(oldXp);
});
it('10.2: double claim still rejected after successful claim', async () => {
const { cookie, characterId } = await setupHero({ energy: 80, level: 1, xp: 0, gold: 50 });
const startRes = await fetch(`${baseUrl}/api/expeditions/start`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...authHeaders(cookie) },
body: JSON.stringify({ defId: 1 }),
});
const startBody = await startRes.json();
const expeditionId = startBody.data.expedition.id;
const db = require('../../src/db').getDb();
db.prepare('UPDATE expeditions SET ends_at = ? WHERE id = ?').run(Date.now() - 600000, expeditionId);
// First claim — should succeed.
const res1 = await fetch(`${baseUrl}/api/expeditions/claim`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...authHeaders(cookie) },
body: JSON.stringify({ expeditionId }),
});
assert.equal(res1.status, 200);
// Second claim — should be rejected.
const res2 = await fetch(`${baseUrl}/api/expeditions/claim`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...authHeaders(cookie) },
body: JSON.stringify({ expeditionId }),
});
const body2 = await res2.json();
assert.equal(res2.status, 409);
assert.equal(body2.ok, false);
assert.equal(body2.error.code, 'ALREADY_CLAIMED');
});
it('10.2: gold is correctly added to existing gold on claim', async () => {
const { cookie, characterId } = await setupHero({ energy: 80, level: 1, xp: 0, gold: 200 });
const startRes = await fetch(`${baseUrl}/api/expeditions/start`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...authHeaders(cookie) },
body: JSON.stringify({ defId: 1 }),
});
const startBody = await startRes.json();
const expeditionId = startBody.data.expedition.id;
const db = require('../../src/db').getDb();
db.prepare('UPDATE expeditions SET ends_at = ? WHERE id = ?').run(Date.now() - 600000, expeditionId);
const res = await fetch(`${baseUrl}/api/expeditions/claim`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...authHeaders(cookie) },
body: JSON.stringify({ expeditionId }),
});
const body = await res.json();
assert.equal(res.status, 200);
assert.equal(body.ok, true);
const ch = db.prepare('SELECT gold FROM characters WHERE id = ?').get(characterId);
assert.equal(ch.gold, 200 + body.data.goldGained, 'gold should be 200 + reward');
});
it('rejects double-claim (claimed_at already set)', async () => {
const { cookie, characterId } = await setupHero({ energy: 80 });
// Start a scout expedition.
const startRes = await fetch(`${baseUrl}/api/expeditions/start`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...authHeaders(cookie) },
body: JSON.stringify({ defId: 1 }),
});
const startBody = await startRes.json();
const expeditionId = startBody.data.expedition.id;
// Fast-forward time past the 10-min scout duration via DB.
const db = require('../../src/db').getDb();
const pastTime = Date.now() - 600000; // 10 min ago
db.prepare('UPDATE expeditions SET ends_at = ? WHERE id = ?').run(pastTime, expeditionId);
// Claim once.
await fetch(`${baseUrl}/api/expeditions/claim`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...authHeaders(cookie) },
body: JSON.stringify({ expeditionId }),
});
// Try to claim again.
const res = await fetch(`${baseUrl}/api/expeditions/claim`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...authHeaders(cookie) },
body: JSON.stringify({ expeditionId }),
});
const body = await res.json();
assert.equal(res.status, 409);
assert.equal(body.ok, false);
assert.equal(body.error.code, 'ALREADY_CLAIMED');
});
it('rejects claim for expedition that does not belong to user', async () => {
const { cookie, characterId } = await setupHero({ energy: 80 });
// Create another user + character.
userSeq += 1;
const otherCookie = await registerAndLogin(baseUrl, `expu_other`, 'password123');
const charRes = await fetch(`${baseUrl}/api/character`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...authHeaders(otherCookie) },
body: JSON.stringify({ name: `ExpHeroOther`, class: 'mage' }),
});
const charJson = await charRes.json();
const otherCharacterId = charJson.data.character.id;
// Grant an expedition to the other character.
const db = require('../../src/db').getDb();
const now = Date.now();
const insertRes = db.prepare('INSERT INTO expeditions (character_id, def_id, started_at, ends_at) VALUES (?, ?, ?, ?)')
.run(otherCharacterId, 1, now - 600000, now - 500000);
const otherExpeditionId = insertRes.lastInsertRowid;
// Try to claim the other user's expedition.
const res = await fetch(`${baseUrl}/api/expeditions/claim`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...authHeaders(cookie) },
body: JSON.stringify({ expeditionId: otherExpeditionId }),
});
const body = await res.json();
assert.equal(res.status, 403); // expedition exists but belongs to another user
assert.equal(body.ok, false);
assert.equal(body.error.code, 'FORBIDDEN');
});
it('active expedition shows isFinished=true after ends_at', async () => {
const { cookie } = await setupHero({ energy: 80 });
// Start a scout expedition.
await fetch(`${baseUrl}/api/expeditions/start`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...authHeaders(cookie) },
body: JSON.stringify({ defId: 1 }),
});
// Fast-forward time past the 10-min scout duration via DB.
const db = require('../../src/db').getDb();
const pastTime = Date.now() - 600000; // 10 min ago
db.prepare('UPDATE expeditions SET ends_at = ? WHERE character_id IN (SELECT id FROM characters WHERE id IN (SELECT character_id FROM expeditions WHERE claimed_at IS NULL))').run(pastTime);
const res = await fetch(`${baseUrl}/api/expeditions/active`, {
headers: authHeaders(cookie),
});
const body = await res.json();
assert.equal(res.status, 200);
assert.equal(body.ok, true);
assert.equal(body.data.isFinished, true, 'should show finished after ends_at');
assert.equal(body.data.timeRemaining, 0);
});
// ─── GET /api/expeditions/defs ────────────────────────────────────────
it('returns all expedition definitions ordered by min_level', async () => {
const { cookie } = await setupHero();
const res = await fetch(`${baseUrl}/api/expeditions/defs`, {
headers: authHeaders(cookie),
});
const body = await res.json();
assert.equal(res.status, 200);
assert.equal(body.ok, true);
assert.ok(Array.isArray(body.data), 'should return an array');
assert.equal(body.data.length, 3, 'should have 3 expedition definitions');
// First definition should be the lowest-level one.
assert.equal(body.data[0].id, 1);
assert.equal(body.data[0].minLevel, 1);
assert.equal(body.data[0].name, 'Scout the Old Road');
assert.equal(body.data[0].energyCost, 10);
assert.equal(body.data[0].durationSec, 600);
assert.equal(body.data[0].xpReward, 30);
assert.equal(body.data[0].goldMin, 5);
assert.equal(body.data[0].goldMax, 15);
// Second definition.
assert.equal(body.data[1].id, 2);
assert.equal(body.data[1].minLevel, 3);
assert.equal(body.data[1].name, 'Raid the Bandit Camp');
// Third definition.
assert.equal(body.data[2].id, 3);
assert.equal(body.data[2].minLevel, 5);
assert.equal(body.data[2].name, 'Delve the Hollow');
});
it('rejects defs endpoint without authentication', async () => {
const res = await fetch(`${baseUrl}/api/expeditions/defs`);
const body = await res.json();
assert.equal(res.status, 401);
assert.equal(body.ok, false);
});
// ─── 10.1: expedition start must deduct energy correctly ──────────────
it('10.1: starts expedition → DB energy = energy - cost, timestamp NOT in future', async () => {
const { cookie, characterId } = await setupHero({ energy: 100, gold: 500 });
const res = await fetch(`${baseUrl}/api/expeditions/start`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...authHeaders(cookie) },
body: JSON.stringify({ defId: 1 }), // scout, cost=10
});
assert.equal(res.status, 200);
const body = await res.json();
assert.equal(body.ok, true);
// Assert DB state directly — this is the core of 10.1.
const db = require('../../src/db').getDb();
const ch = db.prepare('SELECT energy, energy_updated_at FROM characters WHERE id = ?').get(characterId);
assert.equal(ch.energy, 90, 'DB energy must be 100 - 10 = 90');
assert.ok(ch.energy_updated_at <= Date.now(), 'energy_updated_at must NOT be in the future');
// Assert GET /api/character returns the same energy.
const charRes = await fetch(`${baseUrl}/api/character`, {
headers: authHeaders(cookie),
});
assert.equal(charRes.status, 200);
const charBody = await charRes.json();
assert.equal(charBody.data.energy.current, 90);
});
it('10.1: starting at energy 55 → DB energy = 45', async () => {
const { cookie, characterId } = await setupHero({ energy: 55, gold: 500 });
const res = await fetch(`${baseUrl}/api/expeditions/start`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...authHeaders(cookie) },
body: JSON.stringify({ defId: 1 }), // scout, cost=10
});
assert.equal(res.status, 200);
const body = await res.json();
assert.equal(body.ok, true);
const db = require('../../src/db').getDb();
const ch = db.prepare('SELECT energy FROM characters WHERE id = ?').get(characterId);
assert.equal(ch.energy, 45, 'DB energy must be 55 - 10 = 45');
});
});