|
|
|
@ -2,6 +2,7 @@ package s3api |
|
|
|
|
|
|
|
import ( |
|
|
|
"bytes" |
|
|
|
"encoding/hex" |
|
|
|
"fmt" |
|
|
|
"net/http" |
|
|
|
"net/url" |
|
|
|
@ -671,6 +672,86 @@ func TestETagMatching(t *testing.T) { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// TestGetObjectETagWithMd5AndChunks tests the fix for issue #7274
|
|
|
|
// When an object has both Attributes.Md5 and multiple chunks, getObjectETag should
|
|
|
|
// prefer Attributes.Md5 to match the behavior of HeadObject and filer.ETag
|
|
|
|
func TestGetObjectETagWithMd5AndChunks(t *testing.T) { |
|
|
|
s3a := NewS3ApiServerForTest() |
|
|
|
if s3a == nil { |
|
|
|
t.Skip("S3ApiServer not available for testing") |
|
|
|
} |
|
|
|
|
|
|
|
// Create an object with both Md5 and multiple chunks (like in issue #7274)
|
|
|
|
// Md5: ZjcmMwrCVGNVgb4HoqHe9g== (base64) = 663726330ac254635581be07a2a1def6 (hex)
|
|
|
|
md5HexString := "663726330ac254635581be07a2a1def6" |
|
|
|
md5Bytes, err := hex.DecodeString(md5HexString) |
|
|
|
if err != nil { |
|
|
|
t.Fatalf("failed to decode md5 hex string: %v", err) |
|
|
|
} |
|
|
|
|
|
|
|
entry := &filer_pb.Entry{ |
|
|
|
Name: "test-multipart-object", |
|
|
|
Attributes: &filer_pb.FuseAttributes{ |
|
|
|
Mtime: time.Now().Unix(), |
|
|
|
FileSize: 5597744, |
|
|
|
Md5: md5Bytes, |
|
|
|
}, |
|
|
|
// Two chunks - if we only used ETagChunks, it would return format "hash-2"
|
|
|
|
Chunks: []*filer_pb.FileChunk{ |
|
|
|
{ |
|
|
|
FileId: "chunk1", |
|
|
|
Offset: 0, |
|
|
|
Size: 4194304, |
|
|
|
ETag: "9+yCD2DGwMG5uKwAd+y04Q==", |
|
|
|
}, |
|
|
|
{ |
|
|
|
FileId: "chunk2", |
|
|
|
Offset: 4194304, |
|
|
|
Size: 1403440, |
|
|
|
ETag: "cs6SVSTgZ8W3IbIrAKmklg==", |
|
|
|
}, |
|
|
|
}, |
|
|
|
} |
|
|
|
|
|
|
|
// getObjectETag should return the Md5 in hex with quotes
|
|
|
|
expectedETag := "\"" + md5HexString + "\"" |
|
|
|
actualETag := s3a.getObjectETag(entry) |
|
|
|
|
|
|
|
if actualETag != expectedETag { |
|
|
|
t.Errorf("Expected ETag %s, got %s", expectedETag, actualETag) |
|
|
|
} |
|
|
|
|
|
|
|
// Now test that conditional headers work with this ETag
|
|
|
|
bucket := "test-bucket" |
|
|
|
object := "/test-object" |
|
|
|
|
|
|
|
// Test If-Match with the Md5-based ETag (should succeed)
|
|
|
|
t.Run("IfMatch_WithMd5BasedETag_ShouldSucceed", func(t *testing.T) { |
|
|
|
getter := createMockEntryGetter(entry) |
|
|
|
req := createTestGetRequest(bucket, object) |
|
|
|
// Client sends the ETag from HeadObject (without quotes)
|
|
|
|
req.Header.Set(s3_constants.IfMatch, md5HexString) |
|
|
|
|
|
|
|
result := s3a.checkConditionalHeadersForReadsWithGetter(getter, req, bucket, object) |
|
|
|
if result.ErrorCode != s3err.ErrNone { |
|
|
|
t.Errorf("Expected ErrNone when If-Match uses Md5-based ETag, got %v (ETag was %s)", result.ErrorCode, actualETag) |
|
|
|
} |
|
|
|
}) |
|
|
|
|
|
|
|
// Test If-Match with chunk-based ETag format (should fail - this was the old incorrect behavior)
|
|
|
|
t.Run("IfMatch_WithChunkBasedETag_ShouldFail", func(t *testing.T) { |
|
|
|
getter := createMockEntryGetter(entry) |
|
|
|
req := createTestGetRequest(bucket, object) |
|
|
|
// If we incorrectly calculated ETag from chunks, it would be in format "hash-2"
|
|
|
|
req.Header.Set(s3_constants.IfMatch, "123294de680f28bde364b81477549f7d-2") |
|
|
|
|
|
|
|
result := s3a.checkConditionalHeadersForReadsWithGetter(getter, req, bucket, object) |
|
|
|
if result.ErrorCode != s3err.ErrPreconditionFailed { |
|
|
|
t.Errorf("Expected ErrPreconditionFailed when If-Match uses chunk-based ETag format, got %v", result.ErrorCode) |
|
|
|
} |
|
|
|
}) |
|
|
|
} |
|
|
|
|
|
|
|
// 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
|
|
|
|
|