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, round: 1, pickInRound: 1, isDraftComplete: false, totalTeams: 2, draftSlots: DRAFT_SLOTS, }; function makeLeague(overrides: object = {}) { return { id: LEAGUE_ID, discordWebhookUrl: WEBHOOK_URL, discordPicksAnnouncementEnabled: true, ...overrides, }; } function makeSeason(currentPickNumber = 2) { return { id: SEASON_ID, leagueId: LEAGUE_ID, currentPickNumber }; } 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; season?: object | null; nextTeam?: object | null; owner?: object | null; } = {}) { const league = "league" in overrides ? overrides.league : makeLeague(); const season = "season" in overrides ? overrides.season : makeSeason(); 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) }, seasons: { findFirst: vi.fn().mockResolvedValue(season) }, teams: { findFirst: vi.fn().mockResolvedValue(nextTeam) }, users: { findFirst: vi.fn().mockResolvedValue(owner) }, }, }; } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- beforeEach(() => { vi.clearAllMocks(); 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 when fresh season resolves the next slot", async () => { const db = makeMockDb({ season: makeSeason(2) }); // currentPickNumber=2 → pickInRound=2 → NEXT_TEAM_ID await notifyPickMadeOnDiscord({ ...BASE_PARAMS, db: db as never }); expect(sendPickAnnouncementNotification).toHaveBeenCalledWith( expect.objectContaining({ nextTeamName: "Beta United" }) ); }); it("reads next team from fresh DB pick number, not caller-supplied snapshot", async () => { // Simulate autodraft chain advancing 2 picks: currentPickNumber=3 on a 2-team snake // draft means pickInRound=2 again (round 2 reversed) → NEXT_TEAM_ID const db = makeMockDb({ season: makeSeason(3) }); await notifyPickMadeOnDiscord({ ...BASE_PARAMS, db: db as never }); expect(db.query.seasons.findFirst).toHaveBeenCalledTimes(1); }); 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.seasons.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"); }); });