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) } } } // compareWith is the minimal comparison JSON the page needs; new is a marker // that tells the pairs of runs apart in the page source. func compareWith(newAddrs string) string { return `{"runs":{"base":{"id":1,"rechecked":0,"addresses":6440},"target":{"id":2,"rechecked":0,"addresses":900}},` + `"groups":{"new":` + newAddrs + `,"left":2,"common":3,"changed":1,"same":2},"indicators":[],` + `"transitions":{"verdicts":["pass","partial","fail"],"matrix":[[0,0,0],[0,0,0],[0,0,0]],"new":[0,0,0],"left":[0,0,0]},"cancelled":{"base":0,"target":0}}` } // compareFake is analyticsFake with the comparisons of runs 1 and 2 (run 3 is open). func compareFake(t *testing.T) (*fakeControlAPI, *httptest.Server) { t.Helper() fake, ts := analyticsFake(t) fake.compares = map[string]string{"1-2": compareWith("111"), "2-1": compareWith("222")} fake.compareLists = map[string]string{ "1-2/changed": `{"group":"changed","columns":["Адрес","Подсеть"],"rows":[["1.2.3.4","1.2.3.0/24"]]}`, "1-2/new/verdict_pass": `{"group":"new","indicator":"verdict_pass","columns":["Адрес"],"rows":[["5.6.7.8"]]}`, } return fake, ts } func compareRequests(fake *fakeControlAPI) []string { var out []string for _, r := range fake.analyticsReqs { if strings.Contains(r, "/compare") { out = append(out, r) } } return out } // By default B is the newest finished run and A the one before it; the open // run is not offered; the page asks control-api for that pair only. func TestAnalyticsComparePageDefaultPair(t *testing.T) { fake, ts := compareFake(t) page := get(t, ts, "/analytics/compare") for _, want := range []string{ `id="an-base"`, `id="an-target"`, `id="an-swap"`, `id="analytics-data"`, `"new":111`, `