552 lines
18 KiB
JavaScript
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();
|