feat: 完善管理员系统和用户管理模块
- 更新管理员控制器和数据库管理功能 - 完善管理员操作日志系统 - 添加全面的属性测试覆盖 - 优化用户管理和用户档案服务 - 更新代码检查规范文档 功能改进: - 增强管理员权限验证 - 完善操作日志记录 - 优化数据库管理接口 - 提升系统安全性和可维护性
This commit is contained in:
415
src/business/admin/admin_operation_log.interceptor.spec.ts
Normal file
415
src/business/admin/admin_operation_log.interceptor.spec.ts
Normal 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();
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user