getSurfaceEloMap now joins through season_participants and returns rows keyed by seasonParticipantId (was participantId). batchUpsertSurfaceElos now performs a second db.select to resolve canonical links before mirroring writes; tests mock the lookup explicitly and exercise both the "no canonical link" short-circuit path and the "mirror to canonical" path. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
162 lines
6 KiB
TypeScript
162 lines
6 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
|
|
// Mock the database context before importing any model
|
|
vi.mock("~/database/context", () => ({
|
|
database: vi.fn(),
|
|
}));
|
|
|
|
import { database } from "~/database/context";
|
|
import { getSurfaceEloMap, batchUpsertSurfaceElos, getSurfaceElosForSeason } from "../surface-elo";
|
|
|
|
const mockDb = {
|
|
select: vi.fn().mockReturnThis(),
|
|
from: vi.fn().mockReturnThis(),
|
|
where: vi.fn().mockReturnThis(),
|
|
innerJoin: vi.fn().mockReturnThis(),
|
|
orderBy: vi.fn().mockReturnThis(),
|
|
insert: vi.fn().mockReturnThis(),
|
|
values: vi.fn().mockReturnThis(),
|
|
onConflictDoUpdate: vi.fn().mockReturnThis(),
|
|
};
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
(database as ReturnType<typeof vi.fn>).mockReturnValue(mockDb);
|
|
|
|
// Reset chain to return mock itself (supports fluent chaining)
|
|
mockDb.select.mockReturnValue(mockDb);
|
|
mockDb.from.mockReturnValue(mockDb);
|
|
mockDb.where.mockReturnValue(mockDb);
|
|
mockDb.innerJoin.mockReturnValue(mockDb);
|
|
mockDb.orderBy.mockReturnValue(mockDb);
|
|
mockDb.insert.mockReturnValue(mockDb);
|
|
mockDb.values.mockReturnValue(mockDb);
|
|
mockDb.onConflictDoUpdate.mockResolvedValue(undefined);
|
|
});
|
|
|
|
describe("getSurfaceEloMap", () => {
|
|
it("returns an empty Map when no records exist", async () => {
|
|
mockDb.where.mockResolvedValue([]);
|
|
const result = await getSurfaceEloMap("season-1");
|
|
expect(result).toBeInstanceOf(Map);
|
|
expect(result.size).toBe(0);
|
|
});
|
|
|
|
it("returns a Map keyed by seasonParticipantId", async () => {
|
|
mockDb.where.mockResolvedValue([
|
|
{ seasonParticipantId: "p1", worldRanking: 1, eloHard: 1800, eloClay: 1700, eloGrass: 1600 },
|
|
{ seasonParticipantId: "p2", worldRanking: null, eloHard: null, eloClay: 1750, eloGrass: null },
|
|
]);
|
|
const result = await getSurfaceEloMap("season-1");
|
|
expect(result.size).toBe(2);
|
|
expect(result.get("p1")).toEqual({ worldRanking: 1, eloHard: 1800, eloClay: 1700, eloGrass: 1600 });
|
|
expect(result.get("p2")).toEqual({ worldRanking: null, eloHard: null, eloClay: 1750, eloGrass: null });
|
|
});
|
|
|
|
it("preserves null surface Elos (does not coerce to 0)", async () => {
|
|
mockDb.where.mockResolvedValue([
|
|
{ seasonParticipantId: "p1", worldRanking: null, eloHard: null, eloClay: null, eloGrass: null },
|
|
]);
|
|
const result = await getSurfaceEloMap("season-1");
|
|
const entry = result.get("p1");
|
|
expect(entry?.worldRanking).toBeNull();
|
|
expect(entry?.eloHard).toBeNull();
|
|
expect(entry?.eloClay).toBeNull();
|
|
expect(entry?.eloGrass).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("batchUpsertSurfaceElos", () => {
|
|
it("does nothing when given an empty array", async () => {
|
|
await batchUpsertSurfaceElos([]);
|
|
expect(mockDb.insert).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("calls insert with the correct values on the per-window table", async () => {
|
|
// The function first upserts to the per-window table, then reads
|
|
// season_participants for canonical-id lookup. Make the follow-up select
|
|
// return empty so the canonical write path short-circuits and we only
|
|
// observe the per-window insert in this assertion.
|
|
mockDb.where.mockResolvedValue([]);
|
|
|
|
const inputs = [
|
|
{ participantId: "p1", sportsSeasonId: "s1", worldRanking: 1, eloHard: 1800, eloClay: 1700, eloGrass: 1600 },
|
|
{ participantId: "p2", sportsSeasonId: "s1", worldRanking: null, eloHard: null, eloClay: 1750, eloGrass: undefined },
|
|
];
|
|
await batchUpsertSurfaceElos(inputs);
|
|
expect(mockDb.insert).toHaveBeenCalledOnce();
|
|
expect(mockDb.values).toHaveBeenCalledOnce();
|
|
|
|
const passedValues = mockDb.values.mock.calls[0][0] as Array<{
|
|
participantId: string;
|
|
eloHard: number | null;
|
|
eloClay: number | null;
|
|
eloGrass: number | null;
|
|
}>;
|
|
expect(passedValues).toHaveLength(2);
|
|
expect(passedValues[0].participantId).toBe("p1");
|
|
expect(passedValues[0].eloHard).toBe(1800);
|
|
expect(passedValues[1].eloHard).toBeNull();
|
|
expect(passedValues[1].eloGrass).toBeNull(); // undefined → null
|
|
});
|
|
|
|
it("uses onConflictDoUpdate for upsert behaviour", async () => {
|
|
mockDb.where.mockResolvedValue([]); // no canonical links; only per-window write
|
|
|
|
await batchUpsertSurfaceElos([
|
|
{ participantId: "p1", sportsSeasonId: "s1", worldRanking: 5, eloHard: 2000 },
|
|
]);
|
|
expect(mockDb.onConflictDoUpdate).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it("mirrors writes to canonical participant_surface_elos when linked", async () => {
|
|
// season_participants lookup returns canonical links for p1 only.
|
|
mockDb.where.mockResolvedValue([
|
|
{ id: "p1", canonicalId: "canon-1" },
|
|
]);
|
|
|
|
await batchUpsertSurfaceElos([
|
|
{ participantId: "p1", sportsSeasonId: "s1", worldRanking: 5, eloHard: 2000 },
|
|
]);
|
|
|
|
// One per-window insert, one canonical insert → two inserts total.
|
|
expect(mockDb.insert).toHaveBeenCalledTimes(2);
|
|
expect(mockDb.onConflictDoUpdate).toHaveBeenCalledTimes(2);
|
|
|
|
const canonicalValues = mockDb.values.mock.calls[1][0] as Array<{
|
|
participantId: string;
|
|
eloHard: number | null;
|
|
}>;
|
|
expect(canonicalValues).toHaveLength(1);
|
|
expect(canonicalValues[0].participantId).toBe("canon-1");
|
|
expect(canonicalValues[0].eloHard).toBe(2000);
|
|
});
|
|
});
|
|
|
|
describe("getSurfaceElosForSeason", () => {
|
|
it("returns records joined with participant names", async () => {
|
|
mockDb.orderBy.mockResolvedValue([
|
|
{
|
|
id: "rec-1",
|
|
participantId: "p1",
|
|
sportsSeasonId: "s1",
|
|
worldRanking: 1,
|
|
eloHard: 1800,
|
|
eloClay: 1700,
|
|
eloGrass: 1600,
|
|
updatedAt: new Date("2025-01-01"),
|
|
participantName: "Carlos Alcaraz",
|
|
},
|
|
]);
|
|
const result = await getSurfaceElosForSeason("s1");
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].participantName).toBe("Carlos Alcaraz");
|
|
expect(result[0].eloHard).toBe(1800);
|
|
});
|
|
|
|
it("returns empty array when no Elo records exist", async () => {
|
|
mockDb.orderBy.mockResolvedValue([]);
|
|
const result = await getSurfaceElosForSeason("s1");
|
|
expect(result).toEqual([]);
|
|
});
|
|
});
|