negyedik gyakorlat + megoldasok
This commit is contained in:
@@ -0,0 +1,311 @@
|
||||
const Container = require('../../../src/application/services/Container');
|
||||
|
||||
describe('Container - Dependency Injection', () => {
|
||||
let container;
|
||||
|
||||
beforeEach(() => {
|
||||
container = new Container();
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
test('should initialize with empty Maps', () => {
|
||||
expect(container.services).toBeInstanceOf(Map);
|
||||
expect(container.factories).toBeInstanceOf(Map);
|
||||
expect(container.lifetimes).toBeInstanceOf(Map);
|
||||
expect(container.services.size).toBe(0);
|
||||
expect(container.factories.size).toBe(0);
|
||||
expect(container.lifetimes.size).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('register', () => {
|
||||
test('should register a singleton service', () => {
|
||||
const factory = jest.fn(() => ({ id: 1, name: 'Test Service' }));
|
||||
|
||||
container.register('TestService', factory, 'singleton');
|
||||
|
||||
expect(container.factories.has('TestService')).toBe(true);
|
||||
expect(container.lifetimes.get('TestService')).toBe('singleton');
|
||||
expect(factory).toHaveBeenCalled(); // Singleton is created immediately
|
||||
expect(container.services.has('TestService')).toBe(true);
|
||||
});
|
||||
|
||||
test('should register a transient service', () => {
|
||||
const factory = jest.fn(() => ({ id: 2, name: 'Transient Service' }));
|
||||
|
||||
container.register('TransientService', factory, 'transient');
|
||||
|
||||
expect(container.factories.has('TransientService')).toBe(true);
|
||||
expect(container.lifetimes.get('TransientService')).toBe('transient');
|
||||
expect(factory).not.toHaveBeenCalled(); // Transient is NOT created immediately
|
||||
expect(container.services.has('TransientService')).toBe(false);
|
||||
});
|
||||
|
||||
test('should register a scoped service', () => {
|
||||
const factory = jest.fn(() => ({ id: 3, name: 'Scoped Service' }));
|
||||
|
||||
container.register('ScopedService', factory, 'scoped');
|
||||
|
||||
expect(container.factories.has('ScopedService')).toBe(true);
|
||||
expect(container.lifetimes.get('ScopedService')).toBe('scoped');
|
||||
expect(factory).not.toHaveBeenCalled(); // Scoped is NOT created immediately
|
||||
});
|
||||
|
||||
test('should default to singleton if lifetime not specified', () => {
|
||||
const factory = () => ({ id: 4 });
|
||||
|
||||
container.register('DefaultService', factory);
|
||||
|
||||
expect(container.lifetimes.get('DefaultService')).toBe('singleton');
|
||||
expect(container.services.has('DefaultService')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolve - singleton', () => {
|
||||
test('should return the same instance for singleton', () => {
|
||||
container.register('SingletonService', () => ({ id: Math.random() }), 'singleton');
|
||||
|
||||
const instance1 = container.resolve('SingletonService');
|
||||
const instance2 = container.resolve('SingletonService');
|
||||
|
||||
expect(instance1).toBe(instance2);
|
||||
expect(instance1.id).toBe(instance2.id);
|
||||
});
|
||||
|
||||
test('should return the pre-created singleton instance', () => {
|
||||
const mockInstance = { id: 123, name: 'Mock' };
|
||||
container.register('PreCreatedService', () => mockInstance, 'singleton');
|
||||
|
||||
const resolved = container.resolve('PreCreatedService');
|
||||
|
||||
expect(resolved).toBe(mockInstance);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolve - transient', () => {
|
||||
test('should return different instances for transient', () => {
|
||||
container.register('TransientService', () => ({ id: Math.random() }), 'transient');
|
||||
|
||||
const instance1 = container.resolve('TransientService');
|
||||
const instance2 = container.resolve('TransientService');
|
||||
|
||||
expect(instance1).not.toBe(instance2);
|
||||
expect(instance1.id).not.toBe(instance2.id);
|
||||
});
|
||||
|
||||
test('should call factory every time for transient', () => {
|
||||
const factory = jest.fn(() => ({ id: Math.random() }));
|
||||
container.register('TransientService', factory, 'transient');
|
||||
|
||||
container.resolve('TransientService');
|
||||
container.resolve('TransientService');
|
||||
container.resolve('TransientService');
|
||||
|
||||
expect(factory).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolve - scoped', () => {
|
||||
test('should return same instance within the same scope', () => {
|
||||
container.register('ScopedService', () => ({ id: Math.random() }), 'scoped');
|
||||
|
||||
const scope = container.createScope();
|
||||
const instance1 = scope.resolve('ScopedService');
|
||||
const instance2 = scope.resolve('ScopedService');
|
||||
|
||||
expect(instance1).toBe(instance2);
|
||||
expect(instance1.id).toBe(instance2.id);
|
||||
});
|
||||
|
||||
test('should return different instances for different scopes', () => {
|
||||
container.register('ScopedService', () => ({ id: Math.random() }), 'scoped');
|
||||
|
||||
const scope1 = container.createScope();
|
||||
const scope2 = container.createScope();
|
||||
|
||||
const instance1 = scope1.resolve('ScopedService');
|
||||
const instance2 = scope2.resolve('ScopedService');
|
||||
|
||||
expect(instance1).not.toBe(instance2);
|
||||
expect(instance1.id).not.toBe(instance2.id);
|
||||
});
|
||||
|
||||
test('should resolve scoped service with scope parameter', () => {
|
||||
const scopeMap = new Map();
|
||||
container.register('ScopedService', () => ({ id: Math.random() }), 'scoped');
|
||||
|
||||
const instance1 = container.resolve('ScopedService', scopeMap);
|
||||
const instance2 = container.resolve('ScopedService', scopeMap);
|
||||
|
||||
expect(instance1).toBe(instance2);
|
||||
expect(scopeMap.has('ScopedService')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolve - error handling', () => {
|
||||
test('should throw error for unregistered service', () => {
|
||||
expect(() => container.resolve('NonExistentService')).toThrow(
|
||||
"Service 'NonExistentService' is not registered"
|
||||
);
|
||||
});
|
||||
|
||||
test('should provide clear error message', () => {
|
||||
try {
|
||||
container.resolve('MissingService');
|
||||
fail('Should have thrown error');
|
||||
} catch (error) {
|
||||
expect(error.message).toContain('MissingService');
|
||||
expect(error.message).toContain('not registered');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('createScope', () => {
|
||||
test('should create a scope with resolve method', () => {
|
||||
const scope = container.createScope();
|
||||
|
||||
expect(scope).toHaveProperty('resolve');
|
||||
expect(typeof scope.resolve).toBe('function');
|
||||
});
|
||||
|
||||
test('should create independent scopes', () => {
|
||||
container.register('ScopedService', () => ({ id: Math.random() }), 'scoped');
|
||||
|
||||
const scope1 = container.createScope();
|
||||
const scope2 = container.createScope();
|
||||
|
||||
const instance1 = scope1.resolve('ScopedService');
|
||||
const instance2 = scope2.resolve('ScopedService');
|
||||
|
||||
expect(instance1.id).not.toBe(instance2.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('real-world scenarios', () => {
|
||||
test('should handle database connection as singleton', () => {
|
||||
class DatabaseConnection {
|
||||
constructor() {
|
||||
this.id = Math.random();
|
||||
this.connected = true;
|
||||
}
|
||||
}
|
||||
|
||||
container.register('Database', () => new DatabaseConnection(), 'singleton');
|
||||
|
||||
const db1 = container.resolve('Database');
|
||||
const db2 = container.resolve('Database');
|
||||
|
||||
expect(db1).toBe(db2);
|
||||
expect(db1.id).toBe(db2.id);
|
||||
expect(db1.connected).toBe(true);
|
||||
});
|
||||
|
||||
test('should handle logger as transient', () => {
|
||||
class Logger {
|
||||
constructor() {
|
||||
this.id = Math.random();
|
||||
}
|
||||
log(msg) {
|
||||
return `[${this.id}] ${msg}`;
|
||||
}
|
||||
}
|
||||
|
||||
container.register('Logger', () => new Logger(), 'transient');
|
||||
|
||||
const logger1 = container.resolve('Logger');
|
||||
const logger2 = container.resolve('Logger');
|
||||
|
||||
expect(logger1).not.toBe(logger2);
|
||||
expect(logger1.id).not.toBe(logger2.id);
|
||||
});
|
||||
|
||||
test('should handle request context as scoped', () => {
|
||||
class RequestContext {
|
||||
constructor() {
|
||||
this.requestId = Math.random();
|
||||
this.user = null;
|
||||
this.timestamp = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
container.register('RequestContext', () => new RequestContext(), 'scoped');
|
||||
|
||||
// Request 1
|
||||
const request1Scope = container.createScope();
|
||||
const ctx1a = request1Scope.resolve('RequestContext');
|
||||
const ctx1b = request1Scope.resolve('RequestContext');
|
||||
expect(ctx1a).toBe(ctx1b);
|
||||
|
||||
// Request 2
|
||||
const request2Scope = container.createScope();
|
||||
const ctx2 = request2Scope.resolve('RequestContext');
|
||||
expect(ctx1a).not.toBe(ctx2);
|
||||
expect(ctx1a.requestId).not.toBe(ctx2.requestId);
|
||||
});
|
||||
|
||||
test('should handle dependency chain', () => {
|
||||
class Repository {
|
||||
constructor(db) {
|
||||
this.db = db;
|
||||
}
|
||||
}
|
||||
|
||||
class Service {
|
||||
constructor(repo) {
|
||||
this.repo = repo;
|
||||
}
|
||||
}
|
||||
|
||||
const mockDb = { id: 1, connected: true };
|
||||
container.register('Database', () => mockDb, 'singleton');
|
||||
container.register('Repository', () => {
|
||||
return new Repository(container.resolve('Database'));
|
||||
}, 'singleton');
|
||||
container.register('Service', () => {
|
||||
return new Service(container.resolve('Repository'));
|
||||
}, 'singleton');
|
||||
|
||||
const service = container.resolve('Service');
|
||||
|
||||
expect(service.repo).toBeDefined();
|
||||
expect(service.repo.db).toBe(mockDb);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mixed lifecycle scenarios', () => {
|
||||
test('should handle mixed singleton and transient', () => {
|
||||
container.register('Config', () => ({ port: 3000 }), 'singleton');
|
||||
container.register('Handler', () => ({
|
||||
id: Math.random(),
|
||||
config: container.resolve('Config')
|
||||
}), 'transient');
|
||||
|
||||
const handler1 = container.resolve('Handler');
|
||||
const handler2 = container.resolve('Handler');
|
||||
|
||||
// Different handlers
|
||||
expect(handler1).not.toBe(handler2);
|
||||
expect(handler1.id).not.toBe(handler2.id);
|
||||
|
||||
// But same config
|
||||
expect(handler1.config).toBe(handler2.config);
|
||||
});
|
||||
|
||||
test('should handle mixed singleton and scoped', () => {
|
||||
container.register('Database', () => ({ id: 'db' }), 'singleton');
|
||||
container.register('RequestData', () => ({
|
||||
id: Math.random()
|
||||
}), 'scoped');
|
||||
|
||||
const scope1 = container.createScope();
|
||||
const scope2 = container.createScope();
|
||||
|
||||
const data1a = scope1.resolve('RequestData');
|
||||
const data1b = scope1.resolve('RequestData');
|
||||
const data2 = scope2.resolve('RequestData');
|
||||
|
||||
expect(data1a).toBe(data1b); // Same within scope
|
||||
expect(data1a).not.toBe(data2); // Different across scopes
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,117 @@
|
||||
const LoginUserCommandHandler = require('../../../src/application/auth/commands/LoginUserCommandHandler');
|
||||
const LoginUserCommand = require('../../../src/application/auth/commands/LoginUserCommand');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const JwtService = require('../../../src/application/services/JwtService');
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('bcryptjs');
|
||||
jest.mock('../../../src/application/services/JwtService');
|
||||
|
||||
describe('LoginUserCommandHandler', () => {
|
||||
let handler;
|
||||
let mockPrisma;
|
||||
let mockJwtService;
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock JwtService instance
|
||||
mockJwtService = {
|
||||
generateToken: jest.fn()
|
||||
};
|
||||
JwtService.mockImplementation(() => mockJwtService);
|
||||
|
||||
// Mock Prisma
|
||||
mockPrisma = {
|
||||
user: {
|
||||
findUnique: jest.fn()
|
||||
}
|
||||
};
|
||||
|
||||
handler = new LoginUserCommandHandler(mockPrisma);
|
||||
|
||||
// Reset all mocks
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('handle - success cases', () => {
|
||||
it('should login user successfully with valid credentials', async () => {
|
||||
// Arrange
|
||||
const command = new LoginUserCommand('john@example.com', 'password123');
|
||||
|
||||
mockPrisma.user.findUnique.mockResolvedValue({
|
||||
id: 1,
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
password: 'hashed_password',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
});
|
||||
bcrypt.compare.mockResolvedValue(true); // Password is valid
|
||||
mockJwtService.generateToken.mockReturnValue('mock_jwt_token');
|
||||
|
||||
// Act
|
||||
const result = await handler.handle(command);
|
||||
|
||||
// Assert
|
||||
expect(mockPrisma.user.findUnique).toHaveBeenCalledWith({
|
||||
where: { email: 'john@example.com' }
|
||||
});
|
||||
expect(bcrypt.compare).toHaveBeenCalledWith('password123', 'hashed_password');
|
||||
expect(mockJwtService.generateToken).toHaveBeenCalledWith({
|
||||
userId: 1,
|
||||
email: 'john@example.com'
|
||||
});
|
||||
expect(result.user).toEqual({
|
||||
id: 1,
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
createdAt: expect.any(Date),
|
||||
updatedAt: expect.any(Date)
|
||||
});
|
||||
expect(result.token).toBe('mock_jwt_token');
|
||||
expect(result.user.password).toBeUndefined(); // Password should not be returned
|
||||
});
|
||||
});
|
||||
|
||||
describe('handle - validation errors', () => {
|
||||
it('should throw error if email is missing', async () => {
|
||||
// Arrange
|
||||
const command = new LoginUserCommand('', 'password123');
|
||||
|
||||
// Act & Assert
|
||||
await expect(handler.handle(command)).rejects.toThrow('Email and password are required');
|
||||
});
|
||||
|
||||
it('should throw error if password is missing', async () => {
|
||||
// Arrange
|
||||
const command = new LoginUserCommand('john@example.com', '');
|
||||
|
||||
// Act & Assert
|
||||
await expect(handler.handle(command)).rejects.toThrow('Email and password are required');
|
||||
});
|
||||
|
||||
it('should throw error if user does not exist', async () => {
|
||||
// Arrange
|
||||
const command = new LoginUserCommand('nonexistent@example.com', 'password123');
|
||||
|
||||
mockPrisma.user.findUnique.mockResolvedValue(null);
|
||||
|
||||
// Act & Assert
|
||||
await expect(handler.handle(command)).rejects.toThrow('Invalid email or password');
|
||||
});
|
||||
|
||||
it('should throw error if password is incorrect', async () => {
|
||||
// Arrange
|
||||
const command = new LoginUserCommand('john@example.com', 'wrongpassword');
|
||||
|
||||
mockPrisma.user.findUnique.mockResolvedValue({
|
||||
id: 1,
|
||||
email: 'john@example.com',
|
||||
password: 'hashed_password'
|
||||
});
|
||||
bcrypt.compare.mockResolvedValue(false); // Password is invalid
|
||||
|
||||
// Act & Assert
|
||||
await expect(handler.handle(command)).rejects.toThrow('Invalid email or password');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,194 @@
|
||||
const RegisterUserCommandHandler = require('../../../src/application/auth/commands/RegisterUserCommandHandler');
|
||||
const RegisterUserCommand = require('../../../src/application/auth/commands/RegisterUserCommand');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const JwtService = require('../../../src/application/services/JwtService');
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('bcryptjs');
|
||||
jest.mock('../../../src/application/services/JwtService');
|
||||
|
||||
describe('RegisterUserCommandHandler', () => {
|
||||
let handler;
|
||||
let mockPrisma;
|
||||
let mockEmailService;
|
||||
let mockJwtService;
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock JwtService instance
|
||||
mockJwtService = {
|
||||
generateToken: jest.fn()
|
||||
};
|
||||
JwtService.mockImplementation(() => mockJwtService);
|
||||
|
||||
// Mock Prisma
|
||||
mockPrisma = {
|
||||
user: {
|
||||
findUnique: jest.fn(),
|
||||
create: jest.fn()
|
||||
}
|
||||
};
|
||||
|
||||
// Mock EmailService
|
||||
mockEmailService = {
|
||||
sendWelcomeEmail: jest.fn().mockResolvedValue(true)
|
||||
};
|
||||
|
||||
handler = new RegisterUserCommandHandler(mockPrisma, mockEmailService);
|
||||
|
||||
// Reset all mocks
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('handle - success cases', () => {
|
||||
it('should register a new user successfully', async () => {
|
||||
// Arrange
|
||||
const command = new RegisterUserCommand('John Doe', 'john@example.com', 'password123');
|
||||
|
||||
mockPrisma.user.findUnique.mockResolvedValue(null); // No existing user
|
||||
bcrypt.hash.mockResolvedValue('hashed_password');
|
||||
mockPrisma.user.create.mockResolvedValue({
|
||||
id: 1,
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
password: 'hashed_password',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
});
|
||||
mockJwtService.generateToken.mockReturnValue('mock_jwt_token');
|
||||
|
||||
// Act
|
||||
const result = await handler.handle(command);
|
||||
|
||||
// Assert
|
||||
expect(mockPrisma.user.findUnique).toHaveBeenCalledWith({
|
||||
where: { email: 'john@example.com' }
|
||||
});
|
||||
expect(bcrypt.hash).toHaveBeenCalledWith('password123', 10);
|
||||
expect(mockPrisma.user.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
password: 'hashed_password'
|
||||
}
|
||||
});
|
||||
expect(mockJwtService.generateToken).toHaveBeenCalledWith({
|
||||
userId: 1,
|
||||
email: 'john@example.com'
|
||||
});
|
||||
expect(result.user).toEqual({
|
||||
id: 1,
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
createdAt: expect.any(Date),
|
||||
updatedAt: expect.any(Date)
|
||||
});
|
||||
expect(result.token).toBe('mock_jwt_token');
|
||||
expect(result.user.password).toBeUndefined(); // Password should not be returned
|
||||
});
|
||||
|
||||
it('should send welcome email after registration', async () => {
|
||||
// Arrange
|
||||
const command = new RegisterUserCommand('Jane Doe', 'jane@example.com', 'password123');
|
||||
|
||||
mockPrisma.user.findUnique.mockResolvedValue(null);
|
||||
bcrypt.hash.mockResolvedValue('hashed_password');
|
||||
mockPrisma.user.create.mockResolvedValue({
|
||||
id: 2,
|
||||
name: 'Jane Doe',
|
||||
email: 'jane@example.com',
|
||||
password: 'hashed_password',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
});
|
||||
mockJwtService.generateToken.mockReturnValue('mock_jwt_token');
|
||||
|
||||
// Act
|
||||
await handler.handle(command);
|
||||
|
||||
// Assert
|
||||
expect(mockEmailService.sendWelcomeEmail).toHaveBeenCalledWith('jane@example.com', 'Jane Doe');
|
||||
});
|
||||
});
|
||||
|
||||
describe('handle - validation errors', () => {
|
||||
it('should throw error if name is missing', async () => {
|
||||
// Arrange
|
||||
const command = new RegisterUserCommand('', 'john@example.com', 'password123');
|
||||
|
||||
// Act & Assert
|
||||
await expect(handler.handle(command)).rejects.toThrow('Name, email and password are required');
|
||||
});
|
||||
|
||||
it('should throw error if email is missing', async () => {
|
||||
// Arrange
|
||||
const command = new RegisterUserCommand('John Doe', '', 'password123');
|
||||
|
||||
// Act & Assert
|
||||
await expect(handler.handle(command)).rejects.toThrow('Name, email and password are required');
|
||||
});
|
||||
|
||||
it('should throw error if password is missing', async () => {
|
||||
// Arrange
|
||||
const command = new RegisterUserCommand('John Doe', 'john@example.com', '');
|
||||
|
||||
// Act & Assert
|
||||
await expect(handler.handle(command)).rejects.toThrow('Name, email and password are required');
|
||||
});
|
||||
|
||||
it('should throw error if password is too short', async () => {
|
||||
// Arrange
|
||||
const command = new RegisterUserCommand('John Doe', 'john@example.com', '12345');
|
||||
|
||||
// Act & Assert
|
||||
await expect(handler.handle(command)).rejects.toThrow('Password must be at least 6 characters long');
|
||||
});
|
||||
|
||||
it('should throw error if email format is invalid', async () => {
|
||||
// Arrange
|
||||
const command = new RegisterUserCommand('John Doe', 'invalid-email', 'password123');
|
||||
|
||||
// Act & Assert
|
||||
await expect(handler.handle(command)).rejects.toThrow('Invalid email format');
|
||||
});
|
||||
|
||||
it('should throw error if user already exists', async () => {
|
||||
// Arrange
|
||||
const command = new RegisterUserCommand('John Doe', 'john@example.com', 'password123');
|
||||
|
||||
mockPrisma.user.findUnique.mockResolvedValue({
|
||||
id: 1,
|
||||
email: 'john@example.com'
|
||||
});
|
||||
|
||||
// Act & Assert
|
||||
await expect(handler.handle(command)).rejects.toThrow('User with this email already exists');
|
||||
});
|
||||
});
|
||||
|
||||
describe('handle - error handling', () => {
|
||||
it('should not fail if email service throws error', async () => {
|
||||
// Arrange
|
||||
const command = new RegisterUserCommand('John Doe', 'john@example.com', 'password123');
|
||||
|
||||
mockPrisma.user.findUnique.mockResolvedValue(null);
|
||||
bcrypt.hash.mockResolvedValue('hashed_password');
|
||||
mockPrisma.user.create.mockResolvedValue({
|
||||
id: 1,
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
password: 'hashed_password',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
});
|
||||
mockJwtService.generateToken.mockReturnValue('mock_jwt_token');
|
||||
mockEmailService.sendWelcomeEmail.mockRejectedValue(new Error('Email service error'));
|
||||
|
||||
// Act
|
||||
const result = await handler.handle(command);
|
||||
|
||||
// Assert - should still return user and token even if email fails
|
||||
expect(result.user.email).toBe('john@example.com');
|
||||
expect(result.token).toBe('mock_jwt_token');
|
||||
});
|
||||
});
|
||||
});
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
const UpdateUserProfileCommandHandler = require('../../../src/application/user/commands/UpdateUserProfileCommandHandler');
|
||||
const UpdateUserProfileCommand = require('../../../src/application/user/commands/UpdateUserProfileCommand');
|
||||
|
||||
describe('UpdateUserProfileCommandHandler', () => {
|
||||
let handler;
|
||||
let mockPrisma;
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock Prisma
|
||||
mockPrisma = {
|
||||
user: {
|
||||
update: jest.fn()
|
||||
}
|
||||
};
|
||||
|
||||
handler = new UpdateUserProfileCommandHandler(mockPrisma);
|
||||
|
||||
// Reset all mocks
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('handle - success cases', () => {
|
||||
it('should update user profile successfully', async () => {
|
||||
// Arrange
|
||||
const command = new UpdateUserProfileCommand(1, 'Jane Updated');
|
||||
|
||||
mockPrisma.user.update.mockResolvedValue({
|
||||
id: 1,
|
||||
name: 'Jane Updated',
|
||||
email: 'jane@example.com',
|
||||
password: 'hashed_password',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = await handler.handle(command);
|
||||
|
||||
// Assert
|
||||
expect(mockPrisma.user.update).toHaveBeenCalledWith({
|
||||
where: { id: 1 },
|
||||
data: { name: 'Jane Updated' }
|
||||
});
|
||||
expect(result).toEqual({
|
||||
id: 1,
|
||||
name: 'Jane Updated',
|
||||
email: 'jane@example.com',
|
||||
createdAt: expect.any(Date),
|
||||
updatedAt: expect.any(Date)
|
||||
});
|
||||
expect(result.password).toBeUndefined(); // Password should not be returned
|
||||
});
|
||||
});
|
||||
|
||||
describe('handle - validation errors', () => {
|
||||
it('should throw error if name is missing', async () => {
|
||||
// Arrange
|
||||
const command = new UpdateUserProfileCommand(1, '');
|
||||
|
||||
// Act & Assert
|
||||
await expect(handler.handle(command)).rejects.toThrow('Name is required');
|
||||
});
|
||||
|
||||
it('should throw error if name is null', async () => {
|
||||
// Arrange
|
||||
const command = new UpdateUserProfileCommand(1, null);
|
||||
|
||||
// Act & Assert
|
||||
await expect(handler.handle(command)).rejects.toThrow('Name is required');
|
||||
});
|
||||
|
||||
it('should throw error if name is undefined', async () => {
|
||||
// Arrange
|
||||
const command = new UpdateUserProfileCommand(1, undefined);
|
||||
|
||||
// Act & Assert
|
||||
await expect(handler.handle(command)).rejects.toThrow('Name is required');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,194 @@
|
||||
const AuthController = require('../../../src/api/controllers/AuthController');
|
||||
const RegisterUserCommand = require('../../../src/application/auth/commands/RegisterUserCommand');
|
||||
const LoginUserCommand = require('../../../src/application/auth/commands/LoginUserCommand');
|
||||
|
||||
describe('AuthController', () => {
|
||||
let controller;
|
||||
let mockRegisterHandler;
|
||||
let mockLoginHandler;
|
||||
let mockReq;
|
||||
let mockRes;
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock handlers
|
||||
mockRegisterHandler = {
|
||||
handle: jest.fn()
|
||||
};
|
||||
mockLoginHandler = {
|
||||
handle: jest.fn()
|
||||
};
|
||||
|
||||
controller = new AuthController(mockRegisterHandler, mockLoginHandler);
|
||||
|
||||
// Mock Express req/res
|
||||
mockReq = {
|
||||
body: {}
|
||||
};
|
||||
mockRes = {
|
||||
status: jest.fn().mockReturnThis(),
|
||||
json: jest.fn()
|
||||
};
|
||||
|
||||
// Reset all mocks
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('register', () => {
|
||||
it('should register user successfully and return 201', async () => {
|
||||
// Arrange
|
||||
mockReq.body = {
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
password: 'password123'
|
||||
};
|
||||
|
||||
const mockResult = {
|
||||
user: { id: 1, name: 'John Doe', email: 'john@example.com' },
|
||||
token: 'mock_jwt_token'
|
||||
};
|
||||
|
||||
mockRegisterHandler.handle.mockResolvedValue(mockResult);
|
||||
|
||||
// Act
|
||||
await controller.register(mockReq, mockRes);
|
||||
|
||||
// Assert
|
||||
expect(mockRegisterHandler.handle).toHaveBeenCalledWith(
|
||||
expect.any(RegisterUserCommand)
|
||||
);
|
||||
expect(mockRes.status).toHaveBeenCalledWith(201);
|
||||
expect(mockRes.json).toHaveBeenCalledWith({
|
||||
message: 'User registered successfully',
|
||||
data: mockResult
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 400 for validation errors', async () => {
|
||||
// Arrange
|
||||
mockReq.body = {
|
||||
name: '',
|
||||
email: 'john@example.com',
|
||||
password: 'password123'
|
||||
};
|
||||
|
||||
mockRegisterHandler.handle.mockRejectedValue(new Error('Name, email and password are required'));
|
||||
|
||||
// Act
|
||||
await controller.register(mockReq, mockRes);
|
||||
|
||||
// Assert
|
||||
expect(mockRes.status).toHaveBeenCalledWith(400);
|
||||
expect(mockRes.json).toHaveBeenCalledWith({
|
||||
error: 'Name, email and password are required'
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 400 if user already exists', async () => {
|
||||
// Arrange
|
||||
mockReq.body = {
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
password: 'password123'
|
||||
};
|
||||
|
||||
mockRegisterHandler.handle.mockRejectedValue(new Error('User with this email already exists'));
|
||||
|
||||
// Act
|
||||
await controller.register(mockReq, mockRes);
|
||||
|
||||
// Assert
|
||||
expect(mockRes.status).toHaveBeenCalledWith(400);
|
||||
expect(mockRes.json).toHaveBeenCalledWith({
|
||||
error: 'User with this email already exists'
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 500 for unexpected errors', async () => {
|
||||
// Arrange
|
||||
mockReq.body = {
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
password: 'password123'
|
||||
};
|
||||
|
||||
mockRegisterHandler.handle.mockRejectedValue(new Error('Database connection failed'));
|
||||
|
||||
// Act
|
||||
await controller.register(mockReq, mockRes);
|
||||
|
||||
// Assert
|
||||
expect(mockRes.status).toHaveBeenCalledWith(500);
|
||||
expect(mockRes.json).toHaveBeenCalledWith({
|
||||
error: 'Database connection failed'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('login', () => {
|
||||
it('should login user successfully and return 200', async () => {
|
||||
// Arrange
|
||||
mockReq.body = {
|
||||
email: 'john@example.com',
|
||||
password: 'password123'
|
||||
};
|
||||
|
||||
const mockResult = {
|
||||
user: { id: 1, name: 'John Doe', email: 'john@example.com' },
|
||||
token: 'mock_jwt_token'
|
||||
};
|
||||
|
||||
mockLoginHandler.handle.mockResolvedValue(mockResult);
|
||||
|
||||
// Act
|
||||
await controller.login(mockReq, mockRes);
|
||||
|
||||
// Assert
|
||||
expect(mockLoginHandler.handle).toHaveBeenCalledWith(
|
||||
expect.any(LoginUserCommand)
|
||||
);
|
||||
expect(mockRes.status).toHaveBeenCalledWith(200);
|
||||
expect(mockRes.json).toHaveBeenCalledWith({
|
||||
message: 'Login successful',
|
||||
data: mockResult
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 401 for invalid credentials', async () => {
|
||||
// Arrange
|
||||
mockReq.body = {
|
||||
email: 'john@example.com',
|
||||
password: 'wrongpassword'
|
||||
};
|
||||
|
||||
mockLoginHandler.handle.mockRejectedValue(new Error('Invalid email or password'));
|
||||
|
||||
// Act
|
||||
await controller.login(mockReq, mockRes);
|
||||
|
||||
// Assert
|
||||
expect(mockRes.status).toHaveBeenCalledWith(401);
|
||||
expect(mockRes.json).toHaveBeenCalledWith({
|
||||
error: 'Invalid email or password'
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 401 for missing credentials', async () => {
|
||||
// Arrange
|
||||
mockReq.body = {
|
||||
email: '',
|
||||
password: ''
|
||||
};
|
||||
|
||||
mockLoginHandler.handle.mockRejectedValue(new Error('Email and password are required'));
|
||||
|
||||
// Act
|
||||
await controller.login(mockReq, mockRes);
|
||||
|
||||
// Assert
|
||||
expect(mockRes.status).toHaveBeenCalledWith(401);
|
||||
expect(mockRes.json).toHaveBeenCalledWith({
|
||||
error: 'Email and password are required'
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,223 @@
|
||||
const UserController = require('../../../src/api/controllers/UserController');
|
||||
const GetMeQuery = require('../../../src/application/user/queries/GetMeQuery');
|
||||
const GetAllUsersQuery = require('../../../src/application/user/queries/GetAllUsersQuery');
|
||||
const GetUserByIdQuery = require('../../../src/application/user/queries/GetUserByIdQuery');
|
||||
const UpdateUserProfileCommand = require('../../../src/application/user/commands/UpdateUserProfileCommand');
|
||||
|
||||
describe('UserController', () => {
|
||||
let controller;
|
||||
let mockGetMeHandler;
|
||||
let mockGetAllUsersHandler;
|
||||
let mockGetUserByIdHandler;
|
||||
let mockUpdateProfileHandler;
|
||||
let mockReq;
|
||||
let mockRes;
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock handlers
|
||||
mockGetMeHandler = { handle: jest.fn() };
|
||||
mockGetAllUsersHandler = { handle: jest.fn() };
|
||||
mockGetUserByIdHandler = { handle: jest.fn() };
|
||||
mockUpdateProfileHandler = { handle: jest.fn() };
|
||||
|
||||
controller = new UserController(
|
||||
mockGetMeHandler,
|
||||
mockGetAllUsersHandler,
|
||||
mockGetUserByIdHandler,
|
||||
mockUpdateProfileHandler
|
||||
);
|
||||
|
||||
// Mock Express req/res
|
||||
mockReq = {
|
||||
user: { userId: 1 }, // Set by authMiddleware
|
||||
body: {},
|
||||
params: {}
|
||||
};
|
||||
mockRes = {
|
||||
status: jest.fn().mockReturnThis(),
|
||||
json: jest.fn()
|
||||
};
|
||||
|
||||
// Reset all mocks
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('getMe', () => {
|
||||
it('should return current user successfully', async () => {
|
||||
// Arrange
|
||||
const mockUser = {
|
||||
id: 1,
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com'
|
||||
};
|
||||
|
||||
mockGetMeHandler.handle.mockResolvedValue(mockUser);
|
||||
|
||||
// Act
|
||||
await controller.getMe(mockReq, mockRes);
|
||||
|
||||
// Assert
|
||||
expect(mockGetMeHandler.handle).toHaveBeenCalledWith(
|
||||
expect.any(GetMeQuery)
|
||||
);
|
||||
expect(mockRes.status).toHaveBeenCalledWith(200);
|
||||
expect(mockRes.json).toHaveBeenCalledWith({
|
||||
message: 'User retrieved successfully',
|
||||
data: mockUser
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 404 if user not found', async () => {
|
||||
// Arrange
|
||||
mockGetMeHandler.handle.mockRejectedValue(new Error('User not found'));
|
||||
|
||||
// Act
|
||||
await controller.getMe(mockReq, mockRes);
|
||||
|
||||
// Assert
|
||||
expect(mockRes.status).toHaveBeenCalledWith(404);
|
||||
expect(mockRes.json).toHaveBeenCalledWith({
|
||||
error: 'User not found'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAll', () => {
|
||||
it('should return all users successfully', async () => {
|
||||
// Arrange
|
||||
const mockUsers = [
|
||||
{ id: 1, name: 'John Doe', email: 'john@example.com' },
|
||||
{ id: 2, name: 'Jane Doe', email: 'jane@example.com' }
|
||||
];
|
||||
|
||||
mockGetAllUsersHandler.handle.mockResolvedValue(mockUsers);
|
||||
|
||||
// Act
|
||||
await controller.getAll(mockReq, mockRes);
|
||||
|
||||
// Assert
|
||||
expect(mockGetAllUsersHandler.handle).toHaveBeenCalledWith(
|
||||
expect.any(GetAllUsersQuery)
|
||||
);
|
||||
expect(mockRes.status).toHaveBeenCalledWith(200);
|
||||
expect(mockRes.json).toHaveBeenCalledWith({
|
||||
message: 'Users retrieved successfully',
|
||||
data: mockUsers,
|
||||
count: 2
|
||||
});
|
||||
});
|
||||
|
||||
it('should return empty array if no users exist', async () => {
|
||||
// Arrange
|
||||
mockGetAllUsersHandler.handle.mockResolvedValue([]);
|
||||
|
||||
// Act
|
||||
await controller.getAll(mockReq, mockRes);
|
||||
|
||||
// Assert
|
||||
expect(mockRes.json).toHaveBeenCalledWith({
|
||||
message: 'Users retrieved successfully',
|
||||
data: [],
|
||||
count: 0
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getById', () => {
|
||||
it('should return user by ID successfully', async () => {
|
||||
// Arrange
|
||||
mockReq.params = { id: '2' };
|
||||
const mockUser = {
|
||||
id: 2,
|
||||
name: 'Jane Doe',
|
||||
email: 'jane@example.com'
|
||||
};
|
||||
|
||||
mockGetUserByIdHandler.handle.mockResolvedValue(mockUser);
|
||||
|
||||
// Act
|
||||
await controller.getById(mockReq, mockRes);
|
||||
|
||||
// Assert
|
||||
expect(mockGetUserByIdHandler.handle).toHaveBeenCalledWith(
|
||||
expect.any(GetUserByIdQuery)
|
||||
);
|
||||
expect(mockRes.status).toHaveBeenCalledWith(200);
|
||||
expect(mockRes.json).toHaveBeenCalledWith({
|
||||
message: 'User retrieved successfully',
|
||||
data: mockUser
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 400 for invalid user ID', async () => {
|
||||
// Arrange
|
||||
mockReq.params = { id: 'invalid' };
|
||||
|
||||
// Act
|
||||
await controller.getById(mockReq, mockRes);
|
||||
|
||||
// Assert
|
||||
expect(mockRes.status).toHaveBeenCalledWith(400);
|
||||
expect(mockRes.json).toHaveBeenCalledWith({
|
||||
error: 'Invalid user ID'
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 404 if user not found', async () => {
|
||||
// Arrange
|
||||
mockReq.params = { id: '999' };
|
||||
mockGetUserByIdHandler.handle.mockRejectedValue(new Error('User not found'));
|
||||
|
||||
// Act
|
||||
await controller.getById(mockReq, mockRes);
|
||||
|
||||
// Assert
|
||||
expect(mockRes.status).toHaveBeenCalledWith(404);
|
||||
expect(mockRes.json).toHaveBeenCalledWith({
|
||||
error: 'User not found'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateMe', () => {
|
||||
it('should update user profile successfully', async () => {
|
||||
// Arrange
|
||||
mockReq.body = { name: 'John Updated' };
|
||||
const mockUpdatedUser = {
|
||||
id: 1,
|
||||
name: 'John Updated',
|
||||
email: 'john@example.com'
|
||||
};
|
||||
|
||||
mockUpdateProfileHandler.handle.mockResolvedValue(mockUpdatedUser);
|
||||
|
||||
// Act
|
||||
await controller.updateMe(mockReq, mockRes);
|
||||
|
||||
// Assert
|
||||
expect(mockUpdateProfileHandler.handle).toHaveBeenCalledWith(
|
||||
expect.any(UpdateUserProfileCommand)
|
||||
);
|
||||
expect(mockRes.status).toHaveBeenCalledWith(200);
|
||||
expect(mockRes.json).toHaveBeenCalledWith({
|
||||
message: 'Profile updated successfully',
|
||||
data: mockUpdatedUser
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 400 if name is missing', async () => {
|
||||
// Arrange
|
||||
mockReq.body = { name: '' };
|
||||
mockUpdateProfileHandler.handle.mockRejectedValue(new Error('Name is required'));
|
||||
|
||||
// Act
|
||||
await controller.updateMe(mockReq, mockRes);
|
||||
|
||||
// Assert
|
||||
expect(mockRes.status).toHaveBeenCalledWith(400);
|
||||
expect(mockRes.json).toHaveBeenCalledWith({
|
||||
error: 'Name is required'
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,123 @@
|
||||
const authMiddleware = require('../../../src/api/middlewares/authMiddleware');
|
||||
const JwtService = require('../../../src/application/services/JwtService');
|
||||
|
||||
// Mock JwtService
|
||||
jest.mock('../../../src/application/services/JwtService');
|
||||
|
||||
describe('authMiddleware (Cookie-based)', () => {
|
||||
let mockReq;
|
||||
let mockRes;
|
||||
let mockNext;
|
||||
let mockJwtService;
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock Express req/res/next
|
||||
mockReq = {
|
||||
cookies: {}
|
||||
};
|
||||
mockRes = {
|
||||
status: jest.fn().mockReturnThis(),
|
||||
json: jest.fn()
|
||||
};
|
||||
mockNext = jest.fn();
|
||||
|
||||
// Mock JwtService instance
|
||||
mockJwtService = {
|
||||
extractTokenFromCookies: jest.fn(),
|
||||
verifyToken: jest.fn()
|
||||
};
|
||||
|
||||
JwtService.mockImplementation(() => mockJwtService);
|
||||
|
||||
// Reset all mocks
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('successful authentication', () => {
|
||||
it('should authenticate valid JWT token from cookie and call next()', () => {
|
||||
// Arrange
|
||||
mockReq.cookies = { auth_token: 'valid_token_123' };
|
||||
|
||||
const mockDecoded = {
|
||||
userId: 1,
|
||||
email: 'john@example.com'
|
||||
};
|
||||
|
||||
mockJwtService.extractTokenFromCookies.mockReturnValue('valid_token_123');
|
||||
mockJwtService.verifyToken.mockReturnValue(mockDecoded);
|
||||
|
||||
// Act
|
||||
authMiddleware(mockReq, mockRes, mockNext);
|
||||
|
||||
// Assert
|
||||
expect(mockReq.user).toEqual({
|
||||
userId: 1,
|
||||
email: 'john@example.com'
|
||||
});
|
||||
expect(mockNext).toHaveBeenCalled();
|
||||
expect(mockRes.status).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('authentication failures', () => {
|
||||
it('should return 401 if no cookie is present', () => {
|
||||
// Arrange
|
||||
mockReq.cookies = {};
|
||||
|
||||
mockJwtService.extractTokenFromCookies.mockReturnValue(null);
|
||||
|
||||
// Act
|
||||
authMiddleware(mockReq, mockRes, mockNext);
|
||||
|
||||
// Assert
|
||||
expect(mockRes.status).toHaveBeenCalledWith(401);
|
||||
expect(mockRes.json).toHaveBeenCalledWith({
|
||||
error: 'Authentication required',
|
||||
message: 'No token provided in cookies'
|
||||
});
|
||||
expect(mockNext).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return 401 if cookie token is invalid', () => {
|
||||
// Arrange
|
||||
mockReq.cookies = { auth_token: 'invalid_token' };
|
||||
|
||||
mockJwtService.extractTokenFromCookies.mockReturnValue('invalid_token');
|
||||
mockJwtService.verifyToken.mockImplementation(() => {
|
||||
throw new Error('Invalid or expired token');
|
||||
});
|
||||
|
||||
// Act
|
||||
authMiddleware(mockReq, mockRes, mockNext);
|
||||
|
||||
// Assert
|
||||
expect(mockRes.status).toHaveBeenCalledWith(401);
|
||||
expect(mockRes.json).toHaveBeenCalledWith({
|
||||
error: 'Authentication failed',
|
||||
message: 'Invalid or expired token'
|
||||
});
|
||||
expect(mockNext).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return 401 if token is expired', () => {
|
||||
// Arrange
|
||||
mockReq.cookies = { auth_token: 'expired_token' };
|
||||
|
||||
mockJwtService.extractTokenFromCookies.mockReturnValue('expired_token');
|
||||
mockJwtService.verifyToken.mockImplementation(() => {
|
||||
throw new Error('Token has expired');
|
||||
});
|
||||
|
||||
// Act
|
||||
authMiddleware(mockReq, mockRes, mockNext);
|
||||
|
||||
// Assert
|
||||
expect(mockRes.status).toHaveBeenCalledWith(401);
|
||||
expect(mockRes.json).toHaveBeenCalledWith({
|
||||
error: 'Authentication failed',
|
||||
message: 'Token has expired'
|
||||
});
|
||||
expect(mockNext).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,156 @@
|
||||
const corsMiddleware = require('../../../src/api/middlewares/corsMiddleware');
|
||||
|
||||
describe('CORS Middleware', () => {
|
||||
let req, res, next;
|
||||
|
||||
beforeEach(() => {
|
||||
req = {
|
||||
headers: {},
|
||||
method: 'GET',
|
||||
};
|
||||
res = {
|
||||
setHeader: jest.fn(),
|
||||
status: jest.fn().mockReturnThis(),
|
||||
end: jest.fn(),
|
||||
};
|
||||
next = jest.fn();
|
||||
});
|
||||
|
||||
describe('Allowed origins', () => {
|
||||
test('should allow requests from http://localhost:3001', () => {
|
||||
req.headers.origin = 'http://localhost:3001';
|
||||
|
||||
corsMiddleware(req, res, next);
|
||||
|
||||
expect(res.setHeader).toHaveBeenCalledWith('Access-Control-Allow-Origin', 'http://localhost:3001');
|
||||
expect(res.setHeader).toHaveBeenCalledWith('Access-Control-Allow-Credentials', 'true');
|
||||
expect(next).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should allow requests from http://localhost:3000', () => {
|
||||
req.headers.origin = 'http://localhost:3000';
|
||||
|
||||
corsMiddleware(req, res, next);
|
||||
|
||||
expect(res.setHeader).toHaveBeenCalledWith('Access-Control-Allow-Origin', 'http://localhost:3000');
|
||||
expect(res.setHeader).toHaveBeenCalledWith('Access-Control-Allow-Credentials', 'true');
|
||||
expect(next).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should allow requests from https://myapp.com', () => {
|
||||
req.headers.origin = 'https://myapp.com';
|
||||
|
||||
corsMiddleware(req, res, next);
|
||||
|
||||
expect(res.setHeader).toHaveBeenCalledWith('Access-Control-Allow-Origin', 'https://myapp.com');
|
||||
expect(res.setHeader).toHaveBeenCalledWith('Access-Control-Allow-Credentials', 'true');
|
||||
expect(next).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Disallowed origins', () => {
|
||||
test('should reject requests from unknown origins', () => {
|
||||
req.headers.origin = 'http://malicious-site.com';
|
||||
|
||||
corsMiddleware(req, res, next);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(403);
|
||||
expect(res.end).toHaveBeenCalledWith('CORS policy: Origin not allowed');
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should reject requests from http://evil.com', () => {
|
||||
req.headers.origin = 'http://evil.com';
|
||||
|
||||
corsMiddleware(req, res, next);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(403);
|
||||
expect(res.end).toHaveBeenCalledWith('CORS policy: Origin not allowed');
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Preflight requests (OPTIONS)', () => {
|
||||
beforeEach(() => {
|
||||
req.method = 'OPTIONS';
|
||||
});
|
||||
|
||||
test('should handle OPTIONS request from allowed origin', () => {
|
||||
req.headers.origin = 'http://localhost:3001';
|
||||
|
||||
corsMiddleware(req, res, next);
|
||||
|
||||
expect(res.setHeader).toHaveBeenCalledWith('Access-Control-Allow-Origin', 'http://localhost:3001');
|
||||
expect(res.setHeader).toHaveBeenCalledWith('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
||||
expect(res.setHeader).toHaveBeenCalledWith('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
||||
expect(res.setHeader).toHaveBeenCalledWith('Access-Control-Allow-Credentials', 'true');
|
||||
expect(res.status).toHaveBeenCalledWith(204);
|
||||
expect(res.end).toHaveBeenCalled();
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should reject OPTIONS request from disallowed origin', () => {
|
||||
req.headers.origin = 'http://hackersite.com';
|
||||
|
||||
corsMiddleware(req, res, next);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(403);
|
||||
expect(res.end).toHaveBeenCalledWith('CORS policy: Origin not allowed');
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('No origin header', () => {
|
||||
test('should allow requests without origin header (same-origin)', () => {
|
||||
delete req.headers.origin;
|
||||
|
||||
corsMiddleware(req, res, next);
|
||||
|
||||
expect(next).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Headers configuration', () => {
|
||||
test('should set correct CORS headers for allowed origin', () => {
|
||||
req.headers.origin = 'http://localhost:3000';
|
||||
|
||||
corsMiddleware(req, res, next);
|
||||
|
||||
expect(res.setHeader).toHaveBeenCalledWith('Access-Control-Allow-Origin', 'http://localhost:3000');
|
||||
expect(res.setHeader).toHaveBeenCalledWith('Access-Control-Allow-Credentials', 'true');
|
||||
});
|
||||
|
||||
test('should set method headers for OPTIONS request', () => {
|
||||
req.headers.origin = 'http://localhost:3000';
|
||||
req.method = 'OPTIONS';
|
||||
|
||||
corsMiddleware(req, res, next);
|
||||
|
||||
expect(res.setHeader).toHaveBeenCalledWith('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
||||
expect(res.setHeader).toHaveBeenCalledWith('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Real-world scenarios', () => {
|
||||
test('should handle POST request from allowed frontend', () => {
|
||||
req.headers.origin = 'http://localhost:3001';
|
||||
req.method = 'POST';
|
||||
|
||||
corsMiddleware(req, res, next);
|
||||
|
||||
expect(res.setHeader).toHaveBeenCalledWith('Access-Control-Allow-Origin', 'http://localhost:3001');
|
||||
expect(res.setHeader).toHaveBeenCalledWith('Access-Control-Allow-Credentials', 'true');
|
||||
expect(next).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should reject DELETE request from disallowed origin', () => {
|
||||
req.headers.origin = 'http://unauthorized.com';
|
||||
req.method = 'DELETE';
|
||||
|
||||
corsMiddleware(req, res, next);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(403);
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
const GetAllUsersQueryHandler = require('../../../src/application/user/queries/GetAllUsersQueryHandler');
|
||||
const GetAllUsersQuery = require('../../../src/application/user/queries/GetAllUsersQuery');
|
||||
|
||||
describe('GetAllUsersQueryHandler', () => {
|
||||
let handler;
|
||||
let mockPrisma;
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock Prisma
|
||||
mockPrisma = {
|
||||
user: {
|
||||
findMany: jest.fn()
|
||||
}
|
||||
};
|
||||
|
||||
handler = new GetAllUsersQueryHandler(mockPrisma);
|
||||
|
||||
// Reset all mocks
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('handle - success cases', () => {
|
||||
it('should return all users successfully', async () => {
|
||||
// Arrange
|
||||
const query = new GetAllUsersQuery();
|
||||
|
||||
mockPrisma.user.findMany.mockResolvedValue([
|
||||
{
|
||||
id: 1,
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
password: 'hashed_password_1',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Jane Doe',
|
||||
email: 'jane@example.com',
|
||||
password: 'hashed_password_2',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
}
|
||||
]);
|
||||
|
||||
// Act
|
||||
const result = await handler.handle(query);
|
||||
|
||||
// Assert
|
||||
expect(mockPrisma.user.findMany).toHaveBeenCalledWith({
|
||||
orderBy: { createdAt: 'desc' }
|
||||
});
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0]).toEqual({
|
||||
id: 1,
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
createdAt: expect.any(Date),
|
||||
updatedAt: expect.any(Date)
|
||||
});
|
||||
expect(result[1]).toEqual({
|
||||
id: 2,
|
||||
name: 'Jane Doe',
|
||||
email: 'jane@example.com',
|
||||
createdAt: expect.any(Date),
|
||||
updatedAt: expect.any(Date)
|
||||
});
|
||||
// Passwords should not be returned
|
||||
expect(result[0].password).toBeUndefined();
|
||||
expect(result[1].password).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return empty array if no users exist', async () => {
|
||||
// Arrange
|
||||
const query = new GetAllUsersQuery();
|
||||
|
||||
mockPrisma.user.findMany.mockResolvedValue([]);
|
||||
|
||||
// Act
|
||||
const result = await handler.handle(query);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
const GetMeQueryHandler = require('../../../src/application/user/queries/GetMeQueryHandler');
|
||||
const GetMeQuery = require('../../../src/application/user/queries/GetMeQuery');
|
||||
|
||||
describe('GetMeQueryHandler', () => {
|
||||
let handler;
|
||||
let mockPrisma;
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock Prisma
|
||||
mockPrisma = {
|
||||
user: {
|
||||
findUnique: jest.fn()
|
||||
}
|
||||
};
|
||||
|
||||
handler = new GetMeQueryHandler(mockPrisma);
|
||||
|
||||
// Reset all mocks
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('handle - success cases', () => {
|
||||
it('should return current user successfully', async () => {
|
||||
// Arrange
|
||||
const query = new GetMeQuery(1);
|
||||
|
||||
mockPrisma.user.findUnique.mockResolvedValue({
|
||||
id: 1,
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
password: 'hashed_password',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = await handler.handle(query);
|
||||
|
||||
// Assert
|
||||
expect(mockPrisma.user.findUnique).toHaveBeenCalledWith({
|
||||
where: { id: 1 }
|
||||
});
|
||||
expect(result).toEqual({
|
||||
id: 1,
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
createdAt: expect.any(Date),
|
||||
updatedAt: expect.any(Date)
|
||||
});
|
||||
expect(result.password).toBeUndefined(); // Password should not be returned
|
||||
});
|
||||
});
|
||||
|
||||
describe('handle - error cases', () => {
|
||||
it('should throw error if user not found', async () => {
|
||||
// Arrange
|
||||
const query = new GetMeQuery(999);
|
||||
|
||||
mockPrisma.user.findUnique.mockResolvedValue(null);
|
||||
|
||||
// Act & Assert
|
||||
await expect(handler.handle(query)).rejects.toThrow('User not found');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,91 @@
|
||||
const GetUserByIdQueryHandler = require('../../../src/application/user/queries/GetUserByIdQueryHandler');
|
||||
const GetUserByIdQuery = require('../../../src/application/user/queries/GetUserByIdQuery');
|
||||
|
||||
describe('GetUserByIdQueryHandler', () => {
|
||||
let handler;
|
||||
let mockPrisma;
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock Prisma
|
||||
mockPrisma = {
|
||||
user: {
|
||||
findUnique: jest.fn()
|
||||
}
|
||||
};
|
||||
|
||||
handler = new GetUserByIdQueryHandler(mockPrisma);
|
||||
|
||||
// Reset all mocks
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('handle - success cases', () => {
|
||||
it('should return user by ID successfully', async () => {
|
||||
// Arrange
|
||||
const query = new GetUserByIdQuery(1);
|
||||
|
||||
mockPrisma.user.findUnique.mockResolvedValue({
|
||||
id: 1,
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
password: 'hashed_password',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = await handler.handle(query);
|
||||
|
||||
// Assert
|
||||
expect(mockPrisma.user.findUnique).toHaveBeenCalledWith({
|
||||
where: { id: 1 }
|
||||
});
|
||||
expect(result).toEqual({
|
||||
id: 1,
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
createdAt: expect.any(Date),
|
||||
updatedAt: expect.any(Date)
|
||||
});
|
||||
expect(result.password).toBeUndefined(); // Password should not be returned
|
||||
});
|
||||
});
|
||||
|
||||
describe('handle - validation errors', () => {
|
||||
it('should throw error if userId is invalid (NaN)', async () => {
|
||||
// Arrange
|
||||
const query = new GetUserByIdQuery('invalid');
|
||||
|
||||
// Act & Assert
|
||||
await expect(handler.handle(query)).rejects.toThrow('Valid user ID is required');
|
||||
});
|
||||
|
||||
it('should throw error if userId is null', async () => {
|
||||
// Arrange
|
||||
const query = new GetUserByIdQuery(null);
|
||||
|
||||
// Act & Assert
|
||||
await expect(handler.handle(query)).rejects.toThrow('Valid user ID is required');
|
||||
});
|
||||
|
||||
it('should throw error if userId is undefined', async () => {
|
||||
// Arrange
|
||||
const query = new GetUserByIdQuery(undefined);
|
||||
|
||||
// Act & Assert
|
||||
await expect(handler.handle(query)).rejects.toThrow('Valid user ID is required');
|
||||
});
|
||||
});
|
||||
|
||||
describe('handle - error cases', () => {
|
||||
it('should throw error if user not found', async () => {
|
||||
// Arrange
|
||||
const query = new GetUserByIdQuery(999);
|
||||
|
||||
mockPrisma.user.findUnique.mockResolvedValue(null);
|
||||
|
||||
// Act & Assert
|
||||
await expect(handler.handle(query)).rejects.toThrow('User not found');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,240 @@
|
||||
const EmailService = require('../../../src/application/services/EmailService');
|
||||
const nodemailer = require('nodemailer');
|
||||
|
||||
// Mock nodemailer
|
||||
jest.mock('nodemailer');
|
||||
|
||||
describe('EmailService', () => {
|
||||
let emailService;
|
||||
let mockTransporter;
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset mocks
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Mock transporter
|
||||
mockTransporter = {
|
||||
sendMail: jest.fn().mockResolvedValue({
|
||||
messageId: 'test-message-id',
|
||||
response: '250 OK',
|
||||
}),
|
||||
};
|
||||
|
||||
// Mock nodemailer.createTransport
|
||||
nodemailer.createTransport.mockReturnValue(mockTransporter);
|
||||
|
||||
// Create EmailService instance
|
||||
emailService = new EmailService();
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
test('should create transporter with correct configuration', () => {
|
||||
expect(nodemailer.createTransport).toHaveBeenCalled();
|
||||
|
||||
const config = nodemailer.createTransport.mock.calls[0][0];
|
||||
expect(config).toHaveProperty('host');
|
||||
expect(config).toHaveProperty('port');
|
||||
expect(config).toHaveProperty('auth');
|
||||
expect(config.auth).toHaveProperty('user');
|
||||
expect(config.auth).toHaveProperty('pass');
|
||||
});
|
||||
|
||||
test('should initialize transporter instance', () => {
|
||||
expect(emailService.transporter).toBeDefined();
|
||||
expect(emailService.transporter).toBe(mockTransporter);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendWelcomeEmail', () => {
|
||||
test('should send welcome email with correct parameters', async () => {
|
||||
const userEmail = 'test@example.com';
|
||||
const userName = 'John Doe';
|
||||
|
||||
await emailService.sendWelcomeEmail(userEmail, userName);
|
||||
|
||||
expect(mockTransporter.sendMail).toHaveBeenCalledTimes(1);
|
||||
|
||||
const emailOptions = mockTransporter.sendMail.mock.calls[0][0];
|
||||
expect(emailOptions.to).toBe(userEmail);
|
||||
expect(emailOptions.subject).toContain('Üdvözlünk');
|
||||
expect(emailOptions).toHaveProperty('html');
|
||||
});
|
||||
|
||||
test('should include user name in email content', async () => {
|
||||
const userEmail = 'jane@example.com';
|
||||
const userName = 'Jane Smith';
|
||||
|
||||
await emailService.sendWelcomeEmail(userEmail, userName);
|
||||
|
||||
const emailOptions = mockTransporter.sendMail.mock.calls[0][0];
|
||||
expect(emailOptions.html).toContain(userName);
|
||||
});
|
||||
|
||||
test('should include user email in email content', async () => {
|
||||
const userEmail = 'contact@test.com';
|
||||
const userName = 'Test User';
|
||||
|
||||
await emailService.sendWelcomeEmail(userEmail, userName);
|
||||
|
||||
const emailOptions = mockTransporter.sendMail.mock.calls[0][0];
|
||||
expect(emailOptions.html).toContain(userEmail);
|
||||
});
|
||||
|
||||
test('should return messageId on successful send', async () => {
|
||||
const result = await emailService.sendWelcomeEmail('user@test.com', 'User');
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.messageId).toBe('test-message-id');
|
||||
});
|
||||
|
||||
test('should use correct from address', async () => {
|
||||
await emailService.sendWelcomeEmail('recipient@test.com', 'Recipient');
|
||||
|
||||
const emailOptions = mockTransporter.sendMail.mock.calls[0][0];
|
||||
expect(emailOptions).toHaveProperty('from');
|
||||
expect(emailOptions.from).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should handle multiple recipients', async () => {
|
||||
await emailService.sendWelcomeEmail('user1@test.com', 'User 1');
|
||||
await emailService.sendWelcomeEmail('user2@test.com', 'User 2');
|
||||
await emailService.sendWelcomeEmail('user3@test.com', 'User 3');
|
||||
|
||||
expect(mockTransporter.sendMail).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
test('should throw error when email sending fails', async () => {
|
||||
mockTransporter.sendMail.mockRejectedValue(new Error('SMTP connection failed'));
|
||||
|
||||
await expect(
|
||||
emailService.sendWelcomeEmail('user@test.com', 'User')
|
||||
).rejects.toThrow('SMTP connection failed');
|
||||
});
|
||||
|
||||
test('should throw error for invalid email address', async () => {
|
||||
mockTransporter.sendMail.mockRejectedValue(new Error('Invalid recipient'));
|
||||
|
||||
await expect(
|
||||
emailService.sendWelcomeEmail('invalid-email', 'User')
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
test('should throw error when transporter is not configured', async () => {
|
||||
mockTransporter.sendMail.mockRejectedValue(new Error('Transporter not configured'));
|
||||
|
||||
await expect(
|
||||
emailService.sendWelcomeEmail('user@test.com', 'User')
|
||||
).rejects.toThrow('Transporter not configured');
|
||||
});
|
||||
});
|
||||
|
||||
describe('template rendering', () => {
|
||||
test('should render HTML template with Handlebars', async () => {
|
||||
await emailService.sendWelcomeEmail('template@test.com', 'Template User');
|
||||
|
||||
const emailOptions = mockTransporter.sendMail.mock.calls[0][0];
|
||||
|
||||
// Check that HTML is rendered (not raw Handlebars template)
|
||||
expect(emailOptions.html).not.toContain('{{userName}}');
|
||||
expect(emailOptions.html).not.toContain('{{userEmail}}');
|
||||
expect(emailOptions.html).toContain('Template User');
|
||||
expect(emailOptions.html).toContain('template@test.com');
|
||||
});
|
||||
|
||||
test('should handle special characters in user name', async () => {
|
||||
const specialName = "O'Reilly & Sons <script>alert('xss')</script>";
|
||||
|
||||
await emailService.sendWelcomeEmail('user@test.com', specialName);
|
||||
|
||||
const emailOptions = mockTransporter.sendMail.mock.calls[0][0];
|
||||
|
||||
// Handlebars should escape HTML by default
|
||||
expect(emailOptions.html).toBeDefined();
|
||||
});
|
||||
|
||||
test('should use correct template file path', async () => {
|
||||
await emailService.sendWelcomeEmail('user@test.com', 'User');
|
||||
|
||||
// Verify that email was sent (which means template was loaded successfully)
|
||||
expect(mockTransporter.sendMail).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('real-world scenarios', () => {
|
||||
test('should send welcome email after user registration', async () => {
|
||||
const newUser = {
|
||||
email: 'newuser@example.com',
|
||||
name: 'New User',
|
||||
};
|
||||
|
||||
const result = await emailService.sendWelcomeEmail(newUser.email, newUser.name);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(mockTransporter.sendMail).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
to: newUser.email,
|
||||
html: expect.stringContaining(newUser.name),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('should handle concurrent email sends', async () => {
|
||||
const users = [
|
||||
{ email: 'user1@test.com', name: 'User 1' },
|
||||
{ email: 'user2@test.com', name: 'User 2' },
|
||||
{ email: 'user3@test.com', name: 'User 3' },
|
||||
];
|
||||
|
||||
await Promise.all(
|
||||
users.map(user => emailService.sendWelcomeEmail(user.email, user.name))
|
||||
);
|
||||
|
||||
expect(mockTransporter.sendMail).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
test('should work with Ethereal Email test account', async () => {
|
||||
// Simulate Ethereal Email configuration
|
||||
const etherealTransporter = {
|
||||
sendMail: jest.fn().mockResolvedValue({
|
||||
messageId: '<ethereal-id@ethereal.email>',
|
||||
response: '250 Accepted',
|
||||
}),
|
||||
};
|
||||
|
||||
nodemailer.createTransport.mockReturnValue(etherealTransporter);
|
||||
const etherealEmailService = new EmailService();
|
||||
|
||||
const result = await etherealEmailService.sendWelcomeEmail('test@ethereal.email', 'Test User');
|
||||
|
||||
expect(result.messageId).toContain('ethereal');
|
||||
expect(etherealTransporter.sendMail).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('email content validation', () => {
|
||||
test('should include welcome message', async () => {
|
||||
await emailService.sendWelcomeEmail('user@test.com', 'User');
|
||||
|
||||
const emailOptions = mockTransporter.sendMail.mock.calls[0][0];
|
||||
expect(emailOptions.html.toLowerCase()).toMatch(/üdvözl|welcome/i);
|
||||
});
|
||||
|
||||
test('should be HTML formatted', async () => {
|
||||
await emailService.sendWelcomeEmail('user@test.com', 'User');
|
||||
|
||||
const emailOptions = mockTransporter.sendMail.mock.calls[0][0];
|
||||
expect(emailOptions.html).toContain('<html');
|
||||
expect(emailOptions.html).toContain('</html>');
|
||||
});
|
||||
|
||||
test('should have valid subject line', async () => {
|
||||
await emailService.sendWelcomeEmail('user@test.com', 'User');
|
||||
|
||||
const emailOptions = mockTransporter.sendMail.mock.calls[0][0];
|
||||
expect(emailOptions.subject).toBeTruthy();
|
||||
expect(emailOptions.subject.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,218 @@
|
||||
const JwtService = require('../../../src/application/services/JwtService');
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
// Mock jsonwebtoken
|
||||
jest.mock('jsonwebtoken');
|
||||
|
||||
describe('JwtService', () => {
|
||||
let jwtService;
|
||||
const mockSecret = 'test-secret';
|
||||
const mockExpiresIn = '1h';
|
||||
|
||||
beforeEach(() => {
|
||||
// Setup environment variables
|
||||
process.env.JWT_SECRET = mockSecret;
|
||||
process.env.JWT_EXPIRES_IN = mockExpiresIn;
|
||||
|
||||
jwtService = new JwtService();
|
||||
|
||||
// Reset all mocks
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('generateToken', () => {
|
||||
it('should generate a JWT token with payload', () => {
|
||||
// Arrange
|
||||
const payload = { userId: 1, email: 'john@example.com' };
|
||||
const mockToken = 'mock_jwt_token_abc123';
|
||||
|
||||
jwt.sign.mockReturnValue(mockToken);
|
||||
|
||||
// Act
|
||||
const result = jwtService.generateToken(payload);
|
||||
|
||||
// Assert
|
||||
expect(jwt.sign).toHaveBeenCalledWith(payload, mockSecret, { expiresIn: mockExpiresIn });
|
||||
expect(result).toBe(mockToken);
|
||||
});
|
||||
|
||||
it('should use default secret if JWT_SECRET not set', () => {
|
||||
// Arrange
|
||||
delete process.env.JWT_SECRET;
|
||||
jwtService = new JwtService();
|
||||
const payload = { userId: 1, email: 'john@example.com' };
|
||||
|
||||
jwt.sign.mockReturnValue('token');
|
||||
|
||||
// Act
|
||||
jwtService.generateToken(payload);
|
||||
|
||||
// Assert
|
||||
expect(jwt.sign).toHaveBeenCalledWith(payload, 'default-secret-change-me', expect.any(Object));
|
||||
});
|
||||
});
|
||||
|
||||
describe('verifyToken', () => {
|
||||
it('should verify and return decoded token', () => {
|
||||
// Arrange
|
||||
const token = 'valid_token';
|
||||
const mockDecoded = { userId: 1, email: 'john@example.com' };
|
||||
|
||||
jwt.verify.mockReturnValue(mockDecoded);
|
||||
|
||||
// Act
|
||||
const result = jwtService.verifyToken(token);
|
||||
|
||||
// Assert
|
||||
expect(jwt.verify).toHaveBeenCalledWith(token, mockSecret);
|
||||
expect(result).toEqual(mockDecoded);
|
||||
});
|
||||
|
||||
it('should throw error for invalid token', () => {
|
||||
// Arrange
|
||||
const token = 'invalid_token';
|
||||
|
||||
jwt.verify.mockImplementation(() => {
|
||||
throw new Error('jwt malformed');
|
||||
});
|
||||
|
||||
// Act & Assert
|
||||
expect(() => jwtService.verifyToken(token)).toThrow('Invalid or expired token');
|
||||
});
|
||||
|
||||
it('should throw error for expired token', () => {
|
||||
// Arrange
|
||||
const token = 'expired_token';
|
||||
|
||||
jwt.verify.mockImplementation(() => {
|
||||
throw new Error('jwt expired');
|
||||
});
|
||||
|
||||
// Act & Assert
|
||||
expect(() => jwtService.verifyToken(token)).toThrow('Invalid or expired token');
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractTokenFromCookies', () => {
|
||||
it('should extract token from cookies object', () => {
|
||||
// Arrange
|
||||
const cookies = { auth_token: 'abc123xyz' };
|
||||
|
||||
// Act
|
||||
const result = jwtService.extractTokenFromCookies(cookies);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe('abc123xyz');
|
||||
});
|
||||
|
||||
it('should return null if cookies object is empty', () => {
|
||||
// Arrange
|
||||
const cookies = {};
|
||||
|
||||
// Act
|
||||
const result = jwtService.extractTokenFromCookies(cookies);
|
||||
|
||||
// Assert
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null if cookies is null', () => {
|
||||
// Arrange
|
||||
const cookies = null;
|
||||
|
||||
// Act
|
||||
const result = jwtService.extractTokenFromCookies(cookies);
|
||||
|
||||
// Assert
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null if cookies is undefined', () => {
|
||||
// Arrange
|
||||
const cookies = undefined;
|
||||
|
||||
// Act
|
||||
const result = jwtService.extractTokenFromCookies(cookies);
|
||||
|
||||
// Assert
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCookieOptions', () => {
|
||||
it('should return secure cookie options in production', () => {
|
||||
// Arrange
|
||||
process.env.NODE_ENV = 'production';
|
||||
jwtService = new JwtService();
|
||||
|
||||
// Act
|
||||
const options = jwtService.getCookieOptions();
|
||||
|
||||
// Assert
|
||||
expect(options.httpOnly).toBe(true);
|
||||
expect(options.secure).toBe(true);
|
||||
expect(options.sameSite).toBe('strict');
|
||||
expect(options.path).toBe('/');
|
||||
expect(options.maxAge).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should return non-secure cookie options in development', () => {
|
||||
// Arrange
|
||||
process.env.NODE_ENV = 'development';
|
||||
jwtService = new JwtService();
|
||||
|
||||
// Act
|
||||
const options = jwtService.getCookieOptions();
|
||||
|
||||
// Assert
|
||||
expect(options.httpOnly).toBe(true);
|
||||
expect(options.secure).toBe(false);
|
||||
expect(options.sameSite).toBe('strict');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCookieName', () => {
|
||||
it('should return the cookie name', () => {
|
||||
// Act
|
||||
const name = jwtService.getCookieName();
|
||||
|
||||
// Assert
|
||||
expect(name).toBe('auth_token');
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractTokenFromHeader (legacy)', () => {
|
||||
it('should extract token from valid Authorization header', () => {
|
||||
// Arrange
|
||||
const authHeader = 'Bearer abc123xyz';
|
||||
|
||||
// Act
|
||||
const result = jwtService.extractTokenFromHeader(authHeader);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe('abc123xyz');
|
||||
});
|
||||
|
||||
it('should return null if Authorization header is missing', () => {
|
||||
// Arrange
|
||||
const authHeader = null;
|
||||
|
||||
// Act
|
||||
const result = jwtService.extractTokenFromHeader(authHeader);
|
||||
|
||||
// Assert
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null if Authorization header does not start with Bearer', () => {
|
||||
// Arrange
|
||||
const authHeader = 'Basic abc123xyz';
|
||||
|
||||
// Act
|
||||
const result = jwtService.extractTokenFromHeader(authHeader);
|
||||
|
||||
// Assert
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user