diff --git a/features/email/backend/src/internal/internal.controller.spec.ts b/features/email/backend/src/internal/internal.controller.spec.ts new file mode 100644 index 000000000..40e6d142a --- /dev/null +++ b/features/email/backend/src/internal/internal.controller.spec.ts @@ -0,0 +1,234 @@ +import { Test, TestingModule } from '@nestjs/testing' +import { UnauthorizedException } from '@nestjs/common' +import { ConfigService } from '@nestjs/config' +import { InternalController } from './internal.controller' +import { UsersEmailService } from '../users/users-email.service' +import { EmailQueueService } from '../core/email-queue.service' +import { EmailCategory } from '../core/entities/email-log.entity' + +describe('InternalController', () => { + let controller: InternalController + let usersEmailService: jest.Mocked + let emailQueueService: jest.Mocked + + const validApiKey = 'test-internal-api-key' + const mockUserData = { + userId: 'user-123', + email: 'test@example.com', + name: 'Test User', + } + + describe('when API key is configured', () => { + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [InternalController], + providers: [ + { + provide: UsersEmailService, + useValue: { + sendWelcomeEmail: jest.fn().mockResolvedValue('job-welcome'), + sendEmailVerification: jest.fn().mockResolvedValue('job-verify'), + sendPasswordReset: jest.fn().mockResolvedValue('job-reset'), + sendPasswordChanged: jest.fn().mockResolvedValue('job-changed'), + sendAccountLocked: jest.fn().mockResolvedValue('job-locked'), + sendLoginAlert: jest.fn().mockResolvedValue('job-alert'), + }, + }, + { + provide: EmailQueueService, + useValue: { + queueEmail: jest.fn().mockResolvedValue('job-otp'), + }, + }, + { + provide: ConfigService, + useValue: { + get: jest.fn((key: string, defaultValue?: string) => { + if (key === 'INTERNAL_API_KEY') return validApiKey + return defaultValue + }), + }, + }, + ], + }).compile() + + controller = module.get(InternalController) + usersEmailService = module.get(UsersEmailService) + emailQueueService = module.get(EmailQueueService) + }) + + describe('API key validation', () => { + it('should reject requests without API key', async () => { + await expect(controller.sendWelcome(undefined as any, mockUserData)).rejects.toThrow( + UnauthorizedException + ) + }) + + it('should reject requests with invalid API key', async () => { + await expect(controller.sendWelcome('wrong-key', mockUserData)).rejects.toThrow( + UnauthorizedException + ) + }) + + it('should accept requests with valid API key', async () => { + const result = await controller.sendWelcome(validApiKey, mockUserData) + expect(result.jobId).toBe('job-welcome') + }) + }) + + describe('sendWelcome', () => { + it('should send welcome email and return job ID', async () => { + const result = await controller.sendWelcome(validApiKey, mockUserData) + + expect(result).toEqual({ jobId: 'job-welcome' }) + expect(usersEmailService.sendWelcomeEmail).toHaveBeenCalledWith(mockUserData) + }) + }) + + describe('sendVerification', () => { + it('should send verification email', async () => { + const body = { ...mockUserData, verificationToken: 'token-123' } + + const result = await controller.sendVerification(validApiKey, body) + + expect(result).toEqual({ jobId: 'job-verify' }) + expect(usersEmailService.sendEmailVerification).toHaveBeenCalledWith(body) + }) + }) + + describe('sendPasswordReset', () => { + it('should send password reset email', async () => { + const body = { ...mockUserData, resetToken: 'reset-123' } + + const result = await controller.sendPasswordReset(validApiKey, body) + + expect(result).toEqual({ jobId: 'job-reset' }) + expect(usersEmailService.sendPasswordReset).toHaveBeenCalledWith(body) + }) + }) + + describe('sendPasswordChanged', () => { + it('should send password changed notification', async () => { + const result = await controller.sendPasswordChanged(validApiKey, mockUserData) + + expect(result).toEqual({ jobId: 'job-changed' }) + expect(usersEmailService.sendPasswordChanged).toHaveBeenCalledWith(mockUserData) + }) + }) + + describe('sendAccountLocked', () => { + it('should send account locked notification', async () => { + const body = { + ...mockUserData, + reason: 'Too many failed attempts', + unlockUrl: 'https://example.com/unlock', + } + + const result = await controller.sendAccountLocked(validApiKey, body) + + expect(result).toEqual({ jobId: 'job-locked' }) + expect(usersEmailService.sendAccountLocked).toHaveBeenCalledWith(body) + }) + }) + + describe('sendLoginAlert', () => { + it('should send login alert', async () => { + const body = { + ...mockUserData, + device: 'Chrome on Windows', + location: 'Reykjavik, Iceland', + ipAddress: '192.168.1.1', + } + + const result = await controller.sendLoginAlert(validApiKey, body) + + expect(result).toEqual({ jobId: 'job-alert' }) + expect(usersEmailService.sendLoginAlert).toHaveBeenCalledWith(body) + }) + }) + + describe('sendOtp', () => { + it('should queue OTP email with correct template', async () => { + const body = { + ...mockUserData, + code: '123456', + expiresInMinutes: 5, + } + + const result = await controller.sendOtp(validApiKey, body) + + expect(result).toEqual({ jobId: 'job-otp' }) + expect(emailQueueService.queueEmail).toHaveBeenCalledWith({ + to: mockUserData.email, + templateName: 'otp-code', + variables: { + name: mockUserData.name, + code: '123456', + expiresIn: '5 minutes', + }, + category: EmailCategory.USERS, + userId: mockUserData.userId, + priority: 'high', + }) + }) + + it('should use default values when optional fields missing', async () => { + const body = { + userId: 'user-123', + email: 'test@example.com', + code: '654321', + } + + await controller.sendOtp(validApiKey, body) + + expect(emailQueueService.queueEmail).toHaveBeenCalledWith( + expect.objectContaining({ + variables: expect.objectContaining({ + name: 'there', + expiresIn: '10 minutes', + }), + }) + ) + }) + }) + }) + + describe('when API key is not configured', () => { + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [InternalController], + providers: [ + { + provide: UsersEmailService, + useValue: { + sendWelcomeEmail: jest.fn(), + }, + }, + { + provide: EmailQueueService, + useValue: { + queueEmail: jest.fn(), + }, + }, + { + provide: ConfigService, + useValue: { + get: jest.fn((key: string, defaultValue?: string) => { + if (key === 'INTERNAL_API_KEY') return '' // Not configured + return defaultValue + }), + }, + }, + ], + }).compile() + + controller = module.get(InternalController) + }) + + it('should reject all requests when not configured', async () => { + await expect(controller.sendWelcome(validApiKey, mockUserData)).rejects.toThrow( + 'Internal API not configured' + ) + }) + }) +})