add changes
This commit is contained in:
523
backend/tests/test-update-delete-question.js
Normal file
523
backend/tests/test-update-delete-question.js
Normal file
@@ -0,0 +1,523 @@
|
||||
/**
|
||||
* 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();
|
||||
Reference in New Issue
Block a user