CRITICAL ISSUES: Database management service with major problems
WARNING: This commit contains code with significant issues that need immediate attention: 1. Type Safety Issues: - Unused import ZulipAccountsService causing compilation warnings - Implicit 'any' type in formatZulipAccount method parameter - Type inconsistencies in service injections 2. Service Integration Problems: - Inconsistent service interface usage - Missing proper type definitions for injected services - Potential runtime errors due to type mismatches 3. Code Quality Issues: - Violation of TypeScript strict mode requirements - Inconsistent error handling patterns - Missing proper interface implementations Files affected: - src/business/admin/database_management.service.ts (main issue) - Multiple test files and service implementations - Configuration and documentation updates Next steps required: 1. Fix TypeScript compilation errors 2. Implement proper type safety 3. Resolve service injection inconsistencies 4. Add comprehensive error handling 5. Update tests to match new implementations Impact: High - affects admin functionality and system stability Priority: Urgent - requires immediate review and fixes Author: moyin Date: 2026-01-10
This commit is contained in:
@@ -156,7 +156,7 @@ export class LoginController {
|
||||
status: 429,
|
||||
description: '登录尝试过于频繁'
|
||||
})
|
||||
@Throttle(ThrottlePresets.LOGIN)
|
||||
@Throttle(ThrottlePresets.LOGIN_PER_ACCOUNT)
|
||||
@Timeout(TimeoutPresets.NORMAL)
|
||||
@Post('login')
|
||||
@UsePipes(new ValidationPipe({ transform: true }))
|
||||
@@ -408,7 +408,7 @@ export class LoginController {
|
||||
status: 429,
|
||||
description: '发送频率过高'
|
||||
})
|
||||
@Throttle(ThrottlePresets.SEND_CODE)
|
||||
@Throttle(ThrottlePresets.SEND_CODE_PER_EMAIL)
|
||||
@Timeout(TimeoutPresets.EMAIL_SEND)
|
||||
@Post('send-email-verification')
|
||||
@UsePipes(new ValidationPipe({ transform: true }))
|
||||
|
||||
@@ -26,10 +26,15 @@ import { Injectable, Logger, Inject } from '@nestjs/common';
|
||||
import { LoginCoreService, LoginRequest, RegisterRequest, GitHubOAuthRequest, PasswordResetRequest, VerificationCodeLoginRequest, TokenPair } from '../../core/login_core/login_core.service';
|
||||
import { Users } from '../../core/db/users/users.entity';
|
||||
import { ZulipAccountService } from '../../core/zulip_core/services/zulip_account.service';
|
||||
import { ZulipAccountsService } from '../../core/db/zulip_accounts/zulip_accounts.service';
|
||||
import { ZulipAccountsMemoryService } from '../../core/db/zulip_accounts/zulip_accounts_memory.service';
|
||||
import { ApiKeySecurityService } from '../../core/zulip_core/services/api_key_security.service';
|
||||
|
||||
// Import the interface types we need
|
||||
interface IZulipAccountsService {
|
||||
findByGameUserId(gameUserId: string, includeGameUser?: boolean): Promise<any>;
|
||||
create(createDto: any): Promise<any>;
|
||||
deleteByGameUserId(gameUserId: string): Promise<boolean>;
|
||||
}
|
||||
|
||||
// 常量定义
|
||||
const ERROR_CODES = {
|
||||
LOGIN_FAILED: 'LOGIN_FAILED',
|
||||
@@ -120,8 +125,7 @@ export class LoginService {
|
||||
constructor(
|
||||
private readonly loginCoreService: LoginCoreService,
|
||||
private readonly zulipAccountService: ZulipAccountService,
|
||||
@Inject('ZulipAccountsService')
|
||||
private readonly zulipAccountsService: ZulipAccountsService | ZulipAccountsMemoryService,
|
||||
@Inject('ZulipAccountsService') private readonly zulipAccountsService: IZulipAccountsService,
|
||||
private readonly apiKeySecurityService: ApiKeySecurityService,
|
||||
) {}
|
||||
|
||||
@@ -215,25 +219,88 @@ export class LoginService {
|
||||
*/
|
||||
async register(registerRequest: RegisterRequest): Promise<ApiResponse<LoginResponse>> {
|
||||
const startTime = Date.now();
|
||||
const operationId = `register_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
try {
|
||||
this.logger.log(`用户注册尝试: ${registerRequest.username}`);
|
||||
this.logger.log(`[${operationId}] 步骤1: 开始用户注册流程`, {
|
||||
operation: 'register',
|
||||
operationId,
|
||||
username: registerRequest.username,
|
||||
email: registerRequest.email,
|
||||
hasPassword: !!registerRequest.password,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
// 1. 初始化Zulip管理员客户端
|
||||
this.logger.log(`[${operationId}] 步骤2: 开始初始化Zulip管理员客户端`, {
|
||||
operation: 'register',
|
||||
operationId,
|
||||
step: 'initializeZulipAdminClient',
|
||||
});
|
||||
|
||||
await this.initializeZulipAdminClient();
|
||||
|
||||
this.logger.log(`[${operationId}] 步骤2: Zulip管理员客户端初始化成功`, {
|
||||
operation: 'register',
|
||||
operationId,
|
||||
step: 'initializeZulipAdminClient',
|
||||
result: 'success',
|
||||
});
|
||||
|
||||
// 2. 调用核心服务进行注册
|
||||
this.logger.log(`[${operationId}] 步骤3: 开始创建游戏用户账号`, {
|
||||
operation: 'register',
|
||||
operationId,
|
||||
step: 'createGameUser',
|
||||
username: registerRequest.username,
|
||||
});
|
||||
|
||||
const authResult = await this.loginCoreService.register(registerRequest);
|
||||
|
||||
this.logger.log(`[${operationId}] 步骤3: 游戏用户账号创建成功`, {
|
||||
operation: 'register',
|
||||
operationId,
|
||||
step: 'createGameUser',
|
||||
result: 'success',
|
||||
gameUserId: authResult.user.id.toString(),
|
||||
username: authResult.user.username,
|
||||
email: authResult.user.email,
|
||||
});
|
||||
|
||||
// 3. 创建Zulip账号(使用相同的邮箱和密码)
|
||||
let zulipAccountCreated = false;
|
||||
|
||||
if (registerRequest.email && registerRequest.password) {
|
||||
this.logger.log(`[${operationId}] 步骤4: 开始创建/绑定Zulip账号`, {
|
||||
operation: 'register',
|
||||
operationId,
|
||||
step: 'createZulipAccount',
|
||||
gameUserId: authResult.user.id.toString(),
|
||||
email: registerRequest.email,
|
||||
});
|
||||
} else {
|
||||
this.logger.warn(`[${operationId}] 步骤4: 跳过Zulip账号创建(缺少邮箱或密码)`, {
|
||||
operation: 'register',
|
||||
operationId,
|
||||
step: 'createZulipAccount',
|
||||
result: 'skipped',
|
||||
username: registerRequest.username,
|
||||
gameUserId: authResult.user.id.toString(),
|
||||
hasEmail: !!registerRequest.email,
|
||||
hasPassword: !!registerRequest.password,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
if (registerRequest.email && registerRequest.password) {
|
||||
await this.createZulipAccountForUser(authResult.user, registerRequest.password);
|
||||
zulipAccountCreated = true;
|
||||
|
||||
this.logger.log(`Zulip账号创建成功: ${registerRequest.username}`, {
|
||||
this.logger.log(`[${operationId}] 步骤4: Zulip账号创建成功`, {
|
||||
operation: 'register',
|
||||
operationId,
|
||||
step: 'createZulipAccount',
|
||||
result: 'success',
|
||||
gameUserId: authResult.user.id.toString(),
|
||||
email: registerRequest.email,
|
||||
});
|
||||
@@ -247,21 +314,41 @@ export class LoginService {
|
||||
}
|
||||
} catch (zulipError) {
|
||||
const err = zulipError as Error;
|
||||
this.logger.error(`Zulip账号创建失败,回滚用户注册`, {
|
||||
this.logger.error(`[${operationId}] 步骤4: Zulip账号创建失败,开始回滚`, {
|
||||
operation: 'register',
|
||||
operationId,
|
||||
step: 'createZulipAccount',
|
||||
result: 'failed',
|
||||
username: registerRequest.username,
|
||||
gameUserId: authResult.user.id.toString(),
|
||||
zulipError: err.message,
|
||||
}, err.stack);
|
||||
|
||||
// 回滚游戏用户注册
|
||||
this.logger.log(`[${operationId}] 步骤4.1: 开始回滚游戏用户注册`, {
|
||||
operation: 'register',
|
||||
operationId,
|
||||
step: 'rollbackGameUser',
|
||||
gameUserId: authResult.user.id.toString(),
|
||||
});
|
||||
|
||||
try {
|
||||
await this.loginCoreService.deleteUser(authResult.user.id);
|
||||
this.logger.log(`用户注册回滚成功: ${registerRequest.username}`);
|
||||
this.logger.log(`[${operationId}] 步骤4.1: 游戏用户注册回滚成功`, {
|
||||
operation: 'register',
|
||||
operationId,
|
||||
step: 'rollbackGameUser',
|
||||
result: 'success',
|
||||
username: registerRequest.username,
|
||||
gameUserId: authResult.user.id.toString(),
|
||||
});
|
||||
} catch (rollbackError) {
|
||||
const rollbackErr = rollbackError as Error;
|
||||
this.logger.error(`用户注册回滚失败`, {
|
||||
this.logger.error(`[${operationId}] 步骤4.1: 游戏用户注册回滚失败`, {
|
||||
operation: 'register',
|
||||
operationId,
|
||||
step: 'rollbackGameUser',
|
||||
result: 'failed',
|
||||
username: registerRequest.username,
|
||||
gameUserId: authResult.user.id.toString(),
|
||||
rollbackError: rollbackErr.message,
|
||||
@@ -273,9 +360,34 @@ export class LoginService {
|
||||
}
|
||||
|
||||
// 4. 生成JWT令牌对(通过Core层)
|
||||
this.logger.log(`[${operationId}] 步骤5: 开始生成JWT令牌`, {
|
||||
operation: 'register',
|
||||
operationId,
|
||||
step: 'generateTokens',
|
||||
gameUserId: authResult.user.id.toString(),
|
||||
});
|
||||
|
||||
const tokenPair = await this.loginCoreService.generateTokenPair(authResult.user);
|
||||
|
||||
this.logger.log(`[${operationId}] 步骤5: JWT令牌生成成功`, {
|
||||
operation: 'register',
|
||||
operationId,
|
||||
step: 'generateTokens',
|
||||
result: 'success',
|
||||
gameUserId: authResult.user.id.toString(),
|
||||
tokenType: tokenPair.token_type,
|
||||
expiresIn: tokenPair.expires_in,
|
||||
});
|
||||
|
||||
// 5. 格式化响应数据
|
||||
this.logger.log(`[${operationId}] 步骤6: 格式化响应数据`, {
|
||||
operation: 'register',
|
||||
operationId,
|
||||
step: 'formatResponse',
|
||||
gameUserId: authResult.user.id.toString(),
|
||||
zulipAccountCreated,
|
||||
});
|
||||
|
||||
const response: LoginResponse = {
|
||||
user: this.formatUserInfo(authResult.user),
|
||||
access_token: tokenPair.access_token,
|
||||
@@ -288,10 +400,13 @@ export class LoginService {
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
this.logger.log(`用户注册成功: ${authResult.user.username} (ID: ${authResult.user.id})`, {
|
||||
this.logger.log(`[${operationId}] 注册流程完成: 用户注册成功`, {
|
||||
operation: 'register',
|
||||
operationId,
|
||||
result: 'success',
|
||||
gameUserId: authResult.user.id.toString(),
|
||||
username: authResult.user.username,
|
||||
email: authResult.user.email,
|
||||
zulipAccountCreated,
|
||||
duration,
|
||||
timestamp: new Date().toISOString(),
|
||||
@@ -306,9 +421,12 @@ export class LoginService {
|
||||
const duration = Date.now() - startTime;
|
||||
const err = error as Error;
|
||||
|
||||
this.logger.error(`用户注册失败: ${registerRequest.username}`, {
|
||||
this.logger.error(`[${operationId}] 注册流程失败: 用户注册失败`, {
|
||||
operation: 'register',
|
||||
operationId,
|
||||
result: 'failed',
|
||||
username: registerRequest.username,
|
||||
email: registerRequest.email,
|
||||
error: err.message,
|
||||
duration,
|
||||
timestamp: new Date().toISOString(),
|
||||
|
||||
443
src/business/auth/login.service.zulip_integration.spec.ts
Normal file
443
src/business/auth/login.service.zulip_integration.spec.ts
Normal file
@@ -0,0 +1,443 @@
|
||||
/**
|
||||
* 登录服务Zulip集成测试
|
||||
*
|
||||
* 功能描述:
|
||||
* - 测试用户注册时的Zulip账号创建/绑定逻辑
|
||||
* - 测试用户登录时的Zulip集成处理
|
||||
* - 验证API Key的获取和存储机制
|
||||
* - 测试各种异常情况的处理
|
||||
*
|
||||
* 测试场景:
|
||||
* - 注册时Zulip中没有用户:创建新账号
|
||||
* - 注册时Zulip中已有用户:绑定已有账号
|
||||
* - 登录时没有Zulip关联:尝试创建/绑定
|
||||
* - 登录时已有Zulip关联:刷新API Key
|
||||
* - 各种错误情况的处理和回滚
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.0
|
||||
* @since 2026-01-10
|
||||
* @lastModified 2026-01-10
|
||||
*/
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { Logger } from '@nestjs/common';
|
||||
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 { Users } from '../../core/db/users/users.entity';
|
||||
import { ZulipAccountResponseDto } from '../../core/db/zulip_accounts/zulip_accounts.dto';
|
||||
|
||||
describe('LoginService - Zulip Integration', () => {
|
||||
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: Users = {
|
||||
id: BigInt(12345),
|
||||
username: 'testuser',
|
||||
nickname: '测试用户',
|
||||
email: 'test@example.com',
|
||||
email_verified: false,
|
||||
phone: null,
|
||||
password_hash: 'hashedpassword',
|
||||
github_id: null,
|
||||
avatar_url: null,
|
||||
role: 1,
|
||||
status: 'active',
|
||||
created_at: new Date(),
|
||||
updated_at: new Date(),
|
||||
} as Users;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockLoginCoreService = {
|
||||
register: jest.fn(),
|
||||
login: jest.fn(),
|
||||
generateTokenPair: jest.fn(),
|
||||
};
|
||||
|
||||
const mockZulipAccountService = {
|
||||
createZulipAccount: jest.fn(),
|
||||
initializeAdminClient: jest.fn(),
|
||||
};
|
||||
|
||||
const mockZulipAccountsService = {
|
||||
findByGameUserId: jest.fn(),
|
||||
create: jest.fn(),
|
||||
updateByGameUserId: jest.fn(),
|
||||
};
|
||||
|
||||
const mockApiKeySecurityService = {
|
||||
storeApiKey: jest.fn(),
|
||||
getApiKey: 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);
|
||||
|
||||
// 模拟Logger以避免日志输出
|
||||
jest.spyOn(Logger.prototype, 'log').mockImplementation();
|
||||
jest.spyOn(Logger.prototype, 'error').mockImplementation();
|
||||
jest.spyOn(Logger.prototype, 'warn').mockImplementation();
|
||||
jest.spyOn(Logger.prototype, 'debug').mockImplementation();
|
||||
});
|
||||
|
||||
describe('用户注册时的Zulip集成', () => {
|
||||
it('应该在Zulip中不存在用户时创建新账号', async () => {
|
||||
// 准备测试数据
|
||||
const registerRequest = {
|
||||
username: 'testuser',
|
||||
password: 'password123',
|
||||
nickname: '测试用户',
|
||||
email: 'test@example.com',
|
||||
};
|
||||
|
||||
const mockAuthResult = {
|
||||
user: mockUser,
|
||||
isNewUser: true,
|
||||
};
|
||||
|
||||
const mockTokenPair = {
|
||||
access_token: 'access_token',
|
||||
refresh_token: 'refresh_token',
|
||||
expires_in: 3600,
|
||||
token_type: 'Bearer',
|
||||
};
|
||||
|
||||
const mockZulipCreateResult = {
|
||||
success: true,
|
||||
userId: 67890,
|
||||
email: 'test@example.com',
|
||||
apiKey: 'test_api_key_12345678901234567890',
|
||||
isExistingUser: false,
|
||||
};
|
||||
|
||||
// 设置模拟返回值
|
||||
loginCoreService.register.mockResolvedValue(mockAuthResult);
|
||||
loginCoreService.generateTokenPair.mockResolvedValue(mockTokenPair);
|
||||
zulipAccountsService.findByGameUserId.mockResolvedValue(null);
|
||||
zulipAccountService.createZulipAccount.mockResolvedValue(mockZulipCreateResult);
|
||||
apiKeySecurityService.storeApiKey.mockResolvedValue({ success: true, message: 'Stored' });
|
||||
zulipAccountsService.create.mockResolvedValue({} as any);
|
||||
|
||||
// 模拟私有方法
|
||||
const checkZulipUserExistsSpy = jest.spyOn(service as any, 'checkZulipUserExists')
|
||||
.mockResolvedValue({ exists: false });
|
||||
jest.spyOn(service as any, 'initializeZulipAdminClient')
|
||||
.mockResolvedValue(true);
|
||||
|
||||
// 执行测试
|
||||
const result = await service.register(registerRequest);
|
||||
|
||||
// 验证结果
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.is_new_user).toBe(true);
|
||||
expect(result.data?.message).toContain('Zulip');
|
||||
|
||||
// 验证调用
|
||||
expect(loginCoreService.register).toHaveBeenCalledWith(registerRequest);
|
||||
expect(zulipAccountsService.findByGameUserId).toHaveBeenCalledWith('12345');
|
||||
expect(checkZulipUserExistsSpy).toHaveBeenCalledWith('test@example.com');
|
||||
expect(zulipAccountService.createZulipAccount).toHaveBeenCalledWith({
|
||||
email: 'test@example.com',
|
||||
fullName: '测试用户',
|
||||
password: 'password123',
|
||||
});
|
||||
expect(apiKeySecurityService.storeApiKey).toHaveBeenCalledWith('12345', 'test_api_key_12345678901234567890');
|
||||
expect(zulipAccountsService.create).toHaveBeenCalledWith({
|
||||
gameUserId: '12345',
|
||||
zulipUserId: 67890,
|
||||
zulipEmail: 'test@example.com',
|
||||
zulipFullName: '测试用户',
|
||||
zulipApiKeyEncrypted: 'stored_in_redis',
|
||||
status: 'active',
|
||||
lastVerifiedAt: expect.any(Date),
|
||||
});
|
||||
});
|
||||
|
||||
it('应该在Zulip中已存在用户时绑定账号', async () => {
|
||||
// 准备测试数据
|
||||
const registerRequest = {
|
||||
username: 'testuser',
|
||||
password: 'password123',
|
||||
nickname: '测试用户',
|
||||
email: 'test@example.com',
|
||||
};
|
||||
|
||||
const mockAuthResult = {
|
||||
user: mockUser,
|
||||
isNewUser: true,
|
||||
};
|
||||
|
||||
const mockTokenPair = {
|
||||
access_token: 'access_token',
|
||||
refresh_token: 'refresh_token',
|
||||
expires_in: 3600,
|
||||
token_type: 'Bearer',
|
||||
};
|
||||
|
||||
// 设置模拟返回值
|
||||
loginCoreService.register.mockResolvedValue(mockAuthResult);
|
||||
loginCoreService.generateTokenPair.mockResolvedValue(mockTokenPair);
|
||||
zulipAccountsService.findByGameUserId.mockResolvedValue(null);
|
||||
apiKeySecurityService.storeApiKey.mockResolvedValue({ success: true, message: 'Stored' });
|
||||
zulipAccountsService.create.mockResolvedValue({} as any);
|
||||
|
||||
// 模拟私有方法
|
||||
jest.spyOn(service as any, 'checkZulipUserExists')
|
||||
.mockResolvedValue({ exists: true, userId: 67890 });
|
||||
const refreshZulipApiKeySpy = jest.spyOn(service as any, 'refreshZulipApiKey')
|
||||
.mockResolvedValue({ success: true, apiKey: 'existing_api_key_12345678901234567890' });
|
||||
jest.spyOn(service as any, 'initializeZulipAdminClient')
|
||||
.mockResolvedValue(true);
|
||||
|
||||
// 执行测试
|
||||
const result = await service.register(registerRequest);
|
||||
|
||||
// 验证结果
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.message).toContain('绑定');
|
||||
|
||||
// 验证调用
|
||||
expect(refreshZulipApiKeySpy).toHaveBeenCalledWith('test@example.com', 'password123');
|
||||
expect(apiKeySecurityService.storeApiKey).toHaveBeenCalledWith('12345', 'existing_api_key_12345678901234567890');
|
||||
expect(zulipAccountsService.create).toHaveBeenCalledWith({
|
||||
gameUserId: '12345',
|
||||
zulipUserId: 67890,
|
||||
zulipEmail: 'test@example.com',
|
||||
zulipFullName: '测试用户',
|
||||
zulipApiKeyEncrypted: 'stored_in_redis',
|
||||
status: 'active',
|
||||
lastVerifiedAt: expect.any(Date),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('用户登录时的Zulip集成', () => {
|
||||
it('应该在用户没有Zulip关联时尝试创建/绑定', async () => {
|
||||
// 准备测试数据
|
||||
const loginRequest = {
|
||||
identifier: 'testuser',
|
||||
password: 'password123',
|
||||
};
|
||||
|
||||
const mockAuthResult = {
|
||||
user: mockUser,
|
||||
isNewUser: false,
|
||||
};
|
||||
|
||||
const mockTokenPair = {
|
||||
access_token: 'access_token',
|
||||
refresh_token: 'refresh_token',
|
||||
expires_in: 3600,
|
||||
token_type: 'Bearer',
|
||||
};
|
||||
|
||||
const mockZulipCreateResult = {
|
||||
success: true,
|
||||
userId: 67890,
|
||||
email: 'test@example.com',
|
||||
apiKey: 'new_api_key_12345678901234567890',
|
||||
isExistingUser: false,
|
||||
};
|
||||
|
||||
// 设置模拟返回值
|
||||
loginCoreService.login.mockResolvedValue(mockAuthResult);
|
||||
loginCoreService.generateTokenPair.mockResolvedValue(mockTokenPair);
|
||||
zulipAccountsService.findByGameUserId.mockResolvedValue(null);
|
||||
zulipAccountService.createZulipAccount.mockResolvedValue(mockZulipCreateResult);
|
||||
apiKeySecurityService.storeApiKey.mockResolvedValue({ success: true, message: 'Stored' });
|
||||
zulipAccountsService.create.mockResolvedValue({} as any);
|
||||
|
||||
// 模拟私有方法
|
||||
jest.spyOn(service as any, 'checkZulipUserExists')
|
||||
.mockResolvedValue({ exists: false });
|
||||
jest.spyOn(service as any, 'initializeZulipAdminClient')
|
||||
.mockResolvedValue(true);
|
||||
|
||||
// 执行测试
|
||||
const result = await service.login(loginRequest);
|
||||
|
||||
// 验证结果
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.is_new_user).toBe(false);
|
||||
|
||||
// 验证调用
|
||||
expect(loginCoreService.login).toHaveBeenCalledWith(loginRequest);
|
||||
expect(zulipAccountsService.findByGameUserId).toHaveBeenCalledWith('12345');
|
||||
expect(zulipAccountService.createZulipAccount).toHaveBeenCalledWith({
|
||||
email: 'test@example.com',
|
||||
fullName: '测试用户',
|
||||
password: 'password123',
|
||||
});
|
||||
});
|
||||
|
||||
it('应该在用户已有Zulip关联时刷新API Key', async () => {
|
||||
// 准备测试数据
|
||||
const loginRequest = {
|
||||
identifier: 'testuser',
|
||||
password: 'password123',
|
||||
};
|
||||
|
||||
const mockAuthResult = {
|
||||
user: mockUser,
|
||||
isNewUser: false,
|
||||
};
|
||||
|
||||
const mockTokenPair = {
|
||||
access_token: 'access_token',
|
||||
refresh_token: 'refresh_token',
|
||||
expires_in: 3600,
|
||||
token_type: 'Bearer',
|
||||
};
|
||||
|
||||
const mockExistingAccount: ZulipAccountResponseDto = {
|
||||
id: '1',
|
||||
gameUserId: '12345',
|
||||
zulipUserId: 67890,
|
||||
zulipEmail: 'test@example.com',
|
||||
zulipFullName: '测试用户',
|
||||
status: 'active' as const,
|
||||
lastVerifiedAt: new Date().toISOString(),
|
||||
retryCount: 0,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// 设置模拟返回值
|
||||
loginCoreService.login.mockResolvedValue(mockAuthResult);
|
||||
loginCoreService.generateTokenPair.mockResolvedValue(mockTokenPair);
|
||||
zulipAccountsService.findByGameUserId.mockResolvedValue(mockExistingAccount);
|
||||
apiKeySecurityService.storeApiKey.mockResolvedValue({ success: true, message: 'Stored' });
|
||||
zulipAccountsService.updateByGameUserId.mockResolvedValue({} as any);
|
||||
|
||||
// 模拟私有方法
|
||||
const refreshZulipApiKeySpy = jest.spyOn(service as any, 'refreshZulipApiKey')
|
||||
.mockResolvedValue({ success: true, apiKey: 'refreshed_api_key_12345678901234567890' });
|
||||
|
||||
// 执行测试
|
||||
const result = await service.login(loginRequest);
|
||||
|
||||
// 验证结果
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
// 验证调用
|
||||
expect(zulipAccountsService.findByGameUserId).toHaveBeenCalledWith('12345');
|
||||
expect(refreshZulipApiKeySpy).toHaveBeenCalledWith('test@example.com', 'password123');
|
||||
expect(apiKeySecurityService.storeApiKey).toHaveBeenCalledWith('12345', 'refreshed_api_key_12345678901234567890');
|
||||
expect(zulipAccountsService.updateByGameUserId).toHaveBeenCalledWith('12345', {
|
||||
lastVerifiedAt: expect.any(Date),
|
||||
status: 'active',
|
||||
errorMessage: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('错误处理', () => {
|
||||
it('应该在Zulip创建失败时回滚用户注册', async () => {
|
||||
// 准备测试数据
|
||||
const registerRequest = {
|
||||
username: 'testuser',
|
||||
password: 'password123',
|
||||
nickname: '测试用户',
|
||||
email: 'test@example.com',
|
||||
};
|
||||
|
||||
const mockAuthResult = {
|
||||
user: mockUser,
|
||||
isNewUser: true,
|
||||
};
|
||||
|
||||
// 设置模拟返回值
|
||||
loginCoreService.register.mockResolvedValue(mockAuthResult);
|
||||
loginCoreService.deleteUser = jest.fn().mockResolvedValue(true);
|
||||
zulipAccountsService.findByGameUserId.mockResolvedValue(null);
|
||||
|
||||
// 模拟Zulip创建失败
|
||||
jest.spyOn(service as any, 'checkZulipUserExists')
|
||||
.mockResolvedValue({ exists: false });
|
||||
jest.spyOn(service as any, 'initializeZulipAdminClient')
|
||||
.mockResolvedValue(true);
|
||||
|
||||
zulipAccountService.createZulipAccount.mockResolvedValue({
|
||||
success: false,
|
||||
error: 'Zulip服务器错误',
|
||||
});
|
||||
|
||||
// 执行测试
|
||||
const result = await service.register(registerRequest);
|
||||
|
||||
// 验证结果
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toContain('Zulip账号创建失败');
|
||||
|
||||
// 验证回滚调用
|
||||
expect(loginCoreService.deleteUser).toHaveBeenCalledWith(mockUser.id);
|
||||
});
|
||||
|
||||
it('应该在登录时Zulip集成失败但不影响登录', async () => {
|
||||
// 准备测试数据
|
||||
const loginRequest = {
|
||||
identifier: 'testuser',
|
||||
password: 'password123',
|
||||
};
|
||||
|
||||
const mockAuthResult = {
|
||||
user: mockUser,
|
||||
isNewUser: false,
|
||||
};
|
||||
|
||||
const mockTokenPair = {
|
||||
access_token: 'access_token',
|
||||
refresh_token: 'refresh_token',
|
||||
expires_in: 3600,
|
||||
token_type: 'Bearer',
|
||||
};
|
||||
|
||||
// 设置模拟返回值
|
||||
loginCoreService.login.mockResolvedValue(mockAuthResult);
|
||||
loginCoreService.generateTokenPair.mockResolvedValue(mockTokenPair);
|
||||
zulipAccountsService.findByGameUserId.mockResolvedValue(null);
|
||||
|
||||
// 模拟Zulip集成失败
|
||||
jest.spyOn(service as any, 'initializeZulipAdminClient')
|
||||
.mockRejectedValue(new Error('Zulip服务器不可用'));
|
||||
|
||||
// 执行测试
|
||||
const result = await service.login(loginRequest);
|
||||
|
||||
// 验证结果 - 登录应该成功,即使Zulip集成失败
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.access_token).toBe('access_token');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user