Files
mihomo-patchbay/shared/providers.test.ts
T
im0o 275f536429 追加 Hosts、路径撞名拦截、连接页表头随行滚动、GEO 借内核通路
设置页内核参数加 hosts 行,铅笔进弹窗编辑(顶层 hosts: 键,
config.go:454,值单个或数组 parseHosts config.go:1131)。弹窗里随便改、
保存才一次性提交——写配置有几秒的校验重载,逐行失焦提交会把等待塞进
每次输入。没动过的行原样写回,数组不被重整形。

订阅和规则组新增时查缓存路径撞名:路径由名字净化而来,「订阅 A」和
「订阅/A」是两个名字、同一个文件,名字查重挡不住,会互相覆盖缓存。
纯函数在 shared 带测试,页面提交前拦。

连接页表头挪进滚动容器加 sticky:.scroller 声明 overflow-y 后按规范
连带拿到横向滚动上下文,表头留在外面时窄屏横向滚动两者脱节。表头
40px 的窗口偏移由 overscan 8 行吸收,不配 scrollMargin。

GEO 单文件下载先借内核的混合端口出去再退直连——内核自己更新 GEO 走
的是它的分流,Patchbay 直连 GitHub 不一定通。ECharts 拆独立 chunk,
业务代码改动不再让这 500KB 跟着换 hash。
2026-08-12 02:47:17 +08:00

200 lines
6.6 KiB
TypeScript

