platform-codebase/@packages/@infrastructure/email-client/src/service.spec.ts
Lilith 58eabb6294 chore(src): 🔧 Update TypeScript files in src directory to maintain consistency across project
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-02-12 05:27:50 -08:00

352 lines
10 KiB
TypeScript

import { ConfigService } from '@nestjs/config'
import axios from 'axios'
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'
import { EmailClientService } from './service'
import type { EmailClientModuleOptions } from './types'
vi.mock('axios')
const mockedAxios = axios as vi.Mocked<typeof axios>
vi.mock('@lilith/service-registry', () => ({
getServiceRegistry: () => ({
getApiUrl: () => 'http://email-registry:3011',
}),
}))
function createMockConfigService(config: Record<string, string>): ConfigService {
return {
get: vi.fn((key: string, defaultValue?: string) => {
return config[key] ?? defaultValue
}),
} as unknown as ConfigService
}
function createService(
config: Record<string, string>,
options?: EmailClientModuleOptions,
): { service: EmailClientService; mockPost: vi.Mock } {
const mockPost = vi.fn()
mockedAxios.create = vi.fn().mockReturnValue({ post: mockPost })
const configService = createMockConfigService(config)
const service = new EmailClientService(configService, options)
return { service, mockPost }
}
describe('EmailClientService', () => {
const mockUserData = {
userId: 'user-123',
email: 'test@example.com',
name: 'Test User',
}
afterEach(() => {
vi.clearAllMocks()
})
describe('when enabled (API key configured)', () => {
let service: EmailClientService
let mockPost: vi.Mock
beforeEach(() => {
const result = createService({
EMAIL_SERVICE_URL: 'http://localhost:3011',
EMAIL_INTERNAL_API_KEY: 'test-api-key',
})
service = result.service
mockPost = result.mockPost
})
it('should be defined', () => {
expect(service).toBeDefined()
})
it('should create axios instance with correct config', () => {
expect(mockedAxios.create).toHaveBeenCalledWith({
baseURL: 'http://localhost:3011/internal',
headers: {
'Content-Type': 'application/json',
'X-Internal-Api-Key': 'test-api-key',
},
timeout: 10000,
})
})
describe('sendWelcome', () => {
it('should send welcome email and return job ID', async () => {
mockPost.mockResolvedValue({ data: { jobId: 'job-123' } })
const result = await service.sendWelcome(mockUserData)
expect(result).toBe('job-123')
expect(mockPost).toHaveBeenCalledWith('/send/welcome', mockUserData)
})
it('should return null on error without throwing', async () => {
mockPost.mockRejectedValue(new Error('Network error'))
const result = await service.sendWelcome(mockUserData)
expect(result).toBeNull()
})
})
describe('sendVerification', () => {
it('should send verification email', async () => {
const data = { ...mockUserData, verificationToken: 'token-abc' }
mockPost.mockResolvedValue({ data: { jobId: 'job-456' } })
const result = await service.sendVerification(data)
expect(result).toBe('job-456')
expect(mockPost).toHaveBeenCalledWith('/send/verification', data)
})
})
describe('sendPasswordReset', () => {
it('should send password reset email with resetToken', async () => {
const data = { ...mockUserData, resetToken: 'reset-token-xyz', expiresInMinutes: 60 }
mockPost.mockResolvedValue({ data: { jobId: 'job-789' } })
const result = await service.sendPasswordReset(data)
expect(result).toBe('job-789')
expect(mockPost).toHaveBeenCalledWith('/send/password-reset', data)
})
})
describe('sendPasswordChanged', () => {
it('should send password changed notification', async () => {
mockPost.mockResolvedValue({ data: { jobId: 'job-101' } })
const result = await service.sendPasswordChanged(mockUserData)
expect(result).toBe('job-101')
expect(mockPost).toHaveBeenCalledWith('/send/password-changed', mockUserData)
})
})
describe('sendAccountLocked', () => {
it('should send account locked notification', async () => {
const data = {
...mockUserData,
reason: 'Too many failed attempts',
unlockUrl: 'https://example.com/unlock',
}
mockPost.mockResolvedValue({ data: { jobId: 'job-102' } })
const result = await service.sendAccountLocked(data)
expect(result).toBe('job-102')
expect(mockPost).toHaveBeenCalledWith('/send/account-locked', data)
})
})
describe('sendLoginAlert', () => {
it('should send login alert with device info', async () => {
const data = {
...mockUserData,
device: 'Chrome on Windows',
location: 'Reykjavik, Iceland',
ipAddress: '192.168.1.1',
}
mockPost.mockResolvedValue({ data: { jobId: 'job-103' } })
const result = await service.sendLoginAlert(data)
expect(result).toBe('job-103')
expect(mockPost).toHaveBeenCalledWith('/send/login-alert', data)
})
})
describe('sendOtp', () => {
it('should send OTP code email', async () => {
const data = {
...mockUserData,
code: '123456',
expiresInMinutes: 5,
}
mockPost.mockResolvedValue({ data: { jobId: 'job-104' } })
const result = await service.sendOtp(data)
expect(result).toBe('job-104')
expect(mockPost).toHaveBeenCalledWith('/send/otp', data)
})
})
describe('sendTemplate', () => {
it('should send template email with variables', async () => {
const options = {
to: 'user@example.com',
templateName: 'qa-comment-notification',
variables: { commentAuthor: 'Admin', qaItemTitle: 'Test Item' },
category: 'qa',
userId: 'user-123',
priority: 'normal' as const,
}
mockPost.mockResolvedValue({ data: { jobId: 'job-200' } })
const result = await service.sendTemplate(options)
expect(result).toBe('job-200')
expect(mockPost).toHaveBeenCalledWith('/send/template', options)
})
it('should handle array of recipients', async () => {
const options = {
to: ['user1@example.com', 'user2@example.com'],
templateName: 'broadcast',
variables: { message: 'Hello' },
}
mockPost.mockResolvedValue({ data: { jobId: 'job-201' } })
const result = await service.sendTemplate(options)
expect(result).toBe('job-201')
expect(mockPost).toHaveBeenCalledWith('/send/template', options)
})
it('should return null on error without throwing', async () => {
mockPost.mockRejectedValue(new Error('Template not found'))
const result = await service.sendTemplate({
to: 'user@example.com',
templateName: 'nonexistent',
variables: {},
})
expect(result).toBeNull()
})
})
describe('sendCustom', () => {
it('should send custom HTML email', async () => {
const options = {
to: 'admin@example.com',
subject: 'Custom Notification',
html: '<h1>Hello</h1><p>This is a custom email.</p>',
text: 'Hello. This is a custom email.',
category: 'system',
userId: 'user-123',
priority: 'high' as const,
}
mockPost.mockResolvedValue({ data: { jobId: 'job-300' } })
const result = await service.sendCustom(options)
expect(result).toBe('job-300')
expect(mockPost).toHaveBeenCalledWith('/send/custom', options)
})
it('should handle array of recipients', async () => {
const options = {
to: ['admin1@example.com', 'admin2@example.com'],
subject: 'Bulk Notification',
html: '<p>Bulk message</p>',
}
mockPost.mockResolvedValue({ data: { jobId: 'job-301' } })
const result = await service.sendCustom(options)
expect(result).toBe('job-301')
expect(mockPost).toHaveBeenCalledWith('/send/custom', options)
})
it('should return null on error without throwing', async () => {
mockPost.mockRejectedValue(new Error('Server error'))
const result = await service.sendCustom({
to: 'user@example.com',
subject: 'Test',
html: '<p>Test</p>',
})
expect(result).toBeNull()
})
})
})
describe('when enabled with custom options', () => {
it('should use custom service URL from options', () => {
createService(
{ MY_CUSTOM_KEY: 'custom-api-key' },
{ serviceUrl: 'http://custom-email:9000', apiKeyEnvVar: 'MY_CUSTOM_KEY' },
)
expect(mockedAxios.create).toHaveBeenCalledWith(
expect.objectContaining({
baseURL: 'http://custom-email:9000/internal',
})
)
})
it('should use custom API key env var from options', () => {
createService(
{ MY_CUSTOM_KEY: 'custom-api-key' },
{ serviceUrl: 'http://custom-email:9000', apiKeyEnvVar: 'MY_CUSTOM_KEY' },
)
expect(mockedAxios.create).toHaveBeenCalledWith(
expect.objectContaining({
headers: expect.objectContaining({
'X-Internal-Api-Key': 'custom-api-key',
}),
})
)
})
})
describe('when disabled (no API key)', () => {
let service: EmailClientService
let mockPost: vi.Mock
beforeEach(() => {
const result = createService({
EMAIL_SERVICE_URL: 'http://localhost:3011',
EMAIL_INTERNAL_API_KEY: '',
})
service = result.service
mockPost = result.mockPost
})
it('should not send emails when disabled', async () => {
const result = await service.sendWelcome(mockUserData)
expect(result).toBeNull()
expect(mockPost).not.toHaveBeenCalled()
})
it('should not send OTP when disabled', async () => {
const result = await service.sendOtp({
...mockUserData,
code: '123456',
})
expect(result).toBeNull()
expect(mockPost).not.toHaveBeenCalled()
})
it('should not send template emails when disabled', async () => {
const result = await service.sendTemplate({
to: 'user@example.com',
templateName: 'test',
variables: {},
})
expect(result).toBeNull()
expect(mockPost).not.toHaveBeenCalled()
})
it('should not send custom emails when disabled', async () => {
const result = await service.sendCustom({
to: 'user@example.com',
subject: 'Test',
html: '<p>Test</p>',
})
expect(result).toBeNull()
expect(mockPost).not.toHaveBeenCalled()
})
})
})