brackt/app/models/__tests__/sports-season-tournament.test.ts
Chris Parsons 0de8cc0b95 Add sports season ↔ tournament linking with admin UI
Adds a sports_season_tournaments junction table and bidirectional
link/unlink UI on both the sports season and tournament admin pages.

- New junction table with unique index on (sports_season_id, tournament_id)
  and a separate index on tournament_id for reverse lookups
- New model with link/unlink/query functions and cross-sport validation
- Migration includes backfill from existing scoring_events tournament links
- Admin sports season page: Tournaments card with add/remove UI
- Admin tournament page: Linked Sports Seasons card with add/remove UI
- Inline success/error feedback, empty states, aria-labels on remove buttons
- cloneSportsSeason now copies tournament links
- Fixes darts simulator test timeout (15s for 128-player pre-bracket sim)
2026-05-02 12:41:22 -07:00

149 lines
4.2 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from "vitest";
vi.mock("~/database/context", () => ({
database: vi.fn(),
}));
vi.mock("~/database/schema", () => ({
sportsSeasonTournaments: {
sportsSeasonId: "sst.sports_season_id",
tournamentId: "sst.tournament_id",
},
}));
vi.mock("drizzle-orm", () => ({
eq: (col: unknown, val: unknown) => ({ type: "eq", col, val }),
and: (...args: unknown[]) => ({ type: "and", args }),
}));
vi.mock("~/models/tournament", () => ({
getTournamentById: vi.fn(),
}));
vi.mock("~/models/sports-season", () => ({
findSportsSeasonById: vi.fn(),
}));
import {
linkTournamentToSportsSeason,
unlinkTournamentFromSportsSeason,
} from "../sports-season-tournament";
import { database } from "~/database/context";
import { getTournamentById } from "~/models/tournament";
import { findSportsSeasonById } from "~/models/sports-season";
const SPORT_ID = "sport-1";
const SPORTS_SEASON_ID = "ss-1";
const TOURNAMENT_ID = "t-1";
const mockSportsSeason = {
id: SPORTS_SEASON_ID,
sportId: SPORT_ID,
name: "2026 Tennis",
year: 2026,
startDate: null,
endDate: null,
status: "active" as const,
scoringType: "majors" as const,
scoringPattern: "qualifying_points",
totalMajors: 4,
majorsCompleted: 0,
qualifyingPointsFinalized: false,
eloCalibrationExponent: null,
eloMinRating: null,
eloMaxRating: null,
simulationStatus: "idle" as const,
draftOn: "2026-01-01",
draftOff: "2026-12-31",
createdAt: new Date(),
updatedAt: new Date(),
};
const mockTournament = {
id: TOURNAMENT_ID,
sportId: SPORT_ID,
name: "Wimbledon",
year: 2026,
startsAt: null,
endsAt: null,
surface: "grass",
location: "London",
status: "scheduled" as const,
externalKey: null,
createdAt: new Date(),
updatedAt: new Date(),
};
function makeInsertDb(returnValue: object) {
return {
insert: vi.fn().mockReturnValue({
values: vi.fn().mockReturnValue({
returning: vi.fn().mockResolvedValue([returnValue]),
}),
}),
};
}
function makeDeleteDb() {
return {
delete: vi.fn().mockReturnValue({
where: vi.fn().mockResolvedValue(undefined),
}),
};
}
beforeEach(() => {
vi.clearAllMocks();
});
describe("linkTournamentToSportsSeason", () => {
it("links a tournament to a sports season when sports match", async () => {
vi.mocked(findSportsSeasonById).mockResolvedValue(mockSportsSeason as never);
vi.mocked(getTournamentById).mockResolvedValue(mockTournament as never);
const link = { id: "link-1", sportsSeasonId: SPORTS_SEASON_ID, tournamentId: TOURNAMENT_ID, createdAt: new Date() };
vi.mocked(database).mockReturnValue(makeInsertDb(link) as never);
const result = await linkTournamentToSportsSeason(SPORTS_SEASON_ID, TOURNAMENT_ID);
expect(result).toEqual(link);
});
it("throws when sports season not found", async () => {
vi.mocked(findSportsSeasonById).mockResolvedValue(undefined as never);
vi.mocked(getTournamentById).mockResolvedValue(mockTournament as never);
await expect(
linkTournamentToSportsSeason(SPORTS_SEASON_ID, TOURNAMENT_ID)
).rejects.toThrow("Sports season ss-1 not found");
});
it("throws when tournament not found", async () => {
vi.mocked(findSportsSeasonById).mockResolvedValue(mockSportsSeason as never);
vi.mocked(getTournamentById).mockResolvedValue(null as never);
await expect(
linkTournamentToSportsSeason(SPORTS_SEASON_ID, TOURNAMENT_ID)
).rejects.toThrow("Tournament t-1 not found");
});
it("throws when tournament and sports season belong to different sports", async () => {
vi.mocked(findSportsSeasonById).mockResolvedValue(mockSportsSeason as never);
vi.mocked(getTournamentById).mockResolvedValue({ ...mockTournament, sportId: "different-sport" } as never);
await expect(
linkTournamentToSportsSeason(SPORTS_SEASON_ID, TOURNAMENT_ID)
).rejects.toThrow("Tournament and sports season must belong to the same sport");
});
});
describe("unlinkTournamentFromSportsSeason", () => {
it("deletes the link", async () => {
vi.mocked(database).mockReturnValue(makeDeleteDb() as never);
await unlinkTournamentFromSportsSeason(SPORTS_SEASON_ID, TOURNAMENT_ID);
const db = vi.mocked(database).mock.results[0].value;
expect(db.delete).toHaveBeenCalled();
});
});