feat: 完善管理员系统和用户管理模块

- 更新管理员控制器和数据库管理功能
- 完善管理员操作日志系统
- 添加全面的属性测试覆盖
- 优化用户管理和用户档案服务
- 更新代码检查规范文档

功能改进:
- 增强管理员权限验证
- 完善操作日志记录
- 优化数据库管理接口
- 提升系统安全性和可维护性
This commit is contained in:
moyin
2026-01-09 17:05:08 +08:00
parent 8816b29b0a
commit 5f662ef091
30 changed files with 3881 additions and 599 deletions

View File

@@ -0,0 +1,415 @@
/**
* AdminOperationLogInterceptor 单元测试
*
* 功能描述:
* - 测试管理员操作日志拦截器的所有功能
* - 验证操作拦截和日志记录的正确性
* - 测试成功和失败场景的处理
*
* 职责分离:
* - 拦截器逻辑测试,不涉及具体业务
* - Mock日志服务专注拦截器功能
* - 验证日志记录的完整性和准确性
*
* 最近修改:
* - 2026-01-09: 功能新增 - 创建AdminOperationLogInterceptor单元测试 (修改者: moyin)
*
* @author moyin
* @version 1.0.0
* @since 2026-01-09
* @lastModified 2026-01-09
*/
import { Test, TestingModule } from '@nestjs/testing';
import { ExecutionContext, CallHandler } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { of, throwError } from 'rxjs';
import { AdminOperationLogInterceptor } from './admin_operation_log.interceptor';
import { AdminOperationLogService } from './admin_operation_log.service';
import { LOG_ADMIN_OPERATION_KEY, LogAdminOperationOptions } from './log_admin_operation.decorator';
import { OPERATION_RESULTS } from './admin_constants';
describe('AdminOperationLogInterceptor', () => {
let interceptor: AdminOperationLogInterceptor;
let logService: jest.Mocked<AdminOperationLogService>;
let reflector: jest.Mocked<Reflector>;
const mockLogService = {
createLog: jest.fn(),
};
const mockReflector = {
get: jest.fn(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
AdminOperationLogInterceptor,
{
provide: AdminOperationLogService,
useValue: mockLogService,
},
{
provide: Reflector,
useValue: mockReflector,
},
],
}).compile();
interceptor = module.get<AdminOperationLogInterceptor>(AdminOperationLogInterceptor);
logService = module.get(AdminOperationLogService);
reflector = module.get(Reflector);
});
afterEach(() => {
jest.clearAllMocks();
});
const createMockExecutionContext = (requestData: any = {}) => {
const mockRequest = {
method: 'POST',
url: '/admin/users',
route: { path: '/admin/users' },
params: { id: '1' },
query: { limit: '10' },
body: { username: 'testuser' },
headers: { 'user-agent': 'test-agent' },
user: { id: 'admin1', username: 'admin' },
ip: '127.0.0.1',
...requestData,
};
const mockResponse = {};
const mockContext = {
switchToHttp: () => ({
getRequest: () => mockRequest,
getResponse: () => mockResponse,
}),
getHandler: () => ({}),
} as ExecutionContext;
return { mockContext, mockRequest, mockResponse };
};
const createMockCallHandler = (responseData: any = { success: true }) => {
return {
handle: () => of(responseData),
} as CallHandler;
};
describe('intercept', () => {
it('should pass through when no log options configured', (done) => {
const { mockContext } = createMockExecutionContext();
const mockHandler = createMockCallHandler();
reflector.get.mockReturnValue(undefined);
interceptor.intercept(mockContext, mockHandler).subscribe({
next: (result) => {
expect(result).toEqual({ success: true });
expect(logService.createLog).not.toHaveBeenCalled();
done();
},
});
});
it('should log successful operation', (done) => {
const { mockContext } = createMockExecutionContext();
const mockHandler = createMockCallHandler({ success: true, data: { id: '1' } });
const logOptions: LogAdminOperationOptions = {
operationType: 'CREATE',
targetType: 'users',
description: 'Create user',
};
reflector.get.mockReturnValue(logOptions);
logService.createLog.mockResolvedValue({} as any);
interceptor.intercept(mockContext, mockHandler).subscribe({
next: (result) => {
expect(result).toEqual({ success: true, data: { id: '1' } });
// 验证日志记录调用
expect(logService.createLog).toHaveBeenCalledWith(
expect.objectContaining({
adminUserId: 'admin1',
adminUsername: 'admin',
operationType: 'CREATE',
targetType: 'users',
operationDescription: 'Create user',
httpMethodPath: 'POST /admin/users',
operationResult: OPERATION_RESULTS.SUCCESS,
targetId: '1',
requestParams: expect.objectContaining({
params: { id: '1' },
query: { limit: '10' },
body: { username: 'testuser' },
}),
afterData: { success: true, data: { id: '1' } },
clientIp: '127.0.0.1',
userAgent: 'test-agent',
})
);
done();
},
});
});
it('should log failed operation', (done) => {
const { mockContext } = createMockExecutionContext();
const error = new Error('Operation failed');
const mockHandler = {
handle: () => throwError(() => error),
} as CallHandler;
const logOptions: LogAdminOperationOptions = {
operationType: 'UPDATE',
targetType: 'users',
description: 'Update user',
};
reflector.get.mockReturnValue(logOptions);
logService.createLog.mockResolvedValue({} as any);
interceptor.intercept(mockContext, mockHandler).subscribe({
error: (err) => {
expect(err).toBe(error);
// 验证错误日志记录调用
expect(logService.createLog).toHaveBeenCalledWith(
expect.objectContaining({
adminUserId: 'admin1',
adminUsername: 'admin',
operationType: 'UPDATE',
targetType: 'users',
operationDescription: 'Update user',
operationResult: OPERATION_RESULTS.FAILED,
errorMessage: 'Operation failed',
errorCode: 'UNKNOWN_ERROR',
})
);
done();
},
});
});
it('should handle missing admin user', (done) => {
const { mockContext } = createMockExecutionContext({ user: undefined });
const mockHandler = createMockCallHandler();
const logOptions: LogAdminOperationOptions = {
operationType: 'QUERY',
targetType: 'users',
description: 'Query users',
};
reflector.get.mockReturnValue(logOptions);
logService.createLog.mockResolvedValue({} as any);
interceptor.intercept(mockContext, mockHandler).subscribe({
next: () => {
expect(logService.createLog).toHaveBeenCalledWith(
expect.objectContaining({
adminUserId: 'unknown',
adminUsername: 'unknown',
})
);
done();
},
});
});
it('should handle sensitive operations', (done) => {
const { mockContext } = createMockExecutionContext();
const mockHandler = createMockCallHandler();
const logOptions: LogAdminOperationOptions = {
operationType: 'DELETE',
targetType: 'users',
description: 'Delete user',
isSensitive: true,
};
reflector.get.mockReturnValue(logOptions);
logService.createLog.mockResolvedValue({} as any);
interceptor.intercept(mockContext, mockHandler).subscribe({
next: () => {
expect(logService.createLog).toHaveBeenCalledWith(
expect.objectContaining({
isSensitive: true,
})
);
done();
},
});
});
it('should disable request params capture when configured', (done) => {
const { mockContext } = createMockExecutionContext();
const mockHandler = createMockCallHandler();
const logOptions: LogAdminOperationOptions = {
operationType: 'QUERY',
targetType: 'users',
description: 'Query users',
captureRequestParams: false,
};
reflector.get.mockReturnValue(logOptions);
logService.createLog.mockResolvedValue({} as any);
interceptor.intercept(mockContext, mockHandler).subscribe({
next: () => {
expect(logService.createLog).toHaveBeenCalledWith(
expect.objectContaining({
requestParams: undefined,
})
);
done();
},
});
});
it('should disable after data capture when configured', (done) => {
const { mockContext } = createMockExecutionContext();
const mockHandler = createMockCallHandler({ data: 'sensitive' });
const logOptions: LogAdminOperationOptions = {
operationType: 'QUERY',
targetType: 'users',
description: 'Query users',
captureAfterData: false,
};
reflector.get.mockReturnValue(logOptions);
logService.createLog.mockResolvedValue({} as any);
interceptor.intercept(mockContext, mockHandler).subscribe({
next: () => {
expect(logService.createLog).toHaveBeenCalledWith(
expect.objectContaining({
afterData: undefined,
})
);
done();
},
});
});
it('should extract affected records from response', (done) => {
const { mockContext } = createMockExecutionContext();
const responseData = {
success: true,
data: {
items: [{ id: '1' }, { id: '2' }, { id: '3' }],
total: 3,
},
};
const mockHandler = createMockCallHandler(responseData);
const logOptions: LogAdminOperationOptions = {
operationType: 'QUERY',
targetType: 'users',
description: 'Query users',
};
reflector.get.mockReturnValue(logOptions);
logService.createLog.mockResolvedValue({} as any);
interceptor.intercept(mockContext, mockHandler).subscribe({
next: () => {
expect(logService.createLog).toHaveBeenCalledWith(
expect.objectContaining({
affectedRecords: 3, // Should extract from items array length
})
);
done();
},
});
});
it('should handle log service errors gracefully', (done) => {
const { mockContext } = createMockExecutionContext();
const mockHandler = createMockCallHandler();
const logOptions: LogAdminOperationOptions = {
operationType: 'CREATE',
targetType: 'users',
description: 'Create user',
};
reflector.get.mockReturnValue(logOptions);
logService.createLog.mockRejectedValue(new Error('Log service error'));
// 即使日志记录失败,原始操作也应该成功
interceptor.intercept(mockContext, mockHandler).subscribe({
next: (result) => {
expect(result).toEqual({ success: true });
expect(logService.createLog).toHaveBeenCalled();
done();
},
});
});
it('should extract target ID from different sources', (done) => {
const { mockContext } = createMockExecutionContext({
params: {},
body: { id: 'body-id' },
query: { id: 'query-id' },
});
const mockHandler = createMockCallHandler();
const logOptions: LogAdminOperationOptions = {
operationType: 'UPDATE',
targetType: 'users',
description: 'Update user',
};
reflector.get.mockReturnValue(logOptions);
logService.createLog.mockResolvedValue({} as any);
interceptor.intercept(mockContext, mockHandler).subscribe({
next: () => {
expect(logService.createLog).toHaveBeenCalledWith(
expect.objectContaining({
targetId: 'body-id', // Should prefer body over query
})
);
done();
},
});
});
it('should handle missing route information', (done) => {
const { mockContext } = createMockExecutionContext({
route: undefined,
url: '/admin/custom-endpoint',
});
const mockHandler = createMockCallHandler();
const logOptions: LogAdminOperationOptions = {
operationType: 'QUERY',
targetType: 'custom',
description: 'Custom operation',
};
reflector.get.mockReturnValue(logOptions);
logService.createLog.mockResolvedValue({} as any);
interceptor.intercept(mockContext, mockHandler).subscribe({
next: () => {
expect(logService.createLog).toHaveBeenCalledWith(
expect.objectContaining({
httpMethodPath: 'POST /admin/custom-endpoint',
})
);
done();
},
});
});
});
});