Files
Tasks/backend/test-admin-questions-pagination.js
2025-11-20 00:39:00 +02:00

689 lines
26 KiB
JavaScript

const axios = require('axios');
const BASE_URL = 'http://localhost:3000/api';
// Category UUIDs from database
const CATEGORY_IDS = {
JAVASCRIPT: '68b4c87f-db0b-48ea-b8a4-b2f4fce785a2',
NODEJS: '5e3094ab-ab6d-4f8a-9261-8177b9c979ae',
};
let adminToken = '';
let regularUserToken = '';
let guestToken = '';
let createdQuestionIds = [];
let testResults = {
passed: 0,
failed: 0,
total: 0
};
// Test helper
async function runTest(testName, testFn) {
testResults.total++;
try {
await testFn();
testResults.passed++;
console.log(`${testName} - PASSED`);
} catch (error) {
testResults.failed++;
console.log(`${testName} - FAILED`);
console.log(` Error: ${error.message}`);
if (error.response?.data) {
console.log(` Response:`, JSON.stringify(error.response.data, null, 2));
}
}
}
// Setup: Create test questions and login
async function setup() {
try {
console.log('Setting up test data...\n');
// Login as admin
const adminLogin = await axios.post(`${BASE_URL}/auth/login`, {
email: 'admin@quiz.com',
password: 'Admin@123'
});
adminToken = adminLogin.data.data.token;
console.log('✓ Logged in as admin');
// Create regular user
const timestamp = Date.now();
const regularUser = {
username: `testuser${timestamp}`,
email: `testuser${timestamp}@test.com`,
password: 'Test@123'
};
await axios.post(`${BASE_URL}/auth/register`, regularUser);
const userLogin = await axios.post(`${BASE_URL}/auth/login`, {
email: regularUser.email,
password: regularUser.password
});
regularUserToken = userLogin.data.data.token;
console.log('✓ Created and logged in as regular user');
// Start guest session
const deviceId = `test-device-${timestamp}`;
const guestSession = await axios.post(`${BASE_URL}/guest/start-session`, { deviceId });
guestToken = guestSession.data.data.guestToken;
console.log('✓ Started guest session');
// Create test questions with different difficulties and categories
const testQuestions = [
{
questionText: 'What is the purpose of async/await in JavaScript?',
questionType: 'multiple',
options: [
{ id: 'a', text: 'To handle asynchronous operations' },
{ id: 'b', text: 'To create functions' },
{ id: 'c', text: 'To define classes' },
{ id: 'd', text: 'To handle errors' }
],
correctAnswer: 'a',
difficulty: 'easy',
explanation: 'Async/await is syntactic sugar for promises, making asynchronous code easier to read.',
categoryId: CATEGORY_IDS.JAVASCRIPT,
tags: ['async', 'promises', 'es6'],
points: 5
},
{
questionText: 'What is the difference between let and const in JavaScript?',
questionType: 'multiple',
options: [
{ id: 'a', text: 'No difference' },
{ id: 'b', text: 'const cannot be reassigned' },
{ id: 'c', text: 'let is global only' },
{ id: 'd', text: 'const is faster' }
],
correctAnswer: 'b',
difficulty: 'easy',
explanation: 'const creates a read-only reference to a value, while let allows reassignment.',
categoryId: CATEGORY_IDS.JAVASCRIPT,
tags: ['variables', 'es6'],
points: 5
},
{
questionText: 'What is a Promise in JavaScript?',
questionType: 'multiple',
options: [
{ id: 'a', text: 'A commitment to execute code' },
{ id: 'b', text: 'An object representing eventual completion of an async operation' },
{ id: 'c', text: 'A type of loop' },
{ id: 'd', text: 'A conditional statement' }
],
correctAnswer: 'b',
difficulty: 'medium',
explanation: 'A Promise is an object representing the eventual completion or failure of an asynchronous operation.',
categoryId: CATEGORY_IDS.JAVASCRIPT,
tags: ['promises', 'async'],
points: 10
},
{
questionText: 'What is event bubbling in JavaScript?',
questionType: 'multiple',
options: [
{ id: 'a', text: 'Events propagate from child to parent' },
{ id: 'b', text: 'Events disappear' },
{ id: 'c', text: 'Events multiply' },
{ id: 'd', text: 'Events get delayed' }
],
correctAnswer: 'a',
difficulty: 'medium',
explanation: 'Event bubbling is when an event propagates from the target element up through its ancestors.',
categoryId: CATEGORY_IDS.JAVASCRIPT,
tags: ['events', 'dom'],
points: 10
},
{
questionText: 'Explain the prototype chain in JavaScript',
questionType: 'written',
correctAnswer: 'The prototype chain is a mechanism where objects inherit properties from their prototype.',
difficulty: 'hard',
explanation: 'JavaScript uses prototypal inheritance where objects can inherit properties from other objects.',
categoryId: CATEGORY_IDS.JAVASCRIPT,
tags: ['prototypes', 'inheritance', 'oop'],
points: 15
},
{
questionText: 'What is Node.js used for?',
questionType: 'multiple',
options: [
{ id: 'a', text: 'Server-side JavaScript runtime' },
{ id: 'b', text: 'A frontend framework' },
{ id: 'c', text: 'A database' },
{ id: 'd', text: 'A CSS preprocessor' }
],
correctAnswer: 'a',
difficulty: 'easy',
explanation: 'Node.js is a JavaScript runtime built on Chrome\'s V8 engine for server-side development.',
categoryId: CATEGORY_IDS.NODEJS,
tags: ['nodejs', 'runtime'],
points: 5
},
{
questionText: 'What is Express.js in Node.js?',
questionType: 'multiple',
options: [
{ id: 'a', text: 'A web application framework' },
{ id: 'b', text: 'A database' },
{ id: 'c', text: 'A testing library' },
{ id: 'd', text: 'A package manager' }
],
correctAnswer: 'a',
difficulty: 'easy',
explanation: 'Express.js is a minimal and flexible Node.js web application framework.',
categoryId: CATEGORY_IDS.NODEJS,
tags: ['express', 'framework', 'web'],
points: 5
},
{
questionText: 'What is middleware in Express.js?',
questionType: 'multiple',
options: [
{ id: 'a', text: 'Functions that execute during request-response cycle' },
{ id: 'b', text: 'A type of database' },
{ id: 'c', text: 'A routing mechanism' },
{ id: 'd', text: 'A template engine' }
],
correctAnswer: 'a',
difficulty: 'medium',
explanation: 'Middleware functions have access to request, response objects and the next middleware function.',
categoryId: CATEGORY_IDS.NODEJS,
tags: ['express', 'middleware'],
points: 10
}
];
// Create all test questions
for (const questionData of testQuestions) {
const response = await axios.post(`${BASE_URL}/admin/questions`, questionData, {
headers: { Authorization: `Bearer ${adminToken}` }
});
createdQuestionIds.push(response.data.data.id);
}
console.log(`✓ Created ${createdQuestionIds.length} test questions\n`);
} catch (error) {
console.error('Setup failed:', error.response?.data || error.message);
process.exit(1);
}
}
// Cleanup: Delete test questions
async function cleanup() {
console.log('\n========================================');
console.log('Cleaning up test data...');
console.log('========================================\n');
for (const questionId of createdQuestionIds) {
try {
await axios.delete(`${BASE_URL}/admin/questions/${questionId}`, {
headers: { Authorization: `Bearer ${adminToken}` }
});
} catch (error) {
console.log(`Warning: Could not delete question ${questionId}`);
}
}
console.log(`✓ Deleted ${createdQuestionIds.length} test questions`);
}
// Tests
async function runTests() {
console.log('========================================');
console.log('Testing Admin Questions Pagination & Search API');
console.log('========================================\n');
await setup();
// ========================================
// AUTHORIZATION TESTS
// ========================================
console.log('\n--- Authorization Tests ---\n');
await runTest('Test 1: Guest cannot access admin questions endpoint', async () => {
try {
await axios.get(`${BASE_URL}/admin/questions`, {
headers: { 'x-guest-token': guestToken }
});
throw new Error('Guest should not have access');
} catch (error) {
if (error.response?.status !== 401 && error.response?.status !== 403) {
throw new Error(`Expected 401 or 403, got ${error.response?.status}`);
}
}
});
await runTest('Test 2: Regular user cannot access admin questions endpoint', async () => {
try {
await axios.get(`${BASE_URL}/admin/questions`, {
headers: { Authorization: `Bearer ${regularUserToken}` }
});
throw new Error('Regular user should not have access');
} catch (error) {
if (error.response?.status !== 403) {
throw new Error(`Expected 403, got ${error.response?.status}`);
}
}
});
await runTest('Test 3: Admin can access questions endpoint', async () => {
const response = await axios.get(`${BASE_URL}/admin/questions`, {
headers: { Authorization: `Bearer ${adminToken}` }
});
if (response.status !== 200) throw new Error(`Expected 200, got ${response.status}`);
if (!response.data.success) throw new Error('Response should be successful');
});
// ========================================
// PAGINATION TESTS
// ========================================
console.log('\n--- Pagination Tests ---\n');
await runTest('Test 4: Default pagination (page 1, limit 10)', async () => {
const response = await axios.get(`${BASE_URL}/admin/questions`, {
headers: { Authorization: `Bearer ${adminToken}` }
});
if (response.data.page !== 1) throw new Error('Default page should be 1');
if (response.data.limit !== 10) throw new Error('Default limit should be 10');
if (!Array.isArray(response.data.data)) throw new Error('Data should be an array');
if (response.data.count > 10) throw new Error('Count should not exceed limit');
});
await runTest('Test 5: Custom pagination (page 2, limit 5)', async () => {
const response = await axios.get(`${BASE_URL}/admin/questions?page=2&limit=5`, {
headers: { Authorization: `Bearer ${adminToken}` }
});
if (response.data.page !== 2) throw new Error('Page should be 2');
if (response.data.limit !== 5) throw new Error('Limit should be 5');
if (response.data.count > 5) throw new Error('Count should not exceed 5');
});
await runTest('Test 6: Pagination metadata is correct', async () => {
const response = await axios.get(`${BASE_URL}/admin/questions?page=1&limit=3`, {
headers: { Authorization: `Bearer ${adminToken}` }
});
if (typeof response.data.total !== 'number') throw new Error('Total should be a number');
if (typeof response.data.totalPages !== 'number') throw new Error('TotalPages should be a number');
if (response.data.totalPages !== Math.ceil(response.data.total / 3)) {
throw new Error('TotalPages calculation is incorrect');
}
});
await runTest('Test 7: Maximum limit enforcement (max 100)', async () => {
const response = await axios.get(`${BASE_URL}/admin/questions?limit=200`, {
headers: { Authorization: `Bearer ${adminToken}` }
});
if (response.data.limit > 100) throw new Error('Limit should be capped at 100');
});
await runTest('Test 8: Invalid page defaults to 1', async () => {
const response = await axios.get(`${BASE_URL}/admin/questions?page=-5`, {
headers: { Authorization: `Bearer ${adminToken}` }
});
if (response.data.page !== 1) throw new Error('Invalid page should default to 1');
});
// ========================================
// SEARCH TESTS
// ========================================
console.log('\n--- Search Tests ---\n');
await runTest('Test 9: Search by question text (async)', async () => {
const response = await axios.get(`${BASE_URL}/admin/questions?search=async`, {
headers: { Authorization: `Bearer ${adminToken}` }
});
if (response.data.count === 0) throw new Error('Should find questions with "async"');
const hasAsyncQuestion = response.data.data.some(q =>
q.questionText.toLowerCase().includes('async') ||
q.tags?.includes('async')
);
if (!hasAsyncQuestion) throw new Error('Results should contain "async" in text or tags');
});
await runTest('Test 10: Search by explanation text (promise)', async () => {
const response = await axios.get(`${BASE_URL}/admin/questions?search=promise`, {
headers: { Authorization: `Bearer ${adminToken}` }
});
if (response.data.count === 0) throw new Error('Should find questions about promises');
});
await runTest('Test 11: Search with no results', async () => {
const response = await axios.get(`${BASE_URL}/admin/questions?search=xyznonexistent123`, {
headers: { Authorization: `Bearer ${adminToken}` }
});
if (response.data.count !== 0) throw new Error('Should return 0 results for non-existent term');
if (response.data.data.length !== 0) throw new Error('Data array should be empty');
});
await runTest('Test 12: Search with special characters is handled', async () => {
const response = await axios.get(`${BASE_URL}/admin/questions?search=%$#@`, {
headers: { Authorization: `Bearer ${adminToken}` }
});
if (response.status !== 200) throw new Error('Should handle special characters gracefully');
});
// ========================================
// FILTER TESTS
// ========================================
console.log('\n--- Filter Tests ---\n');
await runTest('Test 13: Filter by difficulty (easy)', async () => {
const response = await axios.get(`${BASE_URL}/admin/questions?difficulty=easy`, {
headers: { Authorization: `Bearer ${adminToken}` }
});
if (response.data.count === 0) throw new Error('Should find easy questions');
const allEasy = response.data.data.every(q => q.difficulty === 'easy');
if (!allEasy) throw new Error('All questions should have easy difficulty');
});
await runTest('Test 14: Filter by difficulty (medium)', async () => {
const response = await axios.get(`${BASE_URL}/admin/questions?difficulty=medium`, {
headers: { Authorization: `Bearer ${adminToken}` }
});
const allMedium = response.data.data.every(q => q.difficulty === 'medium');
if (!allMedium) throw new Error('All questions should have medium difficulty');
});
await runTest('Test 15: Filter by difficulty (hard)', async () => {
const response = await axios.get(`${BASE_URL}/admin/questions?difficulty=hard`, {
headers: { Authorization: `Bearer ${adminToken}` }
});
const allHard = response.data.data.every(q => q.difficulty === 'hard');
if (!allHard) throw new Error('All questions should have hard difficulty');
});
await runTest('Test 16: Filter by category (JavaScript)', async () => {
const response = await axios.get(
`${BASE_URL}/admin/questions?category=${CATEGORY_IDS.JAVASCRIPT}`,
{ headers: { Authorization: `Bearer ${adminToken}` } }
);
if (response.data.count === 0) throw new Error('Should find JavaScript questions');
const allJavaScript = response.data.data.every(
q => q.category.id === CATEGORY_IDS.JAVASCRIPT
);
if (!allJavaScript) throw new Error('All questions should be in JavaScript category');
});
await runTest('Test 17: Filter by category (Node.js)', async () => {
const response = await axios.get(
`${BASE_URL}/admin/questions?category=${CATEGORY_IDS.NODEJS}`,
{ headers: { Authorization: `Bearer ${adminToken}` } }
);
const allNodejs = response.data.data.every(
q => q.category.id === CATEGORY_IDS.NODEJS
);
if (!allNodejs) throw new Error('All questions should be in Node.js category');
});
await runTest('Test 18: Invalid category UUID is ignored', async () => {
const response = await axios.get(`${BASE_URL}/admin/questions?category=invalid-uuid`, {
headers: { Authorization: `Bearer ${adminToken}` }
});
if (response.status !== 200) throw new Error('Should handle invalid UUID gracefully');
});
// ========================================
// COMBINED FILTER TESTS
// ========================================
console.log('\n--- Combined Filter Tests ---\n');
await runTest('Test 19: Search + difficulty filter', async () => {
const response = await axios.get(
`${BASE_URL}/admin/questions?search=javascript&difficulty=easy`,
{ headers: { Authorization: `Bearer ${adminToken}` } }
);
if (response.status !== 200) throw new Error('Combined filters should work');
const allEasy = response.data.data.every(q => q.difficulty === 'easy');
if (!allEasy) throw new Error('All results should match difficulty filter');
});
await runTest('Test 20: Search + category filter', async () => {
const response = await axios.get(
`${BASE_URL}/admin/questions?search=async&category=${CATEGORY_IDS.JAVASCRIPT}`,
{ headers: { Authorization: `Bearer ${adminToken}` } }
);
const allCorrectCategory = response.data.data.every(
q => q.category.id === CATEGORY_IDS.JAVASCRIPT
);
if (!allCorrectCategory) throw new Error('All results should match category filter');
});
await runTest('Test 21: Category + difficulty filter', async () => {
const response = await axios.get(
`${BASE_URL}/admin/questions?category=${CATEGORY_IDS.JAVASCRIPT}&difficulty=medium`,
{ headers: { Authorization: `Bearer ${adminToken}` } }
);
const allMatch = response.data.data.every(
q => q.category.id === CATEGORY_IDS.JAVASCRIPT && q.difficulty === 'medium'
);
if (!allMatch) throw new Error('All results should match both filters');
});
await runTest('Test 22: All filters combined', async () => {
const response = await axios.get(
`${BASE_URL}/admin/questions?search=event&category=${CATEGORY_IDS.JAVASCRIPT}&difficulty=medium&limit=5`,
{ headers: { Authorization: `Bearer ${adminToken}` } }
);
if (response.status !== 200) throw new Error('All filters should work together');
});
// ========================================
// SORTING TESTS
// ========================================
console.log('\n--- Sorting Tests ---\n');
await runTest('Test 23: Sort by createdAt DESC (default)', async () => {
const response = await axios.get(`${BASE_URL}/admin/questions?limit=5`, {
headers: { Authorization: `Bearer ${adminToken}` }
});
if (response.data.data.length < 2) return; // Skip if not enough data
const dates = response.data.data.map(q => new Date(q.createdAt).getTime());
const isSorted = dates.every((date, i) => i === 0 || date <= dates[i - 1]);
if (!isSorted) throw new Error('Questions should be sorted by createdAt DESC');
});
await runTest('Test 24: Sort by createdAt ASC', async () => {
const response = await axios.get(`${BASE_URL}/admin/questions?sortBy=createdAt&order=ASC&limit=5`, {
headers: { Authorization: `Bearer ${adminToken}` }
});
if (response.data.data.length < 2) return;
const dates = response.data.data.map(q => new Date(q.createdAt).getTime());
const isSorted = dates.every((date, i) => i === 0 || date >= dates[i - 1]);
if (!isSorted) throw new Error('Questions should be sorted by createdAt ASC');
});
await runTest('Test 25: Sort by difficulty', async () => {
const response = await axios.get(`${BASE_URL}/admin/questions?sortBy=difficulty&order=ASC`, {
headers: { Authorization: `Bearer ${adminToken}` }
});
if (response.status !== 200) throw new Error('Should be able to sort by difficulty');
});
await runTest('Test 26: Sort by points DESC', async () => {
const response = await axios.get(`${BASE_URL}/admin/questions?sortBy=points&order=DESC&limit=5`, {
headers: { Authorization: `Bearer ${adminToken}` }
});
if (response.data.data.length < 2) return;
const points = response.data.data.map(q => q.points);
const isSorted = points.every((point, i) => i === 0 || point <= points[i - 1]);
if (!isSorted) throw new Error('Questions should be sorted by points DESC');
});
await runTest('Test 27: Invalid sort field defaults to createdAt', async () => {
const response = await axios.get(`${BASE_URL}/admin/questions?sortBy=invalidField`, {
headers: { Authorization: `Bearer ${adminToken}` }
});
if (response.status !== 200) throw new Error('Invalid sort field should be handled gracefully');
if (response.data.filters.sortBy !== 'createdAt') {
throw new Error('Invalid sort field should default to createdAt');
}
});
// ========================================
// RESPONSE STRUCTURE TESTS
// ========================================
console.log('\n--- Response Structure Tests ---\n');
await runTest('Test 28: Response has correct structure', async () => {
const response = await axios.get(`${BASE_URL}/admin/questions`, {
headers: { Authorization: `Bearer ${adminToken}` }
});
const requiredFields = ['success', 'count', 'total', 'page', 'totalPages', 'limit', 'filters', 'data', 'message'];
for (const field of requiredFields) {
if (!(field in response.data)) {
throw new Error(`Response missing required field: ${field}`);
}
}
});
await runTest('Test 29: Each question has required fields', async () => {
const response = await axios.get(`${BASE_URL}/admin/questions?limit=1`, {
headers: { Authorization: `Bearer ${adminToken}` }
});
if (response.data.data.length === 0) return;
const question = response.data.data[0];
const requiredFields = [
'id', 'questionText', 'questionType', 'difficulty', 'points',
'explanation', 'category', 'isActive', 'createdAt', 'accuracy'
];
for (const field of requiredFields) {
if (!(field in question)) {
throw new Error(`Question missing required field: ${field}`);
}
}
});
await runTest('Test 30: Category object has required fields', async () => {
const response = await axios.get(`${BASE_URL}/admin/questions?limit=1`, {
headers: { Authorization: `Bearer ${adminToken}` }
});
if (response.data.data.length === 0) return;
const category = response.data.data[0].category;
const requiredFields = ['id', 'name', 'slug', 'icon', 'color'];
for (const field of requiredFields) {
if (!(field in category)) {
throw new Error(`Category missing required field: ${field}`);
}
}
});
await runTest('Test 31: Filters object in response matches query', async () => {
const response = await axios.get(
`${BASE_URL}/admin/questions?search=test&difficulty=easy&sortBy=points&order=ASC`,
{ headers: { Authorization: `Bearer ${adminToken}` } }
);
if (response.data.filters.search !== 'test') throw new Error('Search filter not reflected');
if (response.data.filters.difficulty !== 'easy') throw new Error('Difficulty filter not reflected');
if (response.data.filters.sortBy !== 'points') throw new Error('SortBy not reflected');
if (response.data.filters.order !== 'ASC') throw new Error('Order not reflected');
});
await runTest('Test 32: Admin can see correctAnswer field', async () => {
const response = await axios.get(`${BASE_URL}/admin/questions?limit=1`, {
headers: { Authorization: `Bearer ${adminToken}` }
});
if (response.data.data.length === 0) return;
const question = response.data.data[0];
if (!('correctAnswer' in question)) {
throw new Error('Admin should see correctAnswer field');
}
});
// ========================================
// PERFORMANCE & EDGE CASES
// ========================================
console.log('\n--- Performance & Edge Cases ---\n');
await runTest('Test 33: Empty search string returns all questions', async () => {
const response = await axios.get(`${BASE_URL}/admin/questions?search=`, {
headers: { Authorization: `Bearer ${adminToken}` }
});
if (response.status !== 200) throw new Error('Empty search should work');
});
await runTest('Test 34: Page beyond total pages returns empty array', async () => {
const response = await axios.get(`${BASE_URL}/admin/questions?page=9999`, {
headers: { Authorization: `Bearer ${adminToken}` }
});
if (response.data.data.length > 0) throw new Error('Page beyond total should return empty');
});
await runTest('Test 35: Accuracy is calculated correctly', async () => {
const response = await axios.get(`${BASE_URL}/admin/questions?limit=1`, {
headers: { Authorization: `Bearer ${adminToken}` }
});
if (response.data.data.length === 0) return;
const question = response.data.data[0];
if (typeof question.accuracy !== 'number') {
throw new Error('Accuracy should be a number');
}
if (question.accuracy < 0 || question.accuracy > 100) {
throw new Error('Accuracy should be between 0 and 100');
}
});
// Cleanup
await cleanup();
// Print summary
console.log('\n========================================');
console.log('Test Summary');
console.log('========================================');
console.log(`Total Tests: ${testResults.total}`);
console.log(`Passed: ${testResults.passed}`);
console.log(`Failed: ${testResults.failed}`);
console.log(`Success Rate: ${((testResults.passed / testResults.total) * 100).toFixed(2)}%`);
console.log('========================================\n');
process.exit(testResults.failed > 0 ? 1 : 0);
}
// Run tests
runTests().catch(error => {
console.error('Test suite failed:', error);
process.exit(1);
});