CI / changes (push) Successful in 5s
CI / openapi (push) Has been skipped
CI / go (push) Successful in 21s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m1s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m0s
CI / docker-bird (push) Has been skipped
CI / bird2 (push) Successful in 15s
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 1m31s
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 1m24s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Successful in 1m35s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Successful in 1m17s
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Successful in 1m25s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Has been cancelled
246 lines
6.8 KiB
Go
246 lines
6.8 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("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)
|
|
}
|