19d28a8f89
服务器用户列表缓存跨协议污染修复(HIF-1 / 详见 PR #5 四件套): 1. 缓存 key 加 protocol 维度(`server:user:{server_id}:{protocol}`),对齐 ServerConfig 已有约定 2. 显式枚举协议清除用户列表缓存(AllProtocols + ServerUserListCacheKeysForServer),不用 SCAN 3. 三个兜底分支不写缓存 + Errorw 日志(带 server_id + protocol 字段) 4. hysteria2 → hysteria 兼容归一化 + 6 个新单测 Closes HIF-1
79 lines
2.9 KiB
Go
79 lines
2.9 KiB
Go
package server
|
||
|
||
import (
|
||
"fmt"
|
||
"slices"
|
||
"testing"
|
||
|
||
"github.com/perfect-panel/server/internal/model/node"
|
||
)
|
||
|
||
// TestNormalizeServerUserListProtocol 验证客户端 hysteria2 兼容字段被映射回
|
||
// DB 规范名 "hysteria";其他协议必须原样返回,不可被误改。
|
||
// 缺这一步会导致 hysteria2 永远查不到节点、永远走兜底,HIF-1 表象。
|
||
func TestNormalizeServerUserListProtocol(t *testing.T) {
|
||
cases := []struct {
|
||
name string
|
||
in string
|
||
want string
|
||
}{
|
||
{"hysteria2 maps to hysteria", "hysteria2", "hysteria"},
|
||
{"hysteria unchanged", "hysteria", "hysteria"},
|
||
{"trojan unchanged", "trojan", "trojan"},
|
||
{"vless unchanged", "vless", "vless"},
|
||
{"vmess unchanged", "vmess", "vmess"},
|
||
{"tuic unchanged", "tuic", "tuic"},
|
||
{"shadowsocks unchanged", "shadowsocks", "shadowsocks"},
|
||
{"anytls unchanged", "anytls", "anytls"},
|
||
{"empty unchanged", "", ""},
|
||
}
|
||
for _, c := range cases {
|
||
t.Run(c.name, func(t *testing.T) {
|
||
got := normalizeServerUserListProtocol(c.in)
|
||
if got != c.want {
|
||
t.Errorf("normalizeServerUserListProtocol(%q) = %q, want %q", c.in, got, c.want)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestServerUserListCacheKey_ProtocolIsolation 验证缓存 key 在 server_id 相同、
|
||
// protocol 不同(如 trojan vs tuic)时一定不同——这是 HIF-1 的根因防回归。
|
||
// 若 key 重叠,tuic 的兜底假用户会污染 trojan 的真实用户列表。
|
||
func TestServerUserListCacheKey_ProtocolIsolation(t *testing.T) {
|
||
const serverId int64 = 42
|
||
build := func(protocol string) string {
|
||
return fmt.Sprintf("%s%d:%s", node.ServerUserListCacheKey, serverId, normalizeServerUserListProtocol(protocol))
|
||
}
|
||
|
||
protocols := []string{"trojan", "vless", "vmess", "tuic", "shadowsocks", "anytls"}
|
||
seen := make(map[string]string, len(protocols))
|
||
for _, p := range protocols {
|
||
key := build(p)
|
||
if other, exists := seen[key]; exists {
|
||
t.Fatalf("cache key collision: %q (%q) == %q (%q)", p, key, other, key)
|
||
}
|
||
seen[key] = p
|
||
}
|
||
|
||
// hysteria 与 hysteria2 必须落到同一 key,否则 hysteria2 客户端拿不到 hysteria 节点列表
|
||
hysteriaKey := build("hysteria")
|
||
hysteria2Key := build("hysteria2")
|
||
if hysteriaKey != hysteria2Key {
|
||
t.Errorf("hysteria/hysteria2 expected to share cache key, got %q vs %q", hysteriaKey, hysteria2Key)
|
||
}
|
||
}
|
||
|
||
// TestServerUserListCacheKey_ShapeMatchesDelPath 验证 GetServerUserList 写入的
|
||
// cache key 形态与 ServerUserListCacheKeysForServer(Del 路径)枚举出的 key
|
||
// 形态完全一致。形态一旦漂移,Del 会清不到刚写入的 key,HIF-1 重现。
|
||
func TestServerUserListCacheKey_ShapeMatchesDelPath(t *testing.T) {
|
||
const serverId int64 = 7
|
||
writeShape := fmt.Sprintf("%s%d:%s", node.ServerUserListCacheKey, serverId, normalizeServerUserListProtocol("trojan"))
|
||
|
||
delKeys := node.ServerUserListCacheKeysForServer(serverId)
|
||
if !slices.Contains(delKeys, writeShape) {
|
||
t.Fatalf("write-path key %q not present in Del-path enumeration %v", writeShape, delKeys)
|
||
}
|
||
}
|