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();