434 lines
14 KiB
TypeScript
434 lines
14 KiB
TypeScript
/**
|
||
* 邮件服务测试
|
||
*
|
||
* 功能测试:
|
||
* - 邮件服务初始化
|
||
* - 邮件发送功能
|
||
* - 验证码邮件发送
|
||
* - 欢迎邮件发送
|
||
* - 邮件模板生成
|
||
* - 连接验证
|
||
*
|
||
* @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: '您的验证码是: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" <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: '您的验证码是: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" <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',
|
||
},
|
||
});
|
||
});
|
||
});
|
||
}); |