Backend Complete: Interface Refactoring & Service Container Enhancements
Repository Interface Optimization: - Created IBaseRepository.ts and IPaginatedRepository.ts - Refactored all 7 repository interfaces to extend base interfaces - Eliminated ~200 lines of redundant code (70% reduction) - Improved type safety and maintainability Dependency Injection Improvements: - Added EmailService and GameTokenService to DIContainer - Updated CreateUserCommandHandler constructor for DI - Updated RequestPasswordResetCommandHandler constructor for DI - Enhanced testability and service consistency Environment Configuration: - Created comprehensive .env.example with 40+ variables - Organized into 12 logical sections (Database, Security, Email, etc.) - Added security guidelines and best practices - Documented all backend environment requirements Documentation: - Added comprehensive codebase review - Created refactoring summary report - Added frontend implementation guide Impact: Improved code quality, reduced maintenance overhead, enhanced developer experience
This commit is contained in:
@@ -0,0 +1,403 @@
|
||||
import { JWTService, TokenPayload } from '../../../src/Application/Services/JWTService';
|
||||
import { Request, Response } from 'express';
|
||||
import { UserState } from '../../../src/Domain/User/UserAggregate';
|
||||
|
||||
|
||||
describe('JWTService', () => {
|
||||
let jwtService: JWTService;
|
||||
let mockRequest: Partial<Request>;
|
||||
let mockResponse: Partial<Response>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jwtService = new JWTService();
|
||||
|
||||
// Set a test secret for consistent testing
|
||||
process.env.JWT_SECRET = 'test-secret-key-for-testing';
|
||||
process.env.JWT_EXPIRY = '3600'; // 1 hour
|
||||
|
||||
// Mock express Request and Response
|
||||
mockRequest = {
|
||||
cookies: {}
|
||||
};
|
||||
|
||||
mockResponse = {
|
||||
cookie: jest.fn()
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up environment
|
||||
delete process.env.JWT_SECRET;
|
||||
delete process.env.JWT_EXPIRY;
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create a valid JWT token and set cookie', () => {
|
||||
// Arrange
|
||||
const payload: TokenPayload = {
|
||||
userId: 'user-123',
|
||||
authLevel: 1 as const,
|
||||
userStatus: UserState.VERIFIED_REGULAR,
|
||||
orgId: 'org-456'
|
||||
};
|
||||
|
||||
// Act
|
||||
const token = jwtService.create(payload, mockResponse as Response);
|
||||
|
||||
// Assert
|
||||
expect(token).toBeDefined();
|
||||
expect(typeof token).toBe('string');
|
||||
expect(token.split('.')).toHaveLength(3); // JWT has 3 parts
|
||||
expect(mockResponse.cookie).toHaveBeenCalledWith(
|
||||
'auth_token',
|
||||
token,
|
||||
expect.objectContaining({
|
||||
httpOnly: true,
|
||||
sameSite: 'strict',
|
||||
maxAge: 86400000 // 24 hours in milliseconds
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should create different tokens for different payloads', () => {
|
||||
// Arrange
|
||||
const payload1: TokenPayload = {
|
||||
userId: 'user-1',
|
||||
authLevel: 0 as const,
|
||||
userStatus: UserState.VERIFIED_REGULAR,
|
||||
orgId: 'org-1'
|
||||
};
|
||||
|
||||
const payload2: TokenPayload = {
|
||||
userId: 'user-2',
|
||||
authLevel: 1 as const,
|
||||
userStatus: UserState.VERIFIED_PREMIUM,
|
||||
orgId: 'org-2'
|
||||
};
|
||||
|
||||
// Act
|
||||
const token1 = jwtService.create(payload1, mockResponse as Response);
|
||||
const token2 = jwtService.create(payload2, mockResponse as Response);
|
||||
|
||||
// Assert
|
||||
expect(token1).toBeDefined();
|
||||
expect(token2).toBeDefined();
|
||||
expect(token1).not.toBe(token2);
|
||||
});
|
||||
|
||||
it('should set secure cookie in production environment', () => {
|
||||
// Arrange
|
||||
process.env.NODE_ENV = 'production';
|
||||
const payload: TokenPayload = {
|
||||
userId: 'user-123',
|
||||
authLevel: 1 as const,
|
||||
userStatus: UserState.VERIFIED_REGULAR,
|
||||
orgId: 'org-456'
|
||||
};
|
||||
|
||||
// Act
|
||||
const token = jwtService.create(payload, mockResponse as Response);
|
||||
|
||||
// Assert
|
||||
expect(mockResponse.cookie).toHaveBeenCalledWith(
|
||||
'auth_token',
|
||||
token,
|
||||
expect.objectContaining({
|
||||
secure: true
|
||||
})
|
||||
);
|
||||
|
||||
// Clean up
|
||||
delete process.env.NODE_ENV;
|
||||
});
|
||||
});
|
||||
|
||||
describe('verify', () => {
|
||||
it('should verify a valid token from cookies', () => {
|
||||
// Arrange
|
||||
const payload: TokenPayload = {
|
||||
userId: 'user-123',
|
||||
authLevel: 1 as const,
|
||||
userStatus: UserState.VERIFIED_REGULAR,
|
||||
orgId: 'org-456'
|
||||
};
|
||||
|
||||
const token = jwtService.create(payload, mockResponse as Response);
|
||||
mockRequest.cookies = { auth_token: token };
|
||||
|
||||
// Act
|
||||
const result = jwtService.verify(mockRequest as Request);
|
||||
|
||||
// Assert
|
||||
expect(result).toBeDefined();
|
||||
expect(result!.userId).toBe('user-123');
|
||||
expect(result!.authLevel).toBe(1);
|
||||
expect(result!.orgId).toBe('org-456');
|
||||
});
|
||||
|
||||
it('should return null when no token is present in cookies', () => {
|
||||
// Arrange
|
||||
mockRequest.cookies = {};
|
||||
|
||||
// Act
|
||||
const result = jwtService.verify(mockRequest as Request);
|
||||
|
||||
// Assert
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for invalid token', () => {
|
||||
// Arrange
|
||||
mockRequest.cookies = { auth_token: 'invalid.jwt.token' };
|
||||
|
||||
// Act
|
||||
const result = jwtService.verify(mockRequest as Request);
|
||||
|
||||
// Assert
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for malformed token', () => {
|
||||
// Arrange
|
||||
mockRequest.cookies = { auth_token: 'not-a-jwt-token' };
|
||||
|
||||
// Act
|
||||
const result = jwtService.verify(mockRequest as Request);
|
||||
|
||||
// Assert
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('token creation with different payloads', () => {
|
||||
it('should create tokens with dynamic user data', () => {
|
||||
// Arrange
|
||||
const timestamp = Date.now();
|
||||
const testPayload: TokenPayload = {
|
||||
userId: `test-user-${timestamp}`,
|
||||
authLevel: 1 as const,
|
||||
userStatus: UserState.VERIFIED_REGULAR,
|
||||
orgId: `test-org-${timestamp}`
|
||||
};
|
||||
|
||||
// Act
|
||||
const token = jwtService.create(testPayload, mockResponse as Response);
|
||||
|
||||
// Assert
|
||||
expect(token).toBeDefined();
|
||||
expect(typeof token).toBe('string');
|
||||
expect(mockResponse.cookie).toHaveBeenCalled();
|
||||
|
||||
// Verify we can decode it back
|
||||
mockRequest.cookies = { auth_token: token };
|
||||
const verifiedPayload = jwtService.verify(mockRequest as Request);
|
||||
|
||||
expect(verifiedPayload).toBeDefined();
|
||||
expect(verifiedPayload!.userId).toBe(testPayload.userId);
|
||||
expect(verifiedPayload!.orgId).toBe(testPayload.orgId);
|
||||
expect(verifiedPayload!.authLevel).toBe(testPayload.authLevel);
|
||||
});
|
||||
|
||||
it('should create different tokens for different timestamps', async () => {
|
||||
// Arrange
|
||||
const timestamp1 = Date.now();
|
||||
const payload1: TokenPayload = {
|
||||
userId: `test-user-${timestamp1}`,
|
||||
authLevel: 0 as const,
|
||||
userStatus: UserState.VERIFIED_REGULAR,
|
||||
orgId: `test-org-${timestamp1}`
|
||||
};
|
||||
|
||||
// Add a small delay to ensure different timestamps
|
||||
await new Promise(resolve => setTimeout(resolve, 1));
|
||||
|
||||
const timestamp2 = Date.now();
|
||||
const payload2: TokenPayload = {
|
||||
userId: `test-user-${timestamp2}`,
|
||||
authLevel: 1 as const,
|
||||
userStatus: UserState.VERIFIED_PREMIUM,
|
||||
orgId: `test-org-${timestamp2}`
|
||||
};
|
||||
|
||||
// Act
|
||||
const token1 = jwtService.create(payload1, mockResponse as Response);
|
||||
const token2 = jwtService.create(payload2, mockResponse as Response);
|
||||
|
||||
// Assert
|
||||
expect(token1).not.toBe(token2);
|
||||
expect(payload1.userId).not.toBe(payload2.userId);
|
||||
expect(payload1.orgId).not.toBe(payload2.orgId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('integration scenarios', () => {
|
||||
it('should create and verify token in complete flow', () => {
|
||||
// Arrange
|
||||
const originalPayload: TokenPayload = {
|
||||
userId: 'integration-user',
|
||||
authLevel: 1 as const,
|
||||
userStatus: UserState.VERIFIED_REGULAR,
|
||||
orgId: 'integration-org'
|
||||
};
|
||||
|
||||
// Act - Complete flow
|
||||
const token = jwtService.create(originalPayload, mockResponse as Response);
|
||||
mockRequest.cookies = { auth_token: token };
|
||||
const verifiedPayload = jwtService.verify(mockRequest as Request);
|
||||
|
||||
// Assert
|
||||
expect(token).toBeDefined();
|
||||
expect(verifiedPayload).toBeDefined();
|
||||
expect(verifiedPayload!.userId).toBe('integration-user');
|
||||
expect(verifiedPayload!.authLevel).toBe(1);
|
||||
expect(verifiedPayload!.orgId).toBe('integration-org');
|
||||
});
|
||||
});
|
||||
|
||||
describe('JWT_EXPIRATION duration parsing', () => {
|
||||
it('should parse JWT_EXPIRATION in hours format', () => {
|
||||
// Arrange
|
||||
delete process.env.JWT_EXPIRY;
|
||||
process.env.JWT_EXPIRATION = '2h';
|
||||
|
||||
// Act
|
||||
const newJwtService = new JWTService();
|
||||
const payload: TokenPayload = {
|
||||
userId: 'user-123',
|
||||
authLevel: 1 as const,
|
||||
userStatus: UserState.VERIFIED_REGULAR,
|
||||
orgId: 'org-456'
|
||||
};
|
||||
|
||||
const token = newJwtService.create(payload, mockResponse as Response);
|
||||
|
||||
// Update mock request with the created token
|
||||
mockRequest.cookies = { auth_token: token };
|
||||
const verifiedPayload = newJwtService.verify(mockRequest as Request);
|
||||
|
||||
// Assert
|
||||
expect(token).toBeDefined();
|
||||
expect(verifiedPayload).toBeDefined();
|
||||
expect(verifiedPayload!.exp).toBeDefined();
|
||||
|
||||
// Token should expire in approximately 2 hours (7200 seconds)
|
||||
const expectedExp = Math.floor(Date.now() / 1000) + 7200;
|
||||
expect(verifiedPayload!.exp).toBeCloseTo(expectedExp, -1); // Within 10 seconds
|
||||
|
||||
// Cleanup
|
||||
delete process.env.JWT_EXPIRATION;
|
||||
});
|
||||
|
||||
it('should parse JWT_EXPIRATION in days format', () => {
|
||||
// Arrange
|
||||
delete process.env.JWT_EXPIRY;
|
||||
process.env.JWT_EXPIRATION = '7d';
|
||||
|
||||
// Act
|
||||
const newJwtService = new JWTService();
|
||||
const payload: TokenPayload = {
|
||||
userId: 'user-123',
|
||||
authLevel: 1 as const,
|
||||
userStatus: UserState.VERIFIED_REGULAR,
|
||||
orgId: 'org-456'
|
||||
};
|
||||
|
||||
const token = newJwtService.create(payload, mockResponse as Response);
|
||||
|
||||
// Update mock request with the created token
|
||||
mockRequest.cookies = { auth_token: token };
|
||||
const verifiedPayload = newJwtService.verify(mockRequest as Request);
|
||||
|
||||
// Assert
|
||||
expect(token).toBeDefined();
|
||||
expect(verifiedPayload).toBeDefined();
|
||||
|
||||
// Token should expire in approximately 7 days (604800 seconds)
|
||||
const expectedExp = Math.floor(Date.now() / 1000) + 604800;
|
||||
expect(verifiedPayload!.exp).toBeCloseTo(expectedExp, -1); // Within 10 seconds
|
||||
|
||||
// Cleanup
|
||||
delete process.env.JWT_EXPIRATION;
|
||||
});
|
||||
|
||||
it('should parse JWT_EXPIRATION in minutes format', () => {
|
||||
// Arrange
|
||||
delete process.env.JWT_EXPIRY;
|
||||
process.env.JWT_EXPIRATION = '30m';
|
||||
|
||||
// Act
|
||||
const newJwtService = new JWTService();
|
||||
const payload: TokenPayload = {
|
||||
userId: 'user-123',
|
||||
authLevel: 1 as const,
|
||||
userStatus: UserState.VERIFIED_REGULAR,
|
||||
orgId: 'org-456'
|
||||
};
|
||||
|
||||
const token = newJwtService.create(payload, mockResponse as Response);
|
||||
|
||||
// Update mock request with the created token
|
||||
mockRequest.cookies = { auth_token: token };
|
||||
const verifiedPayload = newJwtService.verify(mockRequest as Request);
|
||||
|
||||
// Assert
|
||||
expect(token).toBeDefined();
|
||||
expect(verifiedPayload).toBeDefined();
|
||||
|
||||
// Token should expire in approximately 30 minutes (1800 seconds)
|
||||
const expectedExp = Math.floor(Date.now() / 1000) + 1800;
|
||||
expect(verifiedPayload!.exp).toBeCloseTo(expectedExp, -1); // Within 10 seconds
|
||||
|
||||
// Cleanup
|
||||
delete process.env.JWT_EXPIRATION;
|
||||
});
|
||||
|
||||
it('should prioritize JWT_EXPIRY over JWT_EXPIRATION when both are set', () => {
|
||||
// Arrange
|
||||
process.env.JWT_EXPIRY = '1800'; // 30 minutes in seconds
|
||||
process.env.JWT_EXPIRATION = '1h'; // 1 hour
|
||||
|
||||
// Act
|
||||
const newJwtService = new JWTService();
|
||||
const payload: TokenPayload = {
|
||||
userId: 'user-123',
|
||||
authLevel: 1 as const,
|
||||
userStatus: UserState.VERIFIED_REGULAR,
|
||||
orgId: 'org-456'
|
||||
};
|
||||
|
||||
const token = newJwtService.create(payload, mockResponse as Response);
|
||||
|
||||
// Update mock request with the created token
|
||||
mockRequest.cookies = { auth_token: token };
|
||||
const verifiedPayload = newJwtService.verify(mockRequest as Request);
|
||||
|
||||
// Assert
|
||||
expect(token).toBeDefined();
|
||||
expect(verifiedPayload).toBeDefined();
|
||||
|
||||
// Should use JWT_EXPIRY (1800 seconds), not JWT_EXPIRATION (3600 seconds)
|
||||
const expectedExp = Math.floor(Date.now() / 1000) + 1800;
|
||||
expect(verifiedPayload!.exp).toBeCloseTo(expectedExp, -1); // Within 10 seconds
|
||||
|
||||
// Cleanup
|
||||
delete process.env.JWT_EXPIRY;
|
||||
delete process.env.JWT_EXPIRATION;
|
||||
});
|
||||
|
||||
it('should throw error for invalid JWT_EXPIRATION format', () => {
|
||||
// Arrange
|
||||
delete process.env.JWT_EXPIRY;
|
||||
process.env.JWT_EXPIRATION = 'invalid-format';
|
||||
|
||||
// Act & Assert
|
||||
expect(() => {
|
||||
new JWTService();
|
||||
}).toThrow('Invalid duration format: invalid-format. Use format like \'24h\', \'7d\', \'30m\'');
|
||||
|
||||
// Cleanup
|
||||
delete process.env.JWT_EXPIRATION;
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user