527 lines
16 KiB
JavaScript
527 lines
16 KiB
JavaScript
const axios = require('axios');
|
|
|
|
const API_URL = 'http://localhost:3000/api';
|
|
|
|
// Test data
|
|
let testUser = {
|
|
email: 'dashboarduser@example.com',
|
|
password: 'Test@123',
|
|
username: 'dashboarduser'
|
|
};
|
|
|
|
let secondUser = {
|
|
email: 'otheruser2@example.com',
|
|
password: 'Test@123',
|
|
username: 'otheruser2'
|
|
};
|
|
|
|
let userToken = null;
|
|
let userId = null;
|
|
let secondUserToken = null;
|
|
let secondUserId = null;
|
|
let testCategory = 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, categoryId, questionCount = 3) {
|
|
const headers = { 'Authorization': `Bearer ${token}` };
|
|
|
|
// Start quiz
|
|
const startRes = await axios.post(`${API_URL}/quiz/start`, {
|
|
categoryId,
|
|
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() * 15) + 5
|
|
}, { 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;
|
|
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');
|
|
}
|
|
console.log(`✓ Test category selected: ${testCategory.name}`);
|
|
|
|
await delay(500);
|
|
|
|
// Create some quizzes for the first user to populate dashboard
|
|
console.log('Creating quiz sessions for dashboard data...');
|
|
|
|
for (let i = 0; i < 3; i++) {
|
|
await createAndCompleteQuiz(userToken, testCategory.id, 3);
|
|
await delay(500);
|
|
}
|
|
|
|
console.log('✓ Quiz sessions created\n');
|
|
|
|
} catch (error) {
|
|
console.error('Setup failed:', error.response?.data || error.message);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Test cases
|
|
const tests = [
|
|
{
|
|
name: 'Test 1: Get user dashboard successfully',
|
|
run: async () => {
|
|
const response = await axios.get(`${API_URL}/users/${userId}/dashboard`, {
|
|
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 { user, stats, recentSessions, categoryPerformance, recentActivity } = response.data.data;
|
|
|
|
if (!user || !stats || !recentSessions || !categoryPerformance || !recentActivity) {
|
|
throw new Error('Missing required dashboard sections');
|
|
}
|
|
|
|
return '✓ Dashboard retrieved successfully';
|
|
}
|
|
},
|
|
{
|
|
name: 'Test 2: User info includes required fields',
|
|
run: async () => {
|
|
const response = await axios.get(`${API_URL}/users/${userId}/dashboard`, {
|
|
headers: { 'Authorization': `Bearer ${userToken}` }
|
|
});
|
|
|
|
const { user } = response.data.data;
|
|
|
|
const requiredFields = ['id', 'username', 'email', 'role', 'memberSince'];
|
|
requiredFields.forEach(field => {
|
|
if (!(field in user)) {
|
|
throw new Error(`Missing user field: ${field}`);
|
|
}
|
|
});
|
|
|
|
if (user.id !== userId) throw new Error('User ID mismatch');
|
|
if (user.email !== testUser.email) throw new Error('Email mismatch');
|
|
|
|
return '✓ User info correct';
|
|
}
|
|
},
|
|
{
|
|
name: 'Test 3: Stats include all required fields',
|
|
run: async () => {
|
|
const response = await axios.get(`${API_URL}/users/${userId}/dashboard`, {
|
|
headers: { 'Authorization': `Bearer ${userToken}` }
|
|
});
|
|
|
|
const { stats } = response.data.data;
|
|
|
|
const requiredFields = [
|
|
'totalQuizzes', 'quizzesPassed', 'passRate',
|
|
'totalQuestionsAnswered', 'correctAnswers', 'overallAccuracy',
|
|
'currentStreak', 'longestStreak', 'streakStatus', 'lastActiveDate'
|
|
];
|
|
|
|
requiredFields.forEach(field => {
|
|
if (!(field in stats)) {
|
|
throw new Error(`Missing stats field: ${field}`);
|
|
}
|
|
});
|
|
|
|
// Validate data types
|
|
if (typeof stats.totalQuizzes !== 'number') throw new Error('totalQuizzes should be number');
|
|
if (typeof stats.overallAccuracy !== 'number') throw new Error('overallAccuracy should be number');
|
|
if (typeof stats.passRate !== 'number') throw new Error('passRate should be number');
|
|
|
|
return '✓ Stats fields correct';
|
|
}
|
|
},
|
|
{
|
|
name: 'Test 4: Stats calculations are accurate',
|
|
run: async () => {
|
|
const response = await axios.get(`${API_URL}/users/${userId}/dashboard`, {
|
|
headers: { 'Authorization': `Bearer ${userToken}` }
|
|
});
|
|
|
|
const { stats } = response.data.data;
|
|
|
|
// Pass rate calculation
|
|
if (stats.totalQuizzes > 0) {
|
|
const expectedPassRate = Math.round((stats.quizzesPassed / stats.totalQuizzes) * 100);
|
|
if (stats.passRate !== expectedPassRate) {
|
|
throw new Error(`Pass rate mismatch: expected ${expectedPassRate}, got ${stats.passRate}`);
|
|
}
|
|
}
|
|
|
|
// Accuracy calculation
|
|
if (stats.totalQuestionsAnswered > 0) {
|
|
const expectedAccuracy = Math.round((stats.correctAnswers / stats.totalQuestionsAnswered) * 100);
|
|
if (stats.overallAccuracy !== expectedAccuracy) {
|
|
throw new Error(`Accuracy mismatch: expected ${expectedAccuracy}, got ${stats.overallAccuracy}`);
|
|
}
|
|
}
|
|
|
|
// Streak validation
|
|
if (stats.currentStreak < 0) throw new Error('Current streak cannot be negative');
|
|
if (stats.longestStreak < stats.currentStreak) {
|
|
throw new Error('Longest streak should be >= current streak');
|
|
}
|
|
|
|
return '✓ Stats calculations accurate';
|
|
}
|
|
},
|
|
{
|
|
name: 'Test 5: Recent sessions returned correctly',
|
|
run: async () => {
|
|
const response = await axios.get(`${API_URL}/users/${userId}/dashboard`, {
|
|
headers: { 'Authorization': `Bearer ${userToken}` }
|
|
});
|
|
|
|
const { recentSessions } = response.data.data;
|
|
|
|
if (!Array.isArray(recentSessions)) throw new Error('recentSessions should be array');
|
|
if (recentSessions.length === 0) throw new Error('Should have recent sessions');
|
|
if (recentSessions.length > 10) throw new Error('Should have max 10 recent sessions');
|
|
|
|
// Validate session structure
|
|
const session = recentSessions[0];
|
|
const requiredFields = [
|
|
'id', 'category', 'quizType', 'difficulty', 'status',
|
|
'score', 'isPassed', 'questionsAnswered', 'correctAnswers',
|
|
'accuracy', 'timeSpent', 'completedAt'
|
|
];
|
|
|
|
requiredFields.forEach(field => {
|
|
if (!(field in session)) {
|
|
throw new Error(`Session missing field: ${field}`);
|
|
}
|
|
});
|
|
|
|
// Validate category structure
|
|
if (!session.category || !session.category.name) {
|
|
throw new Error('Session should have category info');
|
|
}
|
|
|
|
// Validate score structure
|
|
if (!session.score || typeof session.score.earned !== 'number') {
|
|
throw new Error('Session should have score object with earned field');
|
|
}
|
|
|
|
return '✓ Recent sessions correct';
|
|
}
|
|
},
|
|
{
|
|
name: 'Test 6: Recent sessions ordered by completion date',
|
|
run: async () => {
|
|
const response = await axios.get(`${API_URL}/users/${userId}/dashboard`, {
|
|
headers: { 'Authorization': `Bearer ${userToken}` }
|
|
});
|
|
|
|
const { recentSessions } = response.data.data;
|
|
|
|
if (recentSessions.length > 1) {
|
|
for (let i = 1; i < recentSessions.length; i++) {
|
|
const prev = new Date(recentSessions[i - 1].completedAt);
|
|
const curr = new Date(recentSessions[i].completedAt);
|
|
|
|
if (curr > prev) {
|
|
throw new Error('Sessions not ordered by completion date (DESC)');
|
|
}
|
|
}
|
|
}
|
|
|
|
return '✓ Sessions ordered correctly';
|
|
}
|
|
},
|
|
{
|
|
name: 'Test 7: Category performance includes all categories',
|
|
run: async () => {
|
|
const response = await axios.get(`${API_URL}/users/${userId}/dashboard`, {
|
|
headers: { 'Authorization': `Bearer ${userToken}` }
|
|
});
|
|
|
|
const { categoryPerformance } = response.data.data;
|
|
|
|
if (!Array.isArray(categoryPerformance)) {
|
|
throw new Error('categoryPerformance should be array');
|
|
}
|
|
|
|
if (categoryPerformance.length === 0) {
|
|
throw new Error('Should have category performance data');
|
|
}
|
|
|
|
// Validate structure
|
|
const catPerf = categoryPerformance[0];
|
|
if (!catPerf.category || !catPerf.stats || !catPerf.lastAttempt) {
|
|
throw new Error('Category performance missing required fields');
|
|
}
|
|
|
|
const requiredStatsFields = [
|
|
'quizzesTaken', 'quizzesPassed', 'passRate',
|
|
'averageScore', 'totalQuestions', 'correctAnswers', 'accuracy'
|
|
];
|
|
|
|
requiredStatsFields.forEach(field => {
|
|
if (!(field in catPerf.stats)) {
|
|
throw new Error(`Category stats missing field: ${field}`);
|
|
}
|
|
});
|
|
|
|
return '✓ Category performance correct';
|
|
}
|
|
},
|
|
{
|
|
name: 'Test 8: Category performance calculations accurate',
|
|
run: async () => {
|
|
const response = await axios.get(`${API_URL}/users/${userId}/dashboard`, {
|
|
headers: { 'Authorization': `Bearer ${userToken}` }
|
|
});
|
|
|
|
const { categoryPerformance } = response.data.data;
|
|
|
|
categoryPerformance.forEach((catPerf, idx) => {
|
|
const stats = catPerf.stats;
|
|
|
|
// Pass rate
|
|
if (stats.quizzesTaken > 0) {
|
|
const expectedPassRate = Math.round((stats.quizzesPassed / stats.quizzesTaken) * 100);
|
|
if (stats.passRate !== expectedPassRate) {
|
|
throw new Error(`Category ${idx + 1} pass rate mismatch`);
|
|
}
|
|
}
|
|
|
|
// Accuracy
|
|
if (stats.totalQuestions > 0) {
|
|
const expectedAccuracy = Math.round((stats.correctAnswers / stats.totalQuestions) * 100);
|
|
if (stats.accuracy !== expectedAccuracy) {
|
|
throw new Error(`Category ${idx + 1} accuracy mismatch`);
|
|
}
|
|
}
|
|
|
|
// All values should be non-negative
|
|
Object.values(stats).forEach(val => {
|
|
if (typeof val === 'number' && val < 0) {
|
|
throw new Error('Stats values should be non-negative');
|
|
}
|
|
});
|
|
});
|
|
|
|
return '✓ Category performance calculations accurate';
|
|
}
|
|
},
|
|
{
|
|
name: 'Test 9: Recent activity includes date and count',
|
|
run: async () => {
|
|
const response = await axios.get(`${API_URL}/users/${userId}/dashboard`, {
|
|
headers: { 'Authorization': `Bearer ${userToken}` }
|
|
});
|
|
|
|
const { recentActivity } = response.data.data;
|
|
|
|
if (!Array.isArray(recentActivity)) {
|
|
throw new Error('recentActivity should be array');
|
|
}
|
|
|
|
if (recentActivity.length > 0) {
|
|
const activity = recentActivity[0];
|
|
if (!activity.date) throw new Error('Activity missing date');
|
|
if (typeof activity.quizzesCompleted !== 'number') {
|
|
throw new Error('Activity quizzesCompleted should be number');
|
|
}
|
|
}
|
|
|
|
return '✓ Recent activity correct';
|
|
}
|
|
},
|
|
{
|
|
name: 'Test 10: Cannot access other user\'s dashboard (403)',
|
|
run: async () => {
|
|
try {
|
|
await axios.get(`${API_URL}/users/${secondUserId}/dashboard`, {
|
|
headers: { 'Authorization': `Bearer ${userToken}` }
|
|
});
|
|
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 11: Unauthenticated request returns 401',
|
|
run: async () => {
|
|
try {
|
|
await axios.get(`${API_URL}/users/${userId}/dashboard`);
|
|
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 12: Invalid UUID format returns 400',
|
|
run: async () => {
|
|
try {
|
|
await axios.get(`${API_URL}/users/invalid-uuid/dashboard`, {
|
|
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 13: Non-existent user returns 404',
|
|
run: async () => {
|
|
try {
|
|
const fakeUuid = '00000000-0000-0000-0000-000000000000';
|
|
await axios.get(`${API_URL}/users/${fakeUuid}/dashboard`, {
|
|
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 14: Streak status is valid',
|
|
run: async () => {
|
|
const response = await axios.get(`${API_URL}/users/${userId}/dashboard`, {
|
|
headers: { 'Authorization': `Bearer ${userToken}` }
|
|
});
|
|
|
|
const { stats } = response.data.data;
|
|
|
|
const validStatuses = ['active', 'at-risk', 'inactive'];
|
|
if (!validStatuses.includes(stats.streakStatus)) {
|
|
throw new Error(`Invalid streak status: ${stats.streakStatus}`);
|
|
}
|
|
|
|
return '✓ Streak status valid';
|
|
}
|
|
}
|
|
];
|
|
|
|
// Run all tests
|
|
async function runTests() {
|
|
console.log('='.repeat(60));
|
|
console.log('USER DASHBOARD 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();
|