Skip to content

Commit 15d5649

Browse files
authored
Merge pull request #535 from Center-for-AI-Innovation/unit-tests
Add Unit Tests
2 parents 50f33e3 + 2d0fa64 commit 15d5649

File tree

253 files changed

+33776
-791
lines changed

Some content is hidden

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

253 files changed

+33776
-791
lines changed

.github/workflows/test-on-pr.yml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
name: Test on PR
2+
3+
on:
4+
pull_request:
5+
types: [opened, synchronize, reopened, ready_for_review]
6+
branches:
7+
- main
8+
- illinois-chat
9+
workflow_dispatch:
10+
11+
concurrency:
12+
group: test-on-pr-${{ github.ref }}
13+
cancel-in-progress: true
14+
15+
permissions:
16+
contents: read
17+
18+
jobs:
19+
test:
20+
name: Test
21+
runs-on: ubuntu-latest
22+
timeout-minutes: 20
23+
steps:
24+
- name: Checkout
25+
uses: actions/checkout@v4
26+
27+
- name: Setup Node
28+
uses: actions/setup-node@v4
29+
with:
30+
node-version: 20
31+
cache: npm
32+
33+
- name: Install
34+
run: npm ci
35+
36+
- name: Test
37+
run: npm run test:coverage:check
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import { describe, expect, it, vi } from 'vitest'
2+
import { createMockReq, createMockRes } from '~/test-utils/nextApi'
3+
4+
vi.mock('~/pages/api/authorization', () => ({
5+
withCourseOwnerOrAdminAccess: () => (h: any) => h,
6+
withCourseAccessFromRequest: () => (h: any) => h,
7+
}))
8+
9+
vi.mock('~/utils/apiUtils', () => ({
10+
getBackendUrl: () => 'http://backend',
11+
}))
12+
13+
import getAllCourseDataHandler from '~/pages/api/UIUC-api/getAllCourseData'
14+
import downloadMITCourseHandler from '~/pages/api/UIUC-api/downloadMITCourse'
15+
import getN8nWorkflowsHandler from '~/pages/api/UIUC-api/getN8nWorkflows'
16+
17+
describe('UIUC-api backend proxy routes', () => {
18+
it('getAllCourseData validates method/params and proxies fetch', async () => {
19+
const res1 = createMockRes()
20+
await getAllCourseDataHandler(createMockReq({ method: 'POST' }) as any, res1 as any)
21+
expect(res1.status).toHaveBeenCalledWith(405)
22+
23+
const res2 = createMockRes()
24+
await getAllCourseDataHandler(
25+
createMockReq({ method: 'GET', query: {} }) as any,
26+
res2 as any,
27+
)
28+
expect(res2.status).toHaveBeenCalledWith(400)
29+
30+
const fetchSpy = vi
31+
.spyOn(globalThis, 'fetch')
32+
.mockResolvedValueOnce(new Response('nope', { status: 500 }))
33+
.mockResolvedValueOnce(
34+
new Response(JSON.stringify({ ok: true }), {
35+
status: 200,
36+
headers: { 'content-type': 'application/json' },
37+
}),
38+
)
39+
40+
const res3 = createMockRes()
41+
await getAllCourseDataHandler(
42+
createMockReq({ method: 'GET', query: { course_name: 'CS101' } }) as any,
43+
res3 as any,
44+
)
45+
expect(res3.status).toHaveBeenCalledWith(500)
46+
47+
const res4 = createMockRes()
48+
await getAllCourseDataHandler(
49+
createMockReq({ method: 'GET', query: { course_name: 'CS101' } }) as any,
50+
res4 as any,
51+
)
52+
expect(res4.status).toHaveBeenCalledWith(200)
53+
fetchSpy.mockRestore()
54+
})
55+
56+
it('downloadMITCourse validates inputs and proxies fetch', async () => {
57+
const res1 = createMockRes()
58+
await downloadMITCourseHandler(createMockReq({ method: 'POST' }) as any, res1 as any)
59+
expect(res1.status).toHaveBeenCalledWith(405)
60+
61+
const res2 = createMockRes()
62+
await downloadMITCourseHandler(
63+
createMockReq({ method: 'GET', query: { url: 'u' } }) as any,
64+
res2 as any,
65+
)
66+
expect(res2.status).toHaveBeenCalledWith(400)
67+
68+
const fetchSpy = vi
69+
.spyOn(globalThis, 'fetch')
70+
.mockResolvedValueOnce(new Response('nope', { status: 502 }))
71+
.mockResolvedValueOnce(
72+
new Response(JSON.stringify({ ok: true }), {
73+
status: 200,
74+
headers: { 'content-type': 'application/json' },
75+
}),
76+
)
77+
78+
const res3 = createMockRes()
79+
await downloadMITCourseHandler(
80+
createMockReq({
81+
method: 'GET',
82+
query: { url: 'u', course_name: 'CS101', local_dir: 'd' },
83+
}) as any,
84+
res3 as any,
85+
)
86+
expect(res3.status).toHaveBeenCalledWith(502)
87+
88+
const res4 = createMockRes()
89+
await downloadMITCourseHandler(
90+
createMockReq({
91+
method: 'GET',
92+
query: { url: 'u', course_name: 'CS101', local_dir: 'd' },
93+
}) as any,
94+
res4 as any,
95+
)
96+
expect(res4.status).toHaveBeenCalledWith(200)
97+
fetchSpy.mockRestore()
98+
})
99+
100+
it('getN8nWorkflows validates inputs and proxies fetch', async () => {
101+
const res1 = createMockRes()
102+
await getN8nWorkflowsHandler(createMockReq({ method: 'POST' }) as any, res1 as any)
103+
expect(res1.status).toHaveBeenCalledWith(405)
104+
105+
const res2 = createMockRes()
106+
await getN8nWorkflowsHandler(
107+
createMockReq({ method: 'GET', query: {} }) as any,
108+
res2 as any,
109+
)
110+
expect(res2.status).toHaveBeenCalledWith(400)
111+
112+
const fetchSpy = vi
113+
.spyOn(globalThis, 'fetch')
114+
.mockResolvedValueOnce(new Response('nope', { status: 500, statusText: 'boom' }))
115+
.mockResolvedValueOnce(
116+
new Response(JSON.stringify({ workflows: [] }), {
117+
status: 200,
118+
headers: { 'content-type': 'application/json' },
119+
}),
120+
)
121+
122+
const res3 = createMockRes()
123+
await getN8nWorkflowsHandler(
124+
createMockReq({ method: 'GET', query: { api_key: 'k' } }) as any,
125+
res3 as any,
126+
)
127+
expect(res3.status).toHaveBeenCalledWith(500)
128+
129+
const res4 = createMockRes()
130+
await getN8nWorkflowsHandler(
131+
createMockReq({ method: 'GET', query: { api_key: 'k' } }) as any,
132+
res4 as any,
133+
)
134+
expect(res4.status).toHaveBeenCalledWith(200)
135+
fetchSpy.mockRestore()
136+
})
137+
})
138+
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
/* @vitest-environment node */
2+
3+
import { beforeEach, describe, expect, it, vi, afterEach } from 'vitest'
4+
import { createMockReq, createMockRes } from '~/test-utils/nextApi'
5+
6+
const hoisted = vi.hoisted(() => ({
7+
select: vi.fn(),
8+
insert: vi.fn(),
9+
update: vi.fn(),
10+
uuid: vi.fn(() => 'file-upload-id'),
11+
}))
12+
13+
vi.mock('~/pages/api/authorization', () => ({
14+
withCourseAccessFromRequest: () => (handler: any) => handler,
15+
}))
16+
17+
vi.mock('~/utils/apiUtils', () => ({
18+
getBackendUrl: () => 'http://backend',
19+
}))
20+
21+
vi.mock('uuid', () => ({
22+
v4: hoisted.uuid,
23+
}))
24+
25+
vi.mock('drizzle-orm', () => ({
26+
eq: () => ({}),
27+
}))
28+
29+
vi.mock('~/db/dbClient', () => ({
30+
db: {
31+
select: hoisted.select,
32+
insert: hoisted.insert,
33+
update: hoisted.update,
34+
},
35+
conversations: {
36+
id: { name: 'id' },
37+
user_email: { name: 'user_email' },
38+
},
39+
fileUploads: {
40+
id: { name: 'id' },
41+
},
42+
}))
43+
44+
import handler from '~/pages/api/UIUC-api/chat-file-upload'
45+
46+
describe('UIUC-api chat-file-upload', () => {
47+
beforeEach(() => {
48+
hoisted.select.mockReset()
49+
hoisted.insert.mockReset()
50+
hoisted.update.mockReset()
51+
hoisted.uuid.mockClear()
52+
vi.unstubAllGlobals()
53+
})
54+
55+
afterEach(() => {
56+
vi.unstubAllGlobals()
57+
})
58+
59+
it('returns 405 for non-POST', async () => {
60+
const res = createMockRes()
61+
await handler(createMockReq({ method: 'GET' }) as any, res as any)
62+
expect(res.status).toHaveBeenCalledWith(405)
63+
})
64+
65+
it('returns 400 when required params are missing', async () => {
66+
const res = createMockRes()
67+
await handler(
68+
createMockReq({ method: 'POST', body: { courseName: 'CS101' } }) as any,
69+
res as any,
70+
)
71+
expect(res.status).toHaveBeenCalledWith(400)
72+
})
73+
74+
it('returns 500 when creating a missing conversation fails', async () => {
75+
hoisted.select.mockImplementationOnce(() => ({
76+
from: () => ({
77+
where: () => ({
78+
limit: vi.fn().mockResolvedValueOnce([]),
79+
}),
80+
}),
81+
}))
82+
83+
hoisted.insert.mockReturnValueOnce({
84+
values: vi.fn().mockRejectedValueOnce(new Error('convFail')),
85+
})
86+
87+
const res = createMockRes()
88+
await handler(
89+
createMockReq({
90+
method: 'POST',
91+
body: {
92+
conversationId: 'c1',
93+
courseName: 'CS101',
94+
user_id: 'u@example.com',
95+
s3Key: 'courses/CS101/file.txt',
96+
fileName: 'file.txt',
97+
},
98+
}) as any,
99+
res as any,
100+
)
101+
expect(res.status).toHaveBeenCalledWith(500)
102+
expect(res.json).toHaveBeenCalledWith({ error: 'Failed to create conversation' })
103+
})
104+
105+
it('returns 500 when backend processing is not ok', async () => {
106+
hoisted.select.mockImplementationOnce(() => ({
107+
from: () => ({
108+
where: () => ({
109+
limit: vi.fn().mockResolvedValueOnce([{ id: 'c1', user_email: 'u@example.com' }]),
110+
}),
111+
}),
112+
}))
113+
114+
hoisted.insert.mockReturnValueOnce({
115+
values: vi.fn().mockResolvedValueOnce(undefined),
116+
})
117+
118+
vi.stubGlobal(
119+
'fetch',
120+
vi.fn(async () => ({
121+
ok: false,
122+
status: 500,
123+
statusText: 'boom',
124+
text: vi.fn(async () => 'error body'),
125+
})) as any,
126+
)
127+
128+
const res = createMockRes()
129+
await handler(
130+
createMockReq({
131+
method: 'POST',
132+
body: {
133+
conversationId: 'c1',
134+
courseName: 'CS101',
135+
user_id: 'u@example.com',
136+
s3Key: 'courses/CS101/file.txt',
137+
fileName: 'file.txt',
138+
},
139+
}) as any,
140+
res as any,
141+
)
142+
expect(res.status).toHaveBeenCalledWith(500)
143+
})
144+
145+
it('returns 200 on success even if updating upload status fails', async () => {
146+
hoisted.select.mockImplementationOnce(() => ({
147+
from: () => ({
148+
where: () => ({
149+
limit: vi.fn().mockResolvedValueOnce([{ id: 'c1', user_email: 'u@example.com' }]),
150+
}),
151+
}),
152+
}))
153+
154+
hoisted.insert.mockReturnValueOnce({
155+
values: vi.fn().mockResolvedValueOnce(undefined),
156+
})
157+
158+
vi.stubGlobal(
159+
'fetch',
160+
vi.fn(async () => ({
161+
ok: true,
162+
json: vi.fn(async () => ({ chunks_created: 2 })),
163+
})) as any,
164+
)
165+
166+
hoisted.update.mockReturnValueOnce({
167+
set: () => ({
168+
where: vi.fn().mockRejectedValueOnce(new Error('updateFail')),
169+
}),
170+
})
171+
172+
const res = createMockRes()
173+
await handler(
174+
createMockReq({
175+
method: 'POST',
176+
body: {
177+
conversationId: 'c1',
178+
courseName: 'CS101',
179+
user_id: 'u@example.com',
180+
s3Key: 'courses/CS101/file.txt',
181+
fileName: 'file.txt',
182+
},
183+
}) as any,
184+
res as any,
185+
)
186+
187+
expect(res.status).toHaveBeenCalledWith(200)
188+
expect(res.json).toHaveBeenCalledWith(
189+
expect.objectContaining({
190+
success: true,
191+
fileUploadId: 'file-upload-id',
192+
chunks_created: 2,
193+
}),
194+
)
195+
})
196+
})
197+

0 commit comments

Comments
 (0)