UNPKG

boardgame.io

Version:
382 lines (345 loc) 10.2 kB
import { getFilterPlayerView, redactLog } from './filter-player-view'; import * as ActionCreators from '../core/action-creators'; import { Master } from './master'; import { InMemory } from '../server/db/inmemory'; import { PlayerView } from '../core/player-view'; import { INVALID_MOVE } from '../core/constants'; import type { Game, SyncInfo } from '../types'; function TransportAPI(send = jest.fn(), sendAll = jest.fn()) { return { send, sendAll }; } function validateNotTransientState(state: any) { expect(state).toEqual( expect.not.objectContaining({ transients: expect.anything() }) ); } describe('playerView - update', () => { const send = jest.fn(); const sendAll = jest.fn(); const game: Game = { playerView: ({ G, playerID }) => { return { ...G, player: playerID }; }, }; const master = new Master(game, new InMemory(), TransportAPI(send, sendAll)); beforeAll(async () => { await master.onSync('matchID', '0', undefined, 2); }); beforeEach(() => { jest.clearAllMocks(); }); test('sync', async () => { await master.onSync('matchID', '0', undefined, 2); const payload = send.mock.calls[0][0]; const filterPlayerView = getFilterPlayerView(game); expect( (filterPlayerView('0', payload).args[1] as SyncInfo).state ).toMatchObject({ G: { player: '0' }, }); }); test('update', async () => { const action = ActionCreators.gameEvent('endTurn'); await master.onSync('matchID', '0', undefined, 2); await master.onUpdate(action, 0, 'matchID', '0'); const payload = sendAll.mock.calls[sendAll.mock.calls.length - 1][0]; const filterPlayerView = getFilterPlayerView(game); const transportData0 = filterPlayerView('0', payload); const G_player0 = (transportData0.args[1] as any).G; const transportData1 = filterPlayerView('1', payload); const G_player1 = (transportData1.args[1] as any).G; expect(G_player0.player).toBe('0'); expect(G_player1.player).toBe('1'); }); }); describe('playerView - patch', () => { const send = jest.fn(); const sendAll = jest.fn(); const db = new InMemory(); const game: Game = { seed: 0, deltaState: true, setup: () => { return { players: { '0': { cards: ['card3'], }, '1': { cards: [], }, }, cards: ['card0', 'card1', 'card2'], discardedCards: [], }; }, playerView: PlayerView.STRIP_SECRETS, turn: { activePlayers: { currentPlayer: { stage: 'A' } }, stages: { A: { moves: { Invalid: () => { return INVALID_MOVE; }, A: { client: false, move: ({ G, playerID }) => { const card = G.players[playerID].cards.shift(); G.discardedCards.push(card); }, }, B: { client: false, ignoreStaleStateID: true, move: ({ G, playerID }) => { const card = G.cards.pop(); G.players[playerID].cards.push(card); }, }, }, }, }, }, }; const master = new Master(game, db, TransportAPI(send, sendAll)); const move = ActionCreators.makeMove('A', null, '0'); beforeAll(async () => { master.subscribe(({ state }) => { validateNotTransientState(state); }); await master.onSync('matchID', '0', undefined, 2); }); beforeEach(() => { jest.clearAllMocks(); }); test('patch', async () => { await master.onUpdate(move, 0, 'matchID', '0'); expect(sendAll).toBeCalled(); const payload = sendAll.mock.calls[sendAll.mock.calls.length - 1][0]; expect(payload.type).toBe('patch'); const filterPlayerView = getFilterPlayerView(game); const value = filterPlayerView('0', payload); expect(value.type).toBe('patch'); expect(value.args[0]).toBe('matchID'); expect(value.args[1]).toBe(0); expect(value.args[2]).toBe(1); expect(value.args[3]).toMatchObject([ { op: 'remove', path: '/G/players/0/cards/0' }, { op: 'add', path: '/G/discardedCards/-', value: 'card3' }, { op: 'replace', path: '/ctx/numMoves', value: 1 }, { op: 'replace', path: '/_stateID', value: 1 }, ]); }); }); describe('redactLog', () => { test('no-op with undefined log', () => { const result = redactLog(undefined, '0'); expect(result).toBeUndefined(); }); test('no redactedMoves', () => { const logEvents = [ { _stateID: 0, turn: 0, phase: '', action: ActionCreators.gameEvent('endTurn'), }, ]; const result = redactLog(logEvents, '0'); expect(result).toMatchObject(logEvents); }); test('redacted move is only shown with args to the player that made the move', () => { const logEvents = [ { _stateID: 0, turn: 0, phase: '', action: ActionCreators.makeMove('clickCell', [1, 2, 3], '0'), redact: true, }, ]; // player that made the move let result = redactLog(logEvents, '0'); expect(result).toMatchObject(logEvents); // other player result = redactLog(logEvents, '1'); expect(result).toMatchObject([ { _stateID: 0, turn: 0, phase: '', action: { type: 'MAKE_MOVE', payload: { credentials: undefined, playerID: '0', type: 'clickCell', }, }, }, ]); }); test('not redacted move is shown to all', () => { const logEvents = [ { _stateID: 0, turn: 0, phase: '', action: ActionCreators.makeMove('unclickCell', [1, 2, 3], '0'), }, ]; // player that made the move let result = redactLog(logEvents, '0'); expect(result).toMatchObject(logEvents); // other player result = redactLog(logEvents, '1'); expect(result).toMatchObject(logEvents); }); test('can explicitly set showing args to true', () => { const logEvents = [ { _stateID: 0, turn: 0, phase: '', action: ActionCreators.makeMove('unclickCell', [1, 2, 3], '0'), }, ]; // player that made the move let result = redactLog(logEvents, '0'); expect(result).toMatchObject(logEvents); // other player result = redactLog(logEvents, '1'); expect(result).toMatchObject(logEvents); }); test('events are not redacted', () => { const logEvents = [ { _stateID: 0, turn: 0, phase: '', action: ActionCreators.gameEvent('endTurn'), }, ]; // player that made the move let result = redactLog(logEvents, '0'); expect(result).toMatchObject(logEvents); // other player result = redactLog(logEvents, '1'); expect(result).toMatchObject(logEvents); }); test('make sure filter player view redacts the log', async () => { const game: Game = { moves: { A: ({ G }) => G, B: { move: ({ G }) => G, redact: true, }, }, }; const send = jest.fn(); const master = new Master(game, new InMemory(), TransportAPI(send)); const filterPlayerView = getFilterPlayerView(game); const actionA = ActionCreators.makeMove('A', ['not redacted'], '0'); const actionB = ActionCreators.makeMove('B', ['redacted'], '0'); // test: ping-pong two moves, then sync and check the log await master.onSync('matchID', '0', undefined, 2); await master.onUpdate(actionA, 0, 'matchID', '0'); await master.onUpdate(actionB, 1, 'matchID', '0'); await master.onSync('matchID', '1', undefined, 2); const payload = send.mock.calls[send.mock.calls.length - 1][0]; expect( (filterPlayerView('1', payload).args[1] as SyncInfo).log ).toMatchObject([ { action: { type: 'MAKE_MOVE', payload: { type: 'A', args: ['not redacted'], playerID: '0', }, }, _stateID: 0, }, { action: { type: 'MAKE_MOVE', payload: { type: 'B', args: null, playerID: '0', }, }, _stateID: 1, }, ]); }); }); test('make move args to be secret depends on G via conditional redact', async () => { const game: Game = { setup: () => ({ isASecret: false, }), moves: { A: { move: ({ G }) => G, redact: ({ G }) => G.isASecret, }, B: ({ G }) => { return { ...G, isASecret: true }; }, }, }; const send = jest.fn(); const master = new Master(game, new InMemory(), TransportAPI(send)); const filterPlayerView = getFilterPlayerView(game); const actionA0 = ActionCreators.makeMove('A', ['not redacted'], '0'); const actionB = ActionCreators.makeMove('B', ['not redacted'], '0'); const actionA1 = ActionCreators.makeMove('A', ['redacted'], '0'); // test: ping-pong two moves, then sync and check the log await master.onSync('matchID', '0', undefined, 2); await master.onUpdate(actionA0, 0, 'matchID', '0'); await master.onUpdate(actionB, 1, 'matchID', '0'); await master.onUpdate(actionA1, 2, 'matchID', '0'); await master.onSync('matchID', '1', undefined, 2); const payload = send.mock.calls[send.mock.calls.length - 1][0]; expect( (filterPlayerView('1', payload).args[1] as SyncInfo).log ).toMatchObject([ { action: { type: 'MAKE_MOVE', payload: { type: 'A', args: ['not redacted'], playerID: '0', }, }, _stateID: 0, }, { action: { type: 'MAKE_MOVE', payload: { type: 'B', args: ['not redacted'], playerID: '0', }, }, _stateID: 1, }, { action: { type: 'MAKE_MOVE', payload: { type: 'A', args: null, playerID: '0', }, }, _stateID: 2, }, ]); });