618 lines
26 KiB
JavaScript
618 lines
26 KiB
JavaScript
// 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');
|
||
});
|
||
});
|