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) } } func TestAnalyticsPageWithoutRunsAndWithUnknownRun(t *testing.T) { _, caURL := newFakeControlAPI(t) ts := newTestServer(t, caURL) page := get(t, ts, "/analytics") if !strings.Contains(page, "Запусков проверки пока нет") || strings.Contains(page, `id="analytics-data"`) { t.Fatalf("expected the empty state, got:\n%s", page) } _, ts = analyticsFake(t) for _, run := range []string{"99", "3"} { // unknown, and the open one page = get(t, ts, "/analytics?run="+run) if !strings.Contains(page, "не найден или ещё не завершён") { t.Fatalf("run %s: expected a warning, got:\n%s", run, page) } } } func TestAnalyticsListProxyAndCSV(t *testing.T) { _, ts := analyticsFake(t) resp, err := http.Get(ts.URL + "/analytics/lists/egress_https_any?run=1") if err != nil { t.Fatal(err) } body, _ := io.ReadAll(resp.Body) resp.Body.Close() if resp.StatusCode != http.StatusOK || !strings.Contains(string(body), `"1.2.3.4"`) { t.Fatalf("list: %d %s", resp.StatusCode, body) } q := url.Values{"run": {"1"}, "class": {"SSH: таймаут"}} resp, err = http.Get(ts.URL + "/analytics/lists/error?" + q.Encode()) if err != nil { t.Fatal(err) } body, _ = io.ReadAll(resp.Body) resp.Body.Close() if resp.StatusCode != http.StatusOK || !strings.Contains(string(body), `"class":"SSH: таймаут"`) { t.Fatalf("error list: %d %s", resp.StatusCode, body) } resp, err = http.Get(ts.URL + "/analytics/csv/egress_https_any?run=1") if err != nil { t.Fatal(err) } body, _ = io.ReadAll(resp.Body) resp.Body.Close() if resp.StatusCode != http.StatusOK || !strings.HasPrefix(resp.Header.Get("Content-Type"), "text/csv") || !strings.Contains(resp.Header.Get("Content-Disposition"), `attachment; filename="egress_https_any_run1.csv"`) { t.Fatalf("csv: %d %v %s", resp.StatusCode, resp.Header, body) } for path, want := range map[string]int{ "/analytics/lists/egress_https_any": http.StatusBadRequest, // no run "/analytics/csv/egress_https_any?run=abc": http.StatusBadRequest, "/analytics/lists/nonsense?run=1": http.StatusNotFound, // control-api says unknown list } { resp, err := http.Get(ts.URL + path) if err != nil { t.Fatal(err) } resp.Body.Close() if resp.StatusCode != want { t.Fatalf("%s: %d, want %d", path, resp.StatusCode, want) } } } // The sidebar: no window-chrome dots next to the logo, the Analytics entry, // and the link state, theme toggle and logout above the navigation. func TestSidebarSessionBlockOnTop(t *testing.T) { _, caURL := newFakeControlAPI(t) _, ts := newAuthTestServer(t, caURL, nil) cookie := login(t, ts) _, page := doReq(t, ts, reqOpts{path: "/overview", cookie: cookie}) if strings.Contains(page, "brand-chrome") { t.Fatalf("the three dots next to the logo are back") } iBrand := strings.Index(page, `class="brand"`) iAPI := strings.Index(page, `class="session-api"`) iLogout := strings.Index(page, `action="/logout"`) iNav := strings.Index(page, `class="nav-groups"`) iFoot := strings.Index(page, "sidebar-foot") if !(iBrand >= 0 && iBrand < iAPI && iAPI < iLogout && iLogout < iNav) || iFoot >= 0 { t.Fatalf("expected brand < control-api state < logout < navigation (and no old footer): %d %d %d %d foot=%d", iBrand, iAPI, iLogout, iNav, iFoot) } for _, link := range []string{`href="/analytics"`, `href="/registry"`, `href="/settings"`} { if !strings.Contains(page, link) { t.Fatalf("missing nav link %s", link) } } if strings.Contains(page, "pulse-dot down") { t.Fatalf("the link state must be fine while control-api answers") } } // The link indicator turns red when control-api cannot be reached. func TestSidebarShowsLostControlAPI(t *testing.T) { ts := newTestServer(t, "http://127.0.0.1:1") // nothing listens there page := get(t, ts, "/overview") if !strings.Contains(page, "pulse-dot down") || !strings.Contains(page, "нет связи") { t.Fatalf("expected the lost-link indicator, got:\n%s", page) } } func TestSettingsSubnetsForm(t *testing.T) { fake, caURL := newFakeControlAPI(t) ts := newTestServer(t, caURL) body := postForm(t, ts, "PUT", "/settings/subnets", url.Values{"subnets": {"83.166.248.0/21 москва\n\n 10.0.0.0/8\n"}}) if len(fake.subnets.Subnets) != 2 || fake.subnets.Subnets[0] != (subnetEntry{CIDR: "83.166.248.0/21", Label: "москва"}) || fake.subnets.Subnets[1] != (subnetEntry{CIDR: "10.0.0.0/8"}) { t.Fatalf("subnets saved: %+v", fake.subnets) } if !strings.Contains(body, "83.166.248.0/21 москва") || !strings.Contains(body, "10.0.0.0/8") { t.Fatalf("the saved list must come back in the form:\n%s", body) } body = postForm(t, ts, "PUT", "/settings/subnets", url.Values{"subnets": {"not-a-cidr"}}) if !strings.Contains(body, "alert-warning") || !strings.Contains(body, "not a CIDR") { t.Fatalf("expected the validation error in the banner:\n%s", body) } } // The drill-down from the analytics page: run and subnet go to control-api, // come back as hidden fields and a visible chip; a malformed subnet is dropped. func TestRegistryDrillDownFromAnalytics(t *testing.T) { fake, caURL := newFakeControlAPI(t) ts := newTestServer(t, caURL) page := get(t, ts, "/registry?run=2&subnet="+url.QueryEscape("10.0.0.0/24")) if len(fake.registryQueries) == 0 { t.Fatal("no registry request") } last := fake.registryQueries[len(fake.registryQueries)-1] if !strings.Contains(last, "run=2") || !strings.Contains(last, "subnet=10.0.0.0%2F24") { t.Fatalf("control-api request %q lacks the run or subnet", last) } for _, want := range []string{`name="run" value="2"`, `name="subnet" value="10.0.0.0/24"`, "запуск 2", "подсеть 10.0.0.0/24", "сбросить фильтр"} { 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, "сбросить фильтр") { t.Fatalf("a malformed subnet must be ignored: %q", last) } }