@@ -261,6 +261,7 @@ func main() {
261261
262262 runSSETests (ctx , cli , bucket , & fails , step )
263263 runNewFeatureTests (ctx , cli , bucket , ak , sk , endpoint , & fails , step )
264+ runEdgeCaseTests (ctx , cli , bucket , & fails , step )
264265
265266 if fails > 0 {
266267 fmt .Printf ("\n %d FAIL\n " , fails )
@@ -556,6 +557,255 @@ func runNewFeatureTests(ctx context.Context, cli *s3.Client, bucket, ak, sk, end
556557 _ , _ = cli .DeleteObject (ctx , & s3.DeleteObjectInput {Bucket : & bucket , Key : & tagKey })
557558}
558559
560+ func runEdgeCaseTests (ctx context.Context , cli * s3.Client , bucket string , fails * int , step func (string , error , string )) {
561+ // Range tail (bytes=-N)
562+ tailKey := "edge/tail.bin"
563+ tailBody := []byte ("abcdefghijklmnop" )
564+ _ , _ = cli .PutObject (ctx , & s3.PutObjectInput {Bucket : & bucket , Key : & tailKey , Body : bytes .NewReader (tailBody )})
565+ rng := "bytes=-4"
566+ gr , err := cli .GetObject (ctx , & s3.GetObjectInput {Bucket : & bucket , Key : & tailKey , Range : & rng })
567+ if err == nil {
568+ b , _ := io .ReadAll (gr .Body )
569+ _ = gr .Body .Close ()
570+ if string (b ) == "mnop" {
571+ step ("Range(tail bytes=-4)" , nil , fmt .Sprintf ("got=%q" , b ))
572+ } else {
573+ step ("Range(tail bytes=-4)" , fmt .Errorf ("got %q want mnop" , b ), "" )
574+ }
575+ } else {
576+ step ("Range(tail bytes=-4)" , err , "" )
577+ }
578+
579+ // Range out-of-bounds → 416
580+ rngBad := "bytes=9999-99999"
581+ _ , err = cli .GetObject (ctx , & s3.GetObjectInput {Bucket : & bucket , Key : & tailKey , Range : & rngBad })
582+ if err != nil {
583+ step ("Range(out-of-bounds)" , nil , "416 as expected" )
584+ } else {
585+ step ("Range(out-of-bounds)" , fmt .Errorf ("accepted oob range" ), "" )
586+ }
587+
588+ // ListObjects v1 with delimiter
589+ for _ , k := range []string {"docs/a" , "docs/b" , "img/c" } {
590+ _ , _ = cli .PutObject (ctx , & s3.PutObjectInput {Bucket : & bucket , Key : aws .String ("edge/" + k ), Body : bytes .NewReader ([]byte ("x" ))})
591+ }
592+ lod , err := cli .ListObjects (ctx , & s3.ListObjectsInput {Bucket : & bucket , Prefix : aws .String ("edge/" ), Delimiter : aws .String ("/" )})
593+ if err == nil {
594+ step ("ListObjects v1(delimiter)" , nil , fmt .Sprintf ("contents=%d commonPrefixes=%d" , len (lod .Contents ), len (lod .CommonPrefixes )))
595+ } else {
596+ step ("ListObjects v1(delimiter)" , err , "" )
597+ }
598+
599+ // Delete versioned object → delete-marker, ListObjectVersions reflects it
600+ vKey := "edge/versioned.bin"
601+ _ , _ = cli .PutObject (ctx , & s3.PutObjectInput {Bucket : & bucket , Key : & vKey , Body : bytes .NewReader ([]byte ("v1" ))})
602+ _ , _ = cli .PutObject (ctx , & s3.PutObjectInput {Bucket : & bucket , Key : & vKey , Body : bytes .NewReader ([]byte ("v2" ))})
603+ _ , _ = cli .DeleteObject (ctx , & s3.DeleteObjectInput {Bucket : & bucket , Key : & vKey })
604+ lv , err := cli .ListObjectVersions (ctx , & s3.ListObjectVersionsInput {Bucket : & bucket , Prefix : & vKey })
605+ if err == nil {
606+ if len (lv .Versions ) == 2 && len (lv .DeleteMarkers ) == 1 && aws .ToBool (lv .DeleteMarkers [0 ].IsLatest ) {
607+ step ("Delete-marker after Delete" , nil , fmt .Sprintf ("v=%d dm=%d" , len (lv .Versions ), len (lv .DeleteMarkers )))
608+ } else {
609+ step ("Delete-marker after Delete" , fmt .Errorf ("v=%d dm=%d latest=%v" , len (lv .Versions ), len (lv .DeleteMarkers ), len (lv .DeleteMarkers ) > 0 && aws .ToBool (lv .DeleteMarkers [0 ].IsLatest )), "" )
610+ }
611+ } else {
612+ step ("Delete-marker after Delete" , err , "" )
613+ }
614+
615+ // GetObject on deleted versioned object → 404 + x-amz-delete-marker
616+ _ , err = cli .GetObject (ctx , & s3.GetObjectInput {Bucket : & bucket , Key : & vKey })
617+ if err != nil {
618+ step ("Get(deleted versioned)" , nil , "404 as expected" )
619+ } else {
620+ step ("Get(deleted versioned)" , fmt .Errorf ("accepted" ), "" )
621+ }
622+
623+ // Restore previous version via versionId
624+ var prevVID string
625+ for _ , v := range lv .Versions {
626+ if ! aws .ToBool (v .IsLatest ) {
627+ prevVID = aws .ToString (v .VersionId )
628+ break
629+ }
630+ }
631+ if prevVID == "" && len (lv .Versions ) > 0 {
632+ prevVID = aws .ToString (lv .Versions [0 ].VersionId )
633+ }
634+ if prevVID != "" {
635+ gv , gerr := cli .GetObject (ctx , & s3.GetObjectInput {Bucket : & bucket , Key : & vKey , VersionId : & prevVID })
636+ if gerr == nil {
637+ b , _ := io .ReadAll (gv .Body )
638+ _ = gv .Body .Close ()
639+ step ("Get(versionId)" , nil , fmt .Sprintf ("body=%q" , b ))
640+ } else {
641+ step ("Get(versionId)" , gerr , "" )
642+ }
643+ }
644+
645+ // Conditional GET If-Modified-Since past → 200
646+ ifKey := "edge/cond.bin"
647+ _ , _ = cli .PutObject (ctx , & s3.PutObjectInput {Bucket : & bucket , Key : & ifKey , Body : bytes .NewReader ([]byte ("body" ))})
648+ past := time .Date (2000 , 1 , 1 , 0 , 0 , 0 , 0 , time .UTC )
649+ _ , err = cli .GetObject (ctx , & s3.GetObjectInput {Bucket : & bucket , Key : & ifKey , IfModifiedSince : & past })
650+ step ("If-Modified-Since past → 200" , err , "" )
651+ future := time .Now ().Add (24 * time .Hour )
652+ _ , err = cli .GetObject (ctx , & s3.GetObjectInput {Bucket : & bucket , Key : & ifKey , IfModifiedSince : & future })
653+ if err != nil {
654+ step ("If-Modified-Since future → 304" , nil , "304 as expected" )
655+ } else {
656+ step ("If-Modified-Since future → 304" , fmt .Errorf ("accepted" ), "" )
657+ }
658+
659+ // Concurrent writes — 8 PUTs same key, last write wins
660+ concKey := "edge/concurrent.bin"
661+ done := make (chan error , 8 )
662+ for i := 0 ; i < 8 ; i ++ {
663+ i := i
664+ go func () {
665+ payload := []byte (fmt .Sprintf ("writer-%d" , i ))
666+ _ , e := cli .PutObject (ctx , & s3.PutObjectInput {Bucket : & bucket , Key : & concKey , Body : bytes .NewReader (payload )})
667+ done <- e
668+ }()
669+ }
670+ concFails := 0
671+ for i := 0 ; i < 8 ; i ++ {
672+ if e := <- done ; e != nil {
673+ concFails ++
674+ }
675+ }
676+ if concFails == 0 {
677+ step ("Concurrent PUTs x8" , nil , "all succeeded" )
678+ } else {
679+ step ("Concurrent PUTs x8" , fmt .Errorf ("%d errors" , concFails ), "" )
680+ }
681+ hr , err := cli .HeadObject (ctx , & s3.HeadObjectInput {Bucket : & bucket , Key : & concKey })
682+ if err == nil {
683+ step ("HeadObject(after concurrent)" , nil , fmt .Sprintf ("size=%d" , aws .ToInt64 (hr .ContentLength )))
684+ } else {
685+ step ("HeadObject(after concurrent)" , err , "" )
686+ }
687+
688+ // Empty body PUT/GET
689+ emptyKey := "edge/empty.bin"
690+ _ , err = cli .PutObject (ctx , & s3.PutObjectInput {Bucket : & bucket , Key : & emptyKey , Body : bytes .NewReader (nil )})
691+ step ("PutObject(empty)" , err , "" )
692+ hg , err := cli .HeadObject (ctx , & s3.HeadObjectInput {Bucket : & bucket , Key : & emptyKey })
693+ if err == nil && aws .ToInt64 (hg .ContentLength ) == 0 {
694+ step ("HeadObject(empty)" , nil , "size=0" )
695+ } else {
696+ step ("HeadObject(empty)" , fmt .Errorf ("size mismatch err=%v size=%d" , err , aws .ToInt64 (hg .ContentLength )), "" )
697+ }
698+
699+ // Missing key → 404
700+ _ , err = cli .HeadObject (ctx , & s3.HeadObjectInput {Bucket : & bucket , Key : aws .String ("edge/does-not-exist" )})
701+ if err != nil {
702+ step ("HeadObject(missing)" , nil , "404 as expected" )
703+ } else {
704+ step ("HeadObject(missing)" , fmt .Errorf ("accepted" ), "" )
705+ }
706+
707+ // Special chars in key: space, unicode, deep slashes
708+ specialKeys := []string {
709+ "edge/with space.txt" ,
710+ "edge/déjà-vu/café.txt" ,
711+ "edge/a/b/c/d/e/f/g/leaf.bin" ,
712+ "edge/percent%20encoded.txt" ,
713+ }
714+ for _ , sk := range specialKeys {
715+ _ , e := cli .PutObject (ctx , & s3.PutObjectInput {Bucket : & bucket , Key : aws .String (sk ), Body : bytes .NewReader ([]byte ("v" ))})
716+ if e != nil {
717+ step ("PutObject(" + sk + ")" , e , "" )
718+ continue
719+ }
720+ g , e := cli .GetObject (ctx , & s3.GetObjectInput {Bucket : & bucket , Key : aws .String (sk )})
721+ if e != nil {
722+ step ("GetObject(" + sk + ")" , e , "" )
723+ continue
724+ }
725+ b , _ := io .ReadAll (g .Body )
726+ _ = g .Body .Close ()
727+ if string (b ) != "v" {
728+ step ("GetObject(" + sk + ")" , fmt .Errorf ("body mismatch" ), "" )
729+ } else {
730+ step ("Round-trip(" + sk + ")" , nil , "" )
731+ }
732+ }
733+
734+ // Pagination: 1200 keys, V2 with max-keys=100
735+ for i := 0 ; i < 1200 ; i ++ {
736+ _ , _ = cli .PutObject (ctx , & s3.PutObjectInput {Bucket : & bucket , Key : aws .String (fmt .Sprintf ("page/k-%04d" , i )), Body : bytes .NewReader ([]byte ("x" ))})
737+ }
738+ var token * string
739+ total := 0
740+ pages := 0
741+ for {
742+ lo , perr := cli .ListObjectsV2 (ctx , & s3.ListObjectsV2Input {
743+ Bucket : & bucket , Prefix : aws .String ("page/" ),
744+ MaxKeys : aws .Int32 (100 ), ContinuationToken : token ,
745+ })
746+ if perr != nil {
747+ step ("ListObjectsV2(paginate)" , perr , "" )
748+ break
749+ }
750+ total += len (lo .Contents )
751+ pages ++
752+ if ! aws .ToBool (lo .IsTruncated ) {
753+ break
754+ }
755+ token = lo .NextContinuationToken
756+ if pages > 50 {
757+ step ("ListObjectsV2(paginate)" , fmt .Errorf ("too many pages" ), "" )
758+ break
759+ }
760+ }
761+ if total == 1200 {
762+ step ("ListObjectsV2(paginate 1200)" , nil , fmt .Sprintf ("pages=%d total=%d" , pages , total ))
763+ } else {
764+ step ("ListObjectsV2(paginate 1200)" , fmt .Errorf ("got %d total in %d pages" , total , pages ), "" )
765+ }
766+
767+ // DeleteObjects batch with mix of present + missing
768+ dl , err := cli .DeleteObjects (ctx , & s3.DeleteObjectsInput {
769+ Bucket : & bucket ,
770+ Delete : & types.Delete {
771+ Objects : []types.ObjectIdentifier {
772+ {Key : aws .String ("page/k-0000" )},
773+ {Key : aws .String ("page/k-0001" )},
774+ {Key : aws .String ("page/missing" )},
775+ },
776+ Quiet : aws .Bool (false ),
777+ },
778+ })
779+ if err == nil {
780+ step ("DeleteObjects(mixed)" , nil , fmt .Sprintf ("deleted=%d errors=%d" , len (dl .Deleted ), len (dl .Errors )))
781+ } else {
782+ step ("DeleteObjects(mixed)" , err , "" )
783+ }
784+
785+ // Big multipart 50MiB
786+ bigKey := "edge/big.bin"
787+ bigBody := make ([]byte , 50 * 1024 * 1024 )
788+ _ , _ = rand .Read (bigBody )
789+ uploader := manager .NewUploader (cli , func (u * manager.Uploader ) {
790+ u .PartSize = 8 * 1024 * 1024
791+ u .Concurrency = 4
792+ })
793+ _ , err = uploader .Upload (ctx , & s3.PutObjectInput {Bucket : & bucket , Key : & bigKey , Body : bytes .NewReader (bigBody )})
794+ step ("Multipart 50MiB" , err , "" )
795+ if err == nil {
796+ gg , gerr := cli .GetObject (ctx , & s3.GetObjectInput {Bucket : & bucket , Key : & bigKey })
797+ if gerr == nil {
798+ b , _ := io .ReadAll (gg .Body )
799+ _ = gg .Body .Close ()
800+ h1 := sha256 .Sum256 (bigBody )
801+ h2 := sha256 .Sum256 (b )
802+ step ("Get(50MiB) sha256" , nil , fmt .Sprintf ("len=%d ok=%v" , len (b ), h1 == h2 ))
803+ } else {
804+ step ("Get(50MiB)" , gerr , "" )
805+ }
806+ }
807+ }
808+
559809func rawHeadRaw (ctx context.Context , endpoint , bucket , key , ak , sk string ) {
560810 url := endpoint + "/" + bucket + "/" + key
561811 req , _ := http .NewRequestWithContext (ctx , "HEAD" , url , nil )
0 commit comments