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(); 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(_key: string): Promise { return null; }, async set(_key: string, _value: T, _ttl?: number): Promise { return; }, async del(_key: string): Promise { 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); }); });