const request = require('supertest'); const app = require('../../src/app'); const User = require('../../src/models/User'); const Bookmark = require('../../src/models/Bookmark'); const dbConnection = require('../../src/database/connection'); describe('Bookmarks Integration Tests', () => { let testUser1, testUser2; let authToken1, authToken2; beforeAll(async () => { // Clean up any existing test data await dbConnection.query('DELETE FROM bookmarks WHERE user_id IN (SELECT id FROM users WHERE email LIKE $1)', ['%bookmark-test%']); await dbConnection.query('DELETE FROM users WHERE email LIKE $1', ['%bookmark-test%']); // Create test users testUser1 = await User.create({ email: 'bookmark-test-user1@example.com', password: 'TestPassword123!' }); await testUser1.verifyEmail(); testUser2 = await User.create({ email: 'bookmark-test-user2@example.com', password: 'TestPassword123!' }); await testUser2.verifyEmail(); // Login both users to get auth tokens const login1 = await request(app) .post('/api/auth/login') .send({ email: 'bookmark-test-user1@example.com', password: 'TestPassword123!' }); const login2 = await request(app) .post('/api/auth/login') .send({ email: 'bookmark-test-user2@example.com', password: 'TestPassword123!' }); const cookies1 = login1.headers['set-cookie']; const cookies2 = login2.headers['set-cookie']; authToken1 = cookies1.find(cookie => cookie.includes('authToken')).split('authToken=')[1].split(';')[0]; authToken2 = cookies2.find(cookie => cookie.includes('authToken')).split('authToken=')[1].split(';')[0]; }); afterAll(async () => { // Clean up test data await dbConnection.query('DELETE FROM bookmarks WHERE user_id IN ($1, $2)', [testUser1.id, testUser2.id]); await testUser1.delete(); await testUser2.delete(); await dbConnection.end(); }); describe('Bookmark CRUD Operations', () => { let testBookmark; describe('Create Bookmark', () => { it('should create a new bookmark', async () => { const bookmarkData = { title: 'Test Bookmark', url: 'https://example.com', folder: 'Test Folder', status: 'valid' }; const response = await request(app) .post('/api/bookmarks') .set('Cookie', `authToken=${authToken1}`) .send(bookmarkData) .expect(201); expect(response.body).toHaveProperty('message'); expect(response.body).toHaveProperty('bookmark'); expect(response.body.bookmark.title).toBe(bookmarkData.title); expect(response.body.bookmark.url).toBe(bookmarkData.url); expect(response.body.bookmark.folder).toBe(bookmarkData.folder); expect(response.body.bookmark).not.toHaveProperty('user_id'); // Should be filtered out testBookmark = response.body.bookmark; }); it('should require authentication', async () => { const bookmarkData = { title: 'Test Bookmark', url: 'https://example.com' }; const response = await request(app) .post('/api/bookmarks') .send(bookmarkData) .expect(401); expect(response.body).toHaveProperty('error'); }); it('should validate required fields', async () => { const response = await request(app) .post('/api/bookmarks') .set('Cookie', `authToken=${authToken1}`) .send({ title: 'Missing URL' }) .expect(400); expect(response.body.code).toBe('MISSING_REQUIRED_FIELDS'); }); it('should validate URL format', async () => { const bookmarkData = { title: 'Invalid URL Bookmark', url: 'not-a-valid-url' }; const response = await request(app) .post('/api/bookmarks') .set('Cookie', `authToken=${authToken1}`) .send(bookmarkData) .expect(400); expect(response.body.code).toBe('VALIDATION_ERROR'); }); }); describe('Get Bookmarks', () => { beforeEach(async () => { // Create test bookmarks for user1 await Bookmark.create(testUser1.id, { title: 'Work Bookmark 1', url: 'https://work1.com', folder: 'Work', status: 'valid' }); await Bookmark.create(testUser1.id, { title: 'Work Bookmark 2', url: 'https://work2.com', folder: 'Work', status: 'invalid' }); await Bookmark.create(testUser1.id, { title: 'Personal Bookmark', url: 'https://personal.com', folder: 'Personal', status: 'valid' }); // Create bookmark for user2 (should not be visible to user1) await Bookmark.create(testUser2.id, { title: 'User2 Bookmark', url: 'https://user2.com', folder: 'Private', status: 'valid' }); }); afterEach(async () => { // Clean up bookmarks await dbConnection.query('DELETE FROM bookmarks WHERE user_id IN ($1, $2)', [testUser1.id, testUser2.id]); }); it('should get user bookmarks with pagination', async () => { const response = await request(app) .get('/api/bookmarks') .set('Cookie', `authToken=${authToken1}`) .expect(200); expect(response.body).toHaveProperty('bookmarks'); expect(response.body).toHaveProperty('pagination'); expect(response.body.bookmarks).toHaveLength(3); // Only user1's bookmarks expect(response.body.pagination.totalCount).toBe(3); // Verify data isolation - should not see user2's bookmarks const user2Bookmark = response.body.bookmarks.find(b => b.title === 'User2 Bookmark'); expect(user2Bookmark).toBeUndefined(); }); it('should filter bookmarks by folder', async () => { const response = await request(app) .get('/api/bookmarks?folder=Work') .set('Cookie', `authToken=${authToken1}`) .expect(200); expect(response.body.bookmarks).toHaveLength(2); response.body.bookmarks.forEach(bookmark => { expect(bookmark.folder).toBe('Work'); }); }); it('should filter bookmarks by status', async () => { const response = await request(app) .get('/api/bookmarks?status=valid') .set('Cookie', `authToken=${authToken1}`) .expect(200); expect(response.body.bookmarks).toHaveLength(2); response.body.bookmarks.forEach(bookmark => { expect(bookmark.status).toBe('valid'); }); }); it('should search bookmarks by title and URL', async () => { const response = await request(app) .get('/api/bookmarks?search=work') .set('Cookie', `authToken=${authToken1}`) .expect(200); expect(response.body.bookmarks).toHaveLength(2); response.body.bookmarks.forEach(bookmark => { expect(bookmark.title.toLowerCase()).toContain('work'); }); }); it('should handle pagination parameters', async () => { const response = await request(app) .get('/api/bookmarks?page=1&limit=2') .set('Cookie', `authToken=${authToken1}`) .expect(200); expect(response.body.bookmarks).toHaveLength(2); expect(response.body.pagination.page).toBe(1); expect(response.body.pagination.limit).toBe(2); expect(response.body.pagination.totalCount).toBe(3); expect(response.body.pagination.hasNext).toBe(true); }); it('should require authentication', async () => { const response = await request(app) .get('/api/bookmarks') .expect(401); expect(response.body).toHaveProperty('error'); }); }); describe('Get Single Bookmark', () => { let userBookmark; beforeEach(async () => { userBookmark = await Bookmark.create(testUser1.id, { title: 'Single Bookmark Test', url: 'https://single.com', folder: 'Test' }); }); afterEach(async () => { if (userBookmark) { await userBookmark.delete(); } }); it('should get bookmark by ID', async () => { const response = await request(app) .get(`/api/bookmarks/${userBookmark.id}`) .set('Cookie', `authToken=${authToken1}`) .expect(200); expect(response.body).toHaveProperty('bookmark'); expect(response.body.bookmark.id).toBe(userBookmark.id); expect(response.body.bookmark.title).toBe('Single Bookmark Test'); }); it('should not allow access to other users bookmarks', async () => { const response = await request(app) .get(`/api/bookmarks/${userBookmark.id}`) .set('Cookie', `authToken=${authToken2}`) // Different user .expect(404); expect(response.body.code).toBe('BOOKMARK_NOT_FOUND'); }); it('should return 404 for non-existent bookmark', async () => { const response = await request(app) .get('/api/bookmarks/non-existent-id') .set('Cookie', `authToken=${authToken1}`) .expect(404); expect(response.body.code).toBe('BOOKMARK_NOT_FOUND'); }); }); describe('Update Bookmark', () => { let userBookmark; beforeEach(async () => { userBookmark = await Bookmark.create(testUser1.id, { title: 'Original Title', url: 'https://original.com', folder: 'Original Folder' }); }); afterEach(async () => { if (userBookmark) { await userBookmark.delete(); } }); it('should update bookmark successfully', async () => { const updates = { title: 'Updated Title', folder: 'Updated Folder' }; const response = await request(app) .put(`/api/bookmarks/${userBookmark.id}`) .set('Cookie', `authToken=${authToken1}`) .send(updates) .expect(200); expect(response.body).toHaveProperty('message'); expect(response.body).toHaveProperty('bookmark'); expect(response.body.bookmark.title).toBe('Updated Title'); expect(response.body.bookmark.folder).toBe('Updated Folder'); expect(response.body.bookmark.url).toBe('https://original.com'); // Unchanged }); it('should not allow updating other users bookmarks', async () => { const updates = { title: 'Unauthorized Update' }; const response = await request(app) .put(`/api/bookmarks/${userBookmark.id}`) .set('Cookie', `authToken=${authToken2}`) // Different user .send(updates) .expect(404); expect(response.body.code).toBe('BOOKMARK_NOT_FOUND'); }); it('should validate update data', async () => { const updates = { url: 'invalid-url' }; const response = await request(app) .put(`/api/bookmarks/${userBookmark.id}`) .set('Cookie', `authToken=${authToken1}`) .send(updates) .expect(400); expect(response.body.code).toBe('VALIDATION_ERROR'); }); }); describe('Delete Bookmark', () => { let userBookmark; beforeEach(async () => { userBookmark = await Bookmark.create(testUser1.id, { title: 'To Be Deleted', url: 'https://delete.com', folder: 'Delete Test' }); }); it('should delete bookmark successfully', async () => { const response = await request(app) .delete(`/api/bookmarks/${userBookmark.id}`) .set('Cookie', `authToken=${authToken1}`) .expect(200); expect(response.body).toHaveProperty('message'); expect(response.body.message).toContain('deleted successfully'); // Verify bookmark is deleted const deletedBookmark = await Bookmark.findByIdAndUserId(userBookmark.id, testUser1.id); expect(deletedBookmark).toBeNull(); userBookmark = null; // Prevent cleanup attempt }); it('should not allow deleting other users bookmarks', async () => { const response = await request(app) .delete(`/api/bookmarks/${userBookmark.id}`) .set('Cookie', `authToken=${authToken2}`) // Different user .expect(404); expect(response.body.code).toBe('BOOKMARK_NOT_FOUND'); }); it('should return 404 for non-existent bookmark', async () => { const response = await request(app) .delete('/api/bookmarks/non-existent-id') .set('Cookie', `authToken=${authToken1}`) .expect(404); expect(response.body.code).toBe('BOOKMARK_NOT_FOUND'); }); afterEach(async () => { if (userBookmark) { await userBookmark.delete(); } }); }); }); describe('Bulk Operations', () => { afterEach(async () => { // Clean up bookmarks after each test await dbConnection.query('DELETE FROM bookmarks WHERE user_id = $1', [testUser1.id]); }); describe('Bulk Create', () => { it('should create multiple bookmarks', async () => { const bookmarksData = [ { title: 'Bulk Bookmark 1', url: 'https://bulk1.com', folder: 'Bulk Test' }, { title: 'Bulk Bookmark 2', url: 'https://bulk2.com', folder: 'Bulk Test' } ]; const response = await request(app) .post('/api/bookmarks/bulk') .set('Cookie', `authToken=${authToken1}`) .send({ bookmarks: bookmarksData }) .expect(201); expect(response.body).toHaveProperty('message'); expect(response.body).toHaveProperty('count'); expect(response.body).toHaveProperty('bookmarks'); expect(response.body.count).toBe(2); expect(response.body.bookmarks).toHaveLength(2); }); it('should validate all bookmarks before creation', async () => { const bookmarksData = [ { title: 'Valid Bookmark', url: 'https://valid.com' }, { title: '', // Invalid url: 'invalid-url' // Invalid } ]; const response = await request(app) .post('/api/bookmarks/bulk') .set('Cookie', `authToken=${authToken1}`) .send({ bookmarks: bookmarksData }) .expect(400); expect(response.body.code).toBe('VALIDATION_ERROR'); }); it('should reject too many bookmarks', async () => { const bookmarksData = Array(1001).fill({ title: 'Too Many', url: 'https://toomany.com' }); const response = await request(app) .post('/api/bookmarks/bulk') .set('Cookie', `authToken=${authToken1}`) .send({ bookmarks: bookmarksData }) .expect(400); expect(response.body.code).toBe('TOO_MANY_BOOKMARKS'); }); it('should reject invalid data format', async () => { const response = await request(app) .post('/api/bookmarks/bulk') .set('Cookie', `authToken=${authToken1}`) .send({ bookmarks: 'not-an-array' }) .expect(400); expect(response.body.code).toBe('INVALID_DATA_FORMAT'); }); }); describe('Export Bookmarks', () => { beforeEach(async () => { // Create test bookmarks await Bookmark.create(testUser1.id, { title: 'Export Test 1', url: 'https://export1.com', folder: 'Export' }); await Bookmark.create(testUser1.id, { title: 'Export Test 2', url: 'https://export2.com', folder: 'Export' }); }); it('should export user bookmarks as JSON', async () => { const response = await request(app) .post('/api/bookmarks/export') .set('Cookie', `authToken=${authToken1}`) .send({ format: 'json' }) .expect(200); expect(response.body).toHaveProperty('bookmarks'); expect(response.body).toHaveProperty('exportDate'); expect(response.body).toHaveProperty('count'); expect(response.body.bookmarks).toHaveLength(2); expect(response.body.count).toBe(2); // Verify no user_id in exported data response.body.bookmarks.forEach(bookmark => { expect(bookmark).not.toHaveProperty('user_id'); }); }); it('should reject unsupported export format', async () => { const response = await request(app) .post('/api/bookmarks/export') .set('Cookie', `authToken=${authToken1}`) .send({ format: 'xml' }) .expect(400); expect(response.body.code).toBe('UNSUPPORTED_FORMAT'); }); }); describe('Migration', () => { it('should migrate bookmarks with merge strategy', async () => { // Create existing bookmark await Bookmark.create(testUser1.id, { title: 'Existing Bookmark', url: 'https://existing.com', folder: 'Existing' }); const localBookmarks = [ { title: 'Local Bookmark 1', url: 'https://local1.com', folder: 'Local' }, { title: 'Local Bookmark 2', url: 'https://local2.com', folder: 'Local' }, { title: 'Duplicate', url: 'https://existing.com', // Duplicate URL folder: 'Local' } ]; const response = await request(app) .post('/api/bookmarks/migrate') .set('Cookie', `authToken=${authToken1}`) .send({ bookmarks: localBookmarks, strategy: 'merge' }) .expect(201); expect(response.body).toHaveProperty('summary'); expect(response.body.summary.totalProvided).toBe(3); expect(response.body.summary.duplicatesSkipped).toBe(1); expect(response.body.summary.successfullyMigrated).toBe(2); expect(response.body.summary.strategy).toBe('merge'); }); it('should migrate bookmarks with replace strategy', async () => { // Create existing bookmark await Bookmark.create(testUser1.id, { title: 'To Be Replaced', url: 'https://replaced.com', folder: 'Old' }); const localBookmarks = [ { title: 'New Bookmark', url: 'https://new.com', folder: 'New' } ]; const response = await request(app) .post('/api/bookmarks/migrate') .set('Cookie', `authToken=${authToken1}`) .send({ bookmarks: localBookmarks, strategy: 'replace' }) .expect(201); expect(response.body.summary.successfullyMigrated).toBe(1); expect(response.body.summary.strategy).toBe('replace'); // Verify old bookmark was deleted const allBookmarks = await Bookmark.findByUserId(testUser1.id); expect(allBookmarks.bookmarks).toHaveLength(1); expect(allBookmarks.bookmarks[0].title).toBe('New Bookmark'); }); it('should validate migration strategy', async () => { const response = await request(app) .post('/api/bookmarks/migrate') .set('Cookie', `authToken=${authToken1}`) .send({ bookmarks: [], strategy: 'invalid-strategy' }) .expect(400); expect(response.body.code).toBe('INVALID_STRATEGY'); }); }); }); describe('Data Isolation Tests', () => { beforeEach(async () => { // Create bookmarks for both users await Bookmark.create(testUser1.id, { title: 'User1 Private Bookmark', url: 'https://user1-private.com', folder: 'Private' }); await Bookmark.create(testUser2.id, { title: 'User2 Private Bookmark', url: 'https://user2-private.com', folder: 'Private' }); }); afterEach(async () => { await dbConnection.query('DELETE FROM bookmarks WHERE user_id IN ($1, $2)', [testUser1.id, testUser2.id]); }); it('should only return bookmarks for authenticated user', async () => { const response1 = await request(app) .get('/api/bookmarks') .set('Cookie', `authToken=${authToken1}`) .expect(200); const response2 = await request(app) .get('/api/bookmarks') .set('Cookie', `authToken=${authToken2}`) .expect(200); expect(response1.body.bookmarks).toHaveLength(1); expect(response2.body.bookmarks).toHaveLength(1); expect(response1.body.bookmarks[0].title).toBe('User1 Private Bookmark'); expect(response2.body.bookmarks[0].title).toBe('User2 Private Bookmark'); }); it('should not allow access to other users bookmark statistics', async () => { const response1 = await request(app) .get('/api/bookmarks/stats') .set('Cookie', `authToken=${authToken1}`) .expect(200); const response2 = await request(app) .get('/api/bookmarks/stats') .set('Cookie', `authToken=${authToken2}`) .expect(200); expect(response1.body.stats.totalBookmarks).toBe(1); expect(response2.body.stats.totalBookmarks).toBe(1); }); it('should not allow access to other users folders', async () => { const response1 = await request(app) .get('/api/bookmarks/folders') .set('Cookie', `authToken=${authToken1}`) .expect(200); const response2 = await request(app) .get('/api/bookmarks/folders') .set('Cookie', `authToken=${authToken2}`) .expect(200); expect(response1.body.folders).toHaveLength(1); expect(response2.body.folders).toHaveLength(1); expect(response1.body.folders[0].folder).toBe('Private'); expect(response2.body.folders[0].folder).toBe('Private'); }); }); });