Files
whale-town-end/src/business/admin/database_management.service.unit.spec.ts
moyin 5f662ef091 feat: 完善管理员系统和用户管理模块
- 更新管理员控制器和数据库管理功能
- 完善管理员操作日志系统
- 添加全面的属性测试覆盖
- 优化用户管理和用户档案服务
- 更新代码检查规范文档

功能改进:
- 增强管理员权限验证
- 完善操作日志记录
- 优化数据库管理接口
- 提升系统安全性和可维护性
2026-01-09 17:05:08 +08:00

593 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* DatabaseManagementService 单元测试
*
* 测试目标:
* - 验证服务类各个方法的具体实现
* - 测试边界条件和异常情况
* - 确保代码覆盖率达标
*
* 最近修改:
* - 2026-01-08: 注释规范优化 - 修正@author字段更新版本号和修改记录 (修改者: moyin)
* - 2026-01-08: 功能新增 - 创建DatabaseManagementService单元测试 (修改者: assistant)
*
* @author moyin
* @version 1.0.1
* @since 2026-01-08
* @lastModified 2026-01-08
*/
import { Test, TestingModule } from '@nestjs/testing';
import { ConfigModule } from '@nestjs/config';
import { DatabaseManagementService } from './database_management.service';
import { AdminOperationLogService } from './admin_operation_log.service';
import { UserStatus } from '../user_mgmt/user_status.enum';
describe('DatabaseManagementService Unit Tests', () => {
let service: DatabaseManagementService;
let mockUsersService: any;
let mockUserProfilesService: any;
let mockZulipAccountsService: any;
let mockLogService: any;
beforeEach(async () => {
mockUsersService = {
findAll: jest.fn(),
findOne: jest.fn(),
create: jest.fn(),
update: jest.fn(),
remove: jest.fn(),
search: jest.fn(),
count: jest.fn()
};
mockUserProfilesService = {
findAll: jest.fn(),
findOne: jest.fn(),
create: jest.fn(),
update: jest.fn(),
remove: jest.fn(),
findByMap: jest.fn(),
count: jest.fn()
};
mockZulipAccountsService = {
findMany: jest.fn(),
findById: jest.fn(),
create: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
batchUpdateStatus: jest.fn().mockResolvedValue({ success: true, updatedCount: 0 }),
getStatusStatistics: jest.fn()
};
mockLogService = {
createLog: jest.fn().mockResolvedValue({}),
queryLogs: jest.fn().mockResolvedValue({ logs: [], total: 0 }),
getLogById: jest.fn().mockResolvedValue(null),
getStatistics: jest.fn().mockResolvedValue({}),
cleanupExpiredLogs: jest.fn().mockResolvedValue(0),
getAdminOperationHistory: jest.fn().mockResolvedValue([]),
getSensitiveOperations: jest.fn().mockResolvedValue({ logs: [], total: 0 })
};
const module: TestingModule = await Test.createTestingModule({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: ['.env.test', '.env']
})
],
providers: [
DatabaseManagementService,
{
provide: AdminOperationLogService,
useValue: mockLogService
},
{
provide: 'UsersService',
useValue: mockUsersService
},
{
provide: 'IUserProfilesService',
useValue: mockUserProfilesService
},
{
provide: 'ZulipAccountsService',
useValue: mockZulipAccountsService
}
]
}).compile();
service = module.get<DatabaseManagementService>(DatabaseManagementService);
});
describe('User Management', () => {
describe('getUserList', () => {
it('should return paginated user list with correct format', async () => {
const mockUsers = [
{ id: BigInt(1), username: 'user1', email: 'user1@test.com' },
{ id: BigInt(2), username: 'user2', email: 'user2@test.com' }
];
const totalCount = 10;
mockUsersService.findAll.mockResolvedValue(mockUsers);
mockUsersService.count.mockResolvedValue(totalCount);
const result = await service.getUserList(5, 0);
expect(result.success).toBe(true);
expect(result.data.items).toEqual(mockUsers.map(u => ({ ...u, id: u.id.toString() })));
expect(result.data.total).toBe(totalCount);
expect(result.data.limit).toBe(5);
expect(result.data.offset).toBe(0);
expect(result.data.has_more).toBe(true);
});
it('should handle empty result set', async () => {
mockUsersService.findAll.mockResolvedValue([]);
mockUsersService.count.mockResolvedValue(0);
const result = await service.getUserList(10, 0);
expect(result.success).toBe(true);
expect(result.data.items).toEqual([]);
expect(result.data.total).toBe(0);
expect(result.data.has_more).toBe(false);
});
it('should apply limit and offset correctly', async () => {
const mockUsers = [{ id: BigInt(1), username: 'user1' }];
mockUsersService.findAll.mockResolvedValue(mockUsers);
mockUsersService.count.mockResolvedValue(1);
await service.getUserList(20, 10);
expect(mockUsersService.findAll).toHaveBeenCalledWith(undefined, 20, 10);
});
it('should enforce maximum limit', async () => {
mockUsersService.findAll.mockResolvedValue([]);
mockUsersService.count.mockResolvedValue(0);
await service.getUserList(200, 0); // 超过最大限制
expect(mockUsersService.findAll).toHaveBeenCalledWith(undefined, 100, 0);
});
it('should handle negative offset', async () => {
mockUsersService.findAll.mockResolvedValue([]);
mockUsersService.count.mockResolvedValue(0);
await service.getUserList(10, -5);
expect(mockUsersService.findAll).toHaveBeenCalledWith(undefined, 10, 0);
});
});
describe('getUserById', () => {
it('should return user when found', async () => {
const mockUser = { id: BigInt(1), username: 'testuser', email: 'test@example.com' };
mockUsersService.findOne.mockResolvedValue(mockUser);
const result = await service.getUserById(BigInt(1));
expect(result.success).toBe(true);
expect(result.data).toEqual({ ...mockUser, id: '1' });
expect(mockUsersService.findOne).toHaveBeenCalledWith(BigInt(1));
});
it('should return error when user not found', async () => {
mockUsersService.findOne.mockResolvedValue(null);
const result = await service.getUserById(BigInt(999));
expect(result.success).toBe(false);
expect(result.error_code).toBe('USER_NOT_FOUND');
expect(result.message).toContain('User with ID 999 not found');
});
it('should handle invalid ID format', async () => {
const result = await service.getUserById(BigInt(0)); // 使用有效的 bigint
expect(result.success).toBe(false);
expect(result.error_code).toBe('INVALID_USER_ID');
});
it('should handle service errors', async () => {
mockUsersService.findOne.mockRejectedValue(new Error('Database error'));
const result = await service.getUserById(BigInt(1));
expect(result.success).toBe(false);
expect(result.error_code).toBe('DATABASE_ERROR');
});
});
describe('createUser', () => {
it('should create user successfully', async () => {
const userData = {
username: 'newuser',
email: 'new@example.com',
nickname: 'New User',
status: UserStatus.ACTIVE
};
const createdUser = { ...userData, id: BigInt(1) };
mockUsersService.create.mockResolvedValue(createdUser);
const result = await service.createUser(userData);
expect(result.success).toBe(true);
expect(result.data).toEqual({ ...createdUser, id: '1' });
expect(mockUsersService.create).toHaveBeenCalledWith(userData);
});
it('should handle duplicate username error', async () => {
const userData = { username: 'existing', email: 'test@example.com', nickname: 'Existing User', status: UserStatus.ACTIVE };
mockUsersService.create.mockRejectedValue(new Error('Duplicate key violation'));
const result = await service.createUser(userData);
expect(result.success).toBe(false);
expect(result.error_code).toBe('DUPLICATE_USERNAME');
});
it('should validate required fields', async () => {
const invalidData = { username: '', email: 'test@example.com', nickname: 'Test User', status: UserStatus.ACTIVE };
const result = await service.createUser(invalidData);
expect(result.success).toBe(false);
expect(result.error_code).toBe('VALIDATION_ERROR');
});
it('should validate email format', async () => {
const invalidData = { username: 'test', email: 'invalid-email', nickname: 'Test User', status: UserStatus.ACTIVE };
const result = await service.createUser(invalidData);
expect(result.success).toBe(false);
expect(result.error_code).toBe('VALIDATION_ERROR');
});
});
describe('updateUser', () => {
it('should update user successfully', async () => {
const updateData = { nickname: 'Updated Name' };
const existingUser = { id: BigInt(1), username: 'test', email: 'test@example.com' };
const updatedUser = { ...existingUser, ...updateData };
mockUsersService.findOne.mockResolvedValue(existingUser);
mockUsersService.update.mockResolvedValue(updatedUser);
const result = await service.updateUser(BigInt(1), updateData);
expect(result.success).toBe(true);
expect(result.data).toEqual({ ...updatedUser, id: '1' });
expect(mockUsersService.update).toHaveBeenCalledWith(BigInt(1), updateData);
});
it('should return error when user not found', async () => {
mockUsersService.findOne.mockResolvedValue(null);
const result = await service.updateUser(BigInt(999), { nickname: 'New Name' });
expect(result.success).toBe(false);
expect(result.error_code).toBe('USER_NOT_FOUND');
});
it('should handle empty update data', async () => {
const result = await service.updateUser(BigInt(1), {});
expect(result.success).toBe(false);
expect(result.error_code).toBe('VALIDATION_ERROR');
expect(result.message).toContain('No valid fields to update');
});
});
describe('deleteUser', () => {
it('should delete user successfully', async () => {
const existingUser = { id: BigInt(1), username: 'test' };
mockUsersService.findOne.mockResolvedValue(existingUser);
mockUsersService.remove.mockResolvedValue(undefined);
const result = await service.deleteUser(BigInt(1));
expect(result.success).toBe(true);
expect(result.data.deleted).toBe(true);
expect(result.data.id).toBe('1');
expect(mockUsersService.remove).toHaveBeenCalledWith(BigInt(1));
});
it('should return error when user not found', async () => {
mockUsersService.findOne.mockResolvedValue(null);
const result = await service.deleteUser(BigInt(999));
expect(result.success).toBe(false);
expect(result.error_code).toBe('USER_NOT_FOUND');
});
});
describe('searchUsers', () => {
it('should search users successfully', async () => {
const mockUsers = [
{ id: BigInt(1), username: 'testuser', email: 'test@example.com' }
];
mockUsersService.search.mockResolvedValue(mockUsers);
const result = await service.searchUsers('test', 10);
expect(result.success).toBe(true);
expect(result.data.items).toEqual(mockUsers.map(u => ({ ...u, id: u.id.toString() })));
expect(mockUsersService.search).toHaveBeenCalledWith('test', 10);
});
it('should handle empty search term', async () => {
const result = await service.searchUsers('', 10);
expect(result.success).toBe(false);
expect(result.error_code).toBe('VALIDATION_ERROR');
expect(result.message).toContain('Search term cannot be empty');
});
it('should apply search limit', async () => {
mockUsersService.search.mockResolvedValue([]);
await service.searchUsers('test', 200); // 超过限制
expect(mockUsersService.search).toHaveBeenCalledWith('test', 100);
});
});
});
describe('User Profile Management', () => {
describe('getUserProfileList', () => {
it('should return paginated profile list', async () => {
const mockProfiles = [
{ id: BigInt(1), user_id: '1', bio: 'Test bio' }
];
mockUserProfilesService.findAll.mockResolvedValue(mockProfiles);
mockUserProfilesService.count.mockResolvedValue(1);
const result = await service.getUserProfileList(10, 0);
expect(result.success).toBe(true);
expect(result.data.items).toEqual(mockProfiles.map(p => ({ ...p, id: p.id.toString() })));
});
});
describe('createUserProfile', () => {
it('should create profile successfully', async () => {
const profileData = {
user_id: '1',
bio: 'Test bio',
current_map: 'plaza',
pos_x: 100,
pos_y: 200
};
const createdProfile = { ...profileData, id: BigInt(1) };
mockUserProfilesService.create.mockResolvedValue(createdProfile);
const result = await service.createUserProfile(profileData);
expect(result.success).toBe(true);
expect(result.data).toEqual({ ...createdProfile, id: '1' });
});
it('should validate position coordinates', async () => {
const invalidData = {
user_id: '1',
bio: 'Test',
pos_x: 'invalid' as any,
pos_y: 100
};
const result = await service.createUserProfile(invalidData);
expect(result.success).toBe(false);
expect(result.error_code).toBe('VALIDATION_ERROR');
});
});
describe('getUserProfilesByMap', () => {
it('should return profiles by map', async () => {
const mockProfiles = [
{ id: BigInt(1), user_id: '1', current_map: 'plaza' }
];
mockUserProfilesService.findByMap.mockResolvedValue(mockProfiles);
mockUserProfilesService.count.mockResolvedValue(1);
const result = await service.getUserProfilesByMap('plaza', 10, 0);
expect(result.success).toBe(true);
expect(result.data.items).toEqual(mockProfiles.map(p => ({ ...p, id: p.id.toString() })));
expect(mockUserProfilesService.findByMap).toHaveBeenCalledWith('plaza', undefined, 10, 0);
});
it('should validate map name', async () => {
const result = await service.getUserProfilesByMap('', 10, 0);
expect(result.success).toBe(false);
expect(result.error_code).toBe('VALIDATION_ERROR');
expect(result.message).toContain('Map name cannot be empty');
});
});
});
describe('Zulip Account Management', () => {
describe('getZulipAccountList', () => {
it('should return paginated account list', async () => {
const mockAccounts = [
{ id: '1', gameUserId: '1', zulipEmail: 'test@zulip.com' }
];
mockZulipAccountsService.findMany.mockResolvedValue({
accounts: mockAccounts,
total: 1
});
const result = await service.getZulipAccountList(10, 0);
expect(result.success).toBe(true);
expect(result.data.items).toEqual(mockAccounts);
expect(result.data.total).toBe(1);
});
});
describe('createZulipAccount', () => {
it('should create account successfully', async () => {
const accountData = {
gameUserId: '1',
zulipUserId: 123,
zulipEmail: 'test@zulip.com',
zulipFullName: 'Test User',
zulipApiKeyEncrypted: 'encrypted_key',
status: 'active' as const
};
const createdAccount = { ...accountData, id: '1' };
mockZulipAccountsService.create.mockResolvedValue(createdAccount);
const result = await service.createZulipAccount(accountData);
expect(result.success).toBe(true);
expect(result.data).toEqual(createdAccount);
});
it('should validate required fields', async () => {
const invalidData = {
gameUserId: '',
zulipUserId: 123,
zulipEmail: 'test@zulip.com',
zulipFullName: 'Test',
zulipApiKeyEncrypted: 'key',
status: 'active' as const
};
const result = await service.createZulipAccount(invalidData);
expect(result.success).toBe(false);
expect(result.error_code).toBe('VALIDATION_ERROR');
});
});
describe('batchUpdateZulipAccountStatus', () => {
it('should update multiple accounts successfully', async () => {
const ids = ['1', '2'];
const status = 'active';
const reason = 'Test update';
mockZulipAccountsService.update
.mockResolvedValueOnce({ id: '1', status: 'active' })
.mockResolvedValueOnce({ id: '2', status: 'active' });
const result = await service.batchUpdateZulipAccountStatus(ids, status, reason);
expect(result.success).toBe(true);
expect(result.data.total).toBe(2);
expect(result.data.success).toBe(2);
expect(result.data.failed).toBe(0);
expect(result.data.results).toHaveLength(2);
});
it('should handle partial failures', async () => {
const ids = ['1', '2'];
const status = 'active';
const reason = 'Test update';
mockZulipAccountsService.update
.mockResolvedValueOnce({ id: '1', status: 'active' })
.mockRejectedValueOnce(new Error('Update failed'));
const result = await service.batchUpdateZulipAccountStatus(ids, status, reason);
expect(result.success).toBe(true);
expect(result.data.total).toBe(2);
expect(result.data.success).toBe(1);
expect(result.data.failed).toBe(1);
expect(result.data.errors).toHaveLength(1);
});
it('should validate batch data', async () => {
const ids: string[] = [];
const status = 'active';
const reason = 'Test';
const result = await service.batchUpdateZulipAccountStatus(ids, status, reason);
expect(result.success).toBe(false);
expect(result.error_code).toBe('VALIDATION_ERROR');
expect(result.message).toContain('No account IDs provided');
});
});
describe('getZulipAccountStatistics', () => {
it('should return statistics successfully', async () => {
const mockStats = {
active: 10,
inactive: 5,
suspended: 2,
error: 1,
total: 18
};
mockZulipAccountsService.getStatusStatistics.mockResolvedValue(mockStats);
const result = await service.getZulipAccountStatistics();
expect(result.success).toBe(true);
expect(result.data).toEqual(mockStats);
});
});
});
// describe('Health Check', () => {
// describe('healthCheck', () => {
// it('should return healthy status', async () => {
// const result = await service.healthCheck();
// expect(result.success).toBe(true);
// expect(result.data.status).toBe('healthy');
// expect(result.data.timestamp).toBeDefined();
// expect(result.data.services).toBeDefined();
// });
// });
// });
describe('Error Handling', () => {
it('should handle service injection errors', () => {
expect(service).toBeDefined();
expect(service['usersService']).toBeDefined();
expect(service['userProfilesService']).toBeDefined();
expect(service['zulipAccountsService']).toBeDefined();
});
it('should format BigInt IDs correctly', async () => {
const mockUser = { id: BigInt(123456789012345), username: 'test' };
mockUsersService.findOne.mockResolvedValue(mockUser);
const result = await service.getUserById(BigInt('123456789012345'));
expect(result.success).toBe(true);
expect(result.data.id).toBe('123456789012345');
});
it('should handle concurrent operations', async () => {
const mockUser = { id: BigInt(1), username: 'test' };
mockUsersService.findOne.mockResolvedValue(mockUser);
const promises = [
service.getUserById(BigInt(1)),
service.getUserById(BigInt(1)),
service.getUserById(BigInt(1))
];
const results = await Promise.all(promises);
results.forEach(result => {
expect(result.success).toBe(true);
expect(result.data.id).toBe('1');
});
});
});
});