brackt/app/routes/__tests__/admin.tournaments.$id.test.tsx
Chris Parsons 3a4fe41eb5
feat(admin): canonical tournament list + result entry routes
New routes /admin/tournaments and /admin/tournaments/:id. The detail
page lets admins enter tournament results once via paste-and-parse,
then calls syncTournamentResults to fan out to every linked window.

Phase 3 Task 4 of canonical tournament layer migration.
2026-05-01 22:03:39 +00:00

182 lines
5.4 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from "vitest";
vi.mock("~/lib/auth.server", () => ({
auth: { api: { getSession: vi.fn() } },
}));
vi.mock("~/models/user", () => ({
isUserAdmin: vi.fn(),
}));
vi.mock("~/models/tournament", () => ({
getTournamentById: vi.fn(),
updateTournamentStatus: vi.fn(),
}));
vi.mock("~/models/tournament-result", () => ({
getTournamentResults: vi.fn(),
upsertTournamentResult: vi.fn(),
}));
vi.mock("~/models/participant", () => ({
findCanonicalParticipantsBySport: vi.fn(),
}));
vi.mock("~/services/sync-tournament-results", () => ({
syncTournamentResults: vi.fn(),
}));
vi.mock("~/lib/logger", () => ({
logger: { error: vi.fn(), info: vi.fn(), warn: vi.fn() },
}));
import { action } from "~/routes/admin.tournaments.$id";
import { auth } from "~/lib/auth.server";
import { isUserAdmin } from "~/models/user";
import {
getTournamentById,
updateTournamentStatus,
} from "~/models/tournament";
import { upsertTournamentResult } from "~/models/tournament-result";
import { syncTournamentResults } from "~/services/sync-tournament-results";
const TOURNAMENT_ID = "tournament-1";
const ADMIN_USER_ID = "admin-1";
const NON_ADMIN_USER_ID = "user-1";
function makeRequest(body: Record<string, string>) {
const formData = new FormData();
for (const [k, v] of Object.entries(body)) {
formData.append(k, v);
}
return new Request(`http://localhost/admin/tournaments/${TOURNAMENT_ID}`, {
method: "POST",
body: formData,
});
}
function makeActionArgs(request: Request) {
return {
request,
params: { id: TOURNAMENT_ID },
context: {} as never,
} as unknown as Parameters<typeof action>[0];
}
describe("admin.tournaments.$id action", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("batch-upsert-results: upserts each row, marks tournament completed, and fans out", async () => {
vi.mocked(auth.api.getSession).mockResolvedValue({
user: { id: ADMIN_USER_ID },
} as never);
vi.mocked(isUserAdmin).mockResolvedValue(true);
vi.mocked(getTournamentById).mockResolvedValue({
id: TOURNAMENT_ID,
sportId: "sport-1",
name: "US Open",
year: 2026,
status: "scheduled",
} as never);
vi.mocked(upsertTournamentResult).mockResolvedValue({} as never);
vi.mocked(updateTournamentStatus).mockResolvedValue({} as never);
const syncReport = {
tournamentId: TOURNAMENT_ID,
windowsSynced: 2,
windowsFailed: 0,
failures: [],
};
vi.mocked(syncTournamentResults).mockResolvedValue(syncReport);
const results = [
{ participantId: "p-1", placement: 1 },
{ participantId: "p-2", placement: 2 },
{ participantId: "p-3", placement: 3 },
];
const request = makeRequest({
intent: "batch-upsert-results",
results: JSON.stringify(results),
});
const response = await action(makeActionArgs(request));
expect(upsertTournamentResult).toHaveBeenCalledTimes(3);
expect(upsertTournamentResult).toHaveBeenCalledWith({
tournamentId: TOURNAMENT_ID,
participantId: "p-1",
placement: 1,
});
expect(updateTournamentStatus).toHaveBeenCalledWith(
TOURNAMENT_ID,
"completed"
);
expect(syncTournamentResults).toHaveBeenCalledWith(TOURNAMENT_ID);
expect(response).toMatchObject({
success: true,
syncReport,
});
});
it("does not re-update status when tournament is already completed", async () => {
vi.mocked(auth.api.getSession).mockResolvedValue({
user: { id: ADMIN_USER_ID },
} as never);
vi.mocked(isUserAdmin).mockResolvedValue(true);
vi.mocked(getTournamentById).mockResolvedValue({
id: TOURNAMENT_ID,
sportId: "sport-1",
name: "US Open",
year: 2026,
status: "completed",
} as never);
vi.mocked(upsertTournamentResult).mockResolvedValue({} as never);
vi.mocked(syncTournamentResults).mockResolvedValue({
tournamentId: TOURNAMENT_ID,
windowsSynced: 1,
windowsFailed: 0,
failures: [],
});
const request = makeRequest({
intent: "batch-upsert-results",
results: JSON.stringify([{ participantId: "p-1", placement: 1 }]),
});
await action(makeActionArgs(request));
expect(updateTournamentStatus).not.toHaveBeenCalled();
expect(syncTournamentResults).toHaveBeenCalledWith(TOURNAMENT_ID);
});
it("non-admin user is forbidden (403)", async () => {
vi.mocked(auth.api.getSession).mockResolvedValue({
user: { id: NON_ADMIN_USER_ID },
} as never);
vi.mocked(isUserAdmin).mockResolvedValue(false);
const request = makeRequest({
intent: "batch-upsert-results",
results: JSON.stringify([{ participantId: "p-1", placement: 1 }]),
});
await expect(action(makeActionArgs(request))).rejects.toMatchObject({
status: 403,
});
expect(upsertTournamentResult).not.toHaveBeenCalled();
expect(syncTournamentResults).not.toHaveBeenCalled();
});
it("missing tournament returns 404", async () => {
vi.mocked(auth.api.getSession).mockResolvedValue({
user: { id: ADMIN_USER_ID },
} as never);
vi.mocked(isUserAdmin).mockResolvedValue(true);
vi.mocked(getTournamentById).mockResolvedValue(null);
const request = makeRequest({
intent: "batch-upsert-results",
results: JSON.stringify([{ participantId: "p-1", placement: 1 }]),
});
await expect(action(makeActionArgs(request))).rejects.toMatchObject({
status: 404,
});
});
});