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 NODEJS: '5e3094ab-ab6d-4f8a-9261-8177b9c979ae', // Auth only }; let adminToken = ''; let regularUserToken = ''; 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}`); } } // 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 Create Question API (Admin)'); console.log('========================================\n'); await setup(); // Test 1: Admin can create multiple choice question await runTest('Test 1: Admin creates multiple choice question', async () => { const questionData = { questionText: 'What is a closure in JavaScript?', questionType: 'multiple', options: [ { id: 'a', text: 'A function that returns another function' }, { id: 'b', text: 'A function with access to outer scope variables' }, { id: 'c', text: 'A function that closes the program' }, { id: 'd', text: 'A private variable' } ], correctAnswer: 'b', difficulty: 'medium', explanation: 'A closure is a function that has access to variables in its outer (enclosing) lexical scope.', categoryId: CATEGORY_IDS.JAVASCRIPT, tags: ['functions', 'scope', 'closures'], keywords: ['closure', 'lexical scope', 'outer function'] }; const response = await axios.post(`${BASE_URL}/admin/questions`, questionData, { headers: { Authorization: `Bearer ${adminToken}` } }); if (response.status !== 201) throw new Error(`Expected 201, got ${response.status}`); if (response.data.success !== true) throw new Error('Response success should be true'); if (!response.data.data.id) throw new Error('Question ID should be returned'); if (response.data.data.questionText !== questionData.questionText) { throw new Error('Question text mismatch'); } if (response.data.data.points !== 10) throw new Error('Medium questions should be 10 points'); createdQuestionIds.push(response.data.data.id); console.log(` Created question: ${response.data.data.id}`); }); // Test 2: Admin can create trueFalse question await runTest('Test 2: Admin creates trueFalse question', async () => { const questionData = { questionText: 'JavaScript is a statically-typed language', questionType: 'trueFalse', correctAnswer: 'false', difficulty: 'easy', explanation: 'JavaScript is a dynamically-typed language.', categoryId: CATEGORY_IDS.JAVASCRIPT, tags: ['basics', 'types'] }; const response = await axios.post(`${BASE_URL}/admin/questions`, questionData, { headers: { Authorization: `Bearer ${adminToken}` } }); if (response.status !== 201) throw new Error(`Expected 201, got ${response.status}`); if (response.data.data.questionType !== 'trueFalse') throw new Error('Question type mismatch'); if (response.data.data.points !== 5) throw new Error('Easy questions should be 5 points'); createdQuestionIds.push(response.data.data.id); console.log(` Created trueFalse question with 5 points`); }); // Test 3: Admin can create written question await runTest('Test 3: Admin creates written question', async () => { const questionData = { questionText: 'Explain the event loop in Node.js', questionType: 'written', correctAnswer: 'Event loop handles async operations', difficulty: 'hard', explanation: 'The event loop is what allows Node.js to perform non-blocking I/O operations.', categoryId: CATEGORY_IDS.NODEJS, points: 20 // Custom points }; const response = await axios.post(`${BASE_URL}/admin/questions`, questionData, { headers: { Authorization: `Bearer ${adminToken}` } }); if (response.status !== 201) throw new Error(`Expected 201, got ${response.status}`); if (response.data.data.questionType !== 'written') throw new Error('Question type mismatch'); if (response.data.data.points !== 20) throw new Error('Custom points not applied'); createdQuestionIds.push(response.data.data.id); console.log(` Created written question with custom points (20)`); }); // Test 4: Non-admin cannot create question await runTest('Test 4: Non-admin blocked from creating question', async () => { const questionData = { questionText: 'Test question', questionType: 'multiple', options: [{ id: 'a', text: 'Option A' }, { id: 'b', text: 'Option B' }], correctAnswer: 'a', difficulty: 'easy', categoryId: CATEGORY_IDS.JAVASCRIPT }; try { await axios.post(`${BASE_URL}/admin/questions`, questionData, { headers: { Authorization: `Bearer ${regularUserToken}` } }); throw new Error('Should have returned 403'); } catch (error) { if (error.response?.status !== 403) throw new Error(`Expected 403, got ${error.response?.status}`); console.log(` Correctly blocked with 403`); } }); // Test 5: Unauthenticated request blocked await runTest('Test 5: Unauthenticated request blocked', async () => { const questionData = { questionText: 'Test question', questionType: 'multiple', options: [{ id: 'a', text: 'Option A' }], correctAnswer: 'a', difficulty: 'easy', categoryId: CATEGORY_IDS.JAVASCRIPT }; try { await axios.post(`${BASE_URL}/admin/questions`, questionData); throw new Error('Should have returned 401'); } catch (error) { if (error.response?.status !== 401) throw new Error(`Expected 401, got ${error.response?.status}`); console.log(` Correctly blocked with 401`); } }); // Test 6: Missing question text await runTest('Test 6: Missing question text returns 400', async () => { const questionData = { questionType: 'multiple', options: [{ id: 'a', text: 'Option A' }], correctAnswer: 'a', difficulty: 'easy', categoryId: CATEGORY_IDS.JAVASCRIPT }; try { await axios.post(`${BASE_URL}/admin/questions`, questionData, { headers: { Authorization: `Bearer ${adminToken}` } }); 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('text')) { throw new Error('Should mention question text'); } console.log(` Correctly rejected missing question text`); } }); // Test 7: Invalid question type await runTest('Test 7: Invalid question type returns 400', async () => { const questionData = { questionText: 'Test question', questionType: 'invalid', correctAnswer: 'a', difficulty: 'easy', categoryId: CATEGORY_IDS.JAVASCRIPT }; try { await axios.post(`${BASE_URL}/admin/questions`, questionData, { headers: { Authorization: `Bearer ${adminToken}` } }); 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 question type')) { throw new Error('Should mention invalid question type'); } console.log(` Correctly rejected invalid question type`); } }); // Test 8: Missing options for multiple choice await runTest('Test 8: Missing options for multiple choice returns 400', async () => { const questionData = { questionText: 'Test question', questionType: 'multiple', correctAnswer: 'a', difficulty: 'easy', categoryId: CATEGORY_IDS.JAVASCRIPT }; try { await axios.post(`${BASE_URL}/admin/questions`, questionData, { headers: { Authorization: `Bearer ${adminToken}` } }); 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('Options')) { throw new Error('Should mention options'); } console.log(` Correctly rejected missing options`); } }); // Test 9: Insufficient options (less than 2) await runTest('Test 9: Insufficient options returns 400', async () => { const questionData = { questionText: 'Test question', questionType: 'multiple', options: [{ id: 'a', text: 'Only one option' }], correctAnswer: 'a', difficulty: 'easy', categoryId: CATEGORY_IDS.JAVASCRIPT }; try { await axios.post(`${BASE_URL}/admin/questions`, questionData, { headers: { Authorization: `Bearer ${adminToken}` } }); 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('at least 2')) { throw new Error('Should mention minimum options'); } console.log(` Correctly rejected insufficient options`); } }); // Test 10: Correct answer not in options await runTest('Test 10: Correct answer not in options returns 400', async () => { const questionData = { questionText: 'Test question', questionType: 'multiple', options: [ { id: 'a', text: 'Option A' }, { id: 'b', text: 'Option B' } ], correctAnswer: 'c', // Not in options difficulty: 'easy', categoryId: CATEGORY_IDS.JAVASCRIPT }; try { await axios.post(`${BASE_URL}/admin/questions`, questionData, { headers: { Authorization: `Bearer ${adminToken}` } }); 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('match one of the option')) { throw new Error('Should mention correct answer mismatch'); } console.log(` Correctly rejected invalid correct answer`); } }); // Test 11: Invalid difficulty await runTest('Test 11: Invalid difficulty returns 400', async () => { const questionData = { questionText: 'Test question', questionType: 'trueFalse', correctAnswer: 'true', difficulty: 'invalid', categoryId: CATEGORY_IDS.JAVASCRIPT }; try { await axios.post(`${BASE_URL}/admin/questions`, questionData, { headers: { Authorization: `Bearer ${adminToken}` } }); 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 difficulty')) { throw new Error('Should mention invalid difficulty'); } console.log(` Correctly rejected invalid difficulty`); } }); // Test 12: Invalid category UUID await runTest('Test 12: Invalid category UUID returns 400', async () => { const questionData = { questionText: 'Test question', questionType: 'trueFalse', correctAnswer: 'true', difficulty: 'easy', categoryId: 'invalid-uuid' }; try { await axios.post(`${BASE_URL}/admin/questions`, questionData, { headers: { Authorization: `Bearer ${adminToken}` } }); 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 13: Non-existent category await runTest('Test 13: Non-existent category returns 404', async () => { const fakeUuid = '00000000-0000-0000-0000-000000000000'; const questionData = { questionText: 'Test question', questionType: 'trueFalse', correctAnswer: 'true', difficulty: 'easy', categoryId: fakeUuid }; try { await axios.post(`${BASE_URL}/admin/questions`, questionData, { headers: { Authorization: `Bearer ${adminToken}` } }); throw new Error('Should have returned 404'); } catch (error) { if (error.response?.status !== 404) throw new Error(`Expected 404, got ${error.response?.status}`); if (!error.response.data.message.includes('not found')) { throw new Error('Should mention category not found'); } console.log(` Correctly returned 404 for non-existent category`); } }); // Test 14: Invalid trueFalse answer await runTest('Test 14: Invalid trueFalse answer returns 400', async () => { const questionData = { questionText: 'Test true/false question', questionType: 'trueFalse', correctAnswer: 'yes', // Should be 'true' or 'false' difficulty: 'easy', categoryId: CATEGORY_IDS.JAVASCRIPT }; try { await axios.post(`${BASE_URL}/admin/questions`, questionData, { headers: { Authorization: `Bearer ${adminToken}` } }); 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('true') || !error.response.data.message.includes('false')) { throw new Error('Should mention true/false requirement'); } console.log(` Correctly rejected invalid trueFalse answer`); } }); // Test 15: Response structure validation await runTest('Test 15: Response structure validation', async () => { const questionData = { questionText: 'Structure test question', questionType: 'multiple', options: [ { id: 'a', text: 'Option A' }, { id: 'b', text: 'Option B' } ], correctAnswer: 'a', difficulty: 'easy', categoryId: CATEGORY_IDS.JAVASCRIPT, tags: ['test'], keywords: ['structure'] }; const response = await axios.post(`${BASE_URL}/admin/questions`, questionData, { headers: { Authorization: `Bearer ${adminToken}` } }); // Check top-level structure const requiredFields = ['success', 'data', 'message']; for (const field of requiredFields) { if (!(field in response.data)) throw new Error(`Missing field: ${field}`); } // Check question data structure const question = response.data.data; const questionFields = ['id', 'questionText', 'questionType', 'options', 'difficulty', 'points', 'explanation', 'tags', 'keywords', 'category', 'createdAt']; 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 correctAnswer is NOT exposed if ('correctAnswer' in question) { throw new Error('Correct answer should not be exposed in response'); } createdQuestionIds.push(question.id); console.log(` Response structure validated`); }); // Test 16: Tags and keywords validation await runTest('Test 16: Tags and keywords stored correctly', async () => { const questionData = { questionText: 'Test question with tags and keywords', questionType: 'trueFalse', correctAnswer: 'true', difficulty: 'easy', categoryId: CATEGORY_IDS.JAVASCRIPT, tags: ['tag1', 'tag2', 'tag3'], keywords: ['keyword1', 'keyword2'] }; const response = await axios.post(`${BASE_URL}/admin/questions`, questionData, { headers: { Authorization: `Bearer ${adminToken}` } }); if (!Array.isArray(response.data.data.tags)) throw new Error('Tags should be an array'); if (!Array.isArray(response.data.data.keywords)) throw new Error('Keywords should be an array'); if (response.data.data.tags.length !== 3) throw new Error('Tag count mismatch'); if (response.data.data.keywords.length !== 2) throw new Error('Keyword count mismatch'); createdQuestionIds.push(response.data.data.id); console.log(` Tags and keywords stored correctly`); }); // 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(`Created Questions: ${createdQuestionIds.length}`); 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); });