import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; vi.mock("~/database/context", () => ({ database: vi.fn(), })); vi.mock("~/lib/email.server", () => ({ sendEmail: vi.fn().mockResolvedValue({ error: null }), wrapInEmailTemplate: vi.fn((content: string) => `${content}`), emailParagraph: vi.fn((html: string) => `
${html}
`), emailButton: vi.fn((href: string, text: string) => `${text}`), escapeHtml: vi.fn((s: string) => s), })); import { sendOnTheClockEmail } from "../draft-email.server"; import { sendEmail } from "~/lib/email.server"; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- const SEASON_ID = "season-1"; const LEAGUE_ID = "league-1"; const TEAM_ID = "team-1"; const TEAM_ID_2 = "team-2"; const OWNER_ID = "owner-1"; /** 2-team, 4-round draft: picks 1,2,3,4,5,6,7,8 */ const DRAFT_SLOTS_2_TEAM = [ { teamId: TEAM_ID, draftOrder: 1 }, { teamId: TEAM_ID_2, draftOrder: 2 }, ]; function makeSeason(currentPickNumber: number, draftRounds = 4) { return { id: SEASON_ID, leagueId: LEAGUE_ID, currentPickNumber, draftRounds, }; } function makeOwner(emailNotificationsEnabled = true) { return { id: OWNER_ID, email: "owner@example.com", draftEmailNotificationsEnabled: emailNotificationsEnabled, }; } function makeMockDb(overrides: { season?: object | null; team?: object | null; autodraft?: object | null; league?: object | null; owner?: object | null; } = {}) { const season = "season" in overrides ? overrides.season : makeSeason(1); const team = "team" in overrides ? overrides.team : { id: TEAM_ID, name: "Team One", ownerId: OWNER_ID }; const autodraft = "autodraft" in overrides ? overrides.autodraft : null; const league = "league" in overrides ? overrides.league : { id: LEAGUE_ID, name: "Test League" }; const owner = "owner" in overrides ? overrides.owner : makeOwner(); return { query: { seasons: { findFirst: vi.fn().mockResolvedValue(season) }, teams: { findFirst: vi.fn().mockResolvedValue(team) }, autodraftSettings: { findFirst: vi.fn().mockResolvedValue(autodraft) }, leagues: { findFirst: vi.fn().mockResolvedValue(league) }, users: { findFirst: vi.fn().mockResolvedValue(owner) }, }, }; } const BASE_PARAMS = { seasonId: SEASON_ID, totalTeams: 2, draftSlots: DRAFT_SLOTS_2_TEAM, }; // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- beforeEach(() => { vi.clearAllMocks(); process.env.APP_URL = "https://test.brackt.com"; }); afterEach(() => { delete process.env.APP_URL; }); describe("sendOnTheClockEmail", () => { it("sends an email with the correct subject and recipient", async () => { const db = makeMockDb({ season: makeSeason(1) }); await sendOnTheClockEmail({ ...BASE_PARAMS, db: db as never }); expect(sendEmail).toHaveBeenCalledWith( expect.objectContaining({ to: "owner@example.com", subject: "You're on the clock in Test League", }) ); }); it("returns early when season is not found", async () => { const db = makeMockDb({ season: null }); await sendOnTheClockEmail({ ...BASE_PARAMS, db: db as never }); expect(sendEmail).not.toHaveBeenCalled(); }); it("returns early when currentPickNumber exceeds totalPicks", async () => { // 2 teams × 4 rounds = 8 total picks; pick 9 is past the end const db = makeMockDb({ season: makeSeason(9, 4) }); await sendOnTheClockEmail({ ...BASE_PARAMS, db: db as never }); expect(sendEmail).not.toHaveBeenCalled(); }); it("returns early when the team has no owner", async () => { const db = makeMockDb({ team: { id: TEAM_ID, name: "Team One", ownerId: null } }); await sendOnTheClockEmail({ ...BASE_PARAMS, db: db as never }); expect(sendEmail).not.toHaveBeenCalled(); }); it("returns early when owner has email notifications disabled", async () => { const db = makeMockDb({ owner: makeOwner(false) }); await sendOnTheClockEmail({ ...BASE_PARAMS, db: db as never }); expect(sendEmail).not.toHaveBeenCalled(); }); it("returns early when autodraft is enabled for the team", async () => { const db = makeMockDb({ autodraft: { isEnabled: true } }); await sendOnTheClockEmail({ ...BASE_PARAMS, db: db as never }); expect(sendEmail).not.toHaveBeenCalled(); }); it("returns early when league is not found", async () => { const db = makeMockDb({ league: null }); await sendOnTheClockEmail({ ...BASE_PARAMS, db: db as never }); expect(sendEmail).not.toHaveBeenCalled(); }); it("sends back-to-back subject when the same team picks consecutively (snake turnaround)", async () => { // In a 2-team snake draft, pick 2 (team 2 forward) is followed by pick 3 // (team 2 again on the reversal). Team 2 picks back-to-back. const db = makeMockDb({ season: makeSeason(2), team: { id: TEAM_ID_2, name: "Team Two", ownerId: OWNER_ID }, }); await sendOnTheClockEmail({ ...BASE_PARAMS, db: db as never }); expect(sendEmail).toHaveBeenCalledWith( expect.objectContaining({ subject: "You have back-to-back picks in Test League", }) ); }); it("sends a regular subject when the next pick goes to a different team", async () => { // Pick 1 is team 1; pick 2 is team 2 — not consecutive for team 1 const db = makeMockDb({ season: makeSeason(1) }); await sendOnTheClockEmail({ ...BASE_PARAMS, db: db as never }); expect(sendEmail).toHaveBeenCalledWith( expect.objectContaining({ subject: "You're on the clock in Test League", }) ); }); it("does not treat the final pick as consecutive even when it is the last slot", async () => { // Pick 8 is the last pick (2 teams × 4 rounds). No pick 9, so not consecutive. const db = makeMockDb({ season: makeSeason(8, 4) }); await sendOnTheClockEmail({ ...BASE_PARAMS, db: db as never }); expect(sendEmail).toHaveBeenCalledWith( expect.objectContaining({ subject: expect.stringContaining("on the clock"), }) ); }); it("includes a link to the draft room in the email", async () => { const db = makeMockDb({ season: makeSeason(1) }); await sendOnTheClockEmail({ ...BASE_PARAMS, db: db as never }); const call = vi.mocked(sendEmail).mock.calls[0][0]; expect(call.html).toContain( `https://test.brackt.com/leagues/${LEAGUE_ID}/draft/${SEASON_ID}` ); }); it("logs an error but does not throw when sendEmail fails", async () => { const db = makeMockDb({ season: makeSeason(1) }); vi.mocked(sendEmail).mockResolvedValueOnce({ error: new Error("network failure") }); await expect( sendOnTheClockEmail({ ...BASE_PARAMS, db: db as never }) ).resolves.toBeUndefined(); }); it("fetches team, autodraft settings, and league in parallel", async () => { const order: string[] = []; const db = makeMockDb({ season: makeSeason(1) }); db.query.teams.findFirst = vi.fn().mockImplementation(async () => { order.push("team"); return { id: TEAM_ID, name: "Team One", ownerId: OWNER_ID }; }); db.query.autodraftSettings.findFirst = vi.fn().mockImplementation(async () => { order.push("autodraft"); return null; }); db.query.leagues.findFirst = vi.fn().mockImplementation(async () => { order.push("league"); return { id: LEAGUE_ID, name: "Test League" }; }); await sendOnTheClockEmail({ ...BASE_PARAMS, db: db as never }); // All three should be called; order may vary since they run in parallel expect(order).toContain("team"); expect(order).toContain("autodraft"); expect(order).toContain("league"); expect(db.query.teams.findFirst).toHaveBeenCalledTimes(1); expect(db.query.autodraftSettings.findFirst).toHaveBeenCalledTimes(1); expect(db.query.leagues.findFirst).toHaveBeenCalledTimes(1); }); });