2026-03-17 11:16:36 -07:00
|
|
|
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<typeof vi.fn>).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<typeof vi.fn>).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"));
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-17 12:34:10 -07:00
|
|
|
it("uses plain numbers for all ranks in description", async () => {
|
2026-03-17 11:16:36 -07:00
|
|
|
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();
|
2026-03-17 12:34:10 -07:00
|
|
|
expect(desc).toContain("1. Alpha");
|
|
|
|
|
expect(desc).toContain("2. Beta");
|
|
|
|
|
expect(desc).toContain("3. Gamma");
|
2026-03-17 11:16:36 -07:00
|
|
|
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<typeof vi.fn>).mock.calls[0][1].body);
|
|
|
|
|
expect(body.embeds[0].fields).toBeUndefined();
|
|
|
|
|
});
|
|
|
|
|
});
|