const axios = require('axios'); const API_URL = 'http://localhost:3000/api'; // Admin credentials (from seeder) const adminUser = { email: 'admin@quiz.com', password: 'Admin@123' }; // Regular user credentials (with timestamp to avoid conflicts) const timestamp = Date.now(); const regularUser = { username: `testuser${timestamp}`, email: `testuser${timestamp}@example.com`, password: 'Test@123' }; // ANSI color codes const colors = { reset: '\x1b[0m', green: '\x1b[32m', red: '\x1b[31m', yellow: '\x1b[33m', blue: '\x1b[34m', cyan: '\x1b[36m', magenta: '\x1b[35m' }; let adminToken = null; let regularUserToken = null; let testCategoryId = null; let testQuestionId = null; // Test counters let totalTests = 0; let passedTests = 0; let failedTests = 0; /** * Helper: Log test result */ function logTestResult(testName, passed, error = null) { totalTests++; if (passed) { passedTests++; console.log(`${colors.green}✓ ${testName}${colors.reset}`); } else { failedTests++; console.log(`${colors.red}✗ ${testName}${colors.reset}`); if (error) { console.log(` ${colors.red}Error: ${error}${colors.reset}`); } } } /** * Login as admin */ async function loginAdmin() { try { const response = await axios.post(`${API_URL}/auth/login`, adminUser); adminToken = response.data.data.token; console.log(`${colors.cyan}✓ Logged in as admin${colors.reset}`); return adminToken; } catch (error) { console.error(`${colors.red}✗ Failed to login as admin:${colors.reset}`, error.response?.data || error.message); throw error; } } /** * Create and login regular user */ async function createRegularUser() { try { const registerResponse = await axios.post(`${API_URL}/auth/register`, regularUser); regularUserToken = registerResponse.data.data.token; console.log(`${colors.cyan}✓ Created and logged in as regular user${colors.reset}`); return regularUserToken; } catch (error) { console.error(`${colors.red}✗ Failed to create regular user:${colors.reset}`, error.response?.data || error.message); throw error; } } /** * Get first active category */ async function getFirstCategory() { try { const response = await axios.get(`${API_URL}/categories`, { headers: { 'Authorization': `Bearer ${adminToken}` } }); if (response.data.data && response.data.data.length > 0) { testCategoryId = response.data.data[0].id; console.log(`${colors.cyan}✓ Got test category: ${testCategoryId}${colors.reset}`); return testCategoryId; } throw new Error('No categories found'); } catch (error) { console.error(`${colors.red}✗ Failed to get category:${colors.reset}`, error.response?.data || error.message); throw error; } } /** * Create a test question */ async function createTestQuestion() { try { const questionData = { questionText: 'What is the capital of France?', questionType: 'multiple', options: [ { id: 'a', text: 'Paris' }, { id: 'b', text: 'London' }, { id: 'c', text: 'Berlin' }, { id: 'd', text: 'Madrid' } ], correctAnswer: 'a', difficulty: 'easy', points: 10, explanation: 'Paris is the capital and largest city of France.', categoryId: testCategoryId, tags: ['geography', 'capitals'], keywords: ['france', 'paris', 'capital'] }; const response = await axios.post(`${API_URL}/admin/questions`, questionData, { headers: { 'Authorization': `Bearer ${adminToken}` } }); testQuestionId = response.data.data.id; console.log(`${colors.cyan}✓ Created test question: ${testQuestionId}${colors.reset}`); return testQuestionId; } catch (error) { console.error(`${colors.red}✗ Failed to create test question:${colors.reset}`); console.error('Status:', error.response?.status); console.error('Data:', JSON.stringify(error.response?.data, null, 2)); console.error('Message:', error.message); throw error; } } // ============================================================================ // TEST SUITE: UPDATE QUESTION ENDPOINT // ============================================================================ /** * Test 1: Unauthenticated request cannot update question (401) */ async function test01_UnauthenticatedCannotUpdate() { console.log(`\n${colors.blue}Test 1: Unauthenticated request cannot update question${colors.reset}`); try { const updateData = { questionText: 'Updated question text' }; await axios.put(`${API_URL}/admin/questions/${testQuestionId}`, updateData); logTestResult('Test 1: Unauthenticated request cannot update question', false, 'Should have returned 401'); } catch (error) { const status = error.response?.status; const passed = status === 401; logTestResult('Test 1: Unauthenticated request cannot update question', passed, passed ? null : `Expected 401, got ${status}`); } } /** * Test 2: Regular user cannot update question (403) */ async function test02_UserCannotUpdate() { console.log(`\n${colors.blue}Test 2: Regular user cannot update question${colors.reset}`); try { const updateData = { questionText: 'Updated question text' }; await axios.put(`${API_URL}/admin/questions/${testQuestionId}`, updateData, { headers: { 'Authorization': `Bearer ${regularUserToken}` } }); logTestResult('Test 2: Regular user cannot update question', false, 'Should have returned 403'); } catch (error) { const status = error.response?.status; const passed = status === 403; logTestResult('Test 2: Regular user cannot update question', passed, passed ? null : `Expected 403, got ${status}`); } } /** * Test 3: Admin can update question text */ async function test03_UpdateQuestionText() { console.log(`\n${colors.blue}Test 3: Admin can update question text${colors.reset}`); try { const updateData = { questionText: 'What is the capital city of France?' }; const response = await axios.put(`${API_URL}/admin/questions/${testQuestionId}`, updateData, { headers: { 'Authorization': `Bearer ${adminToken}` } }); const { success, data } = response.data; const passed = success && data.questionText === updateData.questionText && data.id === testQuestionId; logTestResult('Test 3: Admin can update question text', passed, passed ? null : 'Question text not updated correctly'); } catch (error) { logTestResult('Test 3: Admin can update question text', false, error.response?.data?.message || error.message); } } /** * Test 4: Update difficulty level */ async function test04_UpdateDifficulty() { console.log(`\n${colors.blue}Test 4: Update difficulty level${colors.reset}`); try { const updateData = { difficulty: 'medium' }; const response = await axios.put(`${API_URL}/admin/questions/${testQuestionId}`, updateData, { headers: { 'Authorization': `Bearer ${adminToken}` } }); const { success, data } = response.data; const passed = success && data.difficulty === 'medium'; logTestResult('Test 4: Update difficulty level', passed, passed ? null : 'Difficulty not updated correctly'); } catch (error) { logTestResult('Test 4: Update difficulty level', false, error.response?.data?.message || error.message); } } /** * Test 5: Update points */ async function test05_UpdatePoints() { console.log(`\n${colors.blue}Test 5: Update points${colors.reset}`); try { const updateData = { points: 20 }; const response = await axios.put(`${API_URL}/admin/questions/${testQuestionId}`, updateData, { headers: { 'Authorization': `Bearer ${adminToken}` } }); const { success, data } = response.data; const passed = success && data.points === 20; logTestResult('Test 5: Update points', passed, passed ? null : 'Points not updated correctly'); } catch (error) { logTestResult('Test 5: Update points', false, error.response?.data?.message || error.message); } } /** * Test 6: Update explanation */ async function test06_UpdateExplanation() { console.log(`\n${colors.blue}Test 6: Update explanation${colors.reset}`); try { const updateData = { explanation: 'Paris has been the capital of France since the 12th century.' }; const response = await axios.put(`${API_URL}/admin/questions/${testQuestionId}`, updateData, { headers: { 'Authorization': `Bearer ${adminToken}` } }); const { success, data } = response.data; const passed = success && data.explanation === updateData.explanation; logTestResult('Test 6: Update explanation', passed, passed ? null : 'Explanation not updated correctly'); } catch (error) { logTestResult('Test 6: Update explanation', false, error.response?.data?.message || error.message); } } /** * Test 7: Update tags */ async function test07_UpdateTags() { console.log(`\n${colors.blue}Test 7: Update tags${colors.reset}`); try { const updateData = { tags: ['geography', 'europe', 'france', 'capitals'] }; const response = await axios.put(`${API_URL}/admin/questions/${testQuestionId}`, updateData, { headers: { 'Authorization': `Bearer ${adminToken}` } }); const { success, data } = response.data; const passed = success && Array.isArray(data.tags) && data.tags.length === 4 && data.tags.includes('europe'); logTestResult('Test 7: Update tags', passed, passed ? null : 'Tags not updated correctly'); } catch (error) { logTestResult('Test 7: Update tags', false, error.response?.data?.message || error.message); } } /** * Test 8: Update multiple choice options */ async function test08_UpdateOptions() { console.log(`\n${colors.blue}Test 8: Update multiple choice options${colors.reset}`); try { const updateData = { options: [ { id: 'a', text: 'Paris' }, { id: 'b', text: 'London' }, { id: 'c', text: 'Berlin' }, { id: 'd', text: 'Madrid' }, { id: 'e', text: 'Rome' } ] }; const response = await axios.put(`${API_URL}/admin/questions/${testQuestionId}`, updateData, { headers: { 'Authorization': `Bearer ${adminToken}` } }); const { success, data } = response.data; const passed = success && Array.isArray(data.options) && data.options.length === 5 && data.options.some(opt => opt.text === 'Rome'); logTestResult('Test 8: Update multiple choice options', passed, passed ? null : 'Options not updated correctly'); } catch (error) { logTestResult('Test 8: Update multiple choice options', false, error.response?.data?.message || error.message); } } /** * Test 9: Update correct answer */ async function test09_UpdateCorrectAnswer() { console.log(`\n${colors.blue}Test 9: Update correct answer${colors.reset}`); try { // First update to add 'Lyon' as an option await axios.put(`${API_URL}/admin/questions/${testQuestionId}`, { options: [ { id: 'a', text: 'Paris' }, { id: 'b', text: 'London' }, { id: 'c', text: 'Berlin' }, { id: 'd', text: 'Madrid' }, { id: 'e', text: 'Lyon' } ] }, { headers: { 'Authorization': `Bearer ${adminToken}` } }); // Note: correctAnswer is not returned in response for security // We just verify the update succeeds const updateData = { correctAnswer: 'a' // Keep as 'a' (Paris) since it's still valid }; const response = await axios.put(`${API_URL}/admin/questions/${testQuestionId}`, updateData, { headers: { 'Authorization': `Bearer ${adminToken}` } }); const { success } = response.data; const passed = success; logTestResult('Test 9: Update correct answer', passed, passed ? null : 'Update failed'); } catch (error) { logTestResult('Test 9: Update correct answer', false, error.response?.data?.message || error.message); } } /** * Test 10: Update isActive status */ async function test10_UpdateIsActive() { console.log(`\n${colors.blue}Test 10: Update isActive status${colors.reset}`); try { const updateData = { isActive: false }; const response = await axios.put(`${API_URL}/admin/questions/${testQuestionId}`, updateData, { headers: { 'Authorization': `Bearer ${adminToken}` } }); const { success, data } = response.data; const passed = success && data.isActive === false; // Reactivate for other tests await axios.put(`${API_URL}/admin/questions/${testQuestionId}`, { isActive: true }, { headers: { 'Authorization': `Bearer ${adminToken}` } }); logTestResult('Test 10: Update isActive status', passed, passed ? null : 'isActive not updated correctly'); } catch (error) { logTestResult('Test 10: Update isActive status', false, error.response?.data?.message || error.message); } } /** * Test 11: Update multiple fields at once */ async function test11_UpdateMultipleFields() { console.log(`\n${colors.blue}Test 11: Update multiple fields at once${colors.reset}`); try { const updateData = { questionText: 'What is the capital and largest city of France?', difficulty: 'hard', points: 30, explanation: 'Paris is both the capital and the most populous city of France.', tags: ['geography', 'france', 'cities'], keywords: ['france', 'paris', 'capital', 'city'] }; const response = await axios.put(`${API_URL}/admin/questions/${testQuestionId}`, updateData, { headers: { 'Authorization': `Bearer ${adminToken}` } }); const { success, data } = response.data; const passed = success && data.questionText === updateData.questionText && data.difficulty === 'hard' && data.points === 30 && data.explanation === updateData.explanation && data.tags.length === 3 && data.keywords.length === 4; logTestResult('Test 11: Update multiple fields at once', passed, passed ? null : 'Multiple fields not updated correctly'); } catch (error) { logTestResult('Test 11: Update multiple fields at once', false, error.response?.data?.message || error.message); } } /** * Test 12: Invalid question ID (400) */ async function test12_InvalidQuestionId() { console.log(`\n${colors.blue}Test 12: Invalid question ID${colors.reset}`); try { const updateData = { questionText: 'Updated text' }; await axios.put(`${API_URL}/admin/questions/invalid-id`, updateData, { headers: { 'Authorization': `Bearer ${adminToken}` } }); logTestResult('Test 12: Invalid question ID', false, 'Should have returned 400'); } catch (error) { const status = error.response?.status; const passed = status === 400; logTestResult('Test 12: Invalid question ID', passed, passed ? null : `Expected 400, got ${status}`); } } /** * Test 13: Non-existent question (404) */ async function test13_NonExistentQuestion() { console.log(`\n${colors.blue}Test 13: Non-existent question${colors.reset}`); try { const fakeUuid = '00000000-0000-0000-0000-000000000000'; const updateData = { questionText: 'Updated text' }; await axios.put(`${API_URL}/admin/questions/${fakeUuid}`, updateData, { headers: { 'Authorization': `Bearer ${adminToken}` } }); logTestResult('Test 13: Non-existent question', false, 'Should have returned 404'); } catch (error) { const status = error.response?.status; const passed = status === 404; logTestResult('Test 13: Non-existent question', passed, passed ? null : `Expected 404, got ${status}`); } } /** * Test 14: Invalid difficulty value (400) */ async function test14_InvalidDifficulty() { console.log(`\n${colors.blue}Test 14: Invalid difficulty value${colors.reset}`); try { const updateData = { difficulty: 'super-hard' }; await axios.put(`${API_URL}/admin/questions/${testQuestionId}`, updateData, { headers: { 'Authorization': `Bearer ${adminToken}` } }); logTestResult('Test 14: Invalid difficulty value', false, 'Should have returned 400'); } catch (error) { const status = error.response?.status; const passed = status === 400; logTestResult('Test 14: Invalid difficulty value', passed, passed ? null : `Expected 400, got ${status}`); } } /** * Test 15: Invalid points value (400) */ async function test15_InvalidPoints() { console.log(`\n${colors.blue}Test 15: Invalid points value${colors.reset}`); try { const updateData = { points: -10 }; await axios.put(`${API_URL}/admin/questions/${testQuestionId}`, updateData, { headers: { 'Authorization': `Bearer ${adminToken}` } }); logTestResult('Test 15: Invalid points value', false, 'Should have returned 400'); } catch (error) { const status = error.response?.status; const passed = status === 400; logTestResult('Test 15: Invalid points value', passed, passed ? null : `Expected 400, got ${status}`); } } /** * Test 16: Empty question text (400) */ async function test16_EmptyQuestionText() { console.log(`\n${colors.blue}Test 16: Empty question text${colors.reset}`); try { const updateData = { questionText: '' }; await axios.put(`${API_URL}/admin/questions/${testQuestionId}`, updateData, { headers: { 'Authorization': `Bearer ${adminToken}` } }); logTestResult('Test 16: Empty question text', false, 'Should have returned 400'); } catch (error) { const status = error.response?.status; const passed = status === 400; logTestResult('Test 16: Empty question text', passed, passed ? null : `Expected 400, got ${status}`); } } /** * Test 17: Update with less than 2 options for multiple choice (400) */ async function test17_InsufficientOptions() { console.log(`\n${colors.blue}Test 17: Insufficient options for multiple choice${colors.reset}`); try { const updateData = { options: [{ id: 'a', text: 'Paris' }] }; await axios.put(`${API_URL}/admin/questions/${testQuestionId}`, updateData, { headers: { 'Authorization': `Bearer ${adminToken}` } }); logTestResult('Test 17: Insufficient options for multiple choice', false, 'Should have returned 400'); } catch (error) { const status = error.response?.status; const passed = status === 400; logTestResult('Test 17: Insufficient options for multiple choice', passed, passed ? null : `Expected 400, got ${status}`); } } /** * Test 18: Correct answer not in options (400) */ async function test18_CorrectAnswerNotInOptions() { console.log(`\n${colors.blue}Test 18: Correct answer not in options${colors.reset}`); try { const updateData = { correctAnswer: 'z' // Invalid option ID }; await axios.put(`${API_URL}/admin/questions/${testQuestionId}`, updateData, { headers: { 'Authorization': `Bearer ${adminToken}` } }); logTestResult('Test 18: Correct answer not in options', false, 'Should have returned 400'); } catch (error) { const status = error.response?.status; const passed = status === 400; logTestResult('Test 18: Correct answer not in options', passed, passed ? null : `Expected 400, got ${status}`); } } /** * Test 19: Update category to non-existent category (404) */ async function test19_NonExistentCategory() { console.log(`\n${colors.blue}Test 19: Update to non-existent category${colors.reset}`); try { const fakeUuid = '00000000-0000-0000-0000-000000000000'; const updateData = { categoryId: fakeUuid }; await axios.put(`${API_URL}/admin/questions/${testQuestionId}`, updateData, { headers: { 'Authorization': `Bearer ${adminToken}` } }); logTestResult('Test 19: Update to non-existent category', false, 'Should have returned 404'); } catch (error) { const status = error.response?.status; const passed = status === 404; logTestResult('Test 19: Update to non-existent category', passed, passed ? null : `Expected 404, got ${status}`); } } /** * Test 20: Response doesn't include correctAnswer (security) */ async function test20_NoCorrectAnswerInResponse() { console.log(`\n${colors.blue}Test 20: Response doesn't expose correct answer${colors.reset}`); try { const updateData = { questionText: 'What is the capital of France? (Updated)' }; const response = await axios.put(`${API_URL}/admin/questions/${testQuestionId}`, updateData, { headers: { 'Authorization': `Bearer ${adminToken}` } }); const { success, data } = response.data; const passed = success && !data.hasOwnProperty('correctAnswer') && !data.hasOwnProperty('correct_answer'); logTestResult('Test 20: Response doesn\'t expose correct answer', passed, passed ? null : 'correctAnswer should not be in response'); } catch (error) { logTestResult('Test 20: Response doesn\'t expose correct answer', false, error.response?.data?.message || error.message); } } // ============================================================================ // CLEANUP // ============================================================================ /** * Delete test question */ async function deleteTestQuestion() { try { if (testQuestionId) { await axios.delete(`${API_URL}/admin/questions/${testQuestionId}`, { headers: { 'Authorization': `Bearer ${adminToken}` } }); console.log(`${colors.cyan}✓ Deleted test question${colors.reset}`); } } catch (error) { console.error(`${colors.yellow}⚠ Failed to delete test question:${colors.reset}`, error.response?.data || error.message); } } // ============================================================================ // MAIN TEST RUNNER // ============================================================================ async function runAllTests() { console.log(`${colors.magenta} ╔════════════════════════════════════════════════════════════╗ ║ ADMIN UPDATE QUESTION ENDPOINT - TEST SUITE ║ ╚════════════════════════════════════════════════════════════╝ ${colors.reset}`); try { // Setup console.log(`${colors.cyan}\n--- Setup Phase ---${colors.reset}`); await loginAdmin(); await createRegularUser(); await getFirstCategory(); await createTestQuestion(); // Run tests console.log(`${colors.cyan}\n--- Running Tests ---${colors.reset}`); // Authorization tests await test01_UnauthenticatedCannotUpdate(); await test02_UserCannotUpdate(); // Update field tests await test03_UpdateQuestionText(); await test04_UpdateDifficulty(); await test05_UpdatePoints(); await test06_UpdateExplanation(); await test07_UpdateTags(); await test08_UpdateOptions(); await test09_UpdateCorrectAnswer(); await test10_UpdateIsActive(); await test11_UpdateMultipleFields(); // Error handling tests await test12_InvalidQuestionId(); await test13_NonExistentQuestion(); await test14_InvalidDifficulty(); await test15_InvalidPoints(); await test16_EmptyQuestionText(); await test17_InsufficientOptions(); await test18_CorrectAnswerNotInOptions(); await test19_NonExistentCategory(); // Security tests await test20_NoCorrectAnswerInResponse(); // Cleanup console.log(`${colors.cyan}\n--- Cleanup Phase ---${colors.reset}`); await deleteTestQuestion(); // Summary console.log(`${colors.magenta} ╔════════════════════════════════════════════════════════════╗ ║ TEST SUMMARY ║ ╚════════════════════════════════════════════════════════════╝ ${colors.reset}`); console.log(`Total Tests: ${totalTests}`); console.log(`${colors.green}Passed: ${passedTests}${colors.reset}`); console.log(`${colors.red}Failed: ${failedTests}${colors.reset}`); console.log(`Success Rate: ${((passedTests / totalTests) * 100).toFixed(2)}%\n`); if (failedTests === 0) { console.log(`${colors.green}✓ All tests passed!${colors.reset}\n`); process.exit(0); } else { console.log(`${colors.red}✗ Some tests failed${colors.reset}\n`); process.exit(1); } } catch (error) { console.error(`${colors.red}\n✗ Test suite failed:${colors.reset}`, error.message); process.exit(1); } } // Run the tests runAllTests();