| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849 |
- package s3api
- import (
- "bytes"
- "fmt"
- "net/http"
- "net/url"
- "testing"
- "time"
- "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
- "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
- "github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
- )
- // TestConditionalHeadersWithExistingObjects tests conditional headers against existing objects
- // This addresses the PR feedback about missing test coverage for object existence scenarios
- func TestConditionalHeadersWithExistingObjects(t *testing.T) {
- bucket := "test-bucket"
- object := "/test-object"
- // Mock object with known ETag and modification time
- testObject := &filer_pb.Entry{
- Name: "test-object",
- Extended: map[string][]byte{
- s3_constants.ExtETagKey: []byte("\"abc123\""),
- },
- Attributes: &filer_pb.FuseAttributes{
- Mtime: time.Date(2024, 6, 15, 12, 0, 0, 0, time.UTC).Unix(), // June 15, 2024
- FileSize: 1024, // Add file size
- },
- Chunks: []*filer_pb.FileChunk{
- // Add a mock chunk to make calculateETagFromChunks work
- {
- FileId: "test-file-id",
- Offset: 0,
- Size: 1024,
- },
- },
- }
- // Test If-None-Match with existing object
- t.Run("IfNoneMatch_ObjectExists", func(t *testing.T) {
- // Test case 1: If-None-Match=* when object exists (should fail)
- t.Run("Asterisk_ShouldFail", func(t *testing.T) {
- getter := createMockEntryGetter(testObject)
- req := createTestPutRequest(bucket, object, "test content")
- req.Header.Set(s3_constants.IfNoneMatch, "*")
- s3a := NewS3ApiServerForTest()
- errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
- if errCode != s3err.ErrPreconditionFailed {
- t.Errorf("Expected ErrPreconditionFailed when object exists with If-None-Match=*, got %v", errCode)
- }
- })
- // Test case 2: If-None-Match with matching ETag (should fail)
- t.Run("MatchingETag_ShouldFail", func(t *testing.T) {
- getter := createMockEntryGetter(testObject)
- req := createTestPutRequest(bucket, object, "test content")
- req.Header.Set(s3_constants.IfNoneMatch, "\"abc123\"")
- s3a := NewS3ApiServerForTest()
- errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
- if errCode != s3err.ErrPreconditionFailed {
- t.Errorf("Expected ErrPreconditionFailed when ETag matches, got %v", errCode)
- }
- })
- // Test case 3: If-None-Match with non-matching ETag (should succeed)
- t.Run("NonMatchingETag_ShouldSucceed", func(t *testing.T) {
- getter := createMockEntryGetter(testObject)
- req := createTestPutRequest(bucket, object, "test content")
- req.Header.Set(s3_constants.IfNoneMatch, "\"xyz789\"")
- s3a := NewS3ApiServerForTest()
- errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
- if errCode != s3err.ErrNone {
- t.Errorf("Expected ErrNone when ETag doesn't match, got %v", errCode)
- }
- })
- // Test case 4: If-None-Match with multiple ETags, one matching (should fail)
- t.Run("MultipleETags_OneMatches_ShouldFail", func(t *testing.T) {
- getter := createMockEntryGetter(testObject)
- req := createTestPutRequest(bucket, object, "test content")
- req.Header.Set(s3_constants.IfNoneMatch, "\"xyz789\", \"abc123\", \"def456\"")
- s3a := NewS3ApiServerForTest()
- errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
- if errCode != s3err.ErrPreconditionFailed {
- t.Errorf("Expected ErrPreconditionFailed when one ETag matches, got %v", errCode)
- }
- })
- // Test case 5: If-None-Match with multiple ETags, none matching (should succeed)
- t.Run("MultipleETags_NoneMatch_ShouldSucceed", func(t *testing.T) {
- getter := createMockEntryGetter(testObject)
- req := createTestPutRequest(bucket, object, "test content")
- req.Header.Set(s3_constants.IfNoneMatch, "\"xyz789\", \"def456\", \"ghi123\"")
- s3a := NewS3ApiServerForTest()
- errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
- if errCode != s3err.ErrNone {
- t.Errorf("Expected ErrNone when no ETags match, got %v", errCode)
- }
- })
- })
- // Test If-Match with existing object
- t.Run("IfMatch_ObjectExists", func(t *testing.T) {
- // Test case 1: If-Match with matching ETag (should succeed)
- t.Run("MatchingETag_ShouldSucceed", func(t *testing.T) {
- getter := createMockEntryGetter(testObject)
- req := createTestPutRequest(bucket, object, "test content")
- req.Header.Set(s3_constants.IfMatch, "\"abc123\"")
- s3a := NewS3ApiServerForTest()
- errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
- if errCode != s3err.ErrNone {
- t.Errorf("Expected ErrNone when ETag matches, got %v", errCode)
- }
- })
- // Test case 2: If-Match with non-matching ETag (should fail)
- t.Run("NonMatchingETag_ShouldFail", func(t *testing.T) {
- getter := createMockEntryGetter(testObject)
- req := createTestPutRequest(bucket, object, "test content")
- req.Header.Set(s3_constants.IfMatch, "\"xyz789\"")
- s3a := NewS3ApiServerForTest()
- errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
- if errCode != s3err.ErrPreconditionFailed {
- t.Errorf("Expected ErrPreconditionFailed when ETag doesn't match, got %v", errCode)
- }
- })
- // Test case 3: If-Match with multiple ETags, one matching (should succeed)
- t.Run("MultipleETags_OneMatches_ShouldSucceed", func(t *testing.T) {
- getter := createMockEntryGetter(testObject)
- req := createTestPutRequest(bucket, object, "test content")
- req.Header.Set(s3_constants.IfMatch, "\"xyz789\", \"abc123\"")
- s3a := NewS3ApiServerForTest()
- errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
- if errCode != s3err.ErrNone {
- t.Errorf("Expected ErrNone when one ETag matches, got %v", errCode)
- }
- })
- // Test case 4: If-Match with wildcard * (should succeed if object exists)
- t.Run("Wildcard_ShouldSucceed", func(t *testing.T) {
- getter := createMockEntryGetter(testObject)
- req := createTestPutRequest(bucket, object, "test content")
- req.Header.Set(s3_constants.IfMatch, "*")
- s3a := NewS3ApiServerForTest()
- errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
- if errCode != s3err.ErrNone {
- t.Errorf("Expected ErrNone when If-Match=* and object exists, got %v", errCode)
- }
- })
- })
- // Test If-Modified-Since with existing object
- t.Run("IfModifiedSince_ObjectExists", func(t *testing.T) {
- // Test case 1: If-Modified-Since with date before object modification (should succeed)
- t.Run("DateBefore_ShouldSucceed", func(t *testing.T) {
- getter := createMockEntryGetter(testObject)
- req := createTestPutRequest(bucket, object, "test content")
- dateBeforeModification := time.Date(2024, 6, 14, 12, 0, 0, 0, time.UTC)
- req.Header.Set(s3_constants.IfModifiedSince, dateBeforeModification.Format(time.RFC1123))
- s3a := NewS3ApiServerForTest()
- errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
- if errCode != s3err.ErrNone {
- t.Errorf("Expected ErrNone when object was modified after date, got %v", errCode)
- }
- })
- // Test case 2: If-Modified-Since with date after object modification (should fail)
- t.Run("DateAfter_ShouldFail", func(t *testing.T) {
- getter := createMockEntryGetter(testObject)
- req := createTestPutRequest(bucket, object, "test content")
- dateAfterModification := time.Date(2024, 6, 16, 12, 0, 0, 0, time.UTC)
- req.Header.Set(s3_constants.IfModifiedSince, dateAfterModification.Format(time.RFC1123))
- s3a := NewS3ApiServerForTest()
- errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
- if errCode != s3err.ErrPreconditionFailed {
- t.Errorf("Expected ErrPreconditionFailed when object wasn't modified since date, got %v", errCode)
- }
- })
- // Test case 3: If-Modified-Since with exact modification date (should fail - not after)
- t.Run("ExactDate_ShouldFail", func(t *testing.T) {
- getter := createMockEntryGetter(testObject)
- req := createTestPutRequest(bucket, object, "test content")
- exactDate := time.Date(2024, 6, 15, 12, 0, 0, 0, time.UTC)
- req.Header.Set(s3_constants.IfModifiedSince, exactDate.Format(time.RFC1123))
- s3a := NewS3ApiServerForTest()
- errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
- if errCode != s3err.ErrPreconditionFailed {
- t.Errorf("Expected ErrPreconditionFailed when object modification time equals header date, got %v", errCode)
- }
- })
- })
- // Test If-Unmodified-Since with existing object
- t.Run("IfUnmodifiedSince_ObjectExists", func(t *testing.T) {
- // Test case 1: If-Unmodified-Since with date after object modification (should succeed)
- t.Run("DateAfter_ShouldSucceed", func(t *testing.T) {
- getter := createMockEntryGetter(testObject)
- req := createTestPutRequest(bucket, object, "test content")
- dateAfterModification := time.Date(2024, 6, 16, 12, 0, 0, 0, time.UTC)
- req.Header.Set(s3_constants.IfUnmodifiedSince, dateAfterModification.Format(time.RFC1123))
- s3a := NewS3ApiServerForTest()
- errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
- if errCode != s3err.ErrNone {
- t.Errorf("Expected ErrNone when object wasn't modified after date, got %v", errCode)
- }
- })
- // Test case 2: If-Unmodified-Since with date before object modification (should fail)
- t.Run("DateBefore_ShouldFail", func(t *testing.T) {
- getter := createMockEntryGetter(testObject)
- req := createTestPutRequest(bucket, object, "test content")
- dateBeforeModification := time.Date(2024, 6, 14, 12, 0, 0, 0, time.UTC)
- req.Header.Set(s3_constants.IfUnmodifiedSince, dateBeforeModification.Format(time.RFC1123))
- s3a := NewS3ApiServerForTest()
- errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
- if errCode != s3err.ErrPreconditionFailed {
- t.Errorf("Expected ErrPreconditionFailed when object was modified after date, got %v", errCode)
- }
- })
- })
- }
- // TestConditionalHeadersForReads tests conditional headers for read operations (GET, HEAD)
- // This implements AWS S3 conditional reads behavior where different conditions return different status codes
- // See: https://docs.aws.amazon.com/AmazonS3/latest/userguide/conditional-reads.html
- func TestConditionalHeadersForReads(t *testing.T) {
- bucket := "test-bucket"
- object := "/test-read-object"
- // Mock existing object to test conditional headers against
- existingObject := &filer_pb.Entry{
- Name: "test-read-object",
- Extended: map[string][]byte{
- s3_constants.ExtETagKey: []byte("\"read123\""),
- },
- Attributes: &filer_pb.FuseAttributes{
- Mtime: time.Date(2024, 6, 15, 12, 0, 0, 0, time.UTC).Unix(),
- FileSize: 1024,
- },
- Chunks: []*filer_pb.FileChunk{
- {
- FileId: "read-file-id",
- Offset: 0,
- Size: 1024,
- },
- },
- }
- // Test conditional reads with existing object
- t.Run("ConditionalReads_ObjectExists", func(t *testing.T) {
- // Test If-None-Match with existing object (should return 304 Not Modified)
- t.Run("IfNoneMatch_ObjectExists_ShouldReturn304", func(t *testing.T) {
- getter := createMockEntryGetter(existingObject)
- req := createTestGetRequest(bucket, object)
- req.Header.Set(s3_constants.IfNoneMatch, "\"read123\"")
- s3a := NewS3ApiServerForTest()
- errCode := s3a.checkConditionalHeadersForReadsWithGetter(getter, req, bucket, object)
- if errCode.ErrorCode != s3err.ErrNotModified {
- t.Errorf("Expected ErrNotModified when If-None-Match matches, got %v", errCode)
- }
- })
- // Test If-None-Match=* with existing object (should return 304 Not Modified)
- t.Run("IfNoneMatchAsterisk_ObjectExists_ShouldReturn304", func(t *testing.T) {
- getter := createMockEntryGetter(existingObject)
- req := createTestGetRequest(bucket, object)
- req.Header.Set(s3_constants.IfNoneMatch, "*")
- s3a := NewS3ApiServerForTest()
- errCode := s3a.checkConditionalHeadersForReadsWithGetter(getter, req, bucket, object)
- if errCode.ErrorCode != s3err.ErrNotModified {
- t.Errorf("Expected ErrNotModified when If-None-Match=* with existing object, got %v", errCode)
- }
- })
- // Test If-None-Match with non-matching ETag (should succeed)
- t.Run("IfNoneMatch_NonMatchingETag_ShouldSucceed", func(t *testing.T) {
- getter := createMockEntryGetter(existingObject)
- req := createTestGetRequest(bucket, object)
- req.Header.Set(s3_constants.IfNoneMatch, "\"different-etag\"")
- s3a := NewS3ApiServerForTest()
- errCode := s3a.checkConditionalHeadersForReadsWithGetter(getter, req, bucket, object)
- if errCode.ErrorCode != s3err.ErrNone {
- t.Errorf("Expected ErrNone when If-None-Match doesn't match, got %v", errCode)
- }
- })
- // Test If-Match with matching ETag (should succeed)
- t.Run("IfMatch_MatchingETag_ShouldSucceed", func(t *testing.T) {
- getter := createMockEntryGetter(existingObject)
- req := createTestGetRequest(bucket, object)
- req.Header.Set(s3_constants.IfMatch, "\"read123\"")
- s3a := NewS3ApiServerForTest()
- errCode := s3a.checkConditionalHeadersForReadsWithGetter(getter, req, bucket, object)
- if errCode.ErrorCode != s3err.ErrNone {
- t.Errorf("Expected ErrNone when If-Match matches, got %v", errCode)
- }
- })
- // Test If-Match with non-matching ETag (should return 412 Precondition Failed)
- t.Run("IfMatch_NonMatchingETag_ShouldReturn412", func(t *testing.T) {
- getter := createMockEntryGetter(existingObject)
- req := createTestGetRequest(bucket, object)
- req.Header.Set(s3_constants.IfMatch, "\"different-etag\"")
- s3a := NewS3ApiServerForTest()
- errCode := s3a.checkConditionalHeadersForReadsWithGetter(getter, req, bucket, object)
- if errCode.ErrorCode != s3err.ErrPreconditionFailed {
- t.Errorf("Expected ErrPreconditionFailed when If-Match doesn't match, got %v", errCode)
- }
- })
- // Test If-Match=* with existing object (should succeed)
- t.Run("IfMatchAsterisk_ObjectExists_ShouldSucceed", func(t *testing.T) {
- getter := createMockEntryGetter(existingObject)
- req := createTestGetRequest(bucket, object)
- req.Header.Set(s3_constants.IfMatch, "*")
- s3a := NewS3ApiServerForTest()
- errCode := s3a.checkConditionalHeadersForReadsWithGetter(getter, req, bucket, object)
- if errCode.ErrorCode != s3err.ErrNone {
- t.Errorf("Expected ErrNone when If-Match=* with existing object, got %v", errCode)
- }
- })
- // Test If-Modified-Since (object modified after date - should succeed)
- t.Run("IfModifiedSince_ObjectModifiedAfter_ShouldSucceed", func(t *testing.T) {
- getter := createMockEntryGetter(existingObject)
- req := createTestGetRequest(bucket, object)
- req.Header.Set(s3_constants.IfModifiedSince, "Sat, 14 Jun 2024 12:00:00 GMT") // Before object mtime
- s3a := NewS3ApiServerForTest()
- errCode := s3a.checkConditionalHeadersForReadsWithGetter(getter, req, bucket, object)
- if errCode.ErrorCode != s3err.ErrNone {
- t.Errorf("Expected ErrNone when object modified after If-Modified-Since date, got %v", errCode)
- }
- })
- // Test If-Modified-Since (object not modified since date - should return 304)
- t.Run("IfModifiedSince_ObjectNotModified_ShouldReturn304", func(t *testing.T) {
- getter := createMockEntryGetter(existingObject)
- req := createTestGetRequest(bucket, object)
- req.Header.Set(s3_constants.IfModifiedSince, "Sun, 16 Jun 2024 12:00:00 GMT") // After object mtime
- s3a := NewS3ApiServerForTest()
- errCode := s3a.checkConditionalHeadersForReadsWithGetter(getter, req, bucket, object)
- if errCode.ErrorCode != s3err.ErrNotModified {
- t.Errorf("Expected ErrNotModified when object not modified since If-Modified-Since date, got %v", errCode)
- }
- })
- // Test If-Unmodified-Since (object not modified since date - should succeed)
- t.Run("IfUnmodifiedSince_ObjectNotModified_ShouldSucceed", func(t *testing.T) {
- getter := createMockEntryGetter(existingObject)
- req := createTestGetRequest(bucket, object)
- req.Header.Set(s3_constants.IfUnmodifiedSince, "Sun, 16 Jun 2024 12:00:00 GMT") // After object mtime
- s3a := NewS3ApiServerForTest()
- errCode := s3a.checkConditionalHeadersForReadsWithGetter(getter, req, bucket, object)
- if errCode.ErrorCode != s3err.ErrNone {
- t.Errorf("Expected ErrNone when object not modified since If-Unmodified-Since date, got %v", errCode)
- }
- })
- // Test If-Unmodified-Since (object modified since date - should return 412)
- t.Run("IfUnmodifiedSince_ObjectModified_ShouldReturn412", func(t *testing.T) {
- getter := createMockEntryGetter(existingObject)
- req := createTestGetRequest(bucket, object)
- req.Header.Set(s3_constants.IfUnmodifiedSince, "Fri, 14 Jun 2024 12:00:00 GMT") // Before object mtime
- s3a := NewS3ApiServerForTest()
- errCode := s3a.checkConditionalHeadersForReadsWithGetter(getter, req, bucket, object)
- if errCode.ErrorCode != s3err.ErrPreconditionFailed {
- t.Errorf("Expected ErrPreconditionFailed when object modified since If-Unmodified-Since date, got %v", errCode)
- }
- })
- })
- // Test conditional reads with non-existent object
- t.Run("ConditionalReads_ObjectNotExists", func(t *testing.T) {
- // Test If-None-Match with non-existent object (should succeed)
- t.Run("IfNoneMatch_ObjectNotExists_ShouldSucceed", func(t *testing.T) {
- getter := createMockEntryGetter(nil) // No object
- req := createTestGetRequest(bucket, object)
- req.Header.Set(s3_constants.IfNoneMatch, "\"any-etag\"")
- s3a := NewS3ApiServerForTest()
- errCode := s3a.checkConditionalHeadersForReadsWithGetter(getter, req, bucket, object)
- if errCode.ErrorCode != s3err.ErrNone {
- t.Errorf("Expected ErrNone when object doesn't exist with If-None-Match, got %v", errCode)
- }
- })
- // Test If-Match with non-existent object (should return 412)
- t.Run("IfMatch_ObjectNotExists_ShouldReturn412", func(t *testing.T) {
- getter := createMockEntryGetter(nil) // No object
- req := createTestGetRequest(bucket, object)
- req.Header.Set(s3_constants.IfMatch, "\"any-etag\"")
- s3a := NewS3ApiServerForTest()
- errCode := s3a.checkConditionalHeadersForReadsWithGetter(getter, req, bucket, object)
- if errCode.ErrorCode != s3err.ErrPreconditionFailed {
- t.Errorf("Expected ErrPreconditionFailed when object doesn't exist with If-Match, got %v", errCode)
- }
- })
- // Test If-Modified-Since with non-existent object (should succeed)
- t.Run("IfModifiedSince_ObjectNotExists_ShouldSucceed", func(t *testing.T) {
- getter := createMockEntryGetter(nil) // No object
- req := createTestGetRequest(bucket, object)
- req.Header.Set(s3_constants.IfModifiedSince, "Sat, 15 Jun 2024 12:00:00 GMT")
- s3a := NewS3ApiServerForTest()
- errCode := s3a.checkConditionalHeadersForReadsWithGetter(getter, req, bucket, object)
- if errCode.ErrorCode != s3err.ErrNone {
- t.Errorf("Expected ErrNone when object doesn't exist with If-Modified-Since, got %v", errCode)
- }
- })
- // Test If-Unmodified-Since with non-existent object (should return 412)
- t.Run("IfUnmodifiedSince_ObjectNotExists_ShouldReturn412", func(t *testing.T) {
- getter := createMockEntryGetter(nil) // No object
- req := createTestGetRequest(bucket, object)
- req.Header.Set(s3_constants.IfUnmodifiedSince, "Sat, 15 Jun 2024 12:00:00 GMT")
- s3a := NewS3ApiServerForTest()
- errCode := s3a.checkConditionalHeadersForReadsWithGetter(getter, req, bucket, object)
- if errCode.ErrorCode != s3err.ErrPreconditionFailed {
- t.Errorf("Expected ErrPreconditionFailed when object doesn't exist with If-Unmodified-Since, got %v", errCode)
- }
- })
- })
- }
- // Helper function to create a GET request for testing
- func createTestGetRequest(bucket, object string) *http.Request {
- return &http.Request{
- Method: "GET",
- Header: make(http.Header),
- URL: &url.URL{
- Path: fmt.Sprintf("/%s%s", bucket, object),
- },
- }
- }
- // TestConditionalHeadersWithNonExistentObjects tests the original scenarios (object doesn't exist)
- func TestConditionalHeadersWithNonExistentObjects(t *testing.T) {
- s3a := NewS3ApiServerForTest()
- if s3a == nil {
- t.Skip("S3ApiServer not available for testing")
- }
- bucket := "test-bucket"
- object := "/test-object"
- // Test If-None-Match header when object doesn't exist
- t.Run("IfNoneMatch_ObjectDoesNotExist", func(t *testing.T) {
- // Test case 1: If-None-Match=* when object doesn't exist (should return ErrNone)
- t.Run("Asterisk_ShouldSucceed", func(t *testing.T) {
- getter := createMockEntryGetter(nil) // No object exists
- req := createTestPutRequest(bucket, object, "test content")
- req.Header.Set(s3_constants.IfNoneMatch, "*")
- s3a := NewS3ApiServerForTest()
- errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
- if errCode != s3err.ErrNone {
- t.Errorf("Expected ErrNone when object doesn't exist, got %v", errCode)
- }
- })
- // Test case 2: If-None-Match with specific ETag when object doesn't exist
- t.Run("SpecificETag_ShouldSucceed", func(t *testing.T) {
- getter := createMockEntryGetter(nil) // No object exists
- req := createTestPutRequest(bucket, object, "test content")
- req.Header.Set(s3_constants.IfNoneMatch, "\"some-etag\"")
- s3a := NewS3ApiServerForTest()
- errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
- if errCode != s3err.ErrNone {
- t.Errorf("Expected ErrNone when object doesn't exist, got %v", errCode)
- }
- })
- })
- // Test If-Match header when object doesn't exist
- t.Run("IfMatch_ObjectDoesNotExist", func(t *testing.T) {
- // Test case 1: If-Match with specific ETag when object doesn't exist (should fail - critical bug fix)
- t.Run("SpecificETag_ShouldFail", func(t *testing.T) {
- getter := createMockEntryGetter(nil) // No object exists
- req := createTestPutRequest(bucket, object, "test content")
- req.Header.Set(s3_constants.IfMatch, "\"some-etag\"")
- s3a := NewS3ApiServerForTest()
- errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
- if errCode != s3err.ErrPreconditionFailed {
- t.Errorf("Expected ErrPreconditionFailed when object doesn't exist with If-Match header, got %v", errCode)
- }
- })
- // Test case 2: If-Match with wildcard * when object doesn't exist (should fail)
- t.Run("Wildcard_ShouldFail", func(t *testing.T) {
- getter := createMockEntryGetter(nil) // No object exists
- req := createTestPutRequest(bucket, object, "test content")
- req.Header.Set(s3_constants.IfMatch, "*")
- s3a := NewS3ApiServerForTest()
- errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
- if errCode != s3err.ErrPreconditionFailed {
- t.Errorf("Expected ErrPreconditionFailed when object doesn't exist with If-Match=*, got %v", errCode)
- }
- })
- })
- // Test date format validation (works regardless of object existence)
- t.Run("DateFormatValidation", func(t *testing.T) {
- // Test case 1: Valid If-Modified-Since date format
- t.Run("IfModifiedSince_ValidFormat", func(t *testing.T) {
- getter := createMockEntryGetter(nil) // No object exists
- req := createTestPutRequest(bucket, object, "test content")
- req.Header.Set(s3_constants.IfModifiedSince, time.Now().Format(time.RFC1123))
- s3a := NewS3ApiServerForTest()
- errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
- if errCode != s3err.ErrNone {
- t.Errorf("Expected ErrNone with valid date format, got %v", errCode)
- }
- })
- // Test case 2: Invalid If-Modified-Since date format
- t.Run("IfModifiedSince_InvalidFormat", func(t *testing.T) {
- getter := createMockEntryGetter(nil) // No object exists
- req := createTestPutRequest(bucket, object, "test content")
- req.Header.Set(s3_constants.IfModifiedSince, "invalid-date")
- s3a := NewS3ApiServerForTest()
- errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
- if errCode != s3err.ErrInvalidRequest {
- t.Errorf("Expected ErrInvalidRequest for invalid date format, got %v", errCode)
- }
- })
- // Test case 3: Invalid If-Unmodified-Since date format
- t.Run("IfUnmodifiedSince_InvalidFormat", func(t *testing.T) {
- getter := createMockEntryGetter(nil) // No object exists
- req := createTestPutRequest(bucket, object, "test content")
- req.Header.Set(s3_constants.IfUnmodifiedSince, "invalid-date")
- s3a := NewS3ApiServerForTest()
- errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
- if errCode != s3err.ErrInvalidRequest {
- t.Errorf("Expected ErrInvalidRequest for invalid date format, got %v", errCode)
- }
- })
- })
- // Test no conditional headers
- t.Run("NoConditionalHeaders", func(t *testing.T) {
- getter := createMockEntryGetter(nil) // No object exists
- req := createTestPutRequest(bucket, object, "test content")
- // Don't set any conditional headers
- s3a := NewS3ApiServerForTest()
- errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
- if errCode != s3err.ErrNone {
- t.Errorf("Expected ErrNone when no conditional headers, got %v", errCode)
- }
- })
- }
- // TestETagMatching tests the etagMatches helper function
- func TestETagMatching(t *testing.T) {
- s3a := NewS3ApiServerForTest()
- if s3a == nil {
- t.Skip("S3ApiServer not available for testing")
- }
- testCases := []struct {
- name string
- headerValue string
- objectETag string
- expected bool
- }{
- {
- name: "ExactMatch",
- headerValue: "\"abc123\"",
- objectETag: "abc123",
- expected: true,
- },
- {
- name: "ExactMatchWithQuotes",
- headerValue: "\"abc123\"",
- objectETag: "\"abc123\"",
- expected: true,
- },
- {
- name: "NoMatch",
- headerValue: "\"abc123\"",
- objectETag: "def456",
- expected: false,
- },
- {
- name: "MultipleETags_FirstMatch",
- headerValue: "\"abc123\", \"def456\"",
- objectETag: "abc123",
- expected: true,
- },
- {
- name: "MultipleETags_SecondMatch",
- headerValue: "\"abc123\", \"def456\"",
- objectETag: "def456",
- expected: true,
- },
- {
- name: "MultipleETags_NoMatch",
- headerValue: "\"abc123\", \"def456\"",
- objectETag: "ghi789",
- expected: false,
- },
- {
- name: "WithSpaces",
- headerValue: " \"abc123\" , \"def456\" ",
- objectETag: "def456",
- expected: true,
- },
- }
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- result := s3a.etagMatches(tc.headerValue, tc.objectETag)
- if result != tc.expected {
- t.Errorf("Expected %v, got %v for headerValue='%s', objectETag='%s'",
- tc.expected, result, tc.headerValue, tc.objectETag)
- }
- })
- }
- }
- // TestConditionalHeadersIntegration tests conditional headers with full integration
- func TestConditionalHeadersIntegration(t *testing.T) {
- // This would be a full integration test that requires a running SeaweedFS instance
- t.Skip("Integration test - requires running SeaweedFS instance")
- }
- // createTestPutRequest creates a test HTTP PUT request
- func createTestPutRequest(bucket, object, content string) *http.Request {
- req, _ := http.NewRequest("PUT", "/"+bucket+object, bytes.NewReader([]byte(content)))
- req.Header.Set("Content-Type", "application/octet-stream")
- // Set up mux vars to simulate the bucket and object extraction
- // In real tests, this would be handled by the gorilla mux router
- return req
- }
- // NewS3ApiServerForTest creates a minimal S3ApiServer for testing
- // Note: This is a simplified version for unit testing conditional logic
- func NewS3ApiServerForTest() *S3ApiServer {
- // In a real test environment, this would set up a proper S3ApiServer
- // with filer connection, etc. For unit testing conditional header logic,
- // we create a minimal instance
- return &S3ApiServer{
- option: &S3ApiServerOption{
- BucketsPath: "/buckets",
- },
- }
- }
- // MockEntryGetter implements the simplified EntryGetter interface for testing
- // Only mocks the data access dependency - tests use production getObjectETag and etagMatches
- type MockEntryGetter struct {
- mockEntry *filer_pb.Entry
- }
- // Implement only the simplified EntryGetter interface
- func (m *MockEntryGetter) getEntry(parentDirectoryPath, entryName string) (*filer_pb.Entry, error) {
- if m.mockEntry != nil {
- return m.mockEntry, nil
- }
- return nil, filer_pb.ErrNotFound
- }
- // createMockEntryGetter creates a mock EntryGetter for testing
- func createMockEntryGetter(mockEntry *filer_pb.Entry) *MockEntryGetter {
- return &MockEntryGetter{
- mockEntry: mockEntry,
- }
- }
- // TestConditionalHeadersMultipartUpload tests conditional headers with multipart uploads
- // This verifies AWS S3 compatibility where conditional headers only apply to CompleteMultipartUpload
- func TestConditionalHeadersMultipartUpload(t *testing.T) {
- bucket := "test-bucket"
- object := "/test-multipart-object"
- // Mock existing object to test conditional headers against
- existingObject := &filer_pb.Entry{
- Name: "test-multipart-object",
- Extended: map[string][]byte{
- s3_constants.ExtETagKey: []byte("\"existing123\""),
- },
- Attributes: &filer_pb.FuseAttributes{
- Mtime: time.Date(2024, 6, 15, 12, 0, 0, 0, time.UTC).Unix(),
- FileSize: 2048,
- },
- Chunks: []*filer_pb.FileChunk{
- {
- FileId: "existing-file-id",
- Offset: 0,
- Size: 2048,
- },
- },
- }
- // Test CompleteMultipartUpload with If-None-Match: * (should fail when object exists)
- t.Run("CompleteMultipartUpload_IfNoneMatchAsterisk_ObjectExists_ShouldFail", func(t *testing.T) {
- getter := createMockEntryGetter(existingObject)
- // Create a mock CompleteMultipartUpload request with If-None-Match: *
- req := &http.Request{
- Method: "POST",
- Header: make(http.Header),
- URL: &url.URL{
- RawQuery: "uploadId=test-upload-id",
- },
- }
- req.Header.Set(s3_constants.IfNoneMatch, "*")
- s3a := NewS3ApiServerForTest()
- errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
- if errCode != s3err.ErrPreconditionFailed {
- t.Errorf("Expected ErrPreconditionFailed when object exists with If-None-Match=*, got %v", errCode)
- }
- })
- // Test CompleteMultipartUpload with If-None-Match: * (should succeed when object doesn't exist)
- t.Run("CompleteMultipartUpload_IfNoneMatchAsterisk_ObjectNotExists_ShouldSucceed", func(t *testing.T) {
- getter := createMockEntryGetter(nil) // No existing object
- req := &http.Request{
- Method: "POST",
- Header: make(http.Header),
- URL: &url.URL{
- RawQuery: "uploadId=test-upload-id",
- },
- }
- req.Header.Set(s3_constants.IfNoneMatch, "*")
- s3a := NewS3ApiServerForTest()
- errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
- if errCode != s3err.ErrNone {
- t.Errorf("Expected ErrNone when object doesn't exist with If-None-Match=*, got %v", errCode)
- }
- })
- // Test CompleteMultipartUpload with If-Match (should succeed when ETag matches)
- t.Run("CompleteMultipartUpload_IfMatch_ETagMatches_ShouldSucceed", func(t *testing.T) {
- getter := createMockEntryGetter(existingObject)
- req := &http.Request{
- Method: "POST",
- Header: make(http.Header),
- URL: &url.URL{
- RawQuery: "uploadId=test-upload-id",
- },
- }
- req.Header.Set(s3_constants.IfMatch, "\"existing123\"")
- s3a := NewS3ApiServerForTest()
- errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
- if errCode != s3err.ErrNone {
- t.Errorf("Expected ErrNone when ETag matches, got %v", errCode)
- }
- })
- // Test CompleteMultipartUpload with If-Match (should fail when object doesn't exist)
- t.Run("CompleteMultipartUpload_IfMatch_ObjectNotExists_ShouldFail", func(t *testing.T) {
- getter := createMockEntryGetter(nil) // No existing object
- req := &http.Request{
- Method: "POST",
- Header: make(http.Header),
- URL: &url.URL{
- RawQuery: "uploadId=test-upload-id",
- },
- }
- req.Header.Set(s3_constants.IfMatch, "\"any-etag\"")
- s3a := NewS3ApiServerForTest()
- errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
- if errCode != s3err.ErrPreconditionFailed {
- t.Errorf("Expected ErrPreconditionFailed when object doesn't exist with If-Match, got %v", errCode)
- }
- })
- // Test CompleteMultipartUpload with If-Match wildcard (should succeed when object exists)
- t.Run("CompleteMultipartUpload_IfMatchWildcard_ObjectExists_ShouldSucceed", func(t *testing.T) {
- getter := createMockEntryGetter(existingObject)
- req := &http.Request{
- Method: "POST",
- Header: make(http.Header),
- URL: &url.URL{
- RawQuery: "uploadId=test-upload-id",
- },
- }
- req.Header.Set(s3_constants.IfMatch, "*")
- s3a := NewS3ApiServerForTest()
- errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
- if errCode != s3err.ErrNone {
- t.Errorf("Expected ErrNone when object exists with If-Match=*, got %v", errCode)
- }
- })
- }
|