Skip to content

Commit 1cb4998

Browse files
committed
add /api/v1/me endpoint
1 parent b8e720f commit 1cb4998

File tree

3 files changed

+442
-15
lines changed

3 files changed

+442
-15
lines changed
Lines changed: 356 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,356 @@
1+
import { describe, test, expect } from 'bun:test'
2+
import { NextRequest } from 'next/server'
3+
4+
import { meGet } from '../../../app/api/v1/me/route'
5+
import { VALID_USER_INFO_FIELDS } from '../../../db/user'
6+
7+
import type {
8+
GetUserInfoFromApiKeyFn,
9+
GetUserInfoFromApiKeyOutput,
10+
} from '@codebuff/common/types/contracts/database'
11+
12+
describe('/api/v1/me route', () => {
13+
const mockUserData: Record<string, any> = {
14+
'test-api-key-123': {
15+
id: 'user-123',
16+
email: 'test@example.com',
17+
discord_id: 'discord-123',
18+
},
19+
'test-api-key-456': {
20+
id: 'user-456',
21+
email: 'test2@example.com',
22+
discord_id: null,
23+
},
24+
}
25+
26+
const mockGetUserInfoFromApiKey: GetUserInfoFromApiKeyFn = async ({
27+
apiKey,
28+
fields,
29+
}) => {
30+
const userData = mockUserData[apiKey]
31+
if (!userData) {
32+
return null
33+
}
34+
return Object.fromEntries(
35+
fields.map((field) => [field, userData[field]])
36+
) as any
37+
}
38+
39+
describe('Authentication', () => {
40+
test('returns 401 when Authorization header is missing', async () => {
41+
const req = new NextRequest('http://localhost:3000/api/v1/me')
42+
const response = await meGet({
43+
req,
44+
getUserInfoFromApiKey: mockGetUserInfoFromApiKey,
45+
})
46+
47+
expect(response.status).toBe(401)
48+
const body = await response.json()
49+
expect(body).toEqual({ error: 'Missing or invalid Authorization header' })
50+
})
51+
52+
test('returns 401 when Authorization header is malformed', async () => {
53+
const req = new NextRequest('http://localhost:3000/api/v1/me', {
54+
headers: { Authorization: 'InvalidFormat' },
55+
})
56+
const response = await meGet({
57+
req,
58+
getUserInfoFromApiKey: mockGetUserInfoFromApiKey,
59+
})
60+
61+
expect(response.status).toBe(401)
62+
const body = await response.json()
63+
expect(body).toEqual({ error: 'Missing or invalid Authorization header' })
64+
})
65+
66+
test('extracts API key from x-codebuff-api-key header', async () => {
67+
const apiKey = 'test-api-key-123'
68+
const req = new NextRequest('http://localhost:3000/api/v1/me', {
69+
headers: { 'x-codebuff-api-key': apiKey },
70+
})
71+
72+
const response = await meGet({
73+
req,
74+
getUserInfoFromApiKey: mockGetUserInfoFromApiKey,
75+
})
76+
expect(response.status).toBe(200)
77+
const body = await response.json()
78+
expect(body).toEqual({ id: 'user-123' })
79+
})
80+
81+
test('extracts API key from Bearer token in Authorization header', async () => {
82+
const apiKey = 'test-api-key-123'
83+
const req = new NextRequest('http://localhost:3000/api/v1/me', {
84+
headers: { Authorization: `Bearer ${apiKey}` },
85+
})
86+
87+
const response = await meGet({
88+
req,
89+
getUserInfoFromApiKey: mockGetUserInfoFromApiKey,
90+
})
91+
expect(response.status).toBe(200)
92+
const body = await response.json()
93+
expect(body).toEqual({ id: 'user-123' })
94+
})
95+
96+
test('returns 404 when API key is invalid', async () => {
97+
const req = new NextRequest('http://localhost:3000/api/v1/me', {
98+
headers: { Authorization: 'Bearer invalid-key' },
99+
})
100+
101+
const response = await meGet({
102+
req,
103+
getUserInfoFromApiKey: mockGetUserInfoFromApiKey,
104+
})
105+
expect(response.status).toBe(404)
106+
const body = await response.json()
107+
expect(body).toEqual({ error: 'Invalid API key or user not found' })
108+
})
109+
})
110+
111+
describe('Field parameter validation', () => {
112+
test('defaults to id field when no fields parameter provided', async () => {
113+
const req = new NextRequest('http://localhost:3000/api/v1/me', {
114+
headers: { Authorization: 'Bearer test-api-key-123' },
115+
})
116+
117+
const response = await meGet({
118+
req,
119+
getUserInfoFromApiKey: mockGetUserInfoFromApiKey,
120+
})
121+
expect(response.status).toBe(200)
122+
const body = await response.json()
123+
expect(body).toEqual({ id: 'user-123' })
124+
})
125+
126+
test('accepts single valid field', async () => {
127+
const req = new NextRequest(
128+
'http://localhost:3000/api/v1/me?fields=email',
129+
{
130+
headers: { Authorization: 'Bearer test-api-key-123' },
131+
}
132+
)
133+
134+
const response = await meGet({
135+
req,
136+
getUserInfoFromApiKey: mockGetUserInfoFromApiKey,
137+
})
138+
expect(response.status).toBe(200)
139+
const body = await response.json()
140+
expect(body).toEqual({ email: 'test@example.com' })
141+
})
142+
143+
test('accepts multiple valid fields', async () => {
144+
const req = new NextRequest(
145+
'http://localhost:3000/api/v1/me?fields=id,email,discord_id',
146+
{
147+
headers: { Authorization: 'Bearer test-api-key-123' },
148+
}
149+
)
150+
151+
const response = await meGet({
152+
req,
153+
getUserInfoFromApiKey: mockGetUserInfoFromApiKey,
154+
})
155+
expect(response.status).toBe(200)
156+
const body = await response.json()
157+
expect(body).toEqual({
158+
id: 'user-123',
159+
email: 'test@example.com',
160+
discord_id: 'discord-123',
161+
})
162+
})
163+
164+
test('trims whitespace from field names', async () => {
165+
const req = new NextRequest(
166+
'http://localhost:3000/api/v1/me?fields=id, email , discord_id',
167+
{
168+
headers: { Authorization: 'Bearer test-api-key-123' },
169+
}
170+
)
171+
172+
const response = await meGet({
173+
req,
174+
getUserInfoFromApiKey: mockGetUserInfoFromApiKey,
175+
})
176+
expect(response.status).toBe(200)
177+
const body = await response.json()
178+
expect(body).toEqual({
179+
id: 'user-123',
180+
email: 'test@example.com',
181+
discord_id: 'discord-123',
182+
})
183+
})
184+
185+
test('returns 400 for invalid field names', async () => {
186+
const req = new NextRequest(
187+
'http://localhost:3000/api/v1/me?fields=invalid_field',
188+
{
189+
headers: { Authorization: 'Bearer test-api-key-123' },
190+
}
191+
)
192+
193+
const response = await meGet({
194+
req,
195+
getUserInfoFromApiKey: mockGetUserInfoFromApiKey,
196+
})
197+
expect(response.status).toBe(400)
198+
const body = await response.json()
199+
expect(body.error).toContain('Invalid fields: invalid_field')
200+
expect(body.error).toContain(
201+
`Valid fields are: ${VALID_USER_INFO_FIELDS.join(', ')}`
202+
)
203+
})
204+
205+
test('returns 400 for multiple invalid field names', async () => {
206+
const req = new NextRequest(
207+
'http://localhost:3000/api/v1/me?fields=invalid1,invalid2,email',
208+
{
209+
headers: { Authorization: 'Bearer test-api-key-123' },
210+
}
211+
)
212+
213+
const response = await meGet({
214+
req,
215+
getUserInfoFromApiKey: mockGetUserInfoFromApiKey,
216+
})
217+
expect(response.status).toBe(400)
218+
const body = await response.json()
219+
expect(body.error).toContain('Invalid fields: invalid1, invalid2')
220+
})
221+
222+
test('returns 400 when mixing valid and invalid fields', async () => {
223+
const req = new NextRequest(
224+
'http://localhost:3000/api/v1/me?fields=id,bad_field,email',
225+
{
226+
headers: { Authorization: 'Bearer test-api-key-123' },
227+
}
228+
)
229+
230+
const response = await meGet({
231+
req,
232+
getUserInfoFromApiKey: mockGetUserInfoFromApiKey,
233+
})
234+
expect(response.status).toBe(400)
235+
})
236+
})
237+
238+
describe('Successful responses', () => {
239+
test('returns user data with default id field', async () => {
240+
const req = new NextRequest('http://localhost:3000/api/v1/me', {
241+
headers: { Authorization: 'Bearer test-api-key-123' },
242+
})
243+
244+
const response = await meGet({
245+
req,
246+
getUserInfoFromApiKey: mockGetUserInfoFromApiKey,
247+
})
248+
expect(response.status).toBe(200)
249+
const body = await response.json()
250+
expect(body).toEqual({ id: 'user-123' })
251+
})
252+
253+
test('returns user data with single requested field', async () => {
254+
const req = new NextRequest(
255+
'http://localhost:3000/api/v1/me?fields=email',
256+
{
257+
headers: { Authorization: 'Bearer test-api-key-123' },
258+
}
259+
)
260+
261+
const response = await meGet({
262+
req,
263+
getUserInfoFromApiKey: mockGetUserInfoFromApiKey,
264+
})
265+
expect(response.status).toBe(200)
266+
const body = await response.json()
267+
expect(body).toEqual({ email: 'test@example.com' })
268+
})
269+
270+
test('returns user data with multiple requested fields', async () => {
271+
const req = new NextRequest(
272+
'http://localhost:3000/api/v1/me?fields=id,email,discord_id',
273+
{
274+
headers: { Authorization: 'Bearer test-api-key-123' },
275+
}
276+
)
277+
278+
const response = await meGet({
279+
req,
280+
getUserInfoFromApiKey: mockGetUserInfoFromApiKey,
281+
})
282+
expect(response.status).toBe(200)
283+
const body = await response.json()
284+
expect(body).toEqual({
285+
id: 'user-123',
286+
email: 'test@example.com',
287+
discord_id: 'discord-123',
288+
})
289+
})
290+
291+
test('handles null discord_id correctly', async () => {
292+
const req = new NextRequest(
293+
'http://localhost:3000/api/v1/me?fields=id,discord_id',
294+
{
295+
headers: { Authorization: 'Bearer test-api-key-456' },
296+
}
297+
)
298+
299+
const response = await meGet({
300+
req,
301+
getUserInfoFromApiKey: mockGetUserInfoFromApiKey,
302+
})
303+
expect(response.status).toBe(200)
304+
const body = await response.json()
305+
expect(body).toEqual({ id: 'user-456', discord_id: null })
306+
})
307+
})
308+
309+
describe('Edge cases', () => {
310+
test('handles empty fields parameter', async () => {
311+
const req = new NextRequest('http://localhost:3000/api/v1/me?fields=', {
312+
headers: { Authorization: 'Bearer test-api-key-123' },
313+
})
314+
315+
const response = await meGet({
316+
req,
317+
getUserInfoFromApiKey: mockGetUserInfoFromApiKey,
318+
})
319+
expect(response.status).toBe(400)
320+
const body = await response.json()
321+
expect(body.error).toContain('Invalid fields')
322+
})
323+
324+
test('handles fields parameter with only commas', async () => {
325+
const req = new NextRequest(
326+
'http://localhost:3000/api/v1/me?fields=,,,',
327+
{
328+
headers: { Authorization: 'Bearer test-api-key-123' },
329+
}
330+
)
331+
332+
const response = await meGet({
333+
req,
334+
getUserInfoFromApiKey: mockGetUserInfoFromApiKey,
335+
})
336+
expect(response.status).toBe(400)
337+
})
338+
339+
test('handles case-sensitive field names', async () => {
340+
const req = new NextRequest(
341+
'http://localhost:3000/api/v1/me?fields=ID,Email',
342+
{
343+
headers: { Authorization: 'Bearer test-api-key-123' },
344+
}
345+
)
346+
347+
const response = await meGet({
348+
req,
349+
getUserInfoFromApiKey: mockGetUserInfoFromApiKey,
350+
})
351+
expect(response.status).toBe(400)
352+
const body = await response.json()
353+
expect(body.error).toContain('Invalid fields: ID, Email')
354+
})
355+
})
356+
})

0 commit comments

Comments
 (0)