333 lines
13 KiB
JavaScript
333 lines
13 KiB
JavaScript
const axios = require('axios');
|
|
|
|
const BASE_URL = 'http://localhost:3000/api';
|
|
|
|
// Question UUIDs from database
|
|
const QUESTION_IDS = {
|
|
GUEST_REACT_EASY: '0891122f-cf0f-4fdf-afd8-5bf0889851f7', // React - easy [GUEST]
|
|
AUTH_TYPESCRIPT_HARD: '08aa3a33-46fa-4deb-994e-8a2799abcf9f', // TypeScript - hard [AUTH]
|
|
GUEST_JS_EASY: '0c414118-fa32-407a-a9d9-4b9f85955e12', // JavaScript - easy [GUEST]
|
|
AUTH_SYSTEM_DESIGN: '14ee37fe-061d-4677-b2a5-b092c711539f', // System Design - medium [AUTH]
|
|
AUTH_NODEJS_HARD: '22df0824-43bd-48b3-9e1b-c8072ce5e5d5', // Node.js - hard [AUTH]
|
|
GUEST_ANGULAR_EASY: '20d1f27b-5ab8-4027-9548-48def7dd9c3a', // Angular - easy [GUEST]
|
|
};
|
|
|
|
let adminToken = '';
|
|
let regularUserToken = '';
|
|
let testResults = {
|
|
passed: 0,
|
|
failed: 0,
|
|
total: 0
|
|
};
|
|
|
|
// Test helper
|
|
async function runTest(testName, testFn) {
|
|
testResults.total++;
|
|
try {
|
|
await testFn();
|
|
testResults.passed++;
|
|
console.log(`✓ ${testName} - PASSED`);
|
|
} catch (error) {
|
|
testResults.failed++;
|
|
console.log(`✗ ${testName} - FAILED`);
|
|
console.log(` Error: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
// Setup: Login as admin and regular user
|
|
async function setup() {
|
|
try {
|
|
// Login as admin
|
|
const adminLogin = await axios.post(`${BASE_URL}/auth/login`, {
|
|
email: 'admin@quiz.com',
|
|
password: 'Admin@123'
|
|
});
|
|
adminToken = adminLogin.data.data.token;
|
|
console.log('✓ Logged in as admin');
|
|
|
|
// Create and login as regular user
|
|
const timestamp = Date.now();
|
|
const regularUser = {
|
|
username: `testuser${timestamp}`,
|
|
email: `testuser${timestamp}@test.com`,
|
|
password: 'Test@123'
|
|
};
|
|
|
|
await axios.post(`${BASE_URL}/auth/register`, regularUser);
|
|
const userLogin = await axios.post(`${BASE_URL}/auth/login`, {
|
|
email: regularUser.email,
|
|
password: regularUser.password
|
|
});
|
|
regularUserToken = userLogin.data.data.token;
|
|
console.log('✓ Created and logged in as regular user\n');
|
|
|
|
} catch (error) {
|
|
console.error('Setup failed:', error.response?.data || error.message);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
// Tests
|
|
async function runTests() {
|
|
console.log('========================================');
|
|
console.log('Testing Get Question by ID API');
|
|
console.log('========================================\n');
|
|
|
|
await setup();
|
|
|
|
// Test 1: Get guest-accessible question without auth
|
|
await runTest('Test 1: Get guest-accessible question without auth', async () => {
|
|
const response = await axios.get(`${BASE_URL}/questions/${QUESTION_IDS.GUEST_REACT_EASY}`);
|
|
|
|
if (response.data.success !== true) throw new Error('Response success should be true');
|
|
if (!response.data.data) throw new Error('Response should contain data');
|
|
if (response.data.data.id !== QUESTION_IDS.GUEST_REACT_EASY) throw new Error('Wrong question ID');
|
|
if (!response.data.data.category) throw new Error('Category info should be included');
|
|
if (response.data.data.category.name !== 'React') throw new Error('Wrong category');
|
|
|
|
console.log(` Retrieved question: "${response.data.data.questionText.substring(0, 50)}..."`);
|
|
});
|
|
|
|
// Test 2: Guest blocked from auth-only question
|
|
await runTest('Test 2: Guest blocked from auth-only question', async () => {
|
|
try {
|
|
await axios.get(`${BASE_URL}/questions/${QUESTION_IDS.AUTH_TYPESCRIPT_HARD}`);
|
|
throw new Error('Should have returned 403');
|
|
} catch (error) {
|
|
if (error.response?.status !== 403) throw new Error(`Expected 403, got ${error.response?.status}`);
|
|
if (!error.response.data.message.includes('authentication')) {
|
|
throw new Error('Error message should mention authentication');
|
|
}
|
|
console.log(` Correctly blocked with: ${error.response.data.message}`);
|
|
}
|
|
});
|
|
|
|
// Test 3: Authenticated user can access auth-only question
|
|
await runTest('Test 3: Authenticated user can access auth-only question', async () => {
|
|
const response = await axios.get(`${BASE_URL}/questions/${QUESTION_IDS.AUTH_TYPESCRIPT_HARD}`, {
|
|
headers: { Authorization: `Bearer ${regularUserToken}` }
|
|
});
|
|
|
|
if (response.data.success !== true) throw new Error('Response success should be true');
|
|
if (!response.data.data) throw new Error('Response should contain data');
|
|
if (response.data.data.category.name !== 'TypeScript') throw new Error('Wrong category');
|
|
if (response.data.data.difficulty !== 'hard') throw new Error('Wrong difficulty');
|
|
|
|
console.log(` Retrieved auth-only question from ${response.data.data.category.name}`);
|
|
});
|
|
|
|
// Test 4: Invalid question UUID format
|
|
await runTest('Test 4: Invalid question UUID format', async () => {
|
|
try {
|
|
await axios.get(`${BASE_URL}/questions/invalid-uuid-123`);
|
|
throw new Error('Should have returned 400');
|
|
} catch (error) {
|
|
if (error.response?.status !== 400) throw new Error(`Expected 400, got ${error.response?.status}`);
|
|
if (!error.response.data.message.includes('Invalid question ID')) {
|
|
throw new Error('Should mention invalid ID format');
|
|
}
|
|
console.log(` Correctly rejected invalid UUID`);
|
|
}
|
|
});
|
|
|
|
// Test 5: Non-existent question
|
|
await runTest('Test 5: Non-existent question', async () => {
|
|
const fakeUuid = '00000000-0000-0000-0000-000000000000';
|
|
try {
|
|
await axios.get(`${BASE_URL}/questions/${fakeUuid}`);
|
|
throw new Error('Should have returned 404');
|
|
} catch (error) {
|
|
if (error.response?.status !== 404) throw new Error(`Expected 404, got ${error.response?.status}`);
|
|
if (!error.response.data.message.includes('not found')) {
|
|
throw new Error('Should mention question not found');
|
|
}
|
|
console.log(` Correctly returned 404 for non-existent question`);
|
|
}
|
|
});
|
|
|
|
// Test 6: Response structure validation
|
|
await runTest('Test 6: Response structure validation', async () => {
|
|
const response = await axios.get(`${BASE_URL}/questions/${QUESTION_IDS.GUEST_JS_EASY}`);
|
|
|
|
// Check top-level structure
|
|
const requiredTopFields = ['success', 'data', 'message'];
|
|
for (const field of requiredTopFields) {
|
|
if (!(field in response.data)) throw new Error(`Missing field: ${field}`);
|
|
}
|
|
|
|
// Check question data structure
|
|
const question = response.data.data;
|
|
const requiredQuestionFields = [
|
|
'id', 'questionText', 'questionType', 'options', 'difficulty',
|
|
'points', 'explanation', 'tags', 'accuracy', 'statistics', 'category'
|
|
];
|
|
for (const field of requiredQuestionFields) {
|
|
if (!(field in question)) throw new Error(`Missing question field: ${field}`);
|
|
}
|
|
|
|
// Check statistics structure
|
|
const statsFields = ['timesAttempted', 'timesCorrect', 'accuracy'];
|
|
for (const field of statsFields) {
|
|
if (!(field in question.statistics)) throw new Error(`Missing statistics field: ${field}`);
|
|
}
|
|
|
|
// Check category structure
|
|
const categoryFields = ['id', 'name', 'slug', 'icon', 'color', 'guestAccessible'];
|
|
for (const field of categoryFields) {
|
|
if (!(field in question.category)) throw new Error(`Missing category field: ${field}`);
|
|
}
|
|
|
|
// Verify correct_answer is NOT exposed
|
|
if ('correctAnswer' in question || 'correct_answer' in question) {
|
|
throw new Error('Correct answer should not be exposed');
|
|
}
|
|
|
|
console.log(` Response structure validated`);
|
|
});
|
|
|
|
// Test 7: Accuracy calculation present
|
|
await runTest('Test 7: Accuracy calculation present', async () => {
|
|
const response = await axios.get(`${BASE_URL}/questions/${QUESTION_IDS.GUEST_ANGULAR_EASY}`, {
|
|
headers: { Authorization: `Bearer ${regularUserToken}` }
|
|
});
|
|
|
|
if (response.data.success !== true) throw new Error('Response success should be true');
|
|
|
|
const question = response.data.data;
|
|
if (typeof question.accuracy !== 'number') throw new Error('Accuracy should be a number');
|
|
if (question.accuracy < 0 || question.accuracy > 100) {
|
|
throw new Error(`Invalid accuracy: ${question.accuracy}`);
|
|
}
|
|
|
|
// Check statistics match
|
|
if (question.accuracy !== question.statistics.accuracy) {
|
|
throw new Error('Accuracy mismatch between root and statistics');
|
|
}
|
|
|
|
console.log(` Accuracy: ${question.accuracy}% (${question.statistics.timesCorrect}/${question.statistics.timesAttempted})`);
|
|
});
|
|
|
|
// Test 8: Multiple question types work
|
|
await runTest('Test 8: Question type field present and valid', async () => {
|
|
const response = await axios.get(`${BASE_URL}/questions/${QUESTION_IDS.GUEST_JS_EASY}`);
|
|
|
|
if (response.data.success !== true) throw new Error('Response success should be true');
|
|
|
|
const question = response.data.data;
|
|
if (!question.questionType) throw new Error('Question type should be present');
|
|
|
|
const validTypes = ['multiple', 'trueFalse', 'written'];
|
|
if (!validTypes.includes(question.questionType)) {
|
|
throw new Error(`Invalid question type: ${question.questionType}`);
|
|
}
|
|
|
|
console.log(` Question type: ${question.questionType}`);
|
|
});
|
|
|
|
// Test 9: Options field present for multiple choice
|
|
await runTest('Test 9: Options field present', async () => {
|
|
const response = await axios.get(`${BASE_URL}/questions/${QUESTION_IDS.GUEST_REACT_EASY}`);
|
|
|
|
if (response.data.success !== true) throw new Error('Response success should be true');
|
|
|
|
const question = response.data.data;
|
|
if (question.questionType === 'multiple' && !question.options) {
|
|
throw new Error('Options should be present for multiple choice questions');
|
|
}
|
|
|
|
if (question.options && !Array.isArray(question.options)) {
|
|
throw new Error('Options should be an array');
|
|
}
|
|
|
|
console.log(` Options field validated (${question.options?.length || 0} options)`);
|
|
});
|
|
|
|
// Test 10: Difficulty levels represented correctly
|
|
await runTest('Test 10: Difficulty levels validated', async () => {
|
|
const testQuestions = [
|
|
{ id: QUESTION_IDS.GUEST_REACT_EASY, expected: 'easy' },
|
|
{ id: QUESTION_IDS.AUTH_SYSTEM_DESIGN, expected: 'medium' },
|
|
{ id: QUESTION_IDS.AUTH_NODEJS_HARD, expected: 'hard' },
|
|
];
|
|
|
|
for (const testQ of testQuestions) {
|
|
const response = await axios.get(`${BASE_URL}/questions/${testQ.id}`, {
|
|
headers: { Authorization: `Bearer ${regularUserToken}` }
|
|
});
|
|
|
|
if (response.data.data.difficulty !== testQ.expected) {
|
|
throw new Error(`Expected difficulty ${testQ.expected}, got ${response.data.data.difficulty}`);
|
|
}
|
|
}
|
|
|
|
console.log(` All difficulty levels validated (easy, medium, hard)`);
|
|
});
|
|
|
|
// Test 11: Points based on difficulty
|
|
await runTest('Test 11: Points correspond to difficulty', async () => {
|
|
const response1 = await axios.get(`${BASE_URL}/questions/${QUESTION_IDS.GUEST_REACT_EASY}`);
|
|
const response2 = await axios.get(`${BASE_URL}/questions/${QUESTION_IDS.AUTH_SYSTEM_DESIGN}`, {
|
|
headers: { Authorization: `Bearer ${regularUserToken}` }
|
|
});
|
|
const response3 = await axios.get(`${BASE_URL}/questions/${QUESTION_IDS.AUTH_NODEJS_HARD}`, {
|
|
headers: { Authorization: `Bearer ${regularUserToken}` }
|
|
});
|
|
|
|
const easyPoints = response1.data.data.points;
|
|
const mediumPoints = response2.data.data.points;
|
|
const hardPoints = response3.data.data.points;
|
|
|
|
// Actual point values from database: easy=5, medium=10, hard=15
|
|
if (easyPoints !== 5) throw new Error(`Easy should be 5 points, got ${easyPoints}`);
|
|
if (mediumPoints !== 10) throw new Error(`Medium should be 10 points, got ${mediumPoints}`);
|
|
if (hardPoints !== 15) throw new Error(`Hard should be 15 points, got ${hardPoints}`);
|
|
|
|
console.log(` Points validated: easy=${easyPoints}, medium=${mediumPoints}, hard=${hardPoints}`);
|
|
});
|
|
|
|
// Test 12: Tags and keywords present
|
|
await runTest('Test 12: Tags and keywords fields present', async () => {
|
|
const response = await axios.get(`${BASE_URL}/questions/${QUESTION_IDS.GUEST_JS_EASY}`);
|
|
|
|
if (response.data.success !== true) throw new Error('Response success should be true');
|
|
|
|
const question = response.data.data;
|
|
|
|
// Tags should be present (can be null or array)
|
|
if (!('tags' in question)) throw new Error('Tags field should be present');
|
|
if (question.tags !== null && !Array.isArray(question.tags)) {
|
|
throw new Error('Tags should be null or array');
|
|
}
|
|
|
|
// Keywords should be present (can be null or array)
|
|
if (!('keywords' in question)) throw new Error('Keywords field should be present');
|
|
if (question.keywords !== null && !Array.isArray(question.keywords)) {
|
|
throw new Error('Keywords should be null or array');
|
|
}
|
|
|
|
console.log(` Tags: ${question.tags?.length || 0}, Keywords: ${question.keywords?.length || 0}`);
|
|
});
|
|
|
|
// Summary
|
|
console.log('\n========================================');
|
|
console.log('Test Summary');
|
|
console.log('========================================');
|
|
console.log(`Passed: ${testResults.passed}`);
|
|
console.log(`Failed: ${testResults.failed}`);
|
|
console.log(`Total: ${testResults.total}`);
|
|
console.log('========================================\n');
|
|
|
|
if (testResults.failed === 0) {
|
|
console.log('✓ All tests passed!\n');
|
|
} else {
|
|
console.log('✗ Some tests failed.\n');
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
// Run tests
|
|
runTests().catch(error => {
|
|
console.error('Test execution failed:', error);
|
|
process.exit(1);
|
|
});
|