Migrate participant_golf_skills from per-season to canonical table (one row per real-world player), mirroring the existing participant_surface_elos pattern. Adds admin UI to copy participants between same-sport seasons with EV stubs, transactional writes, and comprehensive tests.
105 lines
2.7 KiB
TypeScript
105 lines
2.7 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
|
|
vi.mock("~/database/context", () => ({
|
|
database: vi.fn(),
|
|
}));
|
|
|
|
import {
|
|
upsertCanonicalGolfSkills,
|
|
getCanonicalGolfSkills,
|
|
} from "../canonical-golf-skills";
|
|
import { database } from "~/database/context";
|
|
|
|
const PARTICIPANT_ID = "participant-1";
|
|
|
|
const SAMPLE_SKILLS = {
|
|
id: "skill-1",
|
|
participantId: PARTICIPANT_ID,
|
|
sgTotal: "2.50",
|
|
datagolfRank: 3,
|
|
mastersOdds: 800,
|
|
usOpenOdds: 1200,
|
|
openChampionshipOdds: 1000,
|
|
pgaChampionshipOdds: 900,
|
|
updatedAt: new Date("2026-01-28T00:00:00Z"),
|
|
};
|
|
|
|
function makeUpsertDb(returnValue: object) {
|
|
return {
|
|
insert: vi.fn().mockReturnValue({
|
|
values: vi.fn().mockReturnValue({
|
|
onConflictDoUpdate: vi.fn().mockReturnValue({
|
|
returning: vi.fn().mockResolvedValue([returnValue]),
|
|
}),
|
|
}),
|
|
}),
|
|
};
|
|
}
|
|
|
|
function makeSelectDb(rows: object[]) {
|
|
return {
|
|
query: {
|
|
participantGolfSkills: {
|
|
findFirst: vi.fn().mockResolvedValue(rows[0] ?? null),
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
describe("upsertCanonicalGolfSkills", () => {
|
|
it("inserts or updates golf skill data", async () => {
|
|
vi.mocked(database).mockReturnValue(makeUpsertDb(SAMPLE_SKILLS) as never);
|
|
|
|
const result = await upsertCanonicalGolfSkills({
|
|
participantId: PARTICIPANT_ID,
|
|
sgTotal: "2.50",
|
|
datagolfRank: 3,
|
|
mastersOdds: 800,
|
|
});
|
|
|
|
expect(result).toEqual(SAMPLE_SKILLS);
|
|
});
|
|
|
|
it("throws if participantId is missing", async () => {
|
|
await expect(
|
|
upsertCanonicalGolfSkills({ participantId: undefined as unknown as string }),
|
|
).rejects.toThrow("participantId is required");
|
|
});
|
|
|
|
it("uses onConflictDoUpdate on participantId", async () => {
|
|
const mockDb = makeUpsertDb(SAMPLE_SKILLS);
|
|
vi.mocked(database).mockReturnValue(mockDb as never);
|
|
|
|
await upsertCanonicalGolfSkills({
|
|
participantId: PARTICIPANT_ID,
|
|
sgTotal: "2.50",
|
|
});
|
|
|
|
const onConflictCall = (mockDb.insert as ReturnType<typeof vi.fn>)
|
|
.mock.results[0].value.values.mock.results[0].value.onConflictDoUpdate;
|
|
|
|
expect(onConflictCall).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("getCanonicalGolfSkills", () => {
|
|
it("returns skills when found", async () => {
|
|
vi.mocked(database).mockReturnValue(makeSelectDb([SAMPLE_SKILLS]) as never);
|
|
|
|
const result = await getCanonicalGolfSkills(PARTICIPANT_ID);
|
|
|
|
expect(result).toEqual(SAMPLE_SKILLS);
|
|
});
|
|
|
|
it("returns null when not found", async () => {
|
|
vi.mocked(database).mockReturnValue(makeSelectDb([]) as never);
|
|
|
|
const result = await getCanonicalGolfSkills("nonexistent");
|
|
|
|
expect(result).toBeNull();
|
|
});
|
|
});
|