import { describe, it, expect, vi, beforeEach } from "vitest"; import { sendDiscordWebhook, sendStandingsUpdateNotification } from "../discord"; const WEBHOOK_URL = "https://discord.com/api/webhooks/123/abc"; function mockFetch(status: number, body = "") { return vi.fn().mockResolvedValue({ ok: status >= 200 && status < 300, status, text: async () => body, }); } function getDescription(): string { const body = JSON.parse( (fetch as ReturnType).mock.calls[0][1].body ); return body.embeds[0].description as string; } describe("sendDiscordWebhook", () => { beforeEach(() => { vi.stubGlobal("fetch", mockFetch(204)); }); it("POSTs JSON to the webhook URL", async () => { await sendDiscordWebhook(WEBHOOK_URL, { content: "hello" }); expect(fetch).toHaveBeenCalledWith(WEBHOOK_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ content: "hello" }), }); }); it("throws on non-2xx response", async () => { vi.stubGlobal("fetch", mockFetch(400, "Bad Request")); await expect(sendDiscordWebhook(WEBHOOK_URL, {})).rejects.toThrow( "Discord webhook failed: 400" ); }); }); describe("sendStandingsUpdateNotification", () => { beforeEach(() => { vi.stubGlobal("fetch", mockFetch(204)); }); it("sends an embed with the season name as title", async () => { await sendStandingsUpdateNotification({ webhookUrl: WEBHOOK_URL, seasonName: "My League 2025", standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 150, rank: 1 }], previousStandings: new Map(), }); const body = JSON.parse((fetch as ReturnType).mock.calls[0][1].body); expect(body.embeds[0].title).toBe("📊 Standings Update — My League 2025"); }); it("shows point deltas as integers in description", async () => { await sendStandingsUpdateNotification({ webhookUrl: WEBHOOK_URL, seasonName: "Test League", standings: [ { teamId: "a", teamName: "Alpha", totalPoints: 150, rank: 1 }, { teamId: "b", teamName: "Beta", totalPoints: 100, rank: 2 }, ], previousStandings: new Map([ ["a", 125], ["b", 100], ]), }); const desc = getDescription(); expect(desc).toContain("Alpha"); expect(desc).toContain("+25 pts"); expect(desc).not.toContain("+25.0"); // Beta didn't change — no delta shown expect(desc).toMatch(/Beta.*100 pts(?!\s*\()/); }); it("includes sport name and event name in header when both provided", async () => { await sendStandingsUpdateNotification({ webhookUrl: WEBHOOK_URL, seasonName: "My League 2025", standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 100, rank: 1 }], previousStandings: new Map(), sportName: "UEFA Champions League", eventName: "Knockout Stage", }); const desc = getDescription(); expect(desc).toContain("UEFA Champions League — Knockout Stage"); }); it("shows only event name when no sport name provided", async () => { await sendStandingsUpdateNotification({ webhookUrl: WEBHOOK_URL, seasonName: "My League 2025", standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 100, rank: 1 }], previousStandings: new Map(), eventName: "Quarter Finals", }); const desc = getDescription(); expect(desc).toContain("Quarter Finals"); expect(desc).not.toContain("—\n"); }); it("omits header section when neither sport name nor event name provided", async () => { await sendStandingsUpdateNotification({ webhookUrl: WEBHOOK_URL, seasonName: "My League 2025", standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 100, rank: 1 }], previousStandings: new Map(), }); const desc = getDescription(); // Description should start directly with the standings section expect(desc).toContain("**Current Standings**"); expect(desc).not.toContain("**Scored Matches**"); }); it("lists scored matches above standings", async () => { await sendStandingsUpdateNotification({ webhookUrl: WEBHOOK_URL, seasonName: "My League 2025", standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 100, rank: 1 }], previousStandings: new Map(), scoredMatches: [ { winnerName: "Real Madrid", loserName: "Bayern Munich" }, { winnerName: "Arsenal", loserName: "PSG" }, ], }); const desc = getDescription(); expect(desc).toContain("**Scored Matches**"); expect(desc).toContain("• **Real Madrid** def. Bayern Munich"); expect(desc).toContain("• **Arsenal** def. PSG"); // Scored matches section should appear before standings expect(desc.indexOf("Scored Matches")).toBeLessThan(desc.indexOf("Current Standings")); }); it("shows username in parentheses in standings when provided", async () => { await sendStandingsUpdateNotification({ webhookUrl: WEBHOOK_URL, seasonName: "My League 2025", standings: [ { teamId: "a", teamName: "Alpha FC", username: "christhrowsrocks", totalPoints: 150, rank: 1 }, { teamId: "b", teamName: "Beta United", totalPoints: 100, rank: 2 }, ], previousStandings: new Map(), }); const desc = getDescription(); expect(desc).toContain("1. Alpha FC (christhrowsrocks) — 150 pts"); expect(desc).toContain("2. Beta United — 100 pts"); expect(desc).not.toContain("Beta United ("); }); it("shows usernames in parentheses in match results when both are drafted", async () => { await sendStandingsUpdateNotification({ webhookUrl: WEBHOOK_URL, seasonName: "My League 2025", standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 100, rank: 1 }], previousStandings: new Map(), scoredMatches: [ { winnerName: "Sporting", loserName: "Bodø/Glimt", winnerUsername: "christhrowsrocks", loserUsername: "apatel", }, ], }); const desc = getDescription(); expect(desc).toContain("• **Sporting (christhrowsrocks)** def. Bodø/Glimt (apatel)"); }); it("shows no parentheses in match results when neither side has a username", async () => { await sendStandingsUpdateNotification({ webhookUrl: WEBHOOK_URL, seasonName: "My League 2025", standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 100, rank: 1 }], previousStandings: new Map(), scoredMatches: [{ winnerName: "Real Madrid", loserName: "Bayern Munich" }], }); const desc = getDescription(); expect(desc).toContain("• **Real Madrid** def. Bayern Munich"); expect(desc).not.toContain("Real Madrid ("); expect(desc).not.toContain("Bayern Munich ("); }); it("omits username parenthesis only for the undrafted side in match results", async () => { await sendStandingsUpdateNotification({ webhookUrl: WEBHOOK_URL, seasonName: "My League 2025", standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 100, rank: 1 }], previousStandings: new Map(), scoredMatches: [ { winnerName: "Sporting", loserName: "Bodø/Glimt", winnerUsername: "christhrowsrocks", }, ], }); const desc = getDescription(); expect(desc).toContain("• **Sporting (christhrowsrocks)** def. Bodø/Glimt"); expect(desc).not.toContain("Bodø/Glimt ("); }); it("uses plain numbers for all ranks in description", async () => { await sendStandingsUpdateNotification({ webhookUrl: WEBHOOK_URL, seasonName: "My League 2025", standings: [ { teamId: "a", teamName: "Alpha", totalPoints: 150, rank: 1 }, { teamId: "b", teamName: "Beta", totalPoints: 125, rank: 2 }, { teamId: "c", teamName: "Gamma", totalPoints: 100, rank: 3 }, { teamId: "d", teamName: "Delta", totalPoints: 75, rank: 4 }, ], previousStandings: new Map(), }); const desc = getDescription(); expect(desc).toContain("1. Alpha"); expect(desc).toContain("2. Beta"); expect(desc).toContain("3. Gamma"); expect(desc).toContain("4. Delta"); }); it("does not show fields on the embed", async () => { await sendStandingsUpdateNotification({ webhookUrl: WEBHOOK_URL, seasonName: "My League 2025", standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 100, rank: 1 }], previousStandings: new Map(), }); const body = JSON.parse((fetch as ReturnType).mock.calls[0][1].body); expect(body.embeds[0].fields).toBeUndefined(); }); });