add changes
This commit is contained in:
547
backend/tests/test-complete-quiz.js
Normal file
547
backend/tests/test-complete-quiz.js
Normal file
@@ -0,0 +1,547 @@
|
||||
/**
|
||||
* Complete Quiz Session API Tests
|
||||
* Tests for POST /api/quiz/complete endpoint
|
||||
*/
|
||||
|
||||
const axios = require('axios');
|
||||
|
||||
const API_URL = 'http://localhost:3000/api';
|
||||
|
||||
// Test configuration
|
||||
let adminToken = null;
|
||||
let user1Token = null;
|
||||
let user2Token = null;
|
||||
let guestToken = null;
|
||||
let guestSessionId = null;
|
||||
|
||||
// Helper function to create auth config
|
||||
const authConfig = (token) => ({
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
|
||||
// Helper function for guest auth config
|
||||
const guestAuthConfig = (token) => ({
|
||||
headers: { 'X-Guest-Token': token }
|
||||
});
|
||||
|
||||
// Logging helper
|
||||
const log = (message, data = null) => {
|
||||
console.log(`\n${message}`);
|
||||
if (data) {
|
||||
console.log(JSON.stringify(data, null, 2));
|
||||
}
|
||||
};
|
||||
|
||||
// Test setup
|
||||
async function setup() {
|
||||
try {
|
||||
// Login as admin (to get categories)
|
||||
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 test users
|
||||
const timestamp = Date.now();
|
||||
|
||||
// User 1
|
||||
await axios.post(`${API_URL}/auth/register`, {
|
||||
username: `testcomplete1${timestamp}`,
|
||||
email: `testcomplete1${timestamp}@test.com`,
|
||||
password: 'Test@123'
|
||||
});
|
||||
const user1Login = await axios.post(`${API_URL}/auth/login`, {
|
||||
email: `testcomplete1${timestamp}@test.com`,
|
||||
password: 'Test@123'
|
||||
});
|
||||
user1Token = user1Login.data.data.token;
|
||||
console.log('✓ Logged in as testuser1');
|
||||
|
||||
// User 2
|
||||
await axios.post(`${API_URL}/auth/register`, {
|
||||
username: `testcomplete2${timestamp}`,
|
||||
email: `testcomplete2${timestamp}@test.com`,
|
||||
password: 'Test@123'
|
||||
});
|
||||
const user2Login = await axios.post(`${API_URL}/auth/login`, {
|
||||
email: `testcomplete2${timestamp}@test.com`,
|
||||
password: 'Test@123'
|
||||
});
|
||||
user2Token = user2Login.data.data.token;
|
||||
console.log('✓ Logged in as testuser2');
|
||||
|
||||
// Start guest session
|
||||
const guestResponse = await axios.post(`${API_URL}/guest/start-session`, {
|
||||
deviceId: `test-device-${timestamp}`
|
||||
});
|
||||
guestToken = guestResponse.data.data.sessionToken;
|
||||
guestSessionId = guestResponse.data.data.guestId;
|
||||
console.log('✓ Started guest session');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Setup failed:', error.response?.data || error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Test results tracking
|
||||
let testResults = {
|
||||
passed: 0,
|
||||
failed: 0,
|
||||
total: 0
|
||||
};
|
||||
|
||||
// Test runner
|
||||
async function runTest(testName, testFn) {
|
||||
testResults.total++;
|
||||
try {
|
||||
await testFn();
|
||||
console.log(`✓ ${testName} - PASSED`);
|
||||
testResults.passed++;
|
||||
} catch (error) {
|
||||
console.log(`✗ ${testName} - FAILED`);
|
||||
console.log(` ${error.message}`);
|
||||
testResults.failed++;
|
||||
}
|
||||
// Add delay to avoid rate limiting
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
}
|
||||
|
||||
// Helper: Create and complete a quiz session
|
||||
async function createAndAnswerQuiz(token, isGuest = false) {
|
||||
// Get categories
|
||||
const categoriesResponse = await axios.get(`${API_URL}/categories`,
|
||||
isGuest ? guestAuthConfig(token) : authConfig(token)
|
||||
);
|
||||
const categories = categoriesResponse.data.data;
|
||||
const category = categories.find(c => c.guestAccessible) || categories[0];
|
||||
|
||||
// Start quiz
|
||||
const quizResponse = await axios.post(
|
||||
`${API_URL}/quiz/start`,
|
||||
{
|
||||
categoryId: category.id,
|
||||
questionCount: 3,
|
||||
difficulty: 'mixed',
|
||||
quizType: 'practice'
|
||||
},
|
||||
isGuest ? guestAuthConfig(token) : authConfig(token)
|
||||
);
|
||||
|
||||
const sessionId = quizResponse.data.data.sessionId;
|
||||
const questions = quizResponse.data.data.questions;
|
||||
|
||||
// Submit answers for all questions
|
||||
for (const question of questions) {
|
||||
await axios.post(
|
||||
`${API_URL}/quiz/submit`,
|
||||
{
|
||||
quizSessionId: sessionId,
|
||||
questionId: question.id,
|
||||
userAnswer: 'a', // Use consistent answer
|
||||
timeTaken: 5
|
||||
},
|
||||
isGuest ? guestAuthConfig(token) : authConfig(token)
|
||||
);
|
||||
}
|
||||
|
||||
return { sessionId, totalQuestions: questions.length };
|
||||
}
|
||||
|
||||
// ==================== TESTS ====================
|
||||
|
||||
async function runTests() {
|
||||
console.log('\n========================================');
|
||||
console.log('Testing Complete Quiz Session API');
|
||||
console.log('========================================\n');
|
||||
|
||||
await setup();
|
||||
|
||||
// Test 1: Complete quiz with all questions answered
|
||||
await runTest('Complete quiz returns detailed results', async () => {
|
||||
const { sessionId } = await createAndAnswerQuiz(user1Token);
|
||||
|
||||
const response = await axios.post(
|
||||
`${API_URL}/quiz/complete`,
|
||||
{ sessionId },
|
||||
authConfig(user1Token)
|
||||
);
|
||||
|
||||
if (response.status !== 200) throw new Error(`Expected 200, got ${response.status}`);
|
||||
if (!response.data.success) throw new Error('Response success should be true');
|
||||
if (!response.data.data) throw new Error('Missing data in response');
|
||||
|
||||
const results = response.data.data;
|
||||
|
||||
// Validate structure
|
||||
if (!results.sessionId) throw new Error('Missing sessionId');
|
||||
if (!results.status) throw new Error('Missing status');
|
||||
if (!results.category) throw new Error('Missing category');
|
||||
if (!results.score) throw new Error('Missing score');
|
||||
if (!results.questions) throw new Error('Missing questions');
|
||||
if (!results.time) throw new Error('Missing time');
|
||||
if (typeof results.accuracy !== 'number') throw new Error('Missing or invalid accuracy');
|
||||
if (typeof results.isPassed !== 'boolean') throw new Error('Missing or invalid isPassed');
|
||||
|
||||
// Validate score structure
|
||||
if (typeof results.score.earned !== 'number') {
|
||||
console.log(' Score object:', JSON.stringify(results.score, null, 2));
|
||||
throw new Error(`Missing or invalid score.earned (type: ${typeof results.score.earned}, value: ${results.score.earned})`);
|
||||
}
|
||||
if (typeof results.score.total !== 'number') throw new Error('Missing score.total');
|
||||
if (typeof results.score.percentage !== 'number') throw new Error('Missing score.percentage');
|
||||
|
||||
// Validate questions structure
|
||||
if (results.questions.total !== 3) throw new Error('Expected 3 total questions');
|
||||
if (results.questions.answered !== 3) throw new Error('Expected 3 answered questions');
|
||||
|
||||
// Validate time structure
|
||||
if (!results.time.started) throw new Error('Missing time.started');
|
||||
if (!results.time.completed) throw new Error('Missing time.completed');
|
||||
if (typeof results.time.taken !== 'number') throw new Error('Missing time.taken');
|
||||
|
||||
console.log(` Score: ${results.score.earned}/${results.score.total} (${results.score.percentage}%)`);
|
||||
console.log(` Accuracy: ${results.accuracy}%`);
|
||||
console.log(` Passed: ${results.isPassed}`);
|
||||
});
|
||||
|
||||
// Test 2: Guest can complete quiz
|
||||
await runTest('Guest can complete quiz', async () => {
|
||||
const { sessionId } = await createAndAnswerQuiz(guestToken, true);
|
||||
|
||||
const response = await axios.post(
|
||||
`${API_URL}/quiz/complete`,
|
||||
{ sessionId },
|
||||
guestAuthConfig(guestToken)
|
||||
);
|
||||
|
||||
if (response.status !== 200) throw new Error(`Expected 200, got ${response.status}`);
|
||||
if (!response.data.success) throw new Error('Response success should be true');
|
||||
if (!response.data.data.sessionId) throw new Error('Missing sessionId in results');
|
||||
});
|
||||
|
||||
// Test 3: Percentage calculation is correct
|
||||
await runTest('Percentage calculated correctly', async () => {
|
||||
const { sessionId } = await createAndAnswerQuiz(user1Token);
|
||||
|
||||
const response = await axios.post(
|
||||
`${API_URL}/quiz/complete`,
|
||||
{ sessionId },
|
||||
authConfig(user1Token)
|
||||
);
|
||||
|
||||
const results = response.data.data;
|
||||
const expectedPercentage = Math.round((results.score.earned / results.score.total) * 100);
|
||||
|
||||
if (results.score.percentage !== expectedPercentage) {
|
||||
throw new Error(`Expected ${expectedPercentage}%, got ${results.score.percentage}%`);
|
||||
}
|
||||
});
|
||||
|
||||
// Test 4: Pass/fail determination (70% threshold)
|
||||
await runTest('Pass/fail determination works', async () => {
|
||||
const { sessionId } = await createAndAnswerQuiz(user1Token);
|
||||
|
||||
const response = await axios.post(
|
||||
`${API_URL}/quiz/complete`,
|
||||
{ sessionId },
|
||||
authConfig(user1Token)
|
||||
);
|
||||
|
||||
const results = response.data.data;
|
||||
const expectedPassed = results.score.percentage >= 70;
|
||||
|
||||
if (results.isPassed !== expectedPassed) {
|
||||
throw new Error(`Expected isPassed=${expectedPassed}, got ${results.isPassed}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Test 5: Time tracking works
|
||||
await runTest('Time tracking accurate', async () => {
|
||||
const { sessionId } = await createAndAnswerQuiz(user1Token);
|
||||
|
||||
// Wait 2 seconds before completing
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
const response = await axios.post(
|
||||
`${API_URL}/quiz/complete`,
|
||||
{ sessionId },
|
||||
authConfig(user1Token)
|
||||
);
|
||||
|
||||
const results = response.data.data;
|
||||
|
||||
if (results.time.taken < 2) {
|
||||
throw new Error(`Expected at least 2 seconds, got ${results.time.taken}`);
|
||||
}
|
||||
if (results.time.taken > 60) {
|
||||
throw new Error(`Time taken seems too long: ${results.time.taken}s`);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('\n========================================');
|
||||
console.log('Testing Validation');
|
||||
console.log('========================================\n');
|
||||
|
||||
// Test 6: Missing session ID returns 400
|
||||
await runTest('Missing session ID returns 400', async () => {
|
||||
try {
|
||||
await axios.post(
|
||||
`${API_URL}/quiz/complete`,
|
||||
{},
|
||||
authConfig(user1Token)
|
||||
);
|
||||
throw new Error('Should have thrown error');
|
||||
} catch (error) {
|
||||
if (error.response?.status !== 400) {
|
||||
throw new Error(`Expected 400, got ${error.response?.status}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Test 7: Invalid session UUID returns 400
|
||||
await runTest('Invalid session UUID returns 400', async () => {
|
||||
try {
|
||||
await axios.post(
|
||||
`${API_URL}/quiz/complete`,
|
||||
{ sessionId: 'invalid-uuid' },
|
||||
authConfig(user1Token)
|
||||
);
|
||||
throw new Error('Should have thrown error');
|
||||
} catch (error) {
|
||||
if (error.response?.status !== 400) {
|
||||
throw new Error(`Expected 400, got ${error.response?.status}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Test 8: Non-existent session returns 404
|
||||
await runTest('Non-existent session returns 404', async () => {
|
||||
try {
|
||||
await axios.post(
|
||||
`${API_URL}/quiz/complete`,
|
||||
{ sessionId: '00000000-0000-0000-0000-000000000000' },
|
||||
authConfig(user1Token)
|
||||
);
|
||||
throw new Error('Should have thrown error');
|
||||
} catch (error) {
|
||||
if (error.response?.status !== 404) {
|
||||
throw new Error(`Expected 404, got ${error.response?.status}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Test 9: Cannot complete another user's session
|
||||
await runTest('Cannot complete another user\'s session', async () => {
|
||||
const { sessionId } = await createAndAnswerQuiz(user1Token);
|
||||
|
||||
try {
|
||||
await axios.post(
|
||||
`${API_URL}/quiz/complete`,
|
||||
{ sessionId },
|
||||
authConfig(user2Token) // Different user
|
||||
);
|
||||
throw new Error('Should have thrown error');
|
||||
} catch (error) {
|
||||
if (error.response?.status !== 403) {
|
||||
throw new Error(`Expected 403, got ${error.response?.status}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Test 10: Cannot complete already completed session
|
||||
await runTest('Cannot complete already completed session', async () => {
|
||||
const { sessionId } = await createAndAnswerQuiz(user1Token);
|
||||
|
||||
// Complete once
|
||||
await axios.post(
|
||||
`${API_URL}/quiz/complete`,
|
||||
{ sessionId },
|
||||
authConfig(user1Token)
|
||||
);
|
||||
|
||||
// Try to complete again
|
||||
try {
|
||||
await axios.post(
|
||||
`${API_URL}/quiz/complete`,
|
||||
{ sessionId },
|
||||
authConfig(user1Token)
|
||||
);
|
||||
throw new Error('Should have thrown error');
|
||||
} catch (error) {
|
||||
if (error.response?.status !== 400) {
|
||||
throw new Error(`Expected 400, got ${error.response?.status}`);
|
||||
}
|
||||
if (!error.response.data.message.includes('already completed')) {
|
||||
throw new Error('Error message should mention already completed');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Test 11: Unauthenticated request blocked
|
||||
await runTest('Unauthenticated request blocked', async () => {
|
||||
const { sessionId } = await createAndAnswerQuiz(user1Token);
|
||||
|
||||
try {
|
||||
await axios.post(
|
||||
`${API_URL}/quiz/complete`,
|
||||
{ sessionId }
|
||||
// No auth headers
|
||||
);
|
||||
throw new Error('Should have thrown error');
|
||||
} catch (error) {
|
||||
if (error.response?.status !== 401) {
|
||||
throw new Error(`Expected 401, got ${error.response?.status}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log('\n========================================');
|
||||
console.log('Testing Partial Completion');
|
||||
console.log('========================================\n');
|
||||
|
||||
// Test 12: Can complete with unanswered questions
|
||||
await runTest('Can complete with unanswered questions', async () => {
|
||||
// Get category with most questions
|
||||
const categoriesResponse = await axios.get(`${API_URL}/categories`, authConfig(user1Token));
|
||||
const category = categoriesResponse.data.data.sort((a, b) => b.questionCount - a.questionCount)[0];
|
||||
|
||||
// Start quiz with requested questions (but we'll only answer some)
|
||||
const requestedCount = Math.min(5, category.questionCount); // Don't request more than available
|
||||
if (requestedCount < 3) {
|
||||
console.log(' Skipping - not enough questions in category');
|
||||
return; // Skip if not enough questions
|
||||
}
|
||||
|
||||
const quizResponse = await axios.post(
|
||||
`${API_URL}/quiz/start`,
|
||||
{
|
||||
categoryId: category.id,
|
||||
questionCount: requestedCount,
|
||||
difficulty: 'mixed',
|
||||
quizType: 'practice'
|
||||
},
|
||||
authConfig(user1Token)
|
||||
);
|
||||
|
||||
const sessionId = quizResponse.data.data.sessionId;
|
||||
const questions = quizResponse.data.data.questions;
|
||||
const actualCount = questions.length;
|
||||
|
||||
if (actualCount < 3) {
|
||||
console.log(' Skipping - not enough questions returned');
|
||||
return;
|
||||
}
|
||||
|
||||
// Answer only 2 questions (leaving others unanswered)
|
||||
await axios.post(
|
||||
`${API_URL}/quiz/submit`,
|
||||
{
|
||||
quizSessionId: sessionId,
|
||||
questionId: questions[0].id,
|
||||
userAnswer: 'a',
|
||||
timeTaken: 5
|
||||
},
|
||||
authConfig(user1Token)
|
||||
);
|
||||
|
||||
await axios.post(
|
||||
`${API_URL}/quiz/submit`,
|
||||
{
|
||||
quizSessionId: sessionId,
|
||||
questionId: questions[1].id,
|
||||
userAnswer: 'b',
|
||||
timeTaken: 5
|
||||
},
|
||||
authConfig(user1Token)
|
||||
);
|
||||
|
||||
// Complete quiz
|
||||
const response = await axios.post(
|
||||
`${API_URL}/quiz/complete`,
|
||||
{ sessionId },
|
||||
authConfig(user1Token)
|
||||
);
|
||||
|
||||
const results = response.data.data;
|
||||
|
||||
if (results.questions.total !== actualCount) {
|
||||
throw new Error(`Expected ${actualCount} total questions, got ${results.questions.total}`);
|
||||
}
|
||||
if (results.questions.answered !== 2) throw new Error('Expected 2 answered questions');
|
||||
if (results.questions.unanswered !== actualCount - 2) {
|
||||
throw new Error(`Expected ${actualCount - 2} unanswered questions, got ${results.questions.unanswered}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Test 13: Status updated to completed
|
||||
await runTest('Session status updated to completed', async () => {
|
||||
const { sessionId } = await createAndAnswerQuiz(user1Token);
|
||||
|
||||
const response = await axios.post(
|
||||
`${API_URL}/quiz/complete`,
|
||||
{ sessionId },
|
||||
authConfig(user1Token)
|
||||
);
|
||||
|
||||
const results = response.data.data;
|
||||
|
||||
if (results.status !== 'completed') {
|
||||
throw new Error(`Expected status 'completed', got '${results.status}'`);
|
||||
}
|
||||
});
|
||||
|
||||
// Test 14: Category info included in results
|
||||
await runTest('Category info included in results', async () => {
|
||||
const { sessionId } = await createAndAnswerQuiz(user1Token);
|
||||
|
||||
const response = await axios.post(
|
||||
`${API_URL}/quiz/complete`,
|
||||
{ sessionId },
|
||||
authConfig(user1Token)
|
||||
);
|
||||
|
||||
const results = response.data.data;
|
||||
|
||||
if (!results.category.id) throw new Error('Missing category.id');
|
||||
if (!results.category.name) throw new Error('Missing category.name');
|
||||
if (!results.category.slug) throw new Error('Missing category.slug');
|
||||
});
|
||||
|
||||
// Test 15: Correct/incorrect counts accurate
|
||||
await runTest('Correct/incorrect counts accurate', async () => {
|
||||
const { sessionId, totalQuestions } = await createAndAnswerQuiz(user1Token);
|
||||
|
||||
const response = await axios.post(
|
||||
`${API_URL}/quiz/complete`,
|
||||
{ sessionId },
|
||||
authConfig(user1Token)
|
||||
);
|
||||
|
||||
const results = response.data.data;
|
||||
|
||||
const sumCheck = results.questions.correct + results.questions.incorrect + results.questions.unanswered;
|
||||
if (sumCheck !== totalQuestions) {
|
||||
throw new Error(`Question counts don't add up: ${sumCheck} !== ${totalQuestions}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Print summary
|
||||
console.log('\n========================================');
|
||||
console.log('Test Summary');
|
||||
console.log('========================================\n');
|
||||
console.log(`Passed: ${testResults.passed}`);
|
||||
console.log(`Failed: ${testResults.failed}`);
|
||||
console.log(`Total: ${testResults.total}`);
|
||||
console.log('========================================\n');
|
||||
|
||||
process.exit(testResults.failed > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
// Run all tests
|
||||
runTests().catch(error => {
|
||||
console.error('Test execution failed:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user