Skip to content

Commit 714597f

Browse files
authored
Merge pull request #60 from danbim/orpc
Refactor code-base to use oRPC and make UI more type-safe
2 parents 76758ff + f695fa0 commit 714597f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+5909
-2005
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,5 @@ postgres-data/
5151
# Claude Code local settings
5252
.claude/settings.local.json
5353

54+
# Reviews ran locally
55+
reviews/

__tests__/api/orpc/seasons.test.ts

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "bun:test";
2+
import { RPCHandler } from "@orpc/server/fetch";
3+
import { appRouter } from "../../../src/api/orpc/router.js";
4+
import { getDb } from "../../../src/infrastructure/db/index.js";
5+
import { seasons, sessions, users } from "../../../src/infrastructure/db/schema.js";
6+
import { clearTestData, setupTestDb, teardownTestDb } from "../../test-db.js";
7+
8+
const handler = new RPCHandler(appRouter);
9+
10+
const ADMIN_USER_ID = "a0000000-0000-4000-a000-000000000a01";
11+
const JUDGE_USER_ID = "a0000000-0000-4000-a000-000000000a02";
12+
const ADMIN_TOKEN = "b0000000-0000-4000-b000-000000000b01";
13+
const JUDGE_TOKEN = "b0000000-0000-4000-b000-000000000b02";
14+
const TEST_SEASON_ID = "c0000000-0000-4000-a000-000000000c01";
15+
16+
type RpcData = any; // eslint-disable-line @typescript-eslint/no-explicit-any
17+
18+
interface RpcResult {
19+
status: number;
20+
data: RpcData;
21+
}
22+
23+
async function rpc(procedurePath: string, input?: unknown, cookie?: string): Promise<RpcResult> {
24+
const urlPath = procedurePath.replace(/\./g, "/");
25+
const headers: Record<string, string> = { "Content-Type": "application/json" };
26+
if (cookie) headers.cookie = cookie;
27+
const request = new Request(`http://localhost/rpc/${urlPath}`, {
28+
method: "POST",
29+
headers,
30+
body: input !== undefined ? JSON.stringify({ json: input, meta: [] }) : undefined,
31+
});
32+
const { matched, response } = await handler.handle(request, {
33+
prefix: "/rpc",
34+
context: { request },
35+
});
36+
if (!matched || !response) {
37+
throw new Error(`No procedure matched for path: ${urlPath}`);
38+
}
39+
const body = await response.json();
40+
return { status: response.status, data: body.json ?? body };
41+
}
42+
43+
describe("Season oRPC Procedures", () => {
44+
beforeAll(async () => {
45+
await setupTestDb();
46+
});
47+
48+
afterAll(async () => {
49+
await teardownTestDb();
50+
});
51+
52+
beforeEach(async () => {
53+
await clearTestData();
54+
const db = await getDb();
55+
56+
await db.insert(users).values([
57+
{
58+
id: ADMIN_USER_ID,
59+
username: "admin",
60+
email: null,
61+
passwordHash: "hashed",
62+
role: "administrator",
63+
},
64+
{
65+
id: JUDGE_USER_ID,
66+
username: "judge",
67+
email: null,
68+
passwordHash: "hashed",
69+
role: "judge",
70+
},
71+
]);
72+
73+
await db.insert(sessions).values([
74+
{
75+
userId: ADMIN_USER_ID,
76+
token: ADMIN_TOKEN,
77+
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000),
78+
},
79+
{
80+
userId: JUDGE_USER_ID,
81+
token: JUDGE_TOKEN,
82+
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000),
83+
},
84+
]);
85+
86+
await db.insert(seasons).values({
87+
id: TEST_SEASON_ID,
88+
name: "Test Season",
89+
year: 2025,
90+
startDate: new Date("2025-01-01"),
91+
endDate: new Date("2025-12-31"),
92+
});
93+
});
94+
95+
describe("listSeasons", () => {
96+
it("should list seasons when authenticated", async () => {
97+
const result = await rpc("season.list", undefined, `session_token=${ADMIN_TOKEN}`);
98+
99+
expect(result.status).toBe(200);
100+
expect(result.data.seasons).toHaveLength(1);
101+
expect(result.data.seasons[0].name).toBe("Test Season");
102+
});
103+
104+
it("should return 401 without auth", async () => {
105+
const result = await rpc("season.list");
106+
expect(result.status).toBe(401);
107+
});
108+
});
109+
110+
describe("getSeason", () => {
111+
it("should get season by ID", async () => {
112+
const result = await rpc(
113+
"season.get",
114+
{ seasonId: TEST_SEASON_ID },
115+
`session_token=${ADMIN_TOKEN}`
116+
);
117+
118+
expect(result.status).toBe(200);
119+
expect(result.data.name).toBe("Test Season");
120+
expect(result.data.year).toBe(2025);
121+
});
122+
123+
it("should return 404 for nonexistent season", async () => {
124+
const result = await rpc(
125+
"season.get",
126+
{ seasonId: "d0000000-0000-4000-a000-000000000999" },
127+
`session_token=${ADMIN_TOKEN}`
128+
);
129+
expect(result.status).toBe(404);
130+
});
131+
});
132+
133+
describe("createSeason", () => {
134+
it("should create season as admin", async () => {
135+
const result = await rpc(
136+
"season.create",
137+
{ name: "New Season", year: 2026, startDate: "2026-01-01", endDate: "2026-12-31" },
138+
`session_token=${ADMIN_TOKEN}`
139+
);
140+
expect(result.status).toBe(200);
141+
expect(result.data.name).toBe("New Season");
142+
expect(result.data.year).toBe(2026);
143+
expect(result.data.id).toBeDefined();
144+
});
145+
146+
it("should return 403 for judge", async () => {
147+
const result = await rpc(
148+
"season.create",
149+
{ name: "New Season", year: 2026, startDate: "2026-01-01", endDate: "2026-12-31" },
150+
`session_token=${JUDGE_TOKEN}`
151+
);
152+
expect(result.status).toBe(403);
153+
});
154+
});
155+
156+
describe("updateSeason", () => {
157+
it("should update season as admin", async () => {
158+
const result = await rpc(
159+
"season.update",
160+
{ seasonId: TEST_SEASON_ID, data: { name: "Updated Season" } },
161+
`session_token=${ADMIN_TOKEN}`
162+
);
163+
expect(result.status).toBe(200);
164+
expect(result.data.name).toBe("Updated Season");
165+
});
166+
167+
it("should return 403 for judge", async () => {
168+
const result = await rpc(
169+
"season.update",
170+
{ seasonId: TEST_SEASON_ID, data: { name: "Updated" } },
171+
`session_token=${JUDGE_TOKEN}`
172+
);
173+
expect(result.status).toBe(403);
174+
});
175+
});
176+
177+
describe("deleteSeason", () => {
178+
it("should delete season as admin", async () => {
179+
const result = await rpc(
180+
"season.delete",
181+
{ seasonId: TEST_SEASON_ID },
182+
`session_token=${ADMIN_TOKEN}`
183+
);
184+
expect(result.status).toBe(200);
185+
expect(result.data.message).toBe("Season deleted successfully");
186+
});
187+
188+
it("should return 403 for judge", async () => {
189+
const result = await rpc(
190+
"season.delete",
191+
{ seasonId: TEST_SEASON_ID },
192+
`session_token=${JUDGE_TOKEN}`
193+
);
194+
expect(result.status).toBe(403);
195+
});
196+
});
197+
});

__tests__/components/ui/Button.test.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,15 @@ describe("Button", () => {
5959
expect(button).not.toHaveClass("bg-");
6060
});
6161

62+
it("should render danger-text variant with correct styles", () => {
63+
render(() => <Button variant="danger-text">Delete</Button>);
64+
65+
const button = screen.getByRole("button");
66+
expect(button).toHaveClass("text-red-600");
67+
expect(button).toHaveClass("hover:text-red-800");
68+
expect(button).not.toHaveClass("bg-");
69+
});
70+
6271
it("should render small size with correct styles", () => {
6372
render(() => <Button size="sm">Small</Button>);
6473

0 commit comments

Comments
 (0)