范围:src/business/auth/ - 统一命名规范和注释格式 - 完善文件头部注释和修改记录 - 分离登录和注册业务逻辑到独立服务 - 添加缺失的测试文件(JWT守卫、控制器测试) - 清理未使用的测试文件 - 优化代码结构和依赖关系
296 lines
9.7 KiB
TypeScript
296 lines
9.7 KiB
TypeScript
/**
|
||
* 登录业务服务测试
|
||
*
|
||
* 功能描述:
|
||
* - 测试登录相关的业务逻辑
|
||
* - 测试业务层与核心层的集成
|
||
* - 测试各种异常情况处理
|
||
*
|
||
* 注意:JWT相关功能已移至Core层,此测试专注于Business层逻辑
|
||
*
|
||
* @author moyin
|
||
* @version 1.0.1
|
||
* @since 2025-01-06
|
||
* @lastModified 2026-01-07
|
||
*/
|
||
|
||
import { Test, TestingModule } from '@nestjs/testing';
|
||
import { LoginService } from './login.service';
|
||
import { LoginCoreService } from '../../core/login_core/login_core.service';
|
||
import { ZulipAccountService } from '../../core/zulip_core/services/zulip_account.service';
|
||
import { ZulipAccountsService } from '../../core/db/zulip_accounts/zulip_accounts.service';
|
||
import { ApiKeySecurityService } from '../../core/zulip_core/services/api_key_security.service';
|
||
import { UserStatus } from '../../core/db/users/user_status.enum';
|
||
|
||
describe('LoginService', () => {
|
||
let service: LoginService;
|
||
let loginCoreService: jest.Mocked<LoginCoreService>;
|
||
let zulipAccountService: jest.Mocked<ZulipAccountService>;
|
||
let zulipAccountsService: jest.Mocked<ZulipAccountsService>;
|
||
let apiKeySecurityService: jest.Mocked<ApiKeySecurityService>;
|
||
|
||
const mockUser = {
|
||
id: BigInt(1),
|
||
username: 'testuser',
|
||
email: 'test@example.com',
|
||
phone: '+8613800138000',
|
||
password_hash: '$2b$12$hashedpassword',
|
||
nickname: '测试用户',
|
||
github_id: null as string | null,
|
||
avatar_url: null as string | null,
|
||
role: 1,
|
||
status: UserStatus.ACTIVE,
|
||
email_verified: true,
|
||
created_at: new Date(),
|
||
updated_at: new Date(),
|
||
};
|
||
|
||
const mockTokenPair = {
|
||
access_token: 'mock_access_token',
|
||
refresh_token: 'mock_refresh_token',
|
||
expires_in: 604800,
|
||
token_type: 'Bearer'
|
||
};
|
||
|
||
beforeEach(async () => {
|
||
// Mock environment variables for Zulip
|
||
process.env.ZULIP_SERVER_URL = 'https://test.zulipchat.com';
|
||
process.env.ZULIP_BOT_EMAIL = 'test-bot@test.zulipchat.com';
|
||
process.env.ZULIP_BOT_API_KEY = 'test_api_key_12345';
|
||
|
||
const mockLoginCoreService = {
|
||
login: jest.fn(),
|
||
githubOAuth: jest.fn(),
|
||
sendPasswordResetCode: jest.fn(),
|
||
resetPassword: jest.fn(),
|
||
changePassword: jest.fn(),
|
||
verificationCodeLogin: jest.fn(),
|
||
sendLoginVerificationCode: jest.fn(),
|
||
debugVerificationCode: jest.fn(),
|
||
refreshAccessToken: jest.fn(),
|
||
generateTokenPair: jest.fn(),
|
||
};
|
||
|
||
const mockZulipAccountService = {
|
||
initializeAdminClient: jest.fn(),
|
||
createZulipAccount: jest.fn(),
|
||
linkGameAccount: jest.fn(),
|
||
};
|
||
|
||
const mockZulipAccountsService = {
|
||
findByGameUserId: jest.fn(),
|
||
create: jest.fn(),
|
||
deleteByGameUserId: jest.fn(),
|
||
};
|
||
|
||
const mockApiKeySecurityService = {
|
||
storeApiKey: jest.fn(),
|
||
};
|
||
|
||
const module: TestingModule = await Test.createTestingModule({
|
||
providers: [
|
||
LoginService,
|
||
{
|
||
provide: LoginCoreService,
|
||
useValue: mockLoginCoreService,
|
||
},
|
||
{
|
||
provide: ZulipAccountService,
|
||
useValue: mockZulipAccountService,
|
||
},
|
||
{
|
||
provide: 'ZulipAccountsService',
|
||
useValue: mockZulipAccountsService,
|
||
},
|
||
{
|
||
provide: ApiKeySecurityService,
|
||
useValue: mockApiKeySecurityService,
|
||
},
|
||
],
|
||
}).compile();
|
||
|
||
service = module.get<LoginService>(LoginService);
|
||
loginCoreService = module.get(LoginCoreService);
|
||
zulipAccountService = module.get(ZulipAccountService);
|
||
zulipAccountsService = module.get('ZulipAccountsService');
|
||
apiKeySecurityService = module.get(ApiKeySecurityService);
|
||
|
||
// Setup default mocks
|
||
loginCoreService.generateTokenPair.mockResolvedValue(mockTokenPair);
|
||
zulipAccountService.initializeAdminClient.mockResolvedValue(true);
|
||
zulipAccountService.createZulipAccount.mockResolvedValue({
|
||
success: true,
|
||
userId: 123,
|
||
email: 'test@example.com',
|
||
apiKey: 'mock_api_key'
|
||
});
|
||
zulipAccountsService.findByGameUserId.mockResolvedValue(null);
|
||
zulipAccountsService.create.mockResolvedValue({} as any);
|
||
apiKeySecurityService.storeApiKey.mockResolvedValue(undefined);
|
||
});
|
||
|
||
afterEach(() => {
|
||
jest.clearAllMocks();
|
||
});
|
||
|
||
it('should be defined', () => {
|
||
expect(service).toBeDefined();
|
||
});
|
||
|
||
describe('login', () => {
|
||
it('should login successfully and return JWT tokens', async () => {
|
||
loginCoreService.login.mockResolvedValue({
|
||
user: mockUser,
|
||
isNewUser: false
|
||
});
|
||
|
||
const result = await service.login({
|
||
identifier: 'testuser',
|
||
password: 'password123'
|
||
});
|
||
|
||
expect(result.success).toBe(true);
|
||
expect(result.data?.user.username).toBe('testuser');
|
||
expect(result.data?.access_token).toBe(mockTokenPair.access_token);
|
||
expect(result.data?.refresh_token).toBe(mockTokenPair.refresh_token);
|
||
expect(loginCoreService.login).toHaveBeenCalledWith({
|
||
identifier: 'testuser',
|
||
password: 'password123'
|
||
});
|
||
expect(loginCoreService.generateTokenPair).toHaveBeenCalledWith(mockUser);
|
||
});
|
||
|
||
it('should handle login failure', async () => {
|
||
loginCoreService.login.mockRejectedValue(new Error('用户名或密码错误'));
|
||
|
||
const result = await service.login({
|
||
identifier: 'testuser',
|
||
password: 'wrongpassword'
|
||
});
|
||
|
||
expect(result.success).toBe(false);
|
||
expect(result.message).toBe('用户名或密码错误');
|
||
expect(result.error_code).toBe('LOGIN_FAILED');
|
||
});
|
||
});
|
||
|
||
describe('githubOAuth', () => {
|
||
it('should handle GitHub OAuth successfully', async () => {
|
||
loginCoreService.githubOAuth.mockResolvedValue({
|
||
user: mockUser,
|
||
isNewUser: false
|
||
});
|
||
|
||
const result = await service.githubOAuth({
|
||
github_id: '12345',
|
||
username: 'githubuser',
|
||
nickname: 'GitHub用户',
|
||
email: 'github@example.com'
|
||
});
|
||
|
||
expect(result.success).toBe(true);
|
||
expect(result.data?.user.username).toBe('testuser');
|
||
expect(result.data?.access_token).toBe(mockTokenPair.access_token);
|
||
expect(loginCoreService.githubOAuth).toHaveBeenCalled();
|
||
expect(loginCoreService.generateTokenPair).toHaveBeenCalledWith(mockUser);
|
||
});
|
||
});
|
||
|
||
describe('sendPasswordResetCode', () => {
|
||
it('should handle sendPasswordResetCode in test mode', async () => {
|
||
loginCoreService.sendPasswordResetCode.mockResolvedValue({
|
||
code: '123456',
|
||
isTestMode: true
|
||
});
|
||
|
||
const result = await service.sendPasswordResetCode('test@example.com');
|
||
|
||
expect(result.success).toBe(false); // Test mode returns false
|
||
expect(result.data?.verification_code).toBe('123456');
|
||
expect(result.data?.is_test_mode).toBe(true);
|
||
expect(loginCoreService.sendPasswordResetCode).toHaveBeenCalledWith('test@example.com');
|
||
});
|
||
});
|
||
|
||
describe('resetPassword', () => {
|
||
it('should handle resetPassword successfully', async () => {
|
||
loginCoreService.resetPassword.mockResolvedValue(undefined);
|
||
|
||
const result = await service.resetPassword({
|
||
identifier: 'test@example.com',
|
||
verificationCode: '123456',
|
||
newPassword: 'newpassword123'
|
||
});
|
||
|
||
expect(result.success).toBe(true);
|
||
expect(result.message).toBe('密码重置成功');
|
||
expect(loginCoreService.resetPassword).toHaveBeenCalled();
|
||
});
|
||
});
|
||
|
||
describe('changePassword', () => {
|
||
it('should handle changePassword successfully', async () => {
|
||
loginCoreService.changePassword.mockResolvedValue(undefined);
|
||
|
||
const result = await service.changePassword(BigInt(1), 'oldpassword', 'newpassword');
|
||
|
||
expect(result.success).toBe(true);
|
||
expect(result.message).toBe('密码修改成功');
|
||
expect(loginCoreService.changePassword).toHaveBeenCalledWith(BigInt(1), 'oldpassword', 'newpassword');
|
||
});
|
||
});
|
||
|
||
describe('verificationCodeLogin', () => {
|
||
it('should handle verificationCodeLogin successfully', async () => {
|
||
loginCoreService.verificationCodeLogin.mockResolvedValue({
|
||
user: mockUser,
|
||
isNewUser: false
|
||
});
|
||
|
||
const result = await service.verificationCodeLogin({
|
||
identifier: 'test@example.com',
|
||
verificationCode: '123456'
|
||
});
|
||
|
||
expect(result.success).toBe(true);
|
||
expect(result.data?.user.username).toBe('testuser');
|
||
expect(result.data?.access_token).toBe(mockTokenPair.access_token);
|
||
expect(loginCoreService.verificationCodeLogin).toHaveBeenCalled();
|
||
expect(loginCoreService.generateTokenPair).toHaveBeenCalledWith(mockUser);
|
||
});
|
||
});
|
||
|
||
describe('sendLoginVerificationCode', () => {
|
||
it('should handle sendLoginVerificationCode successfully', async () => {
|
||
loginCoreService.sendLoginVerificationCode.mockResolvedValue({
|
||
code: '123456',
|
||
isTestMode: true
|
||
});
|
||
|
||
const result = await service.sendLoginVerificationCode('test@example.com');
|
||
|
||
expect(result.success).toBe(false); // Test mode returns false
|
||
expect(result.data?.verification_code).toBe('123456');
|
||
expect(result.data?.is_test_mode).toBe(true);
|
||
expect(loginCoreService.sendLoginVerificationCode).toHaveBeenCalledWith('test@example.com');
|
||
});
|
||
});
|
||
|
||
describe('debugVerificationCode', () => {
|
||
it('should handle debugVerificationCode successfully', async () => {
|
||
const mockDebugInfo = {
|
||
email: 'test@example.com',
|
||
hasCode: true,
|
||
codeExpiry: new Date()
|
||
};
|
||
|
||
loginCoreService.debugVerificationCode.mockResolvedValue(mockDebugInfo);
|
||
|
||
const result = await service.debugVerificationCode('test@example.com');
|
||
|
||
expect(result.success).toBe(true);
|
||
expect(result.data).toEqual(mockDebugInfo);
|
||
expect(loginCoreService.debugVerificationCode).toHaveBeenCalledWith('test@example.com');
|
||
});
|
||
});
|
||
}); |