add changes

This commit is contained in:
AD2025
2025-12-26 23:56:32 +02:00
parent 410c3d725f
commit e7d26bc981
127 changed files with 36162 additions and 0 deletions

View 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);
});