const axios = require('axios'); const API_URL = 'http://localhost:3000/api'; // Category UUIDs (from database) const CATEGORY_IDS = { JAVASCRIPT: '68b4c87f-db0b-48ea-b8a4-b2f4fce785a2', ANGULAR: '0033017b-6ecb-4e9d-8f13-06fc222c1dfc', REACT: 'd27e3412-6f2b-432d-8d63-1e165ea5fffd', NODEJS: '5e3094ab-ab6d-4f8a-9261-8177b9c979ae', TYPESCRIPT: '7a71ab02-a7e4-4a76-a364-de9d6e3f3411', SQL_DATABASES: '24b7b12d-fa23-448f-9f55-b0b9b82a844f', SYSTEM_DESIGN: '65b3ad28-a19d-413a-9abe-94184f963d77', }; // Test user credentials (from seeder) const testUser = { email: 'admin@quiz.com', password: 'Admin@123' }; // ANSI color codes for output const colors = { reset: '\x1b[0m', green: '\x1b[32m', red: '\x1b[31m', yellow: '\x1b[33m', blue: '\x1b[34m', cyan: '\x1b[36m' }; let userToken = null; let guestToken = null; /** * Login as registered user */ async function loginUser() { try { const response = await axios.post(`${API_URL}/auth/login`, testUser); userToken = response.data.data.token; console.log(`${colors.cyan}✓ Logged in as user${colors.reset}`); return userToken; } catch (error) { console.error(`${colors.red}✗ Failed to login:${colors.reset}`, error.response?.data || error.message); throw error; } } /** * Create guest session */ async function createGuestSession() { try { const response = await axios.post(`${API_URL}/guest/start-session`, { deviceId: 'test-device-category-details' }); guestToken = response.data.sessionToken; console.log(`${colors.cyan}✓ Created guest session${colors.reset}`); return guestToken; } catch (error) { console.error(`${colors.red}✗ Failed to create guest session:${colors.reset}`, error.response?.data || error.message); throw error; } } /** * Test 1: Get guest-accessible category details (JavaScript) */ async function testGetGuestCategoryDetails() { console.log(`\n${colors.blue}Test 1: Get guest-accessible category details (JavaScript)${colors.reset}`); try { const response = await axios.get(`${API_URL}/categories/${CATEGORY_IDS.JAVASCRIPT}`, { headers: { 'X-Guest-Token': guestToken } }); const { success, data, message } = response.data; // Validations if (!success) throw new Error('success should be true'); if (!data.category) throw new Error('Missing category data'); if (!data.questionPreview) throw new Error('Missing questionPreview'); if (!data.stats) throw new Error('Missing stats'); if (data.category.name !== 'JavaScript') throw new Error('Expected JavaScript category'); if (!data.category.guestAccessible) throw new Error('Should be guest-accessible'); console.log(`${colors.green}✓ Test 1 Passed${colors.reset}`); console.log(` Category: ${data.category.name}`); console.log(` Questions Preview: ${data.questionPreview.length}`); console.log(` Total Questions: ${data.stats.totalQuestions}`); console.log(` Average Accuracy: ${data.stats.averageAccuracy}%`); return true; } catch (error) { console.error(`${colors.red}✗ Test 1 Failed:${colors.reset}`, error.response?.data || error.message); return false; } } /** * Test 2: Guest tries to access auth-only category (Node.js) */ async function testGuestAccessAuthCategory() { console.log(`\n${colors.blue}Test 2: Guest tries to access auth-only category (Node.js)${colors.reset}`); try { const response = await axios.get(`${API_URL}/categories/${CATEGORY_IDS.NODEJS}`, { headers: { 'X-Guest-Token': guestToken } }); // Should not reach here console.error(`${colors.red}✗ Test 2 Failed: Should have been rejected${colors.reset}`); return false; } catch (error) { if (error.response && error.response.status === 403) { const { success, message, requiresAuth } = error.response.data; if (success !== false) throw new Error('success should be false'); if (!requiresAuth) throw new Error('requiresAuth should be true'); if (!message.includes('authentication')) throw new Error('Message should mention authentication'); console.log(`${colors.green}✓ Test 2 Passed${colors.reset}`); console.log(` Status: 403 Forbidden`); console.log(` Message: ${message}`); console.log(` Requires Auth: ${requiresAuth}`); return true; } else { console.error(`${colors.red}✗ Test 2 Failed: Wrong error${colors.reset}`, error.response?.data || error.message); return false; } } } /** * Test 3: Authenticated user gets auth-only category details (Node.js) */ async function testAuthUserAccessCategory() { console.log(`\n${colors.blue}Test 3: Authenticated user gets auth-only category details (Node.js)${colors.reset}`); try { const response = await axios.get(`${API_URL}/categories/${CATEGORY_IDS.NODEJS}`, { headers: { 'Authorization': `Bearer ${userToken}` } }); const { success, data } = response.data; if (!success) throw new Error('success should be true'); if (data.category.name !== 'Node.js') throw new Error('Expected Node.js category'); if (data.category.guestAccessible) throw new Error('Should not be guest-accessible'); console.log(`${colors.green}✓ Test 3 Passed${colors.reset}`); console.log(` Category: ${data.category.name}`); console.log(` Guest Accessible: ${data.category.guestAccessible}`); console.log(` Total Questions: ${data.stats.totalQuestions}`); return true; } catch (error) { console.error(`${colors.red}✗ Test 3 Failed:${colors.reset}`, error.response?.data || error.message); return false; } } /** * Test 4: Invalid category ID (non-numeric) */ async function testInvalidCategoryId() { console.log(`\n${colors.blue}Test 4: Invalid category ID (non-numeric)${colors.reset}`); try { const response = await axios.get(`${API_URL}/categories/invalid`, { headers: { 'Authorization': `Bearer ${userToken}` } }); console.error(`${colors.red}✗ Test 4 Failed: Should have been rejected${colors.reset}`); return false; } catch (error) { if (error.response && error.response.status === 400) { const { success, message } = error.response.data; if (success !== false) throw new Error('success should be false'); if (!message.includes('Invalid')) throw new Error('Message should mention invalid ID'); console.log(`${colors.green}✓ Test 4 Passed${colors.reset}`); console.log(` Status: 400 Bad Request`); console.log(` Message: ${message}`); return true; } else { console.error(`${colors.red}✗ Test 4 Failed: Wrong error${colors.reset}`, error.response?.data || error.message); return false; } } } /** * Test 5: Non-existent category ID */ async function testNonExistentCategory() { console.log(`\n${colors.blue}Test 5: Non-existent category ID (999)${colors.reset}`); try { const response = await axios.get(`${API_URL}/categories/999`, { headers: { 'Authorization': `Bearer ${userToken}` } }); console.error(`${colors.red}✗ Test 5 Failed: Should have returned 404${colors.reset}`); return false; } catch (error) { if (error.response && error.response.status === 404) { const { success, message } = error.response.data; if (success !== false) throw new Error('success should be false'); if (!message.includes('not found')) throw new Error('Message should mention not found'); console.log(`${colors.green}✓ Test 5 Passed${colors.reset}`); console.log(` Status: 404 Not Found`); console.log(` Message: ${message}`); return true; } else { console.error(`${colors.red}✗ Test 5 Failed: Wrong error${colors.reset}`, error.response?.data || error.message); return false; } } } /** * Test 6: Verify response structure */ async function testResponseStructure() { console.log(`\n${colors.blue}Test 6: Verify response structure${colors.reset}`); try { const response = await axios.get(`${API_URL}/categories/${CATEGORY_IDS.JAVASCRIPT}`, { headers: { 'Authorization': `Bearer ${userToken}` } }); const { success, data, message } = response.data; const { category, questionPreview, stats } = data; // Check category fields const requiredCategoryFields = ['id', 'name', 'slug', 'description', 'icon', 'color', 'questionCount', 'displayOrder', 'guestAccessible']; for (const field of requiredCategoryFields) { if (!(field in category)) throw new Error(`Missing category field: ${field}`); } // Check question preview structure if (!Array.isArray(questionPreview)) throw new Error('questionPreview should be an array'); if (questionPreview.length > 5) throw new Error('questionPreview should have max 5 questions'); if (questionPreview.length > 0) { const question = questionPreview[0]; const requiredQuestionFields = ['id', 'questionText', 'questionType', 'difficulty', 'points', 'accuracy']; for (const field of requiredQuestionFields) { if (!(field in question)) throw new Error(`Missing question field: ${field}`); } } // Check stats structure const requiredStatsFields = ['totalQuestions', 'questionsByDifficulty', 'totalAttempts', 'totalCorrect', 'averageAccuracy']; for (const field of requiredStatsFields) { if (!(field in stats)) throw new Error(`Missing stats field: ${field}`); } // Check difficulty breakdown const { questionsByDifficulty } = stats; if (!('easy' in questionsByDifficulty)) throw new Error('Missing easy difficulty count'); if (!('medium' in questionsByDifficulty)) throw new Error('Missing medium difficulty count'); if (!('hard' in questionsByDifficulty)) throw new Error('Missing hard difficulty count'); console.log(`${colors.green}✓ Test 6 Passed${colors.reset}`); console.log(` All required fields present`); console.log(` Question preview length: ${questionPreview.length}`); console.log(` Difficulty breakdown: Easy=${questionsByDifficulty.easy}, Medium=${questionsByDifficulty.medium}, Hard=${questionsByDifficulty.hard}`); return true; } catch (error) { console.error(`${colors.red}✗ Test 6 Failed:${colors.reset}`, error.response?.data || error.message); return false; } } /** * Test 7: No authentication (public access to guest category) */ async function testPublicAccessGuestCategory() { console.log(`\n${colors.blue}Test 7: Public access to guest-accessible category (no auth)${colors.reset}`); try { const response = await axios.get(`${API_URL}/categories/${CATEGORY_IDS.JAVASCRIPT}`); const { success, data } = response.data; if (!success) throw new Error('success should be true'); if (data.category.name !== 'JavaScript') throw new Error('Expected JavaScript category'); if (!data.category.guestAccessible) throw new Error('Should be guest-accessible'); console.log(`${colors.green}✓ Test 7 Passed${colors.reset}`); console.log(` Public access allowed for guest-accessible categories`); console.log(` Category: ${data.category.name}`); return true; } catch (error) { console.error(`${colors.red}✗ Test 7 Failed:${colors.reset}`, error.response?.data || error.message); return false; } } /** * Test 8: No authentication (public tries auth-only category) */ async function testPublicAccessAuthCategory() { console.log(`\n${colors.blue}Test 8: Public access to auth-only category (no auth)${colors.reset}`); try { const response = await axios.get(`${API_URL}/categories/${CATEGORY_IDS.NODEJS}`); console.error(`${colors.red}✗ Test 8 Failed: Should have been rejected${colors.reset}`); return false; } catch (error) { if (error.response && error.response.status === 403) { const { success, requiresAuth } = error.response.data; if (success !== false) throw new Error('success should be false'); if (!requiresAuth) throw new Error('requiresAuth should be true'); console.log(`${colors.green}✓ Test 8 Passed${colors.reset}`); console.log(` Public access blocked for auth-only categories`); console.log(` Status: 403 Forbidden`); return true; } else { console.error(`${colors.red}✗ Test 8 Failed: Wrong error${colors.reset}`, error.response?.data || error.message); return false; } } } /** * Test 9: Verify stats calculations */ async function testStatsCalculations() { console.log(`\n${colors.blue}Test 9: Verify stats calculations${colors.reset}`); try { const response = await axios.get(`${API_URL}/categories/${CATEGORY_IDS.JAVASCRIPT}`, { headers: { 'Authorization': `Bearer ${userToken}` } }); const { data } = response.data; const { stats } = data; // Verify difficulty sum equals total const difficultySum = stats.questionsByDifficulty.easy + stats.questionsByDifficulty.medium + stats.questionsByDifficulty.hard; if (difficultySum !== stats.totalQuestions) { throw new Error(`Difficulty sum (${difficultySum}) doesn't match total questions (${stats.totalQuestions})`); } // Verify accuracy is within valid range if (stats.averageAccuracy < 0 || stats.averageAccuracy > 100) { throw new Error(`Invalid accuracy: ${stats.averageAccuracy}%`); } // If there are attempts, verify accuracy calculation if (stats.totalAttempts > 0) { const expectedAccuracy = Math.round((stats.totalCorrect / stats.totalAttempts) * 100); if (stats.averageAccuracy !== expectedAccuracy) { throw new Error(`Accuracy mismatch: expected ${expectedAccuracy}%, got ${stats.averageAccuracy}%`); } } console.log(`${colors.green}✓ Test 9 Passed${colors.reset}`); console.log(` Total Questions: ${stats.totalQuestions}`); console.log(` Difficulty Sum: ${difficultySum}`); console.log(` Total Attempts: ${stats.totalAttempts}`); console.log(` Total Correct: ${stats.totalCorrect}`); console.log(` Average Accuracy: ${stats.averageAccuracy}%`); return true; } catch (error) { console.error(`${colors.red}✗ Test 9 Failed:${colors.reset}`, error.response?.data || error.message); return false; } } /** * Run all tests */ async function runAllTests() { console.log(`${colors.cyan}========================================${colors.reset}`); console.log(`${colors.cyan}Testing Category Details API${colors.reset}`); console.log(`${colors.cyan}========================================${colors.reset}`); const results = []; try { // Setup await loginUser(); await createGuestSession(); // Run tests results.push(await testGetGuestCategoryDetails()); results.push(await testGuestAccessAuthCategory()); results.push(await testAuthUserAccessCategory()); results.push(await testInvalidCategoryId()); results.push(await testNonExistentCategory()); results.push(await testResponseStructure()); results.push(await testPublicAccessGuestCategory()); results.push(await testPublicAccessAuthCategory()); results.push(await testStatsCalculations()); // Summary console.log(`\n${colors.cyan}========================================${colors.reset}`); console.log(`${colors.cyan}Test Summary${colors.reset}`); console.log(`${colors.cyan}========================================${colors.reset}`); const passed = results.filter(r => r === true).length; const failed = results.filter(r => r === false).length; console.log(`${colors.green}Passed: ${passed}${colors.reset}`); console.log(`${colors.red}Failed: ${failed}${colors.reset}`); console.log(`Total: ${results.length}`); if (failed === 0) { console.log(`\n${colors.green}✓ All tests passed!${colors.reset}`); } else { console.log(`\n${colors.red}✗ Some tests failed${colors.reset}`); process.exit(1); } } catch (error) { console.error(`${colors.red}Test execution error:${colors.reset}`, error); process.exit(1); } } // Run tests runAllTests();