CI / changes (push) Successful in 5s
CI / openapi (push) Successful in 22s
CI / go (push) Successful in 38s
CI / bird2 (push) Has been cancelled
CI / docker-go-prime (push) Has been cancelled
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Has been cancelled
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Has been cancelled
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Has been cancelled
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Has been cancelled
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Has been cancelled
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Has been cancelled
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Has been cancelled
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Has been cancelled
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Has started running
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Has been cancelled
CI / docker-bird (push) Has been cancelled
Introduced a new endpoint `GET /v1/router-lists/catalog` that returns a consolidated view of modules, domain entries, ASNs, IP ranges, and communities. Enhanced the existing module listing functionality to support filtering by type and enabled status. Updated documentation to reflect these changes and added tests for the new endpoint and filtering capabilities.
316 lines
8.7 KiB
Go
316 lines
8.7 KiB
Go
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 queues render job", 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.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("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("modules filter by type", func(t *testing.T) {
|
|
req, _ := http.NewRequest(http.MethodGet, base+"/v1/modules?type=IP_RANGES", 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)
|
|
}
|
|
var body struct {
|
|
Items []struct {
|
|
Type string `json:"type"`
|
|
} `json:"items"`
|
|
}
|
|
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(body.Items) == 0 {
|
|
t.Fatalf("expected at least one IP_RANGES module")
|
|
}
|
|
for _, item := range body.Items {
|
|
if item.Type != "IP_RANGES" {
|
|
t.Fatalf("unexpected module type %q", item.Type)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("router lists catalog endpoint", func(t *testing.T) {
|
|
req, _ := http.NewRequest(http.MethodGet, base+"/v1/router-lists/catalog", 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)
|
|
}
|
|
var body struct {
|
|
Modules struct {
|
|
Items []map[string]any `json:"items"`
|
|
} `json:"modules"`
|
|
Domains struct {
|
|
Items []map[string]any `json:"items"`
|
|
} `json:"domains"`
|
|
ASNs struct {
|
|
Items []map[string]any `json:"items"`
|
|
} `json:"asns"`
|
|
IPRanges struct {
|
|
Items []map[string]any `json:"items"`
|
|
} `json:"ip_ranges"`
|
|
Communities struct {
|
|
Items []map[string]any `json:"items"`
|
|
} `json:"communities"`
|
|
}
|
|
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(body.Modules.Items) == 0 {
|
|
t.Fatalf("expected modules in catalog")
|
|
}
|
|
if body.Communities.Items == nil {
|
|
t.Fatalf("expected communities.items field in catalog")
|
|
}
|
|
})
|
|
|
|
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)
|
|
}
|