CI / changes (push) Successful in 7s
CI / openapi (push) Has been skipped
CI / go (push) Successful in 38s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m3s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m2s
CI / docker-bird (push) Has been skipped
CI / bird2 (push) Successful in 18s
CI / docker-go-prime (push) Successful in 22s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Successful in 59s
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Successful in 3m10s
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Successful in 1m22s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Successful in 1m20s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Successful in 1m20s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Successful in 1m6s
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Successful in 1m22s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Successful in 1m21s
Introduced a new endpoint `GET /revisions/{revision_id}/diagnostic-log` to retrieve detailed diagnostic logs for specific revisions. Implemented the corresponding handler to generate and return the log in a structured format. Additionally, updated the frontend to include a download button for the diagnostic log, enhancing user accessibility to revision data.
344 lines
9.5 KiB
Go
344 lines
9.5 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("diagnostic log for revision", func(t *testing.T) {
|
|
req, _ := http.NewRequest(http.MethodGet, base+"/v1/revisions/"+rev+"/diagnostic-log", 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)
|
|
}
|
|
if got := resp.Header.Get("Content-Type"); !strings.Contains(got, "text/plain") {
|
|
t.Fatalf("unexpected content-type: %q", got)
|
|
}
|
|
raw, _ := io.ReadAll(resp.Body)
|
|
body := string(raw)
|
|
for _, needle := range []string{
|
|
"EvoBGP revision diagnostic log",
|
|
"revision_id=" + rev,
|
|
"## Raw rows",
|
|
} {
|
|
if !strings.Contains(body, needle) {
|
|
t.Fatalf("diagnostic log missing %q", needle)
|
|
}
|
|
}
|
|
})
|
|
|
|
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)
|
|
}
|