Files
tasks-backend/tests/test-start-quiz.js
2025-12-26 23:56:32 +02:00

538 lines
20 KiB
JavaScript

/**
* Test Script: Start Quiz Session API
*
* Tests:
* - Start quiz as authenticated user
* - Start quiz as guest user
* - Guest quiz limit enforcement
* - Category validation
* - Question selection and randomization
* - Various quiz types and difficulties
*/
const axios = require('axios');
require('dotenv').config();
const BASE_URL = process.env.API_URL || 'http://localhost:3000';
const API_URL = `${BASE_URL}/api`;
// Test users
let adminToken = null;
let userToken = null;
let guestToken = null;
let guestId = null;
// Test data
let testCategoryId = null;
let guestCategoryId = null;
// Test results
const results = {
passed: 0,
failed: 0,
total: 0
};
// Helper function to log test results
function logTest(testName, passed, details = '') {
results.total++;
if (passed) {
results.passed++;
console.log(`✓ Test ${results.total}: ${testName} - PASSED`);
if (details) console.log(` ${details}`);
} else {
results.failed++;
console.log(`✗ Test ${results.total}: ${testName} - FAILED`);
if (details) console.log(` ${details}`);
}
}
// Helper to create axios config with auth
function authConfig(token) {
return {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
};
}
// Helper to create axios config with guest token
function guestConfig(token) {
return {
headers: {
'X-Guest-Token': token,
'Content-Type': 'application/json'
}
};
}
async function runTests() {
console.log('========================================');
console.log('Testing Start Quiz Session API');
console.log('========================================\n');
try {
// ==========================================
// Setup: Login and get categories
// ==========================================
// Login as admin
const adminLogin = await axios.post(`${API_URL}/auth/login`, {
email: 'admin@quiz.com',
password: 'Admin@123'
});
adminToken = adminLogin.data.data.token;
console.log('✓ Logged in as admin');
// Register and login as regular user
const timestamp = Date.now();
const userRes = await axios.post(`${API_URL}/auth/register`, {
username: `quizuser${timestamp}`,
email: `quizuser${timestamp}@test.com`,
password: 'Test@123'
});
userToken = userRes.data.data.token;
console.log('✓ Created and logged in as regular user');
// Start guest session
const guestRes = await axios.post(`${API_URL}/guest/start-session`, {
deviceId: `test-device-${timestamp}`
});
guestToken = guestRes.data.data.sessionToken;
guestId = guestRes.data.data.guestId;
console.log('✓ Started guest session\n');
// Get test categories - use JavaScript which has questions
const categoriesRes = await axios.get(`${API_URL}/categories`, authConfig(adminToken));
guestCategoryId = categoriesRes.data.data.find(c => c.name === 'JavaScript')?.id; // JavaScript has questions
testCategoryId = guestCategoryId; // Use same category for all tests since it has questions
console.log(`✓ Using test category: ${testCategoryId} (JavaScript - has questions)\n`);
// ==========================================
// AUTHENTICATED USER QUIZ TESTS
// ==========================================
// Test 1: User starts quiz successfully
try {
const res = await axios.post(`${API_URL}/quiz/start`, {
categoryId: testCategoryId,
questionCount: 5,
difficulty: 'medium',
quizType: 'practice'
}, authConfig(userToken));
const passed = res.status === 201
&& res.data.success === true
&& res.data.data.sessionId
&& res.data.data.questions.length > 0 // At least some questions
&& res.data.data.difficulty === 'medium';
logTest('User starts quiz successfully', passed,
passed ? `Session ID: ${res.data.data.sessionId}, ${res.data.data.questions.length} questions` : `Response: ${JSON.stringify(res.data)}`);
} catch (error) {
logTest('User starts quiz successfully', false, error.response?.data?.message || error.message);
}
// Test 2: User starts quiz with mixed difficulty
try {
const res = await axios.post(`${API_URL}/quiz/start`, {
categoryId: testCategoryId,
questionCount: 10,
difficulty: 'mixed',
quizType: 'practice'
}, authConfig(userToken));
const passed = res.status === 201
&& res.data.data.difficulty === 'mixed'
&& res.data.data.questions.length <= 10;
logTest('User starts quiz with mixed difficulty', passed,
passed ? `Got ${res.data.data.questions.length} mixed difficulty questions` : `Response: ${JSON.stringify(res.data)}`);
} catch (error) {
logTest('User starts quiz with mixed difficulty', false, error.response?.data?.message || error.message);
}
// Test 3: User starts timed quiz (has time limit)
try {
const res = await axios.post(`${API_URL}/quiz/start`, {
categoryId: testCategoryId,
questionCount: 5,
difficulty: 'mixed', // Use mixed to ensure we get questions
quizType: 'timed'
}, authConfig(userToken));
const passed = res.status === 201
&& res.data.data.quizType === 'timed'
&& res.data.data.timeLimit !== null
&& res.data.data.timeLimit === res.data.data.questions.length * 2; // 2 min per question
logTest('User starts timed quiz with time limit', passed,
passed ? `Time limit: ${res.data.data.timeLimit} minutes for ${res.data.data.questions.length} questions` : `Response: ${JSON.stringify(res.data)}`);
} catch (error) {
logTest('User starts timed quiz with time limit', false, error.response?.data?.message || error.message);
}
// Test 4: Questions don't expose correct answers
try {
const res = await axios.post(`${API_URL}/quiz/start`, {
categoryId: guestCategoryId,
questionCount: 3,
difficulty: 'easy',
quizType: 'practice'
}, authConfig(userToken));
const hasCorrectAnswer = res.data.data.questions.some(q => q.correctAnswer !== undefined);
const passed = res.status === 201 && !hasCorrectAnswer;
logTest('Questions don\'t expose correct answers', passed,
passed ? 'Correct answers properly hidden' : 'Correct answers exposed in response!');
} catch (error) {
logTest('Questions don\'t expose correct answers', false, error.response?.data?.message || error.message);
}
// Test 5: Response includes category info
try {
const res = await axios.post(`${API_URL}/quiz/start`, {
categoryId: testCategoryId,
questionCount: 5,
difficulty: 'mixed', // Use mixed to ensure we get questions
quizType: 'practice'
}, authConfig(userToken));
const passed = res.status === 201
&& res.data.data.category
&& res.data.data.category.name
&& res.data.data.category.icon
&& res.data.data.category.color;
logTest('Response includes category info', passed,
passed ? `Category: ${res.data.data.category.name}` : `Response: ${JSON.stringify(res.data)}`);
} catch (error) {
logTest('Response includes category info', false, error.response?.data?.message || error.message);
}
// Test 6: Total points calculated correctly
try {
const res = await axios.post(`${API_URL}/quiz/start`, {
categoryId: testCategoryId,
questionCount: 5,
difficulty: 'medium',
quizType: 'practice'
}, authConfig(userToken));
const calculatedPoints = res.data.data.questions.reduce((sum, q) => sum + q.points, 0);
const passed = res.status === 201
&& res.data.data.totalPoints === calculatedPoints;
logTest('Total points calculated correctly', passed,
passed ? `Total: ${res.data.data.totalPoints} points` : `Expected ${calculatedPoints}, got ${res.data.data.totalPoints}`);
} catch (error) {
logTest('Total points calculated correctly', false, error.response?.data?.message || error.message);
}
// ==========================================
// GUEST USER QUIZ TESTS
// ==========================================
console.log('\n--- Testing Guest Quiz Sessions ---\n');
// Test 7: Guest starts quiz in accessible category
try {
const res = await axios.post(`${API_URL}/quiz/start`, {
categoryId: guestCategoryId,
questionCount: 5,
difficulty: 'easy',
quizType: 'practice'
}, guestConfig(guestToken));
const passed = res.status === 201
&& res.data.success === true
&& res.data.data.questions.length === 5;
logTest('Guest starts quiz in accessible category', passed,
passed ? `Quiz started with ${res.data.data.questions.length} questions` : `Response: ${JSON.stringify(res.data)}`);
} catch (error) {
logTest('Guest starts quiz in accessible category', false, error.response?.data?.message || error.message);
}
// Test 8: Guest blocked from non-accessible category
try {
// Find a non-guest accessible category
const nonGuestCategory = categoriesRes.data.data.find(c => !c.guestAccessible)?.id;
if (nonGuestCategory) {
const res = await axios.post(`${API_URL}/quiz/start`, {
categoryId: nonGuestCategory, // Non-guest accessible category
questionCount: 5,
difficulty: 'easy',
quizType: 'practice'
}, guestConfig(guestToken));
logTest('Guest blocked from non-accessible category', false, 'Should have returned 403');
} else {
logTest('Guest blocked from non-accessible category', true, 'Skipped - no non-guest categories available');
}
} catch (error) {
const passed = error.response?.status === 403;
logTest('Guest blocked from non-accessible category', passed,
passed ? 'Correctly blocked with 403' : `Status: ${error.response?.status}`);
}
// Test 9: Guest quiz count incremented
try {
// Get initial count
const beforeRes = await axios.get(`${API_URL}/guest/quiz-limit`, guestConfig(guestToken));
const beforeCount = beforeRes.data.data.quizLimit.quizzesAttempted;
// Start another quiz
await axios.post(`${API_URL}/quiz/start`, {
categoryId: guestCategoryId,
questionCount: 3,
difficulty: 'medium',
quizType: 'practice'
}, guestConfig(guestToken));
// Check count after
const afterRes = await axios.get(`${API_URL}/guest/quiz-limit`, guestConfig(guestToken));
const afterCount = afterRes.data.data.quizLimit.quizzesAttempted;
const passed = afterCount === beforeCount + 1;
logTest('Guest quiz count incremented', passed,
passed ? `Count: ${beforeCount}${afterCount}` : `Expected ${beforeCount + 1}, got ${afterCount}`);
} catch (error) {
logTest('Guest quiz count incremented', false, error.response?.data?.message || error.message);
}
// Test 10: Guest quiz limit enforced (reach limit)
try {
// Start quiz until limit reached
const limitRes = await axios.get(`${API_URL}/guest/quiz-limit`, guestConfig(guestToken));
const remaining = limitRes.data.data.quizLimit.quizzesRemaining;
// Try to start more quizzes than remaining
for (let i = 0; i < remaining; i++) {
await axios.post(`${API_URL}/quiz/start`, {
categoryId: guestCategoryId,
questionCount: 1,
difficulty: 'easy',
quizType: 'practice'
}, guestConfig(guestToken));
}
// This should fail
try {
await axios.post(`${API_URL}/quiz/start`, {
categoryId: guestCategoryId,
questionCount: 1,
difficulty: 'easy',
quizType: 'practice'
}, guestConfig(guestToken));
logTest('Guest quiz limit enforced', false, 'Should have blocked at limit');
} catch (limitError) {
const passed = limitError.response?.status === 403;
logTest('Guest quiz limit enforced', passed,
passed ? 'Correctly blocked when limit reached' : `Status: ${limitError.response?.status}`);
}
} catch (error) {
logTest('Guest quiz limit enforced', false, error.response?.data?.message || error.message);
}
// ==========================================
// VALIDATION TESTS
// ==========================================
console.log('\n--- Testing Validation ---\n');
// Test 11: Missing category ID returns 400
try {
const res = await axios.post(`${API_URL}/quiz/start`, {
questionCount: 5,
difficulty: 'easy'
}, authConfig(userToken));
logTest('Missing category ID returns 400', false, 'Should have returned 400');
} catch (error) {
const passed = error.response?.status === 400;
logTest('Missing category ID returns 400', passed,
passed ? 'Correctly rejected missing category' : `Status: ${error.response?.status}`);
}
// Test 12: Invalid category UUID returns 400
try {
const res = await axios.post(`${API_URL}/quiz/start`, {
categoryId: 'invalid-uuid',
questionCount: 5,
difficulty: 'easy'
}, authConfig(userToken));
logTest('Invalid category UUID returns 400', false, 'Should have returned 400');
} catch (error) {
const passed = error.response?.status === 400;
logTest('Invalid category UUID returns 400', passed,
passed ? 'Correctly rejected invalid UUID' : `Status: ${error.response?.status}`);
}
// Test 13: Non-existent category returns 404
try {
const fakeUuid = '00000000-0000-0000-0000-000000000000';
const res = await axios.post(`${API_URL}/quiz/start`, {
categoryId: fakeUuid,
questionCount: 5,
difficulty: 'easy'
}, authConfig(userToken));
logTest('Non-existent category returns 404', false, 'Should have returned 404');
} catch (error) {
const passed = error.response?.status === 404;
logTest('Non-existent category returns 404', passed,
passed ? 'Correctly returned 404' : `Status: ${error.response?.status}`);
}
// Test 14: Invalid question count rejected
try {
const res = await axios.post(`${API_URL}/quiz/start`, {
categoryId: testCategoryId,
questionCount: 100, // Exceeds max of 50
difficulty: 'easy'
}, authConfig(userToken));
logTest('Invalid question count rejected', false, 'Should have returned 400');
} catch (error) {
const passed = error.response?.status === 400;
logTest('Invalid question count rejected', passed,
passed ? 'Correctly rejected count > 50' : `Status: ${error.response?.status}`);
}
// Test 15: Invalid difficulty rejected
try {
const res = await axios.post(`${API_URL}/quiz/start`, {
categoryId: testCategoryId,
questionCount: 5,
difficulty: 'extreme'
}, authConfig(userToken));
logTest('Invalid difficulty rejected', false, 'Should have returned 400');
} catch (error) {
const passed = error.response?.status === 400;
logTest('Invalid difficulty rejected', passed,
passed ? 'Correctly rejected invalid difficulty' : `Status: ${error.response?.status}`);
}
// Test 16: Invalid quiz type rejected
try {
const res = await axios.post(`${API_URL}/quiz/start`, {
categoryId: testCategoryId,
questionCount: 5,
difficulty: 'easy',
quizType: 'invalid'
}, authConfig(userToken));
logTest('Invalid quiz type rejected', false, 'Should have returned 400');
} catch (error) {
const passed = error.response?.status === 400;
logTest('Invalid quiz type rejected', passed,
passed ? 'Correctly rejected invalid quiz type' : `Status: ${error.response?.status}`);
}
// Test 17: Unauthenticated request blocked
try {
const res = await axios.post(`${API_URL}/quiz/start`, {
categoryId: testCategoryId,
questionCount: 5,
difficulty: 'easy'
});
logTest('Unauthenticated request blocked', false, 'Should have returned 401');
} catch (error) {
const passed = error.response?.status === 401;
logTest('Unauthenticated request blocked', passed,
passed ? 'Correctly blocked with 401' : `Status: ${error.response?.status}`);
}
// Test 18: Default values applied correctly
try {
const res = await axios.post(`${API_URL}/quiz/start`, {
categoryId: testCategoryId
// No questionCount, difficulty, or quizType specified
}, authConfig(userToken));
const passed = res.status === 201
&& res.data.data.totalQuestions <= 10 // Up to default question count (might be less if not enough questions)
&& res.data.data.difficulty === 'mixed' // Default difficulty
&& res.data.data.quizType === 'practice'; // Default quiz type
logTest('Default values applied correctly', passed,
passed ? `Defaults applied: ${res.data.data.totalQuestions} questions, mixed difficulty, practice type` : `Response: ${JSON.stringify(res.data)}`);
} catch (error) {
logTest('Default values applied correctly', false, error.response?.data?.message || error.message);
}
// Test 19: Questions have proper structure
try {
const res = await axios.post(`${API_URL}/quiz/start`, {
categoryId: testCategoryId,
questionCount: 3,
difficulty: 'easy'
}, authConfig(userToken));
const firstQuestion = res.data.data.questions[0];
const passed = res.status === 201
&& firstQuestion.id
&& firstQuestion.questionText
&& firstQuestion.questionType
&& firstQuestion.difficulty
&& firstQuestion.points
&& firstQuestion.order
&& !firstQuestion.correctAnswer; // Should not be exposed
logTest('Questions have proper structure', passed,
passed ? 'All required fields present, correctAnswer hidden' : `Question: ${JSON.stringify(firstQuestion)}`);
} catch (error) {
logTest('Questions have proper structure', false, error.response?.data?.message || error.message);
}
// Test 20: Question order is sequential
try {
const res = await axios.post(`${API_URL}/quiz/start`, {
categoryId: testCategoryId,
questionCount: 5,
difficulty: 'medium'
}, authConfig(userToken));
const orders = res.data.data.questions.map(q => q.order);
const isSequential = orders.every((order, index) => order === index + 1);
const passed = res.status === 201 && isSequential;
logTest('Question order is sequential', passed,
passed ? `Orders: ${orders.join(', ')}` : `Orders: ${orders.join(', ')}`);
} catch (error) {
logTest('Question order is sequential', false, error.response?.data?.message || error.message);
}
} catch (error) {
console.error('\n❌ Fatal error during tests:', error.message);
console.error('Error details:', error);
if (error.response) {
console.error('Response:', error.response.data);
}
if (error.stack) {
console.error('Stack:', error.stack);
}
}
// ==========================================
// Summary
// ==========================================
console.log('\n========================================');
console.log('Test Summary');
console.log('========================================');
console.log(`Passed: ${results.passed}`);
console.log(`Failed: ${results.failed}`);
console.log(`Total: ${results.total}`);
console.log('========================================\n');
if (results.failed === 0) {
console.log('✓ All tests passed!\n');
process.exit(0);
} else {
console.log(`${results.failed} test(s) failed.\n`);
process.exit(1);
}
}
// Run tests
runTests();