import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; vi.mock("~/database/context", () => ({ database: vi.fn(), })); vi.mock("~/services/discord", () => ({ sendPickAnnouncementNotification: vi.fn().mockResolvedValue(undefined), })); vi.mock("~/models/account", () => ({ findDiscordIdsByUserIds: vi.fn().mockResolvedValue(new Map()), })); import { notifyPickMadeOnDiscord } from "../draft-discord.server"; import { sendPickAnnouncementNotification } from "~/services/discord"; import { findDiscordIdsByUserIds } from "~/models/account"; // --------------------------------------------------------------------------- // Fixtures // --------------------------------------------------------------------------- const SEASON_ID = "season-1"; const LEAGUE_ID = "league-1"; const TEAM_ID = "team-1"; const NEXT_TEAM_ID = "team-2"; const NEXT_OWNER_ID = "owner-2"; const WEBHOOK_URL = "https://discord.com/api/webhooks/123/abc"; /** 2-team draft; snake: pick 1→team1, pick 2→team2, pick 3→team2, pick 4→team1 */ const DRAFT_SLOTS = [ { teamId: TEAM_ID, draftOrder: 1 }, { teamId: NEXT_TEAM_ID, draftOrder: 2 }, ]; const BASE_PARAMS = { seasonId: SEASON_ID, leagueId: LEAGUE_ID, pickedTeamName: "Alpha FC", participantName: "Erling Haaland", sportName: "Soccer", pickNumber: 1, nextPickNumber: 2, round: 1, rawPickInRound: 1, isDraftComplete: false, totalTeams: 2, draftSlots: DRAFT_SLOTS, }; function makeLeague(overrides: object = {}) { return { id: LEAGUE_ID, discordWebhookUrl: WEBHOOK_URL, discordPicksAnnouncementEnabled: true, ...overrides, }; } function makeTeam(id: string, ownerId: string | null = null, name = "Beta United") { return { id, name, ownerId }; } function makeOwner(id: string, discordPingEnabled = true) { return { id, discordPingEnabled }; } function makeMockDb(overrides: { league?: object | null; nextTeam?: object | null; owner?: object | null; } = {}) { const league = "league" in overrides ? overrides.league : makeLeague(); const nextTeam = "nextTeam" in overrides ? overrides.nextTeam : makeTeam(NEXT_TEAM_ID, NEXT_OWNER_ID); const owner = "owner" in overrides ? overrides.owner : makeOwner(NEXT_OWNER_ID); return { query: { leagues: { findFirst: vi.fn().mockResolvedValue(league) }, teams: { findFirst: vi.fn().mockResolvedValue(nextTeam) }, users: { findFirst: vi.fn().mockResolvedValue(owner) }, }, }; } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- beforeEach(() => { vi.clearAllMocks(); vi.mocked(sendPickAnnouncementNotification).mockResolvedValue(undefined); process.env.APP_URL = "https://test.brackt.com"; }); afterEach(() => { delete process.env.APP_URL; }); describe("notifyPickMadeOnDiscord", () => { it("returns early when league has no webhook URL", async () => { const db = makeMockDb({ league: makeLeague({ discordWebhookUrl: null }) }); await notifyPickMadeOnDiscord({ ...BASE_PARAMS, db: db as never }); expect(sendPickAnnouncementNotification).not.toHaveBeenCalled(); }); it("returns early when picks announcement toggle is disabled", async () => { const db = makeMockDb({ league: makeLeague({ discordPicksAnnouncementEnabled: false }) }); await notifyPickMadeOnDiscord({ ...BASE_PARAMS, db: db as never }); expect(sendPickAnnouncementNotification).not.toHaveBeenCalled(); }); it("returns early when league is not found", async () => { const db = makeMockDb({ league: null }); await notifyPickMadeOnDiscord({ ...BASE_PARAMS, db: db as never }); expect(sendPickAnnouncementNotification).not.toHaveBeenCalled(); }); it("sends announcement with correct pick details", async () => { const db = makeMockDb(); await notifyPickMadeOnDiscord({ ...BASE_PARAMS, db: db as never }); expect(sendPickAnnouncementNotification).toHaveBeenCalledWith( expect.objectContaining({ webhookUrl: WEBHOOK_URL, pickedTeamName: "Alpha FC", participantName: "Erling Haaland", sportName: "Soccer", pickNumber: 1, round: 1, pickInRound: 1, isDraftComplete: false, }) ); }); it("builds the draft URL from APP_URL env var", async () => { const db = makeMockDb(); await notifyPickMadeOnDiscord({ ...BASE_PARAMS, db: db as never }); expect(sendPickAnnouncementNotification).toHaveBeenCalledWith( expect.objectContaining({ draftUrl: `https://test.brackt.com/leagues/${LEAGUE_ID}/draft/${SEASON_ID}`, }) ); }); it("passes nextTeamName based on caller-supplied nextPickNumber", async () => { // pick 3 in a 2-team snake: round 2 (reversed) → draftOrder 2 → NEXT_TEAM_ID → "Beta United" const db = makeMockDb(); await notifyPickMadeOnDiscord({ ...BASE_PARAMS, nextPickNumber: 3, db: db as never }); expect(sendPickAnnouncementNotification).toHaveBeenCalledWith( expect.objectContaining({ nextTeamName: "Beta United" }) ); }); it("uses caller-supplied nextPickNumber instead of reading from DB", async () => { const db = makeMockDb(); await notifyPickMadeOnDiscord({ ...BASE_PARAMS, db: db as never }); expect((db.query as Record).seasons).toBeUndefined(); }); it("shows the immediate next drafter, not the post-autodraft-chain drafter", async () => { // Regression: manual pick #1 by alpha; beta immediately autodrafts (#2); DB advances to pick #3. // Discord for pick #1 must show beta ("On the clock: beta"), not gamma. // Uses a 3-team snake: pick 1→alpha, pick 2→beta, pick 3→gamma. const ALPHA_ID = "team-alpha"; const BETA_ID = "team-beta"; const GAMMA_ID = "team-gamma"; const threeTeamSlots = [ { teamId: ALPHA_ID, draftOrder: 1 }, { teamId: BETA_ID, draftOrder: 2 }, { teamId: GAMMA_ID, draftOrder: 3 }, ]; const betaTeam = { id: BETA_ID, name: "Beta Squad", ownerId: null }; const db = makeMockDb({ nextTeam: betaTeam }); await notifyPickMadeOnDiscord({ ...BASE_PARAMS, pickNumber: 1, nextPickNumber: 2, // immediately after alpha's pick — before beta's autodraft totalTeams: 3, draftSlots: threeTeamSlots, db: db as never, }); expect(sendPickAnnouncementNotification).toHaveBeenCalledWith( expect.objectContaining({ nextTeamName: "Beta Squad" }) ); }); it("passes nextOwnerDiscordId when owner has discordPingEnabled", async () => { vi.mocked(findDiscordIdsByUserIds).mockResolvedValue(new Map([[NEXT_OWNER_ID, "discord-999"]])); const db = makeMockDb(); await notifyPickMadeOnDiscord({ ...BASE_PARAMS, db: db as never }); expect(sendPickAnnouncementNotification).toHaveBeenCalledWith( expect.objectContaining({ nextOwnerDiscordId: "discord-999" }) ); }); it("omits nextOwnerDiscordId when owner has discordPingEnabled: false", async () => { const db = makeMockDb({ owner: makeOwner(NEXT_OWNER_ID, false) }); await notifyPickMadeOnDiscord({ ...BASE_PARAMS, db: db as never }); expect(findDiscordIdsByUserIds).not.toHaveBeenCalled(); expect(sendPickAnnouncementNotification).toHaveBeenCalledWith( expect.objectContaining({ nextOwnerDiscordId: undefined }) ); }); it("omits nextTeamName when next team has no owner", async () => { const db = makeMockDb({ nextTeam: makeTeam(NEXT_TEAM_ID, null) }); await notifyPickMadeOnDiscord({ ...BASE_PARAMS, db: db as never }); expect(sendPickAnnouncementNotification).toHaveBeenCalledWith( expect.objectContaining({ nextTeamName: "Beta United", nextOwnerDiscordId: undefined }) ); }); it("skips next-team lookup and passes isDraftComplete: true when draft is done", async () => { const db = makeMockDb(); await notifyPickMadeOnDiscord({ ...BASE_PARAMS, isDraftComplete: true, db: db as never }); expect(db.query.teams.findFirst).not.toHaveBeenCalled(); expect(sendPickAnnouncementNotification).toHaveBeenCalledWith( expect.objectContaining({ isDraftComplete: true, nextTeamName: undefined }) ); }); it("suppresses errors thrown by sendPickAnnouncementNotification — caller's .catch handles them", async () => { vi.mocked(sendPickAnnouncementNotification).mockRejectedValue(new Error("webhook down")); const db = makeMockDb(); // The function itself does NOT swallow errors; the caller chains .catch(). await expect( notifyPickMadeOnDiscord({ ...BASE_PARAMS, db: db as never }) ).rejects.toThrow("webhook down"); }); it("uses sequential (not snake-adjusted) pick number in Discord title for even rounds", async () => { // 13-team draft, pick #22: round 2 (even/reversed), rawPickInRound=9, snake-adjusted slot=5 // Discord should say "Round 2, Pick 9" not "Round 2, Pick 5" const db = makeMockDb(); await notifyPickMadeOnDiscord({ ...BASE_PARAMS, pickNumber: 22, round: 2, rawPickInRound: 9, totalTeams: 13, db: db as never, }); expect(sendPickAnnouncementNotification).toHaveBeenCalledWith( expect.objectContaining({ pickNumber: 22, round: 2, pickInRound: 9 }) ); }); });