Deleting an event from a season only affects that season's scoring event; the shared canonical tournament and the other linked seasons survive. The old flow never communicated this and offered no way to manage the relationship, so it felt like deleting an event might wipe the whole shared tournament. - deleteScoringEvent is now shared-tournament-aware: it auto-heals the primary window (promotes the earliest remaining window when the primary is removed) and optionally deletes the canonical tournament when the last linked window is removed. Returns a result describing what happened. - Add deleteTournament model helper. - Events page: delete confirmation now explains exactly what a delete does (removed from this season only vs. last window), with an opt-in checkbox to also remove the orphaned shared tournament. - Tournament page: add a per-window "Remove" control on the Linked Sports Seasons card to unlink a season, reusing the auto-heal path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CBzjCLVkVaQMj1MF3K54t2
198 lines
5.4 KiB
TypeScript
198 lines
5.4 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
|
|
vi.mock("~/database/context", () => ({
|
|
database: vi.fn(),
|
|
}));
|
|
|
|
import {
|
|
createTournament,
|
|
getTournamentById,
|
|
findTournamentsBySport,
|
|
findTournamentBySportNameYear,
|
|
upsertTournament,
|
|
updateTournamentStatus,
|
|
deleteTournament,
|
|
} from "../tournament";
|
|
import { database } from "~/database/context";
|
|
|
|
const SPORT_ID = "sport-1";
|
|
const TOURNAMENT_ID = "tournament-1";
|
|
|
|
const SAMPLE_TOURNAMENT = {
|
|
id: TOURNAMENT_ID,
|
|
sportId: SPORT_ID,
|
|
name: "Australian Open",
|
|
year: 2026,
|
|
startsAt: new Date("2026-01-15T00:00:00Z"),
|
|
endsAt: new Date("2026-01-28T00:00:00Z"),
|
|
surface: "hard",
|
|
location: "Melbourne",
|
|
status: "scheduled" as const,
|
|
externalKey: "ao-2026",
|
|
createdAt: new Date("2026-01-01T00:00:00Z"),
|
|
updatedAt: new Date("2026-01-01T00:00:00Z"),
|
|
};
|
|
|
|
function makeInsertDb(returnValue: object) {
|
|
return {
|
|
insert: vi.fn().mockReturnValue({
|
|
values: vi.fn().mockReturnValue({
|
|
returning: vi.fn().mockResolvedValue([returnValue]),
|
|
}),
|
|
}),
|
|
};
|
|
}
|
|
|
|
function makeSelectDb(rows: object[]) {
|
|
return {
|
|
select: vi.fn().mockReturnValue({
|
|
from: vi.fn().mockReturnValue({
|
|
where: vi.fn().mockReturnValue({
|
|
limit: vi.fn().mockResolvedValue(rows),
|
|
orderBy: vi.fn().mockResolvedValue(rows),
|
|
}),
|
|
}),
|
|
}),
|
|
};
|
|
}
|
|
|
|
function makeUpdateDb(returnValue: object) {
|
|
return {
|
|
update: vi.fn().mockReturnValue({
|
|
set: vi.fn().mockReturnValue({
|
|
where: vi.fn().mockReturnValue({
|
|
returning: vi.fn().mockResolvedValue([returnValue]),
|
|
}),
|
|
}),
|
|
}),
|
|
};
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
describe("createTournament", () => {
|
|
it("inserts a tournament and returns it", async () => {
|
|
vi.mocked(database).mockReturnValue(makeInsertDb(SAMPLE_TOURNAMENT) as never);
|
|
|
|
const result = await createTournament({
|
|
sportId: SPORT_ID,
|
|
name: "Australian Open",
|
|
year: 2026,
|
|
});
|
|
|
|
expect(result).toEqual(SAMPLE_TOURNAMENT);
|
|
});
|
|
});
|
|
|
|
describe("getTournamentById", () => {
|
|
it("returns the tournament when found", async () => {
|
|
vi.mocked(database).mockReturnValue(makeSelectDb([SAMPLE_TOURNAMENT]) as never);
|
|
|
|
const result = await getTournamentById(TOURNAMENT_ID);
|
|
|
|
expect(result).toEqual(SAMPLE_TOURNAMENT);
|
|
});
|
|
|
|
it("returns null when not found", async () => {
|
|
vi.mocked(database).mockReturnValue(makeSelectDb([]) as never);
|
|
|
|
const result = await getTournamentById("nonexistent");
|
|
|
|
expect(result).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("findTournamentsBySport", () => {
|
|
it("returns tournaments ordered by year and date", async () => {
|
|
const tournaments = [SAMPLE_TOURNAMENT];
|
|
vi.mocked(database).mockReturnValue(makeSelectDb(tournaments) as never);
|
|
|
|
const result = await findTournamentsBySport(SPORT_ID);
|
|
|
|
expect(result).toEqual(tournaments);
|
|
});
|
|
});
|
|
|
|
describe("findTournamentBySportNameYear", () => {
|
|
it("returns the tournament when found", async () => {
|
|
vi.mocked(database).mockReturnValue(makeSelectDb([SAMPLE_TOURNAMENT]) as never);
|
|
|
|
const result = await findTournamentBySportNameYear(SPORT_ID, "Australian Open", 2026);
|
|
|
|
expect(result).toEqual(SAMPLE_TOURNAMENT);
|
|
});
|
|
|
|
it("returns null when not found", async () => {
|
|
vi.mocked(database).mockReturnValue(makeSelectDb([]) as never);
|
|
|
|
const result = await findTournamentBySportNameYear(SPORT_ID, "Wimbledon", 2026);
|
|
|
|
expect(result).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("upsertTournament", () => {
|
|
it("returns existing tournament if found", async () => {
|
|
vi.mocked(database).mockReturnValue(makeSelectDb([SAMPLE_TOURNAMENT]) as never);
|
|
|
|
const result = await upsertTournament({
|
|
sportId: SPORT_ID,
|
|
name: "Australian Open",
|
|
year: 2026,
|
|
});
|
|
|
|
expect(result).toEqual(SAMPLE_TOURNAMENT);
|
|
});
|
|
|
|
it("creates new tournament if not found", async () => {
|
|
let callCount = 0;
|
|
vi.mocked(database).mockImplementation(() => {
|
|
callCount++;
|
|
return (callCount === 1 ? makeSelectDb([]) : makeInsertDb(SAMPLE_TOURNAMENT)) as never;
|
|
});
|
|
|
|
const result = await upsertTournament({
|
|
sportId: SPORT_ID,
|
|
name: "Australian Open",
|
|
year: 2026,
|
|
});
|
|
|
|
expect(result).toEqual(SAMPLE_TOURNAMENT);
|
|
});
|
|
});
|
|
|
|
describe("updateTournamentStatus", () => {
|
|
it("updates status and returns the tournament", async () => {
|
|
const updated = { ...SAMPLE_TOURNAMENT, status: "in_progress" as const };
|
|
vi.mocked(database).mockReturnValue(makeUpdateDb(updated) as never);
|
|
|
|
const result = await updateTournamentStatus(TOURNAMENT_ID, "in_progress");
|
|
|
|
expect(result.status).toBe("in_progress");
|
|
});
|
|
});
|
|
|
|
describe("deleteTournament", () => {
|
|
it("deletes the tournament row by id", async () => {
|
|
const where = vi.fn().mockResolvedValue(undefined);
|
|
const db = { delete: vi.fn().mockReturnValue({ where }) };
|
|
vi.mocked(database).mockReturnValue(db as never);
|
|
|
|
await deleteTournament(TOURNAMENT_ID);
|
|
|
|
expect(db.delete).toHaveBeenCalledTimes(1);
|
|
expect(where).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("uses a provided db when passed (transaction)", async () => {
|
|
const where = vi.fn().mockResolvedValue(undefined);
|
|
const providedDb = { delete: vi.fn().mockReturnValue({ where }) };
|
|
|
|
await deleteTournament(TOURNAMENT_ID, providedDb as never);
|
|
|
|
expect(providedDb.delete).toHaveBeenCalledTimes(1);
|
|
expect(vi.mocked(database)).not.toHaveBeenCalled();
|
|
});
|
|
});
|