const request = require('supertest'); const app = require('../src/server'); describe('Health', () => { it('GET /health returns ok', async () => { const res = await request(app).get('/health'); expect(res.status).toBe(200); expect(res.body.status).toBe('ok'); expect(res.body.service).toBe('lms-bgn-backend'); }); }); describe('Epik 1 - Courses', () => { it('GET /api/courses returns paged list', async () => { const res = await request(app).get('/api/courses?page=1&pageSize=20'); expect(res.status).toBe(200); expect(res.body).toHaveProperty('page'); expect(res.body).toHaveProperty('pageSize'); expect(res.body).toHaveProperty('total'); expect(Array.isArray(res.body.items)).toBe(true); }); it('GET /api/courses/:courseId returns detail', async () => { const res = await request(app).get('/api/courses/c1'); expect(res.status).toBe(200); expect(res.body.id).toBe('c1'); expect(res.body).toHaveProperty('modules'); }); it('GET /api/courses/:courseId returns 404 for unknown', async () => { const res = await request(app).get('/api/courses/unknown'); expect(res.status).toBe(404); }); it('POST /api/modules/:moduleId/progress validates progressPercent', async () => { const res = await request(app) .post('/api/modules/m1/progress') .send({ progressPercent: 50 }); expect(res.status).toBe(202); expect(res.body.status).toBe('accepted'); }); }); describe('Epik 1 - Assignments', () => { it('POST /api/assignments/:assignmentId/submission requires idempotencyKey', async () => { const res = await request(app) .post('/api/assignments/a1/submission') .send({ content: 'Answer' }); expect(res.status).toBe(400); }); it('POST /api/assignments/:assignmentId/submission creates submission', async () => { const res = await request(app) .post('/api/assignments/a1/submission') .send({ content: 'Answer', idempotencyKey: 'abc' }); expect(res.status).toBe(201); expect(res.body.status).toBe('created'); }); it('POST /api/assignments/:assignmentId/submission returns 409 on duplicate idempotency', async () => { await request(app) .post('/api/assignments/a1/submission') .send({ content: 'Answer', idempotencyKey: 'dup-key' }); const res = await request(app) .post('/api/assignments/a1/submission') .send({ content: 'Answer', idempotencyKey: 'dup-key' }); expect(res.status).toBe(409); }); }); describe('Epik 2 - Exam Session', () => { let sessionId; it('POST /api/exam-session requires fields', async () => { const res = await request(app) .post('/api/exam-session') .set('Authorization', 'Bearer test-token') .send({}); expect(res.status).toBe(400); }); it('POST /api/exam-session creates session', async () => { const res = await request(app) .post('/api/exam-session') .set('Authorization', 'Bearer test-token') .send({ examId: 'exam-001', userId: 'u1' }); expect(res.status).toBe(201); expect(res.body).toHaveProperty('sessionId'); sessionId = res.body.sessionId; }); it('POST /api/exam-session/:sessionId/score scores answers', async () => { const res = await request(app) .post(`/api/exam-session/${sessionId}/score`) .set('Authorization', 'Bearer test-token') .send({ idempotencyKey: 'score-1', mode: 'batch', answers: [ { questionId: 'q1', choiceIndex: 0 }, { questionId: 'q2', choiceIndex: 2 } ] }); expect(res.status).toBe(200); expect(res.body).toHaveProperty('totalScore'); expect(res.body).toHaveProperty('totalQuestions'); expect(res.body).toHaveProperty('correctAnswers'); }); it('POST /api/exam-session/:sessionId/score returns 409 on duplicate idempotency', async () => { const res = await request(app) .post(`/api/exam-session/${sessionId}/score`) .set('Authorization', 'Bearer test-token') .send({ idempotencyKey: 'score-1', answers: [] }); expect(res.status).toBe(409); }); it('GET /api/exam-session/:sessionId/summary returns summary', async () => { const res = await request(app) .get(`/api/exam-session/${sessionId}/summary`) .set('Authorization', 'Bearer test-token'); expect(res.status).toBe(200); expect(res.body).toHaveProperty('examId'); expect(res.body).toHaveProperty('answers'); }); }); describe('Epik 2 - Exams listing', () => { it('GET /api/exams returns paged list', async () => { const res = await request(app).get('/api/exams?page=1&pageSize=20'); expect(res.status).toBe(200); expect(res.body).toHaveProperty('items'); }); it('GET /api/exams filters by q', async () => { const res = await request(app).get('/api/exams?page=1&pageSize=20&q=HACCP'); expect(res.status).toBe(200); expect(Array.isArray(res.body.items)).toBe(true); }); }); describe('Epik 2 - Certificates', () => { it('POST /api/certificates/:certificateId/resend requires fields', async () => { let res = await request(app) .post('/api/certificates/abc/resend') .send({ idempotencyKey: 'id1' }); expect(res.status).toBe(400); res = await request(app) .post('/api/certificates/abc/resend') .send({ recipientEmail: 'x@y.com' }); expect(res.status).toBe(400); }); it('POST /api/certificates/:certificateId/resend accepts and throttles', async () => { const res1 = await request(app) .post('/api/certificates/abc/resend') .send({ recipientEmail: 'x@y.com', idempotencyKey: 'cert-1' }); expect(res1.status).toBe(202); const res2 = await request(app) .post('/api/certificates/abc/resend') .send({ recipientEmail: 'x@y.com', idempotencyKey: 'cert-1' }); expect(res2.status).toBe(429); }); });