Files
Tasks/backend/tests/test-review-quiz.js
2025-12-25 00:24:11 +02:00

651 lines
21 KiB
JavaScript

const axios = require('axios');
const API_URL = 'http://localhost:3000/api';
// Test data
let testUser = {
email: 'reviewtest@example.com',
password: 'Test@123',
username: 'reviewtester'
};
let secondUser = {
email: 'otherreviewer@example.com',
password: 'Test@123',
username: 'otherreviewer'
};
let userToken = null;
let secondUserToken = null;
let guestToken = null;
let guestId = null;
let testCategory = null;
let completedSessionId = null;
let inProgressSessionId = null;
let guestCompletedSessionId = null;
// Helper to add delay between tests
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
// Helper to create and complete a quiz
async function createAndCompleteQuiz(token, isGuest = false, questionCount = 3) {
const headers = isGuest
? { 'X-Guest-Token': token }
: { 'Authorization': `Bearer ${token}` };
// Get categories
const categoriesRes = await axios.get(`${API_URL}/categories`, { headers });
const categories = categoriesRes.data.data;
const category = categories.find(c => c.questionCount >= questionCount);
if (!category) {
throw new Error('No category with enough questions found');
}
// Start quiz
const startRes = await axios.post(`${API_URL}/quiz/start`, {
categoryId: category.id,
questionCount,
difficulty: 'mixed',
quizType: 'practice'
}, { headers });
const sessionId = startRes.data.data.sessionId;
const questions = startRes.data.data.questions;
// Submit answers for all questions
for (const question of questions) {
let answer;
if (question.questionType === 'multiple') {
answer = question.options[0].id;
} else if (question.questionType === 'trueFalse') {
answer = 'true';
} else {
answer = 'Sample answer';
}
await axios.post(`${API_URL}/quiz/submit`, {
quizSessionId: sessionId,
questionId: question.id,
userAnswer: answer,
timeTaken: Math.floor(Math.random() * 20) + 5 // 5-25 seconds
}, { headers });
await delay(100);
}
// Complete quiz
await axios.post(`${API_URL}/quiz/complete`, {
sessionId
}, { headers });
return sessionId;
}
// Test setup
async function setup() {
console.log('Setting up test data...\n');
try {
// Register first user
try {
const registerRes = await axios.post(`${API_URL}/auth/register`, testUser);
userToken = registerRes.data.data.token;
console.log('✓ First user registered');
} catch (error) {
const loginRes = await axios.post(`${API_URL}/auth/login`, {
email: testUser.email,
password: testUser.password
});
userToken = loginRes.data.data.token;
console.log('✓ First user logged in');
}
// Register second user
try {
const registerRes = await axios.post(`${API_URL}/auth/register`, secondUser);
secondUserToken = registerRes.data.data.token;
console.log('✓ Second user registered');
} catch (error) {
const loginRes = await axios.post(`${API_URL}/auth/login`, {
email: secondUser.email,
password: secondUser.password
});
secondUserToken = loginRes.data.data.token;
console.log('✓ Second user logged in');
}
// Create guest session
const guestRes = await axios.post(`${API_URL}/guest/start-session`);
guestToken = guestRes.data.data.sessionToken;
guestId = guestRes.data.data.guestId;
console.log('✓ Guest session created');
// Get a test category
const categoriesRes = await axios.get(`${API_URL}/categories`, {
headers: { 'Authorization': `Bearer ${userToken}` }
});
const categories = categoriesRes.data.data;
// Sort by questionCount descending to get category with most questions
categories.sort((a, b) => b.questionCount - a.questionCount);
testCategory = categories.find(c => c.questionCount >= 3);
if (!testCategory) {
throw new Error('No category with enough questions found');
}
console.log(`✓ Test category selected: ${testCategory.name} (${testCategory.questionCount} questions)`);
await delay(500);
// Create completed quiz for user (use available question count, max 5)
const quizQuestionCount = Math.min(testCategory.questionCount, 5);
completedSessionId = await createAndCompleteQuiz(userToken, false, quizQuestionCount);
console.log(`✓ User completed session created (${quizQuestionCount} questions)`);
await delay(500);
// Create in-progress quiz for user
const startRes = await axios.post(`${API_URL}/quiz/start`, {
categoryId: testCategory.id,
questionCount: 3,
difficulty: 'easy',
quizType: 'practice'
}, {
headers: { 'Authorization': `Bearer ${userToken}` }
});
inProgressSessionId = startRes.data.data.sessionId;
// Submit one answer to make it in-progress
const questions = startRes.data.data.questions;
let answer = questions[0].questionType === 'multiple'
? questions[0].options[0].id
: 'true';
await axios.post(`${API_URL}/quiz/submit`, {
quizSessionId: inProgressSessionId,
questionId: questions[0].id,
userAnswer: answer,
timeTaken: 10
}, {
headers: { 'Authorization': `Bearer ${userToken}` }
});
console.log('✓ User in-progress session created');
await delay(500);
// Create completed quiz for guest
guestCompletedSessionId = await createAndCompleteQuiz(guestToken, true, 3);
console.log('✓ Guest completed session created\n');
await delay(500);
} catch (error) {
console.error('Setup failed:', error.response?.data || error.message);
throw error;
}
}
// Test cases
const tests = [
{
name: 'Test 1: Review completed quiz (user)',
run: async () => {
const response = await axios.get(`${API_URL}/quiz/review/${completedSessionId}`, {
headers: { 'Authorization': `Bearer ${userToken}` }
});
if (response.status !== 200) throw new Error('Expected 200 status');
if (!response.data.success) throw new Error('Expected success true');
const { session, summary, questions } = response.data.data;
// Validate session
if (!session.id || session.id !== completedSessionId) throw new Error('Invalid session id');
if (session.status !== 'completed') throw new Error('Expected completed status');
if (!session.category || !session.category.name) throw new Error('Missing category info');
// Validate summary
if (typeof summary.score.earned !== 'number') throw new Error('Score.earned should be number');
if (typeof summary.accuracy !== 'number') throw new Error('Accuracy should be number');
if (typeof summary.isPassed !== 'boolean') throw new Error('isPassed should be boolean');
if (summary.questions.total < 3) throw new Error('Expected at least 3 total questions');
// Validate questions
if (questions.length < 3) throw new Error('Expected at least 3 questions');
// All questions should have correct answers shown
questions.forEach((q, idx) => {
if (q.correctAnswer === undefined) {
throw new Error(`Question ${idx + 1} should show correct answer`);
}
if (q.resultStatus === undefined) {
throw new Error(`Question ${idx + 1} should have resultStatus`);
}
if (q.showExplanation !== true) {
throw new Error(`Question ${idx + 1} should have showExplanation`);
}
});
return '✓ Completed quiz review correct';
}
},
{
name: 'Test 2: Review guest completed quiz',
run: async () => {
const response = await axios.get(`${API_URL}/quiz/review/${guestCompletedSessionId}`, {
headers: { 'X-Guest-Token': guestToken }
});
if (response.status !== 200) throw new Error('Expected 200 status');
const { session, summary, questions } = response.data.data;
if (session.id !== guestCompletedSessionId) throw new Error('Invalid session id');
if (session.status !== 'completed') throw new Error('Expected completed status');
if (questions.length !== 3) throw new Error('Expected 3 questions');
return '✓ Guest quiz review works';
}
},
{
name: 'Test 3: Cannot review in-progress quiz (400)',
run: async () => {
try {
await axios.get(`${API_URL}/quiz/review/${inProgressSessionId}`, {
headers: { 'Authorization': `Bearer ${userToken}` }
});
throw new Error('Should have failed with 400');
} catch (error) {
if (error.response?.status !== 400) {
throw new Error(`Expected 400, got ${error.response?.status}`);
}
if (!error.response?.data?.message?.includes('completed')) {
throw new Error('Error message should mention completed status');
}
return '✓ In-progress quiz review blocked';
}
}
},
{
name: 'Test 4: Missing session ID returns 400',
run: async () => {
try {
await axios.get(`${API_URL}/quiz/review/`, {
headers: { 'Authorization': `Bearer ${userToken}` }
});
throw new Error('Should have failed');
} catch (error) {
if (error.response?.status !== 404 && error.response?.status !== 400) {
throw new Error(`Expected 400 or 404, got ${error.response?.status}`);
}
return '✓ Missing session ID handled';
}
}
},
{
name: 'Test 5: Invalid UUID format returns 400',
run: async () => {
try {
await axios.get(`${API_URL}/quiz/review/invalid-uuid`, {
headers: { 'Authorization': `Bearer ${userToken}` }
});
throw new Error('Should have failed with 400');
} catch (error) {
if (error.response?.status !== 400) {
throw new Error(`Expected 400, got ${error.response?.status}`);
}
return '✓ Invalid UUID returns 400';
}
}
},
{
name: 'Test 6: Non-existent session returns 404',
run: async () => {
try {
const fakeUuid = '00000000-0000-0000-0000-000000000000';
await axios.get(`${API_URL}/quiz/review/${fakeUuid}`, {
headers: { 'Authorization': `Bearer ${userToken}` }
});
throw new Error('Should have failed with 404');
} catch (error) {
if (error.response?.status !== 404) {
throw new Error(`Expected 404, got ${error.response?.status}`);
}
return '✓ Non-existent session returns 404';
}
}
},
{
name: 'Test 7: Cannot access other user\'s quiz review (403)',
run: async () => {
try {
await axios.get(`${API_URL}/quiz/review/${completedSessionId}`, {
headers: { 'Authorization': `Bearer ${secondUserToken}` }
});
throw new Error('Should have failed with 403');
} catch (error) {
if (error.response?.status !== 403) {
throw new Error(`Expected 403, got ${error.response?.status}`);
}
return '✓ Cross-user access blocked';
}
}
},
{
name: 'Test 8: Unauthenticated request returns 401',
run: async () => {
try {
await axios.get(`${API_URL}/quiz/review/${completedSessionId}`);
throw new Error('Should have failed with 401');
} catch (error) {
if (error.response?.status !== 401) {
throw new Error(`Expected 401, got ${error.response?.status}`);
}
return '✓ Unauthenticated request blocked';
}
}
},
{
name: 'Test 9: Response includes all required session fields',
run: async () => {
const response = await axios.get(`${API_URL}/quiz/review/${completedSessionId}`, {
headers: { 'Authorization': `Bearer ${userToken}` }
});
const { session } = response.data.data;
const requiredFields = [
'id', 'status', 'quizType', 'difficulty', 'category',
'startedAt', 'completedAt', 'timeSpent'
];
requiredFields.forEach(field => {
if (!(field in session)) {
throw new Error(`Missing required session field: ${field}`);
}
});
return '✓ All required session fields present';
}
},
{
name: 'Test 10: Response includes all required summary fields',
run: async () => {
const response = await axios.get(`${API_URL}/quiz/review/${completedSessionId}`, {
headers: { 'Authorization': `Bearer ${userToken}` }
});
const { summary } = response.data.data;
// Score fields
if (!summary.score || typeof summary.score.earned !== 'number') {
throw new Error('Missing or invalid score.earned');
}
if (typeof summary.score.total !== 'number') {
throw new Error('Missing or invalid score.total');
}
if (typeof summary.score.percentage !== 'number') {
throw new Error('Missing or invalid score.percentage');
}
// Questions summary
const qFields = ['total', 'answered', 'correct', 'incorrect', 'unanswered'];
qFields.forEach(field => {
if (typeof summary.questions[field] !== 'number') {
throw new Error(`Missing or invalid questions.${field}`);
}
});
// Other fields
if (typeof summary.accuracy !== 'number') {
throw new Error('Missing or invalid accuracy');
}
if (typeof summary.isPassed !== 'boolean') {
throw new Error('Missing or invalid isPassed');
}
// Time statistics
if (!summary.timeStatistics) {
throw new Error('Missing timeStatistics');
}
if (typeof summary.timeStatistics.totalTime !== 'number') {
throw new Error('Missing or invalid timeStatistics.totalTime');
}
return '✓ All required summary fields present';
}
},
{
name: 'Test 11: Questions include all required fields',
run: async () => {
const response = await axios.get(`${API_URL}/quiz/review/${completedSessionId}`, {
headers: { 'Authorization': `Bearer ${userToken}` }
});
const { questions } = response.data.data;
if (questions.length === 0) throw new Error('Should have questions');
const requiredFields = [
'id', 'questionText', 'questionType', 'difficulty', 'points',
'explanation', 'order', 'correctAnswer', 'userAnswer', 'isCorrect',
'resultStatus', 'pointsEarned', 'pointsPossible', 'timeTaken',
'answeredAt', 'showExplanation', 'wasAnswered'
];
questions.forEach((q, idx) => {
requiredFields.forEach(field => {
if (!(field in q)) {
throw new Error(`Question ${idx + 1} missing field: ${field}`);
}
});
});
return '✓ Questions have all required fields';
}
},
{
name: 'Test 12: Result status correctly marked',
run: async () => {
const response = await axios.get(`${API_URL}/quiz/review/${completedSessionId}`, {
headers: { 'Authorization': `Bearer ${userToken}` }
});
const { questions } = response.data.data;
questions.forEach((q, idx) => {
if (q.wasAnswered) {
const expectedStatus = q.isCorrect ? 'correct' : 'incorrect';
if (q.resultStatus !== expectedStatus) {
throw new Error(
`Question ${idx + 1} has wrong resultStatus: expected ${expectedStatus}, got ${q.resultStatus}`
);
}
} else {
if (q.resultStatus !== 'unanswered') {
throw new Error(`Question ${idx + 1} should have resultStatus 'unanswered'`);
}
}
});
return '✓ Result status correctly marked';
}
},
{
name: 'Test 13: Explanations always shown in review',
run: async () => {
const response = await axios.get(`${API_URL}/quiz/review/${completedSessionId}`, {
headers: { 'Authorization': `Bearer ${userToken}` }
});
const { questions } = response.data.data;
questions.forEach((q, idx) => {
if (q.showExplanation !== true) {
throw new Error(`Question ${idx + 1} should have showExplanation=true`);
}
// Explanation field should exist (can be null if not provided)
if (!('explanation' in q)) {
throw new Error(`Question ${idx + 1} missing explanation field`);
}
});
return '✓ Explanations shown for all questions';
}
},
{
name: 'Test 14: Points tracking accurate',
run: async () => {
const response = await axios.get(`${API_URL}/quiz/review/${completedSessionId}`, {
headers: { 'Authorization': `Bearer ${userToken}` }
});
const { summary, questions } = response.data.data;
// Calculate points from questions
let totalPointsPossible = 0;
let totalPointsEarned = 0;
questions.forEach(q => {
totalPointsPossible += q.pointsPossible;
totalPointsEarned += q.pointsEarned;
// Points earned should match: correct answers get full points, incorrect get 0
if (q.wasAnswered) {
const expectedPoints = q.isCorrect ? q.pointsPossible : 0;
if (q.pointsEarned !== expectedPoints) {
throw new Error(
`Question points mismatch: expected ${expectedPoints}, got ${q.pointsEarned}`
);
}
}
});
// Totals should match summary
if (totalPointsEarned !== summary.score.earned) {
throw new Error(
`Score mismatch: calculated ${totalPointsEarned}, summary shows ${summary.score.earned}`
);
}
return '✓ Points tracking accurate';
}
},
{
name: 'Test 15: Time statistics calculated correctly',
run: async () => {
const response = await axios.get(`${API_URL}/quiz/review/${completedSessionId}`, {
headers: { 'Authorization': `Bearer ${userToken}` }
});
const { summary, questions } = response.data.data;
// Calculate total time from questions
let calculatedTotalTime = 0;
let answeredCount = 0;
questions.forEach(q => {
if (q.wasAnswered && q.timeTaken) {
calculatedTotalTime += q.timeTaken;
answeredCount++;
}
});
// Check total time
if (calculatedTotalTime !== summary.timeStatistics.totalTime) {
throw new Error(
`Total time mismatch: calculated ${calculatedTotalTime}, summary shows ${summary.timeStatistics.totalTime}`
);
}
// Check average
const expectedAverage = answeredCount > 0
? Math.round(calculatedTotalTime / answeredCount)
: 0;
if (expectedAverage !== summary.timeStatistics.averageTimePerQuestion) {
throw new Error(
`Average time mismatch: expected ${expectedAverage}, got ${summary.timeStatistics.averageTimePerQuestion}`
);
}
return '✓ Time statistics accurate';
}
},
{
name: 'Test 16: Multiple choice options have feedback',
run: async () => {
const response = await axios.get(`${API_URL}/quiz/review/${completedSessionId}`, {
headers: { 'Authorization': `Bearer ${userToken}` }
});
const { questions } = response.data.data;
const mcQuestions = questions.filter(q => q.questionType === 'multiple');
if (mcQuestions.length === 0) {
console.log(' Note: No multiple choice questions in this quiz');
return '✓ Test skipped (no multiple choice questions)';
}
mcQuestions.forEach((q, idx) => {
if (!Array.isArray(q.options)) {
throw new Error(`MC Question ${idx + 1} should have options array`);
}
q.options.forEach((opt, optIdx) => {
if (!('isCorrect' in opt)) {
throw new Error(`Option ${optIdx + 1} missing isCorrect field`);
}
if (!('isSelected' in opt)) {
throw new Error(`Option ${optIdx + 1} missing isSelected field`);
}
if (!('feedback' in opt)) {
throw new Error(`Option ${optIdx + 1} missing feedback field`);
}
});
});
return '✓ Multiple choice options have feedback';
}
}
];
// Run all tests
async function runTests() {
console.log('='.repeat(60));
console.log('QUIZ REVIEW API TESTS');
console.log('='.repeat(60) + '\n');
await setup();
console.log('Running tests...\n');
let passed = 0;
let failed = 0;
for (const test of tests) {
try {
const result = await test.run();
console.log(`${result}`);
passed++;
await delay(500); // Delay between tests
} catch (error) {
console.log(`${test.name}`);
console.log(` Error: ${error.response?.data?.message || error.message}`);
if (error.response?.data && process.env.VERBOSE) {
console.log(` Response:`, JSON.stringify(error.response.data, null, 2));
}
failed++;
}
}
console.log('\n' + '='.repeat(60));
console.log(`RESULTS: ${passed} passed, ${failed} failed out of ${tests.length} tests`);
console.log('='.repeat(60));
process.exit(failed > 0 ? 1 : 0);
}
runTests();