Files
seaweedfs/weed/s3api/s3vectors/limits_test.go
T
Chris Lu 84cd3c621b s3vectors: integrate bucket metrics, audit events, and request limits (#12514)
* s3vectors: attribute request and traffic metrics to authorized buckets

* s3vectors: emit resource and identity audit events

* s3vectors: apply existing S3 request limits and preserve JSON errors

* s3vectors: select limit resources by the dispatched operation

* s3vectors: count decoded chunked request bodies against byte limits
2026-11-09 11:37:06 -08:00

126 lines
4.9 KiB
Go

package s3vectors
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gorilla/mux"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
)
func TestVectorLimitsResolveResourcesAndPreserveBodies(t *testing.T) {
s := NewServer(nil, headerAuth{}, nil)
s.SetRegion("r")
s.SetAccountID("a")
for _, tc := range []struct {
op, action string
body interface{}
}{
{"PutVectors", s3_constants.ACTION_WRITE, PutVectorsRequest{VectorBucketName: "embeddings", IndexName: "documents", Vectors: []*PutInputVector{{Key: "a", Data: &VectorData{Float32: []float32{1, 2}}}}}},
{"QueryVectors", s3_constants.ACTION_READ, QueryVectorsRequest{IndexArn: s.indexArn("embeddings", "documents"), TopK: 1}},
{"GetVectorBucket", s3_constants.ACTION_READ, GetVectorBucketRequest{VectorBucketArn: s.vectorBucketArn("embeddings")}},
} {
t.Run(tc.op, func(t *testing.T) {
payload, err := json.Marshal(tc.body)
if err != nil {
t.Fatal(err)
}
r := httptest.NewRequest(http.MethodPost, "/"+tc.op, bytes.NewReader(payload))
var called bool
s.SetLimiter(func(next http.HandlerFunc, action string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if mux.Vars(r)["bucket"] != "embeddings" || action != tc.action {
t.Fatalf("limit target %v, action %s", mux.Vars(r), action)
}
next(w, r)
}
})
s.instrument(func(w http.ResponseWriter, r *http.Request) {
s.limitRequest(func(w http.ResponseWriter, r *http.Request) {
called = true
var got json.RawMessage
if err := readJSON(r, &got); err != nil || !bytes.Equal(got, payload) {
t.Fatalf("handler body %s: %v", got, err)
}
}, w, r)
})(httptest.NewRecorder(), r)
if !called {
t.Fatal("limited handler was not called")
}
})
}
}
func TestVectorLimitsReturnJSONWithoutCallingHandler(t *testing.T) {
s := NewServer(nil, headerAuth{}, nil)
s.SetLimiter(func(next http.HandlerFunc, action string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
s3err.WriteErrorResponse(w, r, s3err.ErrTooManyRequest)
}
})
r := httptest.NewRequest(http.MethodPost, "/PutVectors", bytes.NewBufferString(`{"vectorBucketName":"embeddings","indexName":"documents"}`))
w := httptest.NewRecorder()
s.instrument(func(w http.ResponseWriter, r *http.Request) {
s.limitRequest(func(http.ResponseWriter, *http.Request) { t.Fatal("rejected handler ran") }, w, r)
})(w, r)
var failure Error
if json.Unmarshal(w.Body.Bytes(), &failure) != nil || failure.Code == "" || w.Code < http.StatusBadRequest || w.Header().Get("Content-Type") != "application/x-amz-json-1.1" {
t.Fatalf("limit response: %d %v %s", w.Code, w.Header(), w.Body)
}
}
func TestVectorLimitsIgnoreUnrelatedResourceFields(t *testing.T) {
s := NewServer(nil, headerAuth{}, nil)
for _, tc := range []struct{ op, body, bucket string }{
{"PutVectors", `{"vectorBucketName":"bucket-a","indexName":"idx","resourceArn":"arn:aws:s3vectors:r:a:bucket/bucket-b"}`, "bucket-a"},
{"PutVectors", `{"vectorBucketName":"bucket-a","indexName":"idx","resourceArn":{"ignored":true}}`, "bucket-a"},
{"GetVectorBucket", `{"vectorBucketName":"bucket-a","indexArn":"invalid","indexName":42}`, "bucket-a"},
{"CreateIndex", `{"vectorBucketName":"bucket-a","indexName":"idx","resourceArn":"ignored"}`, "bucket-a"},
{"ListVectorBuckets", `{"vectorBucketName":"ignored","resourceArn":"ignored"}`, ""},
} {
t.Run(tc.op+tc.body, func(t *testing.T) {
called := false
s.SetLimiter(func(next http.HandlerFunc, action string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
called = true
if got := mux.Vars(r)["bucket"]; got != tc.bucket {
t.Fatalf("limited bucket %q, want %q", got, tc.bucket)
}
next(w, r)
}
})
r := httptest.NewRequest(http.MethodPost, "/"+tc.op, bytes.NewBufferString(tc.body))
s.instrument(func(w http.ResponseWriter, r *http.Request) {
s.limitRequest(func(http.ResponseWriter, *http.Request) {}, w, r)
})(httptest.NewRecorder(), r)
if !called {
t.Fatal("limiter was not invoked")
}
})
}
}
func TestVectorLimitsCountChunkedBodyBytes(t *testing.T) {
s := NewServer(nil, headerAuth{}, nil)
body := `{"vectorBucketName":"embeddings","indexName":"documents"}`
var got int64
s.SetLimiter(func(next http.HandlerFunc, action string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { got = r.ContentLength; next(w, r) }
})
r := httptest.NewRequest(http.MethodPost, "/PutVectors", bytes.NewBufferString(body))
r.ContentLength = -1
s.instrument(func(w http.ResponseWriter, r *http.Request) {
s.limitRequest(func(http.ResponseWriter, *http.Request) {}, w, r)
})(httptest.NewRecorder(), r)
if got != int64(len(body)) {
t.Fatalf("limited body length = %d, want %d", got, len(body))
}
if r.ContentLength != -1 {
t.Fatal("mutated original request")
}
}