e157b16c11
用户规则(可配置分流)里 action==proxy/reject 的域名规则,此前不触发 dns.reverse_mapping(只有 direct 域名规则触发)。而应用自行解析域名后按 IP 发起连接,路由层只剩 IP,无反向映射则 domain 规则永不命中——走隧道/拒绝的 域名规则会静默失效。 translateUserRules 把 hasDomainDirect 扩成 hasDomainRule:任意 action 的 域名类规则(domain/domain_suffix/domain_keyword)都置真、都开 reverse_mapping。 direct 专属的 ip_cidr→route_exclude_address(extraExclude)那条线不变。 这也是把私有服务分流(PANGOLIN_PRIVATE_SPLIT_DOMAINS)改用用户规则表达的 前置修复——否则一条"走隧道"用户规则替代 private-split 会连不上。 回归测试 TestBuildConfigProxyDomainEnablesReverseMapping:唯一一条走隧道/拒绝 域名规则(privateSplit 关、无 direct 域名规则)必须开 reverse_mapping。go test ./... 全绿。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A79VtQA1BwTuQN1ThpvYpo
500 lines
16 KiB
Go
500 lines
16 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/wangjia/pangolin/server/internal/nodes"
|
|
"github.com/wangjia/pangolin/server/internal/routing"
|
|
)
|
|
|
|
func testNode() *nodes.NodeRow {
|
|
return &nodes.NodeRow{
|
|
UUID: "n1", Endpoint: "1.2.3.4:443",
|
|
RealityPBK: "pbk", RealityShortID: "sid", RealitySNI: "www.apple.com",
|
|
}
|
|
}
|
|
|
|
func routeOf(t *testing.T, cfg []byte) map[string]any {
|
|
t.Helper()
|
|
var m map[string]any
|
|
if err := json.Unmarshal(cfg, &m); err != nil {
|
|
t.Fatalf("unmarshal config: %v", err)
|
|
}
|
|
return m["route"].(map[string]any)
|
|
}
|
|
|
|
func TestBuildClientConfigSplitCN(t *testing.T) {
|
|
// 开启分流 + base → geoip-cn/geosite-cn rule_set + cn 直连规则;URL 自托管。
|
|
cfg, err := BuildClientConfig(testNode(), "uuid-1", "k",
|
|
ClientConfigOpts{SplitCN: true, RulesBaseURL: "http://node:8080/"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
route := routeOf(t, cfg)
|
|
if rs, ok := route["rule_set"].([]any); !ok || len(rs) != 2 {
|
|
t.Fatalf("expected 2 rule_set, got %v", route["rule_set"])
|
|
}
|
|
s := string(cfg)
|
|
for _, want := range []string{
|
|
"geoip-cn", "geosite-cn",
|
|
"http://node:8080/v1/rules/geoip-cn.srs",
|
|
"http://node:8080/v1/rules/geosite-cn.srs",
|
|
"download_detour",
|
|
} {
|
|
if !strings.Contains(s, want) {
|
|
t.Errorf("config missing %q", want)
|
|
}
|
|
}
|
|
foundCN := false
|
|
for _, r := range route["rules"].([]any) {
|
|
rm := r.(map[string]any)
|
|
if rm["outbound"] == "direct" && rm["rule_set"] != nil {
|
|
foundCN = true
|
|
}
|
|
}
|
|
if !foundCN {
|
|
t.Error("missing cn-direct route rule (rule_set→direct)")
|
|
}
|
|
|
|
// DNS 面分流:开分流时国内域名(geosite-cn)用 local 解析,不走 remote(隧道)。
|
|
var m map[string]any
|
|
_ = json.Unmarshal(cfg, &m)
|
|
dnsRules, ok := m["dns"].(map[string]any)["rules"].([]any)
|
|
if !ok || len(dnsRules) == 0 {
|
|
t.Fatalf("split on should have dns.rules (geosite-cn→local), got %v", m["dns"])
|
|
}
|
|
dr := dnsRules[0].(map[string]any)
|
|
if dr["server"] != "local" || dr["rule_set"] == nil {
|
|
t.Errorf("dns rule should route geosite-cn → local, got %v", dr)
|
|
}
|
|
|
|
// 关闭分流 → 无 rule_set,且 DNS 无分流规则(全量 remote)。
|
|
cfg2, _ := BuildClientConfig(testNode(), "uuid-1", "k", ClientConfigOpts{})
|
|
if _, ok := routeOf(t, cfg2)["rule_set"]; ok {
|
|
t.Error("split off should have no rule_set")
|
|
}
|
|
var m2 map[string]any
|
|
_ = json.Unmarshal(cfg2, &m2)
|
|
if _, ok := m2["dns"].(map[string]any)["rules"]; ok {
|
|
t.Error("split off should have no dns.rules")
|
|
}
|
|
|
|
// 开启但缺 base → 静默不分流(避免渲染出无效 rule_set URL)。
|
|
cfg3, _ := BuildClientConfig(testNode(), "uuid-1", "k", ClientConfigOpts{SplitCN: true})
|
|
if _, ok := routeOf(t, cfg3)["rule_set"]; ok {
|
|
t.Error("split with empty base should have no rule_set")
|
|
}
|
|
}
|
|
|
|
func TestBuildClientConfigPrivateSplit(t *testing.T) {
|
|
domains := []string{"nas.yanmeiai.com", "git.yanmeiai.com", "win.yanmeiai.com"}
|
|
// 私有分流 + 国内分流同时开:验证规则齐全且顺序正确
|
|
// (LAN 直连 → 私有域名强制走隧道 → 国内直连;私有规则必须在国内直连之前,
|
|
// 否则锚点是国内 IP 会被分流成直连、被 frps 侧安全组限源拦截)。
|
|
cfg, err := BuildClientConfig(testNode(), "uuid-1", "k",
|
|
ClientConfigOpts{SplitCN: true, RulesBaseURL: "http://node:8080",
|
|
PrivateSplitDomains: domains})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var m map[string]any
|
|
if err := json.Unmarshal(cfg, &m); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// ① dns.servers 含系统解析器(type=local):在家吃到局域网 DNS 覆盖(私网IP),
|
|
// 在外用所在网络 DNS 解析出公网锚点。
|
|
dnsm := m["dns"].(map[string]any)
|
|
foundSystem := false
|
|
for _, s := range dnsm["servers"].([]any) {
|
|
sm := s.(map[string]any)
|
|
if sm["tag"] == "dns-system" && sm["type"] == "local" {
|
|
foundSystem = true
|
|
}
|
|
}
|
|
if !foundSystem {
|
|
t.Error("missing dns-system (type=local) dns server")
|
|
}
|
|
|
|
// ② dns.rules 首条 = 私有域名→dns-system(须排在 geosite-cn→local 之前)。
|
|
dnsRules := dnsm["rules"].([]any)
|
|
dr := dnsRules[0].(map[string]any)
|
|
if dr["server"] != "dns-system" || dr["domain"] == nil {
|
|
t.Errorf("dns.rules[0] should be private domains → dns-system, got %v", dr)
|
|
}
|
|
|
|
// ③ reverse_mapping 开启:应用自行解析后按 IP 连接,回映射补回域名元数据,
|
|
// 路由的 domain 规则才有效。
|
|
if dnsm["reverse_mapping"] != true {
|
|
t.Error("reverse_mapping should be true when private split is on")
|
|
}
|
|
|
|
// ④ 路由顺序:LAN 直连 < 私有域名→auto < 国内 rule_set→direct。
|
|
rules := m["route"].(map[string]any)["rules"].([]any)
|
|
lanIdx, privIdx, cnIdx := -1, -1, -1
|
|
for i, r := range rules {
|
|
rm := r.(map[string]any)
|
|
if rm["ip_cidr"] != nil && rm["outbound"] == "direct" {
|
|
lanIdx = i
|
|
}
|
|
if rm["domain"] != nil && rm["outbound"] == "auto" {
|
|
privIdx = i
|
|
}
|
|
if rm["rule_set"] != nil && rm["outbound"] == "direct" {
|
|
cnIdx = i
|
|
}
|
|
}
|
|
if lanIdx < 0 || privIdx < 0 || cnIdx < 0 {
|
|
t.Fatalf("missing rules: lan=%d priv=%d cn=%d", lanIdx, privIdx, cnIdx)
|
|
}
|
|
if !(lanIdx < privIdx && privIdx < cnIdx) {
|
|
t.Errorf("rule order wrong: lan=%d < priv=%d < cn=%d expected", lanIdx, privIdx, cnIdx)
|
|
}
|
|
|
|
// ⑤ 不配置 → 全部不出现(行为与旧版完全一致)。
|
|
cfg2, _ := BuildClientConfig(testNode(), "uuid-1", "k", ClientConfigOpts{})
|
|
var m2 map[string]any
|
|
_ = json.Unmarshal(cfg2, &m2)
|
|
dnsm2 := m2["dns"].(map[string]any)
|
|
if _, ok := dnsm2["reverse_mapping"]; ok {
|
|
t.Error("private split off: reverse_mapping should be absent")
|
|
}
|
|
if strings.Contains(string(cfg2), "dns-system") {
|
|
t.Error("private split off: dns-system should be absent")
|
|
}
|
|
}
|
|
|
|
func TestRulesHandler(t *testing.T) {
|
|
dir := t.TempDir()
|
|
if err := os.WriteFile(filepath.Join(dir, "geoip-cn.srs"), []byte("SRS"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
r := chi.NewRouter()
|
|
r.Get("/v1/rules/{name}", NewRulesHandler(dir).Serve)
|
|
|
|
get := func(path string) (int, string) {
|
|
rec := httptest.NewRecorder()
|
|
r.ServeHTTP(rec, httptest.NewRequest("GET", path, nil))
|
|
return rec.Code, rec.Body.String()
|
|
}
|
|
|
|
if code, body := get("/v1/rules/geoip-cn.srs"); code != 200 || body != "SRS" {
|
|
t.Fatalf("allowed file: code=%d body=%q, want 200/SRS", code, body)
|
|
}
|
|
if code, _ := get("/v1/rules/passwd"); code != 404 {
|
|
t.Errorf("disallowed name: code=%d, want 404", code)
|
|
}
|
|
if code, _ := get("/v1/rules/geosite-cn.srs"); code != 404 {
|
|
t.Errorf("allowed but missing file: code=%d, want 404", code)
|
|
}
|
|
}
|
|
|
|
// TUN 入站必须把私有 LAN 网段从隧道排除(route_exclude_address),否则 strict_route
|
|
// 会在 macOS 把 LAN 强抓进隧道 → 隧道开着连不上局域网/NAS。不得含 172.16/12
|
|
// (隧道自身 172.19.x 在此段,排除会断 DNS)。
|
|
func TestBuildClientConfigLANExclude(t *testing.T) {
|
|
cfg, err := BuildClientConfig(testNode(), "uuid-1", "k", ClientConfigOpts{})
|
|
if err != nil {
|
|
t.Fatalf("build: %v", err)
|
|
}
|
|
var m map[string]any
|
|
if err := json.Unmarshal(cfg, &m); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
var tun map[string]any
|
|
for _, in := range m["inbounds"].([]any) {
|
|
im := in.(map[string]any)
|
|
if im["type"] == "tun" {
|
|
tun = im
|
|
break
|
|
}
|
|
}
|
|
if tun == nil {
|
|
t.Fatal("no tun inbound")
|
|
}
|
|
exRaw, ok := tun["route_exclude_address"]
|
|
if !ok {
|
|
t.Fatal("tun inbound missing route_exclude_address (LAN would be captured by strict_route)")
|
|
}
|
|
got := map[string]bool{}
|
|
for _, v := range exRaw.([]any) {
|
|
got[v.(string)] = true
|
|
}
|
|
if !got["192.168.0.0/16"] {
|
|
t.Error("route_exclude_address must contain 192.168.0.0/16")
|
|
}
|
|
if !got["10.0.0.0/8"] {
|
|
t.Error("route_exclude_address must contain 10.0.0.0/8")
|
|
}
|
|
if got["172.16.0.0/12"] {
|
|
t.Error("route_exclude_address must NOT contain 172.16.0.0/12 (tunnel DNS 172.19.x lives there)")
|
|
}
|
|
}
|
|
|
|
// --- Task 4 test helpers ---
|
|
//
|
|
// NOTE on route_exclude_address: sing-box only honors this key on the TUN
|
|
// inbound (see tunIn construction + comment at clientconfig.go:~116-121;
|
|
// TestBuildClientConfigLANExclude above already asserts it there) — there is
|
|
// no such key under the top-level "route" object. The task brief's sample
|
|
// snippet checked cfg["route"]["route_exclude_address"], which does not
|
|
// exist in main's rendering and would make "IP 直连真生效" a no-op. Fixed
|
|
// here to read it from the tun inbound, consistent with the existing
|
|
// mechanism this task explicitly says to reuse ("main clientconfig.go:121").
|
|
|
|
func ruleIndexByDomain(rules []any, domain string) int {
|
|
for i, r := range rules {
|
|
rm := r.(map[string]any)
|
|
for _, key := range []string{"domain", "domain_suffix", "domain_keyword"} {
|
|
if v, ok := rm[key]; ok {
|
|
for _, s := range toStrings(v) {
|
|
if s == domain {
|
|
return i
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
func ruleIndexByRuleSet(rules []any, tag string) int {
|
|
for i, r := range rules {
|
|
rm := r.(map[string]any)
|
|
if v, ok := rm["rule_set"]; ok {
|
|
for _, s := range toStrings(v) {
|
|
if s == tag {
|
|
return i
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
func hasHijackDNS(rules []any) bool {
|
|
for _, r := range rules {
|
|
rm := r.(map[string]any)
|
|
if rm["action"] == "hijack-dns" {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func toStrings(v any) []string {
|
|
arr, ok := v.([]any)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
out := make([]string, 0, len(arr))
|
|
for _, x := range arr {
|
|
if s, ok := x.(string); ok {
|
|
out = append(out, s)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func contains(ss []string, s string) bool {
|
|
for _, x := range ss {
|
|
if x == s {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// tunRouteExcludeAddress reads route_exclude_address off the TUN inbound
|
|
// (the field that actually has effect — see NOTE above).
|
|
func tunRouteExcludeAddress(t *testing.T, cfg map[string]any) []string {
|
|
t.Helper()
|
|
for _, in := range cfg["inbounds"].([]any) {
|
|
im := in.(map[string]any)
|
|
if im["type"] == "tun" {
|
|
return toStrings(im["route_exclude_address"])
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func TestBuildConfigUserRules(t *testing.T) {
|
|
node := testNode()
|
|
p := routing.Default()
|
|
p.Rules = []routing.Rule{
|
|
{Type: "domain_suffix", Value: "github.com", Action: "proxy", Enabled: true},
|
|
{Type: "ip_cidr", Value: "35.190.0.0/16", Action: "direct", Enabled: true},
|
|
{Type: "domain_suffix", Value: "git.51yanmei.com", Action: "direct", Enabled: true},
|
|
}
|
|
raw, err := BuildClientConfig(node, "dp", "k", ClientConfigOpts{Profile: p, SplitCN: true, RulesBaseURL: "http://x"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var cfg map[string]any
|
|
if err := json.Unmarshal(raw, &cfg); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rules := cfg["route"].(map[string]any)["rules"].([]any)
|
|
// 用户 github→auto 规则应在 geoip-cn 规则之前
|
|
iUser, iCN := ruleIndexByDomain(rules, "github.com"), ruleIndexByRuleSet(rules, "geoip-cn")
|
|
if iUser < 0 || iCN < 0 || iUser > iCN {
|
|
t.Fatalf("user rule must precede geoip-cn: %d vs %d", iUser, iCN)
|
|
}
|
|
// IP 直连并入 route_exclude_address(TUN 入站,见上方 NOTE)
|
|
excl := tunRouteExcludeAddress(t, cfg)
|
|
if !contains(excl, "35.190.0.0/16") {
|
|
t.Fatalf("ip direct not in route_exclude_address: %v", excl)
|
|
}
|
|
// 有域名直连 → reverse_mapping 开
|
|
if cfg["dns"].(map[string]any)["reverse_mapping"] != true {
|
|
t.Fatal("reverse_mapping must be on")
|
|
}
|
|
}
|
|
|
|
// 回归钉:走隧道/拒绝的域名规则也必须开 reverse_mapping。应用自行解析域名后按 IP
|
|
// 发起连接,路由层只剩 IP,无反向映射则 domain 规则永不命中、该规则静默失效。
|
|
// 旧实现只为 action==direct 的域名规则开 reverse_mapping,走隧道/拒绝会漏 → 本测试
|
|
// 用**唯一一条走隧道域名规则**(无任何 direct 域名规则、privateSplit 关)钉死修复。
|
|
func TestBuildConfigProxyDomainEnablesReverseMapping(t *testing.T) {
|
|
for _, action := range []string{"proxy", "reject"} {
|
|
node := testNode()
|
|
p := routing.Default()
|
|
p.Rules = []routing.Rule{
|
|
{Type: "domain_suffix", Value: "example.com", Action: action, Enabled: true},
|
|
}
|
|
// SplitCN 关 + 不给 PrivateSplitDomains → reverse_mapping 只可能由该域名规则触发。
|
|
raw, err := BuildClientConfig(node, "dp", "k", ClientConfigOpts{Profile: p})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var cfg map[string]any
|
|
if err := json.Unmarshal(raw, &cfg); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cfg["dns"].(map[string]any)["reverse_mapping"] != true {
|
|
t.Fatalf("action=%s domain rule must enable reverse_mapping", action)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBuildConfigGlobalMode(t *testing.T) {
|
|
p := routing.Default()
|
|
p.Mode = "global"
|
|
p.Rules = []routing.Rule{{Type: "domain_suffix", Value: "github.com", Action: "direct", Enabled: true}}
|
|
raw, err := BuildClientConfig(testNode(), "dp", "k", ClientConfigOpts{Profile: p, SplitCN: true, RulesBaseURL: "http://x"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var cfg map[string]any
|
|
if err := json.Unmarshal(raw, &cfg); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// global:忽略用户规则 + 无 geoip-cn 直连,final=auto,但系统层(hijack-dns/LAN)仍在
|
|
rules := cfg["route"].(map[string]any)["rules"].([]any)
|
|
if ruleIndexByDomain(rules, "github.com") >= 0 {
|
|
t.Fatal("global must ignore user rules")
|
|
}
|
|
if cfg["route"].(map[string]any)["final"] != "auto" {
|
|
t.Fatal("global final=auto")
|
|
}
|
|
if !hasHijackDNS(rules) {
|
|
t.Fatal("system layer must survive in global")
|
|
}
|
|
}
|
|
|
|
func TestBuildConfigDirectMode(t *testing.T) {
|
|
p := routing.Default()
|
|
p.Mode = "direct"
|
|
p.Rules = []routing.Rule{{Type: "domain_suffix", Value: "github.com", Action: "proxy", Enabled: true}}
|
|
raw, err := BuildClientConfig(testNode(), "dp", "k", ClientConfigOpts{Profile: p, SplitCN: true, RulesBaseURL: "http://x"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var cfg map[string]any
|
|
if err := json.Unmarshal(raw, &cfg); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rules := cfg["route"].(map[string]any)["rules"].([]any)
|
|
if ruleIndexByDomain(rules, "github.com") >= 0 {
|
|
t.Fatal("direct mode must ignore user rules")
|
|
}
|
|
if cfg["route"].(map[string]any)["final"] != "direct" {
|
|
t.Fatal("direct mode final=direct")
|
|
}
|
|
if !hasHijackDNS(rules) {
|
|
t.Fatal("system layer must survive in direct mode")
|
|
}
|
|
}
|
|
|
|
func TestBuildConfigRuleModeFinalDirect(t *testing.T) {
|
|
p := routing.Default()
|
|
p.Final = "direct"
|
|
raw, err := BuildClientConfig(testNode(), "dp", "k", ClientConfigOpts{Profile: p, RulesBaseURL: "http://x"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var cfg map[string]any
|
|
if err := json.Unmarshal(raw, &cfg); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cfg["route"].(map[string]any)["final"] != "direct" {
|
|
t.Fatal("rule mode with Final=direct should render final=direct")
|
|
}
|
|
}
|
|
|
|
func TestBuildConfigUserGeoRuleWithoutSplitCN(t *testing.T) {
|
|
// SplitCN 关闭(Builtin.ChinaDirect=false)但用户手动加了 geoip-cn 规则 →
|
|
// 仍需补 rule_set 定义,规则本身要生效。
|
|
p := routing.Default()
|
|
p.Builtin.ChinaDirect = false
|
|
p.Rules = []routing.Rule{{Type: "geoip", Value: "cn", Action: "direct", Enabled: true}}
|
|
raw, err := BuildClientConfig(testNode(), "dp", "k", ClientConfigOpts{Profile: p, RulesBaseURL: "http://x"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var cfg map[string]any
|
|
if err := json.Unmarshal(raw, &cfg); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
route := cfg["route"].(map[string]any)
|
|
rules := route["rules"].([]any)
|
|
if ruleIndexByRuleSet(rules, "geoip-cn") < 0 {
|
|
t.Fatal("user geoip-cn rule must be present in route.rules")
|
|
}
|
|
rs, ok := route["rule_set"].([]any)
|
|
if !ok || len(rs) == 0 {
|
|
t.Fatal("route.rule_set must define geoip-cn even though SplitCN(Builtin.ChinaDirect) is off")
|
|
}
|
|
found := false
|
|
for _, d := range rs {
|
|
if dm, ok := d.(map[string]any); ok && dm["tag"] == "geoip-cn" {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatal("geoip-cn definition missing from route.rule_set")
|
|
}
|
|
}
|
|
|
|
func TestBuildConfigNilProfileUnchanged(t *testing.T) {
|
|
// Profile==nil → 与现有行为逐字节一致(回退默认)
|
|
a, err := BuildClientConfig(testNode(), "dp", "k", ClientConfigOpts{SplitCN: true, RulesBaseURL: "http://x"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b, err := BuildClientConfig(testNode(), "dp", "k", ClientConfigOpts{Profile: nil, SplitCN: true, RulesBaseURL: "http://x"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !bytes.Equal(a, b) {
|
|
t.Fatal("nil profile must equal no-profile")
|
|
}
|
|
}
|