@ -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