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