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 NEXT_OWNER_ID = "owner-2"; const WEBHOOK_URL = "https://discord.com/api/webhooks/123/abc"; 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, nextTeamName: "Beta United", nextTeamOwnerId: NEXT_OWNER_ID, }; function makeLeague(overrides: object = {}) { return { id: LEAGUE_ID, discordWebhookUrl: WEBHOOK_URL, discordPicksAnnouncementEnabled: true, ...overrides, }; } function makeOwner(id: string, discordPingEnabled = true) { return { id, discordPingEnabled }; } function makeMockDb(overrides: { league?: object | null; owner?: object | null; } = {}) { const league = "league" in overrides ? overrides.league : makeLeague(); const owner = "owner" in overrides ? overrides.owner : makeOwner(NEXT_OWNER_ID); return { query: { leagues: { findFirst: vi.fn().mockResolvedValue(league) }, 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("forwards caller-supplied nextTeamName to the notification", async () => { const db = makeMockDb(); await notifyPickMadeOnDiscord({ ...BASE_PARAMS, nextTeamName: "Beta United", db: db as never }); expect(sendPickAnnouncementNotification).toHaveBeenCalledWith( expect.objectContaining({ nextTeamName: "Beta United" }) ); }); it("does not query the teams table (team resolution is the caller's responsibility)", async () => { const db = makeMockDb(); await notifyPickMadeOnDiscord({ ...BASE_PARAMS, db: db as never }); expect((db.query as Record).teams).toBeUndefined(); }); it("forwards caller-supplied nextTeamName regardless of pick number", async () => { // Callers must pass the correct nextTeamName; this service just forwards it. const db = makeMockDb(); await notifyPickMadeOnDiscord({ ...BASE_PARAMS, nextTeamName: "Beta Squad", 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 nextOwnerDiscordId when nextTeamOwnerId is null", async () => { const db = makeMockDb(); await notifyPickMadeOnDiscord({ ...BASE_PARAMS, nextTeamOwnerId: null, db: db as never }); expect(findDiscordIdsByUserIds).not.toHaveBeenCalled(); expect(sendPickAnnouncementNotification).toHaveBeenCalledWith( expect.objectContaining({ nextTeamName: "Beta United", nextOwnerDiscordId: undefined }) ); }); it("skips owner 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.users.findFirst).not.toHaveBeenCalled(); expect(sendPickAnnouncementNotification).toHaveBeenCalledWith( expect.objectContaining({ isDraftComplete: true }) ); }); 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, pickInRound: 9, db: db as never, }); expect(sendPickAnnouncementNotification).toHaveBeenCalledWith( expect.objectContaining({ pickNumber: 22, round: 2, pickInRound: 9 }) ); }); });