add changes
This commit is contained in:
537
backend/tests/test-start-quiz.js
Normal file
537
backend/tests/test-start-quiz.js
Normal file
@@ -0,0 +1,537 @@
|
||||
/**
|
||||
* 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();
|
||||
Reference in New Issue
Block a user