/** * Test Script: Start Quiz Session API * * Tests: * - Start quiz as authenticated user * - Start quiz as guest user * - Guest quiz limit enforcement * - Category validation * - Question selection and randomization * - Various quiz types and difficulties */ const axios = require('axios'); require('dotenv').config(); const BASE_URL = process.env.API_URL || 'http://localhost:3000'; const API_URL = `${BASE_URL}/api`; // Test users let adminToken = null; let userToken = null; let guestToken = null; let guestId = null; // Test data let testCategoryId = null; let guestCategoryId = null; // Test results const results = { passed: 0, failed: 0, total: 0 }; // Helper function to log test results function logTest(testName, passed, details = '') { results.total++; if (passed) { results.passed++; console.log(`✓ Test ${results.total}: ${testName} - PASSED`); if (details) console.log(` ${details}`); } else { results.failed++; console.log(`✗ Test ${results.total}: ${testName} - FAILED`); if (details) console.log(` ${details}`); } } // Helper to create axios config with auth function authConfig(token) { return { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } }; } // Helper to create axios config with guest token function guestConfig(token) { return { headers: { 'X-Guest-Token': token, 'Content-Type': 'application/json' } }; } async function runTests() { console.log('========================================'); console.log('Testing Start Quiz Session API'); console.log('========================================\n'); try { // ========================================== // Setup: Login and get categories // ========================================== // Login as admin 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 as regular user const timestamp = Date.now(); const userRes = await axios.post(`${API_URL}/auth/register`, { username: `quizuser${timestamp}`, email: `quizuser${timestamp}@test.com`, password: 'Test@123' }); userToken = userRes.data.data.token; console.log('✓ Created and logged in as regular user'); // Start guest session const guestRes = await axios.post(`${API_URL}/guest/start-session`, { deviceId: `test-device-${timestamp}` }); guestToken = guestRes.data.data.sessionToken; guestId = guestRes.data.data.guestId; console.log('✓ Started guest session\n'); // Get test categories - use JavaScript which has questions const categoriesRes = await axios.get(`${API_URL}/categories`, authConfig(adminToken)); guestCategoryId = categoriesRes.data.data.find(c => c.name === 'JavaScript')?.id; // JavaScript has questions testCategoryId = guestCategoryId; // Use same category for all tests since it has questions console.log(`✓ Using test category: ${testCategoryId} (JavaScript - has questions)\n`); // ========================================== // AUTHENTICATED USER QUIZ TESTS // ========================================== // Test 1: User starts quiz successfully try { const res = await axios.post(`${API_URL}/quiz/start`, { categoryId: testCategoryId, questionCount: 5, difficulty: 'medium', quizType: 'practice' }, authConfig(userToken)); const passed = res.status === 201 && res.data.success === true && res.data.data.sessionId && res.data.data.questions.length > 0 // At least some questions && res.data.data.difficulty === 'medium'; logTest('User starts quiz successfully', passed, passed ? `Session ID: ${res.data.data.sessionId}, ${res.data.data.questions.length} questions` : `Response: ${JSON.stringify(res.data)}`); } catch (error) { logTest('User starts quiz successfully', false, error.response?.data?.message || error.message); } // Test 2: User starts quiz with mixed difficulty try { const res = await axios.post(`${API_URL}/quiz/start`, { categoryId: testCategoryId, questionCount: 10, difficulty: 'mixed', quizType: 'practice' }, authConfig(userToken)); const passed = res.status === 201 && res.data.data.difficulty === 'mixed' && res.data.data.questions.length <= 10; logTest('User starts quiz with mixed difficulty', passed, passed ? `Got ${res.data.data.questions.length} mixed difficulty questions` : `Response: ${JSON.stringify(res.data)}`); } catch (error) { logTest('User starts quiz with mixed difficulty', false, error.response?.data?.message || error.message); } // Test 3: User starts timed quiz (has time limit) try { const res = await axios.post(`${API_URL}/quiz/start`, { categoryId: testCategoryId, questionCount: 5, difficulty: 'mixed', // Use mixed to ensure we get questions quizType: 'timed' }, authConfig(userToken)); const passed = res.status === 201 && res.data.data.quizType === 'timed' && res.data.data.timeLimit !== null && res.data.data.timeLimit === res.data.data.questions.length * 2; // 2 min per question logTest('User starts timed quiz with time limit', passed, passed ? `Time limit: ${res.data.data.timeLimit} minutes for ${res.data.data.questions.length} questions` : `Response: ${JSON.stringify(res.data)}`); } catch (error) { logTest('User starts timed quiz with time limit', false, error.response?.data?.message || error.message); } // Test 4: Questions don't expose correct answers try { const res = await axios.post(`${API_URL}/quiz/start`, { categoryId: guestCategoryId, questionCount: 3, difficulty: 'easy', quizType: 'practice' }, authConfig(userToken)); const hasCorrectAnswer = res.data.data.questions.some(q => q.correctAnswer !== undefined); const passed = res.status === 201 && !hasCorrectAnswer; logTest('Questions don\'t expose correct answers', passed, passed ? 'Correct answers properly hidden' : 'Correct answers exposed in response!'); } catch (error) { logTest('Questions don\'t expose correct answers', false, error.response?.data?.message || error.message); } // Test 5: Response includes category info try { const res = await axios.post(`${API_URL}/quiz/start`, { categoryId: testCategoryId, questionCount: 5, difficulty: 'mixed', // Use mixed to ensure we get questions quizType: 'practice' }, authConfig(userToken)); const passed = res.status === 201 && res.data.data.category && res.data.data.category.name && res.data.data.category.icon && res.data.data.category.color; logTest('Response includes category info', passed, passed ? `Category: ${res.data.data.category.name}` : `Response: ${JSON.stringify(res.data)}`); } catch (error) { logTest('Response includes category info', false, error.response?.data?.message || error.message); } // Test 6: Total points calculated correctly try { const res = await axios.post(`${API_URL}/quiz/start`, { categoryId: testCategoryId, questionCount: 5, difficulty: 'medium', quizType: 'practice' }, authConfig(userToken)); const calculatedPoints = res.data.data.questions.reduce((sum, q) => sum + q.points, 0); const passed = res.status === 201 && res.data.data.totalPoints === calculatedPoints; logTest('Total points calculated correctly', passed, passed ? `Total: ${res.data.data.totalPoints} points` : `Expected ${calculatedPoints}, got ${res.data.data.totalPoints}`); } catch (error) { logTest('Total points calculated correctly', false, error.response?.data?.message || error.message); } // ========================================== // GUEST USER QUIZ TESTS // ========================================== console.log('\n--- Testing Guest Quiz Sessions ---\n'); // Test 7: Guest starts quiz in accessible category try { const res = await axios.post(`${API_URL}/quiz/start`, { categoryId: guestCategoryId, questionCount: 5, difficulty: 'easy', quizType: 'practice' }, guestConfig(guestToken)); const passed = res.status === 201 && res.data.success === true && res.data.data.questions.length === 5; logTest('Guest starts quiz in accessible category', passed, passed ? `Quiz started with ${res.data.data.questions.length} questions` : `Response: ${JSON.stringify(res.data)}`); } catch (error) { logTest('Guest starts quiz in accessible category', false, error.response?.data?.message || error.message); } // Test 8: Guest blocked from non-accessible category try { // Find a non-guest accessible category const nonGuestCategory = categoriesRes.data.data.find(c => !c.guestAccessible)?.id; if (nonGuestCategory) { const res = await axios.post(`${API_URL}/quiz/start`, { categoryId: nonGuestCategory, // Non-guest accessible category questionCount: 5, difficulty: 'easy', quizType: 'practice' }, guestConfig(guestToken)); logTest('Guest blocked from non-accessible category', false, 'Should have returned 403'); } else { logTest('Guest blocked from non-accessible category', true, 'Skipped - no non-guest categories available'); } } catch (error) { const passed = error.response?.status === 403; logTest('Guest blocked from non-accessible category', passed, passed ? 'Correctly blocked with 403' : `Status: ${error.response?.status}`); } // Test 9: Guest quiz count incremented try { // Get initial count const beforeRes = await axios.get(`${API_URL}/guest/quiz-limit`, guestConfig(guestToken)); const beforeCount = beforeRes.data.data.quizLimit.quizzesAttempted; // Start another quiz await axios.post(`${API_URL}/quiz/start`, { categoryId: guestCategoryId, questionCount: 3, difficulty: 'medium', quizType: 'practice' }, guestConfig(guestToken)); // Check count after const afterRes = await axios.get(`${API_URL}/guest/quiz-limit`, guestConfig(guestToken)); const afterCount = afterRes.data.data.quizLimit.quizzesAttempted; const passed = afterCount === beforeCount + 1; logTest('Guest quiz count incremented', passed, passed ? `Count: ${beforeCount} → ${afterCount}` : `Expected ${beforeCount + 1}, got ${afterCount}`); } catch (error) { logTest('Guest quiz count incremented', false, error.response?.data?.message || error.message); } // Test 10: Guest quiz limit enforced (reach limit) try { // Start quiz until limit reached const limitRes = await axios.get(`${API_URL}/guest/quiz-limit`, guestConfig(guestToken)); const remaining = limitRes.data.data.quizLimit.quizzesRemaining; // Try to start more quizzes than remaining for (let i = 0; i < remaining; i++) { await axios.post(`${API_URL}/quiz/start`, { categoryId: guestCategoryId, questionCount: 1, difficulty: 'easy', quizType: 'practice' }, guestConfig(guestToken)); } // This should fail try { await axios.post(`${API_URL}/quiz/start`, { categoryId: guestCategoryId, questionCount: 1, difficulty: 'easy', quizType: 'practice' }, guestConfig(guestToken)); logTest('Guest quiz limit enforced', false, 'Should have blocked at limit'); } catch (limitError) { const passed = limitError.response?.status === 403; logTest('Guest quiz limit enforced', passed, passed ? 'Correctly blocked when limit reached' : `Status: ${limitError.response?.status}`); } } catch (error) { logTest('Guest quiz limit enforced', false, error.response?.data?.message || error.message); } // ========================================== // VALIDATION TESTS // ========================================== console.log('\n--- Testing Validation ---\n'); // Test 11: Missing category ID returns 400 try { const res = await axios.post(`${API_URL}/quiz/start`, { questionCount: 5, difficulty: 'easy' }, authConfig(userToken)); logTest('Missing category ID returns 400', false, 'Should have returned 400'); } catch (error) { const passed = error.response?.status === 400; logTest('Missing category ID returns 400', passed, passed ? 'Correctly rejected missing category' : `Status: ${error.response?.status}`); } // Test 12: Invalid category UUID returns 400 try { const res = await axios.post(`${API_URL}/quiz/start`, { categoryId: 'invalid-uuid', questionCount: 5, difficulty: 'easy' }, authConfig(userToken)); logTest('Invalid category UUID returns 400', false, 'Should have returned 400'); } catch (error) { const passed = error.response?.status === 400; logTest('Invalid category UUID returns 400', passed, passed ? 'Correctly rejected invalid UUID' : `Status: ${error.response?.status}`); } // Test 13: Non-existent category returns 404 try { const fakeUuid = '00000000-0000-0000-0000-000000000000'; const res = await axios.post(`${API_URL}/quiz/start`, { categoryId: fakeUuid, questionCount: 5, difficulty: 'easy' }, authConfig(userToken)); logTest('Non-existent category returns 404', false, 'Should have returned 404'); } catch (error) { const passed = error.response?.status === 404; logTest('Non-existent category returns 404', passed, passed ? 'Correctly returned 404' : `Status: ${error.response?.status}`); } // Test 14: Invalid question count rejected try { const res = await axios.post(`${API_URL}/quiz/start`, { categoryId: testCategoryId, questionCount: 100, // Exceeds max of 50 difficulty: 'easy' }, authConfig(userToken)); logTest('Invalid question count rejected', false, 'Should have returned 400'); } catch (error) { const passed = error.response?.status === 400; logTest('Invalid question count rejected', passed, passed ? 'Correctly rejected count > 50' : `Status: ${error.response?.status}`); } // Test 15: Invalid difficulty rejected try { const res = await axios.post(`${API_URL}/quiz/start`, { categoryId: testCategoryId, questionCount: 5, difficulty: 'extreme' }, authConfig(userToken)); logTest('Invalid difficulty rejected', false, 'Should have returned 400'); } catch (error) { const passed = error.response?.status === 400; logTest('Invalid difficulty rejected', passed, passed ? 'Correctly rejected invalid difficulty' : `Status: ${error.response?.status}`); } // Test 16: Invalid quiz type rejected try { const res = await axios.post(`${API_URL}/quiz/start`, { categoryId: testCategoryId, questionCount: 5, difficulty: 'easy', quizType: 'invalid' }, authConfig(userToken)); logTest('Invalid quiz type rejected', false, 'Should have returned 400'); } catch (error) { const passed = error.response?.status === 400; logTest('Invalid quiz type rejected', passed, passed ? 'Correctly rejected invalid quiz type' : `Status: ${error.response?.status}`); } // Test 17: Unauthenticated request blocked try { const res = await axios.post(`${API_URL}/quiz/start`, { categoryId: testCategoryId, questionCount: 5, difficulty: 'easy' }); logTest('Unauthenticated request blocked', false, 'Should have returned 401'); } catch (error) { const passed = error.response?.status === 401; logTest('Unauthenticated request blocked', passed, passed ? 'Correctly blocked with 401' : `Status: ${error.response?.status}`); } // Test 18: Default values applied correctly try { const res = await axios.post(`${API_URL}/quiz/start`, { categoryId: testCategoryId // No questionCount, difficulty, or quizType specified }, authConfig(userToken)); const passed = res.status === 201 && res.data.data.totalQuestions <= 10 // Up to default question count (might be less if not enough questions) && res.data.data.difficulty === 'mixed' // Default difficulty && res.data.data.quizType === 'practice'; // Default quiz type logTest('Default values applied correctly', passed, passed ? `Defaults applied: ${res.data.data.totalQuestions} questions, mixed difficulty, practice type` : `Response: ${JSON.stringify(res.data)}`); } catch (error) { logTest('Default values applied correctly', false, error.response?.data?.message || error.message); } // Test 19: Questions have proper structure try { const res = await axios.post(`${API_URL}/quiz/start`, { categoryId: testCategoryId, questionCount: 3, difficulty: 'easy' }, authConfig(userToken)); const firstQuestion = res.data.data.questions[0]; const passed = res.status === 201 && firstQuestion.id && firstQuestion.questionText && firstQuestion.questionType && firstQuestion.difficulty && firstQuestion.points && firstQuestion.order && !firstQuestion.correctAnswer; // Should not be exposed logTest('Questions have proper structure', passed, passed ? 'All required fields present, correctAnswer hidden' : `Question: ${JSON.stringify(firstQuestion)}`); } catch (error) { logTest('Questions have proper structure', false, error.response?.data?.message || error.message); } // Test 20: Question order is sequential try { const res = await axios.post(`${API_URL}/quiz/start`, { categoryId: testCategoryId, questionCount: 5, difficulty: 'medium' }, authConfig(userToken)); const orders = res.data.data.questions.map(q => q.order); const isSequential = orders.every((order, index) => order === index + 1); const passed = res.status === 201 && isSequential; logTest('Question order is sequential', passed, passed ? `Orders: ${orders.join(', ')}` : `Orders: ${orders.join(', ')}`); } catch (error) { logTest('Question order is sequential', false, error.response?.data?.message || error.message); } } catch (error) { console.error('\n❌ Fatal error during tests:', error.message); console.error('Error details:', error); if (error.response) { console.error('Response:', error.response.data); } if (error.stack) { console.error('Stack:', error.stack); } } // ========================================== // Summary // ========================================== console.log('\n========================================'); console.log('Test Summary'); console.log('========================================'); console.log(`Passed: ${results.passed}`); console.log(`Failed: ${results.failed}`); console.log(`Total: ${results.total}`); console.log('========================================\n'); if (results.failed === 0) { console.log('✓ All tests passed!\n'); process.exit(0); } else { console.log(`✗ ${results.failed} test(s) failed.\n`); process.exit(1); } } // Run tests runTests();