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