248 lines
8 KiB
TypeScript
248 lines
8 KiB
TypeScript
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|||
|
|
|
|||
|
|
vi.mock("~/database/context", () => ({
|
|||
|
|
database: vi.fn(),
|
|||
|
|
}));
|
|||
|
|
|
|||
|
|
vi.mock("~/lib/email.server", () => ({
|
|||
|
|
sendEmail: vi.fn().mockResolvedValue({ error: null }),
|
|||
|
|
wrapInEmailTemplate: vi.fn((content: string) => `<html>${content}</html>`),
|
|||
|
|
emailParagraph: vi.fn((html: string) => `<p>${html}</p>`),
|
|||
|
|
emailButton: vi.fn((href: string, text: string) => `<a href="${href}">${text}</a>`),
|
|||
|
|
escapeHtml: vi.fn((s: string) => s),
|
|||
|
|
}));
|
|||
|
|
|
|||
|
|
import { sendOnTheClockEmail } from "../draft-email.server";
|
|||
|
|
import { sendEmail } from "~/lib/email.server";
|
|||
|
|
|
|||
|
|
// ---------------------------------------------------------------------------
|
|||
|
|
// Helpers
|
|||
|
|
// ---------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
const SEASON_ID = "season-1";
|
|||
|
|
const LEAGUE_ID = "league-1";
|
|||
|
|
const TEAM_ID = "team-1";
|
|||
|
|
const TEAM_ID_2 = "team-2";
|
|||
|
|
const OWNER_ID = "owner-1";
|
|||
|
|
|
|||
|
|
/** 2-team, 4-round draft: picks 1,2,3,4,5,6,7,8 */
|
|||
|
|
const DRAFT_SLOTS_2_TEAM = [
|
|||
|
|
{ teamId: TEAM_ID, draftOrder: 1 },
|
|||
|
|
{ teamId: TEAM_ID_2, draftOrder: 2 },
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
function makeSeason(currentPickNumber: number, draftRounds = 4) {
|
|||
|
|
return {
|
|||
|
|
id: SEASON_ID,
|
|||
|
|
leagueId: LEAGUE_ID,
|
|||
|
|
currentPickNumber,
|
|||
|
|
draftRounds,
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function makeOwner(emailNotificationsEnabled = true) {
|
|||
|
|
return {
|
|||
|
|
id: OWNER_ID,
|
|||
|
|
email: "owner@example.com",
|
|||
|
|
draftEmailNotificationsEnabled: emailNotificationsEnabled,
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function makeMockDb(overrides: {
|
|||
|
|
season?: object | null;
|
|||
|
|
team?: object | null;
|
|||
|
|
autodraft?: object | null;
|
|||
|
|
league?: object | null;
|
|||
|
|
owner?: object | null;
|
|||
|
|
} = {}) {
|
|||
|
|
const season = "season" in overrides ? overrides.season : makeSeason(1);
|
|||
|
|
const team = "team" in overrides ? overrides.team : { id: TEAM_ID, name: "Team One", ownerId: OWNER_ID };
|
|||
|
|
const autodraft = "autodraft" in overrides ? overrides.autodraft : null;
|
|||
|
|
const league = "league" in overrides ? overrides.league : { id: LEAGUE_ID, name: "Test League" };
|
|||
|
|
const owner = "owner" in overrides ? overrides.owner : makeOwner();
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
query: {
|
|||
|
|
seasons: { findFirst: vi.fn().mockResolvedValue(season) },
|
|||
|
|
teams: { findFirst: vi.fn().mockResolvedValue(team) },
|
|||
|
|
autodraftSettings: { findFirst: vi.fn().mockResolvedValue(autodraft) },
|
|||
|
|
leagues: { findFirst: vi.fn().mockResolvedValue(league) },
|
|||
|
|
users: { findFirst: vi.fn().mockResolvedValue(owner) },
|
|||
|
|
},
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const BASE_PARAMS = {
|
|||
|
|
seasonId: SEASON_ID,
|
|||
|
|
totalTeams: 2,
|
|||
|
|
draftSlots: DRAFT_SLOTS_2_TEAM,
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// ---------------------------------------------------------------------------
|
|||
|
|
// Tests
|
|||
|
|
// ---------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
beforeEach(() => {
|
|||
|
|
vi.clearAllMocks();
|
|||
|
|
process.env.APP_URL = "https://test.brackt.com";
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
afterEach(() => {
|
|||
|
|
delete process.env.APP_URL;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe("sendOnTheClockEmail", () => {
|
|||
|
|
it("sends an email with the correct subject and recipient", async () => {
|
|||
|
|
const db = makeMockDb({ season: makeSeason(1) });
|
|||
|
|
|
|||
|
|
await sendOnTheClockEmail({ ...BASE_PARAMS, db: db as never });
|
|||
|
|
|
|||
|
|
expect(sendEmail).toHaveBeenCalledWith(
|
|||
|
|
expect.objectContaining({
|
|||
|
|
to: "owner@example.com",
|
|||
|
|
subject: "You're on the clock in Test League",
|
|||
|
|
})
|
|||
|
|
);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it("returns early when season is not found", async () => {
|
|||
|
|
const db = makeMockDb({ season: null });
|
|||
|
|
|
|||
|
|
await sendOnTheClockEmail({ ...BASE_PARAMS, db: db as never });
|
|||
|
|
|
|||
|
|
expect(sendEmail).not.toHaveBeenCalled();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it("returns early when currentPickNumber exceeds totalPicks", async () => {
|
|||
|
|
// 2 teams × 4 rounds = 8 total picks; pick 9 is past the end
|
|||
|
|
const db = makeMockDb({ season: makeSeason(9, 4) });
|
|||
|
|
|
|||
|
|
await sendOnTheClockEmail({ ...BASE_PARAMS, db: db as never });
|
|||
|
|
|
|||
|
|
expect(sendEmail).not.toHaveBeenCalled();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it("returns early when the team has no owner", async () => {
|
|||
|
|
const db = makeMockDb({ team: { id: TEAM_ID, name: "Team One", ownerId: null } });
|
|||
|
|
|
|||
|
|
await sendOnTheClockEmail({ ...BASE_PARAMS, db: db as never });
|
|||
|
|
|
|||
|
|
expect(sendEmail).not.toHaveBeenCalled();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it("returns early when owner has email notifications disabled", async () => {
|
|||
|
|
const db = makeMockDb({ owner: makeOwner(false) });
|
|||
|
|
|
|||
|
|
await sendOnTheClockEmail({ ...BASE_PARAMS, db: db as never });
|
|||
|
|
|
|||
|
|
expect(sendEmail).not.toHaveBeenCalled();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it("returns early when autodraft is enabled for the team", async () => {
|
|||
|
|
const db = makeMockDb({ autodraft: { isEnabled: true } });
|
|||
|
|
|
|||
|
|
await sendOnTheClockEmail({ ...BASE_PARAMS, db: db as never });
|
|||
|
|
|
|||
|
|
expect(sendEmail).not.toHaveBeenCalled();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it("returns early when league is not found", async () => {
|
|||
|
|
const db = makeMockDb({ league: null });
|
|||
|
|
|
|||
|
|
await sendOnTheClockEmail({ ...BASE_PARAMS, db: db as never });
|
|||
|
|
|
|||
|
|
expect(sendEmail).not.toHaveBeenCalled();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it("sends back-to-back subject when the same team picks consecutively (snake turnaround)", async () => {
|
|||
|
|
// In a 2-team snake draft, pick 2 (team 2 forward) is followed by pick 3
|
|||
|
|
// (team 2 again on the reversal). Team 2 picks back-to-back.
|
|||
|
|
const db = makeMockDb({
|
|||
|
|
season: makeSeason(2),
|
|||
|
|
team: { id: TEAM_ID_2, name: "Team Two", ownerId: OWNER_ID },
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
await sendOnTheClockEmail({ ...BASE_PARAMS, db: db as never });
|
|||
|
|
|
|||
|
|
expect(sendEmail).toHaveBeenCalledWith(
|
|||
|
|
expect.objectContaining({
|
|||
|
|
subject: "You have back-to-back picks in Test League",
|
|||
|
|
})
|
|||
|
|
);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it("sends a regular subject when the next pick goes to a different team", async () => {
|
|||
|
|
// Pick 1 is team 1; pick 2 is team 2 — not consecutive for team 1
|
|||
|
|
const db = makeMockDb({ season: makeSeason(1) });
|
|||
|
|
|
|||
|
|
await sendOnTheClockEmail({ ...BASE_PARAMS, db: db as never });
|
|||
|
|
|
|||
|
|
expect(sendEmail).toHaveBeenCalledWith(
|
|||
|
|
expect.objectContaining({
|
|||
|
|
subject: "You're on the clock in Test League",
|
|||
|
|
})
|
|||
|
|
);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it("does not treat the final pick as consecutive even when it is the last slot", async () => {
|
|||
|
|
// Pick 8 is the last pick (2 teams × 4 rounds). No pick 9, so not consecutive.
|
|||
|
|
const db = makeMockDb({ season: makeSeason(8, 4) });
|
|||
|
|
|
|||
|
|
await sendOnTheClockEmail({ ...BASE_PARAMS, db: db as never });
|
|||
|
|
|
|||
|
|
expect(sendEmail).toHaveBeenCalledWith(
|
|||
|
|
expect.objectContaining({
|
|||
|
|
subject: expect.stringContaining("on the clock"),
|
|||
|
|
})
|
|||
|
|
);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it("includes a link to the draft room in the email", async () => {
|
|||
|
|
const db = makeMockDb({ season: makeSeason(1) });
|
|||
|
|
|
|||
|
|
await sendOnTheClockEmail({ ...BASE_PARAMS, db: db as never });
|
|||
|
|
|
|||
|
|
const call = vi.mocked(sendEmail).mock.calls[0][0];
|
|||
|
|
expect(call.html).toContain(
|
|||
|
|
`https://test.brackt.com/leagues/${LEAGUE_ID}/draft/${SEASON_ID}`
|
|||
|
|
);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it("logs an error but does not throw when sendEmail fails", async () => {
|
|||
|
|
const db = makeMockDb({ season: makeSeason(1) });
|
|||
|
|
vi.mocked(sendEmail).mockResolvedValueOnce({ error: new Error("network failure") });
|
|||
|
|
|
|||
|
|
await expect(
|
|||
|
|
sendOnTheClockEmail({ ...BASE_PARAMS, db: db as never })
|
|||
|
|
).resolves.toBeUndefined();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it("fetches team, autodraft settings, and league in parallel", async () => {
|
|||
|
|
const order: string[] = [];
|
|||
|
|
const db = makeMockDb({ season: makeSeason(1) });
|
|||
|
|
|
|||
|
|
db.query.teams.findFirst = vi.fn().mockImplementation(async () => {
|
|||
|
|
order.push("team");
|
|||
|
|
return { id: TEAM_ID, name: "Team One", ownerId: OWNER_ID };
|
|||
|
|
});
|
|||
|
|
db.query.autodraftSettings.findFirst = vi.fn().mockImplementation(async () => {
|
|||
|
|
order.push("autodraft");
|
|||
|
|
return null;
|
|||
|
|
});
|
|||
|
|
db.query.leagues.findFirst = vi.fn().mockImplementation(async () => {
|
|||
|
|
order.push("league");
|
|||
|
|
return { id: LEAGUE_ID, name: "Test League" };
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
await sendOnTheClockEmail({ ...BASE_PARAMS, db: db as never });
|
|||
|
|
|
|||
|
|
// All three should be called; order may vary since they run in parallel
|
|||
|
|
expect(order).toContain("team");
|
|||
|
|
expect(order).toContain("autodraft");
|
|||
|
|
expect(order).toContain("league");
|
|||
|
|
expect(db.query.teams.findFirst).toHaveBeenCalledTimes(1);
|
|||
|
|
expect(db.query.autodraftSettings.findFirst).toHaveBeenCalledTimes(1);
|
|||
|
|
expect(db.query.leagues.findFirst).toHaveBeenCalledTimes(1);
|
|||
|
|
});
|
|||
|
|
});
|