Files
HomeAgent/internal/sdk/proxy_test.go
JianFeeeee 2c810bbbce feat(webui): 路径挂载的 strip_path 两态 + 尾斜杠重定向(修 /p/huawei 打不开数据)
用户要求用方案 A(路径挂载)让 huawei 插件 UI 在外部可用,
并把「通过反代的插件必须使用单一入口」写入 SDK 声明。

## 实测暴露的两个真问题

1. **Path 的语义不能一刀切**。原设计「原样保留」只对**机器接口**成立
   (设备客户端硬编码 /api/v1/device/ws,不可能知道反代的存在);
   而自带 UI 的服务需要**剥掉前缀**(/p/huawei/api/status → 上游 /api/status)。
   猜错的结果是全部请求 404,且看起来像上游故障 —— 所以由声明者选:
   strip_path=false 别名模式 / true 前缀模式。非法组合被 validate 挡住。

2. **前缀模式的尾斜杠是必需的**(自测发现的 bug)。
   访问 /p/huawei(无尾斜杠)时页面能开,但页面里所有 fetch 都 404 ——
   相对路径以「当前文档目录」为基准,没尾斜杠时浏览器把最后一段当文件名,
   目录退回上一级,fetch('api/status') 打到 /p/api/status。
   修:前缀模式且路径恰等于前缀时 301 到 /p/huawei/(保留查询串)。
   **别名模式不做此事** —— 那类路径是上游真实语义,加斜杠会改坏它。

## 插件侧(huawei_smarthome)

- 前端 4 处根绝对路径(fetch('/api/status') 等)改为相对路径,
  基准由 location.pathname 推导(BASE)。这是 Path 形态能成立的**前提** ——
  否则请求会打到门户自己身上。
- plg.json 声明:host + path=/p/huawei + strip_path=true + auth=homeagent。
- SDK 升到 1.4.0,并用 hmapdev 1.4.0 重新打包(1.3.0 的 hmapdev 无
  proxies 支持,会把声明**静默丢弃** —— 实测确认过,这是打包链路上
  一个不报警的坑,值得记住)。

## 判据

+6 条:TestProxyPathAliasVsStrip(两态各自正确)、
TestProxyPathLongestPrefixWins(/p/app 不得劫持 /p/apple,
且长前缀胜出)、TestProxyStripPathRedirectsToTrailingSlash(尾斜杠,
含查询串保留 + 别名模式不得重定向)。

变异验证(4 条,均按预期打红后还原回绿):
- 删尾斜杠重定向 → 判红(还原了真实 bug 形态)
- 让别名模式也重定向 → 判红(设备网关语义被毁)
- 从 hmapdev schema 探测体删 StripPath → 判红(漂移检测有效)
- 删 SDK 里的「单一入口原则」字样 → 判红(契约不能只剩口头约定)

全量:35 包全绿。
2026-09-26 13:49:59 +08:00

64 lines
2.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package sdk
import "testing"
// 内置插件的运行期声明必须真的被登记、可枚举、可清除——
// remotedevice(内置、无 plugin.json)就靠这条通道。
func TestBuiltinProxyDefRegistry(t *testing.T) {
const p = "test_builtin_proxy"
ClearBuiltinProxyDefs(p)
defer ClearBuiltinProxyDefs(p)
before := BuiltinProxyVersion()
RegisterBuiltinProxy(p, "gw", ProxyDef{Name: "gw", Host: "devices", Target: "127.0.0.1:9890", WebSocket: true, Auth: ProxyAuthNone})
if BuiltinProxyVersion() == before {
t.Error("登记后版本号应递增(反代层靠它判断缓存失效)")
}
got := BuiltinProxyDefs()
list := got[p]
if len(list) != 1 {
t.Fatalf("登记了 %d 条,期望 1: %+v", len(list), got)
}
if !list[0].WebSocket || list[0].Auth != ProxyAuthNone || list[0].Host != "devices" {
t.Errorf("声明内容不对: %+v", list[0])
}
// 重复登记同名(如自动重启后再次 Start)应为刷新而非累积
RegisterBuiltinProxy(p, "gw", ProxyDef{Name: "gw", Host: "devices", Target: "127.0.0.1:9890", WebSocket: true, Auth: ProxyAuthNone})
if l := BuiltinProxyDefs()[p]; len(l) != 1 {
t.Errorf("重复登记应为刷新,实际累积成 %d 条", len(l))
}
// 空 target 必须被拒(不声明的默认就是不被反代,空声明更不该登记)
RegisterBuiltinProxy(p, "bad", ProxyDef{Name: "bad", Target: " "})
if l := BuiltinProxyDefs()[p]; len(l) != 1 {
t.Errorf("空 target 不应被登记,实际 %d 条", len(l))
}
ClearBuiltinProxyDefs(p)
if _, ok := BuiltinProxyDefs()[p]; ok {
t.Error("清除后不应还有该插件的声明")
}
}
// RegisterProxy 必须把声明转发给内核注入的注册回调,且把 name 落进 def
// (与 RegisterTool 的风格一致:name 同时来自参数与 def.Name)。
func TestRegisterProxyForwards(t *testing.T) {
var gotName string
var got []ProxyDef
ps := New("demo", SDKConfig{})
ps.SetProxyRegistrar(func(name string, d ProxyDef) {
gotName = name
got = append(got, d)
})
ps.RegisterProxy("ui", ProxyDef{Name: "ui", Host: "demo", Target: "127.0.0.1:12100", Auth: ProxyAuthNone})
if len(got) != 1 || got[0].Host != "demo" || got[0].Auth != ProxyAuthNone {
t.Fatalf("声明未转发到注册回调: %+v", got)
}
if gotName != "ui" {
t.Errorf("name 参数未透传: %q", gotName)
}
}