step 7.2: done
This commit is contained in:
@@ -0,0 +1,363 @@
|
||||
// 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: 0, 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.
|
||||
const ch = db.prepare('SELECT xp, gold FROM characters WHERE id = ?').get(characterId);
|
||||
assert.equal(ch.xp, 30, 'XP should be 30');
|
||||
assert.equal(ch.gold, 50 + body.data.goldGained, 'gold should increase');
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user