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) } }