252 lines
9 KiB
TypeScript
252 lines
9 KiB
TypeScript
|
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||
|
|
|
||
|
|
vi.mock("~/database/context", () => ({
|
||
|
|
database: vi.fn(),
|
||
|
|
}));
|
||
|
|
|
||
|
|
vi.mock("~/database/schema", () => ({
|
||
|
|
seasonParticipants: { id: "sp.id", sportsSeasonId: "sp.sports_season_id", name: "sp.name", shortName: "sp.short_name", externalId: "sp.external_id", participantId: "sp.participant_id" },
|
||
|
|
seasonParticipantExpectedValues: { sportsSeasonId: "pev.sports_season_id" },
|
||
|
|
}));
|
||
|
|
|
||
|
|
vi.mock("drizzle-orm", () => ({
|
||
|
|
eq: (col: unknown, val: unknown) => ({ type: "eq", col, val }),
|
||
|
|
inArray: vi.fn(),
|
||
|
|
count: vi.fn(),
|
||
|
|
and: (...args: unknown[]) => ({ type: "and", args }),
|
||
|
|
sql: (strings: TemplateStringsArray, ...values: unknown[]) => ({ type: "sql", strings, values }),
|
||
|
|
asc: (col: unknown) => ({ type: "asc", col }),
|
||
|
|
desc: (col: unknown) => ({ type: "desc", col }),
|
||
|
|
}));
|
||
|
|
|
||
|
|
import * as schema from "~/database/schema";
|
||
|
|
import { copyParticipantsFromSeason } from "../season-participant";
|
||
|
|
import { database } from "~/database/context";
|
||
|
|
|
||
|
|
const SOURCE_ID = "source-season";
|
||
|
|
const TARGET_ID = "target-season";
|
||
|
|
|
||
|
|
function makeSourceParticipant(overrides: Record<string, unknown> = {}) {
|
||
|
|
return {
|
||
|
|
id: "sp-src-1",
|
||
|
|
sportsSeasonId: SOURCE_ID,
|
||
|
|
name: "Tiger Woods",
|
||
|
|
shortName: "T. Woods",
|
||
|
|
externalId: "tiger",
|
||
|
|
participantId: "canon-1",
|
||
|
|
...overrides,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function makeSourceEv(overrides: Record<string, unknown> = {}) {
|
||
|
|
return {
|
||
|
|
id: "ev-src-1",
|
||
|
|
participantId: "sp-src-1",
|
||
|
|
sportsSeasonId: SOURCE_ID,
|
||
|
|
sourceOdds: 800,
|
||
|
|
sourceElo: null,
|
||
|
|
source: "futures",
|
||
|
|
worldRanking: 5,
|
||
|
|
...overrides,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function makeMockDb({
|
||
|
|
targetParticipants = [] as object[],
|
||
|
|
sourceParticipants = [] as object[],
|
||
|
|
sourceEvRows = [] as object[],
|
||
|
|
newParticipantRows = sourceParticipants.map((p, i) => ({
|
||
|
|
...(p as Record<string, unknown>),
|
||
|
|
id: `sp-new-${i + 1}`,
|
||
|
|
sportsSeasonId: TARGET_ID,
|
||
|
|
})),
|
||
|
|
}: {
|
||
|
|
targetParticipants?: object[];
|
||
|
|
sourceParticipants?: object[];
|
||
|
|
sourceEvRows?: object[];
|
||
|
|
newParticipantRows?: object[];
|
||
|
|
} = {}) {
|
||
|
|
const insertedRows: Record<string, unknown> = {};
|
||
|
|
|
||
|
|
const findManyCalls: { table: string; args: unknown }[] = [];
|
||
|
|
|
||
|
|
const db = {
|
||
|
|
query: {
|
||
|
|
seasonParticipants: {
|
||
|
|
findMany: vi.fn().mockImplementation((opts: unknown) => {
|
||
|
|
findManyCalls.push({ table: "seasonParticipants", args: opts });
|
||
|
|
const where = (opts as Record<string, unknown>)?.where as { val: string } | undefined;
|
||
|
|
if (where?.val === SOURCE_ID) return sourceParticipants;
|
||
|
|
if (where?.val === TARGET_ID) return targetParticipants;
|
||
|
|
return targetParticipants;
|
||
|
|
}),
|
||
|
|
},
|
||
|
|
seasonParticipantExpectedValues: {
|
||
|
|
findMany: vi.fn().mockResolvedValue(sourceEvRows),
|
||
|
|
},
|
||
|
|
},
|
||
|
|
insert: vi.fn().mockImplementation((table: object) => {
|
||
|
|
let key: string;
|
||
|
|
let returnRows: object[];
|
||
|
|
if (table === schema.seasonParticipants) {
|
||
|
|
key = "seasonParticipants";
|
||
|
|
returnRows = newParticipantRows;
|
||
|
|
} else {
|
||
|
|
key = "participantExpectedValues";
|
||
|
|
returnRows = [];
|
||
|
|
}
|
||
|
|
return {
|
||
|
|
values: vi.fn().mockImplementation((vals: unknown) => {
|
||
|
|
insertedRows[key] = vals;
|
||
|
|
if (key === "seasonParticipants") {
|
||
|
|
return { returning: vi.fn().mockResolvedValue(returnRows) };
|
||
|
|
}
|
||
|
|
return Promise.resolve(undefined);
|
||
|
|
}),
|
||
|
|
};
|
||
|
|
}),
|
||
|
|
transaction: vi.fn().mockImplementation(async (fn: (tx: typeof db) => Promise<unknown>) => fn(db)),
|
||
|
|
_insertedRows: insertedRows,
|
||
|
|
_findManyCalls: findManyCalls,
|
||
|
|
};
|
||
|
|
|
||
|
|
return db;
|
||
|
|
}
|
||
|
|
|
||
|
|
beforeEach(() => {
|
||
|
|
vi.clearAllMocks();
|
||
|
|
});
|
||
|
|
|
||
|
|
describe("copyParticipantsFromSeason", () => {
|
||
|
|
it("returns empty result when source has no participants", async () => {
|
||
|
|
const db = makeMockDb({ sourceParticipants: [] });
|
||
|
|
vi.mocked(database).mockReturnValue(db as never);
|
||
|
|
|
||
|
|
const result = await copyParticipantsFromSeason(SOURCE_ID, TARGET_ID);
|
||
|
|
|
||
|
|
expect(result).toEqual({ copied: 0, skipped: 0, evStubsCopied: 0 });
|
||
|
|
expect(db.transaction).not.toHaveBeenCalled();
|
||
|
|
});
|
||
|
|
|
||
|
|
it("skips participants already present in target (case-insensitive)", async () => {
|
||
|
|
const db = makeMockDb({
|
||
|
|
targetParticipants: [makeSourceParticipant({ id: "sp-tgt-1", sportsSeasonId: TARGET_ID })],
|
||
|
|
sourceParticipants: [makeSourceParticipant()],
|
||
|
|
});
|
||
|
|
vi.mocked(database).mockReturnValue(db as never);
|
||
|
|
|
||
|
|
const result = await copyParticipantsFromSeason(SOURCE_ID, TARGET_ID);
|
||
|
|
|
||
|
|
expect(result).toEqual({ copied: 0, skipped: 1, evStubsCopied: 0 });
|
||
|
|
expect(db.transaction).not.toHaveBeenCalled();
|
||
|
|
});
|
||
|
|
|
||
|
|
it("copies participants and returns counts", async () => {
|
||
|
|
const srcP1 = makeSourceParticipant({ name: "Tiger Woods", externalId: "tiger", participantId: "canon-1" });
|
||
|
|
const srcP2 = makeSourceParticipant({ id: "sp-src-2", name: "Rory McIlroy", externalId: "rory", participantId: "canon-2" });
|
||
|
|
const db = makeMockDb({
|
||
|
|
sourceParticipants: [srcP1, srcP2],
|
||
|
|
newParticipantRows: [
|
||
|
|
{ ...srcP1, id: "sp-new-1", sportsSeasonId: TARGET_ID },
|
||
|
|
{ ...srcP2, id: "sp-new-2", sportsSeasonId: TARGET_ID },
|
||
|
|
],
|
||
|
|
});
|
||
|
|
vi.mocked(database).mockReturnValue(db as never);
|
||
|
|
|
||
|
|
const result = await copyParticipantsFromSeason(SOURCE_ID, TARGET_ID);
|
||
|
|
|
||
|
|
expect(result.copied).toBe(2);
|
||
|
|
expect(result.skipped).toBe(0);
|
||
|
|
expect(db._insertedRows.seasonParticipants).toHaveLength(2);
|
||
|
|
const values = db._insertedRows.seasonParticipants as Array<Record<string, unknown>>;
|
||
|
|
expect(values[0]).toMatchObject({ name: "Tiger Woods", externalId: "tiger", participantId: "canon-1" });
|
||
|
|
expect(values[1]).toMatchObject({ name: "Rory McIlroy", externalId: "rory", participantId: "canon-2" });
|
||
|
|
});
|
||
|
|
|
||
|
|
it("copies EV stubs matched by externalId", async () => {
|
||
|
|
const srcP = makeSourceParticipant({ externalId: "tiger" });
|
||
|
|
const srcEv = makeSourceEv({ participantId: "sp-src-1", sourceOdds: 800 });
|
||
|
|
const db = makeMockDb({
|
||
|
|
sourceParticipants: [srcP],
|
||
|
|
sourceEvRows: [srcEv],
|
||
|
|
newParticipantRows: [{ ...srcP, id: "sp-new-1", sportsSeasonId: TARGET_ID }],
|
||
|
|
});
|
||
|
|
vi.mocked(database).mockReturnValue(db as never);
|
||
|
|
|
||
|
|
const result = await copyParticipantsFromSeason(SOURCE_ID, TARGET_ID);
|
||
|
|
|
||
|
|
expect(result.evStubsCopied).toBe(1);
|
||
|
|
const evRows = db._insertedRows.participantExpectedValues as Array<Record<string, unknown>>;
|
||
|
|
expect(evRows).toHaveLength(1);
|
||
|
|
expect(evRows[0]).toMatchObject({
|
||
|
|
participantId: "sp-new-1",
|
||
|
|
sourceOdds: 800,
|
||
|
|
source: "futures",
|
||
|
|
worldRanking: 5,
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it("copies EV stubs matched by name when externalId is null", async () => {
|
||
|
|
const srcP = makeSourceParticipant({ externalId: null, name: "Tiger Woods" });
|
||
|
|
const srcEv = makeSourceEv({ participantId: "sp-src-1" });
|
||
|
|
const db = makeMockDb({
|
||
|
|
sourceParticipants: [srcP],
|
||
|
|
sourceEvRows: [srcEv],
|
||
|
|
newParticipantRows: [{ ...srcP, id: "sp-new-1", sportsSeasonId: TARGET_ID }],
|
||
|
|
});
|
||
|
|
vi.mocked(database).mockReturnValue(db as never);
|
||
|
|
|
||
|
|
const result = await copyParticipantsFromSeason(SOURCE_ID, TARGET_ID);
|
||
|
|
|
||
|
|
expect(result.evStubsCopied).toBe(1);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("skips EV rows with neither sourceOdds nor sourceElo", async () => {
|
||
|
|
const srcP = makeSourceParticipant();
|
||
|
|
const srcEv = makeSourceEv({ sourceOdds: null, sourceElo: null });
|
||
|
|
const db = makeMockDb({
|
||
|
|
sourceParticipants: [srcP],
|
||
|
|
sourceEvRows: [srcEv],
|
||
|
|
newParticipantRows: [{ ...srcP, id: "sp-new-1", sportsSeasonId: TARGET_ID }],
|
||
|
|
});
|
||
|
|
vi.mocked(database).mockReturnValue(db as never);
|
||
|
|
|
||
|
|
const result = await copyParticipantsFromSeason(SOURCE_ID, TARGET_ID);
|
||
|
|
|
||
|
|
expect(result.evStubsCopied).toBe(0);
|
||
|
|
expect(db._insertedRows.participantExpectedValues).toBeUndefined();
|
||
|
|
});
|
||
|
|
|
||
|
|
it("wraps writes in a transaction", async () => {
|
||
|
|
const srcP = makeSourceParticipant();
|
||
|
|
const db = makeMockDb({
|
||
|
|
sourceParticipants: [srcP],
|
||
|
|
newParticipantRows: [{ ...srcP, id: "sp-new-1", sportsSeasonId: TARGET_ID }],
|
||
|
|
});
|
||
|
|
vi.mocked(database).mockReturnValue(db as never);
|
||
|
|
|
||
|
|
await copyParticipantsFromSeason(SOURCE_ID, TARGET_ID);
|
||
|
|
|
||
|
|
expect(db.transaction).toHaveBeenCalledOnce();
|
||
|
|
});
|
||
|
|
|
||
|
|
it("handles partial duplicates (some copied, some skipped)", async () => {
|
||
|
|
const srcP1 = makeSourceParticipant({ name: "Tiger Woods" });
|
||
|
|
const srcP2 = makeSourceParticipant({ id: "sp-src-2", name: "Rory McIlroy", participantId: "canon-2" });
|
||
|
|
const db = makeMockDb({
|
||
|
|
targetParticipants: [makeSourceParticipant({ id: "sp-tgt-1", sportsSeasonId: TARGET_ID, name: "Tiger Woods" })],
|
||
|
|
sourceParticipants: [srcP1, srcP2],
|
||
|
|
newParticipantRows: [{ ...srcP2, id: "sp-new-1", sportsSeasonId: TARGET_ID }],
|
||
|
|
});
|
||
|
|
vi.mocked(database).mockReturnValue(db as never);
|
||
|
|
|
||
|
|
const result = await copyParticipantsFromSeason(SOURCE_ID, TARGET_ID);
|
||
|
|
|
||
|
|
expect(result.copied).toBe(1);
|
||
|
|
expect(result.skipped).toBe(1);
|
||
|
|
const values = db._insertedRows.seasonParticipants as Array<Record<string, unknown>>;
|
||
|
|
expect(values).toHaveLength(1);
|
||
|
|
expect(values[0]).toMatchObject({ name: "Rory McIlroy" });
|
||
|
|
});
|
||
|
|
});
|