package dashboard import ( "io" "net/http" "net/http/httptest" "net/url" "strings" "testing" "time" ) func fakeRun(id int64, state string, addresses, pass int) analyticsRun { start := time.Date(2026, 10, 2, 13, 47, 0, 0, time.UTC) end := start.Add(8*time.Hour + 42*time.Minute) r := analyticsRun{ID: id, Kind: "manual", State: state, StartedAt: start, Addresses: addresses, Pass: pass, Partial: addresses - pass, Total: addresses} if state == "finalized" { r.FinalizedAt = &end } else { r.Pending = 80 r.Total = addresses + r.Pending } return r } // reportWith is the minimal report JSON the page needs; addresses is a marker // that tells the runs apart in the page source. func reportWith(addresses string) string { return `{"run":{"rechecked":0},"summary":{"addresses":` + addresses + `,"pass":1,"partial":0,"fail":0,"cancelled":0,` + `"egress_ok":1,"ingress_ok":1,"egress_https_any_failed":0,"egress_https_all_failed":0,"egress_https_all_targets_failed":0,` + `"ingress_ssh_any_failed":0,"ingress_ssh_all_failed":0,"addresses_per_minute":1},"reasons":[],"quality":{},"subnets":[],` + `"targets":{"types":[],"targets":[],"failed":{}},"matrix":{},"sites":{"types":[],"rows":[]},"errors":[],"validators":[]}` } func analyticsFake(t *testing.T) (*fakeControlAPI, *httptest.Server) { t.Helper() fake, caURL := newFakeControlAPI(t) fake.runs = []analyticsRun{fakeRun(3, "open", 120, 40), fakeRun(2, "finalized", 900, 300), fakeRun(1, "finalized", 6440, 1962)} fake.reports = map[int64]string{1: reportWith("6440"), 2: reportWith("900")} fake.lists = map[string]string{ "1/egress_https_any": `{"kind":"egress_https_any","columns":["Адрес","Подсеть"],"rows":[["1.2.3.4","1.2.3.0/24"]]}`, "1/error/SSH: таймаут": `{"kind":"error","class":"SSH: таймаут","columns":["Адрес"],"rows":[["1.2.3.4"]]}`, } return fake, newTestServer(t, caURL) } // The page shows one run, by default the newest finished one, and asks // control-api for that run only; the selector lists every run, the open one // disabled. func TestAnalyticsPageShowsOneRun(t *testing.T) { fake, ts := analyticsFake(t) page := get(t, ts, "/analytics") for _, want := range []string{ `id="an-run"`, `id="analytics-data"`, `"addresses":900`, // newest finished run (2) "идёт · ручной · 120 из 200 · недоступен", "6 440 адр. · 30% pass", // run 1's option, from the run list `/analytics?run=1`, // the older run is one step back } { if !strings.Contains(page, want) { t.Fatalf("expected %q in the page, got:\n%s", want, page) } } if strings.Contains(page, `"addresses":6440`) { t.Fatalf("the data of run 1 is on the page of run 2") } if !strings.Contains(page, `value="3" disabled`) { t.Fatalf("the open run must be listed but disabled:\n%s", page) } for _, r := range fake.analyticsReqs { if strings.Contains(r, "/runs/1") || strings.Contains(r, "/runs/3") { t.Fatalf("the page must request only the chosen run, got %v", fake.analyticsReqs) } } other := get(t, ts, "/analytics?run=1") if !strings.Contains(other, `"addresses":6440`) || strings.Contains(other, `"addresses":900`) { t.Fatalf("run 1 page must carry only run 1's data:\n%s", other) } } // The analytics filters: the subnet goes to control-api with the report and the // lists, direction and protocol focus the page and, both given, add the chart of // the registry for the run and subnet; every link of the page keeps the filter. func TestAnalyticsPageFilters(t *testing.T) { fake, ts := analyticsFake(t) fake.subnets = subnetList{Subnets: []subnetEntry{{CIDR: "9.9.9.0/24", Label: "Офис"}, {CIDR: "10.0.0.0/8"}}} fake.breakdown = registryBreakdown{Group: "target", Direction: "egress", Protocol: "https", Addresses: 5, Rows: []breakdownRow{{Key: "a.test", Label: "a.test", Total: 5, OK: 4}}} lastReq := func(reqs []string) string { fake.mu.Lock(); defer fake.mu.Unlock(); return reqs[len(reqs)-1] } page := get(t, ts, "/analytics?run=1&subnet=9.9.9.0/24&direction=egress&protocol=https") for _, want := range []string{ ``, ``, `href="/analytics?run=2&subnet=10.0.0.0%2F24"`, "сбросить фильтры"} { if !strings.Contains(page, want) { t.Fatalf("expected %q in:\n%s", want, page) } } page = get(t, ts, "/registry?subnet=garbage") last = fake.registryQueries[len(fake.registryQueries)-1] if strings.Contains(last, "subnet=") || strings.Contains(page, `garbage`) { t.Fatalf("a malformed subnet must be ignored: %q", last) } } // The filter of a comparison list goes to control-api as it is. func TestAnalyticsCompareListPath(t *testing.T) { got := analyticsCompareListPath(1, 2, "common", compareFilter{Indicator: "verdict_pass", From: "partial", To: "pass"}, true) want := "/api/v1/admin/analytics/compare/lists/common?base=1&format=csv&from=partial&indicator=verdict_pass&target=2&to=pass" if got != want { t.Errorf("path = %q, want %q", got, want) } if got := analyticsCompareListPath(3, 4, "new", compareFilter{}, false); got != "/api/v1/admin/analytics/compare/lists/new?base=3&target=4" { t.Errorf("path = %q", got) } } // The "Подсеть" selector: configured subnets as "CIDR — label", disabled with a // link to /settings when there are none. func TestRegistrySubnetSelect(t *testing.T) { fake, caURL := newFakeControlAPI(t) ts := newTestServer(t, caURL) page := get(t, ts, "/registry") for _, want := range []string{`