add changes

This commit is contained in:
AD2025
2025-12-25 00:24:11 +02:00
parent 079c10e843
commit efb4f69e20
64 changed files with 576 additions and 568 deletions

View 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();