const axios = require('axios'); const API_URL = 'http://localhost:3000/api'; // Test data let testUser = { email: 'reviewtest@example.com', password: 'Test@123', username: 'reviewtester' }; let secondUser = { email: 'otherreviewer@example.com', password: 'Test@123', username: 'otherreviewer' }; let userToken = null; let secondUserToken = null; let guestToken = null; let guestId = null; let testCategory = null; let completedSessionId = null; let inProgressSessionId = null; let guestCompletedSessionId = null; // Helper to add delay between tests const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms)); // Helper to create and complete a quiz async function createAndCompleteQuiz(token, isGuest = false, questionCount = 3) { const headers = isGuest ? { 'X-Guest-Token': token } : { 'Authorization': `Bearer ${token}` }; // Get categories const categoriesRes = await axios.get(`${API_URL}/categories`, { headers }); const categories = categoriesRes.data.data; const category = categories.find(c => c.questionCount >= questionCount); if (!category) { throw new Error('No category with enough questions found'); } // Start quiz const startRes = await axios.post(`${API_URL}/quiz/start`, { categoryId: category.id, questionCount, difficulty: 'mixed', quizType: 'practice' }, { headers }); const sessionId = startRes.data.data.sessionId; const questions = startRes.data.data.questions; // Submit answers for all questions for (const question of questions) { let answer; if (question.questionType === 'multiple') { answer = question.options[0].id; } else if (question.questionType === 'trueFalse') { answer = 'true'; } else { answer = 'Sample answer'; } await axios.post(`${API_URL}/quiz/submit`, { quizSessionId: sessionId, questionId: question.id, userAnswer: answer, timeTaken: Math.floor(Math.random() * 20) + 5 // 5-25 seconds }, { headers }); await delay(100); } // Complete quiz await axios.post(`${API_URL}/quiz/complete`, { sessionId }, { headers }); return sessionId; } // Test setup async function setup() { console.log('Setting up test data...\n'); try { // Register first user try { const registerRes = await axios.post(`${API_URL}/auth/register`, testUser); userToken = registerRes.data.data.token; console.log('✓ First user registered'); } catch (error) { const loginRes = await axios.post(`${API_URL}/auth/login`, { email: testUser.email, password: testUser.password }); userToken = loginRes.data.data.token; console.log('✓ First user logged in'); } // Register second user try { const registerRes = await axios.post(`${API_URL}/auth/register`, secondUser); secondUserToken = registerRes.data.data.token; console.log('✓ Second user registered'); } catch (error) { const loginRes = await axios.post(`${API_URL}/auth/login`, { email: secondUser.email, password: secondUser.password }); secondUserToken = loginRes.data.data.token; console.log('✓ Second user logged in'); } // Create guest session const guestRes = await axios.post(`${API_URL}/guest/start-session`); guestToken = guestRes.data.data.sessionToken; guestId = guestRes.data.data.guestId; console.log('✓ Guest session created'); // Get a test category const categoriesRes = await axios.get(`${API_URL}/categories`, { headers: { 'Authorization': `Bearer ${userToken}` } }); const categories = categoriesRes.data.data; // Sort by questionCount descending to get category with most questions categories.sort((a, b) => b.questionCount - a.questionCount); testCategory = categories.find(c => c.questionCount >= 3); if (!testCategory) { throw new Error('No category with enough questions found'); } console.log(`✓ Test category selected: ${testCategory.name} (${testCategory.questionCount} questions)`); await delay(500); // Create completed quiz for user (use available question count, max 5) const quizQuestionCount = Math.min(testCategory.questionCount, 5); completedSessionId = await createAndCompleteQuiz(userToken, false, quizQuestionCount); console.log(`✓ User completed session created (${quizQuestionCount} questions)`); await delay(500); // Create in-progress quiz for user const startRes = await axios.post(`${API_URL}/quiz/start`, { categoryId: testCategory.id, questionCount: 3, difficulty: 'easy', quizType: 'practice' }, { headers: { 'Authorization': `Bearer ${userToken}` } }); inProgressSessionId = startRes.data.data.sessionId; // Submit one answer to make it in-progress const questions = startRes.data.data.questions; let answer = questions[0].questionType === 'multiple' ? questions[0].options[0].id : 'true'; await axios.post(`${API_URL}/quiz/submit`, { quizSessionId: inProgressSessionId, questionId: questions[0].id, userAnswer: answer, timeTaken: 10 }, { headers: { 'Authorization': `Bearer ${userToken}` } }); console.log('✓ User in-progress session created'); await delay(500); // Create completed quiz for guest guestCompletedSessionId = await createAndCompleteQuiz(guestToken, true, 3); console.log('✓ Guest completed session created\n'); await delay(500); } catch (error) { console.error('Setup failed:', error.response?.data || error.message); throw error; } } // Test cases const tests = [ { name: 'Test 1: Review completed quiz (user)', run: async () => { const response = await axios.get(`${API_URL}/quiz/review/${completedSessionId}`, { headers: { 'Authorization': `Bearer ${userToken}` } }); if (response.status !== 200) throw new Error('Expected 200 status'); if (!response.data.success) throw new Error('Expected success true'); const { session, summary, questions } = response.data.data; // Validate session if (!session.id || session.id !== completedSessionId) throw new Error('Invalid session id'); if (session.status !== 'completed') throw new Error('Expected completed status'); if (!session.category || !session.category.name) throw new Error('Missing category info'); // Validate summary if (typeof summary.score.earned !== 'number') throw new Error('Score.earned should be number'); if (typeof summary.accuracy !== 'number') throw new Error('Accuracy should be number'); if (typeof summary.isPassed !== 'boolean') throw new Error('isPassed should be boolean'); if (summary.questions.total < 3) throw new Error('Expected at least 3 total questions'); // Validate questions if (questions.length < 3) throw new Error('Expected at least 3 questions'); // All questions should have correct answers shown questions.forEach((q, idx) => { if (q.correctAnswer === undefined) { throw new Error(`Question ${idx + 1} should show correct answer`); } if (q.resultStatus === undefined) { throw new Error(`Question ${idx + 1} should have resultStatus`); } if (q.showExplanation !== true) { throw new Error(`Question ${idx + 1} should have showExplanation`); } }); return '✓ Completed quiz review correct'; } }, { name: 'Test 2: Review guest completed quiz', run: async () => { const response = await axios.get(`${API_URL}/quiz/review/${guestCompletedSessionId}`, { headers: { 'X-Guest-Token': guestToken } }); if (response.status !== 200) throw new Error('Expected 200 status'); const { session, summary, questions } = response.data.data; if (session.id !== guestCompletedSessionId) throw new Error('Invalid session id'); if (session.status !== 'completed') throw new Error('Expected completed status'); if (questions.length !== 3) throw new Error('Expected 3 questions'); return '✓ Guest quiz review works'; } }, { name: 'Test 3: Cannot review in-progress quiz (400)', run: async () => { try { await axios.get(`${API_URL}/quiz/review/${inProgressSessionId}`, { headers: { 'Authorization': `Bearer ${userToken}` } }); throw new Error('Should have failed with 400'); } catch (error) { if (error.response?.status !== 400) { throw new Error(`Expected 400, got ${error.response?.status}`); } if (!error.response?.data?.message?.includes('completed')) { throw new Error('Error message should mention completed status'); } return '✓ In-progress quiz review blocked'; } } }, { name: 'Test 4: Missing session ID returns 400', run: async () => { try { await axios.get(`${API_URL}/quiz/review/`, { headers: { 'Authorization': `Bearer ${userToken}` } }); throw new Error('Should have failed'); } catch (error) { if (error.response?.status !== 404 && error.response?.status !== 400) { throw new Error(`Expected 400 or 404, got ${error.response?.status}`); } return '✓ Missing session ID handled'; } } }, { name: 'Test 5: Invalid UUID format returns 400', run: async () => { try { await axios.get(`${API_URL}/quiz/review/invalid-uuid`, { headers: { 'Authorization': `Bearer ${userToken}` } }); throw new Error('Should have failed with 400'); } catch (error) { if (error.response?.status !== 400) { throw new Error(`Expected 400, got ${error.response?.status}`); } return '✓ Invalid UUID returns 400'; } } }, { name: 'Test 6: Non-existent session returns 404', run: async () => { try { const fakeUuid = '00000000-0000-0000-0000-000000000000'; await axios.get(`${API_URL}/quiz/review/${fakeUuid}`, { headers: { 'Authorization': `Bearer ${userToken}` } }); throw new Error('Should have failed with 404'); } catch (error) { if (error.response?.status !== 404) { throw new Error(`Expected 404, got ${error.response?.status}`); } return '✓ Non-existent session returns 404'; } } }, { name: 'Test 7: Cannot access other user\'s quiz review (403)', run: async () => { try { await axios.get(`${API_URL}/quiz/review/${completedSessionId}`, { headers: { 'Authorization': `Bearer ${secondUserToken}` } }); throw new Error('Should have failed with 403'); } catch (error) { if (error.response?.status !== 403) { throw new Error(`Expected 403, got ${error.response?.status}`); } return '✓ Cross-user access blocked'; } } }, { name: 'Test 8: Unauthenticated request returns 401', run: async () => { try { await axios.get(`${API_URL}/quiz/review/${completedSessionId}`); throw new Error('Should have failed with 401'); } catch (error) { if (error.response?.status !== 401) { throw new Error(`Expected 401, got ${error.response?.status}`); } return '✓ Unauthenticated request blocked'; } } }, { name: 'Test 9: Response includes all required session fields', run: async () => { const response = await axios.get(`${API_URL}/quiz/review/${completedSessionId}`, { headers: { 'Authorization': `Bearer ${userToken}` } }); const { session } = response.data.data; const requiredFields = [ 'id', 'status', 'quizType', 'difficulty', 'category', 'startedAt', 'completedAt', 'timeSpent' ]; requiredFields.forEach(field => { if (!(field in session)) { throw new Error(`Missing required session field: ${field}`); } }); return '✓ All required session fields present'; } }, { name: 'Test 10: Response includes all required summary fields', run: async () => { const response = await axios.get(`${API_URL}/quiz/review/${completedSessionId}`, { headers: { 'Authorization': `Bearer ${userToken}` } }); const { summary } = response.data.data; // Score fields if (!summary.score || typeof summary.score.earned !== 'number') { throw new Error('Missing or invalid score.earned'); } if (typeof summary.score.total !== 'number') { throw new Error('Missing or invalid score.total'); } if (typeof summary.score.percentage !== 'number') { throw new Error('Missing or invalid score.percentage'); } // Questions summary const qFields = ['total', 'answered', 'correct', 'incorrect', 'unanswered']; qFields.forEach(field => { if (typeof summary.questions[field] !== 'number') { throw new Error(`Missing or invalid questions.${field}`); } }); // Other fields if (typeof summary.accuracy !== 'number') { throw new Error('Missing or invalid accuracy'); } if (typeof summary.isPassed !== 'boolean') { throw new Error('Missing or invalid isPassed'); } // Time statistics if (!summary.timeStatistics) { throw new Error('Missing timeStatistics'); } if (typeof summary.timeStatistics.totalTime !== 'number') { throw new Error('Missing or invalid timeStatistics.totalTime'); } return '✓ All required summary fields present'; } }, { name: 'Test 11: Questions include all required fields', run: async () => { const response = await axios.get(`${API_URL}/quiz/review/${completedSessionId}`, { headers: { 'Authorization': `Bearer ${userToken}` } }); const { questions } = response.data.data; if (questions.length === 0) throw new Error('Should have questions'); const requiredFields = [ 'id', 'questionText', 'questionType', 'difficulty', 'points', 'explanation', 'order', 'correctAnswer', 'userAnswer', 'isCorrect', 'resultStatus', 'pointsEarned', 'pointsPossible', 'timeTaken', 'answeredAt', 'showExplanation', 'wasAnswered' ]; questions.forEach((q, idx) => { requiredFields.forEach(field => { if (!(field in q)) { throw new Error(`Question ${idx + 1} missing field: ${field}`); } }); }); return '✓ Questions have all required fields'; } }, { name: 'Test 12: Result status correctly marked', run: async () => { const response = await axios.get(`${API_URL}/quiz/review/${completedSessionId}`, { headers: { 'Authorization': `Bearer ${userToken}` } }); const { questions } = response.data.data; questions.forEach((q, idx) => { if (q.wasAnswered) { const expectedStatus = q.isCorrect ? 'correct' : 'incorrect'; if (q.resultStatus !== expectedStatus) { throw new Error( `Question ${idx + 1} has wrong resultStatus: expected ${expectedStatus}, got ${q.resultStatus}` ); } } else { if (q.resultStatus !== 'unanswered') { throw new Error(`Question ${idx + 1} should have resultStatus 'unanswered'`); } } }); return '✓ Result status correctly marked'; } }, { name: 'Test 13: Explanations always shown in review', run: async () => { const response = await axios.get(`${API_URL}/quiz/review/${completedSessionId}`, { headers: { 'Authorization': `Bearer ${userToken}` } }); const { questions } = response.data.data; questions.forEach((q, idx) => { if (q.showExplanation !== true) { throw new Error(`Question ${idx + 1} should have showExplanation=true`); } // Explanation field should exist (can be null if not provided) if (!('explanation' in q)) { throw new Error(`Question ${idx + 1} missing explanation field`); } }); return '✓ Explanations shown for all questions'; } }, { name: 'Test 14: Points tracking accurate', run: async () => { const response = await axios.get(`${API_URL}/quiz/review/${completedSessionId}`, { headers: { 'Authorization': `Bearer ${userToken}` } }); const { summary, questions } = response.data.data; // Calculate points from questions let totalPointsPossible = 0; let totalPointsEarned = 0; questions.forEach(q => { totalPointsPossible += q.pointsPossible; totalPointsEarned += q.pointsEarned; // Points earned should match: correct answers get full points, incorrect get 0 if (q.wasAnswered) { const expectedPoints = q.isCorrect ? q.pointsPossible : 0; if (q.pointsEarned !== expectedPoints) { throw new Error( `Question points mismatch: expected ${expectedPoints}, got ${q.pointsEarned}` ); } } }); // Totals should match summary if (totalPointsEarned !== summary.score.earned) { throw new Error( `Score mismatch: calculated ${totalPointsEarned}, summary shows ${summary.score.earned}` ); } return '✓ Points tracking accurate'; } }, { name: 'Test 15: Time statistics calculated correctly', run: async () => { const response = await axios.get(`${API_URL}/quiz/review/${completedSessionId}`, { headers: { 'Authorization': `Bearer ${userToken}` } }); const { summary, questions } = response.data.data; // Calculate total time from questions let calculatedTotalTime = 0; let answeredCount = 0; questions.forEach(q => { if (q.wasAnswered && q.timeTaken) { calculatedTotalTime += q.timeTaken; answeredCount++; } }); // Check total time if (calculatedTotalTime !== summary.timeStatistics.totalTime) { throw new Error( `Total time mismatch: calculated ${calculatedTotalTime}, summary shows ${summary.timeStatistics.totalTime}` ); } // Check average const expectedAverage = answeredCount > 0 ? Math.round(calculatedTotalTime / answeredCount) : 0; if (expectedAverage !== summary.timeStatistics.averageTimePerQuestion) { throw new Error( `Average time mismatch: expected ${expectedAverage}, got ${summary.timeStatistics.averageTimePerQuestion}` ); } return '✓ Time statistics accurate'; } }, { name: 'Test 16: Multiple choice options have feedback', run: async () => { const response = await axios.get(`${API_URL}/quiz/review/${completedSessionId}`, { headers: { 'Authorization': `Bearer ${userToken}` } }); const { questions } = response.data.data; const mcQuestions = questions.filter(q => q.questionType === 'multiple'); if (mcQuestions.length === 0) { console.log(' Note: No multiple choice questions in this quiz'); return '✓ Test skipped (no multiple choice questions)'; } mcQuestions.forEach((q, idx) => { if (!Array.isArray(q.options)) { throw new Error(`MC Question ${idx + 1} should have options array`); } q.options.forEach((opt, optIdx) => { if (!('isCorrect' in opt)) { throw new Error(`Option ${optIdx + 1} missing isCorrect field`); } if (!('isSelected' in opt)) { throw new Error(`Option ${optIdx + 1} missing isSelected field`); } if (!('feedback' in opt)) { throw new Error(`Option ${optIdx + 1} missing feedback field`); } }); }); return '✓ Multiple choice options have feedback'; } } ]; // Run all tests async function runTests() { console.log('='.repeat(60)); console.log('QUIZ REVIEW API TESTS'); console.log('='.repeat(60) + '\n'); await setup(); console.log('Running tests...\n'); let passed = 0; let failed = 0; for (const test of tests) { try { const result = await test.run(); console.log(`${result}`); passed++; await delay(500); // Delay between tests } catch (error) { console.log(`✗ ${test.name}`); console.log(` Error: ${error.response?.data?.message || error.message}`); if (error.response?.data && process.env.VERBOSE) { console.log(` Response:`, JSON.stringify(error.response.data, null, 2)); } failed++; } } console.log('\n' + '='.repeat(60)); console.log(`RESULTS: ${passed} passed, ${failed} failed out of ${tests.length} tests`); console.log('='.repeat(60)); process.exit(failed > 0 ? 1 : 0); } runTests();