Files
EvoBGP/internal/httpapi/cdn_preview_body_test.go
DenozordecandCursor f8e0520817 feat(httpapi): harden control plane and speed up prefix refresh
Очередь job получила lease и fencing, телеметрия и прогресс deploy сохраняются, ревизия пишет manifest входов. Обновление префиксов не переписывает неизменённый снимок, большие наборы идут через COPY, а устаревшая ревизия не попадает в deploy. Бандл ноды кэшируется, prune и постановка job больше не держат горячий путь.

Co-authored-by: Cursor <[email protected]>
2026-11-02 17:23:59 +07:00

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())
}
}