feat(GOV-009): 建立前端 OpenAPI 客户端边界
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { execFile } from 'node:child_process';
|
||||
import { mkdtemp, mkdir, writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
import { promisify } from 'node:util';
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const checkerPath = path.resolve('tools/boundaries/verify-project-boundaries.mjs');
|
||||
|
||||
async function createFixture() {
|
||||
const repositoryRoot = await mkdtemp(path.join(tmpdir(), 'a-share-boundary-'));
|
||||
await mkdir(path.join(repositoryRoot, 'web/src/api/generated'), { recursive: true });
|
||||
await mkdir(path.join(repositoryRoot, 'server/libs'), { recursive: true });
|
||||
await writeFile(
|
||||
path.join(repositoryRoot, 'web/src/api/generated/schema.ts'),
|
||||
'export interface paths {}\n',
|
||||
);
|
||||
return repositoryRoot;
|
||||
}
|
||||
|
||||
async function runChecker(repositoryRoot) {
|
||||
try {
|
||||
await execFileAsync(process.execPath, [checkerPath, '--root', repositoryRoot]);
|
||||
return { exitCode: 0, stderr: '' };
|
||||
} catch (error) {
|
||||
return {
|
||||
exitCode: error.code,
|
||||
stderr: String(error.stderr),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
test('拒绝 Web 通过静态、动态或导出语句越界依赖 Server', async () => {
|
||||
const repositoryRoot = await createFixture();
|
||||
await writeFile(
|
||||
path.join(repositoryRoot, 'web/src/illegal.ts'),
|
||||
[
|
||||
"import '../../server/libs/static';",
|
||||
"export { value } from '../../server/libs/exported';",
|
||||
"void import('../../server/libs/dynamic');",
|
||||
].join('\n'),
|
||||
);
|
||||
|
||||
const result = await runChecker(repositoryRoot);
|
||||
|
||||
assert.equal(result.exitCode, 1);
|
||||
assert.match(result.stderr, /Web 禁止依赖 Server/);
|
||||
assert.match(result.stderr, /illegal\.ts/);
|
||||
});
|
||||
|
||||
test('拒绝生成契约泄漏 RocketMQ 内部消息结构', async () => {
|
||||
const repositoryRoot = await createFixture();
|
||||
await writeFile(
|
||||
path.join(repositoryRoot, 'web/src/api/generated/schema.ts'),
|
||||
'export interface MessageEnvelope { messageId: string }\n',
|
||||
);
|
||||
|
||||
const result = await runChecker(repositoryRoot);
|
||||
|
||||
assert.equal(result.exitCode, 1);
|
||||
assert.match(result.stderr, /生成契约包含 Server 内部消息/);
|
||||
});
|
||||
|
||||
test('拒绝通过 tsconfig 路径别名依赖 Server', async () => {
|
||||
const repositoryRoot = await createFixture();
|
||||
await writeFile(
|
||||
path.join(repositoryRoot, 'web/tsconfig.json'),
|
||||
JSON.stringify({
|
||||
compilerOptions: {
|
||||
baseUrl: '.',
|
||||
paths: { '@server/*': ['../server/*'] },
|
||||
},
|
||||
}),
|
||||
);
|
||||
await writeFile(
|
||||
path.join(repositoryRoot, 'web/src/aliased.ts'),
|
||||
"import type { Internal } from '@server/libs/internal';\nexport type Leaked = Internal;\n",
|
||||
);
|
||||
await writeFile(
|
||||
path.join(repositoryRoot, 'server/libs/internal.ts'),
|
||||
'export interface Internal { secret: string }\n',
|
||||
);
|
||||
|
||||
const result = await runChecker(repositoryRoot);
|
||||
|
||||
assert.equal(result.exitCode, 1);
|
||||
assert.match(result.stderr, /Web 禁止依赖 Server/);
|
||||
});
|
||||
|
||||
test('接受只消费公开 OpenAPI 类型的 Web 工程', async () => {
|
||||
const repositoryRoot = await createFixture();
|
||||
await writeFile(
|
||||
path.join(repositoryRoot, 'web/src/legal.ts'),
|
||||
"import type { paths } from './api/generated/schema';\nexport type ApiPaths = paths;\n",
|
||||
);
|
||||
|
||||
const result = await runChecker(repositoryRoot);
|
||||
|
||||
assert.equal(result.exitCode, 0);
|
||||
assert.equal(result.stderr, '');
|
||||
});
|
||||
Reference in New Issue
Block a user