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); });