import { GameRecord } from "@mancala/core"; import { Pool } from "pg"; import { v4 as uuid } from "uuid"; import { createPool, runMigrations } from "../../src/db/postgres"; import { PostgresGameHistoryRepository } from "../../src/history/GameHistoryRepository"; import { RunningService, startPostgres } from "../support/containers"; jest.setTimeout(120_000); const ALICE = { userId: "kc:alice", name: "alice", isGuest: false }; const BOB = { userId: "kc:bob", name: "bob", isGuest: false }; const GUEST = { userId: "kc:guest", name: "guest-1a2b3c4d", isGuest: true }; const LEGACY = { userId: "legacy:0123456789abcdef0123", isGuest: true }; let endedAt = Date.UTC(2026, 9, 1); const record = (overrides: Partial = {}): GameRecord => { endedAt += 60_000; return { id: uuid(), player1: ALICE, player2: BOB, board: [0, 0, 0, 0, 0, 0, 30, 0, 0, 0, 0, 0, 0, 18], result: "player1", endReason: "completed", startedAt: endedAt - 600_000, endedAt, moveCount: 1, peakSpectators: 0, totalSpectators: 0, moves: [{ playerId: ALICE.userId, moveIndex: 3, boardSnapshot: [4, 4, 4, 0, 5, 5, 1, 4, 4, 4, 4, 4, 4, 0], gameSteps: [{ index: 6, type: "GAME_STEP_LAST_STONE_IN_BANK", data: null }], at: endedAt - 599_000, }], ...overrides, }; }; describe("PostgresGameHistoryRepository", () => { let postgres: RunningService; let pool: Pool; let repository: PostgresGameHistoryRepository; beforeAll(async () => { postgres = await startPostgres(); pool = createPool(postgres.url); await runMigrations(pool); repository = new PostgresGameHistoryRepository(pool); }); afterAll(async () => { await pool?.end(); await postgres?.container.stop(); }); beforeEach(() => pool.query("TRUNCATE games CASCADE")); it("runs migrations only once", async () => { await runMigrations(pool); const { rows } = await pool.query("SELECT version FROM schema_migrations"); expect(rows).toEqual([{ version: "001_games" }]); }); it("saves a game and reads it back unchanged", async () => { const game = record({ player2: LEGACY, result: null, endReason: "inactive", peakSpectators: 3, totalSpectators: 5 }); await repository.save(game); expect(await repository.get(game.id)).toEqual(game); }); it("keeps leftUserId for games a player left", async () => { const game = record({ result: "player2", endReason: "left", leftUserId: ALICE.userId }); await repository.save(game); expect((await repository.get(game.id))?.leftUserId).toBe(ALICE.userId); }); it("ignores saving the same game again", async () => { const game = record(); await repository.save(game); await repository.save({ ...game, result: "draw" }); expect((await repository.get(game.id))?.result).toBe("player1"); }); it("returns undefined for unknown and malformed ids", async () => { expect(await repository.get(uuid())).toBeUndefined(); expect(await repository.get("not-a-uuid")).toBeUndefined(); }); it("lists a user's games newest first, without the moves", async () => { const first = record(); const second = record({ player1: GUEST, player2: ALICE }); const otherPlayers = record({ player1: BOB, player2: GUEST }); for (const game of [first, second, otherPlayers]) await repository.save(game); const recent = await repository.getRecentForUser(ALICE.userId, 3); expect(recent.map((game) => game.id)).toEqual([second.id, first.id]); expect(recent[0]).not.toHaveProperty("moves"); expect(await repository.getRecentForUser(ALICE.userId, 1)).toHaveLength(1); }); it("lists popular games by peak spectators, then newest", async () => { const quiet = record({ peakSpectators: 0 }); const popular = record({ peakSpectators: 9 }); const alsoPopularButNewer = record({ peakSpectators: 9 }); for (const game of [quiet, popular, alsoPopularButNewer]) await repository.save(game); const ids = (await repository.getPopular(5)).map((game) => game.id); expect(ids).toEqual([alsoPopularButNewer.id, popular.id, quiet.id]); }); });