add changes
This commit is contained in:
551
tests/test-quiz-history.js
Normal file
551
tests/test-quiz-history.js
Normal file
@@ -0,0 +1,551 @@
|
||||
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();
|
||||
Reference in New Issue
Block a user