Files
bookmarksite/backend/tests/integration/bookmarks.test.js
2025-07-20 20:43:06 +02:00

693 lines
26 KiB
JavaScript

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');
});
});
});