const API_URL = process.env.API_URL || 'http://localhost:3001'; const TOKEN = process.env.TEST_TOKEN; if (!TOKEN) { console.error('ERROR: TEST_TOKEN environment variable is required'); console.log('Run: npm run test:get-token'); console.log('Then: export TEST_TOKEN=""'); process.exit(1); } interface TestResult { name: string; passed: boolean; error?: string; } const results: TestResult[] = []; async function request( method: string, path: string, body?: unknown, expectStatus = 200 ): Promise<{ status: number; data: unknown }> { const response = await fetch(`${API_URL}${path}`, { method, headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${TOKEN}`, }, body: body ? JSON.stringify(body) : undefined, }); const data = response.headers.get('content-type')?.includes('application/json') ? await response.json() : null; if (response.status !== expectStatus) { throw new Error(`Expected ${expectStatus}, got ${response.status}: ${JSON.stringify(data)}`); } return { status: response.status, data }; } async function test(name: string, fn: () => Promise) { try { await fn(); results.push({ name, passed: true }); console.log(` ✓ ${name}`); } catch (error) { const message = error instanceof Error ? error.message : String(error); results.push({ name, passed: false, error: message }); console.log(` ✗ ${name}`); console.log(` ${message}`); } } async function runTests() { console.log('\n=== Kaboot API Tests ===\n'); console.log(`API: ${API_URL}`); console.log(''); let createdQuizId: string | null = null; console.log('Health Check:'); await test('GET /health returns ok', async () => { const res = await fetch(`${API_URL}/health`); const data = await res.json(); if (data.status !== 'ok') throw new Error('Health check failed'); }); console.log('\nAuth Tests:'); await test('GET /api/quizzes without token returns 401', async () => { const res = await fetch(`${API_URL}/api/quizzes`); if (res.status !== 401) throw new Error(`Expected 401, got ${res.status}`); }); await test('GET /api/quizzes with invalid token returns 401', async () => { const res = await fetch(`${API_URL}/api/quizzes`, { headers: { Authorization: 'Bearer invalid-token' }, }); if (res.status !== 401) throw new Error(`Expected 401, got ${res.status}`); }); console.log('\nUser Tests:'); await test('GET /api/users/me returns user info', async () => { const { data } = await request('GET', '/api/users/me'); const user = data as Record; if (!user.id) throw new Error('Missing user id'); if (!user.username) throw new Error('Missing username'); }); console.log('\nQuiz CRUD Tests:'); await test('GET /api/quizzes returns array', async () => { const { data } = await request('GET', '/api/quizzes'); if (!Array.isArray(data)) throw new Error('Expected array'); }); await test('POST /api/quizzes creates quiz', async () => { const quiz = { title: 'Test Quiz', source: 'manual', questions: [ { text: 'What is 2 + 2?', timeLimit: 20, options: [ { text: '3', isCorrect: false, shape: 'triangle', color: 'red' }, { text: '4', isCorrect: true, shape: 'diamond', color: 'blue' }, { text: '5', isCorrect: false, shape: 'circle', color: 'yellow' }, { text: '6', isCorrect: false, shape: 'square', color: 'green' }, ], }, ], }; const { data } = await request('POST', '/api/quizzes', quiz, 201); const result = data as { id: string }; if (!result.id) throw new Error('Missing quiz id'); createdQuizId = result.id; }); await test('GET /api/quizzes/:id returns full quiz', async () => { if (!createdQuizId) throw new Error('No quiz created'); const { data } = await request('GET', `/api/quizzes/${createdQuizId}`); const quiz = data as Record; if (quiz.title !== 'Test Quiz') throw new Error('Wrong title'); if (!Array.isArray(quiz.questions)) throw new Error('Missing questions'); const questions = quiz.questions as Record[]; if (questions.length !== 1) throw new Error('Wrong question count'); const q = questions[0]; if (!Array.isArray(q.options)) throw new Error('Missing options'); if ((q.options as unknown[]).length !== 4) throw new Error('Wrong option count'); }); await test('PUT /api/quizzes/:id updates quiz', async () => { if (!createdQuizId) throw new Error('No quiz created'); const updatedQuiz = { title: 'Updated Test Quiz', questions: [ { text: 'Updated question?', timeLimit: 30, options: [ { text: 'A', isCorrect: true, shape: 'triangle', color: 'red' }, { text: 'B', isCorrect: false, shape: 'diamond', color: 'blue' }, { text: 'C', isCorrect: false, shape: 'circle', color: 'yellow' }, { text: 'D', isCorrect: false, shape: 'square', color: 'green' }, ], }, ], }; await request('PUT', `/api/quizzes/${createdQuizId}`, updatedQuiz); const { data } = await request('GET', `/api/quizzes/${createdQuizId}`); const quiz = data as Record; if (quiz.title !== 'Updated Test Quiz') throw new Error('Title not updated'); }); await test('DELETE /api/quizzes/:id deletes quiz', async () => { if (!createdQuizId) throw new Error('No quiz created'); await request('DELETE', `/api/quizzes/${createdQuizId}`, undefined, 204); }); await test('GET /api/quizzes/:id returns 404 for deleted quiz', async () => { if (!createdQuizId) throw new Error('No quiz created'); await request('GET', `/api/quizzes/${createdQuizId}`, undefined, 404); }); console.log('\nQuiz Validation Tests:'); await test('POST /api/quizzes without title returns 400', async () => { const invalidQuiz = { source: 'manual', questions: [ { text: 'Question?', options: [ { text: 'A', isCorrect: true, shape: 'triangle', color: 'red' }, ], }, ], }; await request('POST', '/api/quizzes', invalidQuiz, 400); }); await test('POST /api/quizzes without source returns 400', async () => { const invalidQuiz = { title: 'Missing Source Quiz', questions: [ { text: 'Question?', options: [ { text: 'A', isCorrect: true, shape: 'triangle', color: 'red' }, ], }, ], }; await request('POST', '/api/quizzes', invalidQuiz, 400); }); await test('POST /api/quizzes without questions returns 400', async () => { const invalidQuiz = { title: 'No Questions Quiz', source: 'manual', questions: [], }; await request('POST', '/api/quizzes', invalidQuiz, 400); }); await test('POST /api/quizzes with empty body returns 400', async () => { await request('POST', '/api/quizzes', {}, 400); }); console.log('\nQuiz Not Found Tests:'); await test('GET /api/quizzes/:id with non-existent ID returns 404', async () => { await request('GET', '/api/quizzes/non-existent-uuid-12345', undefined, 404); }); await test('PUT /api/quizzes/:id with non-existent ID returns 404', async () => { const quiz = { title: 'Update Non-Existent', questions: [ { text: 'Q?', options: [{ text: 'A', isCorrect: true, shape: 'triangle', color: 'red' }], }, ], }; await request('PUT', '/api/quizzes/non-existent-uuid-12345', quiz, 404); }); await test('DELETE /api/quizzes/:id with non-existent ID returns 404', async () => { await request('DELETE', '/api/quizzes/non-existent-uuid-12345', undefined, 404); }); console.log('\nQuiz Source Types Tests:'); let aiQuizId: string | null = null; await test('POST /api/quizzes with ai_generated source and aiTopic', async () => { const aiQuiz = { title: 'AI Generated Quiz', source: 'ai_generated', aiTopic: 'Space Exploration', questions: [ { text: 'What planet is known as the Red Planet?', timeLimit: 20, options: [ { text: 'Venus', isCorrect: false, shape: 'triangle', color: 'red' }, { text: 'Mars', isCorrect: true, shape: 'diamond', color: 'blue' }, { text: 'Jupiter', isCorrect: false, shape: 'circle', color: 'yellow' }, { text: 'Saturn', isCorrect: false, shape: 'square', color: 'green' }, ], }, ], }; const { data } = await request('POST', '/api/quizzes', aiQuiz, 201); const result = data as { id: string }; if (!result.id) throw new Error('Missing quiz id'); aiQuizId = result.id; }); await test('GET /api/quizzes/:id returns aiTopic for AI quiz', async () => { if (!aiQuizId) throw new Error('No AI quiz created'); const { data } = await request('GET', `/api/quizzes/${aiQuizId}`); const quiz = data as Record; if (quiz.source !== 'ai_generated') throw new Error('Wrong source'); if (quiz.aiTopic !== 'Space Exploration') throw new Error('Missing or wrong aiTopic'); }); await test('GET /api/quizzes list includes source and questionCount', async () => { const { data } = await request('GET', '/api/quizzes'); const quizzes = data as Record[]; if (quizzes.length === 0) throw new Error('Expected at least one quiz'); const quiz = quizzes.find((q) => q.id === aiQuizId); if (!quiz) throw new Error('AI quiz not in list'); if (quiz.source !== 'ai_generated') throw new Error('Missing source in list'); if (typeof quiz.questionCount !== 'number') throw new Error('Missing questionCount'); if (quiz.questionCount !== 1) throw new Error('Wrong questionCount'); }); await test('DELETE cleanup AI quiz', async () => { if (!aiQuizId) throw new Error('No AI quiz to delete'); await request('DELETE', `/api/quizzes/${aiQuizId}`, undefined, 204); }); console.log('\nQuiz with Multiple Questions Tests:'); let multiQuizId: string | null = null; await test('POST /api/quizzes with multiple questions', async () => { const multiQuiz = { title: 'Multi-Question Quiz', source: 'manual', questions: [ { text: 'Question 1?', timeLimit: 15, options: [ { text: 'A', isCorrect: true, shape: 'triangle', color: 'red' }, { text: 'B', isCorrect: false, shape: 'diamond', color: 'blue' }, ], }, { text: 'Question 2?', timeLimit: 25, options: [ { text: 'X', isCorrect: false, shape: 'circle', color: 'yellow' }, { text: 'Y', isCorrect: true, shape: 'square', color: 'green' }, ], }, { text: 'Question 3?', timeLimit: 30, options: [ { text: 'P', isCorrect: false, shape: 'triangle', color: 'red', reason: 'Wrong because...' }, { text: 'Q', isCorrect: true, shape: 'diamond', color: 'blue', reason: 'Correct because...' }, ], }, ], }; const { data } = await request('POST', '/api/quizzes', multiQuiz, 201); const result = data as { id: string }; multiQuizId = result.id; }); await test('GET /api/quizzes/:id returns all questions with correct order', async () => { if (!multiQuizId) throw new Error('No multi-question quiz created'); const { data } = await request('GET', `/api/quizzes/${multiQuizId}`); const quiz = data as { questions: { text: string; timeLimit: number; options: { reason?: string }[] }[] }; if (quiz.questions.length !== 3) throw new Error(`Expected 3 questions, got ${quiz.questions.length}`); if (quiz.questions[0].text !== 'Question 1?') throw new Error('Wrong order for Q1'); if (quiz.questions[1].text !== 'Question 2?') throw new Error('Wrong order for Q2'); if (quiz.questions[2].text !== 'Question 3?') throw new Error('Wrong order for Q3'); if (quiz.questions[0].timeLimit !== 15) throw new Error('Wrong timeLimit for Q1'); if (quiz.questions[2].options[1].reason !== 'Correct because...') throw new Error('Missing reason field'); }); await test('GET /api/quizzes shows correct questionCount for multi-question quiz', async () => { const { data } = await request('GET', '/api/quizzes'); const quizzes = data as Record[]; const quiz = quizzes.find((q) => q.id === multiQuizId); if (!quiz) throw new Error('Multi-question quiz not in list'); if (quiz.questionCount !== 3) throw new Error(`Expected questionCount 3, got ${quiz.questionCount}`); }); await test('PUT /api/quizzes/:id replaces all questions', async () => { if (!multiQuizId) throw new Error('No multi-question quiz created'); const updatedQuiz = { title: 'Updated Multi Quiz', questions: [ { text: 'Only One Question Now', timeLimit: 10, options: [ { text: 'Solo', isCorrect: true, shape: 'triangle', color: 'red' }, ], }, ], }; await request('PUT', `/api/quizzes/${multiQuizId}`, updatedQuiz); const { data } = await request('GET', `/api/quizzes/${multiQuizId}`); const quiz = data as { title: string; questions: unknown[] }; if (quiz.title !== 'Updated Multi Quiz') throw new Error('Title not updated'); if (quiz.questions.length !== 1) throw new Error(`Expected 1 question after update, got ${quiz.questions.length}`); }); await test('DELETE cleanup multi-question quiz', async () => { if (!multiQuizId) throw new Error('No multi-question quiz to delete'); await request('DELETE', `/api/quizzes/${multiQuizId}`, undefined, 204); }); console.log('\nTimestamp Tests:'); let timestampQuizId: string | null = null; await test('POST /api/quizzes returns quiz with timestamps', async () => { const quiz = { title: 'Timestamp Test Quiz', source: 'manual', questions: [ { text: 'Timestamp Q?', options: [{ text: 'A', isCorrect: true, shape: 'triangle', color: 'red' }], }, ], }; const { data: createData } = await request('POST', '/api/quizzes', quiz, 201); timestampQuizId = (createData as { id: string }).id; const { data } = await request('GET', `/api/quizzes/${timestampQuizId}`); const result = data as Record; if (!result.createdAt) throw new Error('Missing createdAt'); if (!result.updatedAt) throw new Error('Missing updatedAt'); }); await test('PUT /api/quizzes/:id updates updatedAt timestamp', async () => { if (!timestampQuizId) throw new Error('No timestamp quiz created'); const { data: beforeData } = await request('GET', `/api/quizzes/${timestampQuizId}`); const beforeUpdatedAt = (beforeData as Record).updatedAt; await new Promise((resolve) => setTimeout(resolve, 1100)); await request('PUT', `/api/quizzes/${timestampQuizId}`, { title: 'Updated Timestamp Quiz', questions: [ { text: 'Updated Q?', options: [{ text: 'B', isCorrect: true, shape: 'diamond', color: 'blue' }], }, ], }); const { data: afterData } = await request('GET', `/api/quizzes/${timestampQuizId}`); const afterUpdatedAt = (afterData as Record).updatedAt; if (beforeUpdatedAt === afterUpdatedAt) throw new Error('updatedAt should have changed'); }); await test('DELETE cleanup timestamp quiz', async () => { if (!timestampQuizId) throw new Error('No timestamp quiz to delete'); await request('DELETE', `/api/quizzes/${timestampQuizId}`, undefined, 204); }); console.log('\n=== Results ==='); const passed = results.filter((r) => r.passed).length; const failed = results.filter((r) => !r.passed).length; console.log(`Passed: ${passed}/${results.length}`); console.log(`Failed: ${failed}/${results.length}`); if (failed > 0) { console.log('\nFailed tests:'); results .filter((r) => !r.passed) .forEach((r) => console.log(` - ${r.name}: ${r.error}`)); process.exit(1); } console.log('\nAll tests passed!'); } runTests().catch((err) => { console.error('Test runner error:', err); process.exit(1); });