add changes
This commit is contained in:
520
backend/tests/test-user-bookmarks.js
Normal file
520
backend/tests/test-user-bookmarks.js
Normal file
@@ -0,0 +1,520 @@
|
||||
const axios = require('axios');
|
||||
|
||||
const BASE_URL = 'http://localhost:3000/api';
|
||||
|
||||
// Test user credentials
|
||||
const testUser = {
|
||||
username: 'bookmarklist1',
|
||||
email: 'bookmarklist1@example.com',
|
||||
password: 'Test123!@#'
|
||||
};
|
||||
|
||||
const testUser2 = {
|
||||
username: 'bookmarklist2',
|
||||
email: 'bookmarklist2@example.com',
|
||||
password: 'Test123!@#'
|
||||
};
|
||||
|
||||
let userToken = '';
|
||||
let userId = '';
|
||||
let user2Token = '';
|
||||
let user2Id = '';
|
||||
let categoryId = '';
|
||||
let questionIds = [];
|
||||
let bookmarkIds = [];
|
||||
|
||||
// Test results tracking
|
||||
let passedTests = 0;
|
||||
let failedTests = 0;
|
||||
const testResults = [];
|
||||
|
||||
// Helper function to log test results
|
||||
function logTest(testName, passed, error = null) {
|
||||
if (passed) {
|
||||
console.log(`✓ ${testName}`);
|
||||
passedTests++;
|
||||
} else {
|
||||
console.log(`✗ ${testName}`);
|
||||
if (error) {
|
||||
console.log(` Error: ${error}`);
|
||||
}
|
||||
failedTests++;
|
||||
}
|
||||
testResults.push({ testName, passed, error });
|
||||
}
|
||||
|
||||
// Helper function to delay between tests
|
||||
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
async function setup() {
|
||||
try {
|
||||
console.log('Setting up test data...');
|
||||
|
||||
// Register and login first user
|
||||
try {
|
||||
const regRes = await axios.post(`${BASE_URL}/auth/register`, testUser);
|
||||
console.log('✓ Test user registered');
|
||||
} catch (err) {
|
||||
// User might already exist, continue to login
|
||||
if (err.response?.status !== 409) {
|
||||
console.log('Registration error:', err.response?.data?.message || err.message);
|
||||
}
|
||||
}
|
||||
|
||||
const loginRes = await axios.post(`${BASE_URL}/auth/login`, {
|
||||
email: testUser.email,
|
||||
password: testUser.password
|
||||
});
|
||||
|
||||
userToken = loginRes.data.data.token;
|
||||
userId = loginRes.data.data.user.id;
|
||||
console.log('✓ Test user logged in');
|
||||
|
||||
// Register and login second user
|
||||
try {
|
||||
const regRes = await axios.post(`${BASE_URL}/auth/register`, testUser2);
|
||||
console.log('✓ Second user registered');
|
||||
} catch (err) {
|
||||
// User might already exist, continue to login
|
||||
if (err.response?.status !== 409) {
|
||||
console.log('Registration error:', err.response?.data?.message || err.message);
|
||||
}
|
||||
}
|
||||
|
||||
const login2Res = await axios.post(`${BASE_URL}/auth/login`, {
|
||||
email: testUser2.email,
|
||||
password: testUser2.password
|
||||
});
|
||||
|
||||
user2Token = login2Res.data.data.token;
|
||||
user2Id = login2Res.data.data.user.id;
|
||||
console.log('✓ Second user logged in');
|
||||
|
||||
// Get categories
|
||||
const categoriesRes = await axios.get(`${BASE_URL}/categories`);
|
||||
const categories = categoriesRes.data.data;
|
||||
|
||||
// Find a category with questions
|
||||
let testCategory = null;
|
||||
for (const cat of categories) {
|
||||
if (cat.questionCount > 0) {
|
||||
testCategory = cat;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!testCategory) {
|
||||
throw new Error('No categories with questions found');
|
||||
}
|
||||
|
||||
categoryId = testCategory.id;
|
||||
console.log(`✓ Found test category: ${testCategory.name} (${testCategory.questionCount} questions)`);
|
||||
|
||||
// Get questions from this category
|
||||
const questionsRes = await axios.get(`${BASE_URL}/questions/category/${categoryId}?limit=10`);
|
||||
const questions = questionsRes.data.data;
|
||||
|
||||
if (questions.length === 0) {
|
||||
throw new Error('No questions available in category for testing');
|
||||
}
|
||||
|
||||
questionIds = questions.slice(0, Math.min(5, questions.length)).map(q => q.id);
|
||||
console.log(`✓ Found ${questionIds.length} test questions`);
|
||||
|
||||
// Delete any existing bookmarks first (cleanup from previous test runs)
|
||||
for (const questionId of questionIds) {
|
||||
try {
|
||||
await axios.delete(
|
||||
`${BASE_URL}/users/${userId}/bookmarks/${questionId}`,
|
||||
{ headers: { Authorization: `Bearer ${userToken}` } }
|
||||
);
|
||||
} catch (err) {
|
||||
// Ignore if bookmark doesn't exist
|
||||
}
|
||||
}
|
||||
|
||||
// Create bookmarks for testing
|
||||
for (const questionId of questionIds) {
|
||||
const bookmarkRes = await axios.post(
|
||||
`${BASE_URL}/users/${userId}/bookmarks`,
|
||||
{ questionId },
|
||||
{ headers: { Authorization: `Bearer ${userToken}` } }
|
||||
);
|
||||
bookmarkIds.push(bookmarkRes.data.data.id);
|
||||
await delay(100);
|
||||
}
|
||||
console.log(`✓ Created ${bookmarkIds.length} bookmarks for testing`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Setup failed:', error.response?.data || error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function runTests() {
|
||||
console.log('\n============================================================');
|
||||
console.log('USER BOOKMARKS API TESTS');
|
||||
console.log('============================================================\n');
|
||||
|
||||
await setup();
|
||||
|
||||
console.log('\nRunning tests...');
|
||||
|
||||
// Test 1: Get bookmarks with default pagination
|
||||
await delay(100);
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`${BASE_URL}/users/${userId}/bookmarks`,
|
||||
{ headers: { Authorization: `Bearer ${userToken}` } }
|
||||
);
|
||||
|
||||
const passed = response.status === 200 &&
|
||||
response.data.success === true &&
|
||||
Array.isArray(response.data.data.bookmarks) &&
|
||||
response.data.data.bookmarks.length > 0 &&
|
||||
response.data.data.pagination.currentPage === 1 &&
|
||||
response.data.data.pagination.itemsPerPage === 10;
|
||||
|
||||
logTest('Get bookmarks with default pagination', passed);
|
||||
} catch (error) {
|
||||
logTest('Get bookmarks with default pagination', false, error.response?.data?.message || error.message);
|
||||
}
|
||||
|
||||
// Test 2: Pagination structure validation
|
||||
await delay(100);
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`${BASE_URL}/users/${userId}/bookmarks?page=1&limit=5`,
|
||||
{ headers: { Authorization: `Bearer ${userToken}` } }
|
||||
);
|
||||
|
||||
const pagination = response.data.data.pagination;
|
||||
const passed = response.status === 200 &&
|
||||
pagination.currentPage === 1 &&
|
||||
pagination.itemsPerPage === 5 &&
|
||||
typeof pagination.totalPages === 'number' &&
|
||||
typeof pagination.totalItems === 'number' &&
|
||||
typeof pagination.hasNextPage === 'boolean' &&
|
||||
typeof pagination.hasPreviousPage === 'boolean';
|
||||
|
||||
logTest('Pagination structure validation', passed);
|
||||
} catch (error) {
|
||||
logTest('Pagination structure validation', false, error.response?.data?.message || error.message);
|
||||
}
|
||||
|
||||
// Test 3: Bookmark fields validation
|
||||
await delay(100);
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`${BASE_URL}/users/${userId}/bookmarks`,
|
||||
{ headers: { Authorization: `Bearer ${userToken}` } }
|
||||
);
|
||||
|
||||
const bookmark = response.data.data.bookmarks[0];
|
||||
const passed = response.status === 200 &&
|
||||
bookmark.bookmarkId &&
|
||||
bookmark.bookmarkedAt &&
|
||||
bookmark.question &&
|
||||
bookmark.question.id &&
|
||||
bookmark.question.questionText &&
|
||||
bookmark.question.difficulty &&
|
||||
bookmark.question.category &&
|
||||
bookmark.question.statistics;
|
||||
|
||||
logTest('Bookmark fields validation', passed);
|
||||
} catch (error) {
|
||||
logTest('Bookmark fields validation', false, error.response?.data?.message || error.message);
|
||||
}
|
||||
|
||||
// Test 4: Custom limit
|
||||
await delay(100);
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`${BASE_URL}/users/${userId}/bookmarks?limit=2`,
|
||||
{ headers: { Authorization: `Bearer ${userToken}` } }
|
||||
);
|
||||
|
||||
const passed = response.status === 200 &&
|
||||
response.data.data.bookmarks.length <= 2 &&
|
||||
response.data.data.pagination.itemsPerPage === 2;
|
||||
|
||||
logTest('Custom limit', passed);
|
||||
} catch (error) {
|
||||
logTest('Custom limit', false, error.response?.data?.message || error.message);
|
||||
}
|
||||
|
||||
// Test 5: Page 2 navigation
|
||||
await delay(100);
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`${BASE_URL}/users/${userId}/bookmarks?page=2&limit=3`,
|
||||
{ headers: { Authorization: `Bearer ${userToken}` } }
|
||||
);
|
||||
|
||||
const passed = response.status === 200 &&
|
||||
response.data.data.pagination.currentPage === 2 &&
|
||||
response.data.data.pagination.hasPreviousPage === true;
|
||||
|
||||
logTest('Page 2 navigation', passed);
|
||||
} catch (error) {
|
||||
logTest('Page 2 navigation', false, error.response?.data?.message || error.message);
|
||||
}
|
||||
|
||||
// Test 6: Category filter
|
||||
await delay(100);
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`${BASE_URL}/users/${userId}/bookmarks?category=${categoryId}`,
|
||||
{ headers: { Authorization: `Bearer ${userToken}` } }
|
||||
);
|
||||
|
||||
const allMatchCategory = response.data.data.bookmarks.every(
|
||||
b => b.question.category.id === categoryId
|
||||
);
|
||||
|
||||
const passed = response.status === 200 &&
|
||||
allMatchCategory &&
|
||||
response.data.data.filters.category === categoryId;
|
||||
|
||||
logTest('Category filter', passed);
|
||||
} catch (error) {
|
||||
logTest('Category filter', false, error.response?.data?.message || error.message);
|
||||
}
|
||||
|
||||
// Test 7: Difficulty filter
|
||||
await delay(100);
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`${BASE_URL}/users/${userId}/bookmarks?difficulty=medium`,
|
||||
{ headers: { Authorization: `Bearer ${userToken}` } }
|
||||
);
|
||||
|
||||
const allMatchDifficulty = response.data.data.bookmarks.every(
|
||||
b => b.question.difficulty === 'medium'
|
||||
);
|
||||
|
||||
const passed = response.status === 200 &&
|
||||
(response.data.data.bookmarks.length === 0 || allMatchDifficulty) &&
|
||||
response.data.data.filters.difficulty === 'medium';
|
||||
|
||||
logTest('Difficulty filter', passed);
|
||||
} catch (error) {
|
||||
logTest('Difficulty filter', false, error.response?.data?.message || error.message);
|
||||
}
|
||||
|
||||
// Test 8: Sort by difficulty ascending
|
||||
await delay(100);
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`${BASE_URL}/users/${userId}/bookmarks?sortBy=difficulty&sortOrder=asc`,
|
||||
{ headers: { Authorization: `Bearer ${userToken}` } }
|
||||
);
|
||||
|
||||
const passed = response.status === 200 &&
|
||||
response.data.data.sorting.sortBy === 'difficulty' &&
|
||||
response.data.data.sorting.sortOrder === 'asc';
|
||||
|
||||
logTest('Sort by difficulty ascending', passed);
|
||||
} catch (error) {
|
||||
logTest('Sort by difficulty ascending', false, error.response?.data?.message || error.message);
|
||||
}
|
||||
|
||||
// Test 9: Sort by date descending (default)
|
||||
await delay(100);
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`${BASE_URL}/users/${userId}/bookmarks?sortBy=date&sortOrder=desc`,
|
||||
{ headers: { Authorization: `Bearer ${userToken}` } }
|
||||
);
|
||||
|
||||
const passed = response.status === 200 &&
|
||||
response.data.data.sorting.sortBy === 'date' &&
|
||||
response.data.data.sorting.sortOrder === 'desc';
|
||||
|
||||
logTest('Sort by date descending', passed);
|
||||
} catch (error) {
|
||||
logTest('Sort by date descending', false, error.response?.data?.message || error.message);
|
||||
}
|
||||
|
||||
// Test 10: Max limit enforcement (50)
|
||||
await delay(100);
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`${BASE_URL}/users/${userId}/bookmarks?limit=100`,
|
||||
{ headers: { Authorization: `Bearer ${userToken}` } }
|
||||
);
|
||||
|
||||
const passed = response.status === 200 &&
|
||||
response.data.data.pagination.itemsPerPage === 50;
|
||||
|
||||
logTest('Max limit enforcement (50)', passed);
|
||||
} catch (error) {
|
||||
logTest('Max limit enforcement (50)', false, error.response?.data?.message || error.message);
|
||||
}
|
||||
|
||||
// Test 11: Cross-user access blocked
|
||||
await delay(100);
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`${BASE_URL}/users/${user2Id}/bookmarks`,
|
||||
{ headers: { Authorization: `Bearer ${userToken}` } }
|
||||
);
|
||||
|
||||
logTest('Cross-user access blocked', false, 'Expected 403 but got 200');
|
||||
} catch (error) {
|
||||
const passed = error.response?.status === 403;
|
||||
logTest('Cross-user access blocked', passed, passed ? null : `Expected 403 but got ${error.response?.status}`);
|
||||
}
|
||||
|
||||
// Test 12: Unauthenticated request blocked
|
||||
await delay(100);
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`${BASE_URL}/users/${userId}/bookmarks`
|
||||
);
|
||||
|
||||
logTest('Unauthenticated request blocked', false, 'Expected 401 but got 200');
|
||||
} catch (error) {
|
||||
const passed = error.response?.status === 401;
|
||||
logTest('Unauthenticated request blocked', passed, passed ? null : `Expected 401 but got ${error.response?.status}`);
|
||||
}
|
||||
|
||||
// Test 13: Invalid UUID format
|
||||
await delay(100);
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`${BASE_URL}/users/invalid-uuid/bookmarks`,
|
||||
{ headers: { Authorization: `Bearer ${userToken}` } }
|
||||
);
|
||||
|
||||
logTest('Invalid UUID format', false, 'Expected 400 but got 200');
|
||||
} catch (error) {
|
||||
const passed = error.response?.status === 400;
|
||||
logTest('Invalid UUID format', passed, passed ? null : `Expected 400 but got ${error.response?.status}`);
|
||||
}
|
||||
|
||||
// Test 14: Non-existent user
|
||||
await delay(100);
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`${BASE_URL}/users/11111111-1111-1111-1111-111111111111/bookmarks`,
|
||||
{ headers: { Authorization: `Bearer ${userToken}` } }
|
||||
);
|
||||
|
||||
logTest('Non-existent user', false, 'Expected 404 but got 200');
|
||||
} catch (error) {
|
||||
const passed = error.response?.status === 404;
|
||||
logTest('Non-existent user', passed, passed ? null : `Expected 404 but got ${error.response?.status}`);
|
||||
}
|
||||
|
||||
// Test 15: Invalid category ID format
|
||||
await delay(100);
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`${BASE_URL}/users/${userId}/bookmarks?category=invalid-uuid`,
|
||||
{ headers: { Authorization: `Bearer ${userToken}` } }
|
||||
);
|
||||
|
||||
logTest('Invalid category ID format', false, 'Expected 400 but got 200');
|
||||
} catch (error) {
|
||||
const passed = error.response?.status === 400;
|
||||
logTest('Invalid category ID format', passed, passed ? null : `Expected 400 but got ${error.response?.status}`);
|
||||
}
|
||||
|
||||
// Test 16: Invalid difficulty value
|
||||
await delay(100);
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`${BASE_URL}/users/${userId}/bookmarks?difficulty=invalid`,
|
||||
{ headers: { Authorization: `Bearer ${userToken}` } }
|
||||
);
|
||||
|
||||
logTest('Invalid difficulty value', false, 'Expected 400 but got 200');
|
||||
} catch (error) {
|
||||
const passed = error.response?.status === 400;
|
||||
logTest('Invalid difficulty value', passed, passed ? null : `Expected 400 but got ${error.response?.status}`);
|
||||
}
|
||||
|
||||
// Test 17: Invalid sort order
|
||||
await delay(100);
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`${BASE_URL}/users/${userId}/bookmarks?sortOrder=invalid`,
|
||||
{ headers: { Authorization: `Bearer ${userToken}` } }
|
||||
);
|
||||
|
||||
logTest('Invalid sort order', false, 'Expected 400 but got 200');
|
||||
} catch (error) {
|
||||
const passed = error.response?.status === 400;
|
||||
logTest('Invalid sort order', passed, passed ? null : `Expected 400 but got ${error.response?.status}`);
|
||||
}
|
||||
|
||||
// Test 18: Empty bookmarks list
|
||||
await delay(100);
|
||||
try {
|
||||
// Use second user who has no bookmarks
|
||||
const response = await axios.get(
|
||||
`${BASE_URL}/users/${user2Id}/bookmarks`,
|
||||
{ headers: { Authorization: `Bearer ${user2Token}` } }
|
||||
);
|
||||
|
||||
const passed = response.status === 200 &&
|
||||
Array.isArray(response.data.data.bookmarks) &&
|
||||
response.data.data.bookmarks.length === 0 &&
|
||||
response.data.data.pagination.totalItems === 0;
|
||||
|
||||
logTest('Empty bookmarks list', passed);
|
||||
} catch (error) {
|
||||
logTest('Empty bookmarks list', false, error.response?.data?.message || error.message);
|
||||
}
|
||||
|
||||
// Test 19: Combined filters (category + difficulty + sorting)
|
||||
await delay(100);
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`${BASE_URL}/users/${userId}/bookmarks?category=${categoryId}&difficulty=easy&sortBy=date&sortOrder=asc`,
|
||||
{ headers: { Authorization: `Bearer ${userToken}` } }
|
||||
);
|
||||
|
||||
const passed = response.status === 200 &&
|
||||
response.data.data.filters.category === categoryId &&
|
||||
response.data.data.filters.difficulty === 'easy' &&
|
||||
response.data.data.sorting.sortBy === 'date' &&
|
||||
response.data.data.sorting.sortOrder === 'asc';
|
||||
|
||||
logTest('Combined filters', passed);
|
||||
} catch (error) {
|
||||
logTest('Combined filters', false, error.response?.data?.message || error.message);
|
||||
}
|
||||
|
||||
// Test 20: Question statistics included
|
||||
await delay(100);
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`${BASE_URL}/users/${userId}/bookmarks`,
|
||||
{ headers: { Authorization: `Bearer ${userToken}` } }
|
||||
);
|
||||
|
||||
const bookmark = response.data.data.bookmarks[0];
|
||||
const stats = bookmark.question.statistics;
|
||||
const passed = response.status === 200 &&
|
||||
stats &&
|
||||
typeof stats.timesAttempted === 'number' &&
|
||||
typeof stats.timesCorrect === 'number' &&
|
||||
typeof stats.accuracy === 'number';
|
||||
|
||||
logTest('Question statistics included', passed);
|
||||
} catch (error) {
|
||||
logTest('Question statistics included', false, error.response?.data?.message || error.message);
|
||||
}
|
||||
|
||||
// Print results
|
||||
console.log('\n============================================================');
|
||||
console.log(`RESULTS: ${passedTests} passed, ${failedTests} failed out of ${passedTests + failedTests} tests`);
|
||||
console.log('============================================================\n');
|
||||
|
||||
process.exit(failedTests > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
// Run tests
|
||||
runTests();
|
||||
Reference in New Issue
Block a user