455 lines
16 KiB
JavaScript
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();
|