brackt/app/models/__tests__/scoring-event-dashboard.test.ts
Claude 32ae32ab6a
Fix getEventsForDates to include bracket events by game scheduledAt
The previous implementation only matched on scoringEvents.eventDate,
which is often null for bracket events where individual game times are
set on playoffMatchGames.scheduledAt instead.

Switched to a two-step query mirroring getUpcomingEventsForDraftedParticipants:
1. selectDistinct event IDs via left joins through playoffMatches →
   playoffMatchGames, matching where eventDate IN dates OR any game's
   scheduledAt falls within the date range's timestamp bounds
2. findMany on the matched IDs with sportsSeason + sport relations

Updated tests to cover the two-step flow, leftJoin usage, and the
scheduledAt timestamp bounds appearing in the where clause.

https://claude.ai/code/session_014zKK15o5RJfTaodLvnZpF5
2026-03-20 01:57:53 +00:00

259 lines
8.5 KiB
TypeScript

/**
* Tests for getEventsForDates — the admin dashboard "Games to Score" query.
*/
import { describe, it, expect, vi, beforeEach } from "vitest";
// ── DB mock ──────────────────────────────────────────────────────────────────
// selectDistinct returns a fluent chain; findMany is used in step 2.
const mockFindMany = vi.fn();
const mockSelectChainBase = {
from: vi.fn(),
leftJoin: vi.fn(),
where: vi.fn(),
};
function makeSelectChain(rows: { id: string }[]) {
const chain = {
from: vi.fn().mockReturnThis(),
leftJoin: vi.fn().mockReturnThis(),
where: vi.fn().mockResolvedValue(rows),
};
return chain;
}
const mockSelectDistinct = vi.fn();
const mockDb = {
query: {
scoringEvents: { findMany: mockFindMany },
},
selectDistinct: mockSelectDistinct,
};
vi.mock("~/database/context", () => ({
database: () => mockDb,
}));
vi.mock("~/database/schema", () => ({
scoringEvents: {
id: "se.id",
eventDate: "se.event_date",
eventType: "se.event_type",
eventStartsAt: "se.event_starts_at",
createdAt: "se.created_at",
},
playoffMatches: {
scoringEventId: "pm.scoring_event_id",
id: "pm.id",
},
playoffMatchGames: {
playoffMatchId: "pmg.playoff_match_id",
scheduledAt: "pmg.scheduled_at",
},
}));
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 { getEventsForDates } from "../scoring-event";
// ── Helpers ──────────────────────────────────────────────────────────────────
function makeEventRow(overrides: Partial<{
id: string;
name: string;
eventDate: string | null;
eventStartsAt: Date | null;
eventType: string;
isComplete: boolean;
completedAt: Date | null;
sportsSeasonId: string;
sportsSeason: { name: string; sport: { name: string; slug: string | null } };
}> = {}) {
return {
id: overrides.id ?? "event-1",
name: overrides.name ?? "Test Event",
eventDate: overrides.eventDate ?? "2026-03-20",
eventStartsAt: overrides.eventStartsAt ?? null,
eventType: overrides.eventType ?? "playoff_game",
isComplete: overrides.isComplete ?? false,
completedAt: overrides.completedAt ?? null,
sportsSeasonId: overrides.sportsSeasonId ?? "season-1",
sportsSeason: overrides.sportsSeason ?? {
name: "2026 NFL Playoffs",
sport: { name: "NFL", slug: "nfl" },
},
};
}
// ── Tests ─────────────────────────────────────────────────────────────────────
describe("getEventsForDates", () => {
beforeEach(() => {
vi.clearAllMocks();
mockSelectDistinct.mockReturnValue(makeSelectChain([]));
});
it("returns empty array and skips DB when no dates given", async () => {
const result = await getEventsForDates([]);
expect(result).toEqual([]);
expect(mockSelectDistinct).not.toHaveBeenCalled();
expect(mockFindMany).not.toHaveBeenCalled();
});
it("returns empty array when selectDistinct finds no matching event IDs", async () => {
mockSelectDistinct.mockReturnValue(makeSelectChain([]));
const result = await getEventsForDates(["2026-03-20"]);
expect(result).toEqual([]);
expect(mockFindMany).not.toHaveBeenCalled();
});
it("maps database rows to DashboardScoringEvent shape", async () => {
mockSelectDistinct.mockReturnValue(makeSelectChain([{ id: "e1" }]));
mockFindMany.mockResolvedValueOnce([
makeEventRow({
id: "e1",
name: "Conference Finals",
eventDate: "2026-03-20",
eventType: "playoff_game",
isComplete: false,
sportsSeasonId: "ss-1",
sportsSeason: {
name: "2026 NBA Playoffs",
sport: { name: "NBA", slug: "nba" },
},
}),
]);
const result = await getEventsForDates(["2026-03-20"]);
expect(result).toHaveLength(1);
expect(result[0]).toMatchObject({
id: "e1",
name: "Conference Finals",
eventDate: "2026-03-20",
eventType: "playoff_game",
isComplete: false,
sportsSeasonId: "ss-1",
sportsSeasonName: "2026 NBA Playoffs",
sportName: "NBA",
sportSlug: "nba",
});
});
it("returns events for multiple dates", async () => {
mockSelectDistinct.mockReturnValue(
makeSelectChain([{ id: "e1" }, { id: "e2" }])
);
mockFindMany.mockResolvedValueOnce([
makeEventRow({ id: "e1", eventDate: "2026-03-20" }),
makeEventRow({ id: "e2", eventDate: "2026-03-21" }),
]);
const result = await getEventsForDates(["2026-03-20", "2026-03-21"]);
expect(result).toHaveLength(2);
expect(result.map((e) => e.id)).toEqual(["e1", "e2"]);
});
it("includes completed events with isComplete: true", async () => {
mockSelectDistinct.mockReturnValue(makeSelectChain([{ id: "e-done" }]));
mockFindMany.mockResolvedValueOnce([
makeEventRow({
id: "e-done",
isComplete: true,
completedAt: new Date("2026-03-20T18:00:00Z"),
}),
]);
const result = await getEventsForDates(["2026-03-20"]);
expect(result[0].isComplete).toBe(true);
expect(result[0].completedAt).toBeInstanceOf(Date);
});
it("includes eventStartsAt when present", async () => {
const starts = new Date("2026-03-20T19:30:00Z");
mockSelectDistinct.mockReturnValue(makeSelectChain([{ id: "e1" }]));
mockFindMany.mockResolvedValueOnce([
makeEventRow({ id: "e1", eventStartsAt: starts }),
]);
const result = await getEventsForDates(["2026-03-20"]);
expect(result[0].eventStartsAt).toEqual(starts);
});
it("uses two DB calls: selectDistinct then findMany", async () => {
mockSelectDistinct.mockReturnValue(makeSelectChain([{ id: "e1" }]));
mockFindMany.mockResolvedValueOnce([makeEventRow({ id: "e1" })]);
await getEventsForDates(["2026-03-20"]);
expect(mockSelectDistinct).toHaveBeenCalledOnce();
expect(mockFindMany).toHaveBeenCalledOnce();
});
it("passes event IDs from step 1 into findMany in step 2", async () => {
mockSelectDistinct.mockReturnValue(
makeSelectChain([{ id: "abc" }, { id: "def" }])
);
mockFindMany.mockResolvedValueOnce([]);
await getEventsForDates(["2026-03-20"]);
const findManyArgs = mockFindMany.mock.calls[0][0];
const whereStr = JSON.stringify(findManyArgs.where);
expect(whereStr).toContain("abc");
expect(whereStr).toContain("def");
});
it("uses leftJoin so bracket events without playoffMatchGames still qualify via eventDate", async () => {
const chain = makeSelectChain([]);
mockSelectDistinct.mockReturnValue(chain);
await getEventsForDates(["2026-03-20"]);
expect(chain.leftJoin).toHaveBeenCalled();
});
it("includes both eventDate and scheduledAt timestamp bounds in the where clause", async () => {
const chain = makeSelectChain([]);
mockSelectDistinct.mockReturnValue(chain);
await getEventsForDates(["2026-03-20", "2026-03-21"]);
// The where clause is the first (and only) arg to chain.where
const whereArg = JSON.stringify((chain.where as ReturnType<typeof vi.fn>).mock.calls[0][0]);
// eventDate inArray check
expect(whereArg).toContain("2026-03-20");
expect(whereArg).toContain("2026-03-21");
// timestamp bounds for scheduledAt
expect(whereArg).toContain("T00:00:00.000Z");
expect(whereArg).toContain("T23:59:59.999Z");
});
it("excludes schedule_event type via the where clause", async () => {
const chain = makeSelectChain([]);
mockSelectDistinct.mockReturnValue(chain);
await getEventsForDates(["2026-03-20"]);
const whereArg = JSON.stringify((chain.where as ReturnType<typeof vi.fn>).mock.calls[0][0]);
expect(whereArg).toContain("playoff_game");
expect(whereArg).toContain("major_tournament");
expect(whereArg).toContain("final_standings");
expect(whereArg).not.toContain("schedule_event");
});
});