112 lines
4.3 KiB
TypeScript
112 lines
4.3 KiB
TypeScript
|
|
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" }]);
|
||
|
|
});
|
||
|
|
|
||
|
|
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]);
|
||
|
|
});
|
||
|
|
});
|