119 lines
4.5 KiB
TypeScript
119 lines
4.5 KiB
TypeScript
|
|
/**
|
||
|
|
* clear-bracket is the only path that can tear down a bracket, so the guard around it
|
||
|
|
* matters: it discards recorded results and the placements derived from them.
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||
|
|
import {
|
||
|
|
findPlayoffMatchesByEventId,
|
||
|
|
deletePlayoffMatchesByEventId,
|
||
|
|
} from "~/models/playoff-match";
|
||
|
|
import { deleteParticipantResultsBySportsSeasonId } from "~/models/participant-result";
|
||
|
|
import { recalculateAffectedLeagues } from "~/models/scoring-calculator";
|
||
|
|
import { getScoringEventById } from "~/models/scoring-event";
|
||
|
|
import { action } from "../admin.sports-seasons.$id.events.$eventId.bracket.server";
|
||
|
|
|
||
|
|
vi.mock("~/models/scoring-event", async (importOriginal) => ({
|
||
|
|
...(await importOriginal<object>()),
|
||
|
|
getScoringEventById: vi.fn(),
|
||
|
|
updateScoringEvent: vi.fn(),
|
||
|
|
isReadOnlySibling: vi.fn(() => false),
|
||
|
|
}));
|
||
|
|
vi.mock("~/models/playoff-match", async (importOriginal) => ({
|
||
|
|
...(await importOriginal<object>()),
|
||
|
|
findPlayoffMatchesByEventId: vi.fn(),
|
||
|
|
deletePlayoffMatchesByEventId: vi.fn(),
|
||
|
|
}));
|
||
|
|
vi.mock("~/models/participant-result", async (importOriginal) => ({
|
||
|
|
...(await importOriginal<object>()),
|
||
|
|
deleteParticipantResultsBySportsSeasonId: vi.fn(),
|
||
|
|
}));
|
||
|
|
vi.mock("~/models/scoring-calculator", async (importOriginal) => ({
|
||
|
|
...(await importOriginal<object>()),
|
||
|
|
recalculateAffectedLeagues: vi.fn(),
|
||
|
|
}));
|
||
|
|
|
||
|
|
const EVENT = { id: "event-1", sportsSeasonId: "season-1" };
|
||
|
|
const params = { id: "season-1", eventId: "event-1" };
|
||
|
|
|
||
|
|
function clearRequest(confirm?: string): Request {
|
||
|
|
const body = new FormData();
|
||
|
|
body.set("intent", "clear-bracket");
|
||
|
|
if (confirm !== undefined) body.set("confirm", confirm);
|
||
|
|
return new Request("http://localhost/clear", { method: "POST", body });
|
||
|
|
}
|
||
|
|
|
||
|
|
function match(isComplete: boolean) {
|
||
|
|
return { id: `m-${Math.random()}`, isComplete };
|
||
|
|
}
|
||
|
|
|
||
|
|
// The action's real signature carries React Router's generated types; the clear path
|
||
|
|
// only reads request and params.
|
||
|
|
const run = (request: Request) =>
|
||
|
|
(action as unknown as (args: { request: Request; params: typeof params }) => Promise<{
|
||
|
|
error?: string;
|
||
|
|
success?: string;
|
||
|
|
}>)({ request, params });
|
||
|
|
|
||
|
|
describe("clear-bracket", () => {
|
||
|
|
beforeEach(() => {
|
||
|
|
vi.clearAllMocks();
|
||
|
|
vi.mocked(getScoringEventById).mockResolvedValue(
|
||
|
|
EVENT as unknown as Awaited<ReturnType<typeof getScoringEventById>>
|
||
|
|
);
|
||
|
|
vi.mocked(deletePlayoffMatchesByEventId).mockResolvedValue(undefined);
|
||
|
|
vi.mocked(deleteParticipantResultsBySportsSeasonId).mockResolvedValue(undefined);
|
||
|
|
vi.mocked(recalculateAffectedLeagues).mockResolvedValue(
|
||
|
|
undefined as unknown as Awaited<ReturnType<typeof recalculateAffectedLeagues>>
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("deletes the matches and the placements derived from them", async () => {
|
||
|
|
vi.mocked(findPlayoffMatchesByEventId).mockResolvedValue([
|
||
|
|
match(false),
|
||
|
|
match(false),
|
||
|
|
] as unknown as Awaited<ReturnType<typeof findPlayoffMatchesByEventId>>);
|
||
|
|
|
||
|
|
const result = await run(clearRequest());
|
||
|
|
|
||
|
|
expect(result.success).toContain("2 match(es) removed");
|
||
|
|
expect(deletePlayoffMatchesByEventId).toHaveBeenCalledWith("event-1");
|
||
|
|
expect(deleteParticipantResultsBySportsSeasonId).toHaveBeenCalledWith("season-1");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("refuses to discard completed matches without confirmation", async () => {
|
||
|
|
vi.mocked(findPlayoffMatchesByEventId).mockResolvedValue([
|
||
|
|
match(true),
|
||
|
|
match(false),
|
||
|
|
] as unknown as Awaited<ReturnType<typeof findPlayoffMatchesByEventId>>);
|
||
|
|
|
||
|
|
const result = await run(clearRequest());
|
||
|
|
|
||
|
|
expect(result.error).toContain("1 completed match(es)");
|
||
|
|
expect(deletePlayoffMatchesByEventId).not.toHaveBeenCalled();
|
||
|
|
expect(deleteParticipantResultsBySportsSeasonId).not.toHaveBeenCalled();
|
||
|
|
});
|
||
|
|
|
||
|
|
it("discards completed matches once confirmed", async () => {
|
||
|
|
vi.mocked(findPlayoffMatchesByEventId).mockResolvedValue([
|
||
|
|
match(true),
|
||
|
|
] as unknown as Awaited<ReturnType<typeof findPlayoffMatchesByEventId>>);
|
||
|
|
|
||
|
|
const result = await run(clearRequest("true"));
|
||
|
|
|
||
|
|
expect(result.success).toBeDefined();
|
||
|
|
expect(deletePlayoffMatchesByEventId).toHaveBeenCalledWith("event-1");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("rejects an event with no bracket rather than reporting a no-op success", async () => {
|
||
|
|
vi.mocked(findPlayoffMatchesByEventId).mockResolvedValue(
|
||
|
|
[] as unknown as Awaited<ReturnType<typeof findPlayoffMatchesByEventId>>
|
||
|
|
);
|
||
|
|
|
||
|
|
const result = await run(clearRequest("true"));
|
||
|
|
|
||
|
|
expect(result.error).toContain("no bracket to clear");
|
||
|
|
expect(deletePlayoffMatchesByEventId).not.toHaveBeenCalled();
|
||
|
|
});
|
||
|
|
});
|