Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
315 changes: 315 additions & 0 deletions src/app/api/leagues/[leagueId]/calendar/route.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,315 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { NextRequest } from "next/server";

const mocks = vi.hoisted(() => ({
prisma: {
league: {
findUnique: vi.fn(),
},
user: {
findUnique: vi.fn(),
},
leagueMembership: {
findUnique: vi.fn(),
},
member: {
findUnique: vi.fn(),
},
series: {
findMany: vi.fn(),
},
},
getIracingCustIdFromJwt: vi.fn(),
}));

vi.mock("@/lib/prisma", () => ({
prisma: mocks.prisma,
}));

vi.mock("@/lib/auth/iracing", () => ({
getIracingCustIdFromJwt: mocks.getIracingCustIdFromJwt,
}));

import { GET } from "./route";

function buildRequest(accessToken?: string): NextRequest {
return {
cookies: {
get: vi.fn((name: string) =>
name === "irh_access_token" && accessToken
? { value: accessToken }
: undefined,
),
},
} as unknown as NextRequest;
}

function mockLeague() {
mocks.prisma.league.findUnique.mockResolvedValue({
id: "league-1",
iracingLeagueId: 101,
});
}

const FUTURE_DATE = new Date("2099-06-01T18:00:00.000Z");

function makeSchedule(overrides = {}) {
return {
id: "schedule-1",
eventDate: FUTURE_DATE,
raceName: "Round 1",
isOffWeek: false,
pointsCount: true,
canDrop: false,
registrationEnabled: true,
trackName: null,
trackId: null,
raceLength: null,
raceOrder: 1,
iracingSessionId: null,
importedSession: null,
registrations: [],
...overrides,
};
}

function mockSeriesWithSchedule(overrides = {}) {
mocks.prisma.series.findMany.mockResolvedValue([
{
id: "series-1",
name: "Pro Series",
description: null,
isActive: true,
seasons: [
{
id: "season-1",
seasonName: "Season 1",
description: null,
isActive: true,
numDrops: 0,
iracingSeasonId: null,
schedules: [makeSchedule(overrides)],
},
],
},
]);
}

