133 lines
5.5 KiB
Go
133 lines
5.5 KiB
Go
package httpapi
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"encoding/json"
|
||
|
|
"net/http"
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"cloudipvalidator/internal/db"
|
||
|
|
)
|
||
|
|
|
||
|
|
// setupCheckingIP stands up two sites and one validator and leaves 9.9.9.9 in
|
||
|
|
// the checking state.
|
||
|
|
func setupCheckingIP(t *testing.T) (*fakeClient, *db.DB, *db.IPQueueItem) {
|
||
|
|
t.Helper()
|
||
|
|
fc, d, orch, mock := newConfigTestHarness(t)
|
||
|
|
ctx := context.Background()
|
||
|
|
mock.Seed("fip-1", "9.9.9.9", "svc-project")
|
||
|
|
fc.do(http.MethodPut, "/api/v1/admin/config/sites/1", putSiteRequest{SiteID: "site-1"})
|
||
|
|
fc.do(http.MethodPut, "/api/v1/admin/config/sites/2", putSiteRequest{SiteID: "site-2"})
|
||
|
|
fc.do(http.MethodPost, "/api/v1/admin/config/validators", createValidatorRequest{ValidatorID: "validator-1", OSPortID: "port-1"})
|
||
|
|
fc.do(http.MethodPost, "/api/v1/agents/register", registerAgentRequest{ValidatorID: "validator-1"})
|
||
|
|
fc.do(http.MethodPost, "/api/v1/admin/ips", submitIPsRequest{Addresses: []string{"9.9.9.9"}})
|
||
|
|
orch.Tick(ctx)
|
||
|
|
_, body := fc.do(http.MethodGet, "/api/v1/agents/validator-1/assignment", nil)
|
||
|
|
var a assignmentResponse
|
||
|
|
if err := json.Unmarshal(body, &a); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
fc.do(http.MethodPost, "/api/v1/agents/validator-1/self-check", selfCheckRequest{IPID: a.IPID, DetectedEgress: "9.9.9.9", Success: true, Detail: "matched"})
|
||
|
|
fc.do(http.MethodPost, "/api/v1/probers/register", registerProberRequest{SiteID: "site-1"})
|
||
|
|
fc.do(http.MethodPost, "/api/v1/probers/register", registerProberRequest{SiteID: "site-2"})
|
||
|
|
ip, err := d.GetIP(ctx, a.IPID)
|
||
|
|
if err != nil || ip.State != db.IPChecking {
|
||
|
|
t.Fatalf("expected checking: %v %+v", err, ip)
|
||
|
|
}
|
||
|
|
return fc, d, ip
|
||
|
|
}
|
||
|
|
|
||
|
|
func proberAssignmentsFor(t *testing.T, fc *fakeClient, site string) []proberAssignment {
|
||
|
|
t.Helper()
|
||
|
|
resp, body := fc.do(http.MethodGet, "/api/v1/probers/"+site+"/assignments", nil)
|
||
|
|
if resp.StatusCode != http.StatusOK {
|
||
|
|
t.Fatalf("%s assignments: %d %s", site, resp.StatusCode, body)
|
||
|
|
}
|
||
|
|
var out []proberAssignment
|
||
|
|
if err := json.Unmarshal(body, &out); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
return out
|
||
|
|
}
|
||
|
|
|
||
|
|
func proberResult(ip *db.IPQueueItem, ct string, ok, complete bool) proberResultDTO {
|
||
|
|
return proberResultDTO{IPID: ip.ID, IPAddress: ip.IPAddress, CheckType: ct, Success: ok,
|
||
|
|
CheckedAt: time.Now().UTC().Format(time.RFC3339Nano), Complete: complete}
|
||
|
|
}
|
||
|
|
|
||
|
|
// A prober site gets an address until it has reported it complete, then not
|
||
|
|
// again; the other site still gets it.
|
||
|
|
func TestProberAssignmentsOncePerSite(t *testing.T) {
|
||
|
|
fc, _, ip := setupCheckingIP(t)
|
||
|
|
|
||
|
|
if len(proberAssignmentsFor(t, fc, "site-1")) != 1 || len(proberAssignmentsFor(t, fc, "site-2")) != 1 {
|
||
|
|
t.Fatal("both sites must get the address before reporting")
|
||
|
|
}
|
||
|
|
resp, body := fc.do(http.MethodPost, "/api/v1/probers/site-1/results", proberResultsRequest{
|
||
|
|
Results: []proberResultDTO{proberResult(ip, "tcp-22", true, false), proberResult(ip, "icmp", true, true)},
|
||
|
|
})
|
||
|
|
if resp.StatusCode != http.StatusOK {
|
||
|
|
t.Fatalf("results: %d %s", resp.StatusCode, body)
|
||
|
|
}
|
||
|
|
if got := proberAssignmentsFor(t, fc, "site-1"); len(got) != 0 {
|
||
|
|
t.Fatalf("site-1 must not get the address again after completing it: %+v", got)
|
||
|
|
}
|
||
|
|
if got := proberAssignmentsFor(t, fc, "site-2"); len(got) != 1 {
|
||
|
|
t.Fatalf("site-2 still has to probe the address: %+v", got)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Results that arrive after the verdict are refused with ignored>0, leave the
|
||
|
|
// stored checks untouched and leave a result_dropped event behind; the same
|
||
|
|
// for the validator agent's results.
|
||
|
|
func TestResultsAfterVerdictAreIgnored(t *testing.T) {
|
||
|
|
fc, d, ip := setupCheckingIP(t)
|
||
|
|
ctx := context.Background()
|
||
|
|
|
||
|
|
fc.do(http.MethodPost, "/api/v1/probers/site-1/results", proberResultsRequest{
|
||
|
|
Results: []proberResultDTO{proberResult(ip, "tcp-22", true, true)},
|
||
|
|
})
|
||
|
|
fc.do(http.MethodPost, "/api/v1/agents/validator-1/results", agentResultsRequest{
|
||
|
|
Results: []checkResultDTO{{IPID: ip.ID, CheckType: "https", Target: "https://example.test", Success: true, CheckedAt: time.Now().UTC().Format(time.RFC3339Nano)}},
|
||
|
|
})
|
||
|
|
if err := d.SetAggregating(ctx, ip.ID); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if err := d.FinishIP(ctx, ip.ID, db.ResultPartial); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
|
||
|
|
// prober: one new check and one that would flip a stored success
|
||
|
|
resp, body := fc.do(http.MethodPost, "/api/v1/probers/site-1/results", proberResultsRequest{
|
||
|
|
Results: []proberResultDTO{proberResult(ip, "tcp-22", false, false), proberResult(ip, "ssh", false, true)},
|
||
|
|
})
|
||
|
|
var rr resultsResponse
|
||
|
|
if resp.StatusCode != http.StatusOK || json.Unmarshal(body, &rr) != nil || !rr.OK || rr.Ignored != 2 {
|
||
|
|
t.Fatalf("prober late results: %d %s", resp.StatusCode, body)
|
||
|
|
}
|
||
|
|
// agent
|
||
|
|
resp, body = fc.do(http.MethodPost, "/api/v1/agents/validator-1/results", agentResultsRequest{
|
||
|
|
Results: []checkResultDTO{{IPID: ip.ID, CheckType: "https", Target: "https://example.test", Success: false, CheckedAt: time.Now().UTC().Format(time.RFC3339Nano)}},
|
||
|
|
})
|
||
|
|
if resp.StatusCode != http.StatusOK || json.Unmarshal(body, &rr) != nil || rr.Ignored != 1 {
|
||
|
|
t.Fatalf("agent late results: %d %s", resp.StatusCode, body)
|
||
|
|
}
|
||
|
|
|
||
|
|
rows, err := d.ListChecksForAttempt(ctx, ip.ID, ip.AttemptNumber)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if len(rows) != 2 {
|
||
|
|
t.Fatalf("expected the 2 checks written before the verdict, got %d", len(rows))
|
||
|
|
}
|
||
|
|
for _, c := range rows {
|
||
|
|
if !c.Success {
|
||
|
|
t.Errorf("%s/%s changed after the verdict", c.Source, c.CheckType)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
var dropped int
|
||
|
|
if err := d.QueryRowContext(ctx, `SELECT COUNT(*) FROM events WHERE event_type='result_dropped' AND ip_id=?`, ip.ID).Scan(&dropped); err != nil || dropped != 2 {
|
||
|
|
t.Fatalf("expected 2 result_dropped events (prober, agent), got %d err=%v", dropped, err)
|
||
|
|
}
|
||
|
|
}
|