feat:实现管理员系统核心功能

- 添加管理员数据库管理控制器和服务
- 实现管理员操作日志记录系统
- 添加数据库异常处理过滤器
- 完善管理员权限验证和响应格式
- 添加全面的属性测试覆盖
This commit is contained in:
moyin
2026-01-08 23:05:34 +08:00
parent 0f37130832
commit 6924416bbd
34 changed files with 9481 additions and 199 deletions

View File

@@ -0,0 +1,658 @@
/**
* 权限验证属性测试
*
* Property 10: 权限验证严格性
* Property 15: 并发请求限流
*
* Validates: Requirements 5.1, 8.4
*
* 测试目标:
* - 验证权限验证机制的严格性和一致性
* - 确保并发请求限流保护有效
* - 验证权限边界和异常情况处理
*
* 最近修改:
* - 2026-01-08: 注释规范优化 - 修正@author字段更新版本号和修改记录 (修改者: moyin)
* - 2026-01-08: 功能新增 - 创建权限验证属性测试 (修改者: assistant)
*
* @author moyin
* @version 1.0.1
* @since 2026-01-08
* @lastModified 2026-01-08
*/
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AdminDatabaseController } from '../../controllers/admin_database.controller';
import { DatabaseManagementService } from '../../services/database_management.service';
import { AdminOperationLogService } from '../../services/admin_operation_log.service';
import { AdminOperationLogInterceptor } from '../../admin_operation_log.interceptor';
import { AdminDatabaseExceptionFilter } from '../../admin_database_exception.filter';
import { AdminGuard } from '../../admin.guard';
import { UserStatus } from '../../../../core/db/users/user_status.enum';
import {
PropertyTestRunner,
PropertyTestGenerators,
PropertyTestAssertions,
DEFAULT_PROPERTY_CONFIG
} from './admin_property_test.base';
describe('Property Test: 权限验证功能', () => {
let app: INestApplication;
let module: TestingModule;
let controller: AdminDatabaseController;
let mockAdminGuard: any;
let requestCount = 0;
let concurrentRequests = new Set<string>();
beforeAll(async () => {
requestCount = 0;
concurrentRequests.clear();
mockAdminGuard = {
canActivate: jest.fn().mockImplementation((context) => {
const request = context.switchToHttp().getRequest();
const requestId = request.headers['x-request-id'] || `req_${Date.now()}_${Math.random()}`;
// 模拟权限验证逻辑
const authHeader = request.headers.authorization;
const adminRole = request.headers['x-admin-role'];
const adminId = request.headers['x-admin-id'];
// 并发请求跟踪
if (concurrentRequests.has(requestId)) {
return false; // 重复请求
}
concurrentRequests.add(requestId);
// 模拟请求完成后清理
setTimeout(() => {
concurrentRequests.delete(requestId);
}, 100);
requestCount++;
// 权限验证规则
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return false;
}
if (!adminRole || !['super_admin', 'admin', 'moderator'].includes(adminRole)) {
return false;
}
if (!adminId || adminId.length < 3) {
return false;
}
// 模拟频率限制每秒最多10个请求
const now = Date.now();
const windowStart = Math.floor(now / 1000) * 1000;
const recentRequests = Array.from(concurrentRequests).filter(id =>
id.startsWith(`req_${windowStart}`)
);
if (recentRequests.length > 10) {
return false; // 超过频率限制
}
return true;
})
};
module = await Test.createTestingModule({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: ['.env.test', '.env']
})
],
controllers: [AdminDatabaseController],
providers: [
DatabaseManagementService,
{
provide: AdminOperationLogService,
useValue: {
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 })
}
},
{
provide: AdminOperationLogInterceptor,
useValue: {
intercept: jest.fn().mockImplementation((context, next) => next.handle())
}
},
{
provide: 'UsersService',
useValue: {
findAll: jest.fn().mockResolvedValue([]),
findOne: jest.fn().mockImplementation(() => {
const user = PropertyTestGenerators.generateUser();
return Promise.resolve({ ...user, id: BigInt(1) });
}),
create: jest.fn().mockImplementation((userData) => {
return Promise.resolve({ ...userData, id: BigInt(1) });
}),
update: jest.fn().mockImplementation((id, updateData) => {
const user = PropertyTestGenerators.generateUser();
return Promise.resolve({ ...user, ...updateData, id });
}),
remove: jest.fn().mockResolvedValue(undefined),
search: jest.fn().mockResolvedValue([]),
count: jest.fn().mockResolvedValue(0)
}
},
{
provide: 'IUserProfilesService',
useValue: {
findAll: jest.fn().mockResolvedValue([]),
findOne: jest.fn().mockResolvedValue({ id: BigInt(1) }),
create: jest.fn().mockResolvedValue({ id: BigInt(1) }),
update: jest.fn().mockResolvedValue({ id: BigInt(1) }),
remove: jest.fn().mockResolvedValue(undefined),
findByMap: jest.fn().mockResolvedValue([]),
count: jest.fn().mockResolvedValue(0)
}
},
{
provide: 'ZulipAccountsService',
useValue: {
findMany: jest.fn().mockResolvedValue({ accounts: [] }),
findById: jest.fn().mockResolvedValue({ id: '1' }),
create: jest.fn().mockResolvedValue({ id: '1' }),
update: jest.fn().mockResolvedValue({ id: '1' }),
delete: jest.fn().mockResolvedValue(undefined),
getStatusStatistics: jest.fn().mockResolvedValue({
active: 0, inactive: 0, suspended: 0, error: 0, total: 0
})
}
}
]
})
.overrideGuard(AdminGuard)
.useValue(mockAdminGuard)
.compile();
app = module.createNestApplication();
app.useGlobalFilters(new AdminDatabaseExceptionFilter());
await app.init();
controller = module.get<AdminDatabaseController>(AdminDatabaseController);
});
afterAll(async () => {
await app.close();
});
beforeEach(() => {
requestCount = 0;
concurrentRequests.clear();
mockAdminGuard.canActivate.mockClear();
});
describe('Property 10: 权限验证严格性', () => {
it('有效的管理员凭证应该通过验证', async () => {
await PropertyTestRunner.runPropertyTest(
'有效凭证权限验证',
() => {
const roles = ['super_admin', 'admin', 'moderator'];
return {
authToken: `Bearer token_${Math.random().toString(36).substring(7)}`,
adminRole: roles[Math.floor(Math.random() * roles.length)],
adminId: `admin_${Math.floor(Math.random() * 1000) + 100}`
};
},
async ({ authToken, adminRole, adminId }) => {
// 模拟设置请求头
const mockRequest = {
headers: {
authorization: authToken,
'x-admin-role': adminRole,
'x-admin-id': adminId,
'x-request-id': `req_${Date.now()}_${Math.random()}`
}
};
const mockContext = {
switchToHttp: () => ({
getRequest: () => mockRequest
})
};
const canActivate = mockAdminGuard.canActivate(mockContext);
expect(canActivate).toBe(true);
},
{ ...DEFAULT_PROPERTY_CONFIG, iterations: 30 }
);
});
it('无效的认证令牌应该被拒绝', async () => {
await PropertyTestRunner.runPropertyTest(
'无效令牌权限拒绝',
() => {
const invalidTokens = [
'', // 空令牌
'InvalidToken', // 不是Bearer格式
'Bearer', // 只有Bearer前缀
'Basic dGVzdA==', // 错误的认证类型
null,
undefined
];
return {
authToken: invalidTokens[Math.floor(Math.random() * invalidTokens.length)],
adminRole: 'admin',
adminId: 'admin_123'
};
},
async ({ authToken, adminRole, adminId }) => {
const mockRequest = {
headers: {
authorization: authToken,
'x-admin-role': adminRole,
'x-admin-id': adminId,
'x-request-id': `req_${Date.now()}_${Math.random()}`
}
};
const mockContext = {
switchToHttp: () => ({
getRequest: () => mockRequest
})
};
const canActivate = mockAdminGuard.canActivate(mockContext);
expect(canActivate).toBe(false);
},
{ ...DEFAULT_PROPERTY_CONFIG, iterations: 25 }
);
});
it('无效的管理员角色应该被拒绝', async () => {
await PropertyTestRunner.runPropertyTest(
'无效角色权限拒绝',
() => {
const invalidRoles = [
'user', // 普通用户角色
'guest', // 访客角色
'invalid_role', // 无效角色
'', // 空角色
'ADMIN', // 大小写错误
null,
undefined
];
return {
authToken: 'Bearer valid_token_123',
adminRole: invalidRoles[Math.floor(Math.random() * invalidRoles.length)],
adminId: 'admin_123'
};
},
async ({ authToken, adminRole, adminId }) => {
const mockRequest = {
headers: {
authorization: authToken,
'x-admin-role': adminRole,
'x-admin-id': adminId,
'x-request-id': `req_${Date.now()}_${Math.random()}`
}
};
const mockContext = {
switchToHttp: () => ({
getRequest: () => mockRequest
})
};
const canActivate = mockAdminGuard.canActivate(mockContext);
expect(canActivate).toBe(false);
},
{ ...DEFAULT_PROPERTY_CONFIG, iterations: 25 }
);
});
it('无效的管理员ID应该被拒绝', async () => {
await PropertyTestRunner.runPropertyTest(
'无效管理员ID权限拒绝',
() => {
const invalidIds = [
'', // 空ID
'a', // 太短的ID
'ab', // 太短的ID
null,
undefined,
' ', // 只有空格
'id with spaces' // 包含空格
];
return {
authToken: 'Bearer valid_token_123',
adminRole: 'admin',
adminId: invalidIds[Math.floor(Math.random() * invalidIds.length)]
};
},
async ({ authToken, adminRole, adminId }) => {
const mockRequest = {
headers: {
authorization: authToken,
'x-admin-role': adminRole,
'x-admin-id': adminId,
'x-request-id': `req_${Date.now()}_${Math.random()}`
}
};
const mockContext = {
switchToHttp: () => ({
getRequest: () => mockRequest
})
};
const canActivate = mockAdminGuard.canActivate(mockContext);
expect(canActivate).toBe(false);
},
{ ...DEFAULT_PROPERTY_CONFIG, iterations: 25 }
);
});
it('权限验证应该在所有端点中一致执行', async () => {
await PropertyTestRunner.runPropertyTest(
'权限验证一致性',
() => ({
validAuth: {
authToken: 'Bearer valid_token_123',
adminRole: 'admin',
adminId: 'admin_123'
},
invalidAuth: {
authToken: 'InvalidToken',
adminRole: 'admin',
adminId: 'admin_123'
}
}),
async ({ validAuth, invalidAuth }) => {
// 测试有效权限
const validRequest = {
headers: {
authorization: validAuth.authToken,
'x-admin-role': validAuth.adminRole,
'x-admin-id': validAuth.adminId,
'x-request-id': `req_${Date.now()}_${Math.random()}`
}
};
const validContext = {
switchToHttp: () => ({
getRequest: () => validRequest
})
};
expect(mockAdminGuard.canActivate(validContext)).toBe(true);
// 测试无效权限
const invalidRequest = {
headers: {
authorization: invalidAuth.authToken,
'x-admin-role': invalidAuth.adminRole,
'x-admin-id': invalidAuth.adminId,
'x-request-id': `req_${Date.now()}_${Math.random()}`
}
};
const invalidContext = {
switchToHttp: () => ({
getRequest: () => invalidRequest
})
};
expect(mockAdminGuard.canActivate(invalidContext)).toBe(false);
},
{ ...DEFAULT_PROPERTY_CONFIG, iterations: 20 }
);
});
});
describe('Property 15: 并发请求限流', () => {
it('正常频率的请求应该被允许', async () => {
await PropertyTestRunner.runPropertyTest(
'正常频率请求允许',
() => ({
requestCount: Math.floor(Math.random() * 5) + 1 // 1-5个请求
}),
async ({ requestCount }) => {
const results = [];
for (let i = 0; i < requestCount; i++) {
const mockRequest = {
headers: {
authorization: 'Bearer valid_token_123',
'x-admin-role': 'admin',
'x-admin-id': 'admin_123',
'x-request-id': `req_${Date.now()}_${i}_${Math.random()}`
}
};
const mockContext = {
switchToHttp: () => ({
getRequest: () => mockRequest
})
};
const result = mockAdminGuard.canActivate(mockContext);
results.push(result);
// 小延迟避免时间戳冲突
await new Promise(resolve => setTimeout(resolve, 10));
}
// 正常频率的请求都应该被允许
results.forEach(result => {
expect(result).toBe(true);
});
},
{ ...DEFAULT_PROPERTY_CONFIG, iterations: 20 }
);
});
it('重复的请求ID应该被拒绝', async () => {
await PropertyTestRunner.runPropertyTest(
'重复请求ID拒绝',
() => ({
requestId: `req_${Date.now()}_${Math.random()}`
}),
async ({ requestId }) => {
const mockRequest1 = {
headers: {
authorization: 'Bearer valid_token_123',
'x-admin-role': 'admin',
'x-admin-id': 'admin_123',
'x-request-id': requestId
}
};
const mockRequest2 = {
headers: {
authorization: 'Bearer valid_token_456',
'x-admin-role': 'admin',
'x-admin-id': 'admin_456',
'x-request-id': requestId // 相同的请求ID
}
};
const mockContext1 = {
switchToHttp: () => ({
getRequest: () => mockRequest1
})
};
const mockContext2 = {
switchToHttp: () => ({
getRequest: () => mockRequest2
})
};
// 第一个请求应该成功
const result1 = mockAdminGuard.canActivate(mockContext1);
expect(result1).toBe(true);
// 第二个请求重复ID应该被拒绝
const result2 = mockAdminGuard.canActivate(mockContext2);
expect(result2).toBe(false);
},
{ ...DEFAULT_PROPERTY_CONFIG, iterations: 15 }
);
});
it('并发请求数量应该被正确跟踪', async () => {
await PropertyTestRunner.runPropertyTest(
'并发请求跟踪',
() => ({
concurrentCount: Math.floor(Math.random() * 8) + 3 // 3-10个并发请求
}),
async ({ concurrentCount }) => {
const promises = [];
const results = [];
// 创建并发请求
for (let i = 0; i < concurrentCount; i++) {
const promise = new Promise((resolve) => {
const mockRequest = {
headers: {
authorization: 'Bearer valid_token_123',
'x-admin-role': 'admin',
'x-admin-id': `admin_${i}`,
'x-request-id': `req_${Date.now()}_${i}_${Math.random()}`
}
};
const mockContext = {
switchToHttp: () => ({
getRequest: () => mockRequest
})
};
const result = mockAdminGuard.canActivate(mockContext);
results.push(result);
resolve(result);
});
promises.push(promise);
}
// 等待所有请求完成
await Promise.all(promises);
// 验证并发控制
const successCount = results.filter(r => r === true).length;
const failureCount = results.filter(r => r === false).length;
expect(successCount + failureCount).toBe(concurrentCount);
// 如果并发数超过限制,应该有一些请求被拒绝
if (concurrentCount > 10) {
expect(failureCount).toBeGreaterThan(0);
}
},
{ ...DEFAULT_PROPERTY_CONFIG, iterations: 15 }
);
});
it('请求完成后应该释放并发槽位', async () => {
await PropertyTestRunner.runPropertyTest(
'并发槽位释放',
() => ({}),
async () => {
const initialConcurrentSize = concurrentRequests.size;
// 创建一个请求
const mockRequest = {
headers: {
authorization: 'Bearer valid_token_123',
'x-admin-role': 'admin',
'x-admin-id': 'admin_123',
'x-request-id': `req_${Date.now()}_${Math.random()}`
}
};
const mockContext = {
switchToHttp: () => ({
getRequest: () => mockRequest
})
};
const result = mockAdminGuard.canActivate(mockContext);
expect(result).toBe(true);
// 验证并发计数增加
expect(concurrentRequests.size).toBe(initialConcurrentSize + 1);
// 等待请求完成模拟的100ms超时
await new Promise(resolve => setTimeout(resolve, 150));
// 验证并发计数恢复
expect(concurrentRequests.size).toBe(initialConcurrentSize);
},
{ ...DEFAULT_PROPERTY_CONFIG, iterations: 10 }
);
});
it('不同时间窗口的请求应该独立计算', async () => {
await PropertyTestRunner.runPropertyTest(
'时间窗口独立计算',
() => ({}),
async () => {
const timestamp1 = Date.now();
const timestamp2 = timestamp1 + 1100; // 下一秒
// 第一个时间窗口的请求
const mockRequest1 = {
headers: {
authorization: 'Bearer valid_token_123',
'x-admin-role': 'admin',
'x-admin-id': 'admin_123',
'x-request-id': `req_${timestamp1}_1`
}
};
const mockContext1 = {
switchToHttp: () => ({
getRequest: () => mockRequest1
})
};
const result1 = mockAdminGuard.canActivate(mockContext1);
expect(result1).toBe(true);
// 模拟时间推进
await new Promise(resolve => setTimeout(resolve, 1100));
// 第二个时间窗口的请求
const mockRequest2 = {
headers: {
authorization: 'Bearer valid_token_123',
'x-admin-role': 'admin',
'x-admin-id': 'admin_123',
'x-request-id': `req_${timestamp2}_1`
}
};
const mockContext2 = {
switchToHttp: () => ({
getRequest: () => mockRequest2
})
};
const result2 = mockAdminGuard.canActivate(mockContext2);
expect(result2).toBe(true);
},
{ ...DEFAULT_PROPERTY_CONFIG, iterations: 5 }
);
});
});
});