/** * 邮件服务测试 * * 功能测试: * - 邮件服务初始化 * - 邮件发送功能 * - 验证码邮件发送 * - 欢迎邮件发送 * - 邮件模板生成 * - 连接验证 * * @author moyin * @version 1.0.0 * @since 2025-12-17 */ import { Test, TestingModule } from '@nestjs/testing'; import { ConfigService } from '@nestjs/config'; import { EmailService, EmailOptions, VerificationEmailOptions } from './email.service'; import * as nodemailer from 'nodemailer'; // Mock nodemailer jest.mock('nodemailer'); const mockedNodemailer = nodemailer as jest.Mocked; describe('EmailService', () => { let service: EmailService; let configService: jest.Mocked; let mockTransporter: any; beforeEach(async () => { // 创建 mock transporter mockTransporter = { sendMail: jest.fn(), verify: jest.fn(), options: {} }; // Mock ConfigService const mockConfigService = { get: jest.fn(), }; // Mock nodemailer.createTransport mockedNodemailer.createTransport.mockReturnValue(mockTransporter); const module: TestingModule = await Test.createTestingModule({ providers: [ EmailService, { provide: ConfigService, useValue: mockConfigService, }, ], }).compile(); service = module.get(EmailService); configService = module.get(ConfigService); }); afterEach(() => { jest.clearAllMocks(); }); describe('初始化测试', () => { it('应该正确初始化邮件服务', () => { expect(service).toBeDefined(); expect(mockedNodemailer.createTransport).toHaveBeenCalled(); }); it('应该在没有配置时使用测试模式', () => { configService.get.mockReturnValue(undefined); // 重新创建服务实例来测试测试模式 new EmailService(configService); expect(mockedNodemailer.createTransport).toHaveBeenCalledWith({ streamTransport: true, newline: 'unix', buffer: true }); }); it('应该在有配置时使用真实SMTP', () => { configService.get .mockReturnValueOnce('smtp.gmail.com') // EMAIL_HOST .mockReturnValueOnce(587) // EMAIL_PORT .mockReturnValueOnce(false) // EMAIL_SECURE .mockReturnValueOnce('test@gmail.com') // EMAIL_USER .mockReturnValueOnce('password'); // EMAIL_PASS new EmailService(configService); expect(mockedNodemailer.createTransport).toHaveBeenCalledWith({ host: 'smtp.gmail.com', port: 587, secure: false, auth: { user: 'test@gmail.com', pass: 'password', }, }); }); }); describe('sendEmail', () => { it('应该成功发送邮件', async () => { const emailOptions: EmailOptions = { to: 'test@example.com', subject: '测试邮件', html: '

测试内容

', text: '测试内容' }; mockTransporter.sendMail.mockResolvedValue({ messageId: 'test-id' }); configService.get.mockReturnValue('"Test Sender" '); const result = await service.sendEmail(emailOptions); expect(result.success).toBe(true); expect(result.isTestMode).toBe(false); expect(mockTransporter.sendMail).toHaveBeenCalledWith({ from: '"Test Sender" ', to: 'test@example.com', subject: '测试邮件', html: '

测试内容

', text: '测试内容', }); }); it('应该在发送失败时返回false', async () => { const emailOptions: EmailOptions = { to: 'test@example.com', subject: '测试邮件', html: '

测试内容

' }; mockTransporter.sendMail.mockRejectedValue(new Error('发送失败')); const result = await service.sendEmail(emailOptions); expect(result.success).toBe(false); expect(result.error).toBe('发送失败'); }); it('应该在测试模式下输出邮件内容', async () => { const emailOptions: EmailOptions = { to: 'test@example.com', subject: '测试邮件', html: '

测试内容

