Files
crawler-plugin/frontend-vue/tests/shared-api-allowlist.test.ts

107 lines
3.9 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test } from 'node:test'
import assert from 'node:assert/strict'
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
/** task-213:前端共享层导出白名单。页面不得直连 shared/api/http 或拼 URL;只经 API 层/桥。 */
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'src')
function walk(dir: string): string[] {
const out: string[] = []
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name)
if (entry.isDirectory()) out.push(...walk(full))
else if (entry.name.endsWith('.ts') || entry.name.endsWith('.vue')) out.push(full)
}
return out
}
const relative = (f: string) => path.relative(ROOT, f).replaceAll('\\', '/')
/** 命中直连 http 模块 import 的文件(http.ts 内部及 shared 内部允许)。 */
function httpDirectImports(files: string[]): string[] {
const hits: string[] = []
for (const f of files) {
const text = fs.readFileSync(f, 'utf8')
const imp = /from\s+['"][^'"]*(?:shared\/)?api\/http(?:\.ts)?['"]/.test(text)
if (imp && !relative(f).startsWith('shared/')) hits.push(relative(f))
}
return hits
}
/** 直接 import axios(应只在 shared/api/http.ts)。 */
function axiosDirectImports(files: string[]): string[] {
const hits: string[] = []
for (const f of files) {
const text = fs.readFileSync(f, 'utf8')
if (/from\s+['"]axios['"]/.test(text) && relative(f) !== 'shared/api/http.ts') {
hits.push(relative(f))
}
}
return hits
}
const allFiles = walk(ROOT)
test('页面不直连 shared/api/http', () => {
assert.deepEqual(httpDirectImports(allFiles), [])
})
test('页面不直接 import axios(收敛到 http.ts', () => {
assert.deepEqual(axiosDirectImports(allFiles), [])
})
test('违规 import 可被检出(helper 判真)', () => {
const tmp = path.join(ROOT, 'pages/__tmp_violation__.ts')
fs.writeFileSync(tmp, "import { http } from '../../shared/api/http.ts'\n")
try {
const hits = httpDirectImports(walk(ROOT))
assert.ok(hits.some((h) => h.includes('pages/__tmp_violation__')), '应检出 shared 外违规 import')
} finally {
fs.rmSync(tmp)
}
})
test('shared 内部 import http 被放行(允许项)', () => {
const tmp = path.join(ROOT, 'shared/api/__tmp_internal__.ts')
fs.writeFileSync(tmp, "import { http } from './http.ts'\n")
try {
const hits = httpDirectImports([tmp])
assert.deepEqual(hits, [], 'shared 内部允许直连 http')
} finally {
fs.rmSync(tmp)
}
})
test('页面经 java-modules/shared api 层调用(存在且被引用)', () => {
const texts = allFiles.filter((f) => relative(f).startsWith('pages/') || /-main\.ts$/.test(relative(f)))
.map((f) => fs.readFileSync(f, 'utf8'))
const usingApi = texts.filter((t) => /shared\/api\/(java-modules|types\/modules|progress-light|permission|upload|user)/.test(t))
assert.ok(usingApi.length >= 1, '应至少一个页面入口使用共享 API 层')
})
test('bridge 适配层 import http 被放行', () => {
const bridges = allFiles.filter((f) => relative(f).startsWith('shared/bridges/'))
for (const f of bridges) {
const text = fs.readFileSync(f, 'utf8')
if (/api\/http/.test(text)) return // 存在 bridge 使用 http,属允许
}
// 无 bridge 使用 http 也接受(不强求)
assert.ok(true)
})
test('页面文件不得拼接 /newApi URL 字面量', () => {
const pages = allFiles.filter((f) => relative(f).startsWith('pages/') || /-main\.ts$/.test(relative(f)))
for (const f of pages) {
const text = fs.readFileSync(f, 'utf8')
assert.ok(!text.includes("'/newApi") && !text.includes('"/newApi'), `页面不应拼 URL: ${relative(f)}`)
}
})
test('白名单扫描可重复、结果稳定', () => {
assert.deepEqual(httpDirectImports(allFiles), httpDirectImports(allFiles))
assert.deepEqual(axiosDirectImports(allFiles), axiosDirectImports(allFiles))
})