Files
whale-town-end/src/core/utils/email/email.service.spec.ts
moyin bb796a2469 refactor:项目架构重构和命名规范化
- 统一文件命名为snake_case格式(kebab-case  snake_case)
- 重构zulip模块为zulip_core,明确Core层职责
- 重构user-mgmt模块为user_mgmt,统一命名规范
- 调整模块依赖关系,优化架构分层
- 删除过时的文件和目录结构
- 更新相关文档和配置文件

本次重构涉及大量文件重命名和模块重组,
旨在建立更清晰的项目架构和统一的命名规范。
2026-01-08 00:14:14 +08:00

487 lines
16 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.
/**
* 邮件服务测试
*
* 功能测试:
* - 邮件服务初始化
* - 邮件发送功能
* - 验证码邮件发送
* - 欢迎邮件发送
* - 邮件模板生成
* - 连接验证
*
* 职责分离:
* - 单元测试:测试各个方法的功能正确性
* - Mock测试模拟外部依赖进行隔离测试
* - 异常测试:验证错误处理机制
*
* 最近修改:
* - 2026-01-07: 代码规范优化 - 完善文件头注释和修改记录
*
* @author moyin
* @version 1.0.1
* @since 2025-12-17
* @lastModified 2026-01-07
*/
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('应该成功发送登录验证码', async () => {
const options: VerificationEmailOptions = {
email: 'test@example.com',
code: '789012',
nickname: '测试用户',
purpose: 'login_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: '您的验证码是7890125分钟内有效请勿泄露给他人。'
})
);
});
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 () => {
const options: VerificationEmailOptions = {
email: 'test@example.com',
code: '789012',
nickname: '测试用户',
purpose: 'login_verification'
};
mockTransporter.sendMail.mockImplementation((mailOptions: any) => {
expect(mailOptions.html).toContain('789012');
expect(mailOptions.html).toContain('测试用户');
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',
},
});
});
});
});