add new changes
This commit is contained in:
688
backend/test-admin-questions-pagination.js
Normal file
688
backend/test-admin-questions-pagination.js
Normal file
@@ -0,0 +1,688 @@
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user