mancala/apps/backend/tests/history/PostgresGameHistoryRepository.test.ts

128 lines
5.0 KiB
TypeScript
Raw Normal View History

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> = {}): 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" }, { version: "002_clock" }]);
});
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 the clock: the initial time and each move's time left", async () => {
const game = record({ clockInitialMs: 300_000 });
game.moves[0].remainingMs = 291_500;
await repository.save(game);
expect(await repository.get(game.id)).toEqual(game);
expect((await repository.getRecentForUser(ALICE.userId, 1))[0].clockInitialMs).toBe(300_000);
});
it("reads games saved before the game clock without clock data", async () => {
const game = record();
await repository.save(game);
const saved = await repository.get(game.id);
expect(saved).not.toHaveProperty("clockInitialMs");
expect(saved?.moves[0]).not.toHaveProperty("remainingMs");
});
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]);
});
});