describe("GET /api/leagues/[leagueId]/calendar", () => {
beforeEach(() => {
vi.clearAllMocks();
});

it("returns 404 when league is not found", async () => {
mocks.prisma.league.findUnique.mockResolvedValue(null);

const response = await GET(buildRequest(), {
params: Promise.resolve({ leagueId: "unknown" }),
});

expect(response.status).toBe(404);
await expect(response.json()).resolves.toEqual({
error: "league_not_found",
});
});

it("returns 200 with isAdmin=false and isRegisteredByMe=false for unauthenticated request", async () => {
mockLeague();
mockSeriesWithSchedule();

const response = await GET(buildRequest(/* no token */), {
params: Promise.resolve({ leagueId: "101" }),
});

expect(response.status).toBe(200);

const payload = (await response.json()) as {
isAdmin: boolean;
series: Array<{
seasons: Array<{
schedules: Array<{
isRegisteredByMe: boolean;
registeredMembers: unknown[];
}>;
}>;
}>;
};

expect(payload.isAdmin).toBe(false);
const schedule = payload.series[0].seasons[0].schedules[0];
expect(schedule.isRegisteredByMe).toBe(false);
expect(schedule.registeredMembers).toEqual([]);
});

it("returns isAdmin=false and isRegisteredByMe=false when auth token is invalid", async () => {
mockLeague();
mockSeriesWithSchedule();
mocks.getIracingCustIdFromJwt.mockImplementation(() => {
throw new Error("invalid token");
});

const response = await GET(buildRequest("bad-token"), {
params: Promise.resolve({ leagueId: "league-1" }),
});

expect(response.status).toBe(200);

const payload = (await response.json()) as {
isAdmin: boolean;
};

expect(payload.isAdmin).toBe(false);
});

it("returns isAdmin=true for league admin and shows registered members", async () => {
mockLeague();

const member = {
id: "member-1",
custId: 9001,
displayName: "Driver One",
carNumber: "42",
nickName: null,
};

mocks.prisma.series.findMany.mockResolvedValue([
{
id: "series-1",
name: "Pro Series",
description: null,
isActive: true,
seasons: [
{
id: "season-1",
seasonName: "Season 1",
description: null,
isActive: true,
numDrops: 0,
iracingSeasonId: null,
schedules: [
makeSchedule({
registrations: [
{
id: "reg-1",
createdAt: FUTURE_DATE,
memberId: "member-1",
member,
},
],
}),
],
},
],
},
]);

mocks.getIracingCustIdFromJwt.mockReturnValue(9001);
mocks.prisma.user.findUnique.mockResolvedValue({
id: "user-1",
iracingCustId: 9001,
});
mocks.prisma.leagueMembership.findUnique.mockResolvedValue({
owner: false,
admin: true,
});
mocks.prisma.member.findUnique.mockResolvedValue({
id: "member-1",
});

const response = await GET(buildRequest("valid-token"), {
params: Promise.resolve({ leagueId: "league-1" }),
});

expect(response.status).toBe(200);

const payload = (await response.json()) as {
isAdmin: boolean;
series: Array<{
seasons: Array<{
schedules: Array<{
isRegisteredByMe: boolean;
registeredMembers: Array<{ id: string }>;
}>;
}>;
}>;
};

expect(payload.isAdmin).toBe(true);
const schedule = payload.series[0].seasons[0].schedules[0];
expect(schedule.isRegisteredByMe).toBe(true);
expect(schedule.registeredMembers).toHaveLength(1);
expect(schedule.registeredMembers[0].id).toBe("reg-1");
});

it("hides registeredMembers for non-admin authenticated member", async () => {
mockLeague();

mocks.prisma.series.findMany.mockResolvedValue([
{
id: "series-1",
name: "Pro Series",
description: null,
isActive: true,
seasons: [
{
id: "season-1",
seasonName: "Season 1",
description: null,
isActive: true,
numDrops: 0,
iracingSeasonId: null,
schedules: [
makeSchedule({
registrations: [
{
id: "reg-1",
createdAt: FUTURE_DATE,
memberId: "member-1",
member: {
id: "member-1",
custId: 9001,
displayName: "Driver One",
carNumber: null,
nickName: null,
},
},
],
}),
],
},
],
},
]);

mocks.getIracingCustIdFromJwt.mockReturnValue(9001);
mocks.prisma.user.findUnique.mockResolvedValue({
id: "user-1",
iracingCustId: 9001,
});
mocks.prisma.leagueMembership.findUnique.mockResolvedValue({
owner: false,
admin: false,
});
mocks.prisma.member.findUnique.mockResolvedValue({ id: "member-1" });

const response = await GET(buildRequest("valid-token"), {
params: Promise.resolve({ leagueId: "league-1" }),
});

expect(response.status).toBe(200);

const payload = (await response.json()) as {
isAdmin: boolean;
series: Array<{
seasons: Array<{
schedules: Array<{ registeredMembers: unknown[] }>;
}>;
}>;
};

expect(payload.isAdmin).toBe(false);
expect(payload.series[0].seasons[0].schedules[0].registeredMembers).toEqual(
[],
);
});
});
61 changes: 33 additions & 28 deletions src/app/api/leagues/[leagueId]/calendar/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,6 @@ export async function GET(
const { leagueId: rawLeagueId } = await params;

const accessToken = request.cookies.get("irh_access_token")?.value;
if (!accessToken) {
return NextResponse.json({ error: "unauthorized" }, { status: 401 });
}

// leagueId may be either the DB id or the iRacing numeric league ID
const iracingLeagueIdNum = parseInt(rawLeagueId, 10);
Expand All @@ -32,34 +29,42 @@ export async function GET(

const leagueDbId = league.id;

const iracingCustId = getIracingCustIdFromJwt(accessToken);
const user = await prisma.user.findUnique({
where: { iracingCustId },
select: { id: true, iracingCustId: true },
});
if (!user) {
return NextResponse.json({ error: "user_not_found" }, { status: 404 });
}
let isAdmin = false;
let member: { id: string } | null = null;

const membership = await prisma.leagueMembership.findUnique({
where: { userId_leagueId: { userId: user.id, leagueId: leagueDbId } },
select: { owner: true, admin: true },
});
if (!membership) {
return NextResponse.json({ error: "not_a_member" }, { status: 403 });
}
if (accessToken) {
try {
const iracingCustId = getIracingCustIdFromJwt(accessToken);
const user = await prisma.user.findUnique({
where: { iracingCustId },
select: { id: true, iracingCustId: true },
});

const isAdmin = membership.owner || membership.admin;
if (user) {
const membership = await prisma.leagueMembership.findUnique({
where: {
userId_leagueId: { userId: user.id, leagueId: leagueDbId },
},
select: { owner: true, admin: true },
});

const member = await prisma.member.findUnique({
where: {
leagueId_custId: {
leagueId: leagueDbId,
custId: user.iracingCustId,
},
},
select: { id: true },
});
if (membership) {
isAdmin = membership.owner || membership.admin;
member = await prisma.member.findUnique({
where: {
leagueId_custId: {
leagueId: leagueDbId,
custId: user.iracingCustId,
},
},
select: { id: true },
});
}
}
} catch {
// ignore auth errors — serve public data
}
}

const series = await prisma.series.findMany({
where: { leagueId: leagueDbId },
Expand Down
Loading
Loading