docs: 基本完成初版数据库设计
This commit is contained in:
@@ -0,0 +1,306 @@
|
||||
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<string, Account>([
|
||||
[
|
||||
'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<NestExpressApplication>({
|
||||
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<NestExpressApplication>({
|
||||
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);
|
||||
});
|
||||
@@ -1,19 +1,18 @@
|
||||
import 'reflect-metadata';
|
||||
import assert from 'node:assert/strict';
|
||||
import { test } from 'node:test';
|
||||
import { Test } from '@nestjs/testing';
|
||||
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 { testConfig } from './test-config';
|
||||
|
||||
test('服务可以启动并通过带版本前缀的 HTTP 健康检查', async (context) => {
|
||||
const testingModule = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
const app = testingModule.createNestApplication({ logger: false });
|
||||
const app = await createApplication(
|
||||
testConfig,
|
||||
new AppLogger('error', () => {}),
|
||||
);
|
||||
context.after(async () => {
|
||||
await app.close();
|
||||
});
|
||||
configureApplication(app);
|
||||
await app.listen(0, '127.0.0.1');
|
||||
const origin = await app.getUrl();
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { test } from 'node:test';
|
||||
import { AppLogger } from '../src/logging/app-logger';
|
||||
|
||||
test('日志为单行 JSON,递归脱敏并安全处理循环对象与 bigint', () => {
|
||||
const output: string[] = [];
|
||||
const logger = new AppLogger('info', (line) => output.push(line));
|
||||
const nested: Record<string, unknown> = {
|
||||
password: 'private-password',
|
||||
amount: 9n,
|
||||
};
|
||||
nested.self = nested;
|
||||
logger.log('test_event', {
|
||||
authorization: 'Bearer private-token',
|
||||
nested,
|
||||
body: { code: 'login-secret' },
|
||||
});
|
||||
logger.debug('hidden');
|
||||
assert.equal(output.length, 1);
|
||||
const log = JSON.parse(output[0]!);
|
||||
assert.equal(log.context.authorization, '[REDACTED]');
|
||||
assert.equal(log.context.nested.password, '[REDACTED]');
|
||||
assert.equal(log.context.nested.self, '[Circular]');
|
||||
assert.equal(log.context.nested.amount, '9');
|
||||
assert.equal(log.context.body, '[REDACTED]');
|
||||
assert.ok(!output[0]!.includes('private-'));
|
||||
assert.ok(!output[0]!.includes('login-secret'));
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { test } from 'node:test';
|
||||
import { getServerConfig } from '../src/config/server-config';
|
||||
|
||||
const env = { JWT_SECRET: 'test-configuration-secret-12345678901234567890' };
|
||||
|
||||
test('启动配置必须提供有效 JWT 密钥,不使用共享默认密钥', () => {
|
||||
for (const secret of [
|
||||
undefined,
|
||||
'',
|
||||
'short',
|
||||
'change-me-123456789012345678901234567890',
|
||||
]) {
|
||||
assert.throws(
|
||||
() =>
|
||||
getServerConfig(
|
||||
secret === undefined ? {} : { JWT_SECRET: secret },
|
||||
),
|
||||
/JWT_SECRET/,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('Swagger 开发默认开启,生产默认关闭,支持显式开关', () => {
|
||||
assert.equal(getServerConfig(env).swaggerEnabled, true);
|
||||
assert.equal(
|
||||
getServerConfig({ ...env, NODE_ENV: 'production' }).swaggerEnabled,
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
getServerConfig({ ...env, SWAGGER_ENABLED: 'false' }).swaggerEnabled,
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test('拒绝非法 body、JWT 有效期、日志级别、布尔开关和跨域配置', () => {
|
||||
for (const entry of [
|
||||
{ API_BODY_LIMIT_BYTES: '0' },
|
||||
{ API_BODY_LIMIT_BYTES: '1e6' },
|
||||
{ JWT_EXPIRES_IN_SECONDS: '0' },
|
||||
{ SWAGGER_ENABLED: 'yes' },
|
||||
{ LOG_LEVEL: 'all' },
|
||||
{ NODE_ENV: 'staging' },
|
||||
{ JWT_ISSUER: ' ' },
|
||||
{ CORS_ORIGINS: '*' },
|
||||
{ CORS_ORIGINS: 'https://example.com/path' },
|
||||
{ TRUSTED_PROXY_CIDRS: 'true' },
|
||||
])
|
||||
assert.throws(() => getServerConfig({ ...env, ...entry }));
|
||||
assert.deepEqual(
|
||||
getServerConfig({
|
||||
...env,
|
||||
CORS_ORIGINS: 'https://example.com,http://localhost:3001',
|
||||
}).corsOrigins,
|
||||
['https://example.com', 'http://localhost:3001'],
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
import { getServerConfig } from '../src/config/server-config';
|
||||
|
||||
/** 测试独立配置,不读取本地 .env;固定测试密钥不得用于开发或生产。 */
|
||||
export const testConfig = getServerConfig({
|
||||
NODE_ENV: 'test',
|
||||
JWT_SECRET: 'test-only-secret-123456789012345678901234567890',
|
||||
});
|
||||
Reference in New Issue
Block a user