package agentd import ( "os" "path/filepath" "testing" ) // writeACL 把 acl.json 写到指定路径。 func writeACL(t *testing.T, path, body string) { t.Helper() if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(path, []byte(body), 0o600); err != nil { t.Fatal(err) } } const validACL = `{ "enabled": true, "allow_dp_uuids": ["uuid-me-1", "uuid-me-sub"], "targets": [ { "domain": ["brain.51yanmei.com", "git.51yanmei.com"] }, { "ip_cidr": ["182.92.213.171/32"], "port": [5001, 3389, 10022, 10023] } ] }` func TestLoadACLConfig(t *testing.T) { dir := t.TempDir() t.Run("文件不存在返回 nil,nil(未配置,不是错误)", func(t *testing.T) { ac, err := LoadACLConfig(filepath.Join(dir, "missing.json")) if err != nil { t.Fatalf("want nil error, got %v", err) } if ac != nil { t.Fatalf("want nil config, got %+v", ac) } }) t.Run("坏 JSON 返回 error(绝不静默降级)", func(t *testing.T) { p := filepath.Join(dir, "bad.json") writeACL(t, p, `{"enabled": true,`) if _, err := LoadACLConfig(p); err == nil { t.Fatal("want error for malformed JSON, got nil") } }) t.Run("合法配置解析出全部字段", func(t *testing.T) { p := filepath.Join(dir, "acl.json") writeACL(t, p, validACL) ac, err := LoadACLConfig(p) if err != nil { t.Fatal(err) } if !ac.Enabled { t.Error("Enabled = false, want true") } if len(ac.AllowDpUUIDs) != 2 { t.Errorf("AllowDpUUIDs len = %d, want 2", len(ac.AllowDpUUIDs)) } if len(ac.Targets) != 2 { t.Fatalf("Targets len = %d, want 2", len(ac.Targets)) } if len(ac.Targets[0].Domain) != 2 { t.Errorf("Targets[0].Domain len = %d, want 2", len(ac.Targets[0].Domain)) } if len(ac.Targets[1].Port) != 4 { t.Errorf("Targets[1].Port len = %d, want 4", len(ac.Targets[1].Port)) } }) } // active() 的语义与 WARP 相反:空白名单不等于「关闭」,而等于「谁都不许进」。 func TestACLActive_FailClosed(t *testing.T) { cases := []struct { name string ac *ACLConfig want bool }{ {"nil 配置 → 未启用", nil, false}, {"enabled=false → 未启用(唯一的合法关闭途径)", &ACLConfig{ Enabled: false, AllowDpUUIDs: []string{"u"}, Targets: []ACLTarget{{Domain: []string{"a.com"}}}, }, false}, {"无 target → 未启用(无从拒起)", &ACLConfig{ Enabled: true, AllowDpUUIDs: []string{"u"}, }, false}, {"target 全为空条件 → 未启用", &ACLConfig{ Enabled: true, Targets: []ACLTarget{{}}, }, false}, {"白名单为空但有 target → 仍启用(拒绝所有人)", &ACLConfig{ Enabled: true, AllowDpUUIDs: nil, Targets: []ACLTarget{{Domain: []string{"a.com"}}}, }, true}, {"完整配置 → 启用", &ACLConfig{ Enabled: true, AllowDpUUIDs: []string{"u"}, Targets: []ACLTarget{{IPCIDR: []string{"1.2.3.4/32"}, Port: []int{443}}}, }, true}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { if got := tc.ac.active(); got != tc.want { t.Errorf("active() = %v, want %v", got, tc.want) } }) } } func TestACLCleanHelpers(t *testing.T) { ac := &ACLConfig{ Enabled: true, AllowDpUUIDs: []string{" uuid-a ", "", "uuid-b"}, Targets: []ACLTarget{ {Domain: []string{" BRAIN.51yanmei.com ", ""}}, {}, {IPCIDR: []string{"1.2.3.4/32"}}, }, } uuids := ac.cleanUUIDs() if len(uuids) != 2 || uuids[0] != "uuid-a" || uuids[1] != "uuid-b" { t.Errorf("cleanUUIDs() = %v, want [uuid-a uuid-b]", uuids) } targets := ac.cleanTargets() if len(targets) != 2 { t.Fatalf("cleanTargets() len = %d, want 2 (空 target 应被丢弃)", len(targets)) } if targets[0].Domain[0] != "brain.51yanmei.com" { t.Errorf("域名未规范化为小写去空白: %q", targets[0].Domain[0]) } } func TestACLTargetMatchFields(t *testing.T) { tgt := ACLTarget{ Domain: []string{"a.com"}, IPCIDR: []string{"1.2.3.4/32"}, Port: []int{443, 5001}, } m := tgt.matchFields() if _, ok := m["domain"]; !ok { t.Error("缺 domain 字段") } if _, ok := m["ip_cidr"]; !ok { t.Error("缺 ip_cidr 字段") } if _, ok := m["port"]; !ok { t.Error("缺 port 字段") } if _, ok := m["domain_suffix"]; ok { t.Error("空的 domain_suffix 不应出现在输出里") } // matchFields 必须每次返回新 map,否则放行/拒绝两条规则会共享同一对象, // 给其中一条加 "user"/"action" 会污染另一条。 m2 := tgt.matchFields() m2["user"] = []string{"x"} if _, ok := m["user"]; ok { t.Error("matchFields 返回了共享 map,放行与拒绝规则会互相污染") } } func TestConfigACLPaths(t *testing.T) { c := Config{StateDir: "/etc/pangolin-agent"}.withDefaults() if want := "/etc/pangolin-agent/acl.json"; c.ACLConfigPath != want { t.Errorf("ACLConfigPath = %q, want %q", c.ACLConfigPath, want) } if want := "/etc/pangolin-agent/acl.last-good.json"; c.ACLLastGoodPath() != want { t.Errorf("ACLLastGoodPath() = %q, want %q", c.ACLLastGoodPath(), want) } }