task-213: 前端共享层导出白名单(node --test 源码扫描:页面不直连 http.ts/axios/不拼 /newApi URL,shared 内部与 bridge 放行,违规 helper 可检出)+ 8 条测试
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
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))
|
||||
})
|
||||
Reference in New Issue
Block a user