forked from datawhale/whale-town-end
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
443 lines
15 KiB
TypeScript
443 lines
15 KiB
TypeScript
/**
|
||
* 登录服务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');
|
||
});
|
||
});
|
||
}); |