Files
whale-town-end/src/core/utils/email/email.service.spec.ts

434 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 邮件服务测试
*
* 功能测试:
* - 邮件服务初始化
* - 邮件发送功能
* - 验证码邮件发送
* - 欢迎邮件发送
* - 邮件模板生成
* - 连接验证
*
* @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<typeof nodemailer>;
describe('EmailService', () => {
let service: EmailService;
let configService: jest.Mocked<ConfigService>;
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>(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: '<p>测试内容</p>',
text: '测试内容'
};
mockTransporter.sendMail.mockResolvedValue({ messageId: 'test-id' });
configService.get.mockReturnValue('"Test Sender" <noreply@test.com>');
const result = await service.sendEmail(emailOptions);
expect(result.success).toBe(true);
expect(result.isTestMode).toBe(false);
expect(mockTransporter.sendMail).toHaveBeenCalledWith({
from: '"Test Sender" <noreply@test.com>',
to: 'test@example.com',
subject: '测试邮件',
html: '<p>测试内容</p>',
text: '测试内容',
});
});
it('应该在发送失败时返回false', async () => {
const emailOptions: EmailOptions = {
to: 'test@example.com',
subject: '测试邮件',
html: '<p>测试内容</p>'
};
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: '<p>测试内容</p>',
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" <noreply@test.com>');
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: '您的验证码是1234565分钟内有效请勿泄露给他人。'
})
);
});
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" <noreply@test.com>');
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: '您的验证码是6543215分钟内有效请勿泄露给他人。'
})
);
});
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" <noreply@test.com>');
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: '<p>测试内容</p>'
};
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: '<p>测试内容</p>'
};
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',
},
});
});
});
});