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

552 lines
18 KiB
JavaScript

const axios = require('axios');
const API_URL = 'http://localhost:3000/api';
// Test data
const testUser = {
username: 'historytest',
email: 'historytest@example.com',
password: 'Test123!@#'
};
const secondUser = {
username: 'historytest2',
email: 'historytest2@example.com',
password: 'Test123!@#'
};
let userToken;
let userId;
let secondUserToken;
let secondUserId;
let testCategory;
let testSessions = [];
// Helper function to add delay
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// Helper function to create and complete a quiz
async function createAndCompleteQuiz(token, categoryId, numQuestions) {
const headers = { 'Authorization': `Bearer ${token}` };
// Start quiz
const startRes = await axios.post(`${API_URL}/quiz/start`, {
categoryId,
quizType: 'practice',
difficulty: 'medium',
numberOfQuestions: numQuestions
}, { headers });
const sessionId = startRes.data.data.sessionId;
const questions = startRes.data.data.questions;
if (!sessionId) {
throw new Error('No sessionId returned from start quiz');
}
// Submit answers
for (let i = 0; i < questions.length; i++) {
const question = questions[i];
// Just pick a random option ID since we don't know the correct answer
const randomOption = question.options[Math.floor(Math.random() * question.options.length)];
try {
await axios.post(`${API_URL}/quiz/submit`, {
quizSessionId: sessionId, // Fixed: use quizSessionId
questionId: question.id,
userAnswer: randomOption.id, // Fixed: use userAnswer
timeSpent: Math.floor(Math.random() * 30) + 5 // Fixed: use timeSpent
}, { headers });
} catch (error) {
console.error(`Submit error for question ${i + 1}:`, {
sessionId,
questionId: question.id,
userAnswer: randomOption.id,
error: error.response?.data
});
throw error;
}
await delay(100);
}
// Complete quiz
await axios.post(`${API_URL}/quiz/complete`, {
sessionId: sessionId // Field name is sessionId for complete endpoint
}, { 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;
userId = registerRes.data.data.user.id;
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;
userId = loginRes.data.data.user.id;
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;
secondUserId = registerRes.data.data.user.id;
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;
secondUserId = loginRes.data.data.user.id;
console.log('✓ Second user logged in');
}
// Get categories
const categoriesRes = await axios.get(`${API_URL}/categories`, {
headers: { 'Authorization': `Bearer ${userToken}` }
});
const categories = categoriesRes.data.data;
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 (need at least 3 questions)');
}
console.log(`✓ Test category selected: ${testCategory.name} (${testCategory.questionCount} questions)`);
await delay(500);
// Create multiple quizzes for testing pagination and filtering
console.log('Creating quiz sessions for history testing...');
for (let i = 0; i < 8; i++) {
try {
const sessionId = await createAndCompleteQuiz(userToken, testCategory.id, 3);
testSessions.push(sessionId);
console.log(` Created session ${i + 1}/8`);
await delay(500);
} catch (error) {
console.error(` Failed to create session ${i + 1}:`, error.response?.data || error.message);
throw error;
}
}
console.log('✓ Quiz sessions created\n');
} catch (error) {
console.error('Setup failed:', error.response?.data || error.message);
throw error;
}
}
// Tests
const tests = [
{
name: 'Test 1: Get quiz history with default pagination',
run: async () => {
const response = await axios.get(`${API_URL}/users/${userId}/history`, {
headers: { 'Authorization': `Bearer ${userToken}` }
});
if (!response.data.success) throw new Error('Request failed');
if (!response.data.data.sessions) throw new Error('No sessions in response');
if (!response.data.data.pagination) throw new Error('No pagination data');
const { pagination, sessions } = response.data.data;
if (pagination.itemsPerPage !== 10) throw new Error('Default limit should be 10');
if (pagination.currentPage !== 1) throw new Error('Default page should be 1');
if (sessions.length > 10) throw new Error('Should not exceed limit');
return '✓ Default pagination works';
}
},
{
name: 'Test 2: Pagination structure is correct',
run: async () => {
const response = await axios.get(`${API_URL}/users/${userId}/history?page=1&limit=5`, {
headers: { 'Authorization': `Bearer ${userToken}` }
});
const { pagination } = response.data.data;
const requiredFields = ['currentPage', 'totalPages', 'totalItems', 'itemsPerPage', 'hasNextPage', 'hasPreviousPage'];
for (const field of requiredFields) {
if (!(field in pagination)) throw new Error(`Missing pagination field: ${field}`);
}
if (pagination.currentPage !== 1) throw new Error('Current page mismatch');
if (pagination.itemsPerPage !== 5) throw new Error('Items per page mismatch');
if (pagination.hasPreviousPage !== false) throw new Error('First page should not have previous');
return '✓ Pagination structure correct';
}
},
{
name: 'Test 3: Sessions have all required fields',
run: async () => {
const response = await axios.get(`${API_URL}/users/${userId}/history?limit=1`, {
headers: { 'Authorization': `Bearer ${userToken}` }
});
const session = response.data.data.sessions[0];
if (!session) throw new Error('No session in response');
const requiredFields = [
'id', 'category', 'quizType', 'difficulty', 'status',
'score', 'isPassed', 'questions', 'time',
'startedAt', 'completedAt'
];
for (const field of requiredFields) {
if (!(field in session)) throw new Error(`Missing field: ${field}`);
}
// Check nested objects
if (!session.score.earned && session.score.earned !== 0) throw new Error('Missing score.earned');
if (!session.score.total) throw new Error('Missing score.total');
if (!session.score.percentage && session.score.percentage !== 0) throw new Error('Missing score.percentage');
if (!session.questions.answered && session.questions.answered !== 0) throw new Error('Missing questions.answered');
if (!session.questions.total) throw new Error('Missing questions.total');
if (!session.questions.correct && session.questions.correct !== 0) throw new Error('Missing questions.correct');
if (!session.questions.accuracy && session.questions.accuracy !== 0) throw new Error('Missing questions.accuracy');
return '✓ Session fields correct';
}
},
{
name: 'Test 4: Pagination with custom limit',
run: async () => {
const response = await axios.get(`${API_URL}/users/${userId}/history?limit=3`, {
headers: { 'Authorization': `Bearer ${userToken}` }
});
const { sessions, pagination } = response.data.data;
if (sessions.length > 3) throw new Error('Exceeded custom limit');
if (pagination.itemsPerPage !== 3) throw new Error('Limit not applied');
return '✓ Custom limit works';
}
},
{
name: 'Test 5: Navigate to second page',
run: async () => {
const response = await axios.get(`${API_URL}/users/${userId}/history?page=2&limit=5`, {
headers: { 'Authorization': `Bearer ${userToken}` }
});
const { pagination } = response.data.data;
if (pagination.currentPage !== 2) throw new Error('Not on page 2');
if (pagination.hasPreviousPage !== true) throw new Error('Should have previous page');
return '✓ Page navigation works';
}
},
{
name: 'Test 6: Filter by category',
run: async () => {
const response = await axios.get(`${API_URL}/users/${userId}/history?category=${testCategory.id}`, {
headers: { 'Authorization': `Bearer ${userToken}` }
});
const { sessions, filters } = response.data.data;
if (filters.category !== testCategory.id) throw new Error('Category filter not applied');
for (const session of sessions) {
if (session.category.id !== testCategory.id) {
throw new Error('Session from wrong category returned');
}
}
return '✓ Category filter works';
}
},
{
name: 'Test 7: Filter by status',
run: async () => {
const response = await axios.get(`${API_URL}/users/${userId}/history?status=completed`, {
headers: { 'Authorization': `Bearer ${userToken}` }
});
const { sessions, filters } = response.data.data;
if (filters.status !== 'completed') throw new Error('Status filter not applied');
for (const session of sessions) {
if (session.status !== 'completed' && session.status !== 'timeout') {
throw new Error(`Unexpected status: ${session.status}`);
}
}
return '✓ Status filter works';
}
},
{
name: 'Test 8: Sort by score descending',
run: async () => {
const response = await axios.get(`${API_URL}/users/${userId}/history?sortBy=score&sortOrder=desc&limit=5`, {
headers: { 'Authorization': `Bearer ${userToken}` }
});
const { sessions, sorting } = response.data.data;
if (sorting.sortBy !== 'score') throw new Error('Sort by not applied');
if (sorting.sortOrder !== 'desc') throw new Error('Sort order not applied');
// Check if sorted in descending order
for (let i = 0; i < sessions.length - 1; i++) {
if (sessions[i].score.earned < sessions[i + 1].score.earned) {
throw new Error('Not sorted by score descending');
}
}
return '✓ Sort by score works';
}
},
{
name: 'Test 9: Sort by date ascending',
run: async () => {
const response = await axios.get(`${API_URL}/users/${userId}/history?sortBy=date&sortOrder=asc&limit=5`, {
headers: { 'Authorization': `Bearer ${userToken}` }
});
const { sessions } = response.data.data;
// Check if sorted in ascending order by date
for (let i = 0; i < sessions.length - 1; i++) {
const date1 = new Date(sessions[i].completedAt);
const date2 = new Date(sessions[i + 1].completedAt);
if (date1 > date2) {
throw new Error('Not sorted by date ascending');
}
}
return '✓ Sort by date ascending works';
}
},
{
name: 'Test 10: Default sort is by date descending',
run: async () => {
const response = await axios.get(`${API_URL}/users/${userId}/history?limit=5`, {
headers: { 'Authorization': `Bearer ${userToken}` }
});
const { sessions, sorting } = response.data.data;
if (sorting.sortBy !== 'date') throw new Error('Default sort should be date');
if (sorting.sortOrder !== 'desc') throw new Error('Default order should be desc');
// Check if sorted in descending order by date (most recent first)
for (let i = 0; i < sessions.length - 1; i++) {
const date1 = new Date(sessions[i].completedAt);
const date2 = new Date(sessions[i + 1].completedAt);
if (date1 < date2) {
throw new Error('Not sorted by date descending');
}
}
return '✓ Default sort correct';
}
},
{
name: 'Test 11: Limit maximum items per page',
run: async () => {
const response = await axios.get(`${API_URL}/users/${userId}/history?limit=100`, {
headers: { 'Authorization': `Bearer ${userToken}` }
});
const { pagination } = response.data.data;
if (pagination.itemsPerPage > 50) {
throw new Error('Should limit to max 50 items per page');
}
return '✓ Max limit enforced';
}
},
{
name: 'Test 12: Cross-user access blocked',
run: async () => {
try {
await axios.get(`${API_URL}/users/${secondUserId}/history`, {
headers: { 'Authorization': `Bearer ${userToken}` }
});
throw new Error('Should have been blocked');
} catch (error) {
if (error.response?.status !== 403) {
throw new Error(`Expected 403, got ${error.response?.status}`);
}
return '✓ Cross-user access blocked';
}
}
},
{
name: 'Test 13: Unauthenticated request blocked',
run: async () => {
try {
await axios.get(`${API_URL}/users/${userId}/history`);
throw new Error('Should have been blocked');
} catch (error) {
if (error.response?.status !== 401) {
throw new Error(`Expected 401, got ${error.response?.status}`);
}
return '✓ Unauthenticated blocked';
}
}
},
{
name: 'Test 14: Invalid UUID returns 400',
run: async () => {
try {
await axios.get(`${API_URL}/users/invalid-uuid/history`, {
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 15: Non-existent user returns 404',
run: async () => {
try {
const fakeUuid = '00000000-0000-0000-0000-000000000000';
await axios.get(`${API_URL}/users/${fakeUuid}/history`, {
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 user returns 404';
}
}
},
{
name: 'Test 16: Invalid category ID returns 400',
run: async () => {
try {
await axios.get(`${API_URL}/users/${userId}/history?category=invalid-id`, {
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 category ID returns 400';
}
}
},
{
name: 'Test 17: Invalid date format returns 400',
run: async () => {
try {
await axios.get(`${API_URL}/users/${userId}/history?startDate=invalid-date`, {
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 date returns 400';
}
}
},
{
name: 'Test 18: Combine filters and sorting',
run: async () => {
const response = await axios.get(
`${API_URL}/users/${userId}/history?category=${testCategory.id}&sortBy=score&sortOrder=desc&limit=3`,
{ headers: { 'Authorization': `Bearer ${userToken}` } }
);
const { sessions, filters, sorting } = response.data.data;
if (filters.category !== testCategory.id) throw new Error('Category filter not applied');
if (sorting.sortBy !== 'score') throw new Error('Sort not applied');
if (sessions.length > 3) throw new Error('Limit not applied');
// Check category filter
for (const session of sessions) {
if (session.category.id !== testCategory.id) {
throw new Error('Wrong category in results');
}
}
// Check sorting
for (let i = 0; i < sessions.length - 1; i++) {
if (sessions[i].score.earned < sessions[i + 1].score.earned) {
throw new Error('Not sorted correctly');
}
}
return '✓ Combined filters work';
}
}
];
// Run tests
async function runTests() {
console.log('============================================================');
console.log('QUIZ HISTORY API TESTS');
console.log('============================================================\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++;
} catch (error) {
console.log(`${test.name}`);
console.log(` Error: ${error.message}`);
if (error.response?.data) {
console.log(` Response:`, JSON.stringify(error.response.data, null, 2));
}
failed++;
}
}
console.log('\n============================================================');
console.log(`RESULTS: ${passed} passed, ${failed} failed out of ${tests.length} tests`);
console.log('============================================================');
process.exit(failed > 0 ? 1 : 0);
}
runTests();