const axios = require('axios'); const API_URL = 'http://localhost:3000/api'; // Test users const testUser = { username: 'bookmarktest', email: 'bookmarktest@example.com', password: 'Test123!@#' }; const secondUser = { username: 'bookmarktest2', email: 'bookmarktest2@example.com', password: 'Test123!@#' }; let userToken; let userId; let secondUserToken; let secondUserId; let questionId; // Will get from a real question let categoryId; // Will get from a real category // Test setup async function setup() { console.log('Setting up test data...\n'); try { // Register/login 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('✓ Test 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('✓ Test user logged in'); } // Register/login 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 a real category with questions const categoriesRes = await axios.get(`${API_URL}/categories`, { headers: { 'Authorization': `Bearer ${userToken}` } }); if (!categoriesRes.data.data || categoriesRes.data.data.length === 0) { throw new Error('No categories available for testing'); } // Find a category that has questions let foundQuestion = false; for (const category of categoriesRes.data.data) { if (category.questionCount > 0) { categoryId = category.id; console.log(`✓ Found test category: ${category.name} (${category.questionCount} questions)`); // Get a real question from that category const questionsRes = await axios.get(`${API_URL}/questions/category/${categoryId}?limit=1`, { headers: { 'Authorization': `Bearer ${userToken}` } }); if (questionsRes.data.data.length > 0) { questionId = questionsRes.data.data[0].id; console.log(`✓ Found test question`); foundQuestion = true; break; } } } if (!foundQuestion) { throw new Error('No questions available for testing. Please seed the database first.'); } console.log(''); } catch (error) { console.error('Setup failed:', error.response?.data || error.message); throw error; } } // Tests const tests = [ { name: 'Test 1: Add bookmark successfully', run: async () => { const response = await axios.post(`${API_URL}/users/${userId}/bookmarks`, { questionId }, { headers: { 'Authorization': `Bearer ${userToken}` } }); if (!response.data.success) throw new Error('Request failed'); if (response.status !== 201) throw new Error(`Expected 201, got ${response.status}`); if (!response.data.data.id) throw new Error('Missing bookmark ID'); if (response.data.data.questionId !== questionId) { throw new Error('Question ID mismatch'); } if (!response.data.data.bookmarkedAt) throw new Error('Missing bookmarkedAt timestamp'); if (!response.data.data.question) throw new Error('Missing question details'); return '✓ Bookmark added successfully'; } }, { name: 'Test 2: Reject duplicate bookmark', run: async () => { try { await axios.post(`${API_URL}/users/${userId}/bookmarks`, { questionId }, { headers: { 'Authorization': `Bearer ${userToken}` } }); throw new Error('Should have been rejected'); } catch (error) { if (error.response?.status !== 409) { throw new Error(`Expected 409, got ${error.response?.status}`); } if (!error.response.data.message.toLowerCase().includes('already')) { throw new Error('Error message should mention already bookmarked'); } return '✓ Duplicate bookmark rejected'; } } }, { name: 'Test 3: Remove bookmark successfully', run: async () => { const response = await axios.delete(`${API_URL}/users/${userId}/bookmarks/${questionId}`, { headers: { 'Authorization': `Bearer ${userToken}` } }); if (!response.data.success) throw new Error('Request failed'); if (response.status !== 200) throw new Error(`Expected 200, got ${response.status}`); if (response.data.data.questionId !== questionId) { throw new Error('Question ID mismatch'); } return '✓ Bookmark removed successfully'; } }, { name: 'Test 4: Reject removing non-existent bookmark', run: async () => { try { await axios.delete(`${API_URL}/users/${userId}/bookmarks/${questionId}`, { headers: { 'Authorization': `Bearer ${userToken}` } }); throw new Error('Should have been rejected'); } catch (error) { if (error.response?.status !== 404) { throw new Error(`Expected 404, got ${error.response?.status}`); } if (!error.response.data.message.toLowerCase().includes('not found')) { throw new Error('Error message should mention not found'); } return '✓ Non-existent bookmark rejected'; } } }, { name: 'Test 5: Reject missing questionId', run: async () => { try { await axios.post(`${API_URL}/users/${userId}/bookmarks`, {}, { headers: { 'Authorization': `Bearer ${userToken}` } }); throw new Error('Should have been rejected'); } catch (error) { if (error.response?.status !== 400) { throw new Error(`Expected 400, got ${error.response?.status}`); } if (!error.response.data.message.toLowerCase().includes('required')) { throw new Error('Error message should mention required'); } return '✓ Missing questionId rejected'; } } }, { name: 'Test 6: Reject invalid questionId format', run: async () => { try { await axios.post(`${API_URL}/users/${userId}/bookmarks`, { questionId: 'invalid-uuid' }, { headers: { 'Authorization': `Bearer ${userToken}` } }); throw new Error('Should have been rejected'); } catch (error) { if (error.response?.status !== 400) { throw new Error(`Expected 400, got ${error.response?.status}`); } return '✓ Invalid questionId format rejected'; } } }, { name: 'Test 7: Reject non-existent question', run: async () => { try { const fakeQuestionId = '00000000-0000-0000-0000-000000000000'; await axios.post(`${API_URL}/users/${userId}/bookmarks`, { questionId: fakeQuestionId }, { headers: { 'Authorization': `Bearer ${userToken}` } }); throw new Error('Should have been rejected'); } catch (error) { if (error.response?.status !== 404) { throw new Error(`Expected 404, got ${error.response?.status}`); } if (!error.response.data.message.toLowerCase().includes('not found')) { throw new Error('Error message should mention not found'); } return '✓ Non-existent question rejected'; } } }, { name: 'Test 8: Reject invalid userId format', run: async () => { try { await axios.post(`${API_URL}/users/invalid-uuid/bookmarks`, { questionId }, { headers: { 'Authorization': `Bearer ${userToken}` } }); throw new Error('Should have been rejected'); } catch (error) { if (error.response?.status !== 400) { throw new Error(`Expected 400, got ${error.response?.status}`); } return '✓ Invalid userId format rejected'; } } }, { name: 'Test 9: Reject non-existent user', run: async () => { try { const fakeUserId = '00000000-0000-0000-0000-000000000000'; await axios.post(`${API_URL}/users/${fakeUserId}/bookmarks`, { questionId }, { headers: { 'Authorization': `Bearer ${userToken}` } }); throw new Error('Should have been rejected'); } catch (error) { if (error.response?.status !== 404) { throw new Error(`Expected 404, got ${error.response?.status}`); } return '✓ Non-existent user rejected'; } } }, { name: 'Test 10: Cross-user bookmark addition blocked', run: async () => { try { // Try to add bookmark to second user's account using first user's token await axios.post(`${API_URL}/users/${secondUserId}/bookmarks`, { questionId }, { headers: { 'Authorization': `Bearer ${userToken}` } }); throw new Error('Should have been blocked'); } catch (error) { if (error.response?.status !== 403) { throw new Error(`Expected 403, got ${error.response?.status}`); } return '✓ Cross-user bookmark addition blocked'; } } }, { name: 'Test 11: Cross-user bookmark removal blocked', run: async () => { try { // Try to remove bookmark from second user's account using first user's token await axios.delete(`${API_URL}/users/${secondUserId}/bookmarks/${questionId}`, { headers: { 'Authorization': `Bearer ${userToken}` } }); throw new Error('Should have been blocked'); } catch (error) { if (error.response?.status !== 403) { throw new Error(`Expected 403, got ${error.response?.status}`); } return '✓ Cross-user bookmark removal blocked'; } } }, { name: 'Test 12: Unauthenticated add bookmark blocked', run: async () => { try { await axios.post(`${API_URL}/users/${userId}/bookmarks`, { questionId }); throw new Error('Should have been blocked'); } catch (error) { if (error.response?.status !== 401) { throw new Error(`Expected 401, got ${error.response?.status}`); } return '✓ Unauthenticated add bookmark blocked'; } } }, { name: 'Test 13: Unauthenticated remove bookmark blocked', run: async () => { try { await axios.delete(`${API_URL}/users/${userId}/bookmarks/${questionId}`); throw new Error('Should have been blocked'); } catch (error) { if (error.response?.status !== 401) { throw new Error(`Expected 401, got ${error.response?.status}`); } return '✓ Unauthenticated remove bookmark blocked'; } } }, { name: 'Test 14: Response structure validation', run: async () => { // Add a bookmark for testing response structure const response = await axios.post(`${API_URL}/users/${userId}/bookmarks`, { questionId }, { headers: { 'Authorization': `Bearer ${userToken}` } }); if (!response.data.success) throw new Error('success field missing'); if (!response.data.data) throw new Error('data field missing'); if (!response.data.data.id) throw new Error('bookmark id missing'); if (!response.data.data.questionId) throw new Error('questionId missing'); if (!response.data.data.question) throw new Error('question details missing'); if (!response.data.data.question.id) throw new Error('question.id missing'); if (!response.data.data.question.questionText) throw new Error('question.questionText missing'); if (!response.data.data.question.difficulty) throw new Error('question.difficulty missing'); if (!response.data.data.question.category) throw new Error('question.category missing'); if (!response.data.data.bookmarkedAt) throw new Error('bookmarkedAt missing'); if (!response.data.message) throw new Error('message field missing'); // Clean up await axios.delete(`${API_URL}/users/${userId}/bookmarks/${questionId}`, { headers: { 'Authorization': `Bearer ${userToken}` } }); return '✓ Response structure valid'; } } ]; // Run tests async function runTests() { console.log('============================================================'); console.log('BOOKMARK API TESTS'); console.log('============================================================\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++; } catch (error) { console.log(`✗ ${test.name}`); console.log(` Error: ${error.message}`); if (error.response?.data) { console.log(` Response:`, JSON.stringify(error.response.data, null, 2)); } failed++; } // Small delay to avoid rate limiting await new Promise(resolve => setTimeout(resolve, 100)); } console.log('\n============================================================'); console.log(`RESULTS: ${passed} passed, ${failed} failed out of ${tests.length} tests`); console.log('============================================================'); process.exit(failed > 0 ? 1 : 0); } runTests();