/** * Test Script: Update and Delete Question API (Admin) * * Tests: * - Update Question (various fields) * - Delete Question (soft delete) * - Authorization checks * - Validation scenarios */ 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; // Test data let testCategoryId = null; let testQuestionId = null; let secondCategoryId = 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' } }; } async function runTests() { console.log('========================================'); console.log('Testing Update/Delete Question API (Admin)'); console.log('========================================\n'); try { // ========================================== // Setup: Login as admin and regular user // ========================================== // 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: `testuser${timestamp}`, email: `testuser${timestamp}@test.com`, password: 'Test@123' }); userToken = userRes.data.data.token; console.log('✓ Created and logged in as regular user\n'); // Get test categories const categoriesRes = await axios.get(`${API_URL}/categories`, authConfig(adminToken)); testCategoryId = categoriesRes.data.data[0].id; // JavaScript secondCategoryId = categoriesRes.data.data[1].id; // Angular console.log(`✓ Using test categories: ${testCategoryId}, ${secondCategoryId}\n`); // Create a test question first const createRes = await axios.post(`${API_URL}/admin/questions`, { questionText: 'What is a closure in JavaScript?', questionType: 'multiple', options: [ { id: 'a', text: 'A function inside another function' }, { id: 'b', text: 'A loop structure' }, { id: 'c', text: 'A variable declaration' } ], correctAnswer: 'a', difficulty: 'medium', categoryId: testCategoryId, explanation: 'A closure is a function that has access to its outer scope', tags: ['closures', 'functions'], keywords: ['closure', 'scope'] }, authConfig(adminToken)); testQuestionId = createRes.data.data.id; console.log(`✓ Created test question: ${testQuestionId}\n`); // ========================================== // UPDATE QUESTION TESTS // ========================================== // Test 1: Admin updates question text try { const res = await axios.put(`${API_URL}/admin/questions/${testQuestionId}`, { questionText: 'What is a closure in JavaScript? (Updated)' }, authConfig(adminToken)); const passed = res.status === 200 && res.data.success === true && res.data.data.questionText === 'What is a closure in JavaScript? (Updated)'; logTest('Admin updates question text', passed, passed ? 'Question text updated successfully' : `Response: ${JSON.stringify(res.data)}`); } catch (error) { logTest('Admin updates question text', false, error.response?.data?.message || error.message); } // Test 2: Admin updates difficulty (points should auto-update) try { const res = await axios.put(`${API_URL}/admin/questions/${testQuestionId}`, { difficulty: 'hard' }, authConfig(adminToken)); const passed = res.status === 200 && res.data.data.difficulty === 'hard' && res.data.data.points === 15; logTest('Admin updates difficulty with auto-points', passed, passed ? `Difficulty: hard, Points auto-set to: ${res.data.data.points}` : `Response: ${JSON.stringify(res.data)}`); } catch (error) { logTest('Admin updates difficulty with auto-points', false, error.response?.data?.message || error.message); } // Test 3: Admin updates custom points try { const res = await axios.put(`${API_URL}/admin/questions/${testQuestionId}`, { points: 25 }, authConfig(adminToken)); const passed = res.status === 200 && res.data.data.points === 25; logTest('Admin updates custom points', passed, passed ? `Custom points: ${res.data.data.points}` : `Response: ${JSON.stringify(res.data)}`); } catch (error) { logTest('Admin updates custom points', false, error.response?.data?.message || error.message); } // Test 4: Admin updates options and correct answer try { const res = await axios.put(`${API_URL}/admin/questions/${testQuestionId}`, { options: [ { id: 'a', text: 'A function with outer scope access' }, { id: 'b', text: 'A loop structure' }, { id: 'c', text: 'A variable declaration' }, { id: 'd', text: 'A data type' } ], correctAnswer: 'a' }, authConfig(adminToken)); const passed = res.status === 200 && res.data.data.options.length === 4 && !res.data.data.correctAnswer; // Should not expose correct answer logTest('Admin updates options and correct answer', passed, passed ? `Options updated: ${res.data.data.options.length} options` : `Response: ${JSON.stringify(res.data)}`); } catch (error) { logTest('Admin updates options and correct answer', false, error.response?.data?.message || error.message); } // Test 5: Admin updates category try { const res = await axios.put(`${API_URL}/admin/questions/${testQuestionId}`, { categoryId: secondCategoryId }, authConfig(adminToken)); const passed = res.status === 200 && res.data.data.category.id === secondCategoryId; logTest('Admin updates category', passed, passed ? `Category changed to: ${res.data.data.category.name}` : `Response: ${JSON.stringify(res.data)}`); } catch (error) { logTest('Admin updates category', false, error.response?.data?.message || error.message); } // Test 6: Admin updates explanation, tags, keywords try { const res = await axios.put(`${API_URL}/admin/questions/${testQuestionId}`, { explanation: 'Updated: A closure provides access to outer scope', tags: ['closures', 'scope', 'functions', 'advanced'], keywords: ['closure', 'lexical', 'scope'] }, authConfig(adminToken)); const passed = res.status === 200 && res.data.data.explanation.includes('Updated') && res.data.data.tags.length === 4 && res.data.data.keywords.length === 3; logTest('Admin updates explanation, tags, keywords', passed, passed ? `Updated metadata: ${res.data.data.tags.length} tags, ${res.data.data.keywords.length} keywords` : `Response: ${JSON.stringify(res.data)}`); } catch (error) { logTest('Admin updates explanation, tags, keywords', false, error.response?.data?.message || error.message); } // Test 7: Admin updates isActive flag try { const res = await axios.put(`${API_URL}/admin/questions/${testQuestionId}`, { isActive: false }, authConfig(adminToken)); const passed = res.status === 200 && res.data.data.isActive === false; logTest('Admin updates isActive flag', passed, passed ? 'Question marked as inactive' : `Response: ${JSON.stringify(res.data)}`); } catch (error) { logTest('Admin updates isActive flag', false, error.response?.data?.message || error.message); } // Reactivate for remaining tests await axios.put(`${API_URL}/admin/questions/${testQuestionId}`, { isActive: true }, authConfig(adminToken)); // Test 8: Non-admin blocked from updating try { const res = await axios.put(`${API_URL}/admin/questions/${testQuestionId}`, { questionText: 'Hacked question' }, authConfig(userToken)); logTest('Non-admin blocked from updating question', false, 'Should have returned 403'); } catch (error) { const passed = error.response?.status === 403; logTest('Non-admin blocked from updating question', passed, passed ? 'Correctly blocked with 403' : `Status: ${error.response?.status}`); } // Test 9: Unauthenticated request blocked try { const res = await axios.put(`${API_URL}/admin/questions/${testQuestionId}`, { questionText: 'Hacked question' }); 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 10: Invalid UUID format returns 400 try { const res = await axios.put(`${API_URL}/admin/questions/invalid-uuid`, { questionText: 'Test' }, authConfig(adminToken)); logTest('Invalid UUID format returns 400', false, 'Should have returned 400'); } catch (error) { const passed = error.response?.status === 400; logTest('Invalid UUID format returns 400', passed, passed ? 'Correctly rejected invalid UUID' : `Status: ${error.response?.status}`); } // Test 11: Non-existent question returns 404 try { const fakeUuid = '00000000-0000-0000-0000-000000000000'; const res = await axios.put(`${API_URL}/admin/questions/${fakeUuid}`, { questionText: 'Test' }, authConfig(adminToken)); logTest('Non-existent question returns 404', false, 'Should have returned 404'); } catch (error) { const passed = error.response?.status === 404; logTest('Non-existent question returns 404', passed, passed ? 'Correctly returned 404 for non-existent question' : `Status: ${error.response?.status}`); } // Test 12: Empty question text rejected try { const res = await axios.put(`${API_URL}/admin/questions/${testQuestionId}`, { questionText: ' ' }, authConfig(adminToken)); logTest('Empty question text rejected', false, 'Should have returned 400'); } catch (error) { const passed = error.response?.status === 400; logTest('Empty question text rejected', passed, passed ? 'Correctly rejected empty text' : `Status: ${error.response?.status}`); } // Test 13: Invalid question type rejected try { const res = await axios.put(`${API_URL}/admin/questions/${testQuestionId}`, { questionType: 'invalid' }, authConfig(adminToken)); logTest('Invalid question type rejected', false, 'Should have returned 400'); } catch (error) { const passed = error.response?.status === 400; logTest('Invalid question type rejected', passed, passed ? 'Correctly rejected invalid type' : `Status: ${error.response?.status}`); } // Test 14: Invalid difficulty rejected try { const res = await axios.put(`${API_URL}/admin/questions/${testQuestionId}`, { difficulty: 'extreme' }, authConfig(adminToken)); 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 15: Insufficient options rejected (multiple choice) try { const res = await axios.put(`${API_URL}/admin/questions/${testQuestionId}`, { options: [{ id: 'a', text: 'Only one option' }] }, authConfig(adminToken)); logTest('Insufficient options rejected', false, 'Should have returned 400'); } catch (error) { const passed = error.response?.status === 400; logTest('Insufficient options rejected', passed, passed ? 'Correctly rejected insufficient options' : `Status: ${error.response?.status}`); } // Test 16: Too many options rejected (multiple choice) try { const res = await axios.put(`${API_URL}/admin/questions/${testQuestionId}`, { options: [ { id: 'a', text: 'Option 1' }, { id: 'b', text: 'Option 2' }, { id: 'c', text: 'Option 3' }, { id: 'd', text: 'Option 4' }, { id: 'e', text: 'Option 5' }, { id: 'f', text: 'Option 6' }, { id: 'g', text: 'Option 7' } ] }, authConfig(adminToken)); logTest('Too many options rejected', false, 'Should have returned 400'); } catch (error) { const passed = error.response?.status === 400; logTest('Too many options rejected', passed, passed ? 'Correctly rejected too many options' : `Status: ${error.response?.status}`); } // Test 17: Invalid correct answer for options rejected try { const res = await axios.put(`${API_URL}/admin/questions/${testQuestionId}`, { correctAnswer: 'z' // Not in options }, authConfig(adminToken)); logTest('Invalid correct answer rejected', false, 'Should have returned 400'); } catch (error) { const passed = error.response?.status === 400; logTest('Invalid correct answer rejected', passed, passed ? 'Correctly rejected invalid answer' : `Status: ${error.response?.status}`); } // Test 18: Invalid category UUID rejected try { const res = await axios.put(`${API_URL}/admin/questions/${testQuestionId}`, { categoryId: 'invalid-uuid' }, authConfig(adminToken)); logTest('Invalid category UUID rejected', false, 'Should have returned 400'); } catch (error) { const passed = error.response?.status === 400; logTest('Invalid category UUID rejected', passed, passed ? 'Correctly rejected invalid category UUID' : `Status: ${error.response?.status}`); } // Test 19: Non-existent category rejected try { const fakeUuid = '00000000-0000-0000-0000-000000000000'; const res = await axios.put(`${API_URL}/admin/questions/${testQuestionId}`, { categoryId: fakeUuid }, authConfig(adminToken)); logTest('Non-existent category rejected', false, 'Should have returned 404'); } catch (error) { const passed = error.response?.status === 404; logTest('Non-existent category rejected', passed, passed ? 'Correctly returned 404 for non-existent category' : `Status: ${error.response?.status}`); } // ========================================== // DELETE QUESTION TESTS // ========================================== console.log('\n--- Testing Delete Question ---\n'); // Create another question for delete tests const deleteTestRes = await axios.post(`${API_URL}/admin/questions`, { questionText: 'Question to be deleted', questionType: 'trueFalse', correctAnswer: 'true', difficulty: 'easy', categoryId: testCategoryId }, authConfig(adminToken)); const deleteQuestionId = deleteTestRes.data.data.id; console.log(`✓ Created question for delete tests: ${deleteQuestionId}\n`); // Test 20: Admin deletes question (soft delete) try { const res = await axios.delete(`${API_URL}/admin/questions/${deleteQuestionId}`, authConfig(adminToken)); const passed = res.status === 200 && res.data.success === true && res.data.data.id === deleteQuestionId; logTest('Admin deletes question (soft delete)', passed, passed ? `Question deleted: ${res.data.data.questionText}` : `Response: ${JSON.stringify(res.data)}`); } catch (error) { logTest('Admin deletes question (soft delete)', false, error.response?.data?.message || error.message); } // Test 21: Already deleted question rejected try { const res = await axios.delete(`${API_URL}/admin/questions/${deleteQuestionId}`, authConfig(adminToken)); logTest('Already deleted question rejected', false, 'Should have returned 400'); } catch (error) { const passed = error.response?.status === 400; logTest('Already deleted question rejected', passed, passed ? 'Correctly rejected already deleted question' : `Status: ${error.response?.status}`); } // Test 22: Non-admin blocked from deleting try { const res = await axios.delete(`${API_URL}/admin/questions/${testQuestionId}`, authConfig(userToken)); logTest('Non-admin blocked from deleting', false, 'Should have returned 403'); } catch (error) { const passed = error.response?.status === 403; logTest('Non-admin blocked from deleting', passed, passed ? 'Correctly blocked with 403' : `Status: ${error.response?.status}`); } // Test 23: Unauthenticated delete blocked try { const res = await axios.delete(`${API_URL}/admin/questions/${testQuestionId}`); logTest('Unauthenticated delete blocked', false, 'Should have returned 401'); } catch (error) { const passed = error.response?.status === 401; logTest('Unauthenticated delete blocked', passed, passed ? 'Correctly blocked with 401' : `Status: ${error.response?.status}`); } // Test 24: Invalid UUID format for delete returns 400 try { const res = await axios.delete(`${API_URL}/admin/questions/invalid-uuid`, authConfig(adminToken)); logTest('Invalid UUID format for delete returns 400', false, 'Should have returned 400'); } catch (error) { const passed = error.response?.status === 400; logTest('Invalid UUID format for delete returns 400', passed, passed ? 'Correctly rejected invalid UUID' : `Status: ${error.response?.status}`); } // Test 25: Non-existent question for delete returns 404 try { const fakeUuid = '00000000-0000-0000-0000-000000000000'; const res = await axios.delete(`${API_URL}/admin/questions/${fakeUuid}`, authConfig(adminToken)); logTest('Non-existent question for delete returns 404', false, 'Should have returned 404'); } catch (error) { const passed = error.response?.status === 404; logTest('Non-existent question for delete returns 404', passed, passed ? 'Correctly returned 404 for non-existent question' : `Status: ${error.response?.status}`); } // Test 26: Verify deleted question not in active list try { const res = await axios.get(`${API_URL}/questions/${deleteQuestionId}`, authConfig(adminToken)); logTest('Deleted question not accessible', false, 'Should have returned 404'); } catch (error) { const passed = error.response?.status === 404; logTest('Deleted question not accessible', passed, passed ? 'Deleted question correctly hidden from API' : `Status: ${error.response?.status}`); } } catch (error) { console.error('\n❌ Fatal error during tests:', error.message); if (error.response) { console.error('Response:', error.response.data); } } // ========================================== // 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();