475 lines
17 KiB
JavaScript
475 lines
17 KiB
JavaScript
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);
|
|
});
|
|
}); |