import 'reflect-metadata'; import assert from 'node:assert/strict'; import { test } from 'node:test'; import { Body, Controller, Get, Post } from '@nestjs/common'; import type { NestExpressApplication } from '@nestjs/platform-express'; import { Test } from '@nestjs/testing'; import { JwtService } from '@nestjs/jwt'; import type { OpenAPIObject } from '@nestjs/swagger'; import { IsString, MaxLength } from 'class-validator'; import { AppModule } from '../src/app.module'; import { configureApplication } from '../src/app.setup'; import { createApplication } from '../src/app.factory'; import { AppLogger } from '../src/logging/app-logger'; import { ACCOUNT_READER, type Account } from '../src/auth/account-reader'; import { CurrentUser, Public, Roles } from '../src/auth/auth.decorators'; import { JwtTokenService } from '../src/auth/jwt-token.service'; import type { AuthenticatedUser } from '../src/common/http-request'; import { testConfig } from './test-config'; class EchoDto { @IsString() @MaxLength(100) message!: string; } // 只在测试模块注册,生产不增加探测身份或签发 token 的路由。 @Controller('probe') class ProbeController { @Get('me') me(@CurrentUser() user: AuthenticatedUser) { return user; } @Get('admin') @Roles('admin') admin() { return { ok: true }; } @Public() @Post('echo') echo(@Body() body: EchoDto) { return body; } @Public() @Get('failure') failure() { throw new Error('private-internal-error'); } } test('HTTP 中间件、校验、日志、Swagger 与 JWT 全局守卫', async (context) => { const logs: string[] = []; const logger = new AppLogger('info', (line) => logs.push(line)); const config = { ...testConfig, bodyLimitBytes: 1024, corsOrigins: ['http://localhost:3001'], }; // 用可变账户快照模拟禁用、角色和凭证失效,不依赖真实用户或开发数据库。 const accounts = new Map([ [ 'UserA', { id: 'UserA', authVersion: 1, role: 'user', status: 'active' }, ], [ 'AdminA', { id: 'AdminA', authVersion: 1, role: 'admin', status: 'active' }, ], ]); const module = await Test.createTestingModule({ imports: [AppModule.forRoot(config)], controllers: [ProbeController], }) .overrideProvider(ACCOUNT_READER) .useValue({ findById: async (id: string) => accounts.get(id) ?? null }) .compile(); const app = module.createNestApplication({ bodyParser: false, logger, }); context.after(() => app.close()); configureApplication(app, config, logger); await app.listen(0, '127.0.0.1'); const origin = await app.getUrl(); const tokens = app.get(JwtTokenService); const jwt = app.get(JwtService); const userToken = await tokens.signAccessToken('UserA', 1); const request = (path: string, options?: RequestInit) => fetch(`${origin}${path}`, { ...options, signal: AbortSignal.timeout(5000), }); const health = await request('/api/v1/health?code=private-login-code', { headers: { 'x-request-id': 'request_test_123', Origin: 'http://localhost:3001', }, }); assert.equal(health.status, 200); assert.equal(health.headers.get('x-request-id'), 'request_test_123'); assert.equal( health.headers.get('access-control-allow-origin'), 'http://localhost:3001', ); assert.equal(health.headers.get('x-content-type-options'), 'nosniff'); assert.equal(health.headers.get('x-powered-by'), null); const deniedOrigin = await request('/api/v1/health', { headers: { Origin: 'https://untrusted.example' }, }); assert.equal(deniedOrigin.headers.get('access-control-allow-origin'), null); const missingToken = await request('/api/v1/probe/me'); assert.equal(missingToken.status, 401); const errorBody = (await missingToken.json()) as { code: number; requestId: string; }; assert.equal(errorBody.code, 401); assert.equal(errorBody.requestId, missingToken.headers.get('x-request-id')); const authenticated = await request('/api/v1/probe/me', { headers: { Authorization: `Bearer ${userToken}` }, }); assert.equal(authenticated.status, 200); assert.deepEqual(await authenticated.json(), { userId: 'UserA', authVersion: 1, role: 'user', }); assert.equal( ( await request('/api/v1/probe/admin', { headers: { Authorization: `Bearer ${userToken}` }, }) ).status, 403, ); assert.equal( ( await request('/api/v1/probe/admin', { headers: { Authorization: `Bearer ${await tokens.signAccessToken('AdminA', 1)}`, }, }) ).status, 200, ); // 用同一 JWT 库构造异常凭证,验证算法、签发方、受众和字段校验均会生效。 const sign = (claims: object, overrides: object = {}) => jwt.signAsync(claims, { secret: config.jwtSecret, algorithm: 'HS256', issuer: config.jwtIssuer, audience: config.jwtAudience, expiresIn: 60, ...overrides, }); for (const token of [ 'invalid-token', await tokens.signAccessToken('UserA', 0), await tokens.signAccessToken('DeletedUser', 1), await sign({ sub: 'UserA', authVersion: 1 }, { expiresIn: -1 }), await sign({ sub: 'UserA', authVersion: 1 }, { audience: 'other' }), await sign({ sub: 'UserA', authVersion: 1 }, { issuer: 'other' }), await sign({ sub: 'UserA', authVersion: 1 }, { algorithm: 'HS384' }), await sign( { sub: 'UserA', authVersion: 1 }, { secret: 'wrong-secret' }, ), await sign({ sub: 'UserA', authVersion: '1' }), await jwt.signAsync( { sub: 'UserA', authVersion: 1 }, { secret: config.jwtSecret, issuer: config.jwtIssuer, audience: config.jwtAudience, }, ), ]) assert.equal( ( await request('/api/v1/probe/me', { headers: { Authorization: `Bearer ${token}` }, }) ).status, 401, ); // 即使签名正确,token 自报 admin 也不能覆盖服务端账户中的 user 角色。 const forgedRole = await sign({ sub: 'UserA', authVersion: 1, role: 'admin', }); assert.equal( ( await request('/api/v1/probe/admin', { headers: { Authorization: `Bearer ${forgedRole}` }, }) ).status, 403, ); accounts.get('UserA')!.status = 'disabled'; assert.equal( ( await request('/api/v1/probe/me', { headers: { Authorization: `Bearer ${userToken}` }, }) ).status, 401, ); const echo = (body: string, contentType = 'application/json') => request('/api/v1/probe/echo', { method: 'POST', headers: { 'Content-Type': contentType }, body, }); assert.equal((await echo('{"message":"ok"}')).status, 201); assert.equal( (await echo('message=ok', 'application/x-www-form-urlencoded')).status, 201, ); assert.equal((await echo('{"message":12}')).status, 400); assert.equal((await echo('{"message":"ok","extra":true}')).status, 400); const malformed = await echo('{broken-json'); assert.equal(malformed.status, 400); assert.ok(malformed.headers.get('x-request-id')); assert.equal( (await echo(JSON.stringify({ message: 'x'.repeat(2000) }))).status, 413, ); const failure = await request('/api/v1/probe/failure'); assert.equal(failure.status, 500); assert.ok(!(await failure.text()).includes('private-internal-error')); assert.equal((await request('/api-docs')).status, 200); const openapi = (await ( await request('/api-docs-json') ).json()) as OpenAPIObject; assert.ok(openapi.paths['/api/v1/health']); assert.deepEqual(openapi.paths['/api/v1/health']?.get?.security, []); assert.equal( ( openapi.components?.securitySchemes?.['access-token'] as { scheme: string; } ).scheme, 'bearer', ); // 检查完整请求链日志,确认解析失败仍有日志,且查询参数、异常原文和 token 未泄漏。 const requestLogs = logs.filter( (line) => JSON.parse(line).message === 'http_request_completed', ); assert.ok( requestLogs.some( (line) => JSON.parse(line).context.status_code === 413, ), ); assert.ok(!logs.join('\n').includes('private-login-code')); assert.ok(!logs.join('\n').includes('private-internal-error')); assert.ok(!logs.join('\n').includes(userToken)); }); test('生产默认不暴露 Swagger;账户查询未接入时不放行有效 JWT', async (context) => { const config = { ...testConfig, nodeEnv: 'production' as const, swaggerEnabled: false, }; const module = await Test.createTestingModule({ imports: [AppModule.forRoot(config)], controllers: [ProbeController], }).compile(); const logger = new AppLogger('error', () => {}); const app = module.createNestApplication({ bodyParser: false, logger, }); context.after(() => app.close()); configureApplication(app, config, logger); await app.listen(0, '127.0.0.1'); const origin = await app.getUrl(); for (const path of ['/api-docs', '/api-docs-json']) assert.equal((await fetch(`${origin}${path}`)).status, 404); const token = await app.get(JwtTokenService).signAccessToken('UserA', 1); assert.equal( ( await fetch(`${origin}/api/v1/probe/me`, { headers: { Authorization: `Bearer ${token}` }, }) ).status, 503, ); }); test('导入应用工厂不会监听固定端口', async (context) => { const app = await createApplication( testConfig, new AppLogger('error', () => {}), ); context.after(() => app.close()); assert.equal(app.getHttpServer().listening, false); });