Skip to content

Commit 6db75ed

Browse files
authored
test: add test coverage for testcase controller
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
1 parent 1229c15 commit 6db75ed

3 files changed

Lines changed: 367 additions & 0 deletions

File tree

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
import path from 'node:path'
2+
import test from 'ava'
3+
import fse from 'fs-extra'
4+
import supertest from 'supertest'
5+
import app from '../../../src/app'
6+
import config from '../../../src/config'
7+
import { encryptData } from '../../../src/services/crypto'
8+
9+
const server = app.listen()
10+
const request = supertest.agent(server)
11+
12+
// Test variables to track created resources
13+
let testPid: number | null = null
14+
let testcaseUuid: string | null = null
15+
16+
test.before('Login as admin', async (t) => {
17+
const login = await request
18+
.post('/api/account/login')
19+
.send({
20+
username: 'admin',
21+
password: await encryptData(config.deploy.adminInitPwd),
22+
})
23+
24+
t.is(login.status, 200)
25+
})
26+
27+
test.before('Create a test problem', async (t) => {
28+
const create = await request
29+
.post('/api/problem')
30+
.send({
31+
title: 'Test Problem for Testcases',
32+
description: 'A problem to test testcase operations',
33+
input: 'Test input description',
34+
output: 'Test output description',
35+
in: '1 2',
36+
out: '3',
37+
})
38+
39+
t.is(create.status, 200)
40+
t.truthy(create.body.pid)
41+
testPid = create.body.pid
42+
})
43+
44+
test.serial('Find testcases - should return empty array initially', async (t) => {
45+
const res = await request
46+
.get(`/api/problem/${testPid}/testcases`)
47+
48+
t.is(res.status, 200)
49+
t.true(res.body.success)
50+
t.true(Array.isArray(res.body.data))
51+
t.is(res.body.data.length, 0)
52+
})
53+
54+
test.serial('Create testcase - should succeed with valid input and output', async (t) => {
55+
const res = await request
56+
.post(`/api/problem/${testPid}/testcases`)
57+
.send({
58+
in: '1 2\n',
59+
out: '3\n',
60+
})
61+
62+
t.is(res.status, 200)
63+
t.true(res.body.success)
64+
t.true(Array.isArray(res.body.data))
65+
t.is(res.body.data.length, 1)
66+
t.truthy(res.body.data[0].uuid)
67+
68+
// Store UUID for later tests
69+
testcaseUuid = res.body.data[0].uuid
70+
71+
// Verify the testcase files were created
72+
const testDir = path.resolve(__dirname, `../../../data/${testPid}`)
73+
t.true(fse.existsSync(path.resolve(testDir, `${testcaseUuid}.in`)))
74+
t.true(fse.existsSync(path.resolve(testDir, `${testcaseUuid}.out`)))
75+
})
76+
77+
test.serial('Create testcase - should fail without both input and output', async (t) => {
78+
const res = await request
79+
.post(`/api/problem/${testPid}/testcases`)
80+
.send({
81+
in: '',
82+
out: '',
83+
})
84+
85+
t.is(res.status, 400)
86+
})
87+
88+
test.serial('Find testcases - should return created testcase', async (t) => {
89+
const res = await request
90+
.get(`/api/problem/${testPid}/testcases`)
91+
92+
t.is(res.status, 200)
93+
t.true(res.body.success)
94+
t.true(Array.isArray(res.body.data))
95+
t.is(res.body.data.length, 1)
96+
t.is(res.body.data[0].uuid, testcaseUuid)
97+
})
98+
99+
test.serial('Get testcase input file - should return input content', async (t) => {
100+
const res = await request
101+
.get(`/api/problem/${testPid}/testcases/${testcaseUuid}.in`)
102+
103+
t.is(res.status, 200)
104+
t.is(res.type, 'text/plain')
105+
t.is(res.text, '1 2\n')
106+
})
107+
108+
test.serial('Get testcase output file - should return output content', async (t) => {
109+
const res = await request
110+
.get(`/api/problem/${testPid}/testcases/${testcaseUuid}.out`)
111+
112+
t.is(res.status, 200)
113+
t.is(res.type, 'text/plain')
114+
t.is(res.text, '3\n')
115+
})
116+
117+
test.serial('Get testcase - should fail with invalid type', async (t) => {
118+
const res = await request
119+
.get(`/api/problem/${testPid}/testcases/${testcaseUuid}.txt`)
120+
121+
t.is(res.status, 400)
122+
})
123+
124+
test.serial('Get testcase - should fail with invalid UUID', async (t) => {
125+
const res = await request
126+
.get(`/api/problem/${testPid}/testcases/invalid-uuid.in`)
127+
128+
t.is(res.status, 400)
129+
})
130+
131+
test.serial('Get testcase - should fail with non-existent UUID', async (t) => {
132+
const nonExistentUuid = '00000000-0000-0000-0000-000000000000'
133+
const res = await request
134+
.get(`/api/problem/${testPid}/testcases/${nonExistentUuid}.in`)
135+
136+
t.is(res.status, 400)
137+
})
138+
139+
test.serial('Export testcases - should return zip file', async (t) => {
140+
const res = await request
141+
.get(`/api/problem/${testPid}/testcases/export`)
142+
143+
t.is(res.status, 200)
144+
t.is(res.type, 'application/zip')
145+
t.truthy(res.header['content-disposition'])
146+
t.true(res.header['content-disposition'].includes('attachment'))
147+
t.true(res.header['content-disposition'].includes('.zip'))
148+
t.truthy(res.body)
149+
})
150+
151+
test.serial('Create multiple testcases - should return all testcases', async (t) => {
152+
const res1 = await request
153+
.post(`/api/problem/${testPid}/testcases`)
154+
.send({
155+
in: '5 10\n',
156+
out: '15\n',
157+
})
158+
159+
t.is(res1.status, 200)
160+
t.true(res1.body.success)
161+
t.is(res1.body.data.length, 2)
162+
163+
const res2 = await request
164+
.post(`/api/problem/${testPid}/testcases`)
165+
.send({
166+
in: '100 200\n',
167+
out: '300\n',
168+
})
169+
170+
t.is(res2.status, 200)
171+
t.true(res2.body.success)
172+
t.is(res2.body.data.length, 3)
173+
})
174+
175+
test.serial('Remove testcase - should succeed with valid UUID', async (t) => {
176+
const res = await request
177+
.delete(`/api/problem/${testPid}/testcases/${testcaseUuid}`)
178+
179+
t.is(res.status, 200)
180+
t.true(res.body.success)
181+
t.true(Array.isArray(res.body.data))
182+
t.is(res.body.data.length, 2) // Should have 2 remaining testcases
183+
184+
// Verify the UUID is not in the returned list
185+
const uuids = res.body.data.map((tc: any) => tc.uuid)
186+
t.false(uuids.includes(testcaseUuid))
187+
188+
// Verify files still exist (they should not be deleted)
189+
const testDir = path.resolve(__dirname, `../../../data/${testPid}`)
190+
t.true(fse.existsSync(path.resolve(testDir, `${testcaseUuid}.in`)))
191+
t.true(fse.existsSync(path.resolve(testDir, `${testcaseUuid}.out`)))
192+
})
193+
194+
test.serial('Remove testcase - should fail with invalid UUID format', async (t) => {
195+
const res = await request
196+
.delete(`/api/problem/${testPid}/testcases/invalid-uuid`)
197+
198+
t.is(res.status, 400)
199+
})
200+
201+
test.after.always('Cleanup', async (_t) => {
202+
// Clean up test problem and data directory
203+
if (testPid) {
204+
await request.delete(`/api/problem/${testPid}`)
205+
206+
const testDir = path.resolve(__dirname, `../../../data/${testPid}`)
207+
if (fse.existsSync(testDir)) {
208+
await fse.remove(testDir)
209+
}
210+
}
211+
212+
server.close()
213+
})
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import test from 'ava'
2+
import supertest from 'supertest'
3+
import app from '../../../src/app'
4+
import config from '../../../src/config'
5+
import { encryptData } from '../../../src/services/crypto'
6+
7+
const server = app.listen()
8+
const request = supertest.agent(server)
9+
const adminRequest = supertest.agent(server)
10+
11+
let testPid: number | null = null
12+
13+
test.before('Setup - login as admin and create test problem', async (t) => {
14+
// Login as admin first
15+
const adminLogin = await adminRequest
16+
.post('/api/account/login')
17+
.send({
18+
username: 'admin',
19+
password: await encryptData(config.deploy.adminInitPwd),
20+
})
21+
22+
t.is(adminLogin.status, 200)
23+
24+
// Create a problem that regular user won't have access to
25+
const create = await adminRequest
26+
.post('/api/problem')
27+
.send({
28+
title: 'Test Problem for User Permission Tests',
29+
description: 'A problem to test user permission denial',
30+
input: 'Test input',
31+
output: 'Test output',
32+
in: '1',
33+
out: '1',
34+
})
35+
36+
t.is(create.status, 200)
37+
t.truthy(create.body.pid)
38+
testPid = create.body.pid
39+
})
40+
41+
test.before('Login as regular user', async (t) => {
42+
const login = await request
43+
.post('/api/account/login')
44+
.send({
45+
username: 'primaryuser',
46+
password: await encryptData('testtest'),
47+
})
48+
49+
t.is(login.status, 200)
50+
})
51+
52+
test('Find testcases - should be denied for non-admin/non-owner', async (t) => {
53+
const res = await request
54+
.get(`/api/problem/${testPid}/testcases`)
55+
56+
t.is(res.status, 403)
57+
})
58+
59+
test('Create testcase - should be denied for non-admin/non-owner', async (t) => {
60+
const res = await request
61+
.post(`/api/problem/${testPid}/testcases`)
62+
.send({
63+
in: '1 2\n',
64+
out: '3\n',
65+
})
66+
67+
t.is(res.status, 403)
68+
})
69+
70+
test('Export testcases - should be denied for non-admin/non-owner', async (t) => {
71+
const res = await request
72+
.get(`/api/problem/${testPid}/testcases/export`)
73+
74+
t.is(res.status, 403)
75+
})
76+
77+
test('Get testcase - should be denied for non-admin/non-owner', async (t) => {
78+
const dummyUuid = '00000000-0000-0000-0000-000000000000'
79+
const res = await request
80+
.get(`/api/problem/${testPid}/testcases/${dummyUuid}.in`)
81+
82+
t.is(res.status, 403)
83+
})
84+
85+
test('Remove testcase - should be denied for non-admin/non-owner', async (t) => {
86+
const dummyUuid = '00000000-0000-0000-0000-000000000000'
87+
const res = await request
88+
.delete(`/api/problem/${testPid}/testcases/${dummyUuid}`)
89+
90+
t.is(res.status, 403)
91+
})
92+
93+
test.after.always('Cleanup', async (_t) => {
94+
// Clean up test problem
95+
if (testPid) {
96+
await adminRequest.delete(`/api/problem/${testPid}`)
97+
}
98+
99+
server.close()
100+
})
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import test from 'ava'
2+
import supertest from 'supertest'
3+
import app from '../../../src/app'
4+
5+
const server = app.listen()
6+
const request = supertest.agent(server)
7+
8+
// Use an existing problem from seeds (pid: 1000)
9+
const testPid = 1000
10+
11+
test('Find testcases - should require login', async (t) => {
12+
const res = await request
13+
.get(`/api/problem/${testPid}/testcases`)
14+
15+
t.is(res.status, 401)
16+
})
17+
18+
test('Create testcase - should require login', async (t) => {
19+
const res = await request
20+
.post(`/api/problem/${testPid}/testcases`)
21+
.send({
22+
in: '1 2\n',
23+
out: '3\n',
24+
})
25+
26+
t.is(res.status, 401)
27+
})
28+
29+
test('Export testcases - should require login', async (t) => {
30+
const res = await request
31+
.get(`/api/problem/${testPid}/testcases/export`)
32+
33+
t.is(res.status, 401)
34+
})
35+
36+
test('Get testcase - should require login', async (t) => {
37+
const dummyUuid = '00000000-0000-0000-0000-000000000000'
38+
const res = await request
39+
.get(`/api/problem/${testPid}/testcases/${dummyUuid}.in`)
40+
41+
t.is(res.status, 401)
42+
})
43+
44+
test('Remove testcase - should require login', async (t) => {
45+
const dummyUuid = '00000000-0000-0000-0000-000000000000'
46+
const res = await request
47+
.delete(`/api/problem/${testPid}/testcases/${dummyUuid}`)
48+
49+
t.is(res.status, 401)
50+
})
51+
52+
test.after.always('close server', () => {
53+
server.close()
54+
})

0 commit comments

Comments
 (0)