2026-07-06 21:20:47 -07:00
|
|
|
import { describe, it, expect, vi, afterEach } from "vitest";
|
2026-03-21 00:12:01 -07:00
|
|
|
import { findMatchingTeamName } from "~/lib/normalize-team-name";
|
2026-07-06 21:20:47 -07:00
|
|
|
import { OcBlacktopIndyCarStandingsAdapter } from "../f1";
|
|
|
|
|
import type { FetchedStandingsRecord, StandingsSyncAdapter } from "../types";
|
2026-03-21 00:12:01 -07:00
|
|
|
|
|
|
|
|
// 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");
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-07-06 21:20:47 -07:00
|
|
|
|
|
|
|
|
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<FetchedStandingsRecord[]> {
|
|
|
|
|
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");
|
|
|
|
|
});
|
|
|
|
|
});
|