package httpapi import ( "context" "net/http" "net/http/httptest" "sync" "testing" "time" "evobgp/internal/store" ) func TestOperationLimitsSharedCapacityAndWindows(t *testing.T) { l := newOperationLimiter() l.maxActive = 2 l.maxTenantActive = 1 l.rates = [3]int{1, 2, 2} now := time.Now() a, _ := l.acquire("a", lookupOperation, now) if a == nil { t.Fatal("first denied") } if r, _ := l.acquire("a", cdnPreviewOperation, now); r != nil { t.Fatal("tenant concurrency bypass") } b, _ := l.acquire("b", revisionPreviewOperation, now) if b == nil { t.Fatal("other tenant denied") } if r, _ := l.acquire("c", lookupOperation, now); r != nil { t.Fatal("global concurrency bypass") } a() a() b() if l.active != 0 { t.Fatal("release not idempotent") } if r, retry := l.acquire("a", lookupOperation, now.Add(time.Second)); r != nil || retry != 59 { t.Fatalf("rate bypass/retry admitted=%t seconds=%d", r != nil, retry) } c, _ := l.acquire("a", cdnPreviewOperation, now.Add(time.Second)) if c == nil { t.Fatal("independent operation denied") } c() d, _ := l.acquire("a", lookupOperation, now.Add(time.Minute)) if d == nil { t.Fatal("expired window not reset") } d() } func TestOperationTenantMapBoundWithoutActiveEviction(t *testing.T) { l := newOperationLimiter() l.maxTenants = 1 now := time.Now() a, _ := l.acquire("a", lookupOperation, now) if r, _ := l.acquire("b", lookupOperation, now.Add(2*time.Minute)); r != nil { t.Fatal("active entry evicted") } a() b, _ := l.acquire("b", lookupOperation, now.Add(3*time.Minute)) if b == nil || len(l.tenants) != 1 { t.Fatal("idle expiry did not free bounded state") } b() } func TestOperationConcurrentAdmissionBound(t *testing.T) { l := newOperationLimiter() l.maxActive = 4 l.maxTenantActive = 4 l.rates = [3]int{100, 100, 100} results := make(chan func(), 32) var wg sync.WaitGroup for range 32 { wg.Add(1) go func() { defer wg.Done(); r, _ := l.acquire("tenant", lookupOperation, time.Now()); results <- r }() } wg.Wait() close(results) accepted := 0 for r := range results { if r != nil { accepted++ r() } } if accepted != 4 || l.active != 0 { t.Fatalf("accepted=%d active=%d", accepted, l.active) } } type waitingRevisionStore struct { store.Backend entered chan struct{} unblock chan struct{} } func (s waitingRevisionStore) GetRevision(tenant, id string) (*store.Revision, error) { s.entered <- struct{}{} <-s.unblock return s.Backend.GetRevision(tenant, id) } func TestRevisionQuotaHeldUntilWorkReturns(t *testing.T) { t.Setenv("EVOBGP_API_EXPENSIVE_TENANT_CONCURRENT", "1") t.Setenv("EVOBGP_API_REVISION_PREVIEW_PER_MINUTE", "30") m := store.NewMemory() m.SeedDemo() tenant, _, _, revision, _ := m.DemoIDs() entered := make(chan struct{}, 2) unblock := make(chan struct{}) s := &Server{store: waitingRevisionStore{Backend: m, entered: entered, unblock: unblock}} request := func() *http.Request { r := httptest.NewRequest(http.MethodGet, "/v1/revisions/"+revision+"/preview", nil) r.SetPathValue("revision_id", revision) return r.WithContext(context.WithValue(r.Context(), authCtxKey, Auth{TenantID: tenant, Role: "operator"})) } ctx, cancel := context.WithCancel(request().Context()) defer cancel() first := httptest.NewRecorder() done := make(chan struct{}) go func() { defer close(done); s.handleRevisionPreview(first, request().WithContext(ctx)) }() <-entered cancel() // Backend deliberately ignores cancellation: capacity stays reserved. second := httptest.NewRecorder() s.handleRevisionPreview(second, request()) if second.Code != http.StatusTooManyRequests || second.Header().Get("Retry-After") == "" { close(unblock) <-done t.Fatalf("quota: %d %s", second.Code, second.Body.String()) } close(unblock) <-done third := httptest.NewRecorder() s.handleRevisionPreview(third, request()) if third.Code != http.StatusOK { t.Fatalf("release failed: %d %s", third.Code, third.Body.String()) } }