forked from datawhale/whale-town-end
- Add ZulipAccountsEntity, repository, and module for persistent Zulip account storage - Create ZulipAccountService in core layer for managing Zulip account lifecycle - Integrate Zulip account creation into login flow via LoginService - Add comprehensive test suite for Zulip account creation during user registration - Create quick test script for validating registered user Zulip integration - Update UsersEntity to support Zulip account associations - Update auth module to include Zulip and ZulipAccounts dependencies - Fix WebSocket connection protocol from ws:// to wss:// in API documentation - Enhance LoginCoreService to coordinate Zulip account provisioning during authentication
520 lines
19 KiB
TypeScript
520 lines
19 KiB
TypeScript
/**
|
||
* LoginService Zulip账号创建属性测试
|
||
*
|
||
* 功能描述:
|
||
* - 测试用户注册时Zulip账号创建的一致性
|
||
* - 验证账号关联和数据完整性
|
||
* - 测试失败回滚机制
|
||
*
|
||
* 属性测试:
|
||
* - 属性 13: Zulip账号创建一致性
|
||
* - 验证需求: 账号创建成功率和数据一致性
|
||
*
|
||
* @author angjustinl
|
||
* @version 1.0.0
|
||
* @since 2025-01-05
|
||
*/
|
||
|
||
import { Test, TestingModule } from '@nestjs/testing';
|
||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||
import { Repository } from 'typeorm';
|
||
import * as fc from 'fast-check';
|
||
import { LoginService } from './login.service';
|
||
import { LoginCoreService, RegisterRequest } from '../../../core/login_core/login_core.service';
|
||
import { ZulipAccountService } from '../../../core/zulip/services/zulip_account.service';
|
||
import { ZulipAccountsRepository } from '../../../core/db/zulip_accounts/zulip_accounts.repository';
|
||
import { ApiKeySecurityService } from '../../../core/zulip/services/api_key_security.service';
|
||
import { Users } from '../../../core/db/users/users.entity';
|
||
import { ZulipAccounts } from '../../../core/db/zulip_accounts/zulip_accounts.entity';
|
||
|
||
describe('LoginService - Zulip账号创建属性测试', () => {
|
||
let loginService: LoginService;
|
||
let loginCoreService: jest.Mocked<LoginCoreService>;
|
||
let zulipAccountService: jest.Mocked<ZulipAccountService>;
|
||
let zulipAccountsRepository: jest.Mocked<ZulipAccountsRepository>;
|
||
let apiKeySecurityService: jest.Mocked<ApiKeySecurityService>;
|
||
|
||
// 测试用的模拟数据生成器
|
||
const validEmailArb = fc.string({ minLength: 5, maxLength: 50 })
|
||
.filter(s => s.includes('@') && s.includes('.'))
|
||
.map(s => `test_${s.replace(/[^a-zA-Z0-9@._-]/g, '')}@example.com`);
|
||
|
||
const validUsernameArb = fc.string({ minLength: 3, maxLength: 20 })
|
||
.filter(s => /^[a-zA-Z0-9_]+$/.test(s));
|
||
|
||
const validNicknameArb = fc.string({ minLength: 2, maxLength: 50 })
|
||
.filter(s => s.trim().length > 0);
|
||
|
||
const validPasswordArb = fc.string({ minLength: 8, maxLength: 20 })
|
||
.filter(s => /[a-zA-Z]/.test(s) && /\d/.test(s));
|
||
|
||
const registerRequestArb = fc.record({
|
||
username: validUsernameArb,
|
||
email: validEmailArb,
|
||
nickname: validNicknameArb,
|
||
password: validPasswordArb,
|
||
});
|
||
|
||
beforeEach(async () => {
|
||
// 创建模拟服务
|
||
const mockLoginCoreService = {
|
||
register: jest.fn(),
|
||
deleteUser: jest.fn(),
|
||
};
|
||
|
||
const mockZulipAccountService = {
|
||
initializeAdminClient: jest.fn(),
|
||
createZulipAccount: jest.fn(),
|
||
linkGameAccount: jest.fn(),
|
||
};
|
||
|
||
const mockZulipAccountsRepository = {
|
||
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: 'ZulipAccountsRepository',
|
||
useValue: mockZulipAccountsRepository,
|
||
},
|
||
{
|
||
provide: ApiKeySecurityService,
|
||
useValue: mockApiKeySecurityService,
|
||
},
|
||
],
|
||
}).compile();
|
||
|
||
loginService = module.get<LoginService>(LoginService);
|
||
loginCoreService = module.get(LoginCoreService);
|
||
zulipAccountService = module.get(ZulipAccountService);
|
||
zulipAccountsRepository = module.get('ZulipAccountsRepository');
|
||
apiKeySecurityService = module.get(ApiKeySecurityService);
|
||
|
||
// 设置环境变量模拟
|
||
process.env.ZULIP_SERVER_URL = 'https://test.zulip.com';
|
||
process.env.ZULIP_BOT_EMAIL = 'bot@test.zulip.com';
|
||
process.env.ZULIP_BOT_API_KEY = 'test_api_key_123';
|
||
});
|
||
|
||
afterEach(() => {
|
||
jest.clearAllMocks();
|
||
// 清理环境变量
|
||
delete process.env.ZULIP_SERVER_URL;
|
||
delete process.env.ZULIP_BOT_EMAIL;
|
||
delete process.env.ZULIP_BOT_API_KEY;
|
||
});
|
||
|
||
/**
|
||
* 属性 13: Zulip账号创建一致性
|
||
*
|
||
* 验证需求: 账号创建成功率和数据一致性
|
||
*
|
||
* 测试内容:
|
||
* 1. 成功注册时,游戏账号和Zulip账号都应该被创建
|
||
* 2. 账号关联信息应该正确存储
|
||
* 3. Zulip账号创建失败时,游戏账号应该被回滚
|
||
* 4. 数据一致性:邮箱、昵称等信息应该保持一致
|
||
*/
|
||
describe('属性 13: Zulip账号创建一致性', () => {
|
||
it('应该在成功注册时创建一致的游戏账号和Zulip账号', async () => {
|
||
await fc.assert(
|
||
fc.asyncProperty(registerRequestArb, async (registerRequest) => {
|
||
// 准备测试数据
|
||
const mockGameUser: Users = {
|
||
id: BigInt(Math.floor(Math.random() * 1000000)),
|
||
username: registerRequest.username,
|
||
email: registerRequest.email,
|
||
nickname: registerRequest.nickname,
|
||
password_hash: 'hashed_password',
|
||
role: 1,
|
||
created_at: new Date(),
|
||
updated_at: new Date(),
|
||
} as Users;
|
||
|
||
const mockZulipResult = {
|
||
success: true,
|
||
userId: Math.floor(Math.random() * 1000000),
|
||
email: registerRequest.email,
|
||
apiKey: 'zulip_api_key_' + Math.random().toString(36),
|
||
};
|
||
|
||
const mockZulipAccount: ZulipAccounts = {
|
||
id: BigInt(Math.floor(Math.random() * 1000000)),
|
||
gameUserId: mockGameUser.id,
|
||
zulipUserId: mockZulipResult.userId,
|
||
zulipEmail: mockZulipResult.email,
|
||
zulipFullName: registerRequest.nickname,
|
||
zulipApiKeyEncrypted: 'encrypted_' + mockZulipResult.apiKey,
|
||
status: 'active',
|
||
createdAt: new Date(),
|
||
updatedAt: new Date(),
|
||
} as ZulipAccounts;
|
||
|
||
// 设置模拟行为
|
||
zulipAccountService.initializeAdminClient.mockResolvedValue(true);
|
||
loginCoreService.register.mockResolvedValue({
|
||
user: mockGameUser,
|
||
isNewUser: true,
|
||
});
|
||
zulipAccountsRepository.findByGameUserId.mockResolvedValue(null);
|
||
zulipAccountService.createZulipAccount.mockResolvedValue(mockZulipResult);
|
||
apiKeySecurityService.storeApiKey.mockResolvedValue(undefined);
|
||
zulipAccountsRepository.create.mockResolvedValue(mockZulipAccount);
|
||
zulipAccountService.linkGameAccount.mockResolvedValue(true);
|
||
|
||
// 执行注册
|
||
const result = await loginService.register(registerRequest);
|
||
|
||
// 验证结果
|
||
expect(result.success).toBe(true);
|
||
expect(result.data?.user.username).toBe(registerRequest.username);
|
||
expect(result.data?.user.email).toBe(registerRequest.email);
|
||
expect(result.data?.user.nickname).toBe(registerRequest.nickname);
|
||
expect(result.data?.is_new_user).toBe(true);
|
||
|
||
// 验证Zulip管理员客户端初始化
|
||
expect(zulipAccountService.initializeAdminClient).toHaveBeenCalledWith({
|
||
realm: 'https://test.zulip.com',
|
||
username: 'bot@test.zulip.com',
|
||
apiKey: 'test_api_key_123',
|
||
});
|
||
|
||
// 验证游戏用户注册
|
||
expect(loginCoreService.register).toHaveBeenCalledWith(registerRequest);
|
||
|
||
// 验证Zulip账号创建
|
||
expect(zulipAccountService.createZulipAccount).toHaveBeenCalledWith({
|
||
email: registerRequest.email,
|
||
fullName: registerRequest.nickname,
|
||
password: registerRequest.password,
|
||
});
|
||
|
||
// 验证API Key存储
|
||
expect(apiKeySecurityService.storeApiKey).toHaveBeenCalledWith(
|
||
mockGameUser.id.toString(),
|
||
mockZulipResult.apiKey
|
||
);
|
||
|
||
// 验证账号关联创建
|
||
expect(zulipAccountsRepository.create).toHaveBeenCalledWith({
|
||
gameUserId: mockGameUser.id,
|
||
zulipUserId: mockZulipResult.userId,
|
||
zulipEmail: mockZulipResult.email,
|
||
zulipFullName: registerRequest.nickname,
|
||
zulipApiKeyEncrypted: 'stored_in_redis',
|
||
status: 'active',
|
||
});
|
||
|
||
// 验证内存关联
|
||
expect(zulipAccountService.linkGameAccount).toHaveBeenCalledWith(
|
||
mockGameUser.id.toString(),
|
||
mockZulipResult.userId,
|
||
mockZulipResult.email,
|
||
mockZulipResult.apiKey
|
||
);
|
||
}),
|
||
{ numRuns: 100 }
|
||
);
|
||
});
|
||
|
||
it('应该在Zulip账号创建失败时回滚游戏账号', async () => {
|
||
await fc.assert(
|
||
fc.asyncProperty(registerRequestArb, async (registerRequest) => {
|
||
// 准备测试数据
|
||
const mockGameUser: Users = {
|
||
id: BigInt(Math.floor(Math.random() * 1000000)),
|
||
username: registerRequest.username,
|
||
email: registerRequest.email,
|
||
nickname: registerRequest.nickname,
|
||
password_hash: 'hashed_password',
|
||
role: 1,
|
||
created_at: new Date(),
|
||
updated_at: new Date(),
|
||
} as Users;
|
||
|
||
// 设置模拟行为 - Zulip账号创建失败
|
||
zulipAccountService.initializeAdminClient.mockResolvedValue(true);
|
||
loginCoreService.register.mockResolvedValue({
|
||
user: mockGameUser,
|
||
isNewUser: true,
|
||
});
|
||
zulipAccountsRepository.findByGameUserId.mockResolvedValue(null);
|
||
zulipAccountService.createZulipAccount.mockResolvedValue({
|
||
success: false,
|
||
error: 'Zulip服务器连接失败',
|
||
errorCode: 'CONNECTION_FAILED',
|
||
});
|
||
loginCoreService.deleteUser.mockResolvedValue(true);
|
||
|
||
// 执行注册
|
||
const result = await loginService.register(registerRequest);
|
||
|
||
// 验证结果 - 注册应该失败
|
||
expect(result.success).toBe(false);
|
||
expect(result.message).toContain('Zulip账号创建失败');
|
||
|
||
// 验证游戏用户被创建
|
||
expect(loginCoreService.register).toHaveBeenCalledWith(registerRequest);
|
||
|
||
// 验证Zulip账号创建尝试
|
||
expect(zulipAccountService.createZulipAccount).toHaveBeenCalledWith({
|
||
email: registerRequest.email,
|
||
fullName: registerRequest.nickname,
|
||
password: registerRequest.password,
|
||
});
|
||
|
||
// 验证游戏用户被回滚删除
|
||
expect(loginCoreService.deleteUser).toHaveBeenCalledWith(mockGameUser.id);
|
||
|
||
// 验证没有创建账号关联
|
||
expect(zulipAccountsRepository.create).not.toHaveBeenCalled();
|
||
expect(zulipAccountService.linkGameAccount).not.toHaveBeenCalled();
|
||
}),
|
||
{ numRuns: 100 }
|
||
);
|
||
});
|
||
|
||
it('应该正确处理已存在Zulip账号关联的情况', async () => {
|
||
await fc.assert(
|
||
fc.asyncProperty(registerRequestArb, async (registerRequest) => {
|
||
// 准备测试数据
|
||
const mockGameUser: Users = {
|
||
id: BigInt(Math.floor(Math.random() * 1000000)),
|
||
username: registerRequest.username,
|
||
email: registerRequest.email,
|
||
nickname: registerRequest.nickname,
|
||
password_hash: 'hashed_password',
|
||
role: 1,
|
||
created_at: new Date(),
|
||
updated_at: new Date(),
|
||
} as Users;
|
||
|
||
const existingZulipAccount: ZulipAccounts = {
|
||
id: BigInt(Math.floor(Math.random() * 1000000)),
|
||
gameUserId: mockGameUser.id,
|
||
zulipUserId: 12345,
|
||
zulipEmail: registerRequest.email,
|
||
zulipFullName: registerRequest.nickname,
|
||
zulipApiKeyEncrypted: 'existing_encrypted_key',
|
||
status: 'active',
|
||
createdAt: new Date(),
|
||
updatedAt: new Date(),
|
||
} as ZulipAccounts;
|
||
|
||
// 设置模拟行为 - 已存在Zulip账号关联
|
||
zulipAccountService.initializeAdminClient.mockResolvedValue(true);
|
||
loginCoreService.register.mockResolvedValue({
|
||
user: mockGameUser,
|
||
isNewUser: true,
|
||
});
|
||
zulipAccountsRepository.findByGameUserId.mockResolvedValue(existingZulipAccount);
|
||
|
||
// 执行注册
|
||
const result = await loginService.register(registerRequest);
|
||
|
||
// 验证结果 - 注册应该成功
|
||
expect(result.success).toBe(true);
|
||
expect(result.data?.user.username).toBe(registerRequest.username);
|
||
|
||
// 验证游戏用户被创建
|
||
expect(loginCoreService.register).toHaveBeenCalledWith(registerRequest);
|
||
|
||
// 验证检查了现有关联
|
||
expect(zulipAccountsRepository.findByGameUserId).toHaveBeenCalledWith(mockGameUser.id);
|
||
|
||
// 验证没有尝试创建新的Zulip账号
|
||
expect(zulipAccountService.createZulipAccount).not.toHaveBeenCalled();
|
||
expect(zulipAccountsRepository.create).not.toHaveBeenCalled();
|
||
}),
|
||
{ numRuns: 100 }
|
||
);
|
||
});
|
||
|
||
it('应该正确处理缺少邮箱或密码的注册请求', async () => {
|
||
await fc.assert(
|
||
fc.asyncProperty(
|
||
fc.record({
|
||
username: validUsernameArb,
|
||
nickname: validNicknameArb,
|
||
email: fc.option(validEmailArb, { nil: undefined }),
|
||
password: fc.option(validPasswordArb, { nil: undefined }),
|
||
}),
|
||
async (registerRequest) => {
|
||
// 只测试缺少邮箱或密码的情况
|
||
if (registerRequest.email && registerRequest.password) {
|
||
return; // 跳过完整数据的情况
|
||
}
|
||
|
||
// 准备测试数据
|
||
const mockGameUser: Users = {
|
||
id: BigInt(Math.floor(Math.random() * 1000000)),
|
||
username: registerRequest.username,
|
||
email: registerRequest.email || null,
|
||
nickname: registerRequest.nickname,
|
||
password_hash: registerRequest.password ? 'hashed_password' : null,
|
||
role: 1,
|
||
created_at: new Date(),
|
||
updated_at: new Date(),
|
||
} as Users;
|
||
|
||
// 设置模拟行为
|
||
zulipAccountService.initializeAdminClient.mockResolvedValue(true);
|
||
loginCoreService.register.mockResolvedValue({
|
||
user: mockGameUser,
|
||
isNewUser: true,
|
||
});
|
||
|
||
// 执行注册
|
||
const result = await loginService.register(registerRequest as RegisterRequest);
|
||
|
||
// 验证结果 - 注册应该成功,但跳过Zulip账号创建
|
||
expect(result.success).toBe(true);
|
||
expect(result.data?.user.username).toBe(registerRequest.username);
|
||
expect(result.data?.message).toBe('注册成功'); // 不包含Zulip创建信息
|
||
|
||
// 验证游戏用户被创建
|
||
expect(loginCoreService.register).toHaveBeenCalledWith(registerRequest);
|
||
|
||
// 验证没有尝试创建Zulip账号
|
||
expect(zulipAccountService.createZulipAccount).not.toHaveBeenCalled();
|
||
expect(zulipAccountsRepository.create).not.toHaveBeenCalled();
|
||
}
|
||
),
|
||
{ numRuns: 50 }
|
||
);
|
||
});
|
||
|
||
it('应该正确处理Zulip管理员客户端初始化失败', async () => {
|
||
await fc.assert(
|
||
fc.asyncProperty(registerRequestArb, async (registerRequest) => {
|
||
// 设置模拟行为 - 管理员客户端初始化失败
|
||
zulipAccountService.initializeAdminClient.mockResolvedValue(false);
|
||
|
||
// 执行注册
|
||
const result = await loginService.register(registerRequest);
|
||
|
||
// 验证结果 - 注册应该失败
|
||
expect(result.success).toBe(false);
|
||
expect(result.message).toContain('Zulip管理员客户端初始化失败');
|
||
|
||
// 验证没有尝试创建游戏用户
|
||
expect(loginCoreService.register).not.toHaveBeenCalled();
|
||
|
||
// 验证没有尝试创建Zulip账号
|
||
expect(zulipAccountService.createZulipAccount).not.toHaveBeenCalled();
|
||
}),
|
||
{ numRuns: 50 }
|
||
);
|
||
});
|
||
|
||
it('应该正确处理环境变量缺失的情况', async () => {
|
||
await fc.assert(
|
||
fc.asyncProperty(registerRequestArb, async (registerRequest) => {
|
||
// 清除环境变量
|
||
delete process.env.ZULIP_SERVER_URL;
|
||
delete process.env.ZULIP_BOT_EMAIL;
|
||
delete process.env.ZULIP_BOT_API_KEY;
|
||
|
||
// 执行注册
|
||
const result = await loginService.register(registerRequest);
|
||
|
||
// 验证结果 - 注册应该失败
|
||
expect(result.success).toBe(false);
|
||
expect(result.message).toContain('Zulip管理员配置不完整');
|
||
|
||
// 验证没有尝试创建游戏用户
|
||
expect(loginCoreService.register).not.toHaveBeenCalled();
|
||
|
||
// 恢复环境变量
|
||
process.env.ZULIP_SERVER_URL = 'https://test.zulip.com';
|
||
process.env.ZULIP_BOT_EMAIL = 'bot@test.zulip.com';
|
||
process.env.ZULIP_BOT_API_KEY = 'test_api_key_123';
|
||
}),
|
||
{ numRuns: 30 }
|
||
);
|
||
});
|
||
});
|
||
|
||
/**
|
||
* 数据一致性验证测试
|
||
*
|
||
* 验证游戏账号和Zulip账号之间的数据一致性
|
||
*/
|
||
describe('数据一致性验证', () => {
|
||
it('应该确保游戏账号和Zulip账号使用相同的邮箱和昵称', async () => {
|
||
await fc.assert(
|
||
fc.asyncProperty(registerRequestArb, async (registerRequest) => {
|
||
// 准备测试数据
|
||
const mockGameUser: Users = {
|
||
id: BigInt(Math.floor(Math.random() * 1000000)),
|
||
username: registerRequest.username,
|
||
email: registerRequest.email,
|
||
nickname: registerRequest.nickname,
|
||
password_hash: 'hashed_password',
|
||
role: 1,
|
||
created_at: new Date(),
|
||
updated_at: new Date(),
|
||
} as Users;
|
||
|
||
const mockZulipResult = {
|
||
success: true,
|
||
userId: Math.floor(Math.random() * 1000000),
|
||
email: registerRequest.email,
|
||
apiKey: 'zulip_api_key_' + Math.random().toString(36),
|
||
};
|
||
|
||
// 设置模拟行为
|
||
zulipAccountService.initializeAdminClient.mockResolvedValue(true);
|
||
loginCoreService.register.mockResolvedValue({
|
||
user: mockGameUser,
|
||
isNewUser: true,
|
||
});
|
||
zulipAccountsRepository.findByGameUserId.mockResolvedValue(null);
|
||
zulipAccountService.createZulipAccount.mockResolvedValue(mockZulipResult);
|
||
apiKeySecurityService.storeApiKey.mockResolvedValue(undefined);
|
||
zulipAccountsRepository.create.mockResolvedValue({} as ZulipAccounts);
|
||
zulipAccountService.linkGameAccount.mockResolvedValue(true);
|
||
|
||
// 执行注册
|
||
await loginService.register(registerRequest);
|
||
|
||
// 验证Zulip账号创建时使用了正确的数据
|
||
expect(zulipAccountService.createZulipAccount).toHaveBeenCalledWith({
|
||
email: registerRequest.email, // 相同的邮箱
|
||
fullName: registerRequest.nickname, // 相同的昵称
|
||
password: registerRequest.password, // 相同的密码
|
||
});
|
||
|
||
// 验证账号关联存储了正确的数据
|
||
expect(zulipAccountsRepository.create).toHaveBeenCalledWith(
|
||
expect.objectContaining({
|
||
gameUserId: mockGameUser.id,
|
||
zulipUserId: mockZulipResult.userId,
|
||
zulipEmail: registerRequest.email, // 相同的邮箱
|
||
zulipFullName: registerRequest.nickname, // 相同的昵称
|
||
zulipApiKeyEncrypted: 'stored_in_redis',
|
||
status: 'active',
|
||
})
|
||
);
|
||
}),
|
||
{ numRuns: 100 }
|
||
);
|
||
});
|
||
});
|
||
}); |