brackt/app/models/__tests__/upcoming-calendar.test.ts
Chris Parsons e6f3c396ae
All checks were successful
🚀 Deploy / 🧪 Test (pull_request) Successful in 1m29s
🚀 Deploy / ʦ TypeScript (pull_request) Successful in 1m19s
🚀 Deploy / 🔍 Lint (pull_request) Successful in 47s
🚀 Deploy / 🐳 Build (pull_request) Has been skipped
🚀 Deploy / 🚀 Deploy (pull_request) Has been skipped
Fix test mocks for group stage upcoming events changes
Add groupStageMatches, tournamentGroups, and seasonParticipants to
the schema mock, add db.select mock for the group stage query, and
wire default empty results into all bracket-path beforeEach hooks.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 22:34:26 -07:00

690 lines
29 KiB
TypeScript

/**
* Tests for getUpcomingEventsForDraftedParticipants logic.
*
* We test the exported function indirectly by exercising the pure
* business-logic paths (all-compete vs bracket, date filtering,
* deduplication, empty inputs) using a vi.mock of the database context
* so no real DB is required.
*/
import { describe, it, expect, vi, beforeEach } from "vitest";
// ── DB mock ────────────────────────────────────────────────────────────────
// Must be declared before the import that triggers the module graph.
const mockDb = {
query: {
scoringEvents: { findMany: vi.fn() },
seasonParticipants: { findMany: vi.fn() },
},
selectDistinct: vi.fn(),
select: vi.fn(),
};
vi.mock("~/database/context", () => ({
database: () => mockDb,
}));
vi.mock("~/database/schema", () => ({
scoringEvents: { sportsSeasonId: "se.sports_season_id", isComplete: "se.is_complete", eventDate: "se.event_date", eventType: "se.event_type", name: "se.name" },
playoffMatches: { id: "pm.id", scoringEventId: "pm.scoring_event_id", participant1Id: "pm.participant1_id", participant2Id: "pm.participant2_id", round: "pm.round", matchNumber: "pm.match_number", isComplete: "pm.is_complete" },
playoffMatchGames: { playoffMatchId: "pmg.playoff_match_id", scheduledAt: "pmg.scheduled_at", gameNumber: "pmg.game_number" },
groupStageMatches: { id: "gsm.id", tournamentGroupId: "gsm.tournament_group_id", participant1Id: "gsm.participant1_id", participant2Id: "gsm.participant2_id", scheduledAt: "gsm.scheduled_at", isComplete: "gsm.is_complete" },
tournamentGroups: { id: "tg.id", groupName: "tg.group_name", scoringEventId: "tg.scoring_event_id" },
seasonParticipants: { id: "sp.id" },
}));
// Chain helper for the group stage db.select() query (resolves at orderBy).
function makeGroupStageSelectChain(rows: unknown[]) {
return {
from: vi.fn().mockReturnThis(),
innerJoin: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
orderBy: vi.fn().mockResolvedValue(rows),
};
}
vi.mock("drizzle-orm", () => ({
eq: (col: unknown, val: unknown) => ({ type: "eq", col, val }),
and: (...args: unknown[]) => ({ type: "and", args }),
asc: (col: unknown) => ({ type: "asc", col }),
desc: (col: unknown) => ({ type: "desc", col }),
gte: (col: unknown, val: unknown) => ({ type: "gte", col, val }),
lte: (col: unknown, val: unknown) => ({ type: "lte", col, val }),
or: (...args: unknown[]) => ({ type: "or", args }),
inArray: (col: unknown, arr: unknown) => ({ type: "inArray", col, arr }),
isNotNull: (col: unknown) => ({ type: "isNotNull", col }),
}));
import { getUpcomingEventsForDraftedParticipants } from "../scoring-event";
// ── Helpers ────────────────────────────────────────────────────────────────
const DATE_FROM = "2025-04-01";
const DATE_TO = "2025-04-30";
const SPORTS_SEASON_ID = "ss-1";
function makeParticipant(id: string, name: string) {
return { id, name };
}
function makeScoringEvent(overrides: Partial<{
id: string; name: string; eventDate: string | null;
eventStartsAt: Date | null;
eventType: string; sportsSeasonId: string; isComplete: boolean;
}> = {}) {
return {
id: overrides.id ?? "event-1",
name: overrides.name ?? "Test Event",
eventDate: overrides.eventDate ?? "2025-04-10",
eventStartsAt: overrides.eventStartsAt ?? null,
eventType: overrides.eventType ?? "schedule_event",
sportsSeasonId: overrides.sportsSeasonId ?? SPORTS_SEASON_ID,
isComplete: overrides.isComplete ?? false,
};
}
// ── All-compete patterns ───────────────────────────────────────────────────
describe("getUpcomingEventsForDraftedParticipants — all-compete sports", () => {
beforeEach(() => {
vi.clearAllMocks();
// selectDistinct won't be called for all-compete; default for the "unknown
// pattern" test which falls through to the bracket path.
mockDb.selectDistinct.mockReturnValue({ from: vi.fn().mockReturnThis(), innerJoin: vi.fn().mockReturnThis(), leftJoin: vi.fn().mockReturnThis(), where: vi.fn().mockReturnThis(), orderBy: vi.fn().mockResolvedValue([]) });
mockDb.select.mockReturnValue(makeGroupStageSelectChain([]));
mockDb.query.seasonParticipants.findMany.mockResolvedValue([]);
});
it("returns [] when draftedParticipants is empty", async () => {
const result = await getUpcomingEventsForDraftedParticipants(
SPORTS_SEASON_ID, "season_standings", [], DATE_FROM, DATE_TO
);
expect(result).toEqual([]);
expect(mockDb.query.scoringEvents.findMany).not.toHaveBeenCalled();
});
it("returns all events with all drafted participants attached", async () => {
const participants = [makeParticipant("p1", "Verstappen"), makeParticipant("p2", "Hamilton")];
const events = [makeScoringEvent({ id: "e1", name: "Australian GP" })];
mockDb.query.scoringEvents.findMany.mockResolvedValue(events);
const result = await getUpcomingEventsForDraftedParticipants(
SPORTS_SEASON_ID, "season_standings", participants, DATE_FROM, DATE_TO
);
expect(result).toHaveLength(1);
expect(result[0].name).toBe("Australian GP");
expect(result[0].relevantParticipants).toEqual(participants);
});
it("works for qualifying_points pattern", async () => {
const participants = [makeParticipant("g1", "Scheffler")];
mockDb.query.scoringEvents.findMany.mockResolvedValue([makeScoringEvent({ id: "e1", name: "Masters" })]);
const result = await getUpcomingEventsForDraftedParticipants(
SPORTS_SEASON_ID, "qualifying_points", participants, DATE_FROM, DATE_TO
);
expect(result).toHaveLength(1);
});
it("unknown pattern (e.g. golf_qualifying_points) falls through to bracket path, not all-compete", async () => {
// golf_qualifying_points is a simulatorType, not a scoringPattern.
// Unknown patterns are treated as bracket sports (selectDistinct path).
const participants = [makeParticipant("g1", "McIlroy")];
const selectChain = { from: vi.fn().mockReturnThis(), innerJoin: vi.fn().mockReturnThis(), leftJoin: vi.fn().mockReturnThis(), where: vi.fn().mockReturnThis(), orderBy: vi.fn().mockResolvedValue([]) };
mockDb.selectDistinct.mockReturnValue(selectChain);
await getUpcomingEventsForDraftedParticipants(
SPORTS_SEASON_ID, "golf_qualifying_points", participants, DATE_FROM, DATE_TO
);
// Should NOT call the all-compete findMany path
expect(mockDb.query.scoringEvents.findMany).not.toHaveBeenCalled();
// Should call the bracket selectDistinct path
expect(mockDb.selectDistinct).toHaveBeenCalled();
});
it("returns multiple events sorted by eventDate", async () => {
const participants = [makeParticipant("p1", "Verstappen")];
const events = [
makeScoringEvent({ id: "e2", name: "Miami GP", eventDate: "2025-04-20" }),
makeScoringEvent({ id: "e1", name: "Australian GP", eventDate: "2025-04-06" }),
];
// findMany is ordered by asc(eventDate) in the real query; simulate same order
mockDb.query.scoringEvents.findMany.mockResolvedValue([events[1], events[0]]);
const result = await getUpcomingEventsForDraftedParticipants(
SPORTS_SEASON_ID, "season_standings", participants, DATE_FROM, DATE_TO
);
expect(result[0].name).toBe("Australian GP");
expect(result[1].name).toBe("Miami GP");
});
});
// ── Bracket patterns ───────────────────────────────────────────────────────
describe("getUpcomingEventsForDraftedParticipants — bracket sports", () => {
// Main query chain: resolves on orderBy (has leftJoin, orderBy)
function makeSelectChain(rows: unknown[]) {
const chain = {
from: vi.fn().mockReturnThis(),
innerJoin: vi.fn().mockReturnThis(),
leftJoin: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
orderBy: vi.fn().mockResolvedValue(rows),
};
return chain;
}
// Second query chain (max game lookup): resolves on where (no leftJoin, no orderBy)
function makeMaxGameChain(rows: unknown[]) {
const chain = {
from: vi.fn().mockReturnThis(),
innerJoin: vi.fn().mockReturnThis(),
where: vi.fn().mockResolvedValue(rows),
};
return chain;
}
beforeEach(() => {
vi.clearAllMocks();
// Default for the second selectDistinct call (max-game-number lookup).
// Individual tests override the first call with mockReturnValueOnce.
mockDb.selectDistinct.mockReturnValue(makeMaxGameChain([]));
// Default: no group stage matches (db.select used by the group stage query).
mockDb.select.mockReturnValue(makeGroupStageSelectChain([]));
mockDb.query.seasonParticipants.findMany.mockResolvedValue([]);
});
it("returns [] when draftedParticipants is empty", async () => {
const result = await getUpcomingEventsForDraftedParticipants(
SPORTS_SEASON_ID, "playoff_bracket", [], DATE_FROM, DATE_TO
);
expect(result).toEqual([]);
expect(mockDb.selectDistinct).not.toHaveBeenCalled();
});
it("returns one event with one relevant participant (participant1)", async () => {
const rows = [{
id: "e1", name: "UCL QF", eventDate: "2025-04-09",
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
playoffMatchId: "pm1",
participant1Id: "man-city", participant2Id: "bayern",
round: "Quarterfinals", gameNumber: 1, scheduledAt: null,
}];
mockDb.selectDistinct.mockReturnValueOnce(makeSelectChain(rows));
const result = await getUpcomingEventsForDraftedParticipants(
SPORTS_SEASON_ID, "playoff_bracket",
[makeParticipant("man-city", "Man City")],
DATE_FROM, DATE_TO
);
expect(result).toHaveLength(1);
expect(result[0].name).toBe("UCL QF");
expect(result[0].relevantParticipants).toEqual([{ id: "man-city", name: "Man City" }]);
});
it("returns one event when both participants are drafted (no duplication)", async () => {
// Two rows for the same event — the DB query uses selectDistinct on event columns
// but we may still get two rows if the WHERE matches on both participants.
// The grouping logic must deduplicate the event itself.
const rows = [
{
id: "e1", name: "UCL QF", eventDate: "2025-04-09",
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
playoffMatchId: "pm1",
participant1Id: "man-city", participant2Id: "arsenal",
round: "Quarterfinals", gameNumber: 1, scheduledAt: null,
},
];
mockDb.selectDistinct.mockReturnValueOnce(makeSelectChain(rows));
const result = await getUpcomingEventsForDraftedParticipants(
SPORTS_SEASON_ID, "playoff_bracket",
[makeParticipant("man-city", "Man City"), makeParticipant("arsenal", "Arsenal")],
DATE_FROM, DATE_TO
);
expect(result).toHaveLength(1);
expect(result[0].relevantParticipants).toHaveLength(2);
const names = result[0].relevantParticipants.map((p) => p.name);
expect(names).toContain("Man City");
expect(names).toContain("Arsenal");
});
it("returns separate events when picks are in different matches within the same scoring event", async () => {
const rows = [
{
id: "e1", name: "UCL QF", eventDate: "2025-04-09",
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
playoffMatchId: "pm1",
participant1Id: "man-city", participant2Id: "real-madrid",
round: "Quarterfinals", gameNumber: 1, scheduledAt: null,
},
{
id: "e1", name: "UCL QF", eventDate: "2025-04-09",
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
playoffMatchId: "pm2",
participant1Id: "arsenal", participant2Id: "psg",
round: "Quarterfinals", gameNumber: 1, scheduledAt: null,
},
];
mockDb.selectDistinct.mockReturnValueOnce(makeSelectChain(rows));
const result = await getUpcomingEventsForDraftedParticipants(
SPORTS_SEASON_ID, "playoff_bracket",
[makeParticipant("man-city", "Man City"), makeParticipant("arsenal", "Arsenal")],
DATE_FROM, DATE_TO
);
// Each match is now a separate entry
expect(result).toHaveLength(2);
const manCityEntry = result.find((r) => r.relevantParticipants.some((p) => p.id === "man-city"));
const arsenalEntry = result.find((r) => r.relevantParticipants.some((p) => p.id === "arsenal"));
expect(manCityEntry).toBeDefined();
expect(arsenalEntry).toBeDefined();
expect(manCityEntry?.relevantParticipants).toHaveLength(1);
expect(arsenalEntry?.relevantParticipants).toHaveLength(1);
});
it("only includes drafted participants in relevantParticipants, not opponents", async () => {
const rows = [{
id: "e1", name: "UCL QF", eventDate: "2025-04-09",
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
playoffMatchId: "pm1",
participant1Id: "man-city", participant2Id: "real-madrid",
round: "Quarterfinals", gameNumber: 1, scheduledAt: null,
}];
mockDb.selectDistinct.mockReturnValueOnce(makeSelectChain(rows));
const result = await getUpcomingEventsForDraftedParticipants(
SPORTS_SEASON_ID, "playoff_bracket",
[makeParticipant("man-city", "Man City")],
DATE_FROM, DATE_TO
);
expect(result[0].relevantParticipants).toEqual([{ id: "man-city", name: "Man City" }]);
// Real Madrid (the opponent) should NOT appear
expect(result[0].relevantParticipants.find((p) => p.id === "real-madrid")).toBeUndefined();
});
it("works for ucl_bracket pattern", async () => {
mockDb.selectDistinct.mockReturnValueOnce(makeSelectChain([{
id: "e1", name: "UCL SF", eventDate: "2025-04-29",
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
playoffMatchId: "pm1",
participant1Id: "man-city", participant2Id: "arsenal",
round: "Semifinals", gameNumber: 1, scheduledAt: null,
}]));
const result = await getUpcomingEventsForDraftedParticipants(
SPORTS_SEASON_ID, "ucl_bracket",
[makeParticipant("man-city", "Man City")],
DATE_FROM, DATE_TO
);
expect(result).toHaveLength(1);
});
it("includes events with null eventDate when they have games scheduled within the window", async () => {
// Real-world case: admin sets game times on individual games (scheduledAt)
// but leaves scoringEvent.eventDate null — the event must still appear.
const rows = [{
id: "e1", name: "UCL QF",
eventDate: null, // ← no date on the event itself
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
playoffMatchId: "pm1",
participant1Id: "barcelona", participant2Id: "sporting",
round: "Knockout Stage", gameNumber: 1, scheduledAt: null,
}];
mockDb.selectDistinct.mockReturnValueOnce(makeSelectChain(rows));
const result = await getUpcomingEventsForDraftedParticipants(
SPORTS_SEASON_ID, "playoff_bracket",
[makeParticipant("barcelona", "Barcelona")],
DATE_FROM, DATE_TO
);
expect(result).toHaveLength(1);
expect(result[0].name).toBe("UCL QF");
expect(result[0].eventDate).toBeNull();
expect(result[0].relevantParticipants).toEqual([{ id: "barcelona", name: "Barcelona" }]);
});
it("surfaces earliestGameTime from scheduledAt when eventDate is null", async () => {
const gameTime = new Date("2025-04-09T14:00:00.000Z");
const rows = [{
id: "e1", name: "UCL QF",
eventDate: null,
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
playoffMatchId: "pm1",
participant1Id: "barcelona", participant2Id: "sporting",
round: "Knockout Stage", gameNumber: 1, scheduledAt: gameTime,
}];
mockDb.selectDistinct.mockReturnValueOnce(makeSelectChain(rows));
const result = await getUpcomingEventsForDraftedParticipants(
SPORTS_SEASON_ID, "playoff_bracket",
[makeParticipant("barcelona", "Barcelona")],
DATE_FROM, DATE_TO
);
expect(result[0].earliestGameTime).toBe(gameTime.toISOString());
expect(result[0].eventDate).toBeNull();
});
it("series with both games in window produces two entries, each with correct time and label", async () => {
const gameTime1 = new Date("2025-04-09T13:00:00.000Z");
const gameTime2 = new Date("2025-04-11T15:00:00.000Z");
const rows = [
{
id: "e1", name: "UCL QF", eventDate: null,
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
playoffMatchId: "pm1",
participant1Id: "barcelona", participant2Id: "sporting",
round: "Knockout Stage", gameNumber: 1, scheduledAt: gameTime1,
},
{
id: "e1", name: "UCL QF", eventDate: null,
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
playoffMatchId: "pm1",
participant1Id: "barcelona", participant2Id: "sporting",
round: "Knockout Stage", gameNumber: 2, scheduledAt: gameTime2,
},
];
mockDb.selectDistinct
.mockReturnValueOnce(makeSelectChain(rows))
.mockReturnValueOnce(makeMaxGameChain([
{ eventId: "e1", gameNumber: 1 },
{ eventId: "e1", gameNumber: 2 },
]));
const result = await getUpcomingEventsForDraftedParticipants(
SPORTS_SEASON_ID, "playoff_bracket",
[makeParticipant("barcelona", "Barcelona")],
DATE_FROM, DATE_TO
);
expect(result).toHaveLength(2);
const game1 = result.find((r) => r.id === "e1|pm1|1");
const game2 = result.find((r) => r.id === "e1|pm1|2");
expect(game1?.earliestGameTime).toBe(gameTime1.toISOString());
expect(game1?.matchLabel).toBe("Knockout Stage Game #1");
expect(game2?.earliestGameTime).toBe(gameTime2.toISOString());
expect(game2?.matchLabel).toBe("Knockout Stage Game #2");
});
it("sets matchLabel to round name only (no game number) for single-game matchup", async () => {
const rows = [{
id: "e1", name: "PDC World Championship", eventDate: "2025-04-09",
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
playoffMatchId: "pm1",
participant1Id: "man-city", participant2Id: "arsenal",
round: "Semifinals", gameNumber: 1, scheduledAt: null,
}];
mockDb.selectDistinct.mockReturnValueOnce(makeSelectChain(rows));
const result = await getUpcomingEventsForDraftedParticipants(
SPORTS_SEASON_ID, "playoff_bracket",
[makeParticipant("man-city", "Man City")],
DATE_FROM, DATE_TO
);
expect(result[0].matchLabel).toBe("Semifinals");
});
it("sets matchLabel to null when round name matches event name (single game)", async () => {
// e.g. event named "Knockout Stage", round also "Knockout Stage", single game
// → show null rather than redundant label
const rows = [{
id: "e1", name: "Knockout Stage", eventDate: null,
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
playoffMatchId: "pm1",
participant1Id: "barcelona", participant2Id: "sporting",
round: "Knockout Stage", gameNumber: 1, scheduledAt: null,
}];
mockDb.selectDistinct.mockReturnValueOnce(makeSelectChain(rows));
const result = await getUpcomingEventsForDraftedParticipants(
SPORTS_SEASON_ID, "playoff_bracket",
[makeParticipant("barcelona", "Barcelona")],
DATE_FROM, DATE_TO
);
expect(result[0].matchLabel).toBeNull();
});
it("shows game number when only game #2 is in the window (game #1 already played)", async () => {
const rows = [{
id: "e1", name: "PDC World Championship", eventDate: "2025-04-10",
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
playoffMatchId: "pm1",
participant1Id: "man-city", participant2Id: "arsenal",
round: "Semifinals", gameNumber: 2, scheduledAt: null,
}];
mockDb.selectDistinct
.mockReturnValueOnce(makeSelectChain(rows))
.mockReturnValueOnce(makeMaxGameChain([
{ eventId: "e1", gameNumber: 1 },
{ eventId: "e1", gameNumber: 2 },
]));
const result = await getUpcomingEventsForDraftedParticipants(
SPORTS_SEASON_ID, "playoff_bracket",
[makeParticipant("man-city", "Man City")],
DATE_FROM, DATE_TO
);
expect(result[0].matchLabel).toBe("Semifinals Game #2");
});
it("shows Game #1 label when game #1 is upcoming but series has multiple games", async () => {
// Game #2 exists in DB but is outside the date window — only Game #1 appears
// in the main query. The second (un-date-filtered) query reveals it's a series.
const mainRows = [{
id: "e1", name: "NBA Finals", eventDate: "2025-04-10",
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
playoffMatchId: "pm1",
participant1Id: "lakers", participant2Id: "celtics",
round: "Finals", gameNumber: 1, scheduledAt: null,
}];
const allGameRows = [
{ eventId: "e1", gameNumber: 1 },
{ eventId: "e1", gameNumber: 2 },
];
mockDb.selectDistinct
.mockReturnValueOnce(makeSelectChain(mainRows))
.mockReturnValueOnce(makeMaxGameChain(allGameRows));
const result = await getUpcomingEventsForDraftedParticipants(
SPORTS_SEASON_ID, "playoff_bracket",
[makeParticipant("lakers", "Lakers")],
DATE_FROM, DATE_TO
);
expect(result[0].matchLabel).toBe("Finals Game #1");
});
it("separates matches within the same scoring event by scheduled time (snooker-style bracket)", async () => {
const time1 = new Date("2025-04-18T06:30:00.000Z");
const time2 = new Date("2025-04-19T06:30:00.000Z");
const rows = [
{
id: "e1", name: "Snooker World Championship", eventDate: null,
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
playoffMatchId: "pm-williams",
participant1Id: "mark-williams", participant2Id: "opponent-a",
round: "Round of 32", gameNumber: 1, scheduledAt: time1,
},
{
id: "e1", name: "Snooker World Championship", eventDate: null,
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
playoffMatchId: "pm-higgins",
participant1Id: "john-higgins", participant2Id: "opponent-b",
round: "Round of 32", gameNumber: 1, scheduledAt: time2,
},
];
mockDb.selectDistinct.mockReturnValueOnce(makeSelectChain(rows));
const result = await getUpcomingEventsForDraftedParticipants(
SPORTS_SEASON_ID, "playoff_bracket",
[makeParticipant("mark-williams", "Mark Williams"), makeParticipant("john-higgins", "John Higgins")],
DATE_FROM, DATE_TO
);
expect(result).toHaveLength(2);
const williamsEntry = result.find((r) => r.relevantParticipants.some((p) => p.id === "mark-williams"));
const higginsEntry = result.find((r) => r.relevantParticipants.some((p) => p.id === "john-higgins"));
expect(williamsEntry).toBeDefined();
expect(higginsEntry).toBeDefined();
expect(williamsEntry?.earliestGameTime).toBe(time1.toISOString());
expect(higginsEntry?.earliestGameTime).toBe(time2.toISOString());
expect(williamsEntry?.matchLabel).toBe("Round of 32");
expect(higginsEntry?.matchLabel).toBe("Round of 32");
});
it("all-compete events have null earliestGameTime and matchLabel", async () => {
const participants = [makeParticipant("p1", "Verstappen")];
mockDb.query.scoringEvents.findMany.mockResolvedValue([
makeScoringEvent({ id: "e1", name: "Australian GP" }),
]);
const result = await getUpcomingEventsForDraftedParticipants(
SPORTS_SEASON_ID, "season_standings", participants, DATE_FROM, DATE_TO
);
expect(result[0].earliestGameTime).toBeNull();
expect(result[0].matchLabel).toBeNull();
});
});
// ── eventStartsAt — all-compete sports ────────────────────────────────────
describe("getUpcomingEventsForDraftedParticipants — eventStartsAt for all-compete sports", () => {
beforeEach(() => {
vi.clearAllMocks();
mockDb.selectDistinct.mockReturnValue({
from: vi.fn().mockReturnThis(),
innerJoin: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
orderBy: vi.fn().mockResolvedValue([]),
});
});
it("populates earliestGameTime from eventStartsAt when set", async () => {
const raceStart = new Date("2025-04-06T14:00:00.000Z");
const participants = [makeParticipant("p1", "Verstappen")];
mockDb.query.scoringEvents.findMany.mockResolvedValue([
makeScoringEvent({ id: "e1", name: "Australian GP", eventStartsAt: raceStart }),
]);
const result = await getUpcomingEventsForDraftedParticipants(
SPORTS_SEASON_ID, "season_standings", participants, DATE_FROM, DATE_TO
);
expect(result[0].earliestGameTime).toBe(raceStart.toISOString());
});
it("leaves earliestGameTime null when eventStartsAt is not set", async () => {
const participants = [makeParticipant("p1", "Verstappen")];
mockDb.query.scoringEvents.findMany.mockResolvedValue([
makeScoringEvent({ id: "e1", name: "Australian GP", eventStartsAt: null }),
]);
const result = await getUpcomingEventsForDraftedParticipants(
SPORTS_SEASON_ID, "season_standings", participants, DATE_FROM, DATE_TO
);
expect(result[0].earliestGameTime).toBeNull();
});
it("works for qualifying_points pattern with eventStartsAt", async () => {
const majorStart = new Date("2025-04-10T12:00:00.000Z");
const participants = [makeParticipant("g1", "Scheffler")];
mockDb.query.scoringEvents.findMany.mockResolvedValue([
makeScoringEvent({ id: "e1", name: "The Masters", eventStartsAt: majorStart }),
]);
const result = await getUpcomingEventsForDraftedParticipants(
SPORTS_SEASON_ID, "qualifying_points", participants, DATE_FROM, DATE_TO
);
expect(result[0].earliestGameTime).toBe(majorStart.toISOString());
});
it("earliestGameTime from eventStartsAt is used as sort key over eventDate", async () => {
// Event on Apr 6, game starts at 14:00 UTC — both should be reflected in earliestGameTime
const raceStart = new Date("2025-04-06T14:00:00.000Z");
const participants = [makeParticipant("p1", "Hamilton")];
mockDb.query.scoringEvents.findMany.mockResolvedValue([
makeScoringEvent({ id: "e1", name: "Bahrain GP", eventDate: "2025-04-06", eventStartsAt: raceStart }),
]);
const result = await getUpcomingEventsForDraftedParticipants(
SPORTS_SEASON_ID, "season_standings", participants, DATE_FROM, DATE_TO
);
// earliestGameTime carries the full timestamp so the sort key has time precision
expect(result[0].earliestGameTime).toBe("2025-04-06T14:00:00.000Z");
expect(result[0].eventDate).toBe("2025-04-06");
});
});
// ── Bracket: misc ──────────────────────────────────────────────────────────
describe("getUpcomingEventsForDraftedParticipants — bracket misc", () => {
function makeSelectChain(rows: unknown[]) {
return {
from: vi.fn().mockReturnThis(),
innerJoin: vi.fn().mockReturnThis(),
leftJoin: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
orderBy: vi.fn().mockResolvedValue(rows),
};
}
function makeMaxGameChain(rows: unknown[]) {
return {
from: vi.fn().mockReturnThis(),
innerJoin: vi.fn().mockReturnThis(),
where: vi.fn().mockResolvedValue(rows),
};
}
beforeEach(() => {
vi.clearAllMocks();
// Default: no group stage matches.
mockDb.select.mockReturnValue(makeGroupStageSelectChain([]));
mockDb.query.seasonParticipants.findMany.mockResolvedValue([]);
});
it("excludes scored (isComplete) matches by passing the filter to the DB query", async () => {
// The DB mock returns rows only for matches the WHERE clause matches.
// When a match is complete (isComplete=true) the DB should return no rows.
// Simulate this by returning an empty result set.
// No second mockReturnValueOnce is needed: empty rows means eventIdsFromRows
// is empty so the max-game-number lookup is never called.
const chain = makeSelectChain([]);
mockDb.selectDistinct.mockReturnValueOnce(chain);
const result = await getUpcomingEventsForDraftedParticipants(
SPORTS_SEASON_ID, "playoff_bracket",
[makeParticipant("man-city", "Man City")],
DATE_FROM, DATE_TO
);
expect(result).toHaveLength(0);
// The WHERE clause includes eq(schema.playoffMatches.isComplete, false).
// We verify the query was built without error (accessing the field on the mock).
expect(chain.where).toHaveBeenCalled();
});
it("uses leftJoin so events without any games are still considered", async () => {
const chain = makeSelectChain([]);
mockDb.selectDistinct
.mockReturnValueOnce(chain)
.mockReturnValueOnce(makeMaxGameChain([]));
await getUpcomingEventsForDraftedParticipants(
SPORTS_SEASON_ID, "playoff_bracket",
[makeParticipant("man-city", "Man City")],
DATE_FROM, DATE_TO
);
expect(chain.leftJoin).toHaveBeenCalled();
});
});