', text: '测试内容' }; // Mock transporter with streamTransport option const testTransporter = { ...mockTransporter, options: { streamTransport: true }, sendMail: jest.fn().mockResolvedValue({ messageId: 'test-id' }) }; // Mock the service to use test transporter const loggerSpy = jest.spyOn(service['logger'], 'warn').mockImplementation(); service['transporter'] = testTransporter; const result = await service.sendEmail(emailOptions); expect(result.success).toBe(true); expect(result.isTestMode).toBe(true); expect(loggerSpy).toHaveBeenCalledWith('=== 邮件发送(测试模式 - 邮件未真实发送) ==='); loggerSpy.mockRestore(); }); }); describe('sendVerificationCode', () => { it('应该成功发送邮箱验证码', async () => { const options: VerificationEmailOptions = { email: 'test@example.com', code: '123456', nickname: '测试用户', purpose: 'email_verification' }; mockTransporter.sendMail.mockResolvedValue({ messageId: 'test-id' }); configService.get.mockReturnValue('"Test Sender" '); const result = await service.sendVerificationCode(options); expect(result.success).toBe(true); expect(result.isTestMode).toBe(false); expect(mockTransporter.sendMail).toHaveBeenCalledWith( expect.objectContaining({ to: 'test@example.com', subject: '【Whale Town】邮箱验证码', text: '您的验证码是:123456,5分钟内有效,请勿泄露给他人。' }) ); }); it('应该成功发送密码重置验证码', async () => { const options: VerificationEmailOptions = { email: 'test@example.com', code: '654321', nickname: '测试用户', purpose: 'password_reset' }; mockTransporter.sendMail.mockResolvedValue({ messageId: 'test-id' }); configService.get.mockReturnValue('"Test Sender" '); const result = await service.sendVerificationCode(options); expect(result.success).toBe(true); expect(result.isTestMode).toBe(false); expect(mockTransporter.sendMail).toHaveBeenCalledWith( expect.objectContaining({ to: 'test@example.com', subject: '【Whale Town】密码重置验证码', text: '您的验证码是:654321,5分钟内有效,请勿泄露给他人。' }) ); }); it('应该在发送失败时返回false', async () => { const options: VerificationEmailOptions = { email: 'test@example.com', code: '123456', purpose: 'email_verification' }; mockTransporter.sendMail.mockRejectedValue(new Error('发送失败')); const result = await service.sendVerificationCode(options); expect(result.success).toBe(false); expect(result.error).toBe('发送失败'); }); }); describe('sendWelcomeEmail', () => { it('应该成功发送欢迎邮件', async () => { mockTransporter.sendMail.mockResolvedValue({ messageId: 'test-id' }); configService.get.mockReturnValue('"Test Sender" '); const result = await service.sendWelcomeEmail('test@example.com', '测试用户'); expect(result.success).toBe(true); expect(result.isTestMode).toBe(false); expect(mockTransporter.sendMail).toHaveBeenCalledWith( expect.objectContaining({ to: 'test@example.com', subject: '🎮 欢迎加入 Whale Town!', text: '欢迎 测试用户 加入 Whale Town 像素游戏世界!' }) ); }); it('应该在发送失败时返回false', async () => { mockTransporter.sendMail.mockRejectedValue(new Error('发送失败')); const result = await service.sendWelcomeEmail('test@example.com', '测试用户'); expect(result.success).toBe(false); expect(result.error).toBe('发送失败'); }); }); describe('verifyConnection', () => { it('应该在连接成功时返回true', async () => { mockTransporter.verify.mockResolvedValue(true); const result = await service.verifyConnection(); expect(result).toBe(true); expect(mockTransporter.verify).toHaveBeenCalled(); }); it('应该在连接失败时返回false', async () => { mockTransporter.verify.mockRejectedValue(new Error('连接失败')); const result = await service.verifyConnection(); expect(result).toBe(false); }); }); describe('邮件模板测试', () => { it('应该生成包含验证码的邮箱验证模板', async () => { const options: VerificationEmailOptions = { email: 'test@example.com', code: '123456', nickname: '测试用户', purpose: 'email_verification' }; mockTransporter.sendMail.mockImplementation((mailOptions: any) => { expect(mailOptions.html).toContain('123456'); expect(mailOptions.html).toContain('测试用户'); expect(mailOptions.html).toContain('邮箱验证'); expect(mailOptions.html).toContain('Whale Town'); return Promise.resolve({ messageId: 'test-id' }); }); await service.sendVerificationCode(options); }); it('应该生成包含验证码的密码重置模板', async () => { const options: VerificationEmailOptions = { email: 'test@example.com', code: '654321', nickname: '测试用户', purpose: 'password_reset' }; mockTransporter.sendMail.mockImplementation((mailOptions: any) => { expect(mailOptions.html).toContain('654321'); expect(mailOptions.html).toContain('测试用户'); expect(mailOptions.html).toContain('密码重置'); expect(mailOptions.html).toContain('🔐'); return Promise.resolve({ messageId: 'test-id' }); }); await service.sendVerificationCode(options); }); it('应该生成包含用户昵称的欢迎邮件模板', async () => { mockTransporter.sendMail.mockImplementation((mailOptions: any) => { expect(mailOptions.html).toContain('测试用户'); expect(mailOptions.html).toContain('欢迎加入 Whale Town'); expect(mailOptions.html).toContain('🎮'); expect(mailOptions.html).toContain('建造与创造'); expect(mailOptions.html).toContain('社交互动'); expect(mailOptions.html).toContain('任务挑战'); return Promise.resolve({ messageId: 'test-id' }); }); await service.sendWelcomeEmail('test@example.com', '测试用户'); }); it('应该在没有昵称时正确处理模板', async () => { const options: VerificationEmailOptions = { email: 'test@example.com', code: '123456', purpose: 'email_verification' }; mockTransporter.sendMail.mockImplementation((mailOptions: any) => { expect(mailOptions.html).toContain('你好!'); expect(mailOptions.html).not.toContain('你好 undefined'); return Promise.resolve({ messageId: 'test-id' }); }); await service.sendVerificationCode(options); }); }); describe('错误处理测试', () => { it('应该正确处理网络错误', async () => { const emailOptions: EmailOptions = { to: 'test@example.com', subject: '测试邮件', html: '

