Skip to content

Commit 274d2d4

Browse files
committed
test(s3interop): edge cases range tail pagination special keys concurrent multipart
1 parent 66dc68e commit 274d2d4

1 file changed

Lines changed: 250 additions & 0 deletions

File tree

tools/s3interop/main.go

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
559809
func 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

Comments
 (0)