配置每次落盘都打顶层 patchbay: 标记(盐加版本的哈希)。内核忽略未知 顶层键(yaml.Unmarshal 没开严格模式,config/config.go:612)。刻意不对 内容做哈希:本项目只接管部分段落,手改其余段落是正常用法,内容哈希 会把合法手改判成无效。上传配置时靠标记分流:有标记的确认后替换, 没有的用更重的措辞确认导入;两者都走 config-store 完整保护链路 (新端点 POST /api/config/import)。自动备份列表从界面撤下,改成 下载当前配置 + 拖放上传。 内核参数一组编辑五个老式顶层端口和 keep-alive-interval(config.go:444, 整数秒),走带基准指纹的保存链路。资源一组接 POST /configs/geo (同步接口,透传给了 300 秒超时)。测速地址成为本地偏好,延迟测试 读它;prefs 模块顶层的浏览器全局加了存在性守卫——api.ts 开始引用 prefs,而 bun test 环境没有 localStorage。 退出登录挪到侧栏底部;密度设置从设置页撤下(侧栏按钮还在); 免责声明和关于做成子页(左上角返回),登录页输入框下加同一份 免责声明的弹层入口。
178 lines
6.4 KiB
TypeScript
178 lines
6.4 KiB
TypeScript
import { afterAll, describe, expect, it } from 'bun:test'
|
|
import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { applyConfig, listBackups, readConfig, restoreBackup } from './config-store.ts'
|
|
import type { Env } from './env.ts'
|
|
|
|
/**
|
|
* 落盘保护链路的测试,跑在一个假内核上:/version 报在线,PUT /configs 默认放行。
|
|
* mihomo -t 那条离线路径这里测不了(需要真的内核二进制),离线行为只能靠人工验证。
|
|
*/
|
|
let rejectNextReload = false
|
|
let reloadCount = 0
|
|
const kernel = Bun.serve({
|
|
port: 0,
|
|
fetch(req) {
|
|
const url = new URL(req.url)
|
|
if (url.pathname === '/version') return Response.json({ version: 'test' })
|
|
if (url.pathname === '/configs' && req.method === 'PUT') {
|
|
reloadCount += 1
|
|
if (rejectNextReload) {
|
|
rejectNextReload = false
|
|
return new Response('parse error: something wrong', { status: 400 })
|
|
}
|
|
return new Response(null, { status: 204 })
|
|
}
|
|
return new Response(null, { status: 404 })
|
|
},
|
|
})
|
|
afterAll(() => kernel.stop())
|
|
|
|
const ORIGINAL = 'mixed-port: 7890\n'
|
|
|
|
function makeEnv(): Env {
|
|
const dir = mkdtempSync(join(tmpdir(), 'patchbay-store-'))
|
|
const configPath = join(dir, 'config.yaml')
|
|
writeFileSync(configPath, ORIGINAL, 'utf8')
|
|
return {
|
|
mihomoApi: `http://127.0.0.1:${String(kernel.port)}`,
|
|
mihomoSecret: 'x',
|
|
configPath,
|
|
mihomoBin: join(dir, '不存在的内核'),
|
|
mihomoHome: dir,
|
|
host: '127.0.0.1',
|
|
port: 0,
|
|
}
|
|
}
|
|
|
|
describe('落盘保护链路(内核在线)', () => {
|
|
it('正常保存:写盘、留一份改动前的备份、通知重载', async () => {
|
|
const env = makeEnv()
|
|
const { data, revision } = await readConfig(env)
|
|
const before = reloadCount
|
|
|
|
const result = await applyConfig(env, { ...data, 'mixed-port': 7891 }, revision)
|
|
|
|
expect(result.ok).toBe(true)
|
|
if (result.ok) expect(result.reloaded).toBe(true)
|
|
expect(readFileSync(env.configPath, 'utf8')).toContain('mixed-port: 7891')
|
|
expect(reloadCount).toBe(before + 1)
|
|
|
|
const backups = await listBackups(env)
|
|
expect(backups).toHaveLength(1)
|
|
const backupBody = readFileSync(
|
|
join(env.configPath, '..', '.patchbay-backups', backups[0]!),
|
|
'utf8',
|
|
)
|
|
expect(backupBody).toBe(ORIGINAL)
|
|
})
|
|
|
|
it('基准过时被拦住,文件一个字节不动', async () => {
|
|
const env = makeEnv()
|
|
const { data } = await readConfig(env)
|
|
|
|
const result = await applyConfig(env, { ...data, 'mixed-port': 7891 }, '基准已过时')
|
|
|
|
expect(result.ok).toBe(false)
|
|
if (!result.ok) expect(result.stage).toBe('stale')
|
|
expect(readFileSync(env.configPath, 'utf8')).toBe(ORIGINAL)
|
|
expect(await listBackups(env)).toHaveLength(0)
|
|
})
|
|
|
|
it('内容没变直接返回,不备份也不重载', async () => {
|
|
const env = makeEnv()
|
|
// 先落一次盘让文件带上 Patchbay 标记——没有标记的文件第一次保存必有
|
|
// 「补标记」这一处内容变化,短路只发生在稳态
|
|
const first = await readConfig(env)
|
|
await applyConfig(env, first.data, first.revision)
|
|
const { data, revision } = await readConfig(env)
|
|
const before = reloadCount
|
|
const backupsBefore = (await listBackups(env)).length
|
|
|
|
const result = await applyConfig(env, data, revision)
|
|
|
|
expect(result.ok).toBe(true)
|
|
if (result.ok) {
|
|
expect(result.reloaded).toBe(false)
|
|
expect(result.reloadMessage).toBe('内容没有变化')
|
|
}
|
|
expect(reloadCount).toBe(before)
|
|
expect((await listBackups(env)).length).toBe(backupsBefore)
|
|
})
|
|
|
|
it('自检拦下断引用:入站的 proxy 指向不存在的出站', async () => {
|
|
// mihomo -t 和重载都不检查这种引用(实测 v1.19.29),只有这层自检拦得住
|
|
const env = makeEnv()
|
|
const { data, revision } = await readConfig(env)
|
|
const next = {
|
|
...data,
|
|
listeners: [{ name: 'x', type: 'mixed', port: 1080, proxy: '不存在的组' }],
|
|
}
|
|
|
|
const result = await applyConfig(env, next, revision)
|
|
|
|
expect(result.ok).toBe(false)
|
|
if (!result.ok) expect(result.stage).toBe('self-check')
|
|
expect(readFileSync(env.configPath, 'utf8')).toBe(ORIGINAL)
|
|
})
|
|
|
|
it('内核拒绝重载时把文件还原,并让内核载回原样', async () => {
|
|
const env = makeEnv()
|
|
const { data, revision } = await readConfig(env)
|
|
|
|
rejectNextReload = true
|
|
const result = await applyConfig(env, { ...data, 'mixed-port': 7891 }, revision)
|
|
|
|
expect(result.ok).toBe(false)
|
|
if (!result.ok) {
|
|
expect(result.stage).toBe('reload')
|
|
expect(result.restored).toBe(true)
|
|
expect(result.message).toContain('parse error')
|
|
}
|
|
expect(readFileSync(env.configPath, 'utf8')).toBe(ORIGINAL)
|
|
})
|
|
|
|
it('恢复备份走同一条保护链路', async () => {
|
|
const env = makeEnv()
|
|
const first = await readConfig(env)
|
|
await applyConfig(env, { ...first.data, 'mixed-port': 7891 }, first.revision)
|
|
const [backup] = await listBackups(env)
|
|
|
|
const result = await restoreBackup(env, backup!)
|
|
|
|
expect(result.ok).toBe(true)
|
|
// 还原也走落盘链路,所以会重新打上 Patchbay 标记——内容回去了,但不逐字节等于旧文件
|
|
const body = readFileSync(env.configPath, 'utf8')
|
|
expect(body).toContain('mixed-port: 7890')
|
|
expect(body).not.toContain('7891')
|
|
expect(body).toContain('patchbay:')
|
|
})
|
|
|
|
it('恢复只认列表里的名字,路径穿越进不来', async () => {
|
|
const env = makeEnv()
|
|
const result = await restoreBackup(env, '../config.yaml')
|
|
|
|
expect(result.ok).toBe(false)
|
|
if (!result.ok) expect(result.message).toContain('没有这个备份')
|
|
})
|
|
|
|
it('备份最多留 10 份,旧的被清掉', async () => {
|
|
const env = makeEnv()
|
|
const dir = join(env.configPath, '..', '.patchbay-backups')
|
|
mkdirSync(dir, { recursive: true })
|
|
for (let i = 10; i < 22; i += 1) {
|
|
writeFileSync(join(dir, `config.yaml.bak-20200101-0000${String(i)}`), 'old', 'utf8')
|
|
}
|
|
|
|
const { data, revision } = await readConfig(env)
|
|
await applyConfig(env, { ...data, 'mixed-port': 7891 }, revision)
|
|
|
|
const backups = await listBackups(env)
|
|
expect(backups).toHaveLength(10)
|
|
// 名字按时间戳排序,留下的是最新的:刚生成的那份在最前,2020 年的只剩最近的九份
|
|
expect(backups[0]!.startsWith('config.yaml.bak-2020')).toBe(false)
|
|
expect(backups).not.toContain('config.yaml.bak-20200101-000010')
|
|
})
|
|
})
|