package siteprobe import ( "strings" "testing" ) // TestEvalPath 覆盖接口监控最常见的几种 JSONPath 写法。 // Logan.Li func TestEvalPath(t *testing.T) { doc := map[string]any{ "data": map[string]any{ "status": "ok", "ips": []any{ map[string]any{"ip": "1.1.1.1"}, map[string]any{"ip": "2.2.2.2"}, }, }, } cases := []struct { path string want string }{ {"$.data.status", "ok"}, {"data.status", "ok"}, {"$.data.ips[0].ip", "1.1.1.1"}, {"$['data']['status']", "ok"}, } for _, c := range cases { got, err := EvalPath(doc, c.path) if err != nil { t.Fatalf("%s 解析失败: %v", c.path, err) } if len(got) != 1 || got[0] != c.want { t.Fatalf("%s 期望 %q,实际 %v", c.path, c.want, got) } } // 通配 got, err := EvalPath(doc, "$.data.ips[*].ip") if err != nil || len(got) != 2 { t.Fatalf("通配解析失败: %v %v", got, err) } // 未命中应返回错误 if _, err := EvalPath(doc, "$.data.nope"); err == nil { t.Fatal("未命中路径应返回错误") } } // TestNormalizeTextIgnoresVolatileFields 时间戳变化不应触发内容变更。 // Logan.Li func TestNormalizeTextIgnoresVolatileFields(t *testing.T) { a := "173.245.48.0/20\nupdated_at: 2026-09-14T10:00:00Z" b := "173.245.48.0/20\nupdated_at: 2026-09-15T22:31:09Z" rules := []string{`updated_at:.*`} sa := BuildSnapshot(ContentModeText, a, nil, rules, 0, false) sb := BuildSnapshot(ContentModeText, b, nil, rules, 0, false) if sa.Invalid != "" || sb.Invalid != "" { t.Fatalf("快照构建失败: %s / %s", sa.Invalid, sb.Invalid) } if sa.Hash != sb.Hash { t.Fatalf("仅时间戳不同,哈希应一致: %s != %s", sa.Canonical, sb.Canonical) } } // TestNormalizeTextSortLines IP 段顺序变化不应视为变更。 // Logan.Li func TestNormalizeTextSortLines(t *testing.T) { a := "103.21.244.0/22\n173.245.48.0/20" b := "173.245.48.0/20\n103.21.244.0/22" unsortedA := BuildSnapshot(ContentModeText, a, nil, nil, 0, false) unsortedB := BuildSnapshot(ContentModeText, b, nil, nil, 0, false) if unsortedA.Hash == unsortedB.Hash { t.Fatal("未排序时顺序不同应视为不同内容") } sortedA := BuildSnapshot(ContentModeText, a, nil, nil, 0, true) sortedB := BuildSnapshot(ContentModeText, b, nil, nil, 0, true) if sortedA.Hash != sortedB.Hash { t.Fatalf("排序后应一致: %s / %s", sortedA.Canonical, sortedB.Canonical) } } // TestBuildSnapshotInvalid 采集异常不应被当成内容变更。 // Logan.Li func TestBuildSnapshotInvalid(t *testing.T) { // 空响应 if s := BuildSnapshot(ContentModeText, " ", nil, nil, 0, false); s.Invalid == "" { t.Fatal("空响应应判为采集异常") } // 过短(模拟 CDN 拦截页) if s := BuildSnapshot(ContentModeText, "Access denied", nil, nil, 100, false); s.Invalid == "" { t.Fatal("短于最小长度应判为采集异常") } // JSON 模式拿到 HTML if s := BuildSnapshot(ContentModeJSON, "error", []string{"$.a"}, nil, 0, false); s.Invalid == "" { t.Fatal("非 JSON 应判为采集异常") } // 正常 if s := BuildSnapshot(ContentModeText, "1.1.1.1\n2.2.2.2", nil, nil, 5, false); s.Invalid != "" { t.Fatalf("正常内容不应报错: %s", s.Invalid) } } // TestJSONModeOnlyWatchesSelectedFields 未被点名的字段变化不应影响哈希。 // Logan.Li func TestJSONModeOnlyWatchesSelectedFields(t *testing.T) { a := `{"data":{"status":"ok","trace_id":"aaa"},"ts":1}` b := `{"data":{"status":"ok","trace_id":"bbb"},"ts":2}` sa := BuildSnapshot(ContentModeJSON, a, []string{"$.data.status"}, nil, 0, false) sb := BuildSnapshot(ContentModeJSON, b, []string{"$.data.status"}, nil, 0, false) if sa.Hash != sb.Hash { t.Fatalf("只监控 $.data.status 时 trace_id/ts 变化不应影响: %s / %s", sa.Canonical, sb.Canonical) } // status 变化必须被识别 c := `{"data":{"status":"degraded","trace_id":"aaa"},"ts":1}` sc := BuildSnapshot(ContentModeJSON, c, []string{"$.data.status"}, nil, 0, false) if sc.Hash == sa.Hash { t.Fatal("status 变化应改变哈希") } } // TestDiffText 行级差异。 // Logan.Li func TestDiffText(t *testing.T) { old := "a\nb\nc" new := "a\nc\nd" diff := DiffText(old, new) var adds, dels int for _, d := range diff { switch d.Type { case "add": adds++ case "del": dels++ } } if adds != 1 || dels != 1 { t.Fatalf("期望 1 增 1 删,实际 add=%d del=%d: %+v", adds, dels, diff) } } // TestDiffJSONFields 字段级差异。 // Logan.Li func TestDiffJSONFields(t *testing.T) { old := `{"$.data.status":"ok","$.data.level":1}` new := `{"$.data.status":"degraded","$.data.level":1,"$.data.extra":"x"}` diff := DiffJSONFields(old, new) var changed, added int for _, d := range diff { if d.Kind == "changed" && d.Path == "$.data.status" { changed++ if d.Old != "ok" || d.New != "degraded" { t.Fatalf("字段值错误: %+v", d) } } if d.Kind == "added" && d.Path == "$.data.extra" { added++ } } if changed != 1 || added != 1 { t.Fatalf("期望 1 改 1 增,实际: %+v", diff) } } // TestParseRules 兼容 JSON 数组与多行文本两种输入。 // Logan.Li func TestParseRules(t *testing.T) { if r := ParseRules(`["a","b"]`); len(r) != 2 { t.Fatalf("JSON 数组解析失败: %v", r) } if r := ParseRules("a\nb\n"); len(r) != 2 { t.Fatalf("多行解析失败: %v", r) } if r := ParseRules(" "); r != nil { t.Fatalf("空输入应返回 nil: %v", r) } } // TestNormalizeTextDropsBlankLines 空行与首尾空白不应影响比对。 // Logan.Li func TestNormalizeTextDropsBlankLines(t *testing.T) { a := "1.1.1.1\n\n2.2.2.2\n" b := " 1.1.1.1 \n2.2.2.2" sa := BuildSnapshot(ContentModeText, a, nil, nil, 0, false) sb := BuildSnapshot(ContentModeText, b, nil, nil, 0, false) if sa.Hash != sb.Hash { t.Fatalf("空行/空白不应影响: %q vs %q", sa.Canonical, sb.Canonical) } if strings.Contains(sa.Canonical, "\n\n") { t.Fatal("规范化后不应保留空行") } }