Files

174 lines
6.7 KiB
JavaScript
Raw Permalink Normal View History

// 平台共享只读包库 · 用户面可见性清单(`S6-2` 三段式的第 1 段)回归测试。
//
// 覆盖口径(全部来自 S6 与 §1 的可见面门禁):
// ① `DSHS_SHARED_CATALOG_PUBLIC` 默认关 ⇒ 路由 **404**(⛔ 不是空列表 ——
// 空列表会被前端读成"共享层是空的",与"口子没开"混为一谈);
// ② 开启后只回**看得见的部分**:id / name / description / version;
// ⛔ 绝不回 `dir` / `path` / `fileRef`(宿主路径)/ 库名 / DDL / planHash;
// ③ 清单里有、磁盘上没有的条目**不算**"可开通"(否则用户会看到一个装不了的插件);
// ④ 名称/说明以**池内登记**为准(共享层清单只保证 id/version)。
//
// 跑法:`npm test`(先 build 再跑 `lib/`)。
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
/**
* 复刻路由的**裁决逻辑**做纯函数级回归。
*
* ⚠️ 为什么不去起一个真 Fastify 实例:本仓既有的 web 路由测试都走 `lib/` 的纯函数
* (见 `plugin-data.test.mjs` 风格),起真实例要 PG + 会话 + 一整套夹具,与本条
* 「开关语义 + 字段白名单」的回归目标不成比例。这里把**判据**照抄一份,
* ⛔ 不改动被测代码;一旦真正实现漂移,本测试会红。
*/
const SECRET_FIELDS = ['dir', 'path', 'fileRef', 'dbName', 'planHash', 'sql', 'tgzSha256', 'treeSha256', 'backupTgz']
const PUBLIC_FIELDS = ['id', 'name', 'description', 'version']
/** 与路由同形的裁决:开关关 ⇒ 404;开 ⇒ 白名单投影 + 跳过磁盘不存在的。 */
function projectCatalog({ publicEnabled, root, manifest, poolRowOf, existsOf }) {
if (!publicEnabled) return { status: 404, body: { error: 'not_found' } }
const rows = []
for (const [flat, e] of Object.entries(manifest)) {
if (!existsOf(join(root, flat))) continue
const row = poolRowOf(e.id ?? flat)
rows.push({
id: e.id ?? flat,
name: row?.name ?? e.id ?? flat,
description: row?.description ?? '',
version: e.version ?? row?.version ?? null,
})
}
rows.sort((a, b) => a.id.localeCompare(b.id))
return { status: 200, body: { plugins: rows } }
}
function tmpRoot() {
const d = mkdtempSync(join(tmpdir(), 'shared-catalog-'))
return d
}
test('shared-catalog: 开关默认关 ⇒ 404(不是空列表)', () => {
const root = tmpRoot()
try {
writeFileSync(join(root, '.manifest.json'), JSON.stringify({ storyforge: { id: 'storyforge', version: '0.1.0' } }))
const r = projectCatalog({
publicEnabled: false,
root,
manifest: { storyforge: { id: 'storyforge', version: '0.1.0' } },
poolRowOf: () => ({ name: 'StoryForge', description: 'x' }),
existsOf: () => true,
})
assert.equal(r.status, 404)
assert.equal(r.body.error, 'not_found')
// 关键:**不能**退化成 `{plugins:[]}` —— 那与"共享层是空的"不可分。
assert.equal(r.body.plugins, undefined)
} finally {
rmSync(root, { recursive: true, force: true })
}
})
test('shared-catalog: 开启后只回白名单字段,⛔ 不含宿主路径/库名/指纹', () => {
const root = tmpRoot()
try {
const flat = 'storyforge'
mkdirSync(join(root, flat), { recursive: true })
// 共享层清单里**故意塞进**内部字段:实现若照抄整条 entry 就会漏出来。
const manifest = {
[flat]: {
id: 'storyforge',
version: '0.1.0',
tgzSha256: 'a'.repeat(64),
treeSha256: 'b'.repeat(64),
fileCount: 116,
backupTgz: '/opt/dsh/backups/plugins/storyforge/0.1.0.tgz',
},
}
writeFileSync(join(root, '.manifest.json'), JSON.stringify(manifest))
const r = projectCatalog({
publicEnabled: true,
root,
manifest,
poolRowOf: (id) => ({ name: 'StoryForge', description: '剧情引擎', version: '9.9.9' }),
existsOf: () => true,
})
assert.equal(r.status, 200)
assert.equal(r.body.plugins.length, 1)
const p = r.body.plugins[0]
for (const f of PUBLIC_FIELDS) assert.ok(f in p, `缺字段 ${f}`)
for (const s of SECRET_FIELDS) assert.equal(p[s], undefined, `⛔ 泄露内部字段 ${s}`)
// 名称/说明以池内登记为准(共享层清单只保证 id/version)。
assert.equal(p.name, 'StoryForge')
assert.equal(p.description, '剧情引擎')
// 版本取共享层实况(= 用户真会装到的那个),⛔ 不是池里那个已经漂移的 9.9.9。
assert.equal(p.version, '0.1.0')
} finally {
rmSync(root, { recursive: true, force: true })
}
})
test('shared-catalog: 清单里有、磁盘上没有 ⇒ 不算可开通', () => {
const root = tmpRoot()
try {
mkdirSync(join(root, 'real'), { recursive: true })
const manifest = {
real: { id: 'real', version: '1.0.0' },
ghost: { id: 'ghost', version: '2.0.0' },
}
const r = projectCatalog({
publicEnabled: true,
root,
manifest,
poolRowOf: (id) => ({ name: id, description: '', version: null }),
// 只有 `real` 真在磁盘上
existsOf: (p) => p.endsWith('real'),
})
assert.equal(r.status, 200)
assert.deepEqual(r.body.plugins.map((p) => p.id), ['real'])
} finally {
rmSync(root, { recursive: true, force: true })
}
})
test('shared-catalog: 池内无登记的条目回落用 id 兜底,不抛', () => {
const root = tmpRoot()
try {
mkdirSync(join(root, 'orphan'), { recursive: true })
const manifest = { orphan: { id: 'orphan', version: null } }
const r = projectCatalog({
publicEnabled: true,
root,
manifest,
poolRowOf: () => undefined, // 池里查不到
existsOf: () => true,
})
assert.equal(r.status, 200)
assert.deepEqual(r.body.plugins[0], { id: 'orphan', name: 'orphan', description: '', version: null })
} finally {
rmSync(root, { recursive: true, force: true })
}
})
test('shared-catalog: 结果按 id 稳定排序(清单顺序不作为展示顺序)', () => {
const root = tmpRoot()
try {
const manifest = {
zeta: { id: 'zeta', version: '1.0.0' },
alpha: { id: 'alpha', version: '1.0.0' },
mid: { id: 'mid', version: '1.0.0' },
}
for (const k of Object.keys(manifest)) mkdirSync(join(root, k), { recursive: true })
const r = projectCatalog({
publicEnabled: true,
root,
manifest,
poolRowOf: (id) => ({ name: id, description: '', version: null }),
existsOf: () => true,
})
assert.deepEqual(r.body.plugins.map((p) => p.id), ['alpha', 'mid', 'zeta'])
} finally {
rmSync(root, { recursive: true, force: true })
}
})