226 lines
8.5 KiB
Go
226 lines
8.5 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)
|
||
|
|
}
|
||
|
|
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 + "/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)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 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) }
|