Prototype public deal agreements
This commit is contained in:
1 parent
c3f37a574d
commit
86c51b4aa7
15 files changed
+799
-7
No files matched your search
@@ -0,0 +1,105 @@
|
||||
// @dune-suite integration
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import type { GameView } from '../game/engine';
|
||||
|
||||
const base = process.env.DUNE_TEST_URL ?? 'http://localhost:3000';
|
||||
async function request(path: string, cookie?: string, body?: unknown, selected?: string) {
|
||||
const response = await fetch(base + path, {
|
||||
method: body === undefined ? 'GET' : 'POST',
|
||||
signal: AbortSignal.timeout(15_000),
|
||||
headers: {
|
||||
...(body === undefined ? {} : { 'Content-Type': 'application/json' }),
|
||||
...(cookie ? { cookie } : {}),
|
||||
...(selected ? { 'X-Dune-Seat': selected } : {}),
|
||||
},
|
||||
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
||||
});
|
||||
return {
|
||||
status: response.status,
|
||||
cookie: response.headers.get('set-cookie')?.split(';')[0],
|
||||
data: await response.json() as GameView & { error?: string },
|
||||
};
|
||||
}
|
||||
|
||||
void test('public deals use authenticated seats, persist for observers, and preserve normal gameplay', async () => {
|
||||
const host = await request('/api/rooms', undefined, {
|
||||
name: 'Deal host', faction: 'atreides', advanced: false, expansions: [],
|
||||
});
|
||||
assert.equal(host.status, 201); assert.ok(host.cookie);
|
||||
const path = `/api/rooms/${host.data.code}`;
|
||||
const guest = await request(path, undefined, { type: 'join', name: 'Deal recipient', faction: 'harkonnen' });
|
||||
assert.equal(guest.status, 200); assert.ok(guest.cookie);
|
||||
const observer = await request(path, undefined, { type: 'join', name: 'Deal observer', faction: 'emperor' });
|
||||
assert.equal(observer.status, 200); assert.ok(observer.cookie);
|
||||
const seats = [
|
||||
{ id: host.data.me, cookie: host.cookie },
|
||||
{ id: guest.data.me, cookie: guest.cookie },
|
||||
{ id: observer.data.me, cookie: observer.cookie },
|
||||
];
|
||||
let view = observer.data;
|
||||
const act = async (cookie: string, action: object) => {
|
||||
const next = await request(path, cookie, { version: view.version, action });
|
||||
assert.equal(next.status, 200, JSON.stringify(next.data));
|
||||
view = next.data;
|
||||
};
|
||||
for (const seat of seats) await act(seat.cookie, { type: 'ready' });
|
||||
await act(host.cookie, { type: 'start' });
|
||||
for (const seat of seats) {
|
||||
const own = await request(path, seat.cookie);
|
||||
assert.equal(own.status, 200);
|
||||
view = own.data;
|
||||
const choices = own.data.players.find(player => player.id === seat.id)?.traitorChoices;
|
||||
if (choices?.length) await act(seat.cookie, { type: 'traitor', leader: choices[0] });
|
||||
}
|
||||
assert.equal(view.status, 'playing');
|
||||
assert.equal(view.phase, 0);
|
||||
const baseline = {
|
||||
phase: view.phase, turn: view.turn, active: view.active, storm: view.storm,
|
||||
stormDialers: view.stormDialers, stormSubmitted: view.stormSubmitted,
|
||||
players: view.players.map(({ id, faction, ready }) => ({ id, faction, ready })),
|
||||
};
|
||||
|
||||
await act(host.cookie, { type: 'dealOffer', target: guest.data.me, terms: 'I will not attack your forces this turn.' });
|
||||
const offer = view.deals.at(-1);
|
||||
assert.ok(offer);
|
||||
assert.equal(offer.status, 'offered');
|
||||
assert.equal(offer.from, host.data.me);
|
||||
assert.equal(offer.to, guest.data.me);
|
||||
for (const seat of seats) {
|
||||
const read = await request(path, seat.cookie);
|
||||
assert.equal(read.status, 200);
|
||||
assert.deepEqual(read.data.deals.find(deal => deal.id === offer.id), offer);
|
||||
}
|
||||
|
||||
// The selected seat header is not a credential, and an authenticated
|
||||
// bystander cannot answer on the recipient's behalf.
|
||||
const accept = { version: view.version, action: { type: 'dealRespond', deal: offer.id, accept: true } };
|
||||
assert.equal((await request(path, host.cookie, accept, guest.data.me)).status, 409);
|
||||
assert.equal((await request(path, observer.cookie, accept)).status, 409);
|
||||
const unchanged = await request(path, guest.cookie);
|
||||
assert.equal(unchanged.status, 200);
|
||||
assert.equal(unchanged.data.version, view.version);
|
||||
assert.equal(unchanged.data.deals.find(deal => deal.id === offer.id)?.status, 'offered');
|
||||
await act(guest.cookie, accept.action);
|
||||
assert.equal(view.deals.find(deal => deal.id === offer.id)?.status, 'active');
|
||||
await act(host.cookie, { type: 'dealRelease', deal: offer.id, agree: true });
|
||||
assert.equal(view.deals.find(deal => deal.id === offer.id)?.status, 'active');
|
||||
await act(guest.cookie, { type: 'dealRelease', deal: offer.id, agree: true });
|
||||
assert.equal(view.deals.find(deal => deal.id === offer.id)?.status, 'released');
|
||||
const restored = await request(path, observer.cookie);
|
||||
assert.equal(restored.status, 200);
|
||||
assert.equal(restored.data.deals.find(deal => deal.id === offer.id)?.status, 'released');
|
||||
assert.deepEqual({
|
||||
phase: restored.data.phase, turn: restored.data.turn, active: restored.data.active, storm: restored.data.storm,
|
||||
stormDialers: restored.data.stormDialers, stormSubmitted: restored.data.stormSubmitted,
|
||||
players: restored.data.players.map(({ id, faction, ready }) => ({ id, faction, ready })),
|
||||
}, baseline);
|
||||
|
||||
// A native storm dial still succeeds after the agreement lifecycle.
|
||||
const dialer = seats.find(seat => restored.data.stormDialers.includes(seat.id));
|
||||
assert.ok(dialer);
|
||||
view = restored.data;
|
||||
await act(dialer.cookie, { type: 'stormDial', amount: 0 });
|
||||
assert.ok(view.stormSubmitted.includes(dialer.id) || view.phase !== 0);
|
||||
});
|
||||
Reference in new issue
Block a user