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