import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { EspnScheduleAdapter } from "../espn-schedule"; const SAMPLE_RESPONSE = { events: [ { id: "401234567", date: "2025-04-01T18:00:00Z", competitions: [ { competitors: [ { id: "9", team: { id: "9", displayName: "Boston Red Sox" }, score: "5", homeAway: "home" as const, winner: true, }, { id: "4", team: { id: "4", displayName: "New York Yankees" }, score: "3", homeAway: "away" as const, winner: false, }, ], status: { type: { name: "STATUS_FINAL", completed: true } }, matchday: null, }, ], }, { id: "401234568", date: "2025-04-02T19:00:00Z", competitions: [ { competitors: [ { id: "12", team: { id: "12", displayName: "Los Angeles Dodgers" }, score: "0", homeAway: "home" as const, winner: false, }, { id: "15", team: { id: "15", displayName: "San Francisco Giants" }, score: "0", homeAway: "away" as const, winner: false, }, ], status: { type: { name: "STATUS_SCHEDULED", completed: false } }, matchday: null, }, ], }, ], }; describe("EspnScheduleAdapter", () => { beforeEach(() => { vi.stubGlobal("fetch", vi.fn()); }); afterEach(() => { vi.restoreAllMocks(); }); it("maps completed matches with scores and winner", async () => { vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => SAMPLE_RESPONSE, } as Response); const adapter = new EspnScheduleAdapter("baseball/mlb"); const matches = await adapter.fetchMatches("2025"); expect(matches).toHaveLength(2); const completed = matches[0]; expect(completed.externalMatchId).toBe("401234567"); expect(completed.team1ExternalId).toBe("9"); expect(completed.team1Name).toBe("Boston Red Sox"); expect(completed.team2ExternalId).toBe("4"); expect(completed.team2Name).toBe("New York Yankees"); expect(completed.team1Score).toBe(5); expect(completed.team2Score).toBe(3); expect(completed.winnerExternalId).toBe("9"); expect(completed.status).toBe("complete"); expect(completed.completedAt).toBeInstanceOf(Date); }); it("maps scheduled matches with null scores", async () => { vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => SAMPLE_RESPONSE, } as Response); const [, scheduled] = await new EspnScheduleAdapter("baseball/mlb").fetchMatches("2025"); expect(scheduled.status).toBe("scheduled"); expect(scheduled.winnerExternalId).toBeNull(); expect(scheduled.completedAt).toBeNull(); expect(scheduled.startedAt).toBeNull(); }); it("sets matchStage to null for all ESPN matches", async () => { vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => SAMPLE_RESPONSE, } as Response); const matches = await new EspnScheduleAdapter("baseball/mlb").fetchMatches("2025"); for (const m of matches) { expect(m.matchStage).toBeNull(); } }); it("builds correct ESPN URL with sport path and dates param", async () => { vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => ({ events: [] }), } as Response); await new EspnScheduleAdapter("basketball/nba").fetchMatches("2025"); expect(fetch).toHaveBeenCalledWith( expect.stringContaining("basketball/nba") ); expect(fetch).toHaveBeenCalledWith( expect.stringContaining("dates=2025") ); }); it("throws on non-ok API response", async () => { vi.mocked(fetch).mockResolvedValueOnce({ ok: false, status: 429, text: async () => "Too Many Requests", } as Response); await expect(new EspnScheduleAdapter("baseball/mlb").fetchMatches("2025")).rejects.toThrow( "ESPN API error 429" ); }); it("handles in-progress matches", async () => { const response = { events: [ { id: "999", date: "2025-04-03T20:00:00Z", competitions: [ { competitors: [ { id: "1", team: { id: "1", displayName: "Home Team" }, score: "2", homeAway: "home" as const }, { id: "2", team: { id: "2", displayName: "Away Team" }, score: "1", homeAway: "away" as const }, ], status: { type: { name: "STATUS_IN_PROGRESS", completed: false } }, }, ], }, ], }; vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => response } as Response); const [m] = await new EspnScheduleAdapter("baseball/mlb").fetchMatches("2025"); expect(m.status).toBe("in_progress"); expect(m.startedAt).toBeInstanceOf(Date); expect(m.completedAt).toBeNull(); }); it("preserves score of 0 — does not coerce falsy string to null", async () => { const response = { events: [ { id: "777", date: "2025-04-05T18:00:00Z", competitions: [ { competitors: [ { id: "10", team: { id: "10", displayName: "Team A" }, score: "0", homeAway: "home" as const, winner: false }, { id: "11", team: { id: "11", displayName: "Team B" }, score: "3", homeAway: "away" as const, winner: true }, ], status: { type: { name: "STATUS_FINAL", completed: true } }, matchday: null, }, ], }, ], }; vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => response } as Response); const [m] = await new EspnScheduleAdapter("baseball/mlb").fetchMatches("2025"); expect(m.team1Score).toBe(0); expect(m.team2Score).toBe(3); }); it("returns null (not NaN) for non-numeric score strings", async () => { const response = { events: [ { id: "778", date: "2025-04-06T18:00:00Z", competitions: [ { competitors: [ { id: "10", team: { id: "10", displayName: "Team A" }, score: "F/OT", homeAway: "home" as const, winner: true }, { id: "11", team: { id: "11", displayName: "Team B" }, score: "TBD", homeAway: "away" as const, winner: false }, ], status: { type: { name: "STATUS_FINAL", completed: true } }, matchday: null, }, ], }, ], }; vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => response } as Response); const [m] = await new EspnScheduleAdapter("baseball/mlb").fetchMatches("2025"); expect(m.team1Score).toBeNull(); expect(m.team2Score).toBeNull(); }); it("appends seasontype param to URL when seasonType is provided", async () => { vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => ({ events: [] }), } as Response); await new EspnScheduleAdapter("baseball/mlb", 3).fetchMatches("2025"); expect(fetch).toHaveBeenCalledWith( expect.stringContaining("seasontype=3") ); }); });