package httpapi import ( "crypto/ed25519" "encoding/base64" "encoding/json" "io" "net/http" "net/http/httptest" "strings" "testing" "time" "evobgp/internal/jobs" "evobgp/internal/signing" ) const testBundleSeed = "0101010101010101010101010101010101010101010101010101010101010101" func TestAPIRefreshApplyJobsBundle(t *testing.T) { srv, err := New(Options{ InsecureDev: true, SeedDemo: true, BundleSeedHex: testBundleSeed, }) if err != nil { t.Fatal(err) } defer srv.Close() tenant, modCDN, modIP, rev, speaker := srv.Store().DemoIDs() srv.apiKeys = parseAPIKeysSpec("nodekey|" + tenant + "|node,opkey|" + tenant + "|operator") ts := httptest.NewServer(srv.Handler()) defer ts.Close() client := ts.Client() base := ts.URL t.Run("prometheus metrics", func(t *testing.T) { // HTTPMiddleware increments the counter after the handler returns, so the scrape // of /metrics does not include that same request; warm with a public route first. warm, _ := http.NewRequest(http.MethodGet, base+"/v1/health", nil) warmResp, err := client.Do(warm) if err != nil { t.Fatal(err) } _, _ = io.Copy(io.Discard, warmResp.Body) _ = warmResp.Body.Close() req, _ := http.NewRequest(http.MethodGet, base+"/metrics", nil) resp, err := client.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { b, _ := io.ReadAll(resp.Body) t.Fatalf("status %d: %s", resp.StatusCode, b) } raw, _ := io.ReadAll(resp.Body) s := string(raw) for _, needle := range []string{ "evobgp_materialized_prefixes_max", "evobgp_bgp_peers_configured_total", "evobgp_http_requests_total", // incremented by this scrape request } { if !strings.Contains(s, needle) { t.Fatalf("metrics body missing %q", needle) } } }) t.Run("refresh IP_RANGES no op", func(t *testing.T) { req, _ := http.NewRequest(http.MethodPost, base+"/v1/modules/"+modIP+"/refresh", nil) req.Header.Set("Authorization", "Bearer opkey") resp, err := client.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.StatusCode != http.StatusNoContent { b, _ := io.ReadAll(resp.Body) t.Fatalf("status %d: %s", resp.StatusCode, b) } }) t.Run("refresh CDN queues job", func(t *testing.T) { req, _ := http.NewRequest(http.MethodPost, base+"/v1/modules/"+modCDN+"/refresh", nil) req.Header.Set("Authorization", "Bearer opkey") resp, err := client.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.StatusCode != http.StatusAccepted { b, _ := io.ReadAll(resp.Body) t.Fatalf("status %d: %s", resp.StatusCode, b) } var body struct { JobID string `json:"job_id"` } if err := json.NewDecoder(resp.Body).Decode(&body); err != nil { t.Fatal(err) } waitJob(t, client, base, "opkey", body.JobID) }) t.Run("preview revision", func(t *testing.T) { req, _ := http.NewRequest(http.MethodGet, base+"/v1/revisions/"+rev+"/preview", nil) req.Header.Set("Authorization", "Bearer opkey") resp, err := client.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { b, _ := io.ReadAll(resp.Body) t.Fatalf("status %d: %s", resp.StatusCode, b) } }) t.Run("list modules peers speakers", func(t *testing.T) { for _, path := range []string{"/v1/modules", "/v1/peers", "/v1/speakers"} { req, _ := http.NewRequest(http.MethodGet, base+path, nil) req.Header.Set("Authorization", "Bearer opkey") resp, err := client.Do(req) if err != nil { t.Fatal(err) } b, _ := io.ReadAll(resp.Body) resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("%s status %d: %s", path, resp.StatusCode, b) } var body struct { Items []map[string]any `json:"items"` } if err := json.Unmarshal(b, &body); err != nil { t.Fatalf("%s json: %v", path, err) } if len(body.Items) < 1 { t.Fatalf("%s expected items", path) } } }) t.Run("rollback queues job", func(t *testing.T) { req, _ := http.NewRequest(http.MethodPost, base+"/v1/revisions/"+rev+"/rollback", nil) req.Header.Set("Authorization", "Bearer opkey") resp, err := client.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.StatusCode != http.StatusAccepted { b, _ := io.ReadAll(resp.Body) t.Fatalf("status %d: %s", resp.StatusCode, b) } var body struct { JobID string `json:"job_id"` } _ = json.NewDecoder(resp.Body).Decode(&body) waitJob(t, client, base, "opkey", body.JobID) }) t.Run("apply all speakers", func(t *testing.T) { req, _ := http.NewRequest(http.MethodPost, base+"/v1/apply", strings.NewReader(`{"revision_id":"`+rev+`"}`)) req.Header.Set("Authorization", "Bearer opkey") req.Header.Set("Content-Type", "application/json") resp, err := client.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.StatusCode != http.StatusAccepted { b, _ := io.ReadAll(resp.Body) t.Fatalf("status %d: %s", resp.StatusCode, b) } var body struct { JobID string `json:"job_id"` } _ = json.NewDecoder(resp.Body).Decode(&body) waitJob(t, client, base, "opkey", body.JobID) }) pubB64 := srv.BundlePublicKeyBase64() pubBytes, err := base64.StdEncoding.DecodeString(pubB64) if err != nil { t.Fatal(err) } t.Run("node bundle roundtrip verify", func(t *testing.T) { req, _ := http.NewRequest(http.MethodGet, base+"/v1/speakers/"+speaker+"/bundle/"+rev, nil) req.Header.Set("Authorization", "Bearer nodekey") resp, err := client.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { b, _ := io.ReadAll(resp.Body) t.Fatalf("status %d: %s", resp.StatusCode, b) } raw, err := io.ReadAll(resp.Body) if err != nil { t.Fatal(err) } _, err = signing.VerifyGzippedTar(raw, ed25519.PublicKey(pubBytes)) if err != nil { t.Fatal(err) } }) } func waitJob(t *testing.T, client *http.Client, base, token, jobID string) { t.Helper() deadline := time.Now().Add(2 * time.Second) for time.Now().Before(deadline) { req, _ := http.NewRequest(http.MethodGet, base+"/v1/jobs/"+jobID, nil) req.Header.Set("Authorization", "Bearer "+token) resp, err := client.Do(req) if err != nil { t.Fatal(err) } b, _ := io.ReadAll(resp.Body) resp.Body.Close() var body struct { Status string `json:"status"` } _ = json.Unmarshal(b, &body) if body.Status == jobs.StatusSucceeded || body.Status == jobs.StatusFailed { if body.Status != jobs.StatusSucceeded { t.Fatalf("job %s status %s", jobID, body.Status) } return } time.Sleep(5 * time.Millisecond) } t.Fatalf("job %s did not complete", jobID) }