feat(GOV-009): 建立后端契约与健康检查

This commit is contained in:
2026-08-13 17:26:51 +08:00
parent 3fc703de0b
commit 420aa22342
32 changed files with 1625 additions and 8 deletions
+85
View File
@@ -0,0 +1,85 @@
import type { INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import request from 'supertest';
import { afterEach, describe, expect, it } from 'vitest';
import { HealthController } from '../src/health.controller';
import {
HealthService,
MYSQL_HEALTH_PORT,
REDIS_HEALTH_PORT,
type DependencyHealthPort,
} from '../src/health.service';
describe('GET /api/v1/health', () => {
let app: INestApplication | undefined;
afterEach(async () => {
await app?.close();
app = undefined;
});
async function createApp(
redis: DependencyHealthPort,
mysql: DependencyHealthPort,
): Promise<INestApplication> {
const moduleRef = await Test.createTestingModule({
controllers: [HealthController],
providers: [
HealthService,
{ provide: REDIS_HEALTH_PORT, useValue: redis },
{ provide: MYSQL_HEALTH_PORT, useValue: mysql },
],
}).compile();
const nestApp = moduleRef.createNestApplication();
nestApp.setGlobalPrefix('api/v1');
await nestApp.init();
return nestApp;
}
it('依赖健康时返回 200 与 RocketMQ disabled', async () => {
app = await createApp(
{ check: async () => ({ status: 'healthy', detail: 'PONG' }) },
{ check: async () => ({ status: 'healthy', detail: 'MySQL 5.7' }) },
);
const response = await request(app.getHttpServer()).get('/api/v1/health').expect(200);
expect(response.body).toMatchObject({
status: 'healthy',
process: 'api',
dependencies: {
redis: { status: 'healthy' },
mysql: { status: 'healthy' },
rocketMq: { status: 'disabled' },
},
});
});
it('依赖失败时返回 503 且不泄漏错误中的敏感值', async () => {
app = await createApp(
{ check: async () => ({ status: 'unavailable', detail: 'TEST_SENSITIVE_VALUE' }) },
{ check: async () => ({ status: 'healthy', detail: 'MySQL 5.7' }) },
);
const response = await request(app.getHttpServer()).get('/api/v1/health').expect(503);
const serialized = JSON.stringify(response.body);
expect(response.body.status).toBe('unavailable');
expect(serialized).not.toContain('TEST_SENSITIVE_VALUE');
});
it('有意禁用的可选依赖不降低 API 总体状态', async () => {
app = await createApp(
{ check: async () => ({ status: 'healthy', detail: 'PONG' }) },
{ check: async () => ({ status: 'disabled', detail: 'MySQL 未启用' }) },
);
const response = await request(app.getHttpServer()).get('/api/v1/health').expect(200);
expect(response.body).toMatchObject({
status: 'healthy',
dependencies: { mysql: { status: 'disabled' } },
});
});
});
+29
View File
@@ -0,0 +1,29 @@
import { readFile } from 'node:fs/promises';
import path from 'node:path';
import { describe, expect, it } from 'vitest';
import { createOpenApiDocument } from '../../../tools/export-openapi';
describe('Server OpenAPI 单向发布', () => {
it('从真实 Controller 和 DTO 生成健康接口契约', async () => {
const document = await createOpenApiDocument();
expect(document.paths).toHaveProperty('/api/v1/health');
expect(document.components?.schemas).toHaveProperty('HealthResponseDto');
});
it('仓库内 OpenAPI 文件是可解析的同一契约', async () => {
const content = await readFile(
path.resolve(__dirname, '../../../openapi/openapi.json'),
'utf8',
);
const document = JSON.parse(content) as {
paths: Record<string, unknown>;
components?: { schemas?: Record<string, unknown> };
};
expect(document.paths).toHaveProperty('/api/v1/health');
expect(document.components?.schemas).toHaveProperty('HealthResponseDto');
});
});