WIP
This commit is contained in:
475
backend/tests/integration/auth.test.js
Normal file
475
backend/tests/integration/auth.test.js
Normal file
@ -0,0 +1,475 @@
|
||||
const request = require('supertest');
|
||||
const app = require('../../src/app');
|
||||
const User = require('../../src/models/User');
|
||||
const dbConnection = require('../../src/database/connection');
|
||||
|
||||
describe('Authentication Integration Tests', () => {
|
||||
let testUser;
|
||||
let authToken;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Ensure database is clean before tests
|
||||
await dbConnection.query('DELETE FROM users WHERE email LIKE $1', ['%test%']);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// Clean up test data
|
||||
if (testUser) {
|
||||
await dbConnection.query('DELETE FROM users WHERE id = $1', [testUser.id]);
|
||||
}
|
||||
await dbConnection.end();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
testUser = null;
|
||||
authToken = null;
|
||||
});
|
||||
|
||||
describe('User Registration Flow', () => {
|
||||
it('should register a new user successfully', async () => {
|
||||
const userData = {
|
||||
email: 'integration-test@example.com',
|
||||
password: 'TestPassword123!'
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/auth/register')
|
||||
.send(userData)
|
||||
.expect(201);
|
||||
|
||||
expect(response.body).toHaveProperty('message');
|
||||
expect(response.body).toHaveProperty('user');
|
||||
expect(response.body.user.email).toBe(userData.email);
|
||||
expect(response.body.user.is_verified).toBe(false);
|
||||
|
||||
// Store test user for cleanup
|
||||
testUser = await User.findByEmail(userData.email);
|
||||
expect(testUser).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should reject registration with invalid email', async () => {
|
||||
const userData = {
|
||||
email: 'invalid-email',
|
||||
password: 'TestPassword123!'
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/auth/register')
|
||||
.send(userData)
|
||||
.expect(400);
|
||||
|
||||
expect(response.body).toHaveProperty('error');
|
||||
expect(response.body.code).toBe('REGISTRATION_FAILED');
|
||||
});
|
||||
|
||||
it('should reject registration with weak password', async () => {
|
||||
const userData = {
|
||||
email: 'weak-password-test@example.com',
|
||||
password: 'weak'
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/auth/register')
|
||||
.send(userData)
|
||||
.expect(400);
|
||||
|
||||
expect(response.body).toHaveProperty('error');
|
||||
expect(response.body.code).toBe('REGISTRATION_FAILED');
|
||||
});
|
||||
|
||||
it('should reject duplicate email registration', async () => {
|
||||
const userData = {
|
||||
email: 'duplicate-test@example.com',
|
||||
password: 'TestPassword123!'
|
||||
};
|
||||
|
||||
// First registration
|
||||
await request(app)
|
||||
.post('/api/auth/register')
|
||||
.send(userData)
|
||||
.expect(201);
|
||||
|
||||
// Store for cleanup
|
||||
testUser = await User.findByEmail(userData.email);
|
||||
|
||||
// Second registration with same email
|
||||
const response = await request(app)
|
||||
.post('/api/auth/register')
|
||||
.send(userData)
|
||||
.expect(400);
|
||||
|
||||
expect(response.body).toHaveProperty('error');
|
||||
expect(response.body.code).toBe('REGISTRATION_FAILED');
|
||||
});
|
||||
|
||||
it('should reject registration with missing fields', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/auth/register')
|
||||
.send({ email: 'test@example.com' }) // Missing password
|
||||
.expect(400);
|
||||
|
||||
expect(response.body.code).toBe('MISSING_FIELDS');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Email Verification Flow', () => {
|
||||
beforeEach(async () => {
|
||||
// Create unverified user for testing
|
||||
testUser = await User.create({
|
||||
email: 'verification-test@example.com',
|
||||
password: 'TestPassword123!'
|
||||
});
|
||||
});
|
||||
|
||||
it('should verify email with valid token', async () => {
|
||||
const response = await request(app)
|
||||
.get(`/api/auth/verify/${testUser.verification_token}`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toHaveProperty('message');
|
||||
expect(response.body.message).toContain('verified successfully');
|
||||
|
||||
// Check that user is now verified
|
||||
const updatedUser = await User.findById(testUser.id);
|
||||
expect(updatedUser.is_verified).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject verification with invalid token', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/auth/verify/invalid-token')
|
||||
.expect(400);
|
||||
|
||||
expect(response.body).toHaveProperty('error');
|
||||
expect(response.body.code).toBe('EMAIL_VERIFICATION_FAILED');
|
||||
});
|
||||
|
||||
it('should handle already verified email', async () => {
|
||||
// First verification
|
||||
await request(app)
|
||||
.get(`/api/auth/verify/${testUser.verification_token}`)
|
||||
.expect(200);
|
||||
|
||||
// Second verification attempt
|
||||
const response = await request(app)
|
||||
.get(`/api/auth/verify/${testUser.verification_token}`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.message).toContain('already verified');
|
||||
});
|
||||
});
|
||||
|
||||
describe('User Login Flow', () => {
|
||||
beforeEach(async () => {
|
||||
// Create verified user for testing
|
||||
testUser = await User.create({
|
||||
email: 'login-test@example.com',
|
||||
password: 'TestPassword123!'
|
||||
});
|
||||
await testUser.verifyEmail();
|
||||
});
|
||||
|
||||
it('should login with valid credentials', async () => {
|
||||
const loginData = {
|
||||
email: 'login-test@example.com',
|
||||
password: 'TestPassword123!'
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/auth/login')
|
||||
.send(loginData)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toHaveProperty('message');
|
||||
expect(response.body).toHaveProperty('user');
|
||||
expect(response.body.user.email).toBe(loginData.email);
|
||||
|
||||
// Check that auth cookie is set
|
||||
const cookies = response.headers['set-cookie'];
|
||||
expect(cookies).toBeDefined();
|
||||
expect(cookies.some(cookie => cookie.includes('authToken'))).toBe(true);
|
||||
|
||||
// Extract token for further tests
|
||||
const authCookie = cookies.find(cookie => cookie.includes('authToken'));
|
||||
authToken = authCookie.split('authToken=')[1].split(';')[0];
|
||||
});
|
||||
|
||||
it('should reject login with invalid credentials', async () => {
|
||||
const loginData = {
|
||||
email: 'login-test@example.com',
|
||||
password: 'WrongPassword123!'
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/auth/login')
|
||||
.send(loginData)
|
||||
.expect(401);
|
||||
|
||||
expect(response.body).toHaveProperty('error');
|
||||
expect(response.body.code).toBe('INVALID_CREDENTIALS');
|
||||
});
|
||||
|
||||
it('should reject login for unverified user', async () => {
|
||||
// Create unverified user
|
||||
const unverifiedUser = await User.create({
|
||||
email: 'unverified-login-test@example.com',
|
||||
password: 'TestPassword123!'
|
||||
});
|
||||
|
||||
const loginData = {
|
||||
email: 'unverified-login-test@example.com',
|
||||
password: 'TestPassword123!'
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/auth/login')
|
||||
.send(loginData)
|
||||
.expect(403);
|
||||
|
||||
expect(response.body).toHaveProperty('error');
|
||||
expect(response.body.code).toBe('EMAIL_NOT_VERIFIED');
|
||||
|
||||
// Cleanup
|
||||
await unverifiedUser.delete();
|
||||
});
|
||||
|
||||
it('should reject login with missing credentials', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/auth/login')
|
||||
.send({ email: 'test@example.com' }) // Missing password
|
||||
.expect(400);
|
||||
|
||||
expect(response.body.code).toBe('MISSING_CREDENTIALS');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Password Reset Flow', () => {
|
||||
beforeEach(async () => {
|
||||
testUser = await User.create({
|
||||
email: 'password-reset-test@example.com',
|
||||
password: 'TestPassword123!'
|
||||
});
|
||||
await testUser.verifyEmail();
|
||||
});
|
||||
|
||||
it('should request password reset for existing user', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/auth/forgot-password')
|
||||
.send({ email: 'password-reset-test@example.com' })
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toHaveProperty('message');
|
||||
expect(response.body.message).toContain('password reset link has been sent');
|
||||
|
||||
// Check that reset token was set
|
||||
const updatedUser = await User.findById(testUser.id);
|
||||
expect(updatedUser.reset_token).toBeTruthy();
|
||||
expect(updatedUser.reset_expires).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not reveal if email does not exist', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/auth/forgot-password')
|
||||
.send({ email: 'nonexistent@example.com' })
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.message).toContain('password reset link has been sent');
|
||||
});
|
||||
|
||||
it('should reset password with valid token', async () => {
|
||||
// First request password reset
|
||||
await request(app)
|
||||
.post('/api/auth/forgot-password')
|
||||
.send({ email: 'password-reset-test@example.com' });
|
||||
|
||||
// Get the reset token
|
||||
const userWithToken = await User.findById(testUser.id);
|
||||
const resetToken = userWithToken.reset_token;
|
||||
|
||||
// Reset password
|
||||
const newPassword = 'NewPassword123!';
|
||||
const response = await request(app)
|
||||
.post('/api/auth/reset-password')
|
||||
.send({
|
||||
token: resetToken,
|
||||
newPassword: newPassword
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toHaveProperty('message');
|
||||
expect(response.body.message).toContain('reset successfully');
|
||||
|
||||
// Verify user can login with new password
|
||||
const loginResponse = await request(app)
|
||||
.post('/api/auth/login')
|
||||
.send({
|
||||
email: 'password-reset-test@example.com',
|
||||
password: newPassword
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(loginResponse.body.user.email).toBe('password-reset-test@example.com');
|
||||
});
|
||||
|
||||
it('should reject password reset with invalid token', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/auth/reset-password')
|
||||
.send({
|
||||
token: 'invalid-token',
|
||||
newPassword: 'NewPassword123!'
|
||||
})
|
||||
.expect(400);
|
||||
|
||||
expect(response.body).toHaveProperty('error');
|
||||
expect(response.body.code).toBe('PASSWORD_RESET_FAILED');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Logout Flow', () => {
|
||||
beforeEach(async () => {
|
||||
// Create verified user and login
|
||||
testUser = await User.create({
|
||||
email: 'logout-test@example.com',
|
||||
password: 'TestPassword123!'
|
||||
});
|
||||
await testUser.verifyEmail();
|
||||
|
||||
const loginResponse = await request(app)
|
||||
.post('/api/auth/login')
|
||||
.send({
|
||||
email: 'logout-test@example.com',
|
||||
password: 'TestPassword123!'
|
||||
});
|
||||
|
||||
const cookies = loginResponse.headers['set-cookie'];
|
||||
const authCookie = cookies.find(cookie => cookie.includes('authToken'));
|
||||
authToken = authCookie.split('authToken=')[1].split(';')[0];
|
||||
});
|
||||
|
||||
it('should logout successfully', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/auth/logout')
|
||||
.set('Cookie', `authToken=${authToken}`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toHaveProperty('message');
|
||||
expect(response.body.message).toContain('Logged out successfully');
|
||||
|
||||
// Check that auth cookie is cleared
|
||||
const cookies = response.headers['set-cookie'];
|
||||
expect(cookies).toBeDefined();
|
||||
expect(cookies.some(cookie => cookie.includes('authToken=;'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should require authentication for logout', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/auth/logout')
|
||||
.expect(401);
|
||||
|
||||
expect(response.body).toHaveProperty('error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Token Refresh Flow', () => {
|
||||
beforeEach(async () => {
|
||||
// Create verified user and login
|
||||
testUser = await User.create({
|
||||
email: 'refresh-test@example.com',
|
||||
password: 'TestPassword123!'
|
||||
});
|
||||
await testUser.verifyEmail();
|
||||
|
||||
const loginResponse = await request(app)
|
||||
.post('/api/auth/login')
|
||||
.send({
|
||||
email: 'refresh-test@example.com',
|
||||
password: 'TestPassword123!'
|
||||
});
|
||||
|
||||
const cookies = loginResponse.headers['set-cookie'];
|
||||
const authCookie = cookies.find(cookie => cookie.includes('authToken'));
|
||||
authToken = authCookie.split('authToken=')[1].split(';')[0];
|
||||
});
|
||||
|
||||
it('should refresh token successfully', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/auth/refresh')
|
||||
.set('Cookie', `authToken=${authToken}`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toHaveProperty('message');
|
||||
expect(response.body).toHaveProperty('user');
|
||||
expect(response.body.message).toContain('Token refreshed successfully');
|
||||
|
||||
// Check that new auth cookie is set
|
||||
const cookies = response.headers['set-cookie'];
|
||||
expect(cookies).toBeDefined();
|
||||
expect(cookies.some(cookie => cookie.includes('authToken'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should require valid token for refresh', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/auth/refresh')
|
||||
.set('Cookie', 'authToken=invalid-token')
|
||||
.expect(401);
|
||||
|
||||
expect(response.body).toHaveProperty('error');
|
||||
expect(response.body.code).toBe('TOKEN_REFRESH_FAILED');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Rate Limiting', () => {
|
||||
it('should enforce rate limiting on login attempts', async () => {
|
||||
const loginData = {
|
||||
email: 'rate-limit-test@example.com',
|
||||
password: 'WrongPassword123!'
|
||||
};
|
||||
|
||||
// Make multiple failed login attempts
|
||||
const promises = [];
|
||||
for (let i = 0; i < 6; i++) {
|
||||
promises.push(
|
||||
request(app)
|
||||
.post('/api/auth/login')
|
||||
.send(loginData)
|
||||
);
|
||||
}
|
||||
|
||||
const responses = await Promise.all(promises);
|
||||
|
||||
// First 5 should be 401 (invalid credentials)
|
||||
// 6th should be 429 (rate limited)
|
||||
const rateLimitedResponse = responses.find(res => res.status === 429);
|
||||
expect(rateLimitedResponse).toBeDefined();
|
||||
expect(rateLimitedResponse.body.code).toBe('RATE_LIMIT_EXCEEDED');
|
||||
}, 10000); // Increase timeout for this test
|
||||
|
||||
it('should enforce rate limiting on registration attempts', async () => {
|
||||
const promises = [];
|
||||
for (let i = 0; i < 4; i++) {
|
||||
promises.push(
|
||||
request(app)
|
||||
.post('/api/auth/register')
|
||||
.send({
|
||||
email: `rate-limit-register-${i}@example.com`,
|
||||
password: 'TestPassword123!'
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const responses = await Promise.all(promises);
|
||||
|
||||
// 4th registration should be rate limited
|
||||
const rateLimitedResponse = responses.find(res => res.status === 429);
|
||||
expect(rateLimitedResponse).toBeDefined();
|
||||
expect(rateLimitedResponse.body.code).toBe('REGISTRATION_RATE_LIMIT');
|
||||
|
||||
// Cleanup created users
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const user = await User.findByEmail(`rate-limit-register-${i}@example.com`);
|
||||
if (user) {
|
||||
await user.delete();
|
||||
}
|
||||
}
|
||||
}, 10000);
|
||||
});
|
||||
});
|
||||
693
backend/tests/integration/bookmarks.test.js
Normal file
693
backend/tests/integration/bookmarks.test.js
Normal file
@ -0,0 +1,693 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user