Очередь job получила lease и fencing, телеметрия и прогресс deploy сохраняются, ревизия пишет manifest входов. Обновление префиксов не переписывает неизменённый снимок, большие наборы идут через COPY, а устаревшая ревизия не попадает в deploy. Бандл ноды кэшируется, prune и постановка job больше не держат горячий путь. Co-authored-by: Cursor <cursoragent@cursor.com>
82 lines
3.0 KiB
Go
82 lines
3.0 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"evobgp/internal/store"
|
|
)
|
|
|
|
func TestPreviewCDNSourceRejectsOversizedBody(t *testing.T) {
|
|
t.Setenv("EVOBGP_CDN_ALLOW_PRIVATE", "1")
|
|
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
_, _ = io.Copy(w, strings.NewReader(strings.Repeat("x", (8<<20)+1)))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
st := store.NewMemory()
|
|
st.SeedDemo()
|
|
tenant, _, _, _, _ := st.DemoIDs()
|
|
mod, err := st.CreateModule(tenant, &store.Module{Type: "CDN_CIDRS", Name: "oversized-preview", Enabled: true})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s := &Server{store: st, cdnHTTP: srv.Client()}
|
|
body, err := json.Marshal(map[string]string{"url": srv.URL, "source_kind": "plain"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
req := httptest.NewRequest(http.MethodPost, "/v1/modules/"+mod.ID+"/cdn-sources/preview", strings.NewReader(string(body)))
|
|
req.SetPathValue("module_id", mod.ID)
|
|
req = req.WithContext(context.WithValue(req.Context(), authCtxKey, Auth{TenantID: tenant, Role: "editor"}))
|
|
recorder := httptest.NewRecorder()
|
|
s.handlePreviewCDNSource(recorder, req)
|
|
if recorder.Code != http.StatusBadGateway {
|
|
t.Fatalf("oversized preview status=%d body=%s", recorder.Code, recorder.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestPreviewCDNErrorBodyBoundAndQuotaBeforeFetch(t *testing.T) {
|
|
t.Setenv("EVOBGP_CDN_ALLOW_PRIVATE", "1")
|
|
t.Setenv("EVOBGP_API_CDN_PREVIEW_PER_MINUTE", "1")
|
|
m := store.NewMemory()
|
|
m.SeedDemo()
|
|
tenant, module, _, _, _ := m.DemoIDs()
|
|
var calls atomic.Int32
|
|
upstream := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
calls.Add(1)
|
|
w.WriteHeader(http.StatusNotFound)
|
|
_, _ = io.WriteString(w, strings.Repeat("x", 64<<10))
|
|
w.(http.Flusher).Flush()
|
|
// No EOF: an unbounded drain would wait until the caller deadline.
|
|
<-r.Context().Done()
|
|
}))
|
|
defer upstream.Close()
|
|
s := &Server{store: m, cdnHTTP: upstream.Client()}
|
|
request := func() *http.Request {
|
|
r := httptest.NewRequest(http.MethodPost, "/v1/modules/"+module+"/cdn-sources/preview", strings.NewReader(`{"url":"`+upstream.URL+`","source_kind":"plain"}`))
|
|
r.SetPathValue("module_id", module)
|
|
return r.WithContext(context.WithValue(r.Context(), authCtxKey, Auth{TenantID: tenant, Role: "editor"}))
|
|
}
|
|
first := httptest.NewRecorder()
|
|
firstRequest := request()
|
|
ctx, cancel := context.WithTimeout(firstRequest.Context(), 5*time.Second)
|
|
defer cancel()
|
|
s.handlePreviewCDNSource(first, firstRequest.WithContext(ctx))
|
|
if first.Code != http.StatusBadGateway || calls.Load() != 1 || ctx.Err() != nil {
|
|
t.Fatalf("unbounded drain status=%d calls=%d ctx=%v", first.Code, calls.Load(), ctx.Err())
|
|
}
|
|
second := httptest.NewRecorder()
|
|
s.handlePreviewCDNSource(second, request())
|
|
if second.Code != http.StatusTooManyRequests || calls.Load() != 1 || second.Header().Get("Retry-After") == "" {
|
|
t.Fatalf("quota fetch bypass status=%d calls=%d", second.Code, calls.Load())
|
|
}
|
|
}
|