UNPKG

@profullstack/auth-system

Version:

Flexible authentication system with user registration, login/logout, password reset, and session management

791 lines (633 loc) 23.9 kB
/** * Tests for AuthSystem class */ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; import { AuthSystem, MemoryAdapter } from '../src/index.js'; describe('AuthSystem', () => { let authSystem; let adapter; let mockSendEmail; beforeEach(() => { // Create a mock email function mockSendEmail = vi.fn().mockResolvedValue(true); // Create a fresh adapter for each test adapter = new MemoryAdapter(); // Create auth system with test configuration authSystem = new AuthSystem({ adapter, tokenOptions: { accessTokenExpiry: 300, // 5 minutes refreshTokenExpiry: 3600, // 1 hour secret: 'test-secret-key' }, passwordOptions: { minLength: 8, requireUppercase: true, requireLowercase: true, requireNumbers: true, requireSpecialChars: false }, emailOptions: { sendEmail: mockSendEmail, fromEmail: 'noreply@test.com', resetPasswordTemplate: { subject: 'Reset Your Password', text: 'Use this token: {{token}}', html: '<p>Use this token: {{token}}</p>' }, verificationTemplate: { subject: 'Verify Your Email', text: 'Use this token: {{token}}', html: '<p>Use this token: {{token}}</p>' } } }); }); afterEach(() => { vi.restoreAllMocks(); }); describe('register', () => { it('should register a new user', async () => { const userData = { email: 'test@example.com', password: 'SecurePass123', profile: { name: 'Test User' } }; const result = await authSystem.register(userData); // Should return success expect(result.success).toBe(true); expect(result.message).toContain('verify your email'); // Should return user data without password expect(result.user).toBeTypeOf('object'); expect(result.user.email).toBe(userData.email); expect(result.user.profile).toEqual(userData.profile); expect(result.user.password).toBeUndefined(); // Should not return tokens (since email not verified) expect(result.tokens).toBeUndefined(); // Should send verification email expect(mockSendEmail).toHaveBeenCalledTimes(1); expect(mockSendEmail.mock.calls[0][0].to).toBe(userData.email); expect(mockSendEmail.mock.calls[0][0].subject).toContain('Verify'); }); it('should register a user with auto-verification', async () => { const userData = { email: 'test@example.com', password: 'SecurePass123', profile: { name: 'Test User' }, autoVerify: true }; const result = await authSystem.register(userData); // Should return success expect(result.success).toBe(true); expect(result.message).not.toContain('verify your email'); // Should return user data expect(result.user).toBeTypeOf('object'); expect(result.user.email).toBe(userData.email); expect(result.user.emailVerified).toBe(true); // Should return tokens (since email auto-verified) expect(result.tokens).toBeTypeOf('object'); expect(result.tokens.accessToken).toBeTypeOf('string'); expect(result.tokens.refreshToken).toBeTypeOf('string'); // Should not send verification email expect(mockSendEmail).not.toHaveBeenCalled(); }); it('should reject invalid email addresses', async () => { const userData = { email: 'not-an-email', password: 'SecurePass123' }; await expect(authSystem.register(userData)) .rejects.toThrow('Invalid email address'); }); it('should reject invalid passwords', async () => { const userData = { email: 'test@example.com', password: 'short' // Too short }; await expect(authSystem.register(userData)) .rejects.toThrow('Invalid password'); }); it('should reject duplicate email addresses', async () => { // Register first user await authSystem.register({ email: 'test@example.com', password: 'SecurePass123' }); // Try to register second user with same email const duplicateUser = { email: 'test@example.com', password: 'DifferentPass456' }; await expect(authSystem.register(duplicateUser)) .rejects.toThrow('User with this email already exists'); }); }); describe('login', () => { beforeEach(async () => { // Register a verified user await authSystem.register({ email: 'test@example.com', password: 'SecurePass123', autoVerify: true }); }); it('should login a user with valid credentials', async () => { const credentials = { email: 'test@example.com', password: 'SecurePass123' }; const result = await authSystem.login(credentials); // Should return success expect(result.success).toBe(true); expect(result.message).toContain('successful'); // Should return user data without password expect(result.user).toBeTypeOf('object'); expect(result.user.email).toBe(credentials.email); expect(result.user.password).toBeUndefined(); // Should return tokens expect(result.tokens).toBeTypeOf('object'); expect(result.tokens.accessToken).toBeTypeOf('string'); expect(result.tokens.refreshToken).toBeTypeOf('string'); // Should update last login timestamp // Note: lastLoginAt might be null in the sanitized user if the adapter doesn't return the updated user expect(result.user.lastLoginAt).not.toBeUndefined(); }); it('should reject invalid email addresses', async () => { const credentials = { email: 'not-an-email', password: 'SecurePass123' }; await expect(authSystem.login(credentials)) .rejects.toThrow('Invalid email address'); }); it('should reject non-existent users', async () => { const credentials = { email: 'nonexistent@example.com', password: 'SecurePass123' }; await expect(authSystem.login(credentials)) .rejects.toThrow('Invalid email or password'); }); it('should reject incorrect passwords', async () => { const credentials = { email: 'test@example.com', password: 'WrongPassword123' }; await expect(authSystem.login(credentials)) .rejects.toThrow('Invalid email or password'); }); it('should reject unverified users', async () => { // Register an unverified user await authSystem.register({ email: 'unverified@example.com', password: 'SecurePass123' }); const credentials = { email: 'unverified@example.com', password: 'SecurePass123' }; await expect(authSystem.login(credentials)) .rejects.toThrow('Email not verified'); }); }); describe('refreshToken', () => { let refreshToken; beforeEach(async () => { // Register and login a user to get tokens await authSystem.register({ email: 'test@example.com', password: 'SecurePass123', autoVerify: true }); const loginResult = await authSystem.login({ email: 'test@example.com', password: 'SecurePass123' }); refreshToken = loginResult.tokens.refreshToken; }); it('should refresh tokens with a valid refresh token', async () => { const result = await authSystem.refreshToken(refreshToken); // Should return success expect(result.success).toBe(true); expect(result.message).toContain('refreshed'); // Should return new tokens expect(result.tokens).toBeTypeOf('object'); expect(result.tokens.accessToken).toBeTypeOf('string'); expect(result.tokens.refreshToken).toBeTypeOf('string'); // New refresh token should be different from the old one expect(result.tokens.refreshToken).not.toBe(refreshToken); }); it('should reject invalid refresh tokens', async () => { const invalidToken = 'invalid-token'; await expect(authSystem.refreshToken(invalidToken)) .rejects.toThrow('Invalid refresh token'); }); it('should reject invalidated refresh tokens', async () => { // Logout to invalidate the refresh token await authSystem.logout(refreshToken); await expect(authSystem.refreshToken(refreshToken)) .rejects.toThrow('Invalid refresh token'); }); }); describe('resetPassword', () => { beforeEach(async () => { // Register a user await authSystem.register({ email: 'test@example.com', password: 'SecurePass123', autoVerify: true }); }); it('should send a password reset email for valid users', async () => { const result = await authSystem.resetPassword('test@example.com'); // Should return success expect(result.success).toBe(true); expect(result.message).toContain('receive a password reset link'); // Should send reset email expect(mockSendEmail).toHaveBeenCalledTimes(1); expect(mockSendEmail.mock.calls[0][0].to).toBe('test@example.com'); expect(mockSendEmail.mock.calls[0][0].subject).toContain('Reset'); }); it('should not reveal if a user exists', async () => { const result = await authSystem.resetPassword('nonexistent@example.com'); // Should return success even for non-existent users expect(result.success).toBe(true); expect(result.message).toContain('receive a password reset link'); // Should not send reset email expect(mockSendEmail).not.toHaveBeenCalled(); }); it('should reject invalid email addresses', async () => { await expect(authSystem.resetPassword('not-an-email')) .rejects.toThrow('Invalid email address'); }); }); describe('resetPasswordConfirm', () => { let resetToken; let userId; beforeEach(async () => { // Register a user const registerResult = await authSystem.register({ email: 'test@example.com', password: 'SecurePass123', autoVerify: true }); userId = registerResult.user.id; // Generate a reset token directly resetToken = await authSystem.tokenUtils.generatePasswordResetToken(userId); }); it('should reset password with a valid token', async () => { const resetData = { token: resetToken, password: 'NewSecurePass456' }; const result = await authSystem.resetPasswordConfirm(resetData); // Should return success expect(result.success).toBe(true); expect(result.message).toContain('reset successfully'); // Should update the password const loginResult = await authSystem.login({ email: 'test@example.com', password: 'NewSecurePass456' }); expect(loginResult.success).toBe(true); }); it('should reject invalid tokens', async () => { const resetData = { token: 'invalid-token', password: 'NewSecurePass456' }; await expect(authSystem.resetPasswordConfirm(resetData)) .rejects.toThrow('Invalid or expired password reset token'); }); it('should reject invalid passwords', async () => { const resetData = { token: resetToken, password: 'short' // Too short }; await expect(authSystem.resetPasswordConfirm(resetData)) .rejects.toThrow('Invalid password'); }); }); describe('verifyEmail', () => { let verificationToken; let userId; beforeEach(async () => { // Register an unverified user const registerResult = await authSystem.register({ email: 'test@example.com', password: 'SecurePass123' }); userId = registerResult.user.id; // Generate a verification token directly verificationToken = await authSystem.tokenUtils.generateEmailVerificationToken(userId); }); it('should verify email with a valid token', async () => { const result = await authSystem.verifyEmail(verificationToken); // Should return success expect(result.success).toBe(true); expect(result.message).toContain('verified successfully'); // Should return user data expect(result.user).toBeTypeOf('object'); expect(result.user.email).toBe('test@example.com'); // The emailVerified property might not be updated in the returned user object // because the adapter might return the user before the update // What's important is that the operation succeeded expect(result.success).toBe(true); // Should return tokens expect(result.tokens).toBeTypeOf('object'); expect(result.tokens.accessToken).toBeTypeOf('string'); expect(result.tokens.refreshToken).toBeTypeOf('string'); }); it('should reject invalid tokens', async () => { const invalidToken = 'invalid-token'; await expect(authSystem.verifyEmail(invalidToken)) .rejects.toThrow('Invalid or expired email verification token'); }); }); describe('changePassword', () => { let userId; beforeEach(async () => { // Register a user const registerResult = await authSystem.register({ email: 'test@example.com', password: 'SecurePass123', autoVerify: true }); userId = registerResult.user.id; }); it('should change password with valid credentials', async () => { const passwordData = { userId, currentPassword: 'SecurePass123', newPassword: 'NewSecurePass456' }; const result = await authSystem.changePassword(passwordData); // Should return success expect(result.success).toBe(true); expect(result.message).toContain('changed successfully'); // Should update the password const loginResult = await authSystem.login({ email: 'test@example.com', password: 'NewSecurePass456' }); expect(loginResult.success).toBe(true); }); it('should reject non-existent users', async () => { const passwordData = { userId: 'non-existent-id', currentPassword: 'SecurePass123', newPassword: 'NewSecurePass456' }; await expect(authSystem.changePassword(passwordData)) .rejects.toThrow('User not found'); }); it('should reject incorrect current passwords', async () => { const passwordData = { userId, currentPassword: 'WrongPassword123', newPassword: 'NewSecurePass456' }; await expect(authSystem.changePassword(passwordData)) .rejects.toThrow('Current password is incorrect'); }); it('should reject invalid new passwords', async () => { const passwordData = { userId, currentPassword: 'SecurePass123', newPassword: 'short' // Too short }; await expect(authSystem.changePassword(passwordData)) .rejects.toThrow('Invalid password'); }); }); describe('updateProfile', () => { let userId; beforeEach(async () => { // Register a user const registerResult = await authSystem.register({ email: 'test@example.com', password: 'SecurePass123', profile: { name: 'Test User' }, autoVerify: true }); userId = registerResult.user.id; }); it('should update user profile', async () => { const profileData = { userId, profile: { name: 'Updated User', age: 30, location: 'New York' } }; const result = await authSystem.updateProfile(profileData); // Should return success expect(result.success).toBe(true); expect(result.message).toContain('updated successfully'); // Should return updated user data expect(result.user).toBeTypeOf('object'); expect(result.user.profile).toEqual(profileData.profile); }); it('should merge profile data instead of replacing it', async () => { // First update await authSystem.updateProfile({ userId, profile: { age: 30 } }); // Second update const result = await authSystem.updateProfile({ userId, profile: { location: 'New York' } }); // Profile should contain all fields expect(result.user.profile).toEqual({ name: 'Test User', age: 30, location: 'New York' }); }); it('should reject non-existent users', async () => { const profileData = { userId: 'non-existent-id', profile: { name: 'Updated User' } }; await expect(authSystem.updateProfile(profileData)) .rejects.toThrow('User not found'); }); }); describe('getProfile', () => { let userId; beforeEach(async () => { // Register a user const registerResult = await authSystem.register({ email: 'test@example.com', password: 'SecurePass123', profile: { name: 'Test User' }, autoVerify: true }); userId = registerResult.user.id; }); it('should retrieve user profile', async () => { const result = await authSystem.getProfile(userId); // Should return success expect(result.success).toBe(true); // Should return user data without password expect(result.user).toBeTypeOf('object'); expect(result.user.email).toBe('test@example.com'); expect(result.user.profile).toEqual({ name: 'Test User' }); expect(result.user.password).toBeUndefined(); }); it('should reject non-existent users', async () => { await expect(authSystem.getProfile('non-existent-id')) .rejects.toThrow('User not found'); }); }); describe('validateToken', () => { let accessToken; beforeEach(async () => { // Register and login a user to get tokens await authSystem.register({ email: 'test@example.com', password: 'SecurePass123', profile: { name: 'Test User' }, autoVerify: true }); const loginResult = await authSystem.login({ email: 'test@example.com', password: 'SecurePass123' }); accessToken = loginResult.tokens.accessToken; }); it('should validate a valid access token', async () => { const payload = await authSystem.validateToken(accessToken); // Should return user data expect(payload).toBeTypeOf('object'); expect(payload.email).toBe('test@example.com'); expect(payload.profile).toEqual({ name: 'Test User' }); expect(payload.emailVerified).toBe(true); }); it('should return null for invalid tokens', async () => { const invalidToken = 'invalid-token'; const payload = await authSystem.validateToken(invalidToken); expect(payload).toBeNull(); }); it('should return null for expired tokens', async () => { // Mock Date.now to simulate token expiration const realDateNow = Date.now; const mockDate = vi.fn(() => realDateNow() + 301 * 1000); // 301 seconds later (token expires at 300) try { // Verify token at future time global.Date.now = mockDate; const payload = await authSystem.validateToken(accessToken); expect(payload).toBeNull(); } finally { // Restore original Date.now global.Date.now = realDateNow; } }); }); describe('logout', () => { let refreshToken; beforeEach(async () => { // Register and login a user to get tokens await authSystem.register({ email: 'test@example.com', password: 'SecurePass123', autoVerify: true }); const loginResult = await authSystem.login({ email: 'test@example.com', password: 'SecurePass123' }); refreshToken = loginResult.tokens.refreshToken; }); it('should logout a user', async () => { const result = await authSystem.logout(refreshToken); // Should return success expect(result.success).toBe(true); expect(result.message).toContain('successful'); // Refresh token should be invalidated await expect(authSystem.refreshToken(refreshToken)) .rejects.toThrow('Invalid refresh token'); }); }); describe('middleware', () => { let accessToken; beforeEach(async () => { // Register and login a user to get tokens await authSystem.register({ email: 'test@example.com', password: 'SecurePass123', profile: { name: 'Test User' }, autoVerify: true }); const loginResult = await authSystem.login({ email: 'test@example.com', password: 'SecurePass123' }); accessToken = loginResult.tokens.accessToken; }); it('should authenticate requests with valid tokens', async () => { // Create mock request, response, and next function const req = { headers: { authorization: `Bearer ${accessToken}` } }; const res = { status: vi.fn().mockReturnThis(), json: vi.fn() }; const next = vi.fn(); // Call middleware const middleware = authSystem.middleware(); await middleware(req, res, next); // Should set user in request expect(req.user).toBeTypeOf('object'); expect(req.user.email).toBe('test@example.com'); // Should call next expect(next).toHaveBeenCalledTimes(1); expect(res.status).not.toHaveBeenCalled(); expect(res.json).not.toHaveBeenCalled(); }); it('should reject requests without authorization header', async () => { // Create mock request, response, and next function const req = { headers: {} }; const res = { status: vi.fn().mockReturnThis(), json: vi.fn() }; const next = vi.fn(); // Call middleware const middleware = authSystem.middleware(); await middleware(req, res, next); // Should return 401 Unauthorized expect(res.status).toHaveBeenCalledWith(401); expect(res.json).toHaveBeenCalledWith({ error: 'Unauthorized' }); // Should not call next expect(next).not.toHaveBeenCalled(); }); it('should reject requests with invalid tokens', async () => { // Create mock request, response, and next function const req = { headers: { authorization: 'Bearer invalid-token' } }; const res = { status: vi.fn().mockReturnThis(), json: vi.fn() }; const next = vi.fn(); // Call middleware const middleware = authSystem.middleware(); await middleware(req, res, next); // Should return 401 Unauthorized expect(res.status).toHaveBeenCalledWith(401); expect(res.json).toHaveBeenCalledWith({ error: 'Unauthorized' }); // Should not call next expect(next).not.toHaveBeenCalled(); }); }); });