/** * Complete Quiz Session API Tests * Tests for POST /api/quiz/complete endpoint */ const axios = require('axios'); const API_URL = 'http://localhost:3000/api'; // Test configuration let adminToken = null; let user1Token = null; let user2Token = null; let guestToken = null; let guestSessionId = null; // Helper function to create auth config const authConfig = (token) => ({ headers: { 'Authorization': `Bearer ${token}` } }); // Helper function for guest auth config const guestAuthConfig = (token) => ({ headers: { 'X-Guest-Token': token } }); // Logging helper const log = (message, data = null) => { console.log(`\n${message}`); if (data) { console.log(JSON.stringify(data, null, 2)); } }; // Test setup async function setup() { try { // Login as admin (to get categories) const adminLogin = await axios.post(`${API_URL}/auth/login`, { email: 'admin@quiz.com', password: 'Admin@123' }); adminToken = adminLogin.data.data.token; console.log('✓ Logged in as admin'); // Register and login test users const timestamp = Date.now(); // User 1 await axios.post(`${API_URL}/auth/register`, { username: `testcomplete1${timestamp}`, email: `testcomplete1${timestamp}@test.com`, password: 'Test@123' }); const user1Login = await axios.post(`${API_URL}/auth/login`, { email: `testcomplete1${timestamp}@test.com`, password: 'Test@123' }); user1Token = user1Login.data.data.token; console.log('✓ Logged in as testuser1'); // User 2 await axios.post(`${API_URL}/auth/register`, { username: `testcomplete2${timestamp}`, email: `testcomplete2${timestamp}@test.com`, password: 'Test@123' }); const user2Login = await axios.post(`${API_URL}/auth/login`, { email: `testcomplete2${timestamp}@test.com`, password: 'Test@123' }); user2Token = user2Login.data.data.token; console.log('✓ Logged in as testuser2'); // Start guest session const guestResponse = await axios.post(`${API_URL}/guest/start-session`, { deviceId: `test-device-${timestamp}` }); guestToken = guestResponse.data.data.sessionToken; guestSessionId = guestResponse.data.data.guestId; console.log('✓ Started guest session'); } catch (error) { console.error('Setup failed:', error.response?.data || error.message); process.exit(1); } } // Test results tracking let testResults = { passed: 0, failed: 0, total: 0 }; // Test runner async function runTest(testName, testFn) { testResults.total++; try { await testFn(); console.log(`✓ ${testName} - PASSED`); testResults.passed++; } catch (error) { console.log(`✗ ${testName} - FAILED`); console.log(` ${error.message}`); testResults.failed++; } // Add delay to avoid rate limiting await new Promise(resolve => setTimeout(resolve, 500)); } // Helper: Create and complete a quiz session async function createAndAnswerQuiz(token, isGuest = false) { // Get categories const categoriesResponse = await axios.get(`${API_URL}/categories`, isGuest ? guestAuthConfig(token) : authConfig(token) ); const categories = categoriesResponse.data.data; const category = categories.find(c => c.guestAccessible) || categories[0]; // Start quiz const quizResponse = await axios.post( `${API_URL}/quiz/start`, { categoryId: category.id, questionCount: 3, difficulty: 'mixed', quizType: 'practice' }, isGuest ? guestAuthConfig(token) : authConfig(token) ); const sessionId = quizResponse.data.data.sessionId; const questions = quizResponse.data.data.questions; // Submit answers for all questions for (const question of questions) { await axios.post( `${API_URL}/quiz/submit`, { quizSessionId: sessionId, questionId: question.id, userAnswer: 'a', // Use consistent answer timeTaken: 5 }, isGuest ? guestAuthConfig(token) : authConfig(token) ); } return { sessionId, totalQuestions: questions.length }; } // ==================== TESTS ==================== async function runTests() { console.log('\n========================================'); console.log('Testing Complete Quiz Session API'); console.log('========================================\n'); await setup(); // Test 1: Complete quiz with all questions answered await runTest('Complete quiz returns detailed results', async () => { const { sessionId } = await createAndAnswerQuiz(user1Token); const response = await axios.post( `${API_URL}/quiz/complete`, { sessionId }, authConfig(user1Token) ); if (response.status !== 200) throw new Error(`Expected 200, got ${response.status}`); if (!response.data.success) throw new Error('Response success should be true'); if (!response.data.data) throw new Error('Missing data in response'); const results = response.data.data; // Validate structure if (!results.sessionId) throw new Error('Missing sessionId'); if (!results.status) throw new Error('Missing status'); if (!results.category) throw new Error('Missing category'); if (!results.score) throw new Error('Missing score'); if (!results.questions) throw new Error('Missing questions'); if (!results.time) throw new Error('Missing time'); if (typeof results.accuracy !== 'number') throw new Error('Missing or invalid accuracy'); if (typeof results.isPassed !== 'boolean') throw new Error('Missing or invalid isPassed'); // Validate score structure if (typeof results.score.earned !== 'number') { console.log(' Score object:', JSON.stringify(results.score, null, 2)); throw new Error(`Missing or invalid score.earned (type: ${typeof results.score.earned}, value: ${results.score.earned})`); } if (typeof results.score.total !== 'number') throw new Error('Missing score.total'); if (typeof results.score.percentage !== 'number') throw new Error('Missing score.percentage'); // Validate questions structure if (results.questions.total !== 3) throw new Error('Expected 3 total questions'); if (results.questions.answered !== 3) throw new Error('Expected 3 answered questions'); // Validate time structure if (!results.time.started) throw new Error('Missing time.started'); if (!results.time.completed) throw new Error('Missing time.completed'); if (typeof results.time.taken !== 'number') throw new Error('Missing time.taken'); console.log(` Score: ${results.score.earned}/${results.score.total} (${results.score.percentage}%)`); console.log(` Accuracy: ${results.accuracy}%`); console.log(` Passed: ${results.isPassed}`); }); // Test 2: Guest can complete quiz await runTest('Guest can complete quiz', async () => { const { sessionId } = await createAndAnswerQuiz(guestToken, true); const response = await axios.post( `${API_URL}/quiz/complete`, { sessionId }, guestAuthConfig(guestToken) ); if (response.status !== 200) throw new Error(`Expected 200, got ${response.status}`); if (!response.data.success) throw new Error('Response success should be true'); if (!response.data.data.sessionId) throw new Error('Missing sessionId in results'); }); // Test 3: Percentage calculation is correct await runTest('Percentage calculated correctly', async () => { const { sessionId } = await createAndAnswerQuiz(user1Token); const response = await axios.post( `${API_URL}/quiz/complete`, { sessionId }, authConfig(user1Token) ); const results = response.data.data; const expectedPercentage = Math.round((results.score.earned / results.score.total) * 100); if (results.score.percentage !== expectedPercentage) { throw new Error(`Expected ${expectedPercentage}%, got ${results.score.percentage}%`); } }); // Test 4: Pass/fail determination (70% threshold) await runTest('Pass/fail determination works', async () => { const { sessionId } = await createAndAnswerQuiz(user1Token); const response = await axios.post( `${API_URL}/quiz/complete`, { sessionId }, authConfig(user1Token) ); const results = response.data.data; const expectedPassed = results.score.percentage >= 70; if (results.isPassed !== expectedPassed) { throw new Error(`Expected isPassed=${expectedPassed}, got ${results.isPassed}`); } }); // Test 5: Time tracking works await runTest('Time tracking accurate', async () => { const { sessionId } = await createAndAnswerQuiz(user1Token); // Wait 2 seconds before completing await new Promise(resolve => setTimeout(resolve, 2000)); const response = await axios.post( `${API_URL}/quiz/complete`, { sessionId }, authConfig(user1Token) ); const results = response.data.data; if (results.time.taken < 2) { throw new Error(`Expected at least 2 seconds, got ${results.time.taken}`); } if (results.time.taken > 60) { throw new Error(`Time taken seems too long: ${results.time.taken}s`); } }); console.log('\n========================================'); console.log('Testing Validation'); console.log('========================================\n'); // Test 6: Missing session ID returns 400 await runTest('Missing session ID returns 400', async () => { try { await axios.post( `${API_URL}/quiz/complete`, {}, authConfig(user1Token) ); throw new Error('Should have thrown error'); } catch (error) { if (error.response?.status !== 400) { throw new Error(`Expected 400, got ${error.response?.status}`); } } }); // Test 7: Invalid session UUID returns 400 await runTest('Invalid session UUID returns 400', async () => { try { await axios.post( `${API_URL}/quiz/complete`, { sessionId: 'invalid-uuid' }, authConfig(user1Token) ); throw new Error('Should have thrown error'); } catch (error) { if (error.response?.status !== 400) { throw new Error(`Expected 400, got ${error.response?.status}`); } } }); // Test 8: Non-existent session returns 404 await runTest('Non-existent session returns 404', async () => { try { await axios.post( `${API_URL}/quiz/complete`, { sessionId: '00000000-0000-0000-0000-000000000000' }, authConfig(user1Token) ); throw new Error('Should have thrown error'); } catch (error) { if (error.response?.status !== 404) { throw new Error(`Expected 404, got ${error.response?.status}`); } } }); // Test 9: Cannot complete another user's session await runTest('Cannot complete another user\'s session', async () => { const { sessionId } = await createAndAnswerQuiz(user1Token); try { await axios.post( `${API_URL}/quiz/complete`, { sessionId }, authConfig(user2Token) // Different user ); throw new Error('Should have thrown error'); } catch (error) { if (error.response?.status !== 403) { throw new Error(`Expected 403, got ${error.response?.status}`); } } }); // Test 10: Cannot complete already completed session await runTest('Cannot complete already completed session', async () => { const { sessionId } = await createAndAnswerQuiz(user1Token); // Complete once await axios.post( `${API_URL}/quiz/complete`, { sessionId }, authConfig(user1Token) ); // Try to complete again try { await axios.post( `${API_URL}/quiz/complete`, { sessionId }, authConfig(user1Token) ); throw new Error('Should have thrown error'); } catch (error) { if (error.response?.status !== 400) { throw new Error(`Expected 400, got ${error.response?.status}`); } if (!error.response.data.message.includes('already completed')) { throw new Error('Error message should mention already completed'); } } }); // Test 11: Unauthenticated request blocked await runTest('Unauthenticated request blocked', async () => { const { sessionId } = await createAndAnswerQuiz(user1Token); try { await axios.post( `${API_URL}/quiz/complete`, { sessionId } // No auth headers ); throw new Error('Should have thrown error'); } catch (error) { if (error.response?.status !== 401) { throw new Error(`Expected 401, got ${error.response?.status}`); } } }); console.log('\n========================================'); console.log('Testing Partial Completion'); console.log('========================================\n'); // Test 12: Can complete with unanswered questions await runTest('Can complete with unanswered questions', async () => { // Get category with most questions const categoriesResponse = await axios.get(`${API_URL}/categories`, authConfig(user1Token)); const category = categoriesResponse.data.data.sort((a, b) => b.questionCount - a.questionCount)[0]; // Start quiz with requested questions (but we'll only answer some) const requestedCount = Math.min(5, category.questionCount); // Don't request more than available if (requestedCount < 3) { console.log(' Skipping - not enough questions in category'); return; // Skip if not enough questions } const quizResponse = await axios.post( `${API_URL}/quiz/start`, { categoryId: category.id, questionCount: requestedCount, difficulty: 'mixed', quizType: 'practice' }, authConfig(user1Token) ); const sessionId = quizResponse.data.data.sessionId; const questions = quizResponse.data.data.questions; const actualCount = questions.length; if (actualCount < 3) { console.log(' Skipping - not enough questions returned'); return; } // Answer only 2 questions (leaving others unanswered) await axios.post( `${API_URL}/quiz/submit`, { quizSessionId: sessionId, questionId: questions[0].id, userAnswer: 'a', timeTaken: 5 }, authConfig(user1Token) ); await axios.post( `${API_URL}/quiz/submit`, { quizSessionId: sessionId, questionId: questions[1].id, userAnswer: 'b', timeTaken: 5 }, authConfig(user1Token) ); // Complete quiz const response = await axios.post( `${API_URL}/quiz/complete`, { sessionId }, authConfig(user1Token) ); const results = response.data.data; if (results.questions.total !== actualCount) { throw new Error(`Expected ${actualCount} total questions, got ${results.questions.total}`); } if (results.questions.answered !== 2) throw new Error('Expected 2 answered questions'); if (results.questions.unanswered !== actualCount - 2) { throw new Error(`Expected ${actualCount - 2} unanswered questions, got ${results.questions.unanswered}`); } }); // Test 13: Status updated to completed await runTest('Session status updated to completed', async () => { const { sessionId } = await createAndAnswerQuiz(user1Token); const response = await axios.post( `${API_URL}/quiz/complete`, { sessionId }, authConfig(user1Token) ); const results = response.data.data; if (results.status !== 'completed') { throw new Error(`Expected status 'completed', got '${results.status}'`); } }); // Test 14: Category info included in results await runTest('Category info included in results', async () => { const { sessionId } = await createAndAnswerQuiz(user1Token); const response = await axios.post( `${API_URL}/quiz/complete`, { sessionId }, authConfig(user1Token) ); const results = response.data.data; if (!results.category.id) throw new Error('Missing category.id'); if (!results.category.name) throw new Error('Missing category.name'); if (!results.category.slug) throw new Error('Missing category.slug'); }); // Test 15: Correct/incorrect counts accurate await runTest('Correct/incorrect counts accurate', async () => { const { sessionId, totalQuestions } = await createAndAnswerQuiz(user1Token); const response = await axios.post( `${API_URL}/quiz/complete`, { sessionId }, authConfig(user1Token) ); const results = response.data.data; const sumCheck = results.questions.correct + results.questions.incorrect + results.questions.unanswered; if (sumCheck !== totalQuestions) { throw new Error(`Question counts don't add up: ${sumCheck} !== ${totalQuestions}`); } }); // Print summary console.log('\n========================================'); console.log('Test Summary'); console.log('========================================\n'); console.log(`Passed: ${testResults.passed}`); console.log(`Failed: ${testResults.failed}`); console.log(`Total: ${testResults.total}`); console.log('========================================\n'); process.exit(testResults.failed > 0 ? 1 : 0); } // Run all tests runTests().catch(error => { console.error('Test execution failed:', error); process.exit(1); });