Files
wwjcloud-nest-v1/wwjcloud/test/e2e/security.e2e-spec.ts
万物街 127a4db1e3 feat: 完成sys模块迁移,对齐PHP/Java框架
- 重构sys模块架构,严格按admin/api/core分层
- 对齐所有sys实体与数据库表结构
- 实现完整的adminapi控制器,匹配PHP/Java契约
- 修复依赖注入问题,确保服务正确注册
- 添加自动迁移工具和契约验证
- 完善多租户支持和审计功能
- 统一命名规范,与PHP业务逻辑保持一致
2025-09-21 21:29:28 +08:00

226 lines
6.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Controller, Get, Req, UseGuards, Module } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import request from 'supertest';
import { SecurityModule } from '../../src/core/security/securityModule';
import { AdminCheckTokenGuard } from '../../src/core/security/adminCheckToken.guard';
import { ApiCheckTokenGuard } from '../../src/core/security/apiCheckToken.guard';
import { ApiOptionalAuthGuard } from '../../src/core/security/apiOptionalAuth.guard';
import { SiteScopeGuard } from '../../src/core/security/siteScopeGuard';
import { TokenAuthService } from '../../src/core/security/tokenAuth.service';
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { CacheModule as NestCacheModule } from '@nestjs/cache-manager';
@Controller()
class SecurityTestController {
// 管理端受保护路由(仅校验 token
@Get('adminapi/secure')
@UseGuards(AdminCheckTokenGuard)
adminSecure(@Req() req: any) {
return {
appType: req.auth?.('app_type') || req.appType || '',
uid: req.auth?.('uid') ?? req.uid ?? null,
username: req.auth?.('username') || req.username || '',
siteId: req.auth?.('site_id') ?? req.siteId ?? null,
};
}
// 管理端受保护路由(校验 token + 站点隔离)
@Get('adminapi/secure-site')
@UseGuards(AdminCheckTokenGuard, SiteScopeGuard)
adminSecureSite(@Req() req: any) {
return {
appType: req.auth?.('app_type') || req.appType || '',
uid: req.auth?.('uid') ?? req.uid ?? null,
siteId: req.auth?.('site_id') ?? req.siteId ?? null,
};
}
// 前台受保护路由(必须登录)
@Get('api/secure')
@UseGuards(ApiCheckTokenGuard)
apiSecure(@Req() req: any) {
return {
appType: req.auth?.('app_type') || req.appType || '',
memberId: req.auth?.('member_id') ?? req.memberId ?? null,
username: req.auth?.('username') || req.username || '',
siteId: req.auth?.('site_id') ?? req.siteId ?? null,
};
}
// 前台可选登录路由(未登录也可访问)
@Get('api/optional')
@UseGuards(ApiOptionalAuthGuard)
apiOptional(@Req() req: any) {
const hasUser = !!(req.auth?.('member_id') || req.memberId);
return {
appType: req.auth?.('app_type') || req.appType || '',
hasUser,
};
}
}
@Module({
imports: [SecurityModule, NestCacheModule.register()],
controllers: [SecurityTestController],
})
class SecurityTestModule {}
describe('Security Guards (e2e)', () => {
let app: any;
let tokenService: TokenAuthService;
// 内存版 Redis 替身(仅实现 get/set/del
const memory = new Map<string, string>();
const mockRedis = {
async get(key: string) {
return memory.has(key) ? memory.get(key)! : null;
},
async set(key: string, value: string) {
memory.set(key, value);
return 'OK';
},
async del(key: string) {
const existed = memory.delete(key);
return existed ? 1 : 0;
},
} as any;
// 轻量 cache-manager 替身,满足 CACHE_MANAGER 依赖
const mockCache = {
async get<T>(_key: string): Promise<T | null> {
return null;
},
async set<T>(_key: string, _value: T, _ttl?: number): Promise<void> {
return;
},
async del(_key: string): Promise<void> {
return;
},
} as any;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [SecurityTestModule],
})
.overrideProvider('REDIS_CLIENT')
.useValue(mockRedis)
.overrideProvider(CACHE_MANAGER)
.useValue(mockCache)
.compile();
app = (await moduleFixture.createNestApplication()).getHttpServer
? await moduleFixture.createNestApplication()
: await moduleFixture.createNestApplication();
await app.init();
tokenService = moduleFixture.get(TokenAuthService);
});
afterAll(async () => {
if (app) {
await app.close();
}
});
it('Admin: 401 when token missing', async () => {
await request(app.getHttpServer()).get('/adminapi/secure').expect(401);
});
it('Admin: 200 with valid token (no site header)', async () => {
const { token } = await tokenService.createToken(
1001,
'admin',
{
uid: 1001,
username: 'root',
},
600,
);
const res = await request(app.getHttpServer())
.get('/adminapi/secure')
.set('token', token)
.expect(200);
expect(res.body.data?.uid ?? res.body.uid).toBe(1001);
const appType = res.body.data?.appType ?? res.body.appType;
expect(appType).toBe('admin');
});
it('Admin: 403 on site mismatch with SiteScopeGuard', async () => {
const { token, params } = await tokenService.createToken(
2002,
'admin',
{
uid: 2002,
username: 'admin2',
site_id: 2,
},
600,
);
// header 指定不同 site-id 触发越权
await request(app.getHttpServer())
.get('/adminapi/secure-site')
.set('token', token)
.set('site-id', '3')
.expect(403);
});
it('Admin: 200 when site matches with SiteScopeGuard', async () => {
const { token } = await tokenService.createToken(
2003,
'admin',
{
uid: 2003,
username: 'admin3',
site_id: 5,
},
600,
);
const res = await request(app.getHttpServer())
.get('/adminapi/secure-site')
.set('token', token)
.set('site-id', '5')
.expect(200);
expect(res.body.data?.siteId ?? res.body.siteId).toBe(5);
});
it('API: 401 when token missing', async () => {
await request(app.getHttpServer()).get('/api/secure').expect(401);
});
it('API: 200 with valid token', async () => {
const { token } = await tokenService.createToken(
3001,
'api',
{
member_id: 3001,
username: 'member1',
site_id: 8,
},
600,
);
const res = await request(app.getHttpServer())
.get('/api/secure')
.set('token', token)
.expect(200);
const memberId = res.body.data?.memberId ?? res.body.memberId;
expect(memberId).toBe(3001);
});
it('API Optional: 200 without token and no user', async () => {
const res = await request(app.getHttpServer())
.get('/api/optional')
.expect(200);
const hasUser = res.body.data?.hasUser ?? res.body.hasUser;
expect(hasUser).toBe(false);
});
});