import { describe, it, expect, vi, afterEach } from "vitest"; import { findMatchingTeamName } from "~/lib/normalize-team-name"; import { OcBlacktopIndyCarStandingsAdapter } from "../f1"; import type { FetchedStandingsRecord, StandingsSyncAdapter } from "../types"; // Pure unit tests for the name-matching logic used by syncStandings. // The full orchestrator requires DB context; those are covered by integration tests. describe("findMatchingTeamName (sync name-matching logic)", () => { const participants = [ "Boston Celtics", "Los Angeles Lakers", "Golden State Warriors", "Oklahoma City Thunder", "San Antonio Spurs", "New York Knicks", ]; it("matches exact names", () => { expect(findMatchingTeamName("Boston Celtics", participants)).toBe("Boston Celtics"); expect(findMatchingTeamName("New York Knicks", participants)).toBe("New York Knicks"); }); it("is case-insensitive", () => { expect(findMatchingTeamName("golden state warriors", participants)).toBe( "Golden State Warriors" ); expect(findMatchingTeamName("SAN ANTONIO SPURS", participants)).toBe("San Antonio Spurs"); }); it("matches partial city+team via substring", () => { expect(findMatchingTeamName("Golden State", participants)).toBe("Golden State Warriors"); expect(findMatchingTeamName("Oklahoma City", participants)).toBe("Oklahoma City Thunder"); // "New York" is a substring of "New York Knicks" expect(findMatchingTeamName("New York", participants)).toBe("New York Knicks"); }); it("returns null for truly unmatched names", () => { expect(findMatchingTeamName("Phoenix Suns", participants)).toBeNull(); expect(findMatchingTeamName("Charlotte Hornets", participants)).toBeNull(); }); it("returns null for empty string", () => { expect(findMatchingTeamName("", participants)).toBeNull(); }); it("handles extra whitespace gracefully", () => { expect(findMatchingTeamName(" Boston Celtics ", participants)).toBe("Boston Celtics"); }); }); describe("OcBlacktopIndyCarStandingsAdapter", () => { afterEach(() => { vi.restoreAllMocks(); }); // A fallback that records whether it was invoked and returns a sentinel result. function makeFallback(): StandingsSyncAdapter & { called: boolean } { const stub = { called: false, async fetchStandings(): Promise { stub.called = true; return [ { teamName: "ESPN Fallback Driver", externalTeamId: "espn-1", wins: 0, losses: 0, winPct: 0, gamesPlayed: 0, leagueRank: 1, currentPoints: 1, }, ]; }, }; return stub; } it("maps OC Blacktop driver standings to FetchedStandingsRecord", async () => { vi.spyOn(globalThis, "fetch").mockResolvedValue( new Response( JSON.stringify([ { id: "ocb-palou", position: 1, points: "409.00", firstName: "Alex", lastName: "Palou" }, { id: "ocb-kirkwood", position: 2, points: "348.00", firstName: "Kyle", lastName: "Kirkwood" }, ]), { status: 200, headers: { "Content-Type": "application/json" } } ) ); const fallback = makeFallback(); const adapter = new OcBlacktopIndyCarStandingsAdapter("test-key", fallback); const records = await adapter.fetchStandings(); expect(fallback.called).toBe(false); expect(records).toEqual([ expect.objectContaining({ teamName: "Alex Palou", externalTeamId: "ocb-palou", leagueRank: 1, currentPoints: 409, }), expect.objectContaining({ teamName: "Kyle Kirkwood", externalTeamId: "ocb-kirkwood", leagueRank: 2, currentPoints: 348, }), ]); }); it("falls back to ESPN when the API responds non-OK", async () => { vi.spyOn(globalThis, "fetch").mockResolvedValue( new Response("nope", { status: 500, statusText: "Internal Server Error" }) ); const fallback = makeFallback(); const adapter = new OcBlacktopIndyCarStandingsAdapter("test-key", fallback); const records = await adapter.fetchStandings(); expect(fallback.called).toBe(true); expect(records[0].teamName).toBe("ESPN Fallback Driver"); }); it("falls back to ESPN when the payload is empty", async () => { vi.spyOn(globalThis, "fetch").mockResolvedValue( new Response("[]", { status: 200, headers: { "Content-Type": "application/json" } }) ); const fallback = makeFallback(); const adapter = new OcBlacktopIndyCarStandingsAdapter("test-key", fallback); await adapter.fetchStandings(); expect(fallback.called).toBe(true); }); it("falls back to ESPN when the payload is missing expected fields (schema drift)", async () => { // Non-empty array, but `points` was renamed upstream — must fail over, not // silently sync zeros over real standings. vi.spyOn(globalThis, "fetch").mockResolvedValue( new Response( JSON.stringify([ { id: "ocb-palou", position: 1, championshipPoints: "409.00", firstName: "Alex", lastName: "Palou" }, ]), { status: 200, headers: { "Content-Type": "application/json" } } ) ); const fallback = makeFallback(); const adapter = new OcBlacktopIndyCarStandingsAdapter("test-key", fallback); const records = await adapter.fetchStandings(); expect(fallback.called).toBe(true); expect(records[0].teamName).toBe("ESPN Fallback Driver"); }); it("keeps legitimate zero-point drivers (0 points is valid, not a failure)", async () => { vi.spyOn(globalThis, "fetch").mockResolvedValue( new Response( JSON.stringify([ { id: "ocb-1", position: 1, points: "409.00", firstName: "Alex", lastName: "Palou" }, { id: "ocb-2", position: 33, points: "0.00", firstName: "Rookie", lastName: "Driver" }, ]), { status: 200, headers: { "Content-Type": "application/json" } } ) ); const fallback = makeFallback(); const adapter = new OcBlacktopIndyCarStandingsAdapter("test-key", fallback); const records = await adapter.fetchStandings(); expect(fallback.called).toBe(false); expect(records[1]).toEqual( expect.objectContaining({ teamName: "Rookie Driver", leagueRank: 33, currentPoints: 0 }) ); }); it("falls back to ESPN when no API key is configured", async () => { const fetchSpy = vi.spyOn(globalThis, "fetch"); const fallback = makeFallback(); // Empty string is falsy but (unlike undefined) does not trigger the env default. const adapter = new OcBlacktopIndyCarStandingsAdapter("", fallback); const records = await adapter.fetchStandings(); expect(fetchSpy).not.toHaveBeenCalled(); expect(fallback.called).toBe(true); expect(records[0].teamName).toBe("ESPN Fallback Driver"); }); });