2026-10-03 18:36:03 +03:00
|
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-04 10:26:37 +03:00
|
|
|
|
// 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`,
|
|
|
|
|
|
`<option value="1" selected>`, `<option value="2" selected>`, // A is run 1, B is run 2
|
|
|
|
|
|
`id="an-dlg"`, `/static/analytics-dialog.js`, `/static/analytics-compare.js`,
|
|
|
|
|
|
"6\u00a0440 адр. · 30% pass",
|
|
|
|
|
|
} {
|
|
|
|
|
|
if !strings.Contains(page, want) {
|
|
|
|
|
|
t.Fatalf("expected %q in the page, got:\n%s", want, page)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if strings.Count(page, `<option value="1" selected>`) != 1 || strings.Count(page, `<option value="2" selected>`) != 1 {
|
|
|
|
|
|
t.Fatalf("one run is chosen in each list:\n%s", page)
|
|
|
|
|
|
}
|
|
|
|
|
|
if strings.Contains(page, `value="3"`) {
|
|
|
|
|
|
t.Fatalf("an open run must not be offered:\n%s", page)
|
|
|
|
|
|
}
|
|
|
|
|
|
if got := compareRequests(fake); len(got) != 1 || !strings.Contains(got[0], "base=1") || !strings.Contains(got[0], "target=2") {
|
|
|
|
|
|
t.Fatalf("expected one request for base=1&target=2, got %v", got)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// The pair in the address: both ends, only the new one (the base is the run
|
|
|
|
|
|
// before it), only the old one (the target is the newest other run).
|
|
|
|
|
|
func TestAnalyticsComparePageExplicitPair(t *testing.T) {
|
|
|
|
|
|
fake, ts := compareFake(t)
|
|
|
|
|
|
|
|
|
|
|
|
page := get(t, ts, "/analytics/compare?base=2&target=1")
|
|
|
|
|
|
if !strings.Contains(page, `"new":222`) || !strings.Contains(page, `<option value="2" selected>`) {
|
|
|
|
|
|
t.Fatalf("swapped pair:\n%s", page)
|
|
|
|
|
|
}
|
|
|
|
|
|
if page = get(t, ts, "/analytics/compare?target=2"); !strings.Contains(page, `"new":111`) {
|
|
|
|
|
|
t.Fatalf("only target=2 must compare with run 1:\n%s", page)
|
|
|
|
|
|
}
|
|
|
|
|
|
if page = get(t, ts, "/analytics/compare?base=2"); !strings.Contains(page, `"new":222`) {
|
|
|
|
|
|
t.Fatalf("only base=2 must compare with the newest other run, 1:\n%s", page)
|
|
|
|
|
|
}
|
|
|
|
|
|
if page = get(t, ts, "/analytics/compare?target=1"); strings.Contains(page, `id="analytics-data"`) ||
|
|
|
|
|
|
!strings.Contains(page, "нет второго запуска") {
|
|
|
|
|
|
t.Fatalf("run 1 is the oldest, nothing to compare it with:\n%s", page)
|
|
|
|
|
|
}
|
|
|
|
|
|
if got := compareRequests(fake); len(got) != 3 {
|
|
|
|
|
|
t.Fatalf("the refused pair must not reach control-api, got %v", got)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func TestAnalyticsComparePageWarnings(t *testing.T) {
|
|
|
|
|
|
fake, ts := compareFake(t)
|
|
|
|
|
|
for _, c := range []struct{ query, want string }{
|
|
|
|
|
|
{"base=1&target=1", "Выберите два разных запуска"},
|
|
|
|
|
|
{"base=99&target=1", "Запуск 99 не найден или ещё не завершён"},
|
|
|
|
|
|
// the open run
|
|
|
|
|
|
{"base=1&target=3", "Запуск 3 не найден или ещё не завершён"},
|
|
|
|
|
|
{"base=abc&target=1", "Неверный номер запуска"},
|
|
|
|
|
|
{"base=0&target=1", "Неверный номер запуска"},
|
|
|
|
|
|
} {
|
|
|
|
|
|
page := get(t, ts, "/analytics/compare?"+c.query)
|
|
|
|
|
|
if !strings.Contains(page, c.want) || strings.Contains(page, `id="analytics-data"`) || !strings.Contains(page, `id="an-base"`) {
|
|
|
|
|
|
t.Fatalf("%s: expected the warning %q and the run lists without data, got:\n%s", c.query, c.want, page)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if got := compareRequests(fake); len(got) != 0 {
|
|
|
|
|
|
t.Fatalf("a bad pair must not reach control-api, got %v", got)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Fewer than two finished runs: nothing to compare.
|
|
|
|
|
|
fake, caURL := newFakeControlAPI(t)
|
|
|
|
|
|
fake.runs = []analyticsRun{fakeRun(3, "open", 120, 40), fakeRun(1, "finalized", 6440, 1962)}
|
|
|
|
|
|
page := get(t, newTestServer(t, caURL), "/analytics/compare")
|
|
|
|
|
|
if !strings.Contains(page, "минимум два завершённых запуска") || strings.Contains(page, `id="an-base"`) {
|
|
|
|
|
|
t.Fatalf("expected the empty state, got:\n%s", page)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func TestAnalyticsCompareListProxyAndCSV(t *testing.T) {
|
|
|
|
|
|
_, ts := compareFake(t)
|
|
|
|
|
|
fetch := func(path string) (int, http.Header, string) {
|
|
|
|
|
|
t.Helper()
|
|
|
|
|
|
resp, err := http.Get(ts.URL + path)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
|
}
|
|
|
|
|
|
body, _ := io.ReadAll(resp.Body)
|
|
|
|
|
|
resp.Body.Close()
|
|
|
|
|
|
return resp.StatusCode, resp.Header, string(body)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
code, _, body := fetch("/analytics/compare/lists/changed?base=1&target=2")
|
|
|
|
|
|
if code != http.StatusOK || !strings.Contains(body, `"1.2.3.4"`) {
|
|
|
|
|
|
t.Fatalf("list: %d %s", code, body)
|
|
|
|
|
|
}
|
|
|
|
|
|
// The indicator and the verdicts travel to control-api.
|
|
|
|
|
|
code, _, body = fetch("/analytics/compare/lists/new?base=1&target=2&indicator=verdict_pass")
|
|
|
|
|
|
if code != http.StatusOK || !strings.Contains(body, `"5.6.7.8"`) {
|
|
|
|
|
|
t.Fatalf("list with an indicator: %d %s", code, body)
|
|
|
|
|
|
}
|
|
|
|
|
|
code, header, body := fetch("/analytics/compare/csv/changed?base=1&target=2")
|
|
|
|
|
|
if code != http.StatusOK || !strings.HasPrefix(header.Get("Content-Type"), "text/csv") ||
|
|
|
|
|
|
!strings.Contains(header.Get("Content-Disposition"), `attachment; filename="compare_changed_run1-2.csv"`) {
|
|
|
|
|
|
t.Fatalf("csv: %d %v %s", code, header, body)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for _, c := range []struct {
|
|
|
|
|
|
path string
|
|
|
|
|
|
want int
|
|
|
|
|
|
}{
|
|
|
|
|
|
// no runs
|
|
|
|
|
|
{"/analytics/compare/lists/changed", http.StatusBadRequest},
|
|
|
|
|
|
{"/analytics/compare/lists/changed?base=1", http.StatusBadRequest},
|
|
|
|
|
|
{"/analytics/compare/csv/changed?base=abc&target=2", http.StatusBadRequest},
|
|
|
|
|
|
// control-api says unknown list
|
|
|
|
|
|
{"/analytics/compare/lists/nonsense?base=1&target=2", http.StatusNotFound},
|
|
|
|
|
|
{"/analytics/compare/csv/nonsense?base=1&target=2", http.StatusNotFound},
|
|
|
|
|
|
} {
|
|
|
|
|
|
if code, _, _ := fetch(c.path); code != c.want {
|
|
|
|
|
|
t.Errorf("%s: %d, want %d", c.path, code, c.want)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// The single-run page after the dialog moved to analytics-dialog.js: it still
|
|
|
|
|
|
// carries the dialog, loads the shared script before its own, offers the
|
|
|
|
|
|
// comparison, and both scripts are served.
|
|
|
|
|
|
func TestAnalyticsPageUsesSharedDialog(t *testing.T) {
|
|
|
|
|
|
_, ts := analyticsFake(t)
|
|
|
|
|
|
|
|
|
|
|
|
page := get(t, ts, "/analytics")
|
|
|
|
|
|
iDialog, iPage := strings.Index(page, "/static/analytics-dialog.js"), strings.Index(page, "/static/analytics.js")
|
|
|
|
|
|
if iDialog < 0 || iPage < 0 || iDialog > iPage {
|
|
|
|
|
|
t.Fatalf("analytics-dialog.js must come before analytics.js (%d, %d):\n%s", iDialog, iPage, page)
|
|
|
|
|
|
}
|
|
|
|
|
|
for _, want := range []string{`id="an-dlg"`, `id="an-dlg-tbl"`, `id="an-dlg-csv"`, `id="an-tip"`, `href="/analytics/compare?target=2"`} {
|
|
|
|
|
|
if !strings.Contains(page, want) {
|
|
|
|
|
|
t.Fatalf("expected %q in the page, got:\n%s", want, page)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
for path, want := range map[string]string{
|
|
|
|
|
|
"/static/analytics-dialog.js": "window.AnalyticsDialog =",
|
|
|
|
|
|
"/static/analytics.js": "window.AnalyticsDialog",
|
|
|
|
|
|
"/static/analytics-compare.js": "window.AnalyticsDialog",
|
|
|
|
|
|
} {
|
|
|
|
|
|
if body := get(t, ts, path); !strings.Contains(body, want) {
|
|
|
|
|
|
t.Errorf("%s does not contain %q", path, want)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-03 18:36:03 +03:00
|
|
|
|
// 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)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-10-04 10:26:37 +03:00
|
|
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|