Files
cim_summary/backend/src/utils/__tests__/auth.test.ts
Jon 5a3c961bfc feat: Complete implementation of Tasks 1-5 - CIM Document Processor
Backend Infrastructure:
- Complete Express server setup with security middleware (helmet, CORS, rate limiting)
- Comprehensive error handling and logging with Winston
- Authentication system with JWT tokens and session management
- Database models and migrations for Users, Documents, Feedback, and Processing Jobs
- API routes structure for authentication and document management
- Integration tests for all server components (86 tests passing)

Frontend Infrastructure:
- React application with TypeScript and Vite
- Authentication UI with login form, protected routes, and logout functionality
- Authentication context with proper async state management
- Component tests with proper async handling (25 tests passing)
- Tailwind CSS styling and responsive design

Key Features:
- User registration, login, and authentication
- Protected routes with role-based access control
- Comprehensive error handling and user feedback
- Database schema with proper relationships
- Security middleware and validation
- Production-ready build configuration

Test Coverage: 111/111 tests passing
Tasks Completed: 1-5 (Project setup, Database, Auth system, Frontend UI, Backend infrastructure)

Ready for Task 6: File upload backend infrastructure
2025-07-27 13:29:26 -04:00

305 lines
9.3 KiB
TypeScript

import {
generateAccessToken,
generateRefreshToken,
generateAuthTokens,
verifyAccessToken,
verifyRefreshToken,
hashPassword,
comparePassword,
validatePassword,
extractTokenFromHeader,
decodeToken
} from '../auth';
// Config is mocked below, so we don't need to import it
// Mock the config
jest.mock('../../config/env', () => ({
config: {
jwt: {
secret: 'test-secret',
refreshSecret: 'test-refresh-secret',
expiresIn: '1h',
refreshExpiresIn: '7d'
},
security: {
bcryptRounds: 10
}
}
}));
// Mock logger
jest.mock('../logger', () => ({
info: jest.fn(),
error: jest.fn()
}));
describe('Auth Utilities', () => {
const mockPayload = {
userId: '123e4567-e89b-12d3-a456-426614174000',
email: 'test@example.com',
role: 'user'
};
describe('generateAccessToken', () => {
it('should generate a valid access token', () => {
const token = generateAccessToken(mockPayload);
expect(token).toBeDefined();
expect(typeof token).toBe('string');
expect(token.split('.')).toHaveLength(3); // JWT has 3 parts
});
it('should include the correct payload in the token', () => {
const token = generateAccessToken(mockPayload);
const decoded = decodeToken(token);
expect(decoded).toMatchObject({
userId: mockPayload.userId,
email: mockPayload.email,
role: mockPayload.role,
iss: 'cim-processor',
aud: 'cim-processor-users'
});
});
});
describe('generateRefreshToken', () => {
it('should generate a valid refresh token', () => {
const token = generateRefreshToken(mockPayload);
expect(token).toBeDefined();
expect(typeof token).toBe('string');
expect(token.split('.')).toHaveLength(3);
});
it('should use refresh secret for signing', () => {
const token = generateRefreshToken(mockPayload);
const decoded = decodeToken(token);
expect(decoded).toMatchObject({
userId: mockPayload.userId,
email: mockPayload.email,
role: mockPayload.role
});
});
});
describe('generateAuthTokens', () => {
it('should generate both access and refresh tokens', () => {
const tokens = generateAuthTokens(mockPayload);
expect(tokens).toHaveProperty('accessToken');
expect(tokens).toHaveProperty('refreshToken');
expect(tokens).toHaveProperty('expiresIn');
expect(typeof tokens.accessToken).toBe('string');
expect(typeof tokens.refreshToken).toBe('string');
expect(typeof tokens.expiresIn).toBe('number');
});
it('should calculate correct expiration time', () => {
const tokens = generateAuthTokens(mockPayload);
// 1h = 3600 seconds
expect(tokens.expiresIn).toBe(3600);
});
});
describe('verifyAccessToken', () => {
it('should verify a valid access token', () => {
const token = generateAccessToken(mockPayload);
const decoded = verifyAccessToken(token);
expect(decoded).toMatchObject({
userId: mockPayload.userId,
email: mockPayload.email,
role: mockPayload.role
});
});
it('should throw error for invalid token', () => {
expect(() => {
verifyAccessToken('invalid-token');
}).toThrow('Invalid or expired access token');
});
it('should throw error for token signed with wrong secret', () => {
const token = generateRefreshToken(mockPayload); // Uses refresh secret
expect(() => {
verifyAccessToken(token); // Expects access secret
}).toThrow('Invalid or expired access token');
});
});
describe('verifyRefreshToken', () => {
it('should verify a valid refresh token', () => {
const token = generateRefreshToken(mockPayload);
const decoded = verifyRefreshToken(token);
expect(decoded).toMatchObject({
userId: mockPayload.userId,
email: mockPayload.email,
role: mockPayload.role
});
});
it('should throw error for invalid refresh token', () => {
expect(() => {
verifyRefreshToken('invalid-token');
}).toThrow('Invalid or expired refresh token');
});
});
describe('hashPassword', () => {
it('should hash password correctly', async () => {
const password = 'TestPassword123!';
const hashedPassword = await hashPassword(password);
expect(hashedPassword).toBeDefined();
expect(typeof hashedPassword).toBe('string');
expect(hashedPassword).not.toBe(password);
expect(hashedPassword.startsWith('$2a$') || hashedPassword.startsWith('$2b$')).toBe(true); // bcrypt format
});
it('should generate different hashes for same password', async () => {
const password = 'TestPassword123!';
const hash1 = await hashPassword(password);
const hash2 = await hashPassword(password);
expect(hash1).not.toBe(hash2);
});
});
describe('comparePassword', () => {
it('should return true for correct password', async () => {
const password = 'TestPassword123!';
const hashedPassword = await hashPassword(password);
const isMatch = await comparePassword(password, hashedPassword);
expect(isMatch).toBe(true);
});
it('should return false for incorrect password', async () => {
const password = 'TestPassword123!';
const wrongPassword = 'WrongPassword123!';
const hashedPassword = await hashPassword(password);
const isMatch = await comparePassword(wrongPassword, hashedPassword);
expect(isMatch).toBe(false);
});
});
describe('validatePassword', () => {
it('should validate a strong password', () => {
const password = 'StrongPass123!';
const result = validatePassword(password);
expect(result.isValid).toBe(true);
expect(result.errors).toHaveLength(0);
});
it('should reject password that is too short', () => {
const password = 'Short1!';
const result = validatePassword(password);
expect(result.isValid).toBe(false);
expect(result.errors).toContain('Password must be at least 8 characters long');
});
it('should reject password without uppercase letter', () => {
const password = 'lowercase123!';
const result = validatePassword(password);
expect(result.isValid).toBe(false);
expect(result.errors).toContain('Password must contain at least one uppercase letter');
});
it('should reject password without lowercase letter', () => {
const password = 'UPPERCASE123!';
const result = validatePassword(password);
expect(result.isValid).toBe(false);
expect(result.errors).toContain('Password must contain at least one lowercase letter');
});
it('should reject password without number', () => {
const password = 'NoNumbers!';
const result = validatePassword(password);
expect(result.isValid).toBe(false);
expect(result.errors).toContain('Password must contain at least one number');
});
it('should reject password without special character', () => {
const password = 'NoSpecialChar123';
const result = validatePassword(password);
expect(result.isValid).toBe(false);
expect(result.errors).toContain('Password must contain at least one special character');
});
it('should return all validation errors for weak password', () => {
const password = 'weak';
const result = validatePassword(password);
expect(result.isValid).toBe(false);
expect(result.errors).toHaveLength(4); // 'weak' has lowercase, so only 4 errors
expect(result.errors).toContain('Password must be at least 8 characters long');
expect(result.errors).toContain('Password must contain at least one uppercase letter');
expect(result.errors).toContain('Password must contain at least one number');
expect(result.errors).toContain('Password must contain at least one special character');
});
});
describe('extractTokenFromHeader', () => {
it('should extract token from valid Authorization header', () => {
const header = 'Bearer valid-token-here';
const token = extractTokenFromHeader(header);
expect(token).toBe('valid-token-here');
});
it('should return null for missing header', () => {
const token = extractTokenFromHeader(undefined);
expect(token).toBeNull();
});
it('should return null for empty header', () => {
const token = extractTokenFromHeader('');
expect(token).toBeNull();
});
it('should return null for invalid format', () => {
const token = extractTokenFromHeader('InvalidFormat token');
expect(token).toBeNull();
});
it('should return null for missing token part', () => {
const token = extractTokenFromHeader('Bearer ');
expect(token).toBeNull();
});
});
describe('decodeToken', () => {
it('should decode a valid token', () => {
const token = generateAccessToken(mockPayload);
const decoded = decodeToken(token);
expect(decoded).toMatchObject({
userId: mockPayload.userId,
email: mockPayload.email,
role: mockPayload.role
});
});
it('should return null for invalid token', () => {
const decoded = decodeToken('invalid-token');
expect(decoded).toBeNull();
});
});
});