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