测试内容

' }; mockTransporter.sendMail.mockRejectedValue(new Error('ECONNREFUSED')); const result = await service.sendEmail(emailOptions); expect(result.success).toBe(false); expect(result.error).toBe('ECONNREFUSED'); }); it('应该正确处理认证错误', async () => { const emailOptions: EmailOptions = { to: 'test@example.com', subject: '测试邮件', html: '

测试内容

' }; mockTransporter.sendMail.mockRejectedValue(new Error('Invalid login')); const result = await service.sendEmail(emailOptions); expect(result.success).toBe(false); expect(result.error).toBe('Invalid login'); }); it('应该正确处理连接验证错误', async () => { mockTransporter.verify.mockRejectedValue(new Error('Connection timeout')); const result = await service.verifyConnection(); expect(result).toBe(false); }); }); describe('配置测试', () => { it('应该使用默认配置值', () => { configService.get .mockReturnValueOnce(undefined) // EMAIL_HOST .mockReturnValueOnce(undefined) // EMAIL_PORT .mockReturnValueOnce(undefined) // EMAIL_SECURE .mockReturnValueOnce(undefined) // EMAIL_USER .mockReturnValueOnce(undefined); // EMAIL_PASS new EmailService(configService); expect(configService.get).toHaveBeenCalledWith('EMAIL_HOST', 'smtp.gmail.com'); expect(configService.get).toHaveBeenCalledWith('EMAIL_PORT', 587); expect(configService.get).toHaveBeenCalledWith('EMAIL_SECURE', false); }); it('应该使用自定义配置值', () => { configService.get .mockReturnValueOnce('smtp.163.com') // EMAIL_HOST .mockReturnValueOnce(25) // EMAIL_PORT .mockReturnValueOnce(true) // EMAIL_SECURE .mockReturnValueOnce('custom@163.com') // EMAIL_USER .mockReturnValueOnce('custompass'); // EMAIL_PASS new EmailService(configService); expect(mockedNodemailer.createTransport).toHaveBeenCalledWith({ host: 'smtp.163.com', port: 25, secure: true, auth: { user: 'custom@163.com', pass: 'custompass', }, }); }); }); });