Skip to content

Commit 9cd4aac

Browse files
CopilotStarefossen
andauthored
Migrate REST APIs to tRPC for end-to-end type safety (#339)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Starefossen <968267+Starefossen@users.noreply.github.com> Co-authored-by: Hans Kristian Flaatten <hans.kristian.flaatten@nav.no>
1 parent 242758a commit 9cd4aac

37 files changed

Lines changed: 2242 additions & 1862 deletions

__tests__/api/badge/badge-e2e.test.ts

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -476,32 +476,26 @@ describe('Badge System E2E', () => {
476476
// Bake JWT string into SVG
477477
const bakedSVG = bakeBadge(svg, assertion)
478478

479-
// 2. Validate through API (which includes cryptographic verification)
480-
const { POST } = await import('@/app/api/badge/validate/route')
481-
const request = {
482-
json: async () => ({ svg: bakedSVG }),
483-
} as any
479+
// 2. Validate through the validation module (migrated from REST to tRPC)
480+
const { validateBadge } = await import('@/lib/badge/validation')
481+
const result = await validateBadge(bakedSVG)
484482

485-
const response = await POST(request)
486-
expect(response.status).toBe(200)
487-
488-
const result = await response.json()
489483
expect(result.checks).toBeDefined()
490484

491485
// Verify canonicalization check (RDF Dataset Canonicalization)
492486
const canonCheck = result.checks.find(
493-
(c: any) => c.name === 'canonicalization',
487+
(c) => c.name === 'canonicalization',
494488
)
495489
expect(canonCheck).toBeDefined()
496-
expect(canonCheck.status).toBe('success')
497-
expect(canonCheck.details?.canonicalizationResult).toBeDefined()
498-
expect(typeof canonCheck.details?.canonicalizationResult).toBe('string')
490+
expect(canonCheck!.status).toBe('success')
491+
expect(canonCheck!.details?.canonicalizationResult).toBeDefined()
492+
expect(typeof canonCheck!.details?.canonicalizationResult).toBe('string')
499493

500494
// Find the proof check
501-
const proofCheck = result.checks.find((c: any) => c.name === 'proof')
495+
const proofCheck = result.checks.find((c) => c.name === 'proof')
502496
expect(proofCheck).toBeDefined()
503-
expect(proofCheck.status).toBe('success')
504-
expect(proofCheck.details?.signatureValid).toBe(true)
497+
expect(proofCheck!.status).toBe('success')
498+
expect(proofCheck!.details?.signatureValid).toBe(true)
505499

506500
console.log(
507501
'✓ Validator API cryptographically verified badge signature with RDF canonicalization',
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
vi.mock('@/lib/auth', () => ({
2+
getAuthSession: vi.fn().mockResolvedValue(null),
3+
}))
4+
5+
vi.mock('@/lib/events/registry', () => ({}))
6+
7+
import { describe, it, expect, vi } from 'vitest'
8+
import { TRPCError } from '@trpc/server'
9+
import {
10+
createAnonymousCaller,
11+
createAuthenticatedCaller,
12+
createAdminCaller,
13+
speakers,
14+
} from '../../helpers/trpc'
15+
16+
describe('tRPC middleware', () => {
17+
describe('protectedProcedure', () => {
18+
it('should reject unauthenticated requests', async () => {
19+
const caller = createAnonymousCaller()
20+
21+
await expect(caller.proposal.list()).rejects.toThrow(TRPCError)
22+
await expect(caller.proposal.list()).rejects.toMatchObject({
23+
code: 'UNAUTHORIZED',
24+
})
25+
})
26+
27+
it('should allow authenticated requests', async () => {
28+
// This will fail because of missing mock data, but it should NOT throw UNAUTHORIZED
29+
const caller = createAuthenticatedCaller()
30+
try {
31+
await caller.proposal.list()
32+
} catch (error) {
33+
expect(error).toBeInstanceOf(TRPCError)
34+
expect((error as TRPCError).code).not.toBe('UNAUTHORIZED')
35+
}
36+
})
37+
})
38+
39+
describe('adminProcedure', () => {
40+
it('should reject unauthenticated requests', async () => {
41+
const caller = createAnonymousCaller()
42+
43+
await expect(caller.speakers.list({})).rejects.toThrow(TRPCError)
44+
await expect(caller.speakers.list({})).rejects.toMatchObject({
45+
code: 'UNAUTHORIZED',
46+
})
47+
})
48+
49+
it('should reject non-admin users', async () => {
50+
const regularUser = speakers.find((s) => !s.isOrganizer)!
51+
const caller = createAuthenticatedCaller(regularUser._id)
52+
53+
await expect(caller.speakers.list({})).rejects.toThrow(TRPCError)
54+
await expect(caller.speakers.list({})).rejects.toMatchObject({
55+
code: 'FORBIDDEN',
56+
})
57+
})
58+
59+
it('should allow admin users', async () => {
60+
const caller = createAdminCaller()
61+
// Should not throw UNAUTHORIZED or FORBIDDEN — may throw due to missing mock data
62+
try {
63+
await caller.speakers.list({})
64+
} catch (error) {
65+
expect(error).toBeInstanceOf(TRPCError)
66+
expect((error as TRPCError).code).not.toBe('UNAUTHORIZED')
67+
expect((error as TRPCError).code).not.toBe('FORBIDDEN')
68+
}
69+
})
70+
})
71+
})

0 commit comments

Comments
 (0)