New page /analytics/compare and API GET /admin/analytics/compare (+ /lists/{group}):
the administrator picks an old (A) and a new (B) run; the report shows the new
addresses (only in B), the ones that left (only in A) and the common ones whose
membership in the seven indicators (pass, partial, fail, egress https any/all,
ingress ssh any/all) differs, with a "what changed" summary per address; the
dynamics of each indicator (delta = new - left + entered - exited) and a verdict
transition matrix. Every number opens a list with CSV. Cancelled addresses are not
part of a run. The list dialog moved to a shared analytics-dialog.js and template;
/analytics got a "compare with another run" button.
Docs, plan and summary in docs/changes/.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
410 lines
17 KiB
Go
410 lines
17 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"cloudipvalidator/internal/db"
|
|
)
|
|
|
|
// finishedRun builds one finished run of two addresses through the real
|
|
// queue paths and returns its id: 9.9.9.1 passes, 9.9.9.2 has a failed ssh.
|
|
func finishedRun(t *testing.T, d *db.DB) int64 {
|
|
t.Helper()
|
|
ctx := context.Background()
|
|
if _, err := d.SubmitIPsAs(ctx, []string{"9.9.9.1", "9.9.9.2"}, db.RunManual); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, addr := range []string{"9.9.9.1", "9.9.9.2"} {
|
|
ip, err := d.GetIPByAddress(ctx, addr)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := d.SetChecking(ctx, ip.ID, time.Minute); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ip, _ = d.GetIP(ctx, ip.ID)
|
|
put := func(src, typ, target, detail string, ok bool) {
|
|
if _, err := d.UpsertCheckIfOpen(ctx, db.Check{IPID: ip.ID, IPAddress: addr, AttemptNumber: ip.AttemptNumber,
|
|
ValidatorID: "vkiplab-v1", Source: src, CheckType: typ, Target: target, Success: ok, Detail: detail, CheckedAt: db.Now()}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
put(db.SourceEgress, "https", "https://a.test", "", true)
|
|
put(db.InboundSource(1), "icmp", addr, "", true)
|
|
sshOK := addr == "9.9.9.1"
|
|
put(db.InboundSource(1), "ssh", addr, "dial tcp: i/o timeout", sshOK)
|
|
verdict := db.ResultPass
|
|
if !sshOK {
|
|
verdict = db.ResultPartial
|
|
}
|
|
if err := d.FinishIPExpected(ctx, ip.ID, verdict, 3); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
rs, err := d.ListRuns(ctx)
|
|
if err != nil || len(rs) != 1 || rs[0].State != db.RunFinalized {
|
|
t.Fatalf("expected one finalized run: %+v %v", rs, err)
|
|
}
|
|
return rs[0].ID
|
|
}
|
|
|
|
func TestAnalyticsEndpoints(t *testing.T) {
|
|
fc, d, _, _ := newConfigTestHarness(t)
|
|
fc.do(http.MethodPut, "/api/v1/admin/config/sites/1", putSiteRequest{SiteID: "rxmsk"})
|
|
id := finishedRun(t, d)
|
|
base := "/api/v1/admin/analytics/runs"
|
|
|
|
resp, body := fc.do(http.MethodGet, base, nil)
|
|
var list []analyticsRunDTO
|
|
if resp.StatusCode != http.StatusOK || json.Unmarshal(body, &list) != nil || len(list) != 1 ||
|
|
list[0].State != "finalized" || list[0].Addresses != 2 || list[0].Pass != 1 || list[0].Partial != 1 {
|
|
t.Fatalf("runs: %d %s", resp.StatusCode, body)
|
|
}
|
|
|
|
resp, body = fc.do(http.MethodGet, base+"/"+itoa64(id), nil)
|
|
var rep struct {
|
|
Summary struct {
|
|
Addresses int `json:"addresses"`
|
|
IngressSSHAny int `json:"ingress_ssh_any_failed"`
|
|
IngressSSHAll int `json:"ingress_ssh_all_failed"`
|
|
} `json:"summary"`
|
|
Errors []struct {
|
|
Name string `json:"name"`
|
|
Count int `json:"count"`
|
|
} `json:"errors"`
|
|
Sites struct {
|
|
Rows []struct {
|
|
Site string `json:"site"`
|
|
} `json:"rows"`
|
|
} `json:"sites"`
|
|
}
|
|
if resp.StatusCode != http.StatusOK || json.Unmarshal(body, &rep) != nil || rep.Summary.Addresses != 2 ||
|
|
rep.Summary.IngressSSHAny != 1 || rep.Summary.IngressSSHAll != 1 ||
|
|
len(rep.Errors) != 1 || rep.Errors[0].Name != "SSH: таймаут" || len(rep.Sites.Rows) != 1 || rep.Sites.Rows[0].Site != "rxmsk" {
|
|
t.Fatalf("report: %d %s", resp.StatusCode, body)
|
|
}
|
|
|
|
// The list as JSON and as a CSV file with BOM and a download name.
|
|
resp, body = fc.do(http.MethodGet, base+"/"+itoa64(id)+"/lists/ingress_ssh_any", nil)
|
|
var l struct {
|
|
Columns []string `json:"columns"`
|
|
Rows [][]string `json:"rows"`
|
|
}
|
|
if resp.StatusCode != http.StatusOK || json.Unmarshal(body, &l) != nil || len(l.Rows) != 1 || l.Rows[0][0] != "9.9.9.2" {
|
|
t.Fatalf("list: %d %s", resp.StatusCode, body)
|
|
}
|
|
resp, body = fc.do(http.MethodGet, base+"/"+itoa64(id)+"/lists/ingress_ssh_any?format=csv", nil)
|
|
if resp.StatusCode != http.StatusOK || !strings.HasPrefix(string(body), "\xef\xbb\xbf") ||
|
|
!strings.Contains(string(body), "9.9.9.2") || !strings.HasPrefix(resp.Header.Get("Content-Type"), "text/csv") ||
|
|
!strings.Contains(resp.Header.Get("Content-Disposition"), `attachment; filename="ingress_ssh_any_run`+itoa64(id)+`.csv"`) {
|
|
t.Fatalf("csv: %d %v %q", resp.StatusCode, resp.Header, body)
|
|
}
|
|
// A verdict list: 9.9.9.2 is the only partial address.
|
|
resp, body = fc.do(http.MethodGet, base+"/"+itoa64(id)+"/lists/verdict_partial", nil)
|
|
if resp.StatusCode != http.StatusOK || json.Unmarshal(body, &l) != nil || len(l.Rows) != 1 || l.Rows[0][0] != "9.9.9.2" || l.Columns[len(l.Columns)-1] != "Причина" {
|
|
t.Fatalf("verdict list: %d %s", resp.StatusCode, body)
|
|
}
|
|
resp, body = fc.do(http.MethodGet, base+"/"+itoa64(id)+"/lists/verdict_partial?format=csv", nil)
|
|
if resp.StatusCode != http.StatusOK || !strings.Contains(string(body), "9.9.9.2") ||
|
|
!strings.Contains(resp.Header.Get("Content-Disposition"), `attachment; filename="verdict_partial_run`+itoa64(id)+`.csv"`) {
|
|
t.Fatalf("verdict csv: %d %v %q", resp.StatusCode, resp.Header, body)
|
|
}
|
|
q := url.Values{"class": {"SSH: таймаут"}, "format": {"csv"}}
|
|
resp, body = fc.do(http.MethodGet, base+"/"+itoa64(id)+"/lists/error?"+q.Encode(), nil)
|
|
if resp.StatusCode != http.StatusOK || !strings.Contains(resp.Header.Get("Content-Disposition"), "error-ssh_run") {
|
|
t.Fatalf("error csv: %d %v %q", resp.StatusCode, resp.Header, body)
|
|
}
|
|
|
|
for path, want := range map[string]int{
|
|
base + "/" + itoa64(id) + "/lists/error": http.StatusNotFound, // class missing
|
|
base + "/" + itoa64(id) + "/lists/nonsense": http.StatusNotFound,
|
|
base + "/" + itoa64(id) + "/lists/verdict_cancelled": http.StatusNotFound,
|
|
base + "/9999": http.StatusNotFound,
|
|
base + "/abc": http.StatusBadRequest,
|
|
base + "/9999/lists/ingress_ssh_any": http.StatusNotFound,
|
|
} {
|
|
if resp, body := fc.do(http.MethodGet, path, nil); resp.StatusCode != want {
|
|
t.Errorf("%s: %d %s, want %d", path, resp.StatusCode, body, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// secondRun builds the run after finishedRun's and returns its id: 9.9.9.1 is
|
|
// not in it, 9.9.9.2 is checked again and passes now, 9.9.9.3 is new and has a
|
|
// failed ssh. Against the first run: one new, one left, one changed address.
|
|
func secondRun(t *testing.T, d *db.DB) int64 {
|
|
t.Helper()
|
|
ctx := context.Background()
|
|
if _, err := d.SubmitIPsAs(ctx, []string{"9.9.9.2", "9.9.9.3"}, db.RunManual); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, addr := range []string{"9.9.9.2", "9.9.9.3"} {
|
|
ip, err := d.GetIPByAddress(ctx, addr)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := d.SetChecking(ctx, ip.ID, time.Minute); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ip, _ = d.GetIP(ctx, ip.ID)
|
|
put := func(src, typ, target string, ok bool) {
|
|
if _, err := d.UpsertCheckIfOpen(ctx, db.Check{IPID: ip.ID, IPAddress: addr, AttemptNumber: ip.AttemptNumber,
|
|
ValidatorID: "vkiplab-v1", Source: src, CheckType: typ, Target: target, Success: ok, Detail: "dial tcp: i/o timeout", CheckedAt: db.Now()}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
put(db.SourceEgress, "https", "https://a.test", true)
|
|
put(db.InboundSource(1), "icmp", addr, true)
|
|
sshOK := addr == "9.9.9.2"
|
|
put(db.InboundSource(1), "ssh", addr, sshOK)
|
|
verdict := db.ResultPass
|
|
if !sshOK {
|
|
verdict = db.ResultPartial
|
|
}
|
|
if err := d.FinishIPExpected(ctx, ip.ID, verdict, 3); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
rs, err := d.ListRuns(ctx)
|
|
if err != nil || len(rs) != 2 || rs[0].State != db.RunFinalized {
|
|
t.Fatalf("expected two finalized runs: %+v %v", rs, err)
|
|
}
|
|
return rs[0].ID
|
|
}
|
|
|
|
func TestAnalyticsCompareEndpoints(t *testing.T) {
|
|
fc, d, _, _ := newConfigTestHarness(t)
|
|
fc.do(http.MethodPut, "/api/v1/admin/config/sites/1", putSiteRequest{SiteID: "rxmsk"})
|
|
a := finishedRun(t, d)
|
|
b := secondRun(t, d)
|
|
cmp := "/api/v1/admin/analytics/compare"
|
|
q := "?base=" + itoa64(a) + "&target=" + itoa64(b)
|
|
|
|
resp, body := fc.do(http.MethodGet, cmp+q, nil)
|
|
var rep struct {
|
|
Runs struct {
|
|
Base struct{ ID, Addresses int64 } `json:"base"`
|
|
Target struct{ ID, Addresses int64 } `json:"target"`
|
|
} `json:"runs"`
|
|
Groups struct {
|
|
New, Left, Common, Changed, Same int
|
|
} `json:"groups"`
|
|
Indicators []struct {
|
|
Key string
|
|
|
|
Base, Target, Delta, New, Left, Entered, Exited int
|
|
} `json:"indicators"`
|
|
Transitions struct {
|
|
Matrix [][]int `json:"matrix"`
|
|
} `json:"transitions"`
|
|
}
|
|
if resp.StatusCode != http.StatusOK || json.Unmarshal(body, &rep) != nil || rep.Runs.Base.ID != a || rep.Runs.Target.ID != b ||
|
|
rep.Runs.Base.Addresses != 2 || rep.Runs.Target.Addresses != 2 ||
|
|
rep.Groups.New != 1 || rep.Groups.Left != 1 || rep.Groups.Common != 1 || rep.Groups.Changed != 1 || rep.Groups.Same != 0 ||
|
|
len(rep.Indicators) != 7 || len(rep.Transitions.Matrix) != 3 || rep.Transitions.Matrix[1][0] != 1 {
|
|
t.Fatalf("compare: %d %s", resp.StatusCode, body)
|
|
}
|
|
for _, ind := range rep.Indicators {
|
|
if ind.Delta != ind.New-ind.Left+ind.Entered-ind.Exited || ind.Delta != ind.Target-ind.Base {
|
|
t.Errorf("indicator %s: %+v", ind.Key, ind)
|
|
}
|
|
if ind.Key == "ingress_ssh_any" && (ind.Base != 1 || ind.Target != 1 || ind.New != 1 || ind.Exited != 1) {
|
|
t.Errorf("ssh any: %+v", ind)
|
|
}
|
|
}
|
|
|
|
// Lists: JSON, then CSV with BOM and the name of the file.
|
|
var l struct {
|
|
Group string `json:"group"`
|
|
Columns []string `json:"columns"`
|
|
Rows [][]string `json:"rows"`
|
|
}
|
|
resp, body = fc.do(http.MethodGet, cmp+"/lists/changed"+q, nil)
|
|
if resp.StatusCode != http.StatusOK || json.Unmarshal(body, &l) != nil || l.Group != "changed" || len(l.Rows) != 1 ||
|
|
l.Rows[0][0] != "9.9.9.2" || !strings.HasPrefix(l.Rows[0][2], "partial → pass") {
|
|
t.Fatalf("changed list: %d %s", resp.StatusCode, body)
|
|
}
|
|
resp, body = fc.do(http.MethodGet, cmp+"/lists/new"+q+"&indicator=verdict_partial", nil)
|
|
if resp.StatusCode != http.StatusOK || json.Unmarshal(body, &l) != nil || len(l.Rows) != 1 || l.Rows[0][0] != "9.9.9.3" {
|
|
t.Fatalf("new list: %d %s", resp.StatusCode, body)
|
|
}
|
|
resp, body = fc.do(http.MethodGet, cmp+"/lists/left"+q+"&indicator=verdict_pass", nil)
|
|
if resp.StatusCode != http.StatusOK || json.Unmarshal(body, &l) != nil || len(l.Rows) != 1 || l.Rows[0][0] != "9.9.9.1" {
|
|
t.Fatalf("left list: %d %s", resp.StatusCode, body)
|
|
}
|
|
resp, body = fc.do(http.MethodGet, cmp+"/lists/common"+q+"&from=partial&to=pass", nil)
|
|
if resp.StatusCode != http.StatusOK || json.Unmarshal(body, &l) != nil || len(l.Rows) != 1 || l.Rows[0][0] != "9.9.9.2" {
|
|
t.Fatalf("matrix cell list: %d %s", resp.StatusCode, body)
|
|
}
|
|
resp, body = fc.do(http.MethodGet, cmp+"/lists/exited"+q+"&indicator=ingress_ssh_any", nil)
|
|
if resp.StatusCode != http.StatusOK || json.Unmarshal(body, &l) != nil || len(l.Rows) != 1 || l.Rows[0][0] != "9.9.9.2" {
|
|
t.Fatalf("exited list: %d %s", resp.StatusCode, body)
|
|
}
|
|
resp, body = fc.do(http.MethodGet, cmp+"/lists/changed"+q+"&format=csv", nil)
|
|
if resp.StatusCode != http.StatusOK || !strings.HasPrefix(string(body), "\xef\xbb\xbf") || !strings.Contains(string(body), "9.9.9.2") ||
|
|
!strings.HasPrefix(resp.Header.Get("Content-Type"), "text/csv") ||
|
|
!strings.Contains(resp.Header.Get("Content-Disposition"), `attachment; filename="compare_changed_run`+itoa64(a)+"-"+itoa64(b)+`.csv"`) {
|
|
t.Fatalf("csv: %d %v %q", resp.StatusCode, resp.Header, body)
|
|
}
|
|
resp, body = fc.do(http.MethodGet, cmp+"/lists/new"+q+"&indicator=verdict_partial&format=csv", nil)
|
|
if !strings.Contains(resp.Header.Get("Content-Disposition"), `filename="compare_new_verdict_partial_run`+itoa64(a)+"-"+itoa64(b)+`.csv"`) {
|
|
t.Fatalf("csv with an indicator: %d %v %q", resp.StatusCode, resp.Header, body)
|
|
}
|
|
resp, body = fc.do(http.MethodGet, cmp+"/lists/common"+q+"&from=partial&to=pass&format=csv", nil)
|
|
if !strings.Contains(resp.Header.Get("Content-Disposition"), `filename="compare_common_partial-pass_run`+itoa64(a)+"-"+itoa64(b)+`.csv"`) {
|
|
t.Fatalf("csv with verdicts: %d %v %q", resp.StatusCode, resp.Header, body)
|
|
}
|
|
|
|
// The order is the caller's: swapped, the new address is the one that left.
|
|
swapped := "?base=" + itoa64(b) + "&target=" + itoa64(a)
|
|
resp, body = fc.do(http.MethodGet, cmp+"/lists/new"+swapped, nil)
|
|
if resp.StatusCode != http.StatusOK || json.Unmarshal(body, &l) != nil || len(l.Rows) != 1 || l.Rows[0][0] != "9.9.9.1" {
|
|
t.Fatalf("swapped: %d %s", resp.StatusCode, body)
|
|
}
|
|
|
|
// An open run cannot be compared.
|
|
if _, err := d.SubmitIPs(context.Background(), []string{"9.9.9.9"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rs, _ := d.ListRuns(context.Background())
|
|
open := itoa64(rs[0].ID)
|
|
if rs[0].State == db.RunFinalized {
|
|
t.Fatalf("expected an open run: %+v", rs[0])
|
|
}
|
|
|
|
ida, idb := itoa64(a), itoa64(b)
|
|
for _, c := range []struct {
|
|
path string
|
|
want int
|
|
}{
|
|
{cmp, http.StatusBadRequest}, // no ids
|
|
{cmp + "?base=" + ida, http.StatusBadRequest},
|
|
{cmp + "?target=" + ida, http.StatusBadRequest},
|
|
{cmp + "?base=abc&target=" + ida, http.StatusBadRequest},
|
|
{cmp + "?base=0&target=" + ida, http.StatusBadRequest},
|
|
{cmp + "?base=" + ida + "&target=" + ida, http.StatusBadRequest}, // the same run twice
|
|
{cmp + "/lists/changed?base=" + ida + "&target=" + ida, http.StatusBadRequest},
|
|
{cmp + "/lists/changed", http.StatusBadRequest},
|
|
{cmp + "?base=9999&target=" + ida, http.StatusNotFound},
|
|
{cmp + "?base=" + ida + "&target=9999", http.StatusNotFound},
|
|
{cmp + "?base=" + ida + "&target=" + open, http.StatusConflict},
|
|
{cmp + "/lists/changed?base=" + open + "&target=" + idb, http.StatusConflict},
|
|
{cmp + "/lists/nonsense" + q, http.StatusNotFound},
|
|
{cmp + "/lists/new" + q + "&indicator=nonsense", http.StatusNotFound},
|
|
{cmp + "/lists/entered" + q, http.StatusNotFound}, // an indicator is required
|
|
{cmp + "/lists/common" + q + "&from=pass", http.StatusNotFound},
|
|
{cmp + "/lists/common" + q + "&from=pass&to=cancelled", http.StatusNotFound},
|
|
} {
|
|
if resp, body := fc.do(http.MethodGet, c.path, nil); resp.StatusCode != c.want {
|
|
t.Errorf("%s: %d %s, want %d", c.path, resp.StatusCode, body, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// An open run has no analytics yet.
|
|
func TestAnalyticsOfOpenRunIsRefused(t *testing.T) {
|
|
fc, d, _, _ := newConfigTestHarness(t)
|
|
if _, err := d.SubmitIPs(context.Background(), []string{"9.9.9.1"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rs, _ := d.ListRuns(context.Background())
|
|
resp, body := fc.do(http.MethodGet, "/api/v1/admin/analytics/runs/"+itoa64(rs[0].ID), nil)
|
|
if resp.StatusCode != http.StatusConflict {
|
|
t.Fatalf("open run: %d %s", resp.StatusCode, body)
|
|
}
|
|
}
|
|
|
|
// The result is cached while the run is unchanged and recomputed when a
|
|
// check of the run is written or the subnet list changes.
|
|
func TestAnalyticsCacheFollowsData(t *testing.T) {
|
|
fc, d, _, _ := newConfigTestHarness(t)
|
|
id := finishedRun(t, d)
|
|
path := "/api/v1/admin/analytics/runs/" + itoa64(id)
|
|
subnetsOf := func() []string {
|
|
_, body := fc.do(http.MethodGet, path, nil)
|
|
var rep struct {
|
|
Subnets []struct {
|
|
CIDR string `json:"cidr"`
|
|
} `json:"subnets"`
|
|
}
|
|
if err := json.Unmarshal(body, &rep); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var out []string
|
|
for _, s := range rep.Subnets {
|
|
out = append(out, s.CIDR)
|
|
}
|
|
return out
|
|
}
|
|
if got := subnetsOf(); len(got) != 1 || got[0] != "9.9.9.0/24" {
|
|
t.Fatalf("without a list addresses group by /24: %v", got)
|
|
}
|
|
resp, body := fc.do(http.MethodPut, "/api/v1/admin/config/subnets", subnetsDTO{Subnets: []subnetDTO{{CIDR: "9.9.0.0/16", Label: "девятые"}}})
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("put subnets: %d %s", resp.StatusCode, body)
|
|
}
|
|
if got := subnetsOf(); len(got) != 1 || got[0] != "9.9.0.0/16" {
|
|
t.Fatalf("a changed subnet list must change the report: %v", got)
|
|
}
|
|
}
|
|
|
|
func TestSubnetsConfigEndpoints(t *testing.T) {
|
|
fc, _, _, _ := newConfigTestHarness(t)
|
|
resp, body := fc.do(http.MethodPut, "/api/v1/admin/config/subnets", subnetsDTO{Subnets: []subnetDTO{{CIDR: " 10.1.2.3/24 ", Label: "a"}, {CIDR: "10.0.0.0/8"}}})
|
|
var got subnetsDTO
|
|
if resp.StatusCode != http.StatusOK || json.Unmarshal(body, &got) != nil || len(got.Subnets) != 2 || got.Subnets[0].CIDR != "10.0.0.0/8" || got.Subnets[1].CIDR != "10.1.2.0/24" {
|
|
t.Fatalf("put: %d %s", resp.StatusCode, body)
|
|
}
|
|
if resp, _ = fc.do(http.MethodPut, "/api/v1/admin/config/subnets", subnetsDTO{Subnets: []subnetDTO{{CIDR: "garbage"}}}); resp.StatusCode != http.StatusBadRequest {
|
|
t.Fatalf("garbage must be a 400, got %d", resp.StatusCode)
|
|
}
|
|
resp, body = fc.do(http.MethodGet, "/api/v1/admin/config/subnets", nil)
|
|
if resp.StatusCode != http.StatusOK || json.Unmarshal(body, &got) != nil || len(got.Subnets) != 2 {
|
|
t.Fatalf("a rejected list must leave the old one: %d %s", resp.StatusCode, body)
|
|
}
|
|
}
|
|
|
|
func TestRegistryRunAndSubnetFilters(t *testing.T) {
|
|
fc, d, _, _ := newConfigTestHarness(t)
|
|
id := finishedRun(t, d)
|
|
// an address outside the run
|
|
if _, err := d.SubmitIPs(context.Background(), []string{"8.8.8.8"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
count := func(q string) int {
|
|
t.Helper()
|
|
resp, body := fc.do(http.MethodGet, "/api/v1/admin/registry?limit=50&"+q, nil)
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("%s: %d %s", q, resp.StatusCode, body)
|
|
}
|
|
var p registryPageResponse
|
|
if err := json.Unmarshal(body, &p); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return p.Total
|
|
}
|
|
if n := count("run=" + itoa64(id)); n != 2 {
|
|
t.Errorf("run filter: %d", n)
|
|
}
|
|
if n := count("subnet=" + url.QueryEscape("9.9.9.0/24")); n != 2 {
|
|
t.Errorf("subnet filter: %d", n)
|
|
}
|
|
if n := count("run=" + itoa64(id) + "&subnet=" + url.QueryEscape("8.8.8.0/24")); n != 0 {
|
|
t.Errorf("run and subnet together: %d", n)
|
|
}
|
|
for _, bad := range []string{"run=abc", "run=0", "subnet=nonsense"} {
|
|
if resp, _ := fc.do(http.MethodGet, "/api/v1/admin/registry?limit=5&"+bad, nil); resp.StatusCode != http.StatusBadRequest {
|
|
t.Errorf("%s: %d, want 400", bad, resp.StatusCode)
|
|
}
|
|
}
|
|
}
|
|
|
|
func itoa64(n int64) string { return strconv.FormatInt(n, 10) }
|