Files
Tasks/backend/test-category-details.js
2025-11-11 00:25:50 +02:00

455 lines
16 KiB
JavaScript

const axios = require('axios');
const API_URL = 'http://localhost:3000/api';
// Category UUIDs (from database)
const CATEGORY_IDS = {
JAVASCRIPT: '68b4c87f-db0b-48ea-b8a4-b2f4fce785a2',
ANGULAR: '0033017b-6ecb-4e9d-8f13-06fc222c1dfc',
REACT: 'd27e3412-6f2b-432d-8d63-1e165ea5fffd',
NODEJS: '5e3094ab-ab6d-4f8a-9261-8177b9c979ae',
TYPESCRIPT: '7a71ab02-a7e4-4a76-a364-de9d6e3f3411',
SQL_DATABASES: '24b7b12d-fa23-448f-9f55-b0b9b82a844f',
SYSTEM_DESIGN: '65b3ad28-a19d-413a-9abe-94184f963d77',
};
// Test user credentials (from seeder)
const testUser = {
email: 'admin@quiz.com',
password: 'Admin@123'
};
// ANSI color codes for output
const colors = {
reset: '\x1b[0m',
green: '\x1b[32m',
red: '\x1b[31m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
cyan: '\x1b[36m'
};
let userToken = null;
let guestToken = null;
/**
* Login as registered user
*/
async function loginUser() {
try {
const response = await axios.post(`${API_URL}/auth/login`, testUser);
userToken = response.data.data.token;
console.log(`${colors.cyan}✓ Logged in as user${colors.reset}`);
return userToken;
} catch (error) {
console.error(`${colors.red}✗ Failed to login:${colors.reset}`, error.response?.data || error.message);
throw error;
}
}
/**
* Create guest session
*/
async function createGuestSession() {
try {
const response = await axios.post(`${API_URL}/guest/start-session`, {
deviceId: 'test-device-category-details'
});
guestToken = response.data.sessionToken;
console.log(`${colors.cyan}✓ Created guest session${colors.reset}`);
return guestToken;
} catch (error) {
console.error(`${colors.red}✗ Failed to create guest session:${colors.reset}`, error.response?.data || error.message);
throw error;
}
}
/**
* Test 1: Get guest-accessible category details (JavaScript)
*/
async function testGetGuestCategoryDetails() {
console.log(`\n${colors.blue}Test 1: Get guest-accessible category details (JavaScript)${colors.reset}`);
try {
const response = await axios.get(`${API_URL}/categories/${CATEGORY_IDS.JAVASCRIPT}`, {
headers: {
'X-Guest-Token': guestToken
}
});
const { success, data, message } = response.data;
// Validations
if (!success) throw new Error('success should be true');
if (!data.category) throw new Error('Missing category data');
if (!data.questionPreview) throw new Error('Missing questionPreview');
if (!data.stats) throw new Error('Missing stats');
if (data.category.name !== 'JavaScript') throw new Error('Expected JavaScript category');
if (!data.category.guestAccessible) throw new Error('Should be guest-accessible');
console.log(`${colors.green}✓ Test 1 Passed${colors.reset}`);
console.log(` Category: ${data.category.name}`);
console.log(` Questions Preview: ${data.questionPreview.length}`);
console.log(` Total Questions: ${data.stats.totalQuestions}`);
console.log(` Average Accuracy: ${data.stats.averageAccuracy}%`);
return true;
} catch (error) {
console.error(`${colors.red}✗ Test 1 Failed:${colors.reset}`, error.response?.data || error.message);
return false;
}
}
/**
* Test 2: Guest tries to access auth-only category (Node.js)
*/
async function testGuestAccessAuthCategory() {
console.log(`\n${colors.blue}Test 2: Guest tries to access auth-only category (Node.js)${colors.reset}`);
try {
const response = await axios.get(`${API_URL}/categories/${CATEGORY_IDS.NODEJS}`, {
headers: {
'X-Guest-Token': guestToken
}
});
// Should not reach here
console.error(`${colors.red}✗ Test 2 Failed: Should have been rejected${colors.reset}`);
return false;
} catch (error) {
if (error.response && error.response.status === 403) {
const { success, message, requiresAuth } = error.response.data;
if (success !== false) throw new Error('success should be false');
if (!requiresAuth) throw new Error('requiresAuth should be true');
if (!message.includes('authentication')) throw new Error('Message should mention authentication');
console.log(`${colors.green}✓ Test 2 Passed${colors.reset}`);
console.log(` Status: 403 Forbidden`);
console.log(` Message: ${message}`);
console.log(` Requires Auth: ${requiresAuth}`);
return true;
} else {
console.error(`${colors.red}✗ Test 2 Failed: Wrong error${colors.reset}`, error.response?.data || error.message);
return false;
}
}
}
/**
* Test 3: Authenticated user gets auth-only category details (Node.js)
*/
async function testAuthUserAccessCategory() {
console.log(`\n${colors.blue}Test 3: Authenticated user gets auth-only category details (Node.js)${colors.reset}`);
try {
const response = await axios.get(`${API_URL}/categories/${CATEGORY_IDS.NODEJS}`, {
headers: {
'Authorization': `Bearer ${userToken}`
}
});
const { success, data } = response.data;
if (!success) throw new Error('success should be true');
if (data.category.name !== 'Node.js') throw new Error('Expected Node.js category');
if (data.category.guestAccessible) throw new Error('Should not be guest-accessible');
console.log(`${colors.green}✓ Test 3 Passed${colors.reset}`);
console.log(` Category: ${data.category.name}`);
console.log(` Guest Accessible: ${data.category.guestAccessible}`);
console.log(` Total Questions: ${data.stats.totalQuestions}`);
return true;
} catch (error) {
console.error(`${colors.red}✗ Test 3 Failed:${colors.reset}`, error.response?.data || error.message);
return false;
}
}
/**
* Test 4: Invalid category ID (non-numeric)
*/
async function testInvalidCategoryId() {
console.log(`\n${colors.blue}Test 4: Invalid category ID (non-numeric)${colors.reset}`);
try {
const response = await axios.get(`${API_URL}/categories/invalid`, {
headers: {
'Authorization': `Bearer ${userToken}`
}
});
console.error(`${colors.red}✗ Test 4 Failed: Should have been rejected${colors.reset}`);
return false;
} catch (error) {
if (error.response && error.response.status === 400) {
const { success, message } = error.response.data;
if (success !== false) throw new Error('success should be false');
if (!message.includes('Invalid')) throw new Error('Message should mention invalid ID');
console.log(`${colors.green}✓ Test 4 Passed${colors.reset}`);
console.log(` Status: 400 Bad Request`);
console.log(` Message: ${message}`);
return true;
} else {
console.error(`${colors.red}✗ Test 4 Failed: Wrong error${colors.reset}`, error.response?.data || error.message);
return false;
}
}
}
/**
* Test 5: Non-existent category ID
*/
async function testNonExistentCategory() {
console.log(`\n${colors.blue}Test 5: Non-existent category ID (999)${colors.reset}`);
try {
const response = await axios.get(`${API_URL}/categories/999`, {
headers: {
'Authorization': `Bearer ${userToken}`
}
});
console.error(`${colors.red}✗ Test 5 Failed: Should have returned 404${colors.reset}`);
return false;
} catch (error) {
if (error.response && error.response.status === 404) {
const { success, message } = error.response.data;
if (success !== false) throw new Error('success should be false');
if (!message.includes('not found')) throw new Error('Message should mention not found');
console.log(`${colors.green}✓ Test 5 Passed${colors.reset}`);
console.log(` Status: 404 Not Found`);
console.log(` Message: ${message}`);
return true;
} else {
console.error(`${colors.red}✗ Test 5 Failed: Wrong error${colors.reset}`, error.response?.data || error.message);
return false;
}
}
}
/**
* Test 6: Verify response structure
*/
async function testResponseStructure() {
console.log(`\n${colors.blue}Test 6: Verify response structure${colors.reset}`);
try {
const response = await axios.get(`${API_URL}/categories/${CATEGORY_IDS.JAVASCRIPT}`, {
headers: {
'Authorization': `Bearer ${userToken}`
}
});
const { success, data, message } = response.data;
const { category, questionPreview, stats } = data;
// Check category fields
const requiredCategoryFields = ['id', 'name', 'slug', 'description', 'icon', 'color', 'questionCount', 'displayOrder', 'guestAccessible'];
for (const field of requiredCategoryFields) {
if (!(field in category)) throw new Error(`Missing category field: ${field}`);
}
// Check question preview structure
if (!Array.isArray(questionPreview)) throw new Error('questionPreview should be an array');
if (questionPreview.length > 5) throw new Error('questionPreview should have max 5 questions');
if (questionPreview.length > 0) {
const question = questionPreview[0];
const requiredQuestionFields = ['id', 'questionText', 'questionType', 'difficulty', 'points', 'accuracy'];
for (const field of requiredQuestionFields) {
if (!(field in question)) throw new Error(`Missing question field: ${field}`);
}
}
// Check stats structure
const requiredStatsFields = ['totalQuestions', 'questionsByDifficulty', 'totalAttempts', 'totalCorrect', 'averageAccuracy'];
for (const field of requiredStatsFields) {
if (!(field in stats)) throw new Error(`Missing stats field: ${field}`);
}
// Check difficulty breakdown
const { questionsByDifficulty } = stats;
if (!('easy' in questionsByDifficulty)) throw new Error('Missing easy difficulty count');
if (!('medium' in questionsByDifficulty)) throw new Error('Missing medium difficulty count');
if (!('hard' in questionsByDifficulty)) throw new Error('Missing hard difficulty count');
console.log(`${colors.green}✓ Test 6 Passed${colors.reset}`);
console.log(` All required fields present`);
console.log(` Question preview length: ${questionPreview.length}`);
console.log(` Difficulty breakdown: Easy=${questionsByDifficulty.easy}, Medium=${questionsByDifficulty.medium}, Hard=${questionsByDifficulty.hard}`);
return true;
} catch (error) {
console.error(`${colors.red}✗ Test 6 Failed:${colors.reset}`, error.response?.data || error.message);
return false;
}
}
/**
* Test 7: No authentication (public access to guest category)
*/
async function testPublicAccessGuestCategory() {
console.log(`\n${colors.blue}Test 7: Public access to guest-accessible category (no auth)${colors.reset}`);
try {
const response = await axios.get(`${API_URL}/categories/${CATEGORY_IDS.JAVASCRIPT}`);
const { success, data } = response.data;
if (!success) throw new Error('success should be true');
if (data.category.name !== 'JavaScript') throw new Error('Expected JavaScript category');
if (!data.category.guestAccessible) throw new Error('Should be guest-accessible');
console.log(`${colors.green}✓ Test 7 Passed${colors.reset}`);
console.log(` Public access allowed for guest-accessible categories`);
console.log(` Category: ${data.category.name}`);
return true;
} catch (error) {
console.error(`${colors.red}✗ Test 7 Failed:${colors.reset}`, error.response?.data || error.message);
return false;
}
}
/**
* Test 8: No authentication (public tries auth-only category)
*/
async function testPublicAccessAuthCategory() {
console.log(`\n${colors.blue}Test 8: Public access to auth-only category (no auth)${colors.reset}`);
try {
const response = await axios.get(`${API_URL}/categories/${CATEGORY_IDS.NODEJS}`);
console.error(`${colors.red}✗ Test 8 Failed: Should have been rejected${colors.reset}`);
return false;
} catch (error) {
if (error.response && error.response.status === 403) {
const { success, requiresAuth } = error.response.data;
if (success !== false) throw new Error('success should be false');
if (!requiresAuth) throw new Error('requiresAuth should be true');
console.log(`${colors.green}✓ Test 8 Passed${colors.reset}`);
console.log(` Public access blocked for auth-only categories`);
console.log(` Status: 403 Forbidden`);
return true;
} else {
console.error(`${colors.red}✗ Test 8 Failed: Wrong error${colors.reset}`, error.response?.data || error.message);
return false;
}
}
}
/**
* Test 9: Verify stats calculations
*/
async function testStatsCalculations() {
console.log(`\n${colors.blue}Test 9: Verify stats calculations${colors.reset}`);
try {
const response = await axios.get(`${API_URL}/categories/${CATEGORY_IDS.JAVASCRIPT}`, {
headers: {
'Authorization': `Bearer ${userToken}`
}
});
const { data } = response.data;
const { stats } = data;
// Verify difficulty sum equals total
const difficultySum = stats.questionsByDifficulty.easy +
stats.questionsByDifficulty.medium +
stats.questionsByDifficulty.hard;
if (difficultySum !== stats.totalQuestions) {
throw new Error(`Difficulty sum (${difficultySum}) doesn't match total questions (${stats.totalQuestions})`);
}
// Verify accuracy is within valid range
if (stats.averageAccuracy < 0 || stats.averageAccuracy > 100) {
throw new Error(`Invalid accuracy: ${stats.averageAccuracy}%`);
}
// If there are attempts, verify accuracy calculation
if (stats.totalAttempts > 0) {
const expectedAccuracy = Math.round((stats.totalCorrect / stats.totalAttempts) * 100);
if (stats.averageAccuracy !== expectedAccuracy) {
throw new Error(`Accuracy mismatch: expected ${expectedAccuracy}%, got ${stats.averageAccuracy}%`);
}
}
console.log(`${colors.green}✓ Test 9 Passed${colors.reset}`);
console.log(` Total Questions: ${stats.totalQuestions}`);
console.log(` Difficulty Sum: ${difficultySum}`);
console.log(` Total Attempts: ${stats.totalAttempts}`);
console.log(` Total Correct: ${stats.totalCorrect}`);
console.log(` Average Accuracy: ${stats.averageAccuracy}%`);
return true;
} catch (error) {
console.error(`${colors.red}✗ Test 9 Failed:${colors.reset}`, error.response?.data || error.message);
return false;
}
}
/**
* Run all tests
*/
async function runAllTests() {
console.log(`${colors.cyan}========================================${colors.reset}`);
console.log(`${colors.cyan}Testing Category Details API${colors.reset}`);
console.log(`${colors.cyan}========================================${colors.reset}`);
const results = [];
try {
// Setup
await loginUser();
await createGuestSession();
// Run tests
results.push(await testGetGuestCategoryDetails());
results.push(await testGuestAccessAuthCategory());
results.push(await testAuthUserAccessCategory());
results.push(await testInvalidCategoryId());
results.push(await testNonExistentCategory());
results.push(await testResponseStructure());
results.push(await testPublicAccessGuestCategory());
results.push(await testPublicAccessAuthCategory());
results.push(await testStatsCalculations());
// Summary
console.log(`\n${colors.cyan}========================================${colors.reset}`);
console.log(`${colors.cyan}Test Summary${colors.reset}`);
console.log(`${colors.cyan}========================================${colors.reset}`);
const passed = results.filter(r => r === true).length;
const failed = results.filter(r => r === false).length;
console.log(`${colors.green}Passed: ${passed}${colors.reset}`);
console.log(`${colors.red}Failed: ${failed}${colors.reset}`);
console.log(`Total: ${results.length}`);
if (failed === 0) {
console.log(`\n${colors.green}✓ All tests passed!${colors.reset}`);
} else {
console.log(`\n${colors.red}✗ Some tests failed${colors.reset}`);
process.exit(1);
}
} catch (error) {
console.error(`${colors.red}Test execution error:${colors.reset}`, error);
process.exit(1);
}
}
// Run tests
runAllTests();