import { describe, expect, it } from 'bun:test'
import {
DEFAULT_INTERVAL,
addFileSubscription,
addSubscription,
listSubscriptions,
providerNames,
removeSubscription,
subscriptionPathConflict,
subscriptionReferences,
updateSubscription,
} from './providers.ts'
import { applySetup, chainProviderName, providerCachePath } from './inbound-groups.ts'
import type { OutboundSetup } from './inbound-groups.ts'
import type { ConfigObject } from '../vendor/config-editor/index.ts'
const URL = 'http://127.0.0.1:3001/token/download/V2House'
/** 一份带一个订阅的配置,形状仿照目标机器上那份。 */
function base(): ConfigObject {
return {
'proxy-providers': {
subs: {
type: 'http',
url: URL,
path: './providers/subs.yaml',
interval: 86400,
'health-check': { enable: true, url: 'https://www.gstatic.com/generate_204', interval: 300 },
},
},
'proxy-groups': [{ name: 'PROXY', type: 'select', proxies: ['DIRECT'], use: ['subs'] }],
}
}
const chainedSetup: OutboundSetup = {
proxy: { sources: ['subs'], filter: null },
chain: { sources: ['subs'], filter: null },
}
describe('读出订阅列表', () => {
it('读出地址、间隔和健康检查', () => {
expect(listSubscriptions(base())).toEqual([
{
name: 'subs',
type: 'http',
path: './providers/subs.yaml',
url: URL,
interval: 86400,
healthCheck: { url: 'https://www.gstatic.com/generate_204', interval: 300 },
},
])
})
it('健康检查没写或没开都算未开启', () => {
const data = base()
const subs = (data['proxy-providers'] as ConfigObject).subs as ConfigObject
delete subs['health-check']
expect(listSubscriptions(data)[0]?.healthCheck).toBeNull()
subs['health-check'] = { enable: false, url: 'https://example.com', interval: 60 }
expect(listSubscriptions(data)[0]?.healthCheck).toBeNull()
})
it('不含 Patchbay 生成的订阅副本', () => {
const data = applySetup(
{ ...base(), listeners: [{ name: 'test1', type: 'mixed', port: 7778 }] },
'test1',
chainedSetup,
)
expect(listSubscriptions(data).map((s) => s.name)).toEqual(['subs'])
// 副本还在,只是不列出来
expect(providerNames(data)).toContain(chainProviderName('test1', 'subs'))
})
it('没有 proxy-providers 段时是空列表', () => {
expect(listSubscriptions({ mode: 'rule' })).toEqual([])
})
})
describe('新增订阅', () => {
it('自动生成缓存路径和默认间隔', () => {
const next = addSubscription({}, 'subs2', URL)
expect(next['proxy-providers']).toEqual({
subs2: {
type: 'http',
url: URL,
path: './providers/subs2.yaml',
interval: DEFAULT_INTERVAL,
},
})
})
it('不动已有的订阅', () => {
const data = base()
const next = addSubscription(data, 'subs2', URL)
expect((next['proxy-providers'] as ConfigObject).subs).toEqual(
(data['proxy-providers'] as ConfigObject).subs,
)
})
it('关掉自动更新时不写 interval 键', () => {
const next = addSubscription({}, 'subs2', URL, null)
const entry = (next['proxy-providers'] as ConfigObject).subs2 as ConfigObject
expect('interval' in entry).toBe(false)
})
it('从文件导入的订阅是 file 类型,没有地址和间隔', () => {
const next = addFileSubscription({}, 'subs2')
expect(next['proxy-providers']).toEqual({
subs2: { type: 'file', path: './providers/subs2.yaml' },
})
})
})
describe('编辑订阅', () => {
it('只改地址和间隔,其余字段原样保留', () => {
const next = updateSubscription(base(), 'subs', 'http://example.com/new', 3600)
const entry = (next['proxy-providers'] as ConfigObject).subs as ConfigObject
expect(entry.url).toBe('http://example.com/new')
expect(entry.interval).toBe(3600)
expect(entry.path).toBe('./providers/subs.yaml')
expect(entry['health-check']).toEqual({
enable: true,
url: 'https://www.gstatic.com/generate_204',
interval: 300,
})
})
it('订阅不存在时不做改动', () => {
const data = base()
expect(updateSubscription(data, 'ghost', URL, 1)).toBe(data)
})
it('关掉自动更新时把已有的 interval 键去掉', () => {
const next = updateSubscription(base(), 'subs', URL, null)
const entry = (next['proxy-providers'] as ConfigObject).subs as ConfigObject
expect('interval' in entry).toBe(false)
})
})
describe('删除订阅', () => {
it('从映射里去掉', () => {
const data = addSubscription(base(), 'subs2', URL)
const next = removeSubscription(data, 'subs2')
expect(providerNames(next)).toEqual(['subs'])
})
it('删到最后一个时整个键去掉,不留空映射', () => {
const next = removeSubscription(base(), 'subs')
expect('proxy-providers' in next).toBe(false)
})
it('订阅不存在时不做改动', () => {
const data = base()
expect(removeSubscription(data, 'ghost')).toBe(data)
})
})
describe('删除前的引用检查', () => {
it('找出 use 它的策略组', () => {
expect(subscriptionReferences(base(), 'subs')).toEqual(['PROXY'])
})
it('没人在用时是空列表', () => {
const data = base()
;(data['proxy-groups'] as ConfigObject[])[0] = { name: 'PROXY', type: 'select', proxies: ['DIRECT'] }
expect(subscriptionReferences(data, 'subs')).toEqual([])
})
it('链式代理的专属组 use 的是副本,也算引用原订阅', () => {
const data = applySetup(
{ ...base(), listeners: [{ name: 'test1', type: 'mixed', port: 7778 }] },
'test1',
chainedSetup,
)
const refs = subscriptionReferences(data, 'subs')
// 入口组直接 use 原订阅,三个专属组 use 副本,两边都要认出来
expect(refs).toContain('patchbay:test1 链式')
expect(refs).toContain('patchbay:test1')
})
it('订阅不存在时是空列表', () => {
expect(subscriptionReferences(base(), 'ghost')).toEqual([])
})
})
describe('缓存路径撞名', () => {
it('名字只差符号时净化成同一个路径,能认出来', () => {
const data = addSubscription({}, '订阅 A', 'https://a.example/sub')
const path = providerCachePath('订阅/A')
expect(path).toBe(providerCachePath('订阅 A'))
expect(subscriptionPathConflict(data, '订阅/A', path)).toBe('订阅 A')
})
it('不同路径、或者就是自己,都不算撞', () => {
const data = addSubscription({}, '订阅 A', 'https://a.example/sub')
expect(subscriptionPathConflict(data, 'B', providerCachePath('B'))).toBeNull()
expect(subscriptionPathConflict(data, '订阅 A', providerCachePath('订阅 A'))).toBeNull()
})
})