- 更新管理员控制器和数据库管理功能 - 完善管理员操作日志系统 - 添加全面的属性测试覆盖 - 优化用户管理和用户档案服务 - 更新代码检查规范文档 功能改进: - 增强管理员权限验证 - 完善操作日志记录 - 优化数据库管理接口 - 提升系统安全性和可维护性
593 lines
20 KiB
TypeScript
593 lines
20 KiB
TypeScript
/**
|
||
* 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');
|
||
});
|
||
});
|
||
});
|
||
}); |