Files
Tasks/backend/test-question-search.js
2025-11-11 00:25:50 +02:00

343 lines
14 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', // Guest accessible
ANGULAR: '0033017b-6ecb-4e9d-8f13-06fc222c1dfc', // Guest accessible
REACT: 'd27e3412-6f2b-432d-8d63-1e165ea5fffd', // Guest accessible
NODEJS: '5e3094ab-ab6d-4f8a-9261-8177b9c979ae', // Auth only
TYPESCRIPT: '7a71ab02-a7e4-4a76-a364-de9d6e3f3411', // Auth only
};
let adminToken = '';
let regularUserToken = '';
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}`);
}
}
// Setup: Login as admin and regular user
async function setup() {
try {
// 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 and login as 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\n');
} catch (error) {
console.error('Setup failed:', error.response?.data || error.message);
process.exit(1);
}
}
// Tests
async function runTests() {
console.log('========================================');
console.log('Testing Question Search API');
console.log('========================================\n');
await setup();
// Test 1: Basic search without auth (guest accessible only)
await runTest('Test 1: Basic search without auth', async () => {
const response = await axios.get(`${BASE_URL}/questions/search?q=javascript`);
if (response.data.success !== true) throw new Error('Response success should be true');
if (!Array.isArray(response.data.data)) throw new Error('Response data should be an array');
if (response.data.query !== 'javascript') throw new Error('Query not reflected in response');
if (typeof response.data.total !== 'number') throw new Error('Total should be a number');
console.log(` Found ${response.data.total} results for "javascript" (guest)`);
});
// Test 2: Missing search query
await runTest('Test 2: Missing search query returns 400', async () => {
try {
await axios.get(`${BASE_URL}/questions/search`);
throw new Error('Should have returned 400');
} catch (error) {
if (error.response?.status !== 400) throw new Error(`Expected 400, got ${error.response?.status}`);
if (!error.response.data.message.includes('required')) {
throw new Error('Error message should mention required query');
}
console.log(` Correctly rejected missing query`);
}
});
// Test 3: Empty search query
await runTest('Test 3: Empty search query returns 400', async () => {
try {
await axios.get(`${BASE_URL}/questions/search?q=`);
throw new Error('Should have returned 400');
} catch (error) {
if (error.response?.status !== 400) throw new Error(`Expected 400, got ${error.response?.status}`);
console.log(` Correctly rejected empty query`);
}
});
// Test 4: Authenticated user sees more results
await runTest('Test 4: Authenticated user sees more results', async () => {
const guestResponse = await axios.get(`${BASE_URL}/questions/search?q=node`);
const authResponse = await axios.get(`${BASE_URL}/questions/search?q=node`, {
headers: { Authorization: `Bearer ${regularUserToken}` }
});
if (authResponse.data.total < guestResponse.data.total) {
throw new Error('Authenticated user should see at least as many results as guest');
}
console.log(` Guest: ${guestResponse.data.total} results, Auth: ${authResponse.data.total} results`);
});
// Test 5: Search with category filter
await runTest('Test 5: Search with category filter', async () => {
const response = await axios.get(
`${BASE_URL}/questions/search?q=what&category=${CATEGORY_IDS.JAVASCRIPT}`
);
if (response.data.success !== true) throw new Error('Response success should be true');
if (response.data.filters.category !== CATEGORY_IDS.JAVASCRIPT) {
throw new Error('Category filter not applied');
}
// Verify all results are from JavaScript category
const allFromCategory = response.data.data.every(q => q.category.name === 'JavaScript');
if (!allFromCategory) throw new Error('Not all results are from JavaScript category');
console.log(` Found ${response.data.count} JavaScript questions matching "what"`);
});
// Test 6: Search with difficulty filter
await runTest('Test 6: Search with difficulty filter', async () => {
const response = await axios.get(`${BASE_URL}/questions/search?q=what&difficulty=easy`);
if (response.data.success !== true) throw new Error('Response success should be true');
if (response.data.filters.difficulty !== 'easy') {
throw new Error('Difficulty filter not applied');
}
// Verify all results are easy difficulty
const allEasy = response.data.data.every(q => q.difficulty === 'easy');
if (!allEasy) throw new Error('Not all results are easy difficulty');
console.log(` Found ${response.data.count} easy questions matching "what"`);
});
// Test 7: Search with combined filters
await runTest('Test 7: Search with combined filters', async () => {
const response = await axios.get(
`${BASE_URL}/questions/search?q=what&category=${CATEGORY_IDS.REACT}&difficulty=easy`,
{ headers: { Authorization: `Bearer ${regularUserToken}` } }
);
if (response.data.success !== true) throw new Error('Response success should be true');
if (response.data.filters.category !== CATEGORY_IDS.REACT) {
throw new Error('Category filter not applied');
}
if (response.data.filters.difficulty !== 'easy') {
throw new Error('Difficulty filter not applied');
}
console.log(` Found ${response.data.count} easy React questions matching "what"`);
});
// Test 8: Invalid category UUID
await runTest('Test 8: Invalid category UUID returns 400', async () => {
try {
await axios.get(`${BASE_URL}/questions/search?q=javascript&category=invalid-uuid`);
throw new Error('Should have returned 400');
} catch (error) {
if (error.response?.status !== 400) throw new Error(`Expected 400, got ${error.response?.status}`);
if (!error.response.data.message.includes('Invalid category ID')) {
throw new Error('Should mention invalid category ID');
}
console.log(` Correctly rejected invalid category UUID`);
}
});
// Test 9: Pagination - page 1
await runTest('Test 9: Pagination support (page 1)', async () => {
const response = await axios.get(`${BASE_URL}/questions/search?q=what&limit=3&page=1`);
if (response.data.success !== true) throw new Error('Response success should be true');
if (response.data.page !== 1) throw new Error('Page should be 1');
if (response.data.limit !== 3) throw new Error('Limit should be 3');
if (response.data.data.length > 3) throw new Error('Should return max 3 results');
if (typeof response.data.totalPages !== 'number') throw new Error('totalPages should be present');
console.log(` Page 1: ${response.data.count} results (total: ${response.data.total}, pages: ${response.data.totalPages})`);
});
// Test 10: Pagination - page 2
await runTest('Test 10: Pagination (page 2)', async () => {
const page1 = await axios.get(`${BASE_URL}/questions/search?q=what&limit=2&page=1`);
const page2 = await axios.get(`${BASE_URL}/questions/search?q=what&limit=2&page=2`);
if (page2.data.page !== 2) throw new Error('Page should be 2');
// Verify different results on page 2
const page1Ids = page1.data.data.map(q => q.id);
const page2Ids = page2.data.data.map(q => q.id);
const hasDifferentIds = page2Ids.some(id => !page1Ids.includes(id));
if (!hasDifferentIds && page2.data.data.length > 0) {
throw new Error('Page 2 should have different results than page 1');
}
console.log(` Page 2: ${page2.data.count} results`);
});
// Test 11: Response structure validation
await runTest('Test 11: Response structure validation', async () => {
const response = await axios.get(`${BASE_URL}/questions/search?q=javascript&limit=1`);
// Check top-level structure
const requiredFields = ['success', 'count', 'total', 'page', 'totalPages', 'limit', 'query', 'filters', 'data', 'message'];
for (const field of requiredFields) {
if (!(field in response.data)) throw new Error(`Missing field: ${field}`);
}
// Check filters structure
if (!('category' in response.data.filters)) throw new Error('Missing filters.category');
if (!('difficulty' in response.data.filters)) throw new Error('Missing filters.difficulty');
// Check question structure (if results exist)
if (response.data.data.length > 0) {
const question = response.data.data[0];
const questionFields = ['id', 'questionText', 'highlightedText', 'questionType', 'difficulty', 'points', 'accuracy', 'relevance', 'category'];
for (const field of questionFields) {
if (!(field in question)) throw new Error(`Missing question field: ${field}`);
}
// Check category structure
const categoryFields = ['id', 'name', 'slug', 'icon', 'color'];
for (const field of categoryFields) {
if (!(field in question.category)) throw new Error(`Missing category field: ${field}`);
}
// Verify correct_answer is NOT exposed
if ('correctAnswer' in question || 'correct_answer' in question) {
throw new Error('Correct answer should not be exposed');
}
}
console.log(` Response structure validated`);
});
// Test 12: Text highlighting present
await runTest('Test 12: Text highlighting in results', async () => {
const response = await axios.get(`${BASE_URL}/questions/search?q=javascript`);
if (response.data.success !== true) throw new Error('Response success should be true');
if (response.data.data.length > 0) {
const question = response.data.data[0];
// Check that highlightedText exists
if (!('highlightedText' in question)) throw new Error('highlightedText should be present');
// Check if highlighting was applied (basic check for ** markers)
const hasHighlight = question.highlightedText && question.highlightedText.includes('**');
console.log(` Highlighting ${hasHighlight ? 'applied' : 'not applied (no match in this result)'}`);
} else {
console.log(` No results to check highlighting`);
}
});
// Test 13: Relevance scoring
await runTest('Test 13: Relevance scoring present', async () => {
const response = await axios.get(`${BASE_URL}/questions/search?q=react hooks`);
if (response.data.success !== true) throw new Error('Response success should be true');
if (response.data.data.length > 0) {
// Check that relevance field exists
for (const question of response.data.data) {
if (!('relevance' in question)) throw new Error('relevance should be present');
if (typeof question.relevance !== 'number') throw new Error('relevance should be a number');
}
// Check that results are ordered by relevance (descending)
for (let i = 0; i < response.data.data.length - 1; i++) {
if (response.data.data[i].relevance < response.data.data[i + 1].relevance) {
throw new Error('Results should be ordered by relevance (descending)');
}
}
console.log(` Relevance scores: ${response.data.data.map(q => q.relevance.toFixed(2)).join(', ')}`);
} else {
console.log(` No results to check relevance`);
}
});
// Test 14: Max limit enforcement (100)
await runTest('Test 14: Max limit enforcement (limit=200 should cap at 100)', async () => {
const response = await axios.get(`${BASE_URL}/questions/search?q=what&limit=200`);
if (response.data.success !== true) throw new Error('Response success should be true');
if (response.data.limit > 100) throw new Error('Limit should be capped at 100');
if (response.data.data.length > 100) throw new Error('Should return max 100 results');
console.log(` Limit capped at ${response.data.limit} (requested 200)`);
});
// Summary
console.log('\n========================================');
console.log('Test Summary');
console.log('========================================');
console.log(`Passed: ${testResults.passed}`);
console.log(`Failed: ${testResults.failed}`);
console.log(`Total: ${testResults.total}`);
console.log('========================================\n');
if (testResults.failed === 0) {
console.log('✓ All tests passed!\n');
} else {
console.log('✗ Some tests failed.\n');
process.exit(1);
}
}
// Run tests
runTests().catch(error => {
console.error('Test execution failed:', error);
process.exit(1);
});