纯库、零接线:不碰 gateway/frontend/Docs/.env*(本片刻意不接线,避免与在途切片争用文件面)。 - pkg/plugins/sdk:面板声明类型(JSON tag 与 design/09 §5.2 示例逐字同形); 字段类型与 section 类型都是**闭集**;When 三态 + **严格 UnmarshalJSON**(未知条件键报错, 不静默丢) - internal/capability/paneldecl:闭集校验(47 条拒绝用例,断言的是**分类哨兵 + JSON 路径**, 不是错误字符串)· catalog(并发安全、返回拷贝、按 (group, order, 登记序号) 的**全序**稳定排序)· when 求值(上下文由 Resolver 注入,本包不读环境变量/文件)· 角色过滤(viewer 只留 status/log/table 且摘掉 rowActions,纯写面板整体隐藏;未知角色 fail-closed) - 拒绝粒度:插件 id 非法 ⇒ 整插件拒绝;单个面板非法 ⇒ 只拒该面板 + 聚合显式报错(理由见报告) 【我的验收】隔离 worktree(19 文件)build/vet/linux/darwin 全过、69 包全绿; 我亲手打红变异:把同组同序的回退比较 反成 ⇒ TestCatalog_StableOrder FAIL。 (记录一条观察:比较器已靠 seq 构成全序,故 sort.SliceStable 的稳定性是冗余保护 —— 我第一次用「SliceStable→Slice」打不红,正是因为全序与算法无关;这不是测试虚护。)
187 lines
6.2 KiB
Go
187 lines
6.2 KiB
Go
package paneldecl
|
|
|
|
import (
|
|
"git.yeij.top/AskaEth/Cyrene_v2/pkg/plugins/sdk"
|
|
)
|
|
|
|
// 本文件是字段层与表格元素层(列 / 行内动作)的拒绝用例。
|
|
|
|
// rejectCasesFieldCore 是字段的必填、形状与选项来源。
|
|
func rejectCasesFieldCore() []rejectCase {
|
|
return []rejectCase{
|
|
{
|
|
name: "字段 key 缺失",
|
|
panels: func() []sdk.Panel {
|
|
return panelsOf(samplePanel(), func(p *sdk.Panel) { p.Sections[0].Fields[0].Key = "" })
|
|
},
|
|
want: ErrMissingField, wantPath: "adminPanels[0].sections[0].fields[0].key",
|
|
},
|
|
{
|
|
name: "字段 key 形状非法(中划线)",
|
|
panels: func() []sdk.Panel {
|
|
return panelsOf(samplePanel(), func(p *sdk.Panel) { p.Sections[0].Fields[0].Key = "api-key" })
|
|
},
|
|
want: ErrInvalidFieldKey, wantPath: "adminPanels[0].sections[0].fields[0].key",
|
|
},
|
|
{
|
|
name: "字段类型不在闭集",
|
|
panels: func() []sdk.Panel {
|
|
return panelsOf(samplePanel(), func(p *sdk.Panel) { p.Sections[0].Fields[0].Type = "chart" })
|
|
},
|
|
want: ErrUnknownFieldType, wantPath: "adminPanels[0].sections[0].fields[0].type",
|
|
},
|
|
{
|
|
name: "字段 key 在区块内重复",
|
|
panels: func() []sdk.Panel {
|
|
return panelsOf(samplePanel(), func(p *sdk.Panel) { p.Sections[0].Fields[1].Key = "provider" })
|
|
},
|
|
want: ErrDuplicateID, wantPath: "adminPanels[0].sections[0].fields[1].key",
|
|
},
|
|
{
|
|
name: "optionsFrom 塞了任意 URL",
|
|
panels: func() []sdk.Panel {
|
|
return panelsOf(samplePanel(), func(p *sdk.Panel) {
|
|
p.Sections[0].Fields[0].OptionsFrom = "https://api.example.com/list"
|
|
})
|
|
},
|
|
want: ErrInvalidOptionsFrom, wantPath: "adminPanels[0].sections[0].fields[0].optionsFrom",
|
|
},
|
|
{
|
|
name: "optionsFrom 配在 text 字段上",
|
|
panels: func() []sdk.Panel {
|
|
return panelsOf(samplePanel(), func(p *sdk.Panel) {
|
|
p.Sections[0].Fields[2].OptionsFrom = "plugin:providers"
|
|
})
|
|
},
|
|
want: ErrInvalidOptionsFrom, wantPath: "adminPanels[0].sections[0].fields[2].optionsFrom",
|
|
},
|
|
{
|
|
name: "字段文案含控制字符",
|
|
panels: func() []sdk.Panel {
|
|
return panelsOf(samplePanel(), func(p *sdk.Panel) {
|
|
p.Sections[0].Fields[2].Label = "接口\n地址"
|
|
})
|
|
},
|
|
want: ErrInvalidText, wantPath: "adminPanels[0].sections[0].fields[2].label",
|
|
},
|
|
}
|
|
}
|
|
|
|
// rejectCasesFieldBoundsAndDefaults 是 secret 默认值、数值边界与默认值类型。
|
|
func rejectCasesFieldBoundsAndDefaults() []rejectCase {
|
|
return []rejectCase{
|
|
{
|
|
name: "secret 字段声明默认值",
|
|
panels: func() []sdk.Panel {
|
|
return panelsOf(samplePanel(), func(p *sdk.Panel) {
|
|
p.Sections[0].Fields[1].Default = "sk-leaked"
|
|
})
|
|
},
|
|
want: ErrSecretDefault, wantPath: "adminPanels[0].sections[0].fields[1].default",
|
|
},
|
|
{
|
|
name: "min/max 配在 text 字段上",
|
|
panels: func() []sdk.Panel {
|
|
return panelsOf(samplePanel(), func(p *sdk.Panel) {
|
|
p.Sections[0].Fields[2].Min = floatPtr(1)
|
|
})
|
|
},
|
|
want: ErrInvalidBounds, wantPath: "adminPanels[0].sections[0].fields[2].min",
|
|
},
|
|
{
|
|
name: "min 大于 max",
|
|
panels: func() []sdk.Panel {
|
|
return panelsOf(samplePanel(), func(p *sdk.Panel) {
|
|
f := &p.Sections[0].Fields[2]
|
|
f.Type = sdk.FieldNumber
|
|
f.Min = floatPtr(10)
|
|
f.Max = floatPtr(1)
|
|
})
|
|
},
|
|
want: ErrInvalidBounds, wantPath: "adminPanels[0].sections[0].fields[2].min",
|
|
},
|
|
{
|
|
name: "默认值与字段类型不匹配(下拉字段给了数字)",
|
|
panels: func() []sdk.Panel {
|
|
return panelsOf(samplePanel(), func(p *sdk.Panel) {
|
|
p.Sections[0].Fields[0].Default = 42
|
|
})
|
|
},
|
|
want: ErrInvalidDefault, wantPath: "adminPanels[0].sections[0].fields[0].default",
|
|
},
|
|
}
|
|
}
|
|
|
|
// rejectCasesTableItems 是列与行内动作的拒绝用例。
|
|
func rejectCasesTableItems() []rejectCase {
|
|
return []rejectCase{
|
|
{
|
|
name: "列 key 缺失",
|
|
panels: func() []sdk.Panel {
|
|
return panelsOf(samplePanel(), func(p *sdk.Panel) { p.Sections[1].Columns[0].Key = "" })
|
|
},
|
|
want: ErrMissingField, wantPath: "adminPanels[0].sections[1].columns[0].key",
|
|
},
|
|
{
|
|
name: "列 key 重复",
|
|
panels: func() []sdk.Panel {
|
|
return panelsOf(samplePanel(), func(p *sdk.Panel) { p.Sections[1].Columns[1].Key = "provider" })
|
|
},
|
|
want: ErrDuplicateID, wantPath: "adminPanels[0].sections[1].columns[1].key",
|
|
},
|
|
{
|
|
name: "列 label 含控制字符",
|
|
panels: func() []sdk.Panel {
|
|
return panelsOf(samplePanel(), func(p *sdk.Panel) { p.Sections[1].Columns[0].Label = "状\t态" })
|
|
},
|
|
want: ErrInvalidText, wantPath: "adminPanels[0].sections[1].columns[0].label",
|
|
},
|
|
{
|
|
name: "行内动作 id 含大写",
|
|
panels: func() []sdk.Panel {
|
|
return panelsOf(samplePanel(), func(p *sdk.Panel) { p.Sections[1].RowActions[0].ID = "Test" })
|
|
},
|
|
want: ErrInvalidRowActionID, wantPath: "adminPanels[0].sections[1].rowActions[0].id",
|
|
},
|
|
{
|
|
name: "行内动作缺 button 文案",
|
|
panels: func() []sdk.Panel {
|
|
return panelsOf(samplePanel(), func(p *sdk.Panel) { p.Sections[1].RowActions[0].Label = "" })
|
|
},
|
|
want: ErrMissingField, wantPath: "adminPanels[0].sections[1].rowActions[0].label",
|
|
},
|
|
{
|
|
name: "行内动作塞了任意 URL",
|
|
panels: func() []sdk.Panel {
|
|
return panelsOf(samplePanel(), func(p *sdk.Panel) {
|
|
p.Sections[1].RowActions[0].Action = "https://evil.example.com/x"
|
|
})
|
|
},
|
|
want: ErrInvalidAction, wantPath: "adminPanels[0].sections[1].rowActions[0].action",
|
|
},
|
|
{
|
|
name: "行内动作 id 重复",
|
|
panels: func() []sdk.Panel {
|
|
return panelsOf(samplePanel(), func(p *sdk.Panel) { p.Sections[1].RowActions[1].ID = "test" })
|
|
},
|
|
want: ErrDuplicateID, wantPath: "adminPanels[0].sections[1].rowActions[1].id",
|
|
},
|
|
{
|
|
name: "submit 有动作键但没有动作名",
|
|
panels: func() []sdk.Panel {
|
|
return panelsOf(samplePanel(), func(p *sdk.Panel) { p.Sections[0].Submit = &sdk.Submit{} })
|
|
},
|
|
want: ErrMissingField, wantPath: "adminPanels[0].sections[0].submit.action",
|
|
},
|
|
{
|
|
name: "source 动作名含冒号(URL 形态)",
|
|
panels: func() []sdk.Panel {
|
|
return panelsOf(samplePanel(), func(p *sdk.Panel) {
|
|
p.Sections[1].Source = &sdk.Source{Action: "http://api.example.com/list"}
|
|
})
|
|
},
|
|
want: ErrInvalidAction, wantPath: "adminPanels[0].sections[1].source.action",
|
|
},
|
|
}
|
|
}
|