651 lines
21 KiB
JavaScript
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();
|