import { describe, it, expect, vi, beforeEach } from "vitest"; import type * as DrizzleOrm from "drizzle-orm"; vi.mock("~/database/context", () => ({ database: vi.fn(), })); vi.mock("~/models/scoring-calculator", () => ({ processQualifyingEvent: vi.fn(), })); import { syncTournamentResults } from "../sync-tournament-results"; import { database } from "~/database/context"; import { processQualifyingEvent } from "~/models/scoring-calculator"; // --------------------------------------------------------------------------- // Mock data types // --------------------------------------------------------------------------- interface FakeTournamentResult { tournamentId: string; participantId: string; placement: number | null; rawScore: string | null; } interface FakeScoringEvent { id: string; sportsSeasonId: string; tournamentId: string | null; } interface FakeSeasonParticipant { id: string; sportsSeasonId: string; participantId: string | null; name: string; } interface FakeEventResult { id: string; scoringEventId: string; seasonParticipantId: string; placement: number | null; qualifyingPointsAwarded: string | null; rawScore: string | null; } // --------------------------------------------------------------------------- // Fake in-memory DB // // We build a single shared object with a chainable select/insert/update API // that the service uses identically whether it's the top-level `db` or the // `tx` inside a transaction. This lets us both observe the writes that would // have happened and emulate idempotent upserts. // // Internal state holds the three tables the service queries: // - tournament_results // - scoring_events // - season_participants // - event_results // --------------------------------------------------------------------------- interface FakeDbState { tournamentResults: FakeTournamentResult[]; scoringEvents: FakeScoringEvent[]; seasonParticipants: FakeSeasonParticipant[]; eventResults: FakeEventResult[]; } /** * A very thin fake drizzle-ish db. The service only uses: * - db.select().from(table).where(cond) -- returns rows * - db.insert(table).values(row[|rows]) -- insert * - db.update(table).set(...).where(cond) -- update * - db.transaction(fn) -- runs fn with the same db * * We identify the target table by reference equality to the schema objects. * The service passes schema.tournamentResults etc., so we capture the real * schema module and compare by ref. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any function tableName(table: any): string { // drizzle-orm PgTable exposes the table name under a symbol. Try a few. const sym = Object.getOwnPropertySymbols(table).find( (s) => s.toString() === "Symbol(drizzle:Name)" ); if (sym) return table[sym]; // Fallback for our shim: read `.tableName` property if set. return table.tableName; } function makeFakeDb(state: FakeDbState) { // We mirror the shape in a lazy way: each predicate function is stored on the // call so we can match rows. In practice the service only filters by simple // eq() / and() combinations — we don't try to parse drizzle's SQL tree. // Instead, we stash the filters the service applies by table. // // The simpler and more faithful approach: every call records enough info for // us to return the right subset. // eslint-disable-next-line @typescript-eslint/no-explicit-any function rowsFor(table: any): any[] { // Table objects are drizzle PgTable objects; we identify them by name. const name = tableName(table); switch (name) { case "tournament_results": return state.tournamentResults; case "scoring_events": return state.scoringEvents; case "season_participants": return state.seasonParticipants; case "event_results": return state.eventResults; default: throw new Error(`Unknown table in fake db: ${name}`); } } const db = { // ---------- select ---------- select: vi.fn().mockImplementation(() => { const chain = { // eslint-disable-next-line @typescript-eslint/no-explicit-any _table: null as any, // eslint-disable-next-line @typescript-eslint/no-explicit-any from(table: any) { chain._table = table; return chain; }, // eslint-disable-next-line @typescript-eslint/no-explicit-any where(predicate: any) { // The predicate is a drizzle SQL expression; we can't introspect it // easily. The fake predicate stash sets a callback (see below). If // the predicate is a function we apply it; otherwise we return all // rows for the table (the caller will then filter). const rows = rowsFor(chain._table); if (typeof predicate === "function") { return Promise.resolve(rows.filter(predicate)); } // Unknown predicate: the test harness attaches `__fakeFilter` to the // predicate value we interposed. When we can't evaluate, just return // all rows — the fake is used only by the service and tests that // control what rows exist. return Promise.resolve(rows); }, }; return chain; }), // ---------- insert ---------- // eslint-disable-next-line @typescript-eslint/no-explicit-any insert: vi.fn().mockImplementation((table: any) => { return { // eslint-disable-next-line @typescript-eslint/no-explicit-any values(row: any) { const rows = Array.isArray(row) ? row : [row]; const existing = rowsFor(table); for (const r of rows) { // Assign a fake id for event_results (others aren't re-read by ID). existing.push({ id: `er-${existing.length + 1}`, ...r }); } return { returning: vi.fn().mockResolvedValue(rows), }; }, }; }), // ---------- update ---------- // eslint-disable-next-line @typescript-eslint/no-explicit-any update: vi.fn().mockImplementation((table: any) => { return { // eslint-disable-next-line @typescript-eslint/no-explicit-any set(patch: any) { return { // eslint-disable-next-line @typescript-eslint/no-explicit-any where(predicate: any) { const rows = rowsFor(table); for (const r of rows) { if (typeof predicate === "function" ? predicate(r) : true) { Object.assign(r, patch); } } return Promise.resolve(); }, }; }, }; }), // ---------- transaction ---------- transaction: vi .fn() // eslint-disable-next-line @typescript-eslint/no-explicit-any .mockImplementation(async (fn: (tx: any) => any) => fn(db)), }; return db; } // --------------------------------------------------------------------------- // The service builds drizzle SQL expressions via eq() / and() from // drizzle-orm. We can't intercept those without re-mocking the whole module. // Instead we mock drizzle-orm's eq/and to return predicate functions directly. // --------------------------------------------------------------------------- vi.mock("drizzle-orm", async () => { const actual = await vi.importActual("drizzle-orm"); return { ...actual, // eslint-disable-next-line @typescript-eslint/no-explicit-any eq: (col: any, val: any) => { // Each column object (PgColumn) has a `.name` property giving the DB // column name. Return a predicate that reads that key off the row. const key = colKey(col); // eslint-disable-next-line @typescript-eslint/no-explicit-any return (row: any) => row[key] === val; }, // eslint-disable-next-line @typescript-eslint/no-explicit-any and: (...preds: any[]) => (row: any) => preds.every((p) => p(row)), }; }); // eslint-disable-next-line @typescript-eslint/no-explicit-any function colKey(col: any): string { // drizzle PgColumn: its .name is the snake_case DB name, but our fake state // uses camelCase JS field names. Map common columns manually. const n = col?.name ?? col?.config?.name ?? ""; const map: Record = { tournament_id: "tournamentId", sports_season_id: "sportsSeasonId", scoring_event_id: "scoringEventId", season_participant_id: "seasonParticipantId", participant_id: "participantId", id: "id", }; return map[n] ?? n; } // --------------------------------------------------------------------------- // Helpers for building fixture data // --------------------------------------------------------------------------- beforeEach(() => { vi.clearAllMocks(); }); function seedBasicState(overrides: Partial = {}): FakeDbState { return { tournamentResults: [], scoringEvents: [], seasonParticipants: [], eventResults: [], ...overrides, }; } // --------------------------------------------------------------------------- // Test 1: Single window, full roster match // --------------------------------------------------------------------------- describe("syncTournamentResults", () => { it("syncs a single window with two canonical results and fills 0-QP for unmatched", async () => { const state = seedBasicState({ tournamentResults: [ { tournamentId: "t-1", participantId: "cp-A", placement: 1, rawScore: "280", }, { tournamentId: "t-1", participantId: "cp-B", placement: 2, rawScore: "282", }, ], scoringEvents: [ { id: "ev-1", sportsSeasonId: "ss-1", tournamentId: "t-1" }, ], seasonParticipants: [ { id: "sp-A", sportsSeasonId: "ss-1", participantId: "cp-A", name: "A", }, { id: "sp-B", sportsSeasonId: "ss-1", participantId: "cp-B", name: "B", }, { id: "sp-C", sportsSeasonId: "ss-1", participantId: "cp-C", name: "C", }, ], }); const db = makeFakeDb(state); vi.mocked(database).mockReturnValue(db as never); vi.mocked(processQualifyingEvent).mockResolvedValue(undefined); const report = await syncTournamentResults("t-1"); expect(report.windowsSynced).toBe(1); expect(report.windowsFailed).toBe(0); expect(report.failures).toEqual([]); const eventRows = state.eventResults.filter( (r) => r.scoringEventId === "ev-1" ); // sp-A: placement 1, sp-B: placement 2, sp-C: 0-QP filler expect(eventRows).toHaveLength(3); const a = eventRows.find((r) => r.seasonParticipantId === "sp-A"); expect(a?.placement).toBe(1); expect(a?.rawScore).toBe("280"); const b = eventRows.find((r) => r.seasonParticipantId === "sp-B"); expect(b?.placement).toBe(2); expect(b?.rawScore).toBe("282"); const c = eventRows.find((r) => r.seasonParticipantId === "sp-C"); expect(c?.placement).toBeNull(); expect(c?.qualifyingPointsAwarded).toBe("0"); expect(processQualifyingEvent).toHaveBeenCalledTimes(1); expect(processQualifyingEvent).toHaveBeenCalledWith("ev-1", db); }); // ------------------------------------------------------------------------- // Test 2: Two overlapping windows — same tournament, different sports_seasons // ------------------------------------------------------------------------- it("fans out to multiple windows, each getting only its own participants", async () => { const state = seedBasicState({ tournamentResults: [ { tournamentId: "t-1", participantId: "cp-X", placement: 1, rawScore: null }, { tournamentId: "t-1", participantId: "cp-Y", placement: 2, rawScore: null }, { tournamentId: "t-1", participantId: "cp-Z", placement: 3, rawScore: null }, ], scoringEvents: [ { id: "ev-A", sportsSeasonId: "ss-A", tournamentId: "t-1" }, { id: "ev-B", sportsSeasonId: "ss-B", tournamentId: "t-1" }, ], seasonParticipants: [ // Window A has X, Y { id: "sp-AX", sportsSeasonId: "ss-A", participantId: "cp-X", name: "X" }, { id: "sp-AY", sportsSeasonId: "ss-A", participantId: "cp-Y", name: "Y" }, // Window B has Y, Z { id: "sp-BY", sportsSeasonId: "ss-B", participantId: "cp-Y", name: "Y" }, { id: "sp-BZ", sportsSeasonId: "ss-B", participantId: "cp-Z", name: "Z" }, ], }); const db = makeFakeDb(state); vi.mocked(database).mockReturnValue(db as never); vi.mocked(processQualifyingEvent).mockResolvedValue(undefined); const report = await syncTournamentResults("t-1"); expect(report.windowsSynced).toBe(2); expect(report.windowsFailed).toBe(0); const aRows = state.eventResults.filter((r) => r.scoringEventId === "ev-A"); const bRows = state.eventResults.filter((r) => r.scoringEventId === "ev-B"); // Window A: sp-AX (placement 1), sp-AY (placement 2). No 0-QP fillers // since both roster members got results. expect(aRows).toHaveLength(2); expect(aRows.find((r) => r.seasonParticipantId === "sp-AX")?.placement).toBe(1); expect(aRows.find((r) => r.seasonParticipantId === "sp-AY")?.placement).toBe(2); // Window B: sp-BY (placement 2), sp-BZ (placement 3). Again no fillers. expect(bRows).toHaveLength(2); expect(bRows.find((r) => r.seasonParticipantId === "sp-BY")?.placement).toBe(2); expect(bRows.find((r) => r.seasonParticipantId === "sp-BZ")?.placement).toBe(3); expect(processQualifyingEvent).toHaveBeenCalledTimes(2); expect(processQualifyingEvent).toHaveBeenNthCalledWith(1, "ev-A", db); expect(processQualifyingEvent).toHaveBeenNthCalledWith(2, "ev-B", db); }); // ------------------------------------------------------------------------- // Test 3: Idempotent re-run // ------------------------------------------------------------------------- it("is idempotent: a second run leaves the same end state", async () => { const state = seedBasicState({ tournamentResults: [ { tournamentId: "t-1", participantId: "cp-A", placement: 1, rawScore: "10" }, ], scoringEvents: [ { id: "ev-1", sportsSeasonId: "ss-1", tournamentId: "t-1" }, ], seasonParticipants: [ { id: "sp-A", sportsSeasonId: "ss-1", participantId: "cp-A", name: "A" }, { id: "sp-B", sportsSeasonId: "ss-1", participantId: "cp-B", name: "B" }, ], }); const db = makeFakeDb(state); vi.mocked(database).mockReturnValue(db as never); vi.mocked(processQualifyingEvent).mockResolvedValue(undefined); await syncTournamentResults("t-1"); const afterFirst = JSON.parse(JSON.stringify(state.eventResults)); await syncTournamentResults("t-1"); const afterSecond = state.eventResults; // Same row count, same per-row placement / QP / participant / event. expect(afterSecond).toHaveLength(afterFirst.length); for (const first of afterFirst) { const match = afterSecond.find( (r) => r.scoringEventId === first.scoringEventId && r.seasonParticipantId === first.seasonParticipantId ); expect(match).toBeDefined(); expect(match?.placement).toBe(first.placement); expect(match?.qualifyingPointsAwarded).toBe(first.qualifyingPointsAwarded); expect(match?.rawScore).toBe(first.rawScore); } }); // ------------------------------------------------------------------------- // Test 4: Correction propagation // ------------------------------------------------------------------------- it("propagates canonical placement corrections to all linked windows", async () => { const state = seedBasicState({ tournamentResults: [ { tournamentId: "t-1", participantId: "cp-A", placement: 1, rawScore: null }, ], scoringEvents: [ { id: "ev-A", sportsSeasonId: "ss-A", tournamentId: "t-1" }, { id: "ev-B", sportsSeasonId: "ss-B", tournamentId: "t-1" }, ], seasonParticipants: [ { id: "sp-AA", sportsSeasonId: "ss-A", participantId: "cp-A", name: "A" }, { id: "sp-BA", sportsSeasonId: "ss-B", participantId: "cp-A", name: "A" }, ], }); const db = makeFakeDb(state); vi.mocked(database).mockReturnValue(db as never); vi.mocked(processQualifyingEvent).mockResolvedValue(undefined); await syncTournamentResults("t-1"); // Sanity — both windows have sp-* at placement 1. expect( state.eventResults.find((r) => r.seasonParticipantId === "sp-AA")?.placement ).toBe(1); expect( state.eventResults.find((r) => r.seasonParticipantId === "sp-BA")?.placement ).toBe(1); // Correct the canonical placement from 1 to 5. state.tournamentResults[0].placement = 5; state.tournamentResults[0].rawScore = "99"; await syncTournamentResults("t-1"); expect( state.eventResults.find((r) => r.seasonParticipantId === "sp-AA")?.placement ).toBe(5); expect( state.eventResults.find((r) => r.seasonParticipantId === "sp-AA")?.rawScore ).toBe("99"); expect( state.eventResults.find((r) => r.seasonParticipantId === "sp-BA")?.placement ).toBe(5); expect( state.eventResults.find((r) => r.seasonParticipantId === "sp-BA")?.rawScore ).toBe("99"); }); // ------------------------------------------------------------------------- // Test 5: Partial failure isolation // ------------------------------------------------------------------------- it("isolates failures: one window's error does not stop the others", async () => { const state = seedBasicState({ tournamentResults: [ { tournamentId: "t-1", participantId: "cp-A", placement: 1, rawScore: null }, ], scoringEvents: [ { id: "ev-A", sportsSeasonId: "ss-A", tournamentId: "t-1" }, { id: "ev-B", sportsSeasonId: "ss-B", tournamentId: "t-1" }, ], seasonParticipants: [ { id: "sp-AA", sportsSeasonId: "ss-A", participantId: "cp-A", name: "A" }, { id: "sp-BA", sportsSeasonId: "ss-B", participantId: "cp-A", name: "A" }, ], }); const db = makeFakeDb(state); vi.mocked(database).mockReturnValue(db as never); // First window throws in processQualifyingEvent; second succeeds. vi.mocked(processQualifyingEvent) .mockImplementationOnce(async () => { throw new Error("QP calc failed"); }) .mockImplementationOnce(async () => undefined); const report = await syncTournamentResults("t-1"); expect(report.windowsSynced).toBe(1); expect(report.windowsFailed).toBe(1); expect(report.failures).toHaveLength(1); expect(report.failures[0].scoringEventId).toBe("ev-A"); expect(report.failures[0].sportsSeasonId).toBe("ss-A"); expect(report.failures[0].error).toContain("QP calc failed"); // Second window (ev-B) still has its row committed in our fake (we don't // simulate rollback, but the service called through to the writes path, // which is what we care about — the try/catch did not propagate). expect( state.eventResults.find( (r) => r.scoringEventId === "ev-B" && r.seasonParticipantId === "sp-BA" ) ).toBeDefined(); expect(processQualifyingEvent).toHaveBeenCalledTimes(2); }); // ------------------------------------------------------------------------- // Test 6: Participant in roster but not in canonical results → 0-QP filler // ------------------------------------------------------------------------- it("writes 0-QP filler rows for roster participants absent from canonical results", async () => { const state = seedBasicState({ tournamentResults: [ { tournamentId: "t-1", participantId: "cp-A", placement: 1, rawScore: null }, ], scoringEvents: [ { id: "ev-1", sportsSeasonId: "ss-1", tournamentId: "t-1" }, ], seasonParticipants: [ { id: "sp-A", sportsSeasonId: "ss-1", participantId: "cp-A", name: "A" }, { id: "sp-B", sportsSeasonId: "ss-1", participantId: "cp-B", name: "B" }, { id: "sp-C", sportsSeasonId: "ss-1", participantId: "cp-C", name: "C" }, ], }); const db = makeFakeDb(state); vi.mocked(database).mockReturnValue(db as never); vi.mocked(processQualifyingEvent).mockResolvedValue(undefined); await syncTournamentResults("t-1"); const rows = state.eventResults; expect(rows).toHaveLength(3); const a = rows.find((r) => r.seasonParticipantId === "sp-A"); expect(a?.placement).toBe(1); expect(a?.qualifyingPointsAwarded).toBeUndefined(); // not set by this service const b = rows.find((r) => r.seasonParticipantId === "sp-B"); expect(b?.placement).toBeNull(); expect(b?.qualifyingPointsAwarded).toBe("0"); const c = rows.find((r) => r.seasonParticipantId === "sp-C"); expect(c?.placement).toBeNull(); expect(c?.qualifyingPointsAwarded).toBe("0"); }); });