Files
Dune/tests/nexus-emperor-purchase-quote.test.ts

126 lines
4.4 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import {
EmperorNexusPurchaseError,
quoteEmperorNexusPurchase,
emperorNexusPurchaseEvent,
signEmperorNexusPurchase,
validateEmperorNexusPurchase,
type EmperorNexusBankPurchase,
} from '../game/nexus-emperor-purchase';
import { emperorNexusEvent } from '../game/nexus-emperor-secret-ally';
const context = {
turn: 4,
advanced: false,
players: [
{ id: 'winner', faction: 'atreides' as const },
{ id: 'rival', faction: 'guild' as const },
],
};
function purchase(): EmperorNexusBankPurchase {
const receipt: EmperorNexusBankPurchase = {
kind: 'purchase',
event: emperorNexusPurchaseEvent(3, 'winner', 2, 1, 4),
owner: 'winner',
turn: 3,
phase: 3,
sequence: 2,
faction: 'atreides',
advanced: false,
price: 4,
auctionIndex: 1,
card: 'treachery-17',
beforeSpice: 7,
afterSpice: 7,
signature: '',
};
receipt.signature = signEmperorNexusPurchase(receipt);
return receipt;
}
void test('only a positive whole bid affordable personally and without ally contribution can be quoted', () => {
for (const [price, ownSpice, allyPayment] of [
[0, 10, 0], [-1, 10, 0], [1.5, 10, 0], [NaN, 10, 0],
[Infinity, 10, 0], [Number.MAX_SAFE_INTEGER + 1, Number.MAX_SAFE_INTEGER + 1, 0],
[5, 4, 0], [5, 4.5, 0], [5, -1, 0], [5, Infinity, 0],
[5, 10, 1], [5, 10, -1], [5, 10, NaN],
]) {
assert.throws(
() => quoteEmperorNexusPurchase(price, ownSpice, allyPayment),
EmperorNexusPurchaseError,
`price=${price} own=${ownSpice} ally=${allyPayment}`,
);
}
});
void test('receipt fields and financial transitions cannot be changed after signing', () => {
const receipt = purchase();
const invalid = (patch: Partial<EmperorNexusBankPurchase>) =>
assert.throws(
() => validateEmperorNexusPurchase(context, { ...receipt, ...patch }),
EmperorNexusPurchaseError,
);
invalid({ kind: 'revival' as never });
invalid({ owner: 'rival' });
invalid({ faction: 'guild' });
invalid({ advanced: true });
invalid({ turn: 4 });
invalid({ phase: 4 as never });
invalid({ sequence: 3 });
invalid({ event: emperorNexusEvent(3, 4, 'winner', 2) });
invalid({ price: 5 });
invalid({ auctionIndex: 2 });
invalid({ card: 'treachery-18' });
invalid({ beforeSpice: 8 });
invalid({ afterSpice: 6 });
invalid({ signature: 'forged' });
});
void test('even re-signed invalid receipts reject future, nonauction, malformed, or non-reimbursed records', () => {
const receipt = purchase();
const invalid = (patch: Partial<EmperorNexusBankPurchase>) => {
const changed = { ...receipt, ...patch };
changed.signature = signEmperorNexusPurchase(changed);
assert.throws(() => validateEmperorNexusPurchase(context, changed), EmperorNexusPurchaseError);
};
invalid({ turn: 5, event: emperorNexusEvent(5, 3, 'winner', 2) });
invalid({ turn: 0, event: emperorNexusEvent(0, 3, 'winner', 2) });
invalid({ phase: 4 as never, event: emperorNexusEvent(3, 4, 'winner', 2) });
invalid({ sequence: -1, event: emperorNexusEvent(3, 3, 'winner', -1) });
invalid({ sequence: Number.MAX_SAFE_INTEGER + 1, event: emperorNexusEvent(3, 3, 'winner', Number.MAX_SAFE_INTEGER + 1) });
invalid({ price: 0 });
invalid({ auctionIndex: -1 });
invalid({ price: 8 });
invalid({ price: 1.5 });
invalid({ beforeSpice: 7.5, afterSpice: 7.5 });
invalid({ afterSpice: 6 });
invalid({ afterSpice: 11 });
invalid({ card: ' ' });
invalid({ beforeSpice: Number.MAX_SAFE_INTEGER + 1, afterSpice: Number.MAX_SAFE_INTEGER + 1 });
invalid({ owner: ' ', event: emperorNexusEvent(3, 3, ' ', 2) });
invalid({ owner: 'stranger', event: emperorNexusEvent(3, 3, 'stranger', 2) });
invalid({ faction: 'guild' });
invalid({ advanced: true });
invalid({ extra: 'hidden' } as never);
invalid({ price: undefined } as never);
assert.throws(
() => validateEmperorNexusPurchase(context, null as never),
EmperorNexusPurchaseError,
);
assert.throws(
() => validateEmperorNexusPurchase({ ...context, turn: 2 }, receipt),
EmperorNexusPurchaseError,
);
assert.throws(
() => validateEmperorNexusPurchase({ ...context, players: context.players.slice(1) }, receipt),
EmperorNexusPurchaseError,
);
assert.throws(
() => validateEmperorNexusPurchase({ ...context, players: [context.players[0], context.players[0]] }, receipt),
EmperorNexusPurchaseError,
);
});