前端(Vite + React)、API/worker(Fastify + BullMQ)、api-contracts 契约包、 迁移脚本、测试与自托管 docker-compose 示例。 开源准备:运维脚本与机器信息已拆到私有仓库;公司名、备案号、隐私邮箱改读 publicCompliance.local.json(仓库只保留占位模板);站点地址与客服二维码改为 VITE_ 环境变量;fixtures 中的真实用户标识已匿名化;三个 package.json 标注 Apache-2.0。 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
93 lines
3.0 KiB
TypeScript
93 lines
3.0 KiB
TypeScript
import fastify from 'fastify';
|
|
import { serializerCompiler, validatorCompiler } from 'fastify-type-provider-zod';
|
|
import { describe, expect, it } from 'vitest';
|
|
import {
|
|
nonJsonApiRouteExemptions,
|
|
registerApiContractRouteHook,
|
|
} from '../src/apiContractRoutes.js';
|
|
import { setApiErrorHandler } from '../src/app.js';
|
|
|
|
describe('API 路由契约覆盖', () => {
|
|
it('第一方 JSON 路由缺失共享契约时在注册阶段失败', async () => {
|
|
const app = fastify({ logger: false });
|
|
registerApiContractRouteHook(app);
|
|
try {
|
|
expect(() => {
|
|
app.get('/api/contract-was-forgotten', async () => ({ ok: true }));
|
|
}).toThrow(/\u7f3a\u5c11\u5171\u4eab API \u5951\u7ea6/);
|
|
} finally {
|
|
await app.close();
|
|
}
|
|
});
|
|
|
|
it('只豁免明确的 Provider 回调、原始对象流和 302 下载', () => {
|
|
expect([...nonJsonApiRouteExemptions.keys()]).toEqual([
|
|
'POST /api/payments/notify',
|
|
'POST /api/payments/refunds/notify',
|
|
'POST /api/providers/ark/callback/:token',
|
|
'GET /api/providers/volc/real-portrait-validation/:opaqueNonce',
|
|
'GET /api/assets/:id/download',
|
|
'PUT /api/storage/local/*',
|
|
'GET /api/storage/local/*',
|
|
]);
|
|
});
|
|
|
|
it('自动注入的成功响应 Schema 会过滤未声明字段', async () => {
|
|
const app = contractApp();
|
|
app.get('/api/health', async () => ({ ok: true, internalState: 'hidden' }));
|
|
try {
|
|
const response = await app.inject({ method: 'GET', url: '/api/health' });
|
|
expect(response.statusCode).toBe(200);
|
|
expect(response.json()).toEqual({ ok: true });
|
|
} finally {
|
|
await app.close();
|
|
}
|
|
});
|
|
|
|
it('自动注入的成功响应 Schema 不匹配时返回统一 500', async () => {
|
|
const app = contractApp();
|
|
app.get('/api/health', async () => ({ ok: false }));
|
|
try {
|
|
const response = await app.inject({ method: 'GET', url: '/api/health' });
|
|
expect(response.statusCode).toBe(500);
|
|
expect(response.json()).toEqual({
|
|
message: '服务器响应格式错误',
|
|
code: 'API_RESPONSE_SCHEMA_MISMATCH',
|
|
requestId: expect.any(String),
|
|
});
|
|
} finally {
|
|
await app.close();
|
|
}
|
|
});
|
|
|
|
it('自动注入的公共错误 Schema 会过滤未声明的服务端字段', async () => {
|
|
const app = contractApp();
|
|
app.get('/api/health', async (request, reply) => reply.code(418).send({
|
|
message: '测试错误',
|
|
code: 'TEST_ERROR',
|
|
requestId: request.id,
|
|
databaseDebug: '不得发送',
|
|
}));
|
|
try {
|
|
const response = await app.inject({ method: 'GET', url: '/api/health' });
|
|
expect(response.statusCode).toBe(418);
|
|
expect(response.json()).toEqual({
|
|
message: '测试错误',
|
|
code: 'TEST_ERROR',
|
|
requestId: expect.any(String),
|
|
});
|
|
} finally {
|
|
await app.close();
|
|
}
|
|
});
|
|
});
|
|
|
|
function contractApp() {
|
|
const app = fastify({ logger: false });
|
|
app.setValidatorCompiler(validatorCompiler);
|
|
app.setSerializerCompiler(serializerCompiler);
|
|
registerApiContractRouteHook(app);
|
|
setApiErrorHandler(app);
|
|
return app;
|
|
}
|