import { describe, it, expect } from 'vitest'; import { Chess } from 'chess.js'; import { buildView, ownSquares } from '../../src/view.js'; import type { Game } from '../../src/state.js'; import { RATE_LIMIT } from '../../src/state.js'; function makeGame(mode: 'blind' | 'vanilla', fen?: string, status: 'active' | 'finished' = 'active'): Game { return { id: 'testtest', mode, highlightingEnabled: false, status, createdAt: Date.now(), chess: fen ? new Chess(fen) : new Chess(), moveHistory: [], announcements: [], players: { w: { token: 'w'.repeat(24), socket: null, joinedAt: 0, rateBucket: { tokens: RATE_LIMIT.capacity, last: 0 } }, b: { token: 'b'.repeat(24), socket: null, joinedAt: 0, rateBucket: { tokens: RATE_LIMIT.capacity, last: 0 } }, }, armed: null, drawOffer: null, disconnectAt: {}, lastBroadcastIdx: { w: 0, b: 0 }, }; } describe('buildView: security boundary', () => { it('blind/active white view contains zero black pieces', () => { const g = makeGame('blind'); const view = buildView(g, 'w'); for (const piece of Object.values(view.pieces)) { expect(piece?.color).toBe('w'); } expect(Object.keys(view.pieces).length).toBe(16); // all 16 white pieces }); it('blind/active black view contains zero white pieces', () => { const g = makeGame('blind'); const view = buildView(g, 'b'); for (const piece of Object.values(view.pieces)) { expect(piece?.color).toBe('b'); } expect(Object.keys(view.pieces).length).toBe(16); }); it('vanilla/active shows both colors', () => { const g = makeGame('vanilla'); const view = buildView(g, 'w'); expect(Object.keys(view.pieces).length).toBe(32); }); it('blind/finished reveals both colors (post-game review)', () => { const g = makeGame('blind', undefined, 'finished'); const view = buildView(g, 'w'); expect(Object.keys(view.pieces).length).toBe(32); }); it('blind: inCheck is null for non-actor (info leak prevention)', () => { // Black to move and is in check. White's view says null (it's not white's turn, // and revealing inCheck-status of opponent leaks info). const g = makeGame('blind', 'rnb1kbnr/pppp1ppp/8/4p3/6Pq/5P2/PPPPP2P/RNBQKBNR w KQkq - 1 3'); const view = buildView(g, 'b'); // It's white's turn here. Black viewer is not the to-move side. expect(view.inCheck).toBeNull(); }); }); describe('ownSquares', () => { it('starting position returns 16 own squares', () => { const g = makeGame('blind'); expect(ownSquares(g, 'w').size).toBe(16); expect(ownSquares(g, 'b').size).toBe(16); }); it('contains only own-color squares', () => { const g = makeGame('blind'); const wSet = ownSquares(g, 'w'); expect(wSet.has('e2')).toBe(true); expect(wSet.has('e7')).toBe(false); }); });