Files
versitygw/tests/integration/versioning.go
T
niksis02 bca801bf3a fix: match S3 for conditional deletes of missing keys, delete markers and versions
A conditional delete of a key with no current version used to succeed. `DeleteObject` with `If-Match`, and a `DeleteObjects` key with an `ETag`, reported success for a missing key. When the current version was a delete marker, the condition was checked against the ETag the marker kept from the version it hides, so it could pass and create a second marker. S3 fails both with `NoSuchKey` and creates no marker. The posix backend now checks the conditions against the current version in `checkDeletePreconditions`, before its missing-key shortcuts. The azure backend returns `NoSuchKey` for a missing blob instead of skipping the conditions. `ObjectDeletePreconditions.IsSet` reports whether a delete has any condition.

A condition only applies to the current version. `DeleteObject` with a `versionId` and `If-Match` now returns `NotImplemented` with `<Header>If-Match</Header>` and the S3 additional message, ahead of authorization. Before, it checked the condition against that version and deleted the version. In `DeleteObjects`, an empty `<ETag>` fails its key with `InvalidArgument` and a `VersionId` together with an `ETag` fails it with `NotImplemented`, both ahead of the key's authorization.

The `PreconditionFailed` and `NotImplemented` messages drop their trailing period, which S3 doesn't use on any operation.
2026-10-10 11:43:19 +01:00

7960 lines
237 KiB
Go

// Copyright 2023 Versity Software
// This file is licensed under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package integration
import (
"context"
"crypto/sha256"
"errors"
"fmt"
"io"
"maps"
"net/http"
"net/url"
"strings"
"sync"
"time"
awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/versity/versitygw/s3err"
)
func Versioning_DeleteBucket_not_empty(s *S3Conf) error {
testName := "Versioning_DeleteBucket_not_empty"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
return forEachKey([]string{"my-obj", "my-dir/"}, func(obj string) error {
versions, err := createObjVersions(s3client, bucket, obj, 2)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.DeleteBucket(ctx, &s3.DeleteBucketInput{
Bucket: &bucket,
})
cancel()
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrVersionedBucketNotEmpty)); err != nil {
return err
}
// delete the key versions, so that the next key
// is the only one in the bucket
for _, version := range versions {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: &bucket,
Key: &obj,
VersionId: version.VersionId,
})
cancel()
if err != nil {
return err
}
}
return nil
})
}, withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_PutObject_suspended_null_versionId_obj(s *S3Conf) error {
testName := "Versioning_PutObject_suspended_null_versionId_obj"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
return forEachKey([]string{"my-obj", "my-dir/"}, func(obj string) error {
out, err := putObjectWithData(objDataLen(obj, 1222), &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
}, s3client)
if err != nil {
return err
}
if out.res.VersionId != nil {
return fmt.Errorf("expected PutObject response to omit versionId, instead got %v",
getString(out.res.VersionId))
}
return nil
})
}, withVersioning(types.BucketVersioningStatusSuspended))
}
func Versioning_PutObject_null_versionId_obj(s *S3Conf) error {
testName := "Versioning_PutObject_null_versionId_obj"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
keys := []string{"my-obj", "my-dir/"}
nullVersions := map[string]types.ObjectVersion{}
err := forEachKey(keys, func(obj string) error {
lgth := objDataLen(obj, 1234)
out, err := putObjectWithData(lgth, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
}, s3client)
if err != nil {
return err
}
nullVersions[obj] = types.ObjectVersion{
ETag: out.res.ETag,
IsLatest: getBoolPtr(false),
Key: &obj,
Size: &lgth,
VersionId: &nullVersionId,
StorageClass: types.ObjectVersionStorageClassStandard,
}
return nil
})
if err != nil {
return err
}
// Enable bucket versioning
err = putBucketVersioningStatus(s3client, bucket, types.BucketVersioningStatusEnabled)
if err != nil {
return err
}
return forEachKey(keys, func(obj string) error {
versions, err := createObjVersions(s3client, bucket, obj, 4)
if err != nil {
return err
}
versions = append(versions, nullVersions[obj])
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.ListObjectVersions(ctx, &s3.ListObjectVersionsInput{
Bucket: &bucket,
Prefix: &obj,
})
cancel()
if err != nil {
return err
}
if !compareVersions(versions, res.Versions) {
return fmt.Errorf("expected the listed versions to be %v, instead got %v",
versions, res.Versions)
}
return nil
})
})
}
func Versioning_PutObject_overwrite_null_versionId_obj(s *S3Conf) error {
testName := "Versioning_PutObject_overwrite_null_versionId_obj"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
keys := []string{"my-obj", "my-dir/"}
err := forEachKey(keys, func(obj string) error {
_, err := putObjectWithData(objDataLen(obj, 1233), &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
}, s3client)
return err
})
if err != nil {
return err
}
// Enable bucket versioning
err = putBucketVersioningStatus(s3client, bucket, types.BucketVersioningStatusEnabled)
if err != nil {
return err
}
objVersions := map[string][]types.ObjectVersion{}
err = forEachKey(keys, func(obj string) error {
versions, err := createObjVersions(s3client, bucket, obj, 4)
if err != nil {
return err
}
objVersions[obj] = versions
return nil
})
if err != nil {
return err
}
// Set bucket versioning status to Suspended
err = putBucketVersioningStatus(s3client, bucket, types.BucketVersioningStatusSuspended)
if err != nil {
return err
}
return forEachKey(keys, func(obj string) error {
lgth := objDataLen(obj, 3200)
out, err := putObjectWithData(lgth, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
}, s3client)
if err != nil {
return err
}
if out.res.VersionId != nil {
return fmt.Errorf("expected PutObject response to omit versionId, instead got %v",
getString(out.res.VersionId))
}
versions := objVersions[obj]
versions[0].IsLatest = getBoolPtr(false)
versions = append([]types.ObjectVersion{
{
ETag: out.res.ETag,
IsLatest: getBoolPtr(true),
Key: &obj,
Size: &lgth,
VersionId: &nullVersionId,
StorageClass: types.ObjectVersionStorageClassStandard,
ChecksumType: out.res.ChecksumType,
},
}, versions...)
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.ListObjectVersions(ctx, &s3.ListObjectVersionsInput{
Bucket: &bucket,
Prefix: &obj,
})
cancel()
if err != nil {
return err
}
if !compareVersions(versions, res.Versions) {
return fmt.Errorf("expected the listed versions to be %v, instead got %v",
versions, res.Versions)
}
return nil
})
})
}
func Versioning_PutObject_success(s *S3Conf) error {
testName := "Versioning_PutObject_success"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
return forEachKey([]string{"my-obj", "my-dir/"}, func(obj string) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.PutObject(ctx, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
})
cancel()
if err != nil {
return err
}
if res.VersionId == nil || *res.VersionId == "" {
return fmt.Errorf("expected the versionId to be returned")
}
return nil
})
}, withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_PutObject_dir_object_new_version_resets_attributes(s *S3Conf) error {
testName := "Versioning_PutObject_dir_object_new_version_resets_attributes"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
obj := "my-dir/"
tags := []types.Tag{
{Key: getPtr("key1"), Value: getPtr("val1")},
{Key: getPtr("key2"), Value: getPtr("val2")},
}
meta := map[string]string{
"foo": "bar",
"baz": "quxx",
}
redirect := "/some/redirect"
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
first, err := s3client.PutObject(ctx, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
Tagging: getPtr("key1=val1&key2=val2"),
Metadata: meta,
WebsiteRedirectLocation: &redirect,
})
cancel()
if err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
second, err := s3client.PutObject(ctx, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
})
cancel()
if err != nil {
return err
}
if getString(first.VersionId) == "" || getString(second.VersionId) == "" {
return fmt.Errorf("expected non empty versionIds, instead got %q and %q",
getString(first.VersionId), getString(second.VersionId))
}
if *first.VersionId == *second.VersionId {
return fmt.Errorf("expected a new versionId, instead got %v twice", *first.VersionId)
}
// the current version holds none of the first version attributes
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
cur, err := s3client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: &bucket,
Key: &obj,
})
cancel()
if err != nil {
return err
}
if getString(cur.VersionId) != *second.VersionId {
return fmt.Errorf("expected the current versionId to be %v, instead got %v",
*second.VersionId, getString(cur.VersionId))
}
if len(cur.Metadata) != 0 {
return fmt.Errorf("expected empty metadata, instead got %v", cur.Metadata)
}
if cur.WebsiteRedirectLocation != nil {
return fmt.Errorf("expected nil website-redirect-location, instead got %v",
*cur.WebsiteRedirectLocation)
}
if cur.TagCount != nil {
return fmt.Errorf("expected nil tag count, instead got %v", *cur.TagCount)
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
curTags, err := s3client.GetObjectTagging(ctx, &s3.GetObjectTaggingInput{
Bucket: &bucket,
Key: &obj,
})
cancel()
if err != nil {
return err
}
if len(curTags.TagSet) != 0 {
return fmt.Errorf("expected empty tag set, instead got %v", curTags.TagSet)
}
// the first version keeps its attributes
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
old, err := s3client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: &bucket,
Key: &obj,
VersionId: first.VersionId,
})
cancel()
if err != nil {
return err
}
if getString(old.VersionId) != *first.VersionId {
return fmt.Errorf("expected the versionId to be %v, instead got %v",
*first.VersionId, getString(old.VersionId))
}
if old.ContentLength == nil {
return fmt.Errorf("expected non nil ContentLength")
}
if *old.ContentLength != 0 {
return fmt.Errorf("expected zero content-length, instead got %v",
*old.ContentLength)
}
if getString(old.ContentType) != directoryContentType {
return fmt.Errorf("expected the content-type to be %v, instead got %v",
directoryContentType, getString(old.ContentType))
}
if !areMapsSame(meta, old.Metadata) {
return fmt.Errorf("expected the metadata to be %v, instead got %v",
meta, old.Metadata)
}
if getString(old.WebsiteRedirectLocation) != redirect {
return fmt.Errorf("expected the website-redirect-location to be %v, instead got %v",
redirect, getString(old.WebsiteRedirectLocation))
}
if old.TagCount == nil {
return fmt.Errorf("expected non nil TagCount")
}
if *old.TagCount != int32(len(tags)) {
return fmt.Errorf("expected the tag count to be %v, instead got %v",
len(tags), *old.TagCount)
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
oldTags, err := s3client.GetObjectTagging(ctx, &s3.GetObjectTaggingInput{
Bucket: &bucket,
Key: &obj,
VersionId: first.VersionId,
})
cancel()
if err != nil {
return err
}
if !areTagsSame(tags, oldTags.TagSet) {
return fmt.Errorf("expected the tag set to be %v, instead got %v",
tags, oldTags.TagSet)
}
return nil
}, withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_PutObject_new_version_resets_attributes(s *S3Conf) error {
testName := "Versioning_PutObject_new_version_resets_attributes"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
obj := "my-obj"
meta := map[string]string{"foo": "bar"}
tags := []types.Tag{{Key: getPtr("key"), Value: getPtr("val")}}
first, err := putObjectWithData(10, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
ContentType: getPtr("text/plain"),
ContentEncoding: getPtr("gzip"),
CacheControl: getPtr("no-cache"),
Metadata: meta,
Tagging: getPtr("key=val"),
}, s3client)
if err != nil {
return err
}
second, err := putObjectWithData(20, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
}, s3client)
if err != nil {
return err
}
// the current version holds none of the first version attributes
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
cur, err := s3client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: &bucket,
Key: &obj,
})
cancel()
if err != nil {
return err
}
if getString(cur.VersionId) != getString(second.res.VersionId) {
return fmt.Errorf("expected the current versionId to be %v, instead got %v",
getString(second.res.VersionId), getString(cur.VersionId))
}
if getString(cur.ContentType) != defaultContentType {
return fmt.Errorf("expected default %s Content-Type, instead got %s",
defaultContentType, getString(cur.ContentType))
}
if cur.ContentEncoding != nil {
return fmt.Errorf("expected nil Content-Encoding, instead got %v", *cur.ContentEncoding)
}
if cur.CacheControl != nil {
return fmt.Errorf("expected nil Cache-Control, instead got %v", *cur.CacheControl)
}
if len(cur.Metadata) != 0 {
return fmt.Errorf("expected empty metadata, instead got %v", cur.Metadata)
}
if cur.TagCount != nil {
return fmt.Errorf("expected nil tag count, instead got %v", *cur.TagCount)
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
curTags, err := s3client.GetObjectTagging(ctx, &s3.GetObjectTaggingInput{
Bucket: &bucket,
Key: &obj,
})
cancel()
if err != nil {
return err
}
if len(curTags.TagSet) != 0 {
return fmt.Errorf("expected empty tag set, instead got %v", curTags.TagSet)
}
// the first version keeps its attributes
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
old, err := s3client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: &bucket,
Key: &obj,
VersionId: first.res.VersionId,
})
cancel()
if err != nil {
return err
}
if getString(old.ContentType) != "text/plain" {
return fmt.Errorf("expected the content-type to be text/plain, instead got %v",
getString(old.ContentType))
}
if getString(old.ContentEncoding) != "gzip" {
return fmt.Errorf("expected the content-encoding to be gzip, instead got %v",
getString(old.ContentEncoding))
}
if !areMapsSame(meta, old.Metadata) {
return fmt.Errorf("expected the metadata to be %v, instead got %v",
meta, old.Metadata)
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
oldTags, err := s3client.GetObjectTagging(ctx, &s3.GetObjectTaggingInput{
Bucket: &bucket,
Key: &obj,
VersionId: first.res.VersionId,
})
cancel()
if err != nil {
return err
}
if !areTagsSame(tags, oldTags.TagSet) {
return fmt.Errorf("expected the tag set to be %v, instead got %v",
tags, oldTags.TagSet)
}
return nil
}, withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_PutObject_over_delete_marker_resets_attributes(s *S3Conf) error {
testName := "Versioning_PutObject_over_delete_marker_resets_attributes"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
obj := "my-obj"
_, err := putObjectWithData(10, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
ContentType: getPtr("text/plain"),
ContentEncoding: getPtr("gzip"),
CacheControl: getPtr("no-cache"),
Metadata: map[string]string{"foo": "bar"},
Tagging: getPtr("key=val"),
}, s3client)
if err != nil {
return err
}
_, err = createDeleteMarker(s3client, bucket, obj)
if err != nil {
return err
}
res, err := putObjectWithData(20, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
}, s3client)
if err != nil {
return err
}
// the new version holds none of the attributes of the version
// under the delete marker
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
cur, err := s3client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: &bucket,
Key: &obj,
})
cancel()
if err != nil {
return err
}
if getString(cur.VersionId) != getString(res.res.VersionId) {
return fmt.Errorf("expected the current versionId to be %v, instead got %v",
getString(res.res.VersionId), getString(cur.VersionId))
}
if getString(cur.ContentType) != defaultContentType {
return fmt.Errorf("expected default %s Content-Type, instead got %s",
defaultContentType, getString(cur.ContentType))
}
if cur.ContentEncoding != nil {
return fmt.Errorf("expected nil Content-Encoding, instead got %v", *cur.ContentEncoding)
}
if cur.CacheControl != nil {
return fmt.Errorf("expected nil Cache-Control, instead got %v", *cur.CacheControl)
}
if len(cur.Metadata) != 0 {
return fmt.Errorf("expected empty metadata, instead got %v", cur.Metadata)
}
if cur.TagCount != nil {
return fmt.Errorf("expected nil tag count, instead got %v", *cur.TagCount)
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
curTags, err := s3client.GetObjectTagging(ctx, &s3.GetObjectTaggingInput{
Bucket: &bucket,
Key: &obj,
})
cancel()
if err != nil {
return err
}
if len(curTags.TagSet) != 0 {
return fmt.Errorf("expected empty tag set, instead got %v", curTags.TagSet)
}
return nil
}, withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_PutObject_suspended_null_version_resets_attributes(s *S3Conf) error {
testName := "Versioning_PutObject_suspended_null_version_resets_attributes"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
obj := "my-obj"
_, err := putObjectWithData(10, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
ContentType: getPtr("text/plain"),
ContentEncoding: getPtr("gzip"),
CacheControl: getPtr("no-cache"),
Metadata: map[string]string{"foo": "bar"},
Tagging: getPtr("key=val"),
}, s3client)
if err != nil {
return err
}
// the null version is replaced by the new null version
_, err = putObjectWithData(20, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
}, s3client)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
cur, err := s3client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: &bucket,
Key: &obj,
})
cancel()
if err != nil {
return err
}
if getString(cur.VersionId) != nullVersionId {
return fmt.Errorf("expected the current versionId to be %v, instead got %v",
nullVersionId, getString(cur.VersionId))
}
if getString(cur.ContentType) != defaultContentType {
return fmt.Errorf("expected default %s Content-Type, instead got %s",
defaultContentType, getString(cur.ContentType))
}
if cur.ContentEncoding != nil {
return fmt.Errorf("expected nil Content-Encoding, instead got %v", *cur.ContentEncoding)
}
if cur.CacheControl != nil {
return fmt.Errorf("expected nil Cache-Control, instead got %v", *cur.CacheControl)
}
if len(cur.Metadata) != 0 {
return fmt.Errorf("expected empty metadata, instead got %v", cur.Metadata)
}
if cur.TagCount != nil {
return fmt.Errorf("expected nil tag count, instead got %v", *cur.TagCount)
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
curTags, err := s3client.GetObjectTagging(ctx, &s3.GetObjectTaggingInput{
Bucket: &bucket,
Key: &obj,
})
cancel()
if err != nil {
return err
}
if len(curTags.TagSet) != 0 {
return fmt.Errorf("expected empty tag set, instead got %v", curTags.TagSet)
}
return nil
}, withVersioning(types.BucketVersioningStatusSuspended))
}
// A delete marker as the current version leaves the key without an object,
// although the version it hides still has an ETag: If-Match finds no key,
// while If-None-Match lets the upload through as a new version.
func Versioning_PutObject_conditional_writes_over_delete_marker(s *S3Conf) error {
testName := "Versioning_PutObject_conditional_writes_over_delete_marker"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
return forEachKey([]string{"my-obj", "my-dir/"}, func(obj string) error {
versions, err := createObjVersions(s3client, bucket, obj, 1)
if err != nil {
return err
}
if _, err := createDeleteMarker(s3client, bucket, obj); err != nil {
return err
}
_, err = putObjectWithData(objDataLen(obj, 5), &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
IfMatch: versions[0].ETag,
}, s3client)
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrNoSuchKey)); err != nil {
return err
}
res, err := putObjectWithData(objDataLen(obj, 5), &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
IfNoneMatch: getPtr("*"),
}, s3client)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
head, err := s3client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: &bucket,
Key: &obj,
})
cancel()
if err != nil {
return err
}
if getString(head.VersionId) != getString(res.res.VersionId) {
return fmt.Errorf("expected the current version to be %v, instead got %v",
getString(res.res.VersionId), getString(head.VersionId))
}
return nil
})
}, withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_CopyObject_invalid_versionId(s *S3Conf) error {
testName := "Versioning_CopyObject_invalid_versionId"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
dstObj, srcObj := "dst-obj", "src-obj"
srcObjLen := int64(2345)
_, err := putObjectWithData(srcObjLen, &s3.PutObjectInput{
Bucket: &bucket,
Key: &srcObj,
}, s3client)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: &bucket,
Key: &dstObj,
CopySource: getPtr(fmt.Sprintf("%v/%v?versionId=invalid_versionId", bucket, srcObj)),
})
cancel()
return checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgVersionId, "invalid_versionId"))
}, withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_CopyObject_encoded_versionid_separator_invalid_versionId(s *S3Conf) error {
testName := "Versioning_CopyObject_encoded_versionid_separator_invalid_versionId"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
dstObj, srcObj := "dst-obj", "src-obj"
srcObjLen := int64(2345)
_, err := putObjectWithData(srcObjLen, &s3.PutObjectInput{
Bucket: &bucket,
Key: &srcObj,
}, s3client)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: &bucket,
Key: &dstObj,
CopySource: getPtr(fmt.Sprintf("%v/%v%%3FversionId%%3D..%%2f..%%2fsecret.txt", bucket, srcObj)),
})
cancel()
return checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgVersionId, "../../secret.txt"))
}, withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_CopyObject_success(s *S3Conf) error {
testName := "Versioning_CopyObject_success"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
srcBucket := getBucketName()
if err := setup(s, srcBucket); err != nil {
return err
}
srcObjLens := map[string]int64{
"src-obj": 2345,
"src-dir/": 0,
}
for srcObj, srcObjLen := range srcObjLens {
_, err := putObjectWithData(srcObjLen, &s3.PutObjectInput{
Bucket: &srcBucket,
Key: &srcObj,
}, s3client)
if err != nil {
return err
}
}
// destination object -> source object
srcObjs := map[string]string{
"dst-obj": "src-obj",
"dst-dir/": "src-dir/",
"dst-file": "src-dir/",
}
err := forEachKey([]string{"dst-obj", "dst-dir/", "dst-file"}, func(dstObj string) error {
srcObj := srcObjs[dstObj]
dstObjVersions, err := createObjVersions(s3client, bucket, dstObj, 1)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: &bucket,
Key: &dstObj,
CopySource: getPtr(fmt.Sprintf("%v/%v", srcBucket, srcObj)),
})
cancel()
if err != nil {
return err
}
if out.VersionId == nil || *out.VersionId == "" {
return fmt.Errorf("expected non empty versionId in the result")
}
srcObjLen := srcObjLens[srcObj]
dstObjVersions[0].IsLatest = getBoolPtr(false)
versions := append([]types.ObjectVersion{
{
ETag: out.CopyObjectResult.ETag,
IsLatest: getBoolPtr(true),
Key: &dstObj,
Size: &srcObjLen,
VersionId: out.VersionId,
StorageClass: types.ObjectVersionStorageClassStandard,
ChecksumType: out.CopyObjectResult.ChecksumType,
},
}, dstObjVersions...)
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.ListObjectVersions(ctx, &s3.ListObjectVersionsInput{
Bucket: &bucket,
Prefix: &dstObj,
})
cancel()
if err != nil {
return err
}
if !compareVersions(versions, res.Versions) {
return fmt.Errorf("expected the resulting versions to be %v, instead got %v",
versions, res.Versions)
}
return nil
})
if err != nil {
return err
}
return teardown(s, srcBucket)
}, withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_CopyObject_non_existing_version_id(s *S3Conf) error {
testName := "Versioning_CopyObject_non_existing_version_id"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
dstBucket := getBucketName()
if err := setup(s, dstBucket); err != nil {
return err
}
err := forEachKey([]string{"my-obj", "my-dir/"}, func(obj string) error {
_, err := createObjVersions(s3client, bucket, obj, 1)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: &dstBucket,
Key: &obj,
CopySource: getPtr(fmt.Sprintf("%v/%v?versionId=01BX5ZZKBKACTAV9WEVGEMMVRZ",
bucket, obj)),
})
cancel()
return checkApiErr(err, s3err.GetAPIError(s3err.ErrNoSuchVersion))
})
if err != nil {
return err
}
if err := teardown(s, dstBucket); err != nil {
return err
}
return nil
}, withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_CopyObject_from_an_object_version(s *S3Conf) error {
testName := "Versioning_CopyObject_from_an_object_version"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
srcBucket := getBucketName()
if err := setup(s, srcBucket, withVersioning(types.BucketVersioningStatusEnabled)); err != nil {
return err
}
// copy the noncurrent and then the current source object version
dstObjs := map[string]string{
"my-obj": "my-dst-obj",
"my-dir/": "my-dst-dir/",
}
err := forEachKey([]string{"my-obj", "my-dir/"}, func(srcObj string) error {
dstObj := dstObjs[srcObj]
srcObjVersions, err := createObjVersions(s3client, srcBucket, srcObj, 2)
if err != nil {
return err
}
for _, srcObjVersion := range reverseSlice(srcObjVersions) {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: &bucket,
Key: &dstObj,
CopySource: getPtr(fmt.Sprintf("%v/%v?versionId=%v", srcBucket, srcObj, *srcObjVersion.VersionId)),
})
cancel()
if err != nil {
return err
}
if out.VersionId == nil || *out.VersionId == "" {
return fmt.Errorf("expected non empty versionId")
}
if out.CopySourceVersionId == nil {
return fmt.Errorf("expected non nil CopySourceVersionId")
}
if *out.CopySourceVersionId != *srcObjVersion.VersionId {
return fmt.Errorf("expected the SourceVersionId to be %v, instead got %v",
*srcObjVersion.VersionId, *out.CopySourceVersionId)
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: &bucket,
Key: &dstObj,
VersionId: out.VersionId,
})
cancel()
if err != nil {
return err
}
if res.ContentLength == nil {
return fmt.Errorf("expected non nil ContentLength")
}
if res.VersionId == nil {
return fmt.Errorf("expected non nil VersionId")
}
if *res.ContentLength != *srcObjVersion.Size {
return fmt.Errorf("expected the copied object size to be %v, instead got %v",
*srcObjVersion.Size, *res.ContentLength)
}
if *res.VersionId != *out.VersionId {
return fmt.Errorf("expected the copied object versionId to be %v, instead got %v",
*out.VersionId, *res.VersionId)
}
}
return nil
})
if err != nil {
return err
}
return teardown(s, srcBucket)
}, withVersioning(types.BucketVersioningStatusEnabled))
}
// A copy source that resolves to a delete marker is rejected: the key has no
// current version when the marker is the latest, and naming the marker by
// version id is an invalid request. Versions the marker hides stay copyable.
func Versioning_CopyObject_from_a_delete_marker(s *S3Conf) error {
testName := "Versioning_CopyObject_from_a_delete_marker"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
dstBucket, dstObj := getBucketName(), "dst-obj"
if err := setup(s, dstBucket); err != nil {
return err
}
err := forEachKey([]string{"my-obj", "my-dir/"}, func(srcObj string) error {
srcObjVersions, err := createObjVersions(s3client, bucket, srcObj, 1)
if err != nil {
return err
}
delMarker, err := createDeleteMarker(s3client, bucket, srcObj)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: &dstBucket,
Key: &dstObj,
CopySource: getPtr(fmt.Sprintf("%v/%v", bucket, srcObj)),
})
cancel()
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrNoSuchKey)); err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: &dstBucket,
Key: &dstObj,
CopySource: getPtr(fmt.Sprintf("%v/%v?versionId=%v",
bucket, srcObj, delMarker)),
})
cancel()
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrCopySourceDeleteMarker)); err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: &dstBucket,
Key: &dstObj,
CopySource: getPtr(fmt.Sprintf("%v/%v?versionId=%v",
bucket, srcObj, getString(srcObjVersions[0].VersionId))),
})
cancel()
if err != nil {
return err
}
if getString(out.CopySourceVersionId) != getString(srcObjVersions[0].VersionId) {
return fmt.Errorf("expected the copy-source-version-id to be %v, instead got %v",
getString(srcObjVersions[0].VersionId), getString(out.CopySourceVersionId))
}
return nil
})
if err != nil {
return err
}
return teardown(s, dstBucket)
}, withVersioning(types.BucketVersioningStatusEnabled))
}
// A copy of an object onto itself in a versioned bucket is an ordinary
// write: it creates a new version and leaves the one it replaces untouched.
// Without a metadata directive there is nothing to replace, so it's rejected.
func Versioning_CopyObject_to_itself(s *S3Conf) error {
testName := "Versioning_CopyObject_to_itself"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
return forEachKey([]string{"my-obj", "my-dir/"}, func(obj string) error {
// directory objects always carry the directory content-type
srcContentType, dstContentType := "text/plain", "application/json"
if strings.HasSuffix(obj, "/") {
srcContentType, dstContentType = directoryContentType, directoryContentType
}
srcMeta := map[string]string{"key": "value"}
r, err := putObjectWithData(objDataLen(obj, 1234), &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
ContentType: getPtr("text/plain"),
Metadata: srcMeta,
}, s3client)
if err != nil {
return err
}
srcVersionId := getString(r.res.VersionId)
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: &bucket,
Key: &obj,
CopySource: getPtr(fmt.Sprintf("%v/%v", bucket, obj)),
})
cancel()
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidCopyDest)); err != nil {
return err
}
dstMeta := map[string]string{"new-key": "new-value"}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: &bucket,
Key: &obj,
CopySource: getPtr(fmt.Sprintf("%v/%v", bucket, obj)),
MetadataDirective: types.MetadataDirectiveReplace,
ContentType: getPtr("application/json"),
Metadata: dstMeta,
})
cancel()
if err != nil {
return err
}
dstVersionId := getString(out.VersionId)
if dstVersionId == "" {
return fmt.Errorf("expected non empty versionId")
}
if dstVersionId == srcVersionId {
return fmt.Errorf("expected a new versionId, instead got %v", dstVersionId)
}
// the replaced version keeps its own metadata
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: &bucket,
Key: &obj,
VersionId: &srcVersionId,
})
cancel()
if err != nil {
return err
}
if getString(res.ContentType) != srcContentType {
return fmt.Errorf("expected the source version content-type to be %v, instead got %v",
srcContentType, getString(res.ContentType))
}
if !areMapsSame(res.Metadata, srcMeta) {
return fmt.Errorf("expected the source version metadata to be %v, instead got %v",
srcMeta, res.Metadata)
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
res, err = s3client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: &bucket,
Key: &obj,
})
cancel()
if err != nil {
return err
}
if getString(res.VersionId) != dstVersionId {
return fmt.Errorf("expected the current versionId to be %v, instead got %v",
dstVersionId, getString(res.VersionId))
}
if getString(res.ContentType) != dstContentType {
return fmt.Errorf("expected the new version content-type to be %v, instead got %v",
dstContentType, getString(res.ContentType))
}
if !areMapsSame(res.Metadata, dstMeta) {
return fmt.Errorf("expected the new version metadata to be %v, instead got %v",
dstMeta, res.Metadata)
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
vRes, err := s3client.ListObjectVersions(ctx, &s3.ListObjectVersionsInput{
Bucket: &bucket,
Prefix: &obj,
})
cancel()
if err != nil {
return err
}
if len(vRes.Versions) != 2 {
return fmt.Errorf("expected 2 object versions, instead got %v", len(vRes.Versions))
}
return nil
})
}, withVersioning(types.BucketVersioningStatusEnabled))
}
// Naming the current version in the copy source makes a copy onto the same
// key a regular copy, so it is accepted even without a metadata directive.
func Versioning_CopyObject_to_itself_from_the_current_version(s *S3Conf) error {
testName := "Versioning_CopyObject_to_itself_from_the_current_version"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
return forEachKey([]string{"my-obj", "my-dir/"}, func(obj string) error {
versions, err := createObjVersions(s3client, bucket, obj, 1)
if err != nil {
return err
}
srcVersionId := getString(versions[0].VersionId)
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: &bucket,
Key: &obj,
CopySource: getPtr(fmt.Sprintf("%v/%v?versionId=%v", bucket, obj, srcVersionId)),
})
cancel()
if err != nil {
return err
}
if getString(out.CopySourceVersionId) != srcVersionId {
return fmt.Errorf("expected the copy-source-version-id to be %v, instead got %v",
srcVersionId, getString(out.CopySourceVersionId))
}
if getString(out.VersionId) == srcVersionId {
return fmt.Errorf("expected a new versionId, instead got %v", getString(out.VersionId))
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: &bucket,
Key: &obj,
VersionId: &srcVersionId,
})
cancel()
if err != nil {
return err
}
if getString(res.VersionId) != srcVersionId {
return fmt.Errorf("expected the source version to remain, instead got %v",
getString(res.VersionId))
}
return nil
})
}, withVersioning(types.BucketVersioningStatusEnabled))
}
// A rejected conditional copy creates no version. A delete marker as the
// current version leaves the destination without an object: If-Match finds
// no key, while If-None-Match lets the copy through as a new version.
func Versioning_CopyObject_conditional_writes(s *S3Conf) error {
testName := "Versioning_CopyObject_conditional_writes"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
src, obj := "src-obj", "my-obj"
if _, err := createObjVersions(s3client, bucket, src, 1); err != nil {
return err
}
versions, err := createObjVersions(s3client, bucket, obj, 2)
if err != nil {
return err
}
copySource := getPtr(fmt.Sprintf("%v/%v", bucket, src))
for i, test := range []struct {
ifMatch *string
ifNoneMatch *string
}{
{getPtr("incorrect_etag"), nil},
{getPtr(getString(versions[1].ETag)), nil},
{nil, getPtr("*")},
} {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: &bucket,
Key: &obj,
CopySource: copySource,
IfMatch: test.ifMatch,
IfNoneMatch: test.ifNoneMatch,
})
cancel()
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrPreconditionFailed)); err != nil {
return fmt.Errorf("test case %v: %w", i, err)
}
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: &bucket,
Key: &obj,
CopySource: copySource,
IfMatch: versions[0].ETag,
})
cancel()
if err != nil {
return err
}
if getString(out.VersionId) == "" || getString(out.VersionId) == getString(versions[0].VersionId) {
return fmt.Errorf("expected a new versionId, instead got %v", getString(out.VersionId))
}
if _, err := createDeleteMarker(s3client, bucket, obj); err != nil {
return err
}
for i, test := range []struct {
ifMatch *string
err s3err.APIError
}{
{out.CopyObjectResult.ETag, s3err.GetAPIError(s3err.ErrNoSuchKey)},
{getPtr("*"), s3err.GetAPIError(s3err.ErrNotImplemented)},
} {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: &bucket,
Key: &obj,
CopySource: copySource,
IfMatch: test.ifMatch,
})
cancel()
if err := checkApiErr(err, test.err); err != nil {
return fmt.Errorf("delete marker test case %v: %w", i, err)
}
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
out, err = s3client.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: &bucket,
Key: &obj,
CopySource: copySource,
IfNoneMatch: getPtr("*"),
})
cancel()
if err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
head, err := s3client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: &bucket,
Key: &obj,
})
cancel()
if err != nil {
return err
}
if getString(head.VersionId) != getString(out.VersionId) {
return fmt.Errorf("expected the current version to be %v, instead got %v",
getString(out.VersionId), getString(head.VersionId))
}
// two versions were created, the two successful copies and the
// delete marker between them
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.ListObjectVersions(ctx, &s3.ListObjectVersionsInput{
Bucket: &bucket,
Prefix: &obj,
})
cancel()
if err != nil {
return err
}
if len(res.Versions) != 4 || len(res.DeleteMarkers) != 1 {
return fmt.Errorf("expected 4 versions and 1 delete marker, instead got %v and %v",
len(res.Versions), len(res.DeleteMarkers))
}
return nil
}, withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_CopyObject_special_chars(s *S3Conf) error {
testName := "Versioning_CopyObject_special_chars"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
dstBucket := getBucketName()
err := setup(s, dstBucket)
if err != nil {
return err
}
// source object -> destination object
dstObjs := map[string]string{
"foo?bar": "bar&foo",
"baz?dir/": "dir&baz/",
}
err = forEachKey([]string{"foo?bar", "baz?dir/"}, func(srcObj string) error {
dstObj := dstObjs[srcObj]
srcObjVersions, err := createObjVersions(s3client, bucket, srcObj, 1)
if err != nil {
return err
}
srcObjVersionId := *srcObjVersions[0].VersionId
copySource := fmt.Sprintf("%v/%v?versionId=%v",
bucket,
url.PathEscape(srcObj),
url.QueryEscape(srcObjVersionId),
)
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: &bucket,
Key: &dstObj,
CopySource: getPtr(copySource),
})
cancel()
if err != nil {
return err
}
if res.VersionId == nil || *res.VersionId == "" {
return fmt.Errorf("expected non empty versionId")
}
if res.CopySourceVersionId == nil {
return fmt.Errorf("expected non nil CopySourceVersionId")
}
if *res.CopySourceVersionId != srcObjVersionId {
return fmt.Errorf("expected the SourceVersionId to be %v, instead got %v",
srcObjVersionId, *res.CopySourceVersionId)
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: &bucket,
Key: &dstObj,
VersionId: res.VersionId,
})
cancel()
if err != nil {
return err
}
if out.VersionId == nil {
return fmt.Errorf("expected non nil VersionId")
}
if *out.VersionId != *res.VersionId {
return fmt.Errorf("expected the copied object versionId to be %v, instead got %v",
*res.VersionId, *out.VersionId)
}
return nil
})
if err != nil {
return err
}
err = teardown(s, dstBucket)
if err != nil {
return err
}
return nil
}, withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_HeadObject_invalid_versionId(s *S3Conf) error {
testName := "Versioning_HeadObject_invalid_versionId"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
obj := "my-obj"
_, err := putObjectWithData(10, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
}, s3client)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: &bucket,
Key: &obj,
VersionId: getPtr("invalid_versionId"),
})
cancel()
if err := checkSdkApiErr(err, "BadRequest"); err != nil {
return err
}
return nil
}, withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_HeadObject_non_existing_object_version(s *S3Conf) error {
testName := "Versioning_HeadObject_non_existing_object_version"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
return forEachKey([]string{"my-obj", "my-dir/"}, func(obj string) error {
dLen := objDataLen(obj, 2000)
_, err := putObjectWithData(dLen, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
}, s3client)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: &bucket,
Key: &obj,
VersionId: getPtr("01G65Z755AFWAKHE12NY0CQ9FH"),
})
cancel()
if err := checkSdkApiErr(err, "NotFound"); err != nil {
return err
}
return nil
})
})
}
func Versioning_HeadObject_invalid_parent(s *S3Conf) error {
testName := "Versioning_HeadObject_invalid_parent"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
dLen := int64(2000)
obj := "not-a-dir"
r, err := putObjectWithData(dLen, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
}, s3client)
if err != nil {
return err
}
return forEachKey([]string{"not-a-dir/bad-obj", "not-a-dir/bad-dir/"}, func(obj string) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: &bucket,
Key: &obj,
VersionId: r.res.VersionId,
})
cancel()
if err := checkSdkApiErr(err, "NotFound"); err != nil {
return err
}
return nil
})
})
}
func Versioning_HeadObject_success(s *S3Conf) error {
testName := "Versioning_HeadObject_success"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
return forEachKey([]string{"my-obj", "my-dir/"}, func(obj string) error {
dLen := objDataLen(obj, 2000)
r, err := putObjectWithData(dLen, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
}, s3client)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: &bucket,
Key: &obj,
VersionId: r.res.VersionId,
})
cancel()
if err != nil {
return err
}
if out.ContentLength == nil {
return fmt.Errorf("expected non nil ContentLength")
}
if out.VersionId == nil {
return fmt.Errorf("expected non nil VersionId")
}
if *out.ContentLength != dLen {
return fmt.Errorf("expected the object content-length to be %v, instead got %v",
dLen, *out.ContentLength)
}
if *out.VersionId != *r.res.VersionId {
return fmt.Errorf("expected the versionId to be %v, instead got %v",
*r.res.VersionId, *out.VersionId)
}
if strings.HasSuffix(obj, "/") && getString(out.ContentType) != directoryContentType {
return fmt.Errorf("expected the content type to be %v, instead got %v",
directoryContentType, getString(out.ContentType))
}
return nil
})
}, withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_HeadObject_dir_object_versions(s *S3Conf) error {
testName := "Versioning_HeadObject_dir_object_versions"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
obj := "my-dir/"
type dirVersion struct {
versionId *string
metadata map[string]string
}
// each version gets its own metadata key
versions := []dirVersion{}
for i := range 3 {
metadata := map[string]string{
fmt.Sprintf("key%v", i): fmt.Sprintf("value%v", i),
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.PutObject(ctx, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
Metadata: metadata,
})
cancel()
if err != nil {
return err
}
if getString(res.VersionId) == "" {
return fmt.Errorf("expected non empty versionId")
}
versions = append(versions, dirVersion{
versionId: res.VersionId,
metadata: metadata,
})
}
checkVersion := func(v dirVersion, contentLength *int64, contentType, versionId *string, metadata map[string]string) error {
if contentLength == nil {
return fmt.Errorf("expected non nil ContentLength")
}
if *contentLength != 0 {
return fmt.Errorf("expected the object content-length to be 0, instead got %v",
*contentLength)
}
if getString(contentType) != directoryContentType {
return fmt.Errorf("expected the content type to be %v, instead got %v",
directoryContentType, getString(contentType))
}
if getString(versionId) != *v.versionId {
return fmt.Errorf("expected the versionId to be %v, instead got %v",
*v.versionId, getString(versionId))
}
if !areMapsSame(metadata, v.metadata) {
return fmt.Errorf("expected the object metadata to be %v, instead got %v",
v.metadata, metadata)
}
return nil
}
for _, v := range versions {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
head, err := s3client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: &bucket,
Key: &obj,
VersionId: v.versionId,
})
cancel()
if err != nil {
return fmt.Errorf("head version %v: %w", *v.versionId, err)
}
err = checkVersion(v, head.ContentLength, head.ContentType, head.VersionId, head.Metadata)
if err != nil {
return fmt.Errorf("head version %v: %w", *v.versionId, err)
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
get, err := s3client.GetObject(ctx, &s3.GetObjectInput{
Bucket: &bucket,
Key: &obj,
VersionId: v.versionId,
})
if err != nil {
cancel()
return fmt.Errorf("get version %v: %w", *v.versionId, err)
}
bdy, err := io.ReadAll(get.Body)
get.Body.Close()
cancel()
if err != nil {
return fmt.Errorf("get version %v: %w", *v.versionId, err)
}
if len(bdy) != 0 {
return fmt.Errorf("get version %v: expected empty body, instead got %v bytes",
*v.versionId, len(bdy))
}
err = checkVersion(v, get.ContentLength, get.ContentType, get.VersionId, get.Metadata)
if err != nil {
return fmt.Errorf("get version %v: %w", *v.versionId, err)
}
}
return nil
}, withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_HeadObject_without_versionId(s *S3Conf) error {
testName := "Versioning_HeadObject_without_versionId"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
return forEachKey([]string{"my-obj", "my-dir/"}, func(obj string) error {
versions, err := createObjVersions(s3client, bucket, obj, 3)
if err != nil {
return err
}
lastVersion := versions[0]
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: &bucket,
Key: &obj,
})
cancel()
if err != nil {
return err
}
if getString(res.VersionId) != *lastVersion.VersionId {
return fmt.Errorf("expected versionId to be %v, instead got %v",
*lastVersion.VersionId, getString(res.VersionId))
}
return nil
})
}, withVersioning(types.BucketVersioningStatusEnabled))
}
// Versioning_HeadObject_null_version_without_versionId heads an object put
// into a versioning-suspended bucket, without naming a version id.
func Versioning_HeadObject_null_versionId_obj(s *S3Conf) error {
testName := "Versioning_HeadObject_null_versionId_obj"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
keys, dataLen := []string{"my-obj", "my-dir/"}, int64(321)
// the objects are put before versioning is enabled
etags := make(map[string]string, len(keys))
err := forEachKey(keys, func(obj string) error {
out, err := putObjectWithData(objDataLen(obj, dataLen), &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
}, s3client)
if err != nil {
return err
}
etags[obj] = getString(out.res.ETag)
return nil
})
if err != nil {
return err
}
err = putBucketVersioningStatus(s3client, bucket, types.BucketVersioningStatusEnabled)
if err != nil {
return err
}
return forEachKey(keys, func(obj string) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: &bucket,
Key: &obj,
VersionId: &nullVersionId,
})
cancel()
if err != nil {
return err
}
if getString(res.VersionId) != nullVersionId {
return fmt.Errorf("expected the versionId to be %v, instead got %v",
nullVersionId, getString(res.VersionId))
}
if res.ContentLength == nil || *res.ContentLength != objDataLen(obj, dataLen) {
return fmt.Errorf("expected the Content-Length to be %v, instead got %v",
objDataLen(obj, dataLen), res.ContentLength)
}
if getString(res.ETag) != etags[obj] {
return fmt.Errorf("expected the ETag to be %v, instead got %v",
etags[obj], getString(res.ETag))
}
return nil
})
})
}
func Versioning_HeadObject_null_version_without_versionId(s *S3Conf) error {
testName := "Versioning_HeadObject_null_version_without_versionId"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
return forEachKey([]string{"my-obj", "my-dir/"}, func(obj string) error {
dataLen := objDataLen(obj, 765)
out, err := putObjectWithData(dataLen, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
}, s3client)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: &bucket,
Key: &obj,
})
cancel()
if err != nil {
return err
}
if getString(res.VersionId) != nullVersionId {
return fmt.Errorf("expected the versionId to be %v, instead got %v",
nullVersionId, getString(res.VersionId))
}
if res.ContentLength == nil || *res.ContentLength != dataLen {
return fmt.Errorf("expected the Content-Length to be %v, instead got %v",
dataLen, res.ContentLength)
}
if getString(res.ETag) != getString(out.res.ETag) {
return fmt.Errorf("expected the ETag to be %v, instead got %v",
getString(out.res.ETag), getString(res.ETag))
}
return nil
})
}, withVersioning(types.BucketVersioningStatusSuspended))
}
func Versioning_HeadObject_delete_marker(s *S3Conf) error {
testName := "Versioning_HeadObject_delete_marker"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
return forEachKey([]string{"my-obj", "my-dir/"}, func(obj string) error {
dLen := objDataLen(obj, 2000)
_, err := putObjectWithData(dLen, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
}, s3client)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: &bucket,
Key: &obj,
})
cancel()
if err != nil {
return err
}
if out.VersionId == nil || *out.VersionId == "" {
return fmt.Errorf("expected non empty versionId")
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: &bucket,
Key: &obj,
VersionId: out.VersionId,
})
cancel()
if err := checkSdkApiErr(err, "MethodNotAllowed"); err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: &bucket,
Key: &obj,
})
cancel()
if err := checkSdkApiErr(err, "NotFound"); err != nil {
return err
}
return nil
})
}, withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_GetObject_invalid_versionId(s *S3Conf) error {
testName := "Versioning_GetObject_invalid_versionId"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
obj := "my-obj"
_, err := putObjectWithData(10, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
}, s3client)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.GetObject(ctx, &s3.GetObjectInput{
Bucket: &bucket,
Key: &obj,
VersionId: getPtr("invalid_version_id"),
})
cancel()
return checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgVersionId, "invalid_version_id"))
}, withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_GetObject_non_existing_object_version(s *S3Conf) error {
testName := "Versioning_GetObject_non_existing_object_version"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
return forEachKey([]string{"my-obj", "my-dir/"}, func(obj string) error {
dLen := objDataLen(obj, 2000)
_, err := putObjectWithData(dLen, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
}, s3client)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.GetObject(ctx, &s3.GetObjectInput{
Bucket: &bucket,
Key: &obj,
VersionId: getPtr("01G65Z755AFWAKHE12NY0CQ9FH"),
})
cancel()
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrNoSuchVersion)); err != nil {
return err
}
return nil
})
}, withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_GetObject_success(s *S3Conf) error {
testName := "Versioning_GetObject_success"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
return forEachKey([]string{"my-obj", "my-dir/"}, func(obj string) error {
dLen := objDataLen(obj, 2000)
r, err := putObjectWithData(dLen, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
}, s3client)
if err != nil {
return err
}
// getObject checks that the object read with versionId is
// the uploaded version
getObject := func(versionId *string) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.GetObject(ctx, &s3.GetObjectInput{
Bucket: &bucket,
Key: &obj,
VersionId: versionId,
})
defer cancel()
if err != nil {
return err
}
if out.ContentLength == nil {
return fmt.Errorf("expected non nil ContentLength")
}
if out.VersionId == nil {
return fmt.Errorf("expected non nil VersionId")
}
if *out.ContentLength != dLen {
return fmt.Errorf("expected the object content-length to be %v, instead got %v",
dLen, *out.ContentLength)
}
if *out.VersionId != *r.res.VersionId {
return fmt.Errorf("expected the versionId to be %v, instead got %v",
*r.res.VersionId, *out.VersionId)
}
bdy, err := io.ReadAll(out.Body)
if err != nil {
return err
}
out.Body.Close()
outCsum := sha256.Sum256(bdy)
if outCsum != r.csum {
return fmt.Errorf("incorrect output content")
}
return nil
}
// Get the object by versionId
if err := getObject(r.res.VersionId); err != nil {
return err
}
// Get the object without versionId
if err := getObject(nil); err != nil {
return err
}
// Get the noncurrent object version by versionId
_, err = putObjectWithData(objDataLen(obj, 1000), &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
}, s3client)
if err != nil {
return err
}
return getObject(r.res.VersionId)
})
}, withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_GetObject_delete_marker_without_versionId(s *S3Conf) error {
testName := "Versioning_GetObject_delete_marker_without_versionId"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
keys := []string{"my-obj", "my-dir/"}
// the objects are put before versioning is enabled
err := forEachKey(keys, func(obj string) error {
_, err := putObjectWithData(objDataLen(obj, 1234), &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
}, s3client)
return err
})
if err != nil {
return err
}
err = putBucketVersioningStatus(s3client, bucket, types.BucketVersioningStatusEnabled)
if err != nil {
return err
}
return forEachKey(keys, func(obj string) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: &bucket,
Key: &obj,
})
cancel()
if err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.GetObject(ctx, &s3.GetObjectInput{
Bucket: &bucket,
Key: &obj,
})
cancel()
if err := checkSdkApiErr(err, "NoSuchKey"); err != nil {
return err
}
return nil
})
})
}
func Versioning_GetObject_delete_marker(s *S3Conf) error {
testName := "Versioning_GetObject_delete_marker"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
return forEachKey([]string{"my-obj", "my-dir/"}, func(obj string) error {
dLen := objDataLen(obj, 2000)
_, err := putObjectWithData(dLen, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
}, s3client)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: &bucket,
Key: &obj,
})
cancel()
if err != nil {
return err
}
if out.VersionId == nil || *out.VersionId == "" {
return fmt.Errorf("expected non empty versionId")
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.GetObject(ctx, &s3.GetObjectInput{
Bucket: &bucket,
Key: &obj,
VersionId: out.VersionId,
})
cancel()
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrMethodNotAllowed)); err != nil {
return err
}
return nil
})
}, withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_GetObject_null_versionId_obj(s *S3Conf) error {
testName := "Versioning_GetObject_null_versionId_obj"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
keys, dataLen := []string{"my-obj", "my-dir/"}, int64(234)
// the objects are put before versioning is enabled
etags := make(map[string]string, len(keys))
err := forEachKey(keys, func(obj string) error {
out, err := putObjectWithData(objDataLen(obj, dataLen), &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
}, s3client)
if err != nil {
return err
}
etags[obj] = getString(out.res.ETag)
return nil
})
if err != nil {
return err
}
err = putBucketVersioningStatus(s3client, bucket, types.BucketVersioningStatusEnabled)
if err != nil {
return err
}
return forEachKey(keys, func(obj string) error {
lgth, etag := objDataLen(obj, dataLen), etags[obj]
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.GetObject(ctx, &s3.GetObjectInput{
Bucket: &bucket,
Key: &obj,
VersionId: &nullVersionId,
})
cancel()
if err != nil {
return err
}
if res.ContentLength == nil {
return fmt.Errorf("expected non nil ContentLength")
}
if res.VersionId == nil {
return fmt.Errorf("expected non nil VersionId")
}
if res.ETag == nil {
return fmt.Errorf("expected non nil ETag")
}
if *res.ContentLength != lgth {
return fmt.Errorf("expected the Content-Length to be %v, instead got %v",
lgth, *res.ContentLength)
}
if *res.VersionId != nullVersionId {
return fmt.Errorf("expected the versionId to be %v, insted got %v",
nullVersionId, *res.VersionId)
}
if *res.ETag != etag {
return fmt.Errorf("expecte the ETag to be %v, instead got %v",
etag, *res.ETag)
}
return nil
})
})
}
// Versioning_GetObject_null_version_without_versionId reads an object put
// into a versioning-suspended bucket, without naming a version id. The object
// is the null version and the response has to report it as null rather than
// leave the version id out.
func Versioning_GetObject_null_version_without_versionId(s *S3Conf) error {
testName := "Versioning_GetObject_null_version_without_versionId"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
return forEachKey([]string{"my-obj", "my-dir/"}, func(obj string) error {
dataLen := objDataLen(obj, 543)
out, err := putObjectWithData(dataLen, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
}, s3client)
if err != nil {
return err
}
// a put into a versioning-suspended bucket creates the null
// version and reports no version id
if out.res.VersionId != nil {
return fmt.Errorf("expected PutObject response to omit versionId, instead got %v",
getString(out.res.VersionId))
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.GetObject(ctx, &s3.GetObjectInput{
Bucket: &bucket,
Key: &obj,
})
cancel()
if err != nil {
return err
}
if getString(res.VersionId) != nullVersionId {
return fmt.Errorf("expected the versionId to be %v, instead got %v",
nullVersionId, getString(res.VersionId))
}
if res.ContentLength == nil || *res.ContentLength != dataLen {
return fmt.Errorf("expected the Content-Length to be %v, instead got %v",
dataLen, res.ContentLength)
}
if getString(res.ETag) != getString(out.res.ETag) {
return fmt.Errorf("expected the ETag to be %v, instead got %v",
getString(out.res.ETag), getString(res.ETag))
}
return nil
})
}, withVersioning(types.BucketVersioningStatusSuspended))
}
// Versioning_unversioned_bucket_omits_versionId reads an object in a bucket
// that never had versioning configured. Such a bucket has no versions at all,
// so neither GetObject nor HeadObject reports a version id, not even the null
// one, even though the gateway itself runs with versioning enabled.
func Versioning_unversioned_bucket_omits_versionId(s *S3Conf) error {
testName := "Versioning_unversioned_bucket_omits_versionId"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
vRes, err := s3client.GetBucketVersioning(ctx, &s3.GetBucketVersioningInput{
Bucket: &bucket,
})
cancel()
if err != nil {
return err
}
// guard the premise of the test
if vRes.Status != "" {
return fmt.Errorf("expected the bucket versioning to be unconfigured, instead got %v",
vRes.Status)
}
return forEachKey([]string{"my-obj", "my-dir/"}, func(obj string) error {
_, err := putObjectWithData(objDataLen(obj, 432), &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
}, s3client)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
gRes, err := s3client.GetObject(ctx, &s3.GetObjectInput{
Bucket: &bucket,
Key: &obj,
})
cancel()
if err != nil {
return err
}
if gRes.VersionId != nil {
return fmt.Errorf("expected GetObject to omit the versionId, instead got %v",
*gRes.VersionId)
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
hRes, err := s3client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: &bucket,
Key: &obj,
})
cancel()
if err != nil {
return err
}
if hRes.VersionId != nil {
return fmt.Errorf("expected HeadObject to omit the versionId, instead got %v",
*hRes.VersionId)
}
return nil
})
})
}
func Versioning_GetObjectAttributes_invalid_versionId(s *S3Conf) error {
testName := "Versioning_GetObjectAttributes_invalid_versionId"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
obj := "my-obj"
_, err := createObjVersions(s3client, bucket, obj, 1)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.GetObjectAttributes(ctx, &s3.GetObjectAttributesInput{
Bucket: &bucket,
Key: &obj,
VersionId: getPtr("invalid_versionId"),
ObjectAttributes: []types.ObjectAttributes{
types.ObjectAttributesEtag,
},
})
cancel()
return checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgVersionId, "invalid_versionId"))
})
}
func Versioning_GetObjectAttributes_object_version(s *S3Conf) error {
testName := "Versioning_GetObjectAttributes_object_version"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
return forEachKey([]string{"my-obj", "my-dir/"}, func(obj string) error {
versions, err := createObjVersions(s3client, bucket, obj, 1)
if err != nil {
return err
}
version := versions[0]
getObjAttrs := func(versionId *string) (*s3.GetObjectAttributesOutput, error) {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.GetObjectAttributes(ctx, &s3.GetObjectAttributesInput{
Bucket: &bucket,
Key: &obj,
VersionId: versionId,
ObjectAttributes: []types.ObjectAttributes{
types.ObjectAttributesEtag,
},
})
cancel()
return res, err
}
// By specifying the versionId
res, err := getObjAttrs(version.VersionId)
if err != nil {
return err
}
if getString(res.ETag) != strings.Trim(*version.ETag, "\"") {
return fmt.Errorf("expected the uploaded object ETag to be %v, instead got %v",
strings.Trim(*version.ETag, "\""), getString(res.ETag))
}
if getString(res.VersionId) != *version.VersionId {
return fmt.Errorf("expected the uploaded versionId to be %v, instead got %v",
*version.VersionId, getString(res.VersionId))
}
// Without versionId
res, err = getObjAttrs(nil)
if err != nil {
return err
}
if getString(res.ETag) != strings.Trim(*version.ETag, "\"") {
return fmt.Errorf("expected the uploaded object ETag to be %v, instead got %v",
strings.Trim(*version.ETag, "\""), getString(res.ETag))
}
if getString(res.VersionId) != *version.VersionId {
return fmt.Errorf("expected the uploaded object versionId to be %v, instead got %v",
*version.VersionId, getString(res.VersionId))
}
return nil
})
}, withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_GetObjectAttributes_delete_marker(s *S3Conf) error {
testName := "Versioning_GetObjectAttributes_delete_marker"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
return forEachKey([]string{"my-obj", "my-dir/"}, func(obj string) error {
_, err := createObjVersions(s3client, bucket, obj, 1)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: &bucket,
Key: &obj,
})
cancel()
if err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.GetObjectAttributes(ctx, &s3.GetObjectAttributesInput{
Bucket: &bucket,
Key: &obj,
VersionId: res.VersionId,
ObjectAttributes: []types.ObjectAttributes{
types.ObjectAttributesEtag,
},
})
cancel()
if err := checkSdkApiErr(err, "NoSuchKey"); err != nil {
return err
}
return nil
})
}, withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_DeleteObject_invalid_versionId(s *S3Conf) error {
testName := "Versioning_DeleteObject_invalid_versionId"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
obj := "my-obj"
_, err := putObjectWithData(3, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
}, s3client)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: &bucket,
Key: &obj,
VersionId: getPtr("invalid_versionId"),
})
cancel()
return checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgVersionId, "invalid_versionId"))
})
}
func Versioning_DeleteObject_delete_object_version(s *S3Conf) error {
testName := "Versioning_DeleteObject_delete_object_version"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
return forEachKey([]string{"my-obj", "my-dir/"}, func(obj string) error {
oLen := objDataLen(obj, 1000)
r, err := putObjectWithData(oLen, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
}, s3client)
if err != nil {
return err
}
versionId := r.res.VersionId
if versionId == nil || *versionId == "" {
return fmt.Errorf("expected non empty versionId")
}
_, err = putObjects(s3client, []string{obj}, bucket)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: &bucket,
Key: &obj,
VersionId: versionId,
})
cancel()
if err != nil {
return err
}
if out.VersionId == nil {
return fmt.Errorf("expected non nil versionId")
}
if *out.VersionId != *versionId {
return fmt.Errorf("expected deleted object versionId to be %v, instead got %v",
*versionId, *out.VersionId)
}
return nil
})
}, withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_DeleteObject_dir_object_latest_version(s *S3Conf) error {
testName := "Versioning_DeleteObject_dir_object_latest_version"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
obj := "my-dir/"
versionIds := []string{}
for i := range 3 {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.PutObject(ctx, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
Metadata: map[string]string{
"version": fmt.Sprint(i),
},
})
cancel()
if err != nil {
return err
}
if getString(out.VersionId) == "" {
return fmt.Errorf("expected non empty versionId")
}
versionIds = append(versionIds, *out.VersionId)
}
// deleting the latest version makes the previous one the current
for i := 2; i > 0; i-- {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: &bucket,
Key: &obj,
VersionId: &versionIds[i],
})
cancel()
if err != nil {
return err
}
if getString(out.VersionId) != versionIds[i] {
return fmt.Errorf("expected the deleted versionId to be %v, instead got %v",
versionIds[i], getString(out.VersionId))
}
if out.DeleteMarker != nil && *out.DeleteMarker {
return fmt.Errorf("expected the response DeleteMarker to be false")
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: &bucket,
Key: &obj,
})
cancel()
if err != nil {
return err
}
if getString(res.VersionId) != versionIds[i-1] {
return fmt.Errorf("expected the versionId to be %v, instead got %v",
versionIds[i-1], getString(res.VersionId))
}
expectedMeta := map[string]string{
"version": fmt.Sprint(i - 1),
}
if !areMapsSame(res.Metadata, expectedMeta) {
return fmt.Errorf("expected the object metadata to be %v, instead got %v",
expectedMeta, res.Metadata)
}
if getString(res.ContentType) != directoryContentType {
return fmt.Errorf("expected the content type to be %v, instead got %v",
directoryContentType, getString(res.ContentType))
}
if res.ContentLength == nil || *res.ContentLength != 0 {
return fmt.Errorf("expected zero content length, instead got %v",
res.ContentLength)
}
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: &bucket,
Key: &obj,
VersionId: &versionIds[0],
})
cancel()
if err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: &bucket,
Key: &obj,
})
cancel()
if err == nil {
return fmt.Errorf("expected NotFound, instead got nil")
}
if err := checkSdkApiErr(err, "NotFound"); err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.ListObjectVersions(ctx, &s3.ListObjectVersionsInput{
Bucket: &bucket,
})
cancel()
if err != nil {
return err
}
if len(res.Versions) != 0 {
return fmt.Errorf("expected empty object versions, instead got %v", res.Versions)
}
if len(res.DeleteMarkers) != 0 {
return fmt.Errorf("expected empty delete markers list, instead got %v", res.DeleteMarkers)
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.PutObject(ctx, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
})
cancel()
if err != nil {
return err
}
if getString(out.VersionId) == "" {
return fmt.Errorf("expected non empty versionId")
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
res, err = s3client.ListObjectVersions(ctx, &s3.ListObjectVersionsInput{
Bucket: &bucket,
})
cancel()
if err != nil {
return err
}
if len(res.Versions) != 1 {
return fmt.Errorf("expected 1 object version, instead got %v", len(res.Versions))
}
if getString(res.Versions[0].Key) != obj {
return fmt.Errorf("expected the version key to be %v, instead got %v",
obj, getString(res.Versions[0].Key))
}
if getString(res.Versions[0].VersionId) != *out.VersionId {
return fmt.Errorf("expected the versionId to be %v, instead got %v",
*out.VersionId, getString(res.Versions[0].VersionId))
}
if res.Versions[0].IsLatest == nil || !*res.Versions[0].IsLatest {
return fmt.Errorf("expected the version to be the latest")
}
if len(res.DeleteMarkers) != 0 {
return fmt.Errorf("expected empty delete markers list, instead got %v", res.DeleteMarkers)
}
return nil
}, withVersioning(types.BucketVersioningStatusEnabled))
}
// Versioning_DeleteObject_latest_version_with_null_version deletes the current
// version of an object whose history also holds a null version, created while
// versioning was suspended. The version that becomes current has to be the one
// created right before the deleted version, not the older null version.
func Versioning_DeleteObject_latest_version_with_null_version(s *S3Conf) error {
testName := "Versioning_DeleteObject_latest_version_with_null_version"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
return forEachKey([]string{"my-obj", "my-dir/"}, func(obj string) error {
// the versions are told apart by a metadata entry, as a
// directory object carries no data
put := func(marker string) (*s3.PutObjectOutput, error) {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
defer cancel()
return s3client.PutObject(ctx, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
Metadata: map[string]string{"marker": marker},
})
}
if _, err := put("v1"); err != nil {
return err
}
if _, err := put("v2"); err != nil {
return err
}
err := putBucketVersioningStatus(s3client, bucket, types.BucketVersioningStatusSuspended)
if err != nil {
return err
}
// the null version sits in the middle of the version history
if _, err := put("null"); err != nil {
return err
}
err = putBucketVersioningStatus(s3client, bucket, types.BucketVersioningStatusEnabled)
if err != nil {
return err
}
third, err := put("v3")
if err != nil {
return err
}
latest, err := put("v4")
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: &bucket,
Key: &obj,
VersionId: latest.VersionId,
})
cancel()
if err != nil {
return err
}
if getString(out.VersionId) != getString(latest.VersionId) {
return fmt.Errorf("expected the deleted versionId to be %v, instead got %v",
getString(latest.VersionId), getString(out.VersionId))
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: &bucket,
Key: &obj,
})
cancel()
if err != nil {
return err
}
expectedMeta := map[string]string{"marker": "v3"}
if !areMapsSame(res.Metadata, expectedMeta) {
return fmt.Errorf("expected the object metadata to be %v, instead got %v",
expectedMeta, res.Metadata)
}
if getString(res.VersionId) != getString(third.VersionId) {
return fmt.Errorf("expected the current versionId to be %v, instead got %v",
getString(third.VersionId), getString(res.VersionId))
}
// the null version has to be left untouched by the delete
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
versions, err := s3client.ListObjectVersions(ctx, &s3.ListObjectVersionsInput{
Bucket: &bucket,
Prefix: &obj,
})
cancel()
if err != nil {
return err
}
nullFound := false
for _, v := range versions.Versions {
if getString(v.VersionId) == nullVersionId {
nullFound = true
if v.IsLatest != nil && *v.IsLatest {
return fmt.Errorf("expected the null version not to be the latest")
}
}
}
if !nullFound {
return fmt.Errorf("expected the null version to be kept in %v",
versions.Versions)
}
return nil
})
}, withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_DeleteObject_latest_version_null_version_order(s *S3Conf) error {
testName := "Versioning_DeleteObject_latest_version_null_version_order"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
// the null version is either an object put or a delete marker
// created while versioning is suspended
cases := []struct {
obj string
delMarker bool
}{
{"my-obj", false},
{"my-dir/", false},
{"my-dm-obj", true},
{"my-dm-dir/", true},
}
for _, c := range cases {
obj := c.obj
// the versions are told apart by a metadata entry, as a
// directory object carries no data
put := func(marker string) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
defer cancel()
out, err := s3client.PutObject(ctx, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
Metadata: map[string]string{"marker": marker},
})
if err != nil {
return "", err
}
return getString(out.VersionId), nil
}
// history is the version ids from the newest to the oldest,
// with the marker each one is put with
type version struct {
id, marker string
}
var history []version
for _, marker := range []string{"v1", "v2"} {
id, err := put(marker)
if err != nil {
return err
}
history = append([]version{{id, marker}}, history...)
}
err := putBucketVersioningStatus(s3client, bucket, types.BucketVersioningStatusSuspended)
if err != nil {
return err
}
// the null version sits in the middle of the version history
if c.delMarker {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: &bucket,
Key: &obj,
})
cancel()
if err != nil {
return err
}
if getString(out.VersionId) != nullVersionId {
return fmt.Errorf("%v: expected the delete marker versionId to be %v, instead got %v",
obj, nullVersionId, getString(out.VersionId))
}
history = append([]version{{nullVersionId, ""}}, history...)
} else {
if _, err := put("null"); err != nil {
return err
}
history = append([]version{{nullVersionId, "null"}}, history...)
}
err = putBucketVersioningStatus(s3client, bucket, types.BucketVersioningStatusEnabled)
if err != nil {
return err
}
for _, marker := range []string{"v3", "v4"} {
id, err := put(marker)
if err != nil {
return err
}
history = append([]version{{id, marker}}, history...)
}
// deleting the latest version makes the version created right
// before it the latest one, whether it's the null version or not
for i, deleted := range history {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: &bucket,
Key: &obj,
VersionId: &deleted.id,
})
cancel()
if err != nil {
return fmt.Errorf("%v: delete version %v: %w", obj, deleted.id, err)
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: &bucket,
Key: &obj,
})
cancel()
// the object is left with no current version when the
// null delete marker or none of the versions is latest
if i+1 == len(history) || history[i+1].marker == "" {
if err == nil {
return fmt.Errorf("%v: expected the object to have no current version after deleting %v, instead got version %v",
obj, deleted.id, getString(res.VersionId))
}
if err := checkSdkApiErr(err, "NotFound"); err != nil {
return fmt.Errorf("%v: after deleting %v: %w", obj, deleted.id, err)
}
continue
}
if err != nil {
return fmt.Errorf("%v: head object after deleting %v: %w", obj, deleted.id, err)
}
expected := history[i+1]
if getString(res.VersionId) != expected.id {
return fmt.Errorf("%v: expected the current versionId after deleting %v to be %v, instead got %v",
obj, deleted.id, expected.id, getString(res.VersionId))
}
expectedMeta := map[string]string{"marker": expected.marker}
if !areMapsSame(res.Metadata, expectedMeta) {
return fmt.Errorf("%v: expected the object metadata after deleting %v to be %v, instead got %v",
obj, deleted.id, expectedMeta, res.Metadata)
}
}
}
return nil
}, withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_DeleteObject_promoted_version_attributes(s *S3Conf) error {
testName := "Versioning_DeleteObject_promoted_version_attributes"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
obj := "my-obj"
expires := time.Now().Add(time.Hour)
dataLen := int64(10)
first, err := putObjectWithData(dataLen, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
}, s3client)
if err != nil {
return err
}
second, err := putObjectWithData(20, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
ContentType: getPtr("text/plain"),
ContentEncoding: getPtr("gzip"),
ContentDisposition: getPtr("inline"),
ContentLanguage: getPtr("en"),
CacheControl: getPtr("no-cache"),
Expires: &expires,
WebsiteRedirectLocation: getPtr("/redirect"),
Metadata: map[string]string{"foo": "bar"},
Tagging: getPtr("key=val"),
}, s3client)
if err != nil {
return err
}
// deleting the latest version makes the first version current again
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: &bucket,
Key: &obj,
VersionId: second.res.VersionId,
})
cancel()
if err != nil {
return err
}
// the first version holds none of the deleted version attributes
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
cur, err := s3client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: &bucket,
Key: &obj,
})
cancel()
if err != nil {
return err
}
if getString(cur.VersionId) != getString(first.res.VersionId) {
return fmt.Errorf("expected the current versionId to be %v, instead got %v",
getString(first.res.VersionId), getString(cur.VersionId))
}
if cur.ContentLength == nil {
return fmt.Errorf("expected non nil ContentLength")
}
if *cur.ContentLength != dataLen {
return fmt.Errorf("expected the content length to be %v, instead got %v",
dataLen, *cur.ContentLength)
}
if getString(cur.ContentType) != defaultContentType {
return fmt.Errorf("expected default %s Content-Type, instead got %s",
defaultContentType, getString(cur.ContentType))
}
for _, hdr := range []struct {
name string
value *string
}{
{"Content-Encoding", cur.ContentEncoding},
{"Content-Disposition", cur.ContentDisposition},
{"Content-Language", cur.ContentLanguage},
{"Cache-Control", cur.CacheControl},
{"Expires", cur.ExpiresString},
{"x-amz-website-redirect-location", cur.WebsiteRedirectLocation},
} {
if hdr.value != nil {
return fmt.Errorf("expected nil %s, instead got %s", hdr.name, *hdr.value)
}
}
if len(cur.Metadata) != 0 {
return fmt.Errorf("expected empty metadata, instead got %v", cur.Metadata)
}
if cur.TagCount != nil {
return fmt.Errorf("expected nil tag count, instead got %v", *cur.TagCount)
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
curTags, err := s3client.GetObjectTagging(ctx, &s3.GetObjectTaggingInput{
Bucket: &bucket,
Key: &obj,
})
cancel()
if err != nil {
return err
}
if len(curTags.TagSet) != 0 {
return fmt.Errorf("expected empty tag set, instead got %v", curTags.TagSet)
}
return nil
}, withVersioning(types.BucketVersioningStatusEnabled))
}
// Versioning_DeleteObject_promoted_version_last_modified deletes the current
// version of objects, which makes an older version the current one: the
// version put before it, the version under a delete marker or the null
// version. The promoted version keeps its last modified time.
func Versioning_DeleteObject_promoted_version_last_modified(s *S3Conf) error {
testName := "Versioning_DeleteObject_promoted_version_last_modified"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
cases := []struct {
obj string
// the version to promote is put while versioning is suspended
null bool
// the version to promote is hidden by a delete marker
delMarker bool
// the last modified time of the version to promote
lastModified *time.Time
}{
{obj: "my-obj"},
{obj: "my-dir/"},
{obj: "my-dm-obj", delMarker: true},
{obj: "my-dm-dir/", delMarker: true},
{obj: "my-null-obj", null: true},
{obj: "my-null-dir/", null: true},
}
put := func(obj string) (*string, error) {
r, err := putObjectWithData(objDataLen(obj, 100), &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
}, s3client)
if err != nil {
return nil, err
}
return r.res.VersionId, nil
}
// the versions to promote are put first, the null versions while
// versioning is suspended
for _, status := range []types.BucketVersioningStatus{
types.BucketVersioningStatusSuspended,
types.BucketVersioningStatusEnabled,
} {
err := putBucketVersioningStatus(s3client, bucket, status)
if err != nil {
return err
}
for i, c := range cases {
if c.null != (status == types.BucketVersioningStatusSuspended) {
continue
}
if _, err := put(c.obj); err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: &bucket,
Key: &c.obj,
})
cancel()
if err != nil {
return err
}
if res.LastModified == nil {
return fmt.Errorf("%v: expected non nil LastModified", c.obj)
}
cases[i].lastModified = res.LastModified
}
}
// the versions are promoted in a later second than they were put
time.Sleep(time.Second)
for _, c := range cases {
var latest *string
if c.delMarker {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: &bucket,
Key: &c.obj,
})
cancel()
if err != nil {
return err
}
latest = out.VersionId
} else {
versionId, err := put(c.obj)
if err != nil {
return err
}
latest = versionId
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: &bucket,
Key: &c.obj,
VersionId: latest,
})
cancel()
if err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: &bucket,
Key: &c.obj,
})
cancel()
if err != nil {
return err
}
if res.LastModified == nil || !res.LastModified.Equal(*c.lastModified) {
return fmt.Errorf("%v: expected the promoted version LastModified to be %v, instead got %v",
c.obj, *c.lastModified, res.LastModified)
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
versions, err := s3client.ListObjectVersions(ctx, &s3.ListObjectVersionsInput{
Bucket: &bucket,
Prefix: &c.obj,
})
cancel()
if err != nil {
return err
}
if len(versions.Versions) != 1 {
return fmt.Errorf("%v: expected 1 object version, instead got %v",
c.obj, len(versions.Versions))
}
v := versions.Versions[0]
if v.LastModified == nil || !v.LastModified.Equal(*c.lastModified) {
return fmt.Errorf("%v: expected the listed version LastModified to be %v, instead got %v",
c.obj, *c.lastModified, v.LastModified)
}
}
return nil
}, withVersioning(types.BucketVersioningStatusEnabled))
}
// Versioning_DeleteObject_delete_marker_last_modified creates delete markers
// over an object version, over another delete marker and, while versioning
// is suspended, over the null version. A delete marker is last modified when
// it's created, not when the version it hides was.
func Versioning_DeleteObject_delete_marker_last_modified(s *S3Conf) error {
testName := "Versioning_DeleteObject_delete_marker_last_modified"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
cases := []struct {
obj string
// the object is put and deleted while versioning is suspended
null bool
// the last modified time of the object version
lastModified *time.Time
// the delete markers, from the oldest to the newest
delMarkers []string
}{
{obj: "my-obj"},
{obj: "my-dir/"},
{obj: "my-null-obj", null: true},
{obj: "my-null-dir/", null: true},
}
// delMarkerLastModified returns the last modified time of the delete
// marker of obj, which the listing and the Last-Modified header of a
// HeadObject request for it report alike
delMarkerLastModified := func(obj, versionId string) (time.Time, error) {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.ListObjectVersions(ctx, &s3.ListObjectVersionsInput{
Bucket: &bucket,
Prefix: &obj,
})
cancel()
if err != nil {
return time.Time{}, err
}
var lastModified *time.Time
for _, dm := range res.DeleteMarkers {
if getString(dm.Key) == obj && getString(dm.VersionId) == versionId {
lastModified = dm.LastModified
}
}
if lastModified == nil {
return time.Time{}, fmt.Errorf("%v: expected the delete marker %v to be listed with LastModified, instead got %v",
obj, versionId, res.DeleteMarkers)
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: &bucket,
Key: &obj,
VersionId: &versionId,
})
cancel()
if err := checkSdkApiErr(err, "MethodNotAllowed"); err != nil {
return time.Time{}, err
}
var respErr *awshttp.ResponseError
if !errors.As(err, &respErr) {
return time.Time{}, fmt.Errorf("expected an HTTP response error, instead got %w", err)
}
header, err := http.ParseTime(respErr.Response.Header.Get("Last-Modified"))
if err != nil {
return time.Time{}, fmt.Errorf("%v: parse the delete marker %v Last-Modified header: %w",
obj, versionId, err)
}
if !header.Equal(*lastModified) {
return time.Time{}, fmt.Errorf("%v: expected the delete marker %v Last-Modified header to be %v, instead got %v",
obj, versionId, *lastModified, header)
}
return *lastModified, nil
}
// deleteObjects creates a delete marker for each of the cases the
// versioning status applies to
deleteObjects := func(null bool) error {
for i, c := range cases {
if c.null != null {
continue
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: &bucket,
Key: &c.obj,
})
cancel()
if err != nil {
return err
}
cases[i].delMarkers = append(cases[i].delMarkers, getString(out.VersionId))
}
return nil
}
// the null versions are put and deleted while versioning is suspended
for _, status := range []types.BucketVersioningStatus{
types.BucketVersioningStatusSuspended,
types.BucketVersioningStatusEnabled,
} {
err := putBucketVersioningStatus(s3client, bucket, status)
if err != nil {
return err
}
for i, c := range cases {
if c.null != (status == types.BucketVersioningStatusSuspended) {
continue
}
_, err := putObjectWithData(objDataLen(c.obj, 100), &s3.PutObjectInput{
Bucket: &bucket,
Key: &c.obj,
}, s3client)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: &bucket,
Key: &c.obj,
})
cancel()
if err != nil {
return err
}
if res.LastModified == nil {
return fmt.Errorf("%v: expected non nil LastModified", c.obj)
}
cases[i].lastModified = res.LastModified
}
}
// every delete marker is created in a later second than the
// version or the delete marker it hides
time.Sleep(time.Second)
if err := deleteObjects(false); err != nil {
return err
}
err := putBucketVersioningStatus(s3client, bucket, types.BucketVersioningStatusSuspended)
if err != nil {
return err
}
if err := deleteObjects(true); err != nil {
return err
}
err = putBucketVersioningStatus(s3client, bucket, types.BucketVersioningStatusEnabled)
if err != nil {
return err
}
time.Sleep(time.Second)
if err := deleteObjects(false); err != nil {
return err
}
for _, c := range cases {
prev := *c.lastModified
var lastModified []time.Time
for _, versionId := range c.delMarkers {
dmLastModified, err := delMarkerLastModified(c.obj, versionId)
if err != nil {
return err
}
if !dmLastModified.After(prev) {
return fmt.Errorf("%v: expected the delete marker %v LastModified %v to be after %v",
c.obj, versionId, dmLastModified, prev)
}
prev = dmLastModified
lastModified = append(lastModified, dmLastModified)
}
if c.null {
continue
}
// deleting the newest delete marker makes the one it hides
// the latest, with its own last modified time
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: &bucket,
Key: &c.obj,
VersionId: &c.delMarkers[1],
})
cancel()
if err != nil {
return err
}
dmLastModified, err := delMarkerLastModified(c.obj, c.delMarkers[0])
if err != nil {
return err
}
if !dmLastModified.Equal(lastModified[0]) {
return fmt.Errorf("%v: expected the promoted delete marker LastModified to be %v, instead got %v",
c.obj, lastModified[0], dmLastModified)
}
}
return nil
}, withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_DeleteObject_non_existing_object(s *S3Conf) error {
testName := "Versioning_DeleteObject_non_existing_object"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
return forEachKey([]string{"my-obj", "my-dir/"}, func(obj string) error {
ctx, canel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: &bucket,
Key: &obj,
})
canel()
if err != nil {
return err
}
ctx, canel = context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: &bucket,
Key: &obj,
VersionId: getPtr("non_existing_version_id"),
})
canel()
if err := checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgVersionId, "non_existing_version_id")); err != nil {
return err
}
return nil
})
}, withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_DeleteObject_implicit_dir(s *S3Conf) error {
testName := "Versioning_DeleteObject_implicit_dir"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
dir, obj := "my-dir/", "my-dir/obj"
versions, err := createObjVersions(s3client, bucket, obj, 1)
if err != nil {
return err
}
// "my-dir/" is only the parent directory of "my-dir/obj", not an object
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: &bucket,
Key: &dir,
})
cancel()
if err != nil {
return err
}
if out.DeleteMarker != nil && *out.DeleteMarker {
return fmt.Errorf("expected the response DeleteMarker to be false")
}
if getString(out.VersionId) != "" {
return fmt.Errorf("expected empty versionId, instead got %v",
getString(out.VersionId))
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.ListObjectVersions(ctx, &s3.ListObjectVersionsInput{
Bucket: &bucket,
})
cancel()
if err != nil {
return err
}
if !compareVersions(versions, res.Versions) {
return fmt.Errorf("expected the versions to be %v, instead got %v",
versions, res.Versions)
}
if len(res.DeleteMarkers) != 0 {
return fmt.Errorf("expected empty delete markers list, instead got %v",
res.DeleteMarkers)
}
return nil
}, withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_DeleteObject_delete_a_delete_marker(s *S3Conf) error {
testName := "Versioning_DeleteObject_delete_a_delete_marker"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
return forEachKey([]string{"my-obj", "my-dir/"}, func(obj string) error {
oLen := objDataLen(obj, 1000)
_, err := putObjectWithData(oLen, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
}, s3client)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: &bucket,
Key: &obj,
})
cancel()
if err != nil {
return err
}
if out.DeleteMarker == nil || !*out.DeleteMarker {
return fmt.Errorf("expected the response DeleteMarker to be true")
}
if out.VersionId == nil || *out.VersionId == "" {
return fmt.Errorf("expected non empty versionId")
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: &bucket,
Key: &obj,
VersionId: out.VersionId,
})
cancel()
if err != nil {
return err
}
if res.DeleteMarker == nil || !*res.DeleteMarker {
return fmt.Errorf("expected the response DeleteMarker to be true")
}
if res.VersionId == nil {
return fmt.Errorf("expected non empty versionId")
}
if *res.VersionId != *out.VersionId {
return fmt.Errorf("expected the versionId to be %v, instead got %v",
*out.VersionId, *res.VersionId)
}
return nil
})
}, withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_DeleteObject_dir_object_with_children(s *S3Conf) error {
testName := "Versioning_DeleteObject_dir_object_with_children"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
dir, child := "my-dir/", "my-dir/child"
dirVersions, err := createObjVersions(s3client, bucket, dir, 1)
if err != nil {
return err
}
childData, err := putObjectWithData(100, &s3.PutObjectInput{
Bucket: &bucket,
Key: &child,
}, s3client)
if err != nil {
return err
}
// checkObjects checks that the child object is readable and that
// the bucket lists exactly keys
checkObjects := func(keys ...string) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.GetObject(ctx, &s3.GetObjectInput{
Bucket: &bucket,
Key: &child,
})
cancel()
if err != nil {
return err
}
body, err := io.ReadAll(out.Body)
out.Body.Close()
if err != nil {
return err
}
if sha256.Sum256(body) != childData.csum {
return fmt.Errorf("expected the %v data checksum to match", child)
}
objs, _, err := listObjects(s3client, bucket, "", "", 1000)
if err != nil {
return err
}
if !hasObjNames(objs, keys) {
return fmt.Errorf("expected the listed objects to be %v, instead got %v",
keys, objs)
}
return nil
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: &bucket,
Key: &dir,
})
cancel()
if err != nil {
return err
}
if out.DeleteMarker == nil || !*out.DeleteMarker {
return fmt.Errorf("expected the response DeleteMarker to be true")
}
if getString(out.VersionId) == "" {
return fmt.Errorf("expected non empty versionId")
}
err = checkObjects(child)
if err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: &bucket,
Key: &dir,
VersionId: out.VersionId,
})
cancel()
if err != nil {
return err
}
if res.DeleteMarker == nil || !*res.DeleteMarker {
return fmt.Errorf("expected the response DeleteMarker to be true")
}
if getString(res.VersionId) != *out.VersionId {
return fmt.Errorf("expected the versionId to be %v, instead got %v",
*out.VersionId, getString(res.VersionId))
}
// removing the delete marker restores the directory object
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
head, err := s3client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: &bucket,
Key: &dir,
})
cancel()
if err != nil {
return err
}
if getString(head.VersionId) != *dirVersions[0].VersionId {
return fmt.Errorf("expected the versionId to be %v, instead got %v",
*dirVersions[0].VersionId, getString(head.VersionId))
}
err = checkObjects(dir, child)
if err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
res, err = s3client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: &bucket,
Key: &dir,
VersionId: dirVersions[0].VersionId,
})
cancel()
if err != nil {
return err
}
if getString(res.VersionId) != *dirVersions[0].VersionId {
return fmt.Errorf("expected the versionId to be %v, instead got %v",
*dirVersions[0].VersionId, getString(res.VersionId))
}
// with no versions left the directory is only the parent of the child
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: &bucket,
Key: &dir,
})
cancel()
if err == nil {
return fmt.Errorf("expected NotFound, instead got nil")
}
if err := checkSdkApiErr(err, "NotFound"); err != nil {
return err
}
err = checkObjects(child)
if err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
versions, err := s3client.ListObjectVersions(ctx, &s3.ListObjectVersionsInput{
Bucket: &bucket,
})
cancel()
if err != nil {
return err
}
if len(versions.Versions) != 1 || getString(versions.Versions[0].Key) != child {
return fmt.Errorf("expected only %v versions, instead got %v",
child, versions.Versions)
}
if len(versions.DeleteMarkers) != 0 {
return fmt.Errorf("expected empty delete markers list, instead got %v",
versions.DeleteMarkers)
}
return nil
}, withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_DeleteObject_trailing_slash_counterpart(s *S3Conf) error {
testName := "Versioning_DeleteObject_trailing_slash_counterpart"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
expected := []types.ObjectVersion{}
// deleting the key with or without a trailing slash doesn't delete
// the object or any of its versions
for _, keys := range [][2]string{{"my-dir/", "my-dir"}, {"my-obj", "my-obj/"}} {
obj, other := keys[0], keys[1]
versions, err := createObjVersions(s3client, bucket, obj, 2)
if err != nil {
return err
}
expected = append(expected, versions...)
for _, versionId := range []*string{versions[0].VersionId, versions[1].VersionId} {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: &bucket,
Key: &other,
VersionId: versionId,
})
cancel()
if err != nil {
return fmt.Errorf("%v: %w", other, err)
}
if getString(out.VersionId) != *versionId {
return fmt.Errorf("%v: expected the versionId to be %v, instead got %v",
other, *versionId, getString(out.VersionId))
}
if out.DeleteMarker != nil && *out.DeleteMarker {
return fmt.Errorf("%v: expected the response DeleteMarker to be false", other)
}
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: &bucket,
Key: &other,
})
cancel()
if err != nil {
return fmt.Errorf("%v: %w", other, err)
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: &bucket,
Key: &obj,
})
cancel()
if err != nil {
return fmt.Errorf("%v: %w", obj, err)
}
if getString(res.VersionId) != getString(versions[0].VersionId) {
return fmt.Errorf("%v: expected the versionId to be %v, instead got %v",
obj, getString(versions[0].VersionId), getString(res.VersionId))
}
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.ListObjectVersions(ctx, &s3.ListObjectVersionsInput{
Bucket: &bucket,
})
cancel()
if err != nil {
return err
}
if !compareVersions(expected, res.Versions) {
return fmt.Errorf("expected the versions to be %v, instead got %v",
expected, res.Versions)
}
return nil
}, withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_Delete_null_versionId_object(s *S3Conf) error {
testName := "Versioning_Delete_null_versionId_object"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
objs, nObjLgth := []string{"my-obj", "my-dir/"}, int64(3211)
// the null versions are created before versioning is enabled
for _, obj := range objs {
_, err := putObjectWithData(objDataLen(obj, nObjLgth), &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
}, s3client)
if err != nil {
return err
}
}
err := putBucketVersioningStatus(s3client, bucket, types.BucketVersioningStatusEnabled)
if err != nil {
return err
}
return forEachKey(objs, func(obj string) error {
_, err := createObjVersions(s3client, bucket, obj, 3)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: &bucket,
Key: &obj,
VersionId: getPtr(nullVersionId),
})
cancel()
if err != nil {
return err
}
if getString(res.VersionId) != nullVersionId {
return fmt.Errorf("expected the versionId to be %v, instead got %v",
nullVersionId, getString(res.VersionId))
}
return nil
})
})
}
func Versioning_DeleteObject_nested_dir_object(s *S3Conf) error {
testName := "Versioning_DeleteObject_nested_dir_object"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
return forEachKey([]string{"foo/bar/baz", "foo/bar/baz/"}, func(obj string) error {
out, err := putObjectWithData(objDataLen(obj, 1000), &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
}, s3client)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: &bucket,
Key: &obj,
VersionId: out.res.VersionId,
})
cancel()
if err != nil {
return err
}
if getString(res.VersionId) != getString(out.res.VersionId) {
return fmt.Errorf("expected the versionId to be %v, instead got %v",
getString(out.res.VersionId), getString(res.VersionId))
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.DeleteBucket(ctx, &s3.DeleteBucketInput{
Bucket: &bucket,
})
cancel()
if err != nil {
return err
}
// Then create the bucket back to not get error on teardown
if err := setup(s, bucket, withLock()); err != nil {
return err
}
return nil
})
}, withLock())
}
func Versioning_DeleteObject_non_existing_objects(s *S3Conf) error {
testName := "Versioning_DeleteObject_non_existing_objects"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
out, err := putObjectWithData(2, &s3.PutObjectInput{
Bucket: &bucket,
Key: getPtr("foo"),
}, s3client)
if err != nil {
return err
}
versionId := getString(out.res.VersionId)
out, err = putObjectWithData(0, &s3.PutObjectInput{
Bucket: &bucket,
Key: getPtr("my-dir/"),
}, s3client)
if err != nil {
return err
}
dirVersionId := getString(out.res.VersionId)
for _, test := range []struct {
key string
versionId string
}{
{"foo/bar", "01KF2YVN948NAZ4JJR4X1AAVRA"},
{"foo/bar/baz", "01KF2YVN948NAZ4JJR4X1AAVRA"},
{"hello", "01KF2YVN948NAZ4JJR4X1AAVRA"},
{"hello/world", "01KF2YVN948NAZ4JJR4X1AAVRA"},
{"foo/bar/", "01KF2YVN948NAZ4JJR4X1AAVRA"},
{"hello/", "01KF2YVN948NAZ4JJR4X1AAVRA"},
{"hello/world/", "01KF2YVN948NAZ4JJR4X1AAVRA"},
{"foo/bar/baz/quxx", versionId},
{"foo/bar/baz/quxx/", versionId},
{"my-dir/hello/", dirVersionId},
{"foo", versionId},
{"my-dir/", dirVersionId},
} {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: &bucket,
Key: &test.key,
VersionId: &test.versionId,
})
cancel()
if err != nil {
return err
}
if getString(res.VersionId) != test.versionId {
return fmt.Errorf("expected the versionId to be %s, instead got %s", test.versionId, getString(res.VersionId))
}
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
resp, err := s3client.ListObjectVersions(ctx, &s3.ListObjectVersionsInput{
Bucket: &bucket,
})
cancel()
if err != nil {
return err
}
if len(resp.Versions) != 0 {
return fmt.Errorf("expected empty object versions, instead got %v", resp.Versions)
}
if len(resp.DeleteMarkers) != 0 {
return fmt.Errorf("expected empty delete markers list, insead got %v", resp.DeleteMarkers)
}
return nil
}, withLock())
}
func Versioning_DeleteObject_suspended(s *S3Conf) error {
testName := "Versioning_DeleteObject_suspended"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
return forEachKey([]string{"my-obj", "my-dir/"}, func(obj string) error {
// the object version is created while versioning is enabled
err := putBucketVersioningStatus(s3client, bucket, types.BucketVersioningStatusEnabled)
if err != nil {
return err
}
versions, err := createObjVersions(s3client, bucket, obj, 1)
if err != nil {
return err
}
versions[0].IsLatest = getBoolPtr(false)
err = putBucketVersioningStatus(s3client, bucket, types.BucketVersioningStatusSuspended)
if err != nil {
return err
}
for range 5 {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: &bucket,
Key: &obj,
})
cancel()
if err != nil {
return err
}
if res.DeleteMarker == nil {
return fmt.Errorf("expected the delete marker to be true")
}
if !*res.DeleteMarker {
return fmt.Errorf("expected the delete marker to be true, instead got %v",
*res.DeleteMarker)
}
if res.VersionId == nil {
return fmt.Errorf("expected non nil versionId")
}
if *res.VersionId != nullVersionId {
return fmt.Errorf("expected the versionId to be %v, instead got %v",
nullVersionId, *res.VersionId)
}
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.ListObjectVersions(ctx, &s3.ListObjectVersionsInput{
Bucket: &bucket,
Prefix: &obj,
})
cancel()
if err != nil {
return err
}
delMarkers := []types.DeleteMarkerEntry{
{
IsLatest: getBoolPtr(true),
Key: &obj,
VersionId: &nullVersionId,
},
}
if !compareVersions(versions, res.Versions) {
return fmt.Errorf("expected the versions to be %v, instead got %v",
versions, res.Versions)
}
if !compareDelMarkers(res.DeleteMarkers, delMarkers) {
return fmt.Errorf("expected the delete markers to be %v, instead got %v",
delMarkers, res.DeleteMarkers)
}
return nil
})
}, withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_DeleteObject_never_versioned_bucket(s *S3Conf) error {
testName := "Versioning_DeleteObject_never_versioned_bucket"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
versionId := "01KF2YVN948NAZ4JJR4X1AAVRA"
return forEachKey([]string{"my-obj", "my-dir/"}, func(obj string) error {
out, err := putObjectWithData(objDataLen(obj, 10), &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
}, s3client)
if err != nil {
return err
}
// the object only has a null version, so deleting any other
// version succeeds without deleting the object
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: &bucket,
Key: &obj,
VersionId: &versionId,
})
cancel()
if err != nil {
return err
}
if getString(res.VersionId) != versionId {
return fmt.Errorf("expected the versionId to be %v, instead got %v",
versionId, getString(res.VersionId))
}
if res.DeleteMarker != nil && *res.DeleteMarker {
return fmt.Errorf("expected the response DeleteMarker to be false")
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
head, err := s3client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: &bucket,
Key: &obj,
})
cancel()
if err != nil {
return err
}
if getString(head.ETag) != getString(out.res.ETag) {
return fmt.Errorf("expected the ETag to be %v, instead got %v",
getString(out.res.ETag), getString(head.ETag))
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: &bucket,
Key: &obj,
VersionId: &nullVersionId,
})
cancel()
if err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: &bucket,
Key: &obj,
})
cancel()
if err == nil {
return fmt.Errorf("expected NotFound, instead got nil")
}
return checkSdkApiErr(err, "NotFound")
})
})
}
// Versioning_DeleteObject_conditional_delete_marker checks that a
// conditional delete of a key whose current version is a delete marker
// fails with NoSuchKey and creates no other marker, while If-Match "*"
// on a key with a current version creates one.
func Versioning_DeleteObject_conditional_delete_marker(s *S3Conf) error {
testName := "Versioning_DeleteObject_conditional_delete_marker"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
return forEachKey([]string{"my-obj", "my-dir/"}, func(obj string) error {
versions, err := createObjVersions(s3client, bucket, obj, 1)
if err != nil {
return err
}
if _, err := createDeleteMarker(s3client, bucket, obj); err != nil {
return err
}
// the marker hides the version the etag belongs to
for _, ifMatch := range []*string{versions[0].ETag, getPtr("*")} {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: &bucket,
Key: &obj,
IfMatch: ifMatch,
})
cancel()
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrNoSuchKey)); err != nil {
return fmt.Errorf("If-Match %v: %w", *ifMatch, err)
}
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.ListObjectVersions(ctx, &s3.ListObjectVersionsInput{
Bucket: &bucket,
Prefix: &obj,
})
cancel()
if err != nil {
return err
}
if len(res.DeleteMarkers) != 1 {
return fmt.Errorf("expected 1 delete marker, instead got %v", len(res.DeleteMarkers))
}
if _, err := createObjVersions(s3client, bucket, obj, 1); err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: &bucket,
Key: &obj,
IfMatch: getPtr("*"),
})
cancel()
if err != nil {
return err
}
if out.DeleteMarker == nil || !*out.DeleteMarker || getString(out.VersionId) == "" {
return fmt.Errorf("expected a delete marker to be created, instead got %v, %v",
out.DeleteMarker, getString(out.VersionId))
}
return nil
})
}, withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_DeleteObjects_success(s *S3Conf) error {
testName := "Versioning_DeleteObjects_success"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
obj1, obj2, obj3 := "foo", "bar", "baz"
dir1, dir2 := "foo-dir/", "baz-dir/"
obj1Version, err := createObjVersions(s3client, bucket, obj1, 1)
if err != nil {
return err
}
obj2Version, err := createObjVersions(s3client, bucket, obj2, 1)
if err != nil {
return err
}
obj3Version, err := createObjVersions(s3client, bucket, obj3, 1)
if err != nil {
return err
}
dir1Version, err := createObjVersions(s3client, bucket, dir1, 1)
if err != nil {
return err
}
dir2Version, err := createObjVersions(s3client, bucket, dir2, 1)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.DeleteObjects(ctx, &s3.DeleteObjectsInput{
Bucket: &bucket,
Delete: &types.Delete{
Objects: []types.ObjectIdentifier{
{
Key: obj1Version[0].Key,
VersionId: obj1Version[0].VersionId,
},
{
Key: obj2Version[0].Key,
},
{
Key: obj3Version[0].Key,
},
{
Key: dir1Version[0].Key,
VersionId: dir1Version[0].VersionId,
},
{
Key: dir2Version[0].Key,
},
},
},
})
cancel()
if err != nil {
return err
}
delResult := []types.DeletedObject{
{
Key: obj1Version[0].Key,
VersionId: obj1Version[0].VersionId,
DeleteMarker: getBoolPtr(false),
},
{
Key: obj2Version[0].Key,
DeleteMarker: getBoolPtr(true),
},
{
Key: obj3Version[0].Key,
DeleteMarker: getBoolPtr(true),
},
{
Key: dir1Version[0].Key,
VersionId: dir1Version[0].VersionId,
DeleteMarker: getBoolPtr(false),
},
{
Key: dir2Version[0].Key,
DeleteMarker: getBoolPtr(true),
},
}
if len(out.Errors) != 0 {
return fmt.Errorf("errors occurred during the deletion: %v",
out.Errors)
}
if !compareDelObjects(delResult, out.Deleted) {
return fmt.Errorf("expected the deleted objects to be %v, instead got %v",
delResult, out.Deleted)
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.ListObjectVersions(ctx, &s3.ListObjectVersionsInput{
Bucket: &bucket,
})
cancel()
if err != nil {
return err
}
obj2Version[0].IsLatest = getBoolPtr(false)
obj3Version[0].IsLatest = getBoolPtr(false)
dir2Version[0].IsLatest = getBoolPtr(false)
versions := append(obj2Version, obj3Version...)
versions = append(versions, dir2Version...)
delMarkers := []types.DeleteMarkerEntry{
{
IsLatest: getBoolPtr(true),
Key: out.Deleted[1].Key,
VersionId: out.Deleted[1].DeleteMarkerVersionId,
},
{
IsLatest: getBoolPtr(true),
Key: out.Deleted[2].Key,
VersionId: out.Deleted[2].DeleteMarkerVersionId,
},
{
IsLatest: getBoolPtr(true),
Key: out.Deleted[4].Key,
VersionId: out.Deleted[4].DeleteMarkerVersionId,
},
}
if !compareVersions(versions, res.Versions) {
return fmt.Errorf("expected the resulting versions to be %v, instead got %v",
versions, res.Versions)
}
if !compareDelMarkers(delMarkers, res.DeleteMarkers) {
return fmt.Errorf("expected the resulting delete markers to be %v, instead got %v",
delMarkers, res.DeleteMarkers)
}
return nil
}, withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_DeleteObjects_delete_deleteMarkers(s *S3Conf) error {
testName := "Versioning_DeleteObjects_delete_deleteMarkers"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
obj1, obj2 := "foo", "bar"
dir1, dir2 := "foo-dir/", "bar-dir/"
obj1Version, err := createObjVersions(s3client, bucket, obj1, 1)
if err != nil {
return err
}
obj2Version, err := createObjVersions(s3client, bucket, obj2, 1)
if err != nil {
return err
}
dir1Version, err := createObjVersions(s3client, bucket, dir1, 1)
if err != nil {
return err
}
dir2Version, err := createObjVersions(s3client, bucket, dir2, 1)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.DeleteObjects(ctx, &s3.DeleteObjectsInput{
Bucket: &bucket,
Delete: &types.Delete{
Objects: []types.ObjectIdentifier{
{
Key: obj1Version[0].Key,
},
{
Key: obj2Version[0].Key,
},
{
Key: dir1Version[0].Key,
},
{
Key: dir2Version[0].Key,
},
},
},
})
cancel()
if err != nil {
return err
}
delResult := []types.DeletedObject{
{
Key: obj1Version[0].Key,
DeleteMarker: getBoolPtr(true),
},
{
Key: obj2Version[0].Key,
DeleteMarker: getBoolPtr(true),
},
{
Key: dir1Version[0].Key,
DeleteMarker: getBoolPtr(true),
},
{
Key: dir2Version[0].Key,
DeleteMarker: getBoolPtr(true),
},
}
if len(out.Errors) != 0 {
return fmt.Errorf("errors occurred during the deletion: %v",
out.Errors)
}
if !compareDelObjects(delResult, out.Deleted) {
return fmt.Errorf("expected the deleted objects to be %v, instead got %v",
delResult, out.Deleted)
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.DeleteObjects(ctx, &s3.DeleteObjectsInput{
Bucket: &bucket,
Delete: &types.Delete{
Objects: []types.ObjectIdentifier{
{
Key: out.Deleted[0].Key,
VersionId: out.Deleted[0].VersionId,
},
{
Key: out.Deleted[1].Key,
VersionId: out.Deleted[1].VersionId,
},
{
Key: out.Deleted[2].Key,
VersionId: out.Deleted[2].VersionId,
},
{
Key: out.Deleted[3].Key,
VersionId: out.Deleted[3].VersionId,
},
},
},
})
cancel()
if err != nil {
return err
}
if len(out.Errors) != 0 {
return fmt.Errorf("errors occurred during the deletion: %v",
out.Errors)
}
delResult = []types.DeletedObject{
{
Key: out.Deleted[0].Key,
DeleteMarker: getBoolPtr(true),
DeleteMarkerVersionId: out.Deleted[0].VersionId,
VersionId: out.Deleted[0].VersionId,
},
{
Key: out.Deleted[1].Key,
DeleteMarker: getBoolPtr(true),
DeleteMarkerVersionId: out.Deleted[1].VersionId,
VersionId: out.Deleted[1].VersionId,
},
{
Key: out.Deleted[2].Key,
DeleteMarker: getBoolPtr(true),
DeleteMarkerVersionId: out.Deleted[2].VersionId,
VersionId: out.Deleted[2].VersionId,
},
{
Key: out.Deleted[3].Key,
DeleteMarker: getBoolPtr(true),
DeleteMarkerVersionId: out.Deleted[3].VersionId,
VersionId: out.Deleted[3].VersionId,
},
}
if !compareDelObjects(delResult, res.Deleted) {
return fmt.Errorf("expected the deleted objects to be %v, instead got %v",
delResult, res.Deleted)
}
return nil
}, withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_DeleteObjects_never_versioned_bucket(s *S3Conf) error {
testName := "Versioning_DeleteObjects_never_versioned_bucket"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
versionId := "01KF2YVN948NAZ4JJR4X1AAVRA"
objs := []string{"my-obj", "my-dir/"}
etags := map[string]string{}
objIds := []types.ObjectIdentifier{}
for _, obj := range objs {
out, err := putObjectWithData(objDataLen(obj, 10), &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
}, s3client)
if err != nil {
return err
}
etags[obj] = getString(out.res.ETag)
objIds = append(objIds, types.ObjectIdentifier{
Key: &obj,
VersionId: &versionId,
})
}
// the objects only have a null version, so deleting any other
// version succeeds without deleting the objects
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.DeleteObjects(ctx, &s3.DeleteObjectsInput{
Bucket: &bucket,
Delete: &types.Delete{
Objects: objIds,
},
})
cancel()
if err != nil {
return err
}
if len(res.Errors) != 0 {
return fmt.Errorf("expected no errors, instead got %v", res.Errors)
}
delResult := []types.DeletedObject{}
for _, objId := range objIds {
delResult = append(delResult, types.DeletedObject{
Key: objId.Key,
VersionId: objId.VersionId,
})
}
if !compareDelObjects(delResult, res.Deleted) {
return fmt.Errorf("expected the deleted objects to be %v, instead got %v",
delResult, res.Deleted)
}
return forEachKey(objs, func(obj string) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
head, err := s3client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: &bucket,
Key: &obj,
})
cancel()
if err != nil {
return err
}
if getString(head.ETag) != etags[obj] {
return fmt.Errorf("expected the ETag to be %v, instead got %v",
etags[obj], getString(head.ETag))
}
return nil
})
})
}
// Versioning_DeleteObjects_conditional checks the ETag conditions of
// DeleteObjects in a versioned bucket. The checks don't depend on the order
// of the results.
func Versioning_DeleteObjects_conditional(s *S3Conf) error {
testName := "Versioning_DeleteObjects_conditional"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
versions := map[string]types.ObjectVersion{}
for _, obj := range []string{"live", "marked", "versioned", "stale"} {
v, err := createObjVersions(s3client, bucket, obj, 1)
if err != nil {
return err
}
versions[obj] = v[0]
}
if _, err := createDeleteMarker(s3client, bucket, "marked"); err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.DeleteObjects(ctx, &s3.DeleteObjectsInput{
Bucket: &bucket,
Delete: &types.Delete{
Objects: []types.ObjectIdentifier{
{Key: getPtr("live"), ETag: versions["live"].ETag},
{Key: getPtr("marked"), ETag: getPtr("*")},
{Key: getPtr("missing"), ETag: getPtr("*")},
{Key: getPtr("versioned"), VersionId: versions["versioned"].VersionId, ETag: versions["versioned"].ETag},
{Key: getPtr("stale"), ETag: getPtr("00000000000000000000000000000000")},
},
},
})
cancel()
if err != nil {
return err
}
if len(out.Deleted) != 1 {
return fmt.Errorf("expected 1 deleted object, instead got %v", out.Deleted)
}
del := out.Deleted[0]
if getString(del.Key) != "live" || del.DeleteMarker == nil || !*del.DeleteMarker ||
getString(del.DeleteMarkerVersionId) == "" || del.VersionId != nil {
return fmt.Errorf("expected a delete marker to be created for live, instead got %+v", del)
}
wantErrs := map[string]s3err.S3Error{
"marked": s3err.GetAPIError(s3err.ErrNoSuchKey),
"missing": s3err.GetAPIError(s3err.ErrNoSuchKey),
"versioned": s3err.GetAPIError(s3err.ErrNotImplementedFormField),
"stale": s3err.GetAPIError(s3err.ErrPreconditionFailed),
}
if len(out.Errors) != len(wantErrs) {
return fmt.Errorf("expected %v errors, instead got %v", len(wantErrs), out.Errors)
}
for _, e := range out.Errors {
key := getString(e.Key)
wantErr, ok := wantErrs[key]
if !ok {
return fmt.Errorf("unexpected error for %v: %v", key, getString(e.Code))
}
if err := checkDeleteObjectsErr(e, key, wantErr); err != nil {
return err
}
if key == "versioned" && getString(e.VersionId) != getString(versions["versioned"].VersionId) {
return fmt.Errorf("expected the versioned error to name version %v, instead got %v",
getString(versions["versioned"].VersionId), getString(e.VersionId))
}
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.ListObjectVersions(ctx, &s3.ListObjectVersionsInput{
Bucket: &bucket,
})
cancel()
if err != nil {
return err
}
markers := map[string]int{}
for _, dm := range res.DeleteMarkers {
markers[getString(dm.Key)]++
}
if !maps.Equal(markers, map[string]int{"live": 1, "marked": 1}) {
return fmt.Errorf("expected one delete marker for live and marked, instead got %v", markers)
}
if len(res.Versions) != len(versions) {
return fmt.Errorf("expected %v versions, instead got %v", len(versions), len(res.Versions))
}
return nil
}, withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_Multipart_Upload_success(s *S3Conf) error {
testName := "Versioning_Multipart_Upload_success"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
obj := "my-obj"
out, err := createMp(s3client, bucket, obj)
if err != nil {
return err
}
objSize := int64(25 * 1024 * 1024)
parts, _, err := uploadParts(s3client, objSize, 5, bucket, obj, *out.UploadId)
if err != nil {
return err
}
compParts := []types.CompletedPart{}
for _, el := range parts {
compParts = append(compParts, types.CompletedPart{
ETag: el.ETag,
PartNumber: el.PartNumber,
})
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.CompleteMultipartUpload(ctx, &s3.CompleteMultipartUploadInput{
Bucket: &bucket,
Key: &obj,
UploadId: out.UploadId,
MultipartUpload: &types.CompletedMultipartUpload{
Parts: compParts,
},
})
cancel()
if err != nil {
return err
}
if res.Key == nil {
return fmt.Errorf("expected the object key to be %v, instead got nil",
obj)
}
if *res.Key != obj {
return fmt.Errorf("expected object key to be %v, instead got %v",
obj, *res.Key)
}
if res.Bucket == nil {
return fmt.Errorf("expected the bucket name to be %v, instead got nil",
bucket)
}
if *res.Bucket != bucket {
return fmt.Errorf("expected the bucket name to be %v, instead got %v",
bucket, *res.Bucket)
}
if res.ETag == nil || *res.ETag == "" {
return fmt.Errorf("expected non-empty ETag")
}
if res.VersionId == nil || *res.VersionId == "" {
return fmt.Errorf("expected non-empty versionId")
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
resp, err := s3client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: &bucket,
Key: &obj,
VersionId: res.VersionId,
})
cancel()
if err != nil {
return err
}
if resp.ETag == nil || *resp.ETag == "" {
return fmt.Errorf("expected (head object) non-empty ETag")
}
if *resp.ETag != *res.ETag {
return fmt.Errorf("expected the uploaded object etag to be %v, instead got %v",
*res.ETag, *resp.ETag)
}
if resp.ContentLength == nil {
return fmt.Errorf("expected (head object) non nil content length")
}
if *resp.ContentLength != int64(objSize) {
return fmt.Errorf("expected the uploaded object size to be %v, instead got %v",
objSize, resp.ContentLength)
}
if resp.VersionId == nil {
return fmt.Errorf("expected (head object) non nil versionId")
}
if *resp.VersionId != *res.VersionId {
return fmt.Errorf("expected the versionId to be %v, instead got %v",
*res.VersionId, *resp.VersionId)
}
return nil
}, withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_Multipart_Upload_overwrite_an_object(s *S3Conf) error {
testName := "Versioning_Multipart_Upload_overwrite_an_object"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
obj := "my-obj"
objVersions, err := createObjVersions(s3client, bucket, obj, 2)
if err != nil {
return err
}
out, err := createMp(s3client, bucket, obj)
if err != nil {
return err
}
objSize := int64(25 * 1024 * 1024)
parts, _, err := uploadParts(s3client, objSize, 5, bucket, obj, *out.UploadId)
if err != nil {
return err
}
compParts := []types.CompletedPart{}
for _, el := range parts {
compParts = append(compParts, types.CompletedPart{
ETag: el.ETag,
PartNumber: el.PartNumber,
})
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.CompleteMultipartUpload(ctx, &s3.CompleteMultipartUploadInput{
Bucket: &bucket,
Key: &obj,
UploadId: out.UploadId,
MultipartUpload: &types.CompletedMultipartUpload{
Parts: compParts,
},
})
cancel()
if err != nil {
return err
}
if res.Key == nil {
return fmt.Errorf("expected the object key to be %v, instead got nil",
obj)
}
if *res.Key != obj {
return fmt.Errorf("expected object key to be %v, instead got %v",
obj, *res.Key)
}
if res.Bucket == nil {
return fmt.Errorf("expected the bucket name to be %v, instead got nil",
bucket)
}
if *res.Bucket != bucket {
return fmt.Errorf("expected the bucket name to be %v, instead got %v",
bucket, *res.Bucket)
}
if res.ETag == nil || *res.ETag == "" {
return fmt.Errorf("expected non-empty ETag")
}
if res.VersionId == nil || *res.VersionId == "" {
return fmt.Errorf("expected non-empty versionId")
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
resp, err := s3client.ListObjectVersions(ctx, &s3.ListObjectVersionsInput{
Bucket: &bucket,
})
cancel()
if err != nil {
return err
}
size := int64(objSize)
objVersions[0].IsLatest = getBoolPtr(false)
versions := append([]types.ObjectVersion{
{
Key: &obj,
VersionId: res.VersionId,
ETag: res.ETag,
IsLatest: getBoolPtr(true),
Size: &size,
StorageClass: types.ObjectVersionStorageClassStandard,
},
}, objVersions...)
if !compareVersions(versions, resp.Versions) {
return fmt.Errorf("expected the resulting versions to be %v, instead got %v",
versions, resp.Versions)
}
return nil
}, withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_Multipart_Upload_suspended_overwrite_versioned_object(s *S3Conf) error {
testName := "Versioning_Multipart_Upload_suspended_overwrite_versioned_object"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
obj := "my-obj"
// the object becomes the null version once versioning is enabled
_, err := putObjectWithData(200, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
}, s3client)
if err != nil {
return err
}
err = putBucketVersioningStatus(s3client, bucket, types.BucketVersioningStatusEnabled)
if err != nil {
return err
}
// archives the null version
verLen := int64(300)
ver, err := putObjectWithData(verLen, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
}, s3client)
if err != nil {
return err
}
if getString(ver.res.VersionId) == "" {
return fmt.Errorf("expected non-empty versionId")
}
err = putBucketVersioningStatus(s3client, bucket, types.BucketVersioningStatusSuspended)
if err != nil {
return err
}
out, err := createMp(s3client, bucket, obj)
if err != nil {
return err
}
objSize := int64(400)
parts, _, err := uploadParts(s3client, objSize, 1, bucket, obj, *out.UploadId)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.CompleteMultipartUpload(ctx, &s3.CompleteMultipartUploadInput{
Bucket: &bucket,
Key: &obj,
UploadId: out.UploadId,
MultipartUpload: &types.CompletedMultipartUpload{
Parts: []types.CompletedPart{
{
ETag: parts[0].ETag,
PartNumber: parts[0].PartNumber,
},
},
},
})
cancel()
if err != nil {
return err
}
if res.VersionId != nil {
return fmt.Errorf("expected CompleteMultipartUpload response to omit versionId, instead got %v",
*res.VersionId)
}
// the new object replaces the null version and the versioned
// object it overwrites is kept
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
resp, err := s3client.ListObjectVersions(ctx, &s3.ListObjectVersionsInput{
Bucket: &bucket,
})
cancel()
if err != nil {
return err
}
versions := []types.ObjectVersion{
{
Key: &obj,
VersionId: &nullVersionId,
ETag: res.ETag,
IsLatest: getBoolPtr(true),
Size: &objSize,
StorageClass: types.ObjectVersionStorageClassStandard,
},
{
Key: &obj,
VersionId: ver.res.VersionId,
ETag: ver.res.ETag,
IsLatest: getBoolPtr(false),
Size: &verLen,
StorageClass: types.ObjectVersionStorageClassStandard,
ChecksumType: ver.res.ChecksumType,
},
}
if !compareVersions(versions, resp.Versions) {
return fmt.Errorf("expected the resulting versions to be %v, instead got %v",
versions, resp.Versions)
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
head, err := s3client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: &bucket,
Key: &obj,
})
cancel()
if err != nil {
return err
}
if getString(head.VersionId) != nullVersionId {
return fmt.Errorf("expected the current versionId to be %v, instead got %v",
nullVersionId, getString(head.VersionId))
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
get, err := s3client.GetObject(ctx, &s3.GetObjectInput{
Bucket: &bucket,
Key: &obj,
VersionId: ver.res.VersionId,
})
if err != nil {
cancel()
return err
}
bdy, err := io.ReadAll(get.Body)
get.Body.Close()
cancel()
if err != nil {
return err
}
if !isSameData(bdy, ver.data) {
return fmt.Errorf("expected the overwritten version %v data to be kept",
*ver.res.VersionId)
}
return nil
})
}
func Versioning_Multipart_Upload_overwrite_keeps_previous_version_metadata(s *S3Conf) error {
testName := "Versioning_Multipart_Upload_overwrite_keeps_previous_version_metadata"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
obj := "my-obj"
prevLen := int64(100)
prev, err := putObjectWithData(prevLen, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
ContentType: getPtr("text/plain"),
ContentDisposition: getPtr("inline"),
CacheControl: getPtr("no-cache"),
Metadata: map[string]string{"prev": "val"},
Tagging: getPtr("key=prev"),
}, s3client)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.CreateMultipartUpload(ctx, &s3.CreateMultipartUploadInput{
Bucket: &bucket,
Key: &obj,
ContentType: getPtr("application/json"),
ContentDisposition: getPtr("attachment"),
CacheControl: getPtr("max-age=60"),
Metadata: map[string]string{"mp": "val"},
Tagging: getPtr("key=mp"),
})
cancel()
if err != nil {
return err
}
objSize := int64(500)
parts, _, err := uploadParts(s3client, objSize, 1, bucket, obj, *out.UploadId)
if err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.CompleteMultipartUpload(ctx, &s3.CompleteMultipartUploadInput{
Bucket: &bucket,
Key: &obj,
UploadId: out.UploadId,
MultipartUpload: &types.CompletedMultipartUpload{
Parts: []types.CompletedPart{
{
ETag: parts[0].ETag,
PartNumber: parts[0].PartNumber,
},
},
},
})
cancel()
if err != nil {
return err
}
if getString(res.VersionId) == "" {
return fmt.Errorf("expected non-empty versionId")
}
for _, v := range []struct {
versionId *string
etag *string
size int64
contentType string
contentDisposition string
cacheControl string
metadata map[string]string
tag string
}{
{prev.res.VersionId, prev.res.ETag, prevLen, "text/plain", "inline", "no-cache", map[string]string{"prev": "val"}, "prev"},
{res.VersionId, res.ETag, objSize, "application/json", "attachment", "max-age=60", map[string]string{"mp": "val"}, "mp"},
} {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
head, err := s3client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: &bucket,
Key: &obj,
VersionId: v.versionId,
})
cancel()
if err != nil {
return fmt.Errorf("head version %v: %w", *v.versionId, err)
}
if getString(head.ETag) != getString(v.etag) {
return fmt.Errorf("version %v: expected the etag to be %v, instead got %v",
*v.versionId, getString(v.etag), getString(head.ETag))
}
if head.ContentLength == nil || *head.ContentLength != v.size {
return fmt.Errorf("version %v: expected the content length to be %v, instead got %v",
*v.versionId, v.size, head.ContentLength)
}
if getString(head.ContentType) != v.contentType {
return fmt.Errorf("version %v: expected the content type to be %v, instead got %v",
*v.versionId, v.contentType, getString(head.ContentType))
}
if getString(head.ContentDisposition) != v.contentDisposition {
return fmt.Errorf("version %v: expected the content disposition to be %v, instead got %v",
*v.versionId, v.contentDisposition, getString(head.ContentDisposition))
}
if getString(head.CacheControl) != v.cacheControl {
return fmt.Errorf("version %v: expected the cache control to be %v, instead got %v",
*v.versionId, v.cacheControl, getString(head.CacheControl))
}
if !areMapsSame(head.Metadata, v.metadata) {
return fmt.Errorf("version %v: expected the metadata to be %v, instead got %v",
*v.versionId, v.metadata, head.Metadata)
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
tagging, err := s3client.GetObjectTagging(ctx, &s3.GetObjectTaggingInput{
Bucket: &bucket,
Key: &obj,
VersionId: v.versionId,
})
cancel()
if err != nil {
return fmt.Errorf("get version %v tagging: %w", *v.versionId, err)
}
expectedTags := []types.Tag{{Key: getPtr("key"), Value: getPtr(v.tag)}}
if !areTagsSame(tagging.TagSet, expectedTags) {
return fmt.Errorf("version %v: expected the tags to be %v, instead got %v",
*v.versionId, expectedTags, tagging.TagSet)
}
}
return nil
}, withVersioning(types.BucketVersioningStatusEnabled))
}
// A delete marker as the current version leaves the key without an object:
// completing an upload with If-Match finds no key, while If-None-Match lets
// it through as a new version.
func Versioning_Multipart_Upload_conditional_writes_over_delete_marker(s *S3Conf) error {
testName := "Versioning_Multipart_Upload_conditional_writes_over_delete_marker"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
obj := "my-obj"
versions, err := createObjVersions(s3client, bucket, obj, 1)
if err != nil {
return err
}
if _, err := createDeleteMarker(s3client, bucket, obj); err != nil {
return err
}
for i, test := range []struct {
ifMatch *string
ifNoneMatch *string
err error
}{
{versions[0].ETag, nil, s3err.GetAPIError(s3err.ErrNoSuchKey)},
{nil, getPtr("*"), nil},
} {
mp, err := createMp(s3client, bucket, obj)
if err != nil {
return err
}
parts, _, err := uploadParts(s3client, 5*1024*1024, 1, bucket, obj, *mp.UploadId)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.CompleteMultipartUpload(ctx, &s3.CompleteMultipartUploadInput{
Bucket: &bucket,
Key: &obj,
UploadId: mp.UploadId,
MultipartUpload: &types.CompletedMultipartUpload{
Parts: []types.CompletedPart{
{
ETag: parts[0].ETag,
PartNumber: parts[0].PartNumber,
ChecksumCRC64NVME: parts[0].ChecksumCRC64NVME,
},
},
},
IfMatch: test.ifMatch,
IfNoneMatch: test.ifNoneMatch,
})
cancel()
if test.err != nil {
if err := checkApiErr(err, test.err.(s3err.APIError)); err != nil {
return fmt.Errorf("test case %v: %w", i, err)
}
continue
}
if err != nil {
return fmt.Errorf("test case %v: %w", i, err)
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
head, err := s3client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: &bucket,
Key: &obj,
})
cancel()
if err != nil {
return err
}
if getString(head.VersionId) != getString(out.VersionId) {
return fmt.Errorf("test case %v: expected the current version to be %v, instead got %v",
i, getString(out.VersionId), getString(head.VersionId))
}
}
return nil
}, withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_Multipart_Upload_already_completed(s *S3Conf) error {
testName := "Versioning_Multipart_Upload_already_completed"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
obj := "my-obj"
out, err := createMp(s3client, bucket, obj)
if err != nil {
return err
}
parts, _, err := uploadParts(s3client, 5*1024*1024, 1, bucket, obj, *out.UploadId)
if err != nil {
return err
}
completeInput := &s3.CompleteMultipartUploadInput{
Bucket: &bucket,
Key: &obj,
UploadId: out.UploadId,
MultipartUpload: &types.CompletedMultipartUpload{
Parts: []types.CompletedPart{
{ETag: parts[0].ETag, PartNumber: parts[0].PartNumber},
},
},
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
first, err := s3client.CompleteMultipartUpload(ctx, completeInput)
cancel()
if err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
second, err := s3client.CompleteMultipartUpload(ctx, completeInput)
cancel()
if err != nil {
return err
}
if getString(first.VersionId) == "" {
return fmt.Errorf("expected non-empty versionId")
}
if getString(second.VersionId) != getString(first.VersionId) {
return fmt.Errorf("expected the versionId to be %s, instead got %s",
getString(first.VersionId), getString(second.VersionId))
}
if getString(second.ETag) != getString(first.ETag) {
return fmt.Errorf("expected the ETag to be %s, instead got %s",
getString(first.ETag), getString(second.ETag))
}
return nil
}, withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_UploadPartCopy_invalid_versionId(s *S3Conf) error {
testName := "Versioning_UploadPartCopy_invalid_versionId"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
dstObj, srcObj := "dst-obj", "src-obj"
_, err := putObjectWithData(10, &s3.PutObjectInput{
Bucket: &bucket,
Key: &srcObj,
}, s3client)
if err != nil {
return err
}
mp, err := createMp(s3client, bucket, dstObj)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.UploadPartCopy(ctx, &s3.UploadPartCopyInput{
Bucket: &bucket,
Key: &dstObj,
UploadId: mp.UploadId,
PartNumber: getPtr(int32(1)),
CopySource: getPtr(fmt.Sprintf("%v/%v?versionId=invalid_versionId",
bucket, srcObj)),
})
cancel()
return checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgVersionId, "invalid_versionId"))
})
}
func Versioning_UploadPartCopy_encoded_versionid_separator_invalid_versionId(s *S3Conf) error {
testName := "Versioning_UploadPartCopy_encoded_versionid_separator_invalid_versionId"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
dstObj, srcObj := "dst-obj", "src-obj"
_, err := putObjectWithData(10, &s3.PutObjectInput{
Bucket: &bucket,
Key: &srcObj,
}, s3client)
if err != nil {
return err
}
mp, err := createMp(s3client, bucket, dstObj)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.UploadPartCopy(ctx, &s3.UploadPartCopyInput{
Bucket: &bucket,
Key: &dstObj,
UploadId: mp.UploadId,
PartNumber: getPtr(int32(1)),
CopySource: getPtr(fmt.Sprintf("%v/%v%%3FversionId%%3D..%%2f..%%2fsecret.txt", bucket, srcObj)),
})
cancel()
return checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgVersionId, "../../secret.txt"))
})
}
func Versioning_UploadPartCopy_non_existing_versionId(s *S3Conf) error {
testName := "Versioning_UploadPartCopy_non_existing_versionId"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
return forEachKey([]string{"src-obj", "src-dir/"}, func(srcObj string) error {
dstBucket, dstObj := getBucketName(), "dst-obj"
lgth := objDataLen(srcObj, 100)
_, err := putObjectWithData(lgth, &s3.PutObjectInput{
Bucket: &bucket,
Key: &srcObj,
}, s3client)
if err != nil {
return err
}
if err := setup(s, dstBucket); err != nil {
return err
}
mp, err := createMp(s3client, dstBucket, dstObj)
if err != nil {
return err
}
pNumber := int32(1)
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.UploadPartCopy(ctx, &s3.UploadPartCopyInput{
Bucket: &dstBucket,
Key: &dstObj,
UploadId: mp.UploadId,
PartNumber: &pNumber,
CopySource: getPtr(fmt.Sprintf("%v/%v?versionId=01BX5ZZKBKACTAV9WEVGEMMVS0",
bucket, srcObj)),
})
cancel()
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrNoSuchVersion)); err != nil {
return err
}
if err := teardown(s, dstBucket); err != nil {
return err
}
return nil
})
}, withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_UploadPartCopy_from_an_object_version(s *S3Conf) error {
testName := "Versioning_UploadPartCopy_from_an_object_version"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
return forEachKey([]string{"my-obj", "my-dir/"}, func(srcObj string) error {
dstBucket, obj := getBucketName(), "dst-obj"
err := setup(s, dstBucket)
if err != nil {
return err
}
// the latest version and a noncurrent one
srcObjVersions, err := createObjVersions(s3client, bucket, srcObj, 2)
if err != nil {
return err
}
for _, srcObjVersion := range srcObjVersions {
out, err := createMp(s3client, dstBucket, obj)
if err != nil {
return err
}
partNumber := int32(1)
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
copyOut, err := s3client.UploadPartCopy(ctx, &s3.UploadPartCopyInput{
Bucket: &dstBucket,
CopySource: getPtr(fmt.Sprintf("%v/%v?versionId=%v", bucket, srcObj, *srcObjVersion.VersionId)),
UploadId: out.UploadId,
Key: &obj,
PartNumber: &partNumber,
})
cancel()
if err != nil {
return err
}
if getString(copyOut.CopySourceVersionId) != getString(srcObjVersion.VersionId) {
return fmt.Errorf("expected the copy-source-version-id to be %v, instead got %v",
getString(srcObjVersion.VersionId), getString(copyOut.CopySourceVersionId))
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.ListParts(ctx, &s3.ListPartsInput{
Bucket: &dstBucket,
Key: &obj,
UploadId: out.UploadId,
})
cancel()
if err != nil {
return err
}
if len(res.Parts) != 1 {
return fmt.Errorf("expected parts to be 1, instead got %v",
len(res.Parts))
}
if res.Parts[0].PartNumber == nil {
return fmt.Errorf("expected part-number to be non nil")
}
if *res.Parts[0].PartNumber != partNumber {
return fmt.Errorf("expected part-number to be %v, instead got %v",
partNumber, res.Parts[0].PartNumber)
}
if res.Parts[0].Size == nil {
return fmt.Errorf("expected part size to be non nil")
}
if *res.Parts[0].Size != *srcObjVersion.Size {
return fmt.Errorf("expected part size to be %v, instead got %v",
*srcObjVersion.Size, res.Parts[0].Size)
}
if getString(res.Parts[0].ETag) != getString(copyOut.CopyPartResult.ETag) {
return fmt.Errorf("expected part etag to be %v, instead got %v",
getString(copyOut.CopyPartResult.ETag), getString(res.Parts[0].ETag))
}
}
if err := teardown(s, dstBucket); err != nil {
return err
}
return nil
})
}, withVersioning(types.BucketVersioningStatusEnabled))
}
// A copy source that resolves to a delete marker is rejected: the key has no
// current version when the marker is the latest, and naming the marker by
// version id is an invalid request. Versions the marker hides stay copyable.
func Versioning_UploadPartCopy_from_a_delete_marker(s *S3Conf) error {
testName := "Versioning_UploadPartCopy_from_a_delete_marker"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
return forEachKey([]string{"my-obj", "my-dir/"}, func(srcObj string) error {
dstBucket, dstObj := getBucketName(), "dst-obj"
if err := setup(s, dstBucket); err != nil {
return err
}
srcObjVersions, err := createObjVersions(s3client, bucket, srcObj, 1)
if err != nil {
return err
}
delMarker, err := createDeleteMarker(s3client, bucket, srcObj)
if err != nil {
return err
}
mp, err := createMp(s3client, dstBucket, dstObj)
if err != nil {
return err
}
partNumber := int32(1)
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.UploadPartCopy(ctx, &s3.UploadPartCopyInput{
Bucket: &dstBucket,
Key: &dstObj,
UploadId: mp.UploadId,
PartNumber: &partNumber,
CopySource: getPtr(fmt.Sprintf("%v/%v", bucket, srcObj)),
})
cancel()
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrNoSuchKey)); err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.UploadPartCopy(ctx, &s3.UploadPartCopyInput{
Bucket: &dstBucket,
Key: &dstObj,
UploadId: mp.UploadId,
PartNumber: &partNumber,
CopySource: getPtr(fmt.Sprintf("%v/%v?versionId=%v",
bucket, srcObj, delMarker)),
})
cancel()
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrCopySourceDeleteMarker)); err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.UploadPartCopy(ctx, &s3.UploadPartCopyInput{
Bucket: &dstBucket,
Key: &dstObj,
UploadId: mp.UploadId,
PartNumber: &partNumber,
CopySource: getPtr(fmt.Sprintf("%v/%v?versionId=%v",
bucket, srcObj, getString(srcObjVersions[0].VersionId))),
})
cancel()
if err != nil {
return err
}
if getString(out.CopySourceVersionId) != getString(srcObjVersions[0].VersionId) {
return fmt.Errorf("expected the copy-source-version-id to be %v, instead got %v",
getString(srcObjVersions[0].VersionId), getString(out.CopySourceVersionId))
}
return teardown(s, dstBucket)
})
}, withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_Enable_object_lock(s *S3Conf) error {
testName := "Versioning_Enable_object_lock"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.GetBucketVersioning(ctx, &s3.GetBucketVersioningInput{
Bucket: &bucket,
})
cancel()
if err != nil {
return err
}
if res.Status != types.BucketVersioningStatusEnabled {
return fmt.Errorf("expected the bucket versioning status to be %v, instead got %v",
types.BucketVersioningStatusEnabled, res.Status)
}
return nil
}, withLock())
}
func Versioning_object_lock_not_enabled_on_bucket_creation(s *S3Conf) error {
testName := "Versioning_not_enabled_on_bucket_creation"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.PutObjectLockConfiguration(ctx, &s3.PutObjectLockConfigurationInput{
Bucket: &bucket,
ObjectLockConfiguration: &types.ObjectLockConfiguration{
ObjectLockEnabled: types.ObjectLockEnabledEnabled,
Rule: &types.ObjectLockRule{
DefaultRetention: &types.DefaultRetention{
Mode: types.ObjectLockRetentionModeCompliance,
Days: getPtr(int32(10)),
},
},
},
})
cancel()
return checkApiErr(err, s3err.GetAPIError(s3err.ErrObjectLockConfigurationNotAllowed))
})
}
func Versioning_status_switch_to_suspended_with_object_lock(s *S3Conf) error {
testName := "Versioning_status_switch_to_suspended_with_object_lock"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
err := putBucketVersioningStatus(s3client, bucket, types.BucketVersioningStatusSuspended)
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrSuspendedVersioningNotAllowed)); err != nil {
return err
}
return nil
}, withLock())
}
func Versioning_PutObjectRetention_invalid_versionId(s *S3Conf) error {
testName := "Versioning_PutObjectRetention_invalid_versionId"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
obj := "my-obj"
_, err := createObjVersions(s3client, bucket, obj, 1)
if err != nil {
return err
}
rDate := time.Now().Add(time.Hour * 48)
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.PutObjectRetention(ctx, &s3.PutObjectRetentionInput{
Bucket: &bucket,
Key: &obj,
VersionId: getPtr("invalid_version_id"),
Retention: &types.ObjectLockRetention{
Mode: types.ObjectLockRetentionModeGovernance,
RetainUntilDate: &rDate,
},
})
cancel()
return checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgVersionId, "invalid_version_id"))
}, withLock())
}
func Versioning_PutObjectRetention_non_existing_object_version(s *S3Conf) error {
testName := "Versioning_PutObjectRetention_non_existing_object_version"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
return forEachKey([]string{"my-obj", "my-dir/"}, func(obj string) error {
_, err := createObjVersions(s3client, bucket, obj, 3)
if err != nil {
return err
}
rDate := time.Now().Add(time.Hour * 48)
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.PutObjectRetention(ctx, &s3.PutObjectRetentionInput{
Bucket: &bucket,
Key: &obj,
VersionId: getPtr("01G65Z755AFWAKHE12NY0CQ9FH"),
Retention: &types.ObjectLockRetention{
Mode: types.ObjectLockRetentionModeGovernance,
RetainUntilDate: &rDate,
},
})
cancel()
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrNoSuchVersion)); err != nil {
return err
}
return nil
})
}, withLock(), withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_GetObjectRetention_invalid_versionId(s *S3Conf) error {
testName := "Versioning_GetObjectRetention_invalid_versionId"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
obj := "my-obj"
_, err := createObjVersions(s3client, bucket, obj, 1)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.GetObjectRetention(ctx, &s3.GetObjectRetentionInput{
Bucket: &bucket,
Key: &obj,
VersionId: getPtr("invalid_versionId"),
})
cancel()
return checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgVersionId, "invalid_versionId"))
}, withLock())
}
func Versioning_GetObjectRetention_non_existing_object_version(s *S3Conf) error {
testName := "Versioning_GetObjectRetention_non_existing_object_version"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
return forEachKey([]string{"my-obj", "my-dir/"}, func(obj string) error {
_, err := createObjVersions(s3client, bucket, obj, 3)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.GetObjectRetention(ctx, &s3.GetObjectRetentionInput{
Bucket: &bucket,
Key: &obj,
VersionId: getPtr("01G65Z755AFWAKHE12NY0CQ9FH"),
})
cancel()
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrNoSuchVersion)); err != nil {
return err
}
return nil
})
}, withLock(), withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_Put_GetObjectRetention_delete_marker(s *S3Conf) error {
testName := "Versioning_Put_GetObjectRetention_delete_marker"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
return forEachKey([]string{"my-object", "my-dir/"}, func(obj string) error {
_, err := putObjectWithData(objDataLen(obj, 10), &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
}, s3client)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: &bucket,
Key: &obj,
})
cancel()
if err != nil {
return err
}
// PutObjectRetention
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.PutObjectRetention(ctx, &s3.PutObjectRetentionInput{
Bucket: &bucket,
Key: &obj,
VersionId: out.VersionId,
Retention: &types.ObjectLockRetention{
Mode: types.ObjectLockRetentionModeCompliance,
RetainUntilDate: getPtr(time.Now().AddDate(1, 0, 0)),
},
})
cancel()
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrMethodNotAllowed)); err != nil {
return err
}
// GetObjectRetention
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.GetObjectRetention(ctx, &s3.GetObjectRetentionInput{
Bucket: &bucket,
Key: &obj,
VersionId: out.VersionId,
})
cancel()
return checkApiErr(err, s3err.GetAPIError(s3err.ErrMethodNotAllowed))
})
}, withLock())
}
func Versioning_Put_GetObjectRetention_success(s *S3Conf) error {
testName := "Versioning_Put_GetObjectRetention_success"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
return forEachKey([]string{"my-obj", "my-dir/"}, func(obj string) error {
objVersions, err := createObjVersions(s3client, bucket, obj, 3)
if err != nil {
return err
}
objVersion := objVersions[1]
rDate := time.Now().Add(time.Hour * 48)
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.PutObjectRetention(ctx, &s3.PutObjectRetentionInput{
Bucket: &bucket,
Key: &obj,
VersionId: objVersion.VersionId,
Retention: &types.ObjectLockRetention{
Mode: types.ObjectLockRetentionModeGovernance,
RetainUntilDate: &rDate,
},
})
cancel()
if err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.GetObjectRetention(ctx, &s3.GetObjectRetentionInput{
Bucket: &bucket,
Key: &obj,
VersionId: objVersion.VersionId,
})
cancel()
if err != nil {
return err
}
if res.Retention.Mode != types.ObjectLockRetentionModeGovernance {
return fmt.Errorf("expected the object retention mode to be %v, instead got %v",
types.ObjectLockRetentionModeGovernance, res.Retention.Mode)
}
return cleanupLockedObjects(s3client, bucket, []objToDelete{{key: getString(objVersion.Key), versionId: getString(objVersion.VersionId)}})
})
}, withLock(), withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_PutObjectLegalHold_invalid_versionId(s *S3Conf) error {
testName := "Versioning_PutObjectLegalHold_invalid_versionId"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
obj := "my-obj"
_, err := createObjVersions(s3client, bucket, obj, 1)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.PutObjectLegalHold(ctx, &s3.PutObjectLegalHoldInput{
Bucket: &bucket,
Key: &obj,
VersionId: getPtr("invalid_version_id"),
LegalHold: &types.ObjectLockLegalHold{
Status: types.ObjectLockLegalHoldStatusOn,
},
})
cancel()
return checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgVersionId, "invalid_version_id"))
}, withLock())
}
func Versioning_PutObjectLegalHold_non_existing_object_version(s *S3Conf) error {
testName := "Versioning_PutObjectLegalHold_non_existing_object_version"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
return forEachKey([]string{"my-obj", "my-dir/"}, func(obj string) error {
_, err := createObjVersions(s3client, bucket, obj, 3)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.PutObjectLegalHold(ctx, &s3.PutObjectLegalHoldInput{
Bucket: &bucket,
Key: &obj,
VersionId: getPtr("01G65Z755AFWAKHE12NY0CQ9FH"),
LegalHold: &types.ObjectLockLegalHold{
Status: types.ObjectLockLegalHoldStatusOn,
},
})
cancel()
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrNoSuchVersion)); err != nil {
return err
}
return nil
})
}, withLock(), withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_GetObjectLegalHold_invalid_versionId(s *S3Conf) error {
testName := "Versioning_GetObjectLegalHold_invalid_versionId"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
obj := "my-obj"
_, err := createObjVersions(s3client, bucket, obj, 3)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.GetObjectLegalHold(ctx, &s3.GetObjectLegalHoldInput{
Bucket: &bucket,
Key: &obj,
VersionId: getPtr("invalid_version_id"),
})
cancel()
return checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgVersionId, "invalid_version_id"))
}, withLock())
}
func Versioning_GetObjectLegalHold_non_existing_object_version(s *S3Conf) error {
testName := "Versioning_GetObjectLegalHold_non_existing_object_version"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
return forEachKey([]string{"my-obj", "my-dir/"}, func(obj string) error {
_, err := createObjVersions(s3client, bucket, obj, 3)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.GetObjectLegalHold(ctx, &s3.GetObjectLegalHoldInput{
Bucket: &bucket,
Key: &obj,
VersionId: getPtr("01G65Z755AFWAKHE12NY0CQ9FH"),
})
cancel()
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrNoSuchVersion)); err != nil {
return err
}
return nil
})
}, withLock(), withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_PutGetObjectLegalHold_delete_marker(s *S3Conf) error {
testName := "Versioning_PutGetObjectLegalHold_delete_marker"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
return forEachKey([]string{"my-object", "my-dir/"}, func(obj string) error {
_, err := putObjectWithData(objDataLen(obj, 10), &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
}, s3client)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: &bucket,
Key: &obj,
})
cancel()
if err != nil {
return err
}
// PutObjectLegalHold
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.PutObjectLegalHold(ctx, &s3.PutObjectLegalHoldInput{
Bucket: &bucket,
Key: &obj,
VersionId: out.VersionId,
LegalHold: &types.ObjectLockLegalHold{
Status: types.ObjectLockLegalHoldStatusOn,
},
})
cancel()
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrMethodNotAllowed)); err != nil {
return err
}
// GetObjectLegalHold
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.GetObjectLegalHold(ctx, &s3.GetObjectLegalHoldInput{
Bucket: &bucket,
Key: &obj,
VersionId: out.VersionId,
})
cancel()
return checkApiErr(err, s3err.GetAPIError(s3err.ErrMethodNotAllowed))
})
}, withLock())
}
func Versioning_Put_GetObjectLegalHold_success(s *S3Conf) error {
testName := "Versioning_Put_GetObjectLegalHold_success"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
return forEachKey([]string{"my-obj", "my-dir/"}, func(obj string) error {
objVersions, err := createObjVersions(s3client, bucket, obj, 3)
if err != nil {
return err
}
objVersion := objVersions[1]
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.PutObjectLegalHold(ctx, &s3.PutObjectLegalHoldInput{
Bucket: &bucket,
Key: &obj,
VersionId: objVersion.VersionId,
LegalHold: &types.ObjectLockLegalHold{
Status: types.ObjectLockLegalHoldStatusOn,
},
})
cancel()
if err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.GetObjectLegalHold(ctx, &s3.GetObjectLegalHoldInput{
Bucket: &bucket,
Key: &obj,
VersionId: objVersion.VersionId,
})
cancel()
if err != nil {
return err
}
if res.LegalHold.Status != types.ObjectLockLegalHoldStatusOn {
return fmt.Errorf("expected the object version legal hold status to be %v, instead got %v",
types.ObjectLockLegalHoldStatusOn, res.LegalHold.Status)
}
return cleanupLockedObjects(s3client, bucket, []objToDelete{
{
key: getString(objVersion.Key),
versionId: getString(objVersion.VersionId),
removeOnlyLeglHold: true,
},
})
})
}, withLock(), withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_WORM_obj_version_locked_with_legal_hold(s *S3Conf) error {
testName := "Versioning_WORM_obj_version_locked_with_legal_hold"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
return forEachKey([]string{"my-obj", "my-dir/"}, func(obj string) error {
objVersions, err := createObjVersions(s3client, bucket, obj, 2)
if err != nil {
return err
}
version := objVersions[1]
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.PutObjectLegalHold(ctx, &s3.PutObjectLegalHoldInput{
Bucket: &bucket,
Key: &obj,
VersionId: version.VersionId,
LegalHold: &types.ObjectLockLegalHold{
Status: types.ObjectLockLegalHoldStatusOn,
},
})
cancel()
if err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: &bucket,
Key: &obj,
VersionId: version.VersionId,
})
cancel()
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrObjectLocked)); err != nil {
return err
}
return cleanupLockedObjects(s3client, bucket, []objToDelete{
{
key: obj,
versionId: getString(version.VersionId),
removeOnlyLeglHold: true,
},
})
})
}, withLock(), withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_WORM_dir_object_lock_headers(s *S3Conf) error {
testName := "Versioning_WORM_dir_object_lock_headers"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
obj := "my-dir/"
rDate := time.Now().Add(time.Hour * 48)
lockedObjs := []objToDelete{}
for i, test := range []struct {
legalHold types.ObjectLockLegalHoldStatus
mode types.ObjectLockMode
retainUntilDate *time.Time
}{
{legalHold: types.ObjectLockLegalHoldStatusOn},
{mode: types.ObjectLockModeGovernance, retainUntilDate: &rDate},
} {
res, err := putObjectWithData(0, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
ChecksumAlgorithm: types.ChecksumAlgorithmCrc32,
ObjectLockLegalHoldStatus: test.legalHold,
ObjectLockMode: test.mode,
ObjectLockRetainUntilDate: test.retainUntilDate,
}, s3client)
if err != nil {
return fmt.Errorf("test %v: %w", i+1, err)
}
lockedObjs = append(lockedObjs, objToDelete{
key: obj,
versionId: getString(res.res.VersionId),
removeOnlyLeglHold: test.legalHold == types.ObjectLockLegalHoldStatusOn,
})
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: &bucket,
Key: &obj,
})
cancel()
if err != nil {
return fmt.Errorf("test %v: %w", i+1, err)
}
if out.ObjectLockLegalHoldStatus != test.legalHold {
return fmt.Errorf("test %v: expected the object legal hold status to be %q, instead got %q",
i+1, test.legalHold, out.ObjectLockLegalHoldStatus)
}
if out.ObjectLockMode != test.mode {
return fmt.Errorf("test %v: expected the object lock mode to be %q, instead got %q",
i+1, test.mode, out.ObjectLockMode)
}
if test.retainUntilDate == nil {
if out.ObjectLockRetainUntilDate != nil {
return fmt.Errorf("test %v: expected nil object lock retain until date, instead got %v",
i+1, *out.ObjectLockRetainUntilDate)
}
} else if out.ObjectLockRetainUntilDate == nil ||
out.ObjectLockRetainUntilDate.Unix() != test.retainUntilDate.Unix() {
return fmt.Errorf("test %v: expected the object lock retain until date to be %v, instead got %v",
i+1, test.retainUntilDate.Format(time.RFC3339), out.ObjectLockRetainUntilDate)
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: &bucket,
Key: &obj,
VersionId: res.res.VersionId,
})
cancel()
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrObjectLocked)); err != nil {
return fmt.Errorf("test %v: %w", i+1, err)
}
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: &bucket,
Key: &obj,
})
cancel()
if err != nil {
return err
}
if out.DeleteMarker == nil || !*out.DeleteMarker {
return fmt.Errorf("expected the delete marker to be true, instead got %v", out.DeleteMarker)
}
if getString(out.VersionId) == "" {
return fmt.Errorf("expected non empty delete marker versionId")
}
// the noncurrent versions keep their lock settings
for _, lockedObj := range lockedObjs {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: &bucket,
Key: &obj,
VersionId: &lockedObj.versionId,
})
cancel()
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrObjectLocked)); err != nil {
return fmt.Errorf("version %v: %w", lockedObj.versionId, err)
}
}
return cleanupLockedObjects(s3client, bucket, lockedObjs)
}, withLock())
}
func Versioning_WORM_obj_version_locked_with_governance_retention(s *S3Conf) error {
testName := "Versioning_WORM_obj_version_locked_with_governance_retention"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
return forEachKey([]string{"my-obj", "my-dir/"}, func(obj string) error {
objVersions, err := createObjVersions(s3client, bucket, obj, 2)
if err != nil {
return err
}
version := objVersions[0]
rDate := time.Now().Add(time.Hour * 48)
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.PutObjectRetention(ctx, &s3.PutObjectRetentionInput{
Bucket: &bucket,
Key: &obj,
VersionId: version.VersionId,
Retention: &types.ObjectLockRetention{
Mode: types.ObjectLockRetentionModeGovernance,
RetainUntilDate: &rDate,
},
})
cancel()
if err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: &bucket,
Key: &obj,
VersionId: version.VersionId,
})
cancel()
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrObjectLocked)); err != nil {
return err
}
return cleanupLockedObjects(s3client, bucket, []objToDelete{
{
key: obj,
versionId: getString(version.VersionId),
},
})
})
}, withLock(), withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_WORM_obj_version_locked_with_compliance_retention(s *S3Conf) error {
testName := "Versioning_WORM_obj_version_locked_with_compliance_retention"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
// COMPLIANCE retentions can only be waited out, so the locked
// versions of all the keys are cleaned up together
lockedObjs := []objToDelete{}
err := forEachKey([]string{"my-obj", "my-dir/"}, func(obj string) error {
objVersions, err := createObjVersions(s3client, bucket, obj, 2)
if err != nil {
return err
}
version := objVersions[0]
rDate := time.Now().Add(2 * complianceTestRetention)
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.PutObjectRetention(ctx, &s3.PutObjectRetentionInput{
Bucket: &bucket,
Key: &obj,
VersionId: version.VersionId,
Retention: &types.ObjectLockRetention{
Mode: types.ObjectLockRetentionModeCompliance,
RetainUntilDate: &rDate,
},
})
cancel()
if err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: &bucket,
Key: &obj,
VersionId: version.VersionId,
})
cancel()
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrObjectLocked)); err != nil {
return err
}
lockedObjs = append(lockedObjs, objToDelete{
key: obj,
versionId: getString(version.VersionId),
isCompliance: true,
})
return nil
})
if err != nil {
return err
}
return cleanupLockedObjects(s3client, bucket, lockedObjs)
}, withLock(), withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_WORM_delete_marker_locked_object_legal_hold(s *S3Conf) error {
testName := "Versioning_WORM_delete_marker_locked_object_legal_hold"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
return forEachKey([]string{"my-obj", "my-dir/"}, func(obj string) error {
objVersions, err := createObjVersions(s3client, bucket, obj, 1)
if err != nil {
return err
}
version := objVersions[0]
objVersions[0].IsLatest = getPtr(false)
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.PutObjectLegalHold(ctx, &s3.PutObjectLegalHoldInput{
Bucket: &bucket,
Key: &obj,
LegalHold: &types.ObjectLockLegalHold{
Status: types.ObjectLockLegalHoldStatusOn,
},
})
cancel()
if err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: &bucket,
Key: &obj,
})
cancel()
if err != nil {
return err
}
delMarkers := []types.DeleteMarkerEntry{
{
IsLatest: getPtr(true),
Key: &obj,
VersionId: out.VersionId,
},
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
resp, err := s3client.ListObjectVersions(ctx, &s3.ListObjectVersionsInput{
Bucket: &bucket,
Prefix: &obj,
})
cancel()
if err != nil {
return err
}
if !compareVersions(objVersions, resp.Versions) {
return fmt.Errorf("expected the object versions to be %v, instead got %v", objVersions, resp.Versions)
}
if !compareDelMarkers(delMarkers, resp.DeleteMarkers) {
return fmt.Errorf("expected the object delete markers to be %v, instead got %v", delMarkers, resp.DeleteMarkers)
}
return cleanupLockedObjects(s3client, bucket, []objToDelete{
{
key: obj,
versionId: getString(version.VersionId),
removeOnlyLeglHold: true,
},
})
})
}, withLock())
}
func Versioning_WORM_delete_marker_locked_object_governance_retention(s *S3Conf) error {
testName := "Versioning_WORM_delete_marker_locked_object_governance_retention"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
return forEachKey([]string{"my-obj", "my-dir/"}, func(obj string) error {
objVersions, err := createObjVersions(s3client, bucket, obj, 1)
if err != nil {
return err
}
version := objVersions[0]
objVersions[0].IsLatest = getPtr(false)
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.PutObjectRetention(ctx, &s3.PutObjectRetentionInput{
Bucket: &bucket,
Key: &obj,
Retention: &types.ObjectLockRetention{
Mode: types.ObjectLockRetentionModeGovernance,
RetainUntilDate: getPtr(time.Now().AddDate(1, 0, 0)),
},
})
cancel()
if err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: &bucket,
Key: &obj,
})
cancel()
if err != nil {
return err
}
delMarkers := []types.DeleteMarkerEntry{
{
IsLatest: getPtr(true),
Key: &obj,
VersionId: out.VersionId,
},
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
resp, err := s3client.ListObjectVersions(ctx, &s3.ListObjectVersionsInput{
Bucket: &bucket,
Prefix: &obj,
})
cancel()
if err != nil {
return err
}
if !compareVersions(objVersions, resp.Versions) {
return fmt.Errorf("expected the object versions to be %v, instead got %v", objVersions, resp.Versions)
}
if !compareDelMarkers(delMarkers, resp.DeleteMarkers) {
return fmt.Errorf("expected the object delete markers to be %v, instead got %v", delMarkers, resp.DeleteMarkers)
}
return cleanupLockedObjects(s3client, bucket, []objToDelete{
{
key: obj,
versionId: getString(version.VersionId),
isCompliance: false,
},
})
})
}, withLock())
}
func Versioning_WORM_delete_marker_locked_object_compliance_retention(s *S3Conf) error {
testName := "Versioning_WORM_delete_marker_locked_object_compliance_retention"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
// COMPLIANCE retentions can only be waited out, so the locked
// versions of all the keys are cleaned up together
lockedObjs := []objToDelete{}
err := forEachKey([]string{"my-obj", "my-dir/"}, func(obj string) error {
objVersions, err := createObjVersions(s3client, bucket, obj, 1)
if err != nil {
return err
}
version := objVersions[0]
objVersions[0].IsLatest = getPtr(false)
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.PutObjectRetention(ctx, &s3.PutObjectRetentionInput{
Bucket: &bucket,
Key: &obj,
Retention: &types.ObjectLockRetention{
Mode: types.ObjectLockRetentionModeCompliance,
RetainUntilDate: getPtr(time.Now().Add(complianceTestRetention)),
},
})
cancel()
if err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: &bucket,
Key: &obj,
})
cancel()
if err != nil {
return err
}
delMarkers := []types.DeleteMarkerEntry{
{
IsLatest: getPtr(true),
Key: &obj,
VersionId: out.VersionId,
},
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
resp, err := s3client.ListObjectVersions(ctx, &s3.ListObjectVersionsInput{
Bucket: &bucket,
Prefix: &obj,
})
cancel()
if err != nil {
return err
}
if !compareVersions(objVersions, resp.Versions) {
return fmt.Errorf("expected the object versions to be %v, instead got %v", objVersions, resp.Versions)
}
if !compareDelMarkers(delMarkers, resp.DeleteMarkers) {
return fmt.Errorf("expected the object delete markers to be %v, instead got %v", delMarkers, resp.DeleteMarkers)
}
lockedObjs = append(lockedObjs, objToDelete{
key: obj,
versionId: getString(version.VersionId),
isCompliance: true,
})
return nil
})
if err != nil {
return err
}
return cleanupLockedObjects(s3client, bucket, lockedObjs)
}, withLock())
}
func Versioning_WORM_PutObject_overwrite_locked_object(s *S3Conf) error {
testName := "Versioning_WORM_PutObject_overwrite_locked_object"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
return forEachKey([]string{"my-obj", "my-dir/"}, func(obj string) error {
versions, err := createObjVersions(s3client, bucket, obj, 1)
if err != nil {
return err
}
v := versions[0]
v.IsLatest = getPtr(false)
// lock the object with legal hold
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.PutObjectLegalHold(ctx, &s3.PutObjectLegalHoldInput{
Bucket: &bucket,
Key: &obj,
LegalHold: &types.ObjectLockLegalHold{
Status: types.ObjectLockLegalHoldStatusOn,
},
})
cancel()
if err != nil {
return err
}
dataLen := objDataLen(obj, 10)
// overwrite the locked object with a new version
r, err := putObjectWithData(dataLen, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
}, s3client)
if err != nil {
return err
}
version := types.ObjectVersion{
ETag: r.res.ETag,
IsLatest: getPtr(true),
Key: &obj,
Size: &dataLen,
VersionId: r.res.VersionId,
StorageClass: types.ObjectVersionStorageClassStandard,
ChecksumType: r.res.ChecksumType,
}
result := []types.ObjectVersion{version, v}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.ListObjectVersions(ctx, &s3.ListObjectVersionsInput{
Bucket: &bucket,
Prefix: &obj,
})
cancel()
if err != nil {
return err
}
if !compareVersions(result, out.Versions) {
return fmt.Errorf("expected the object versions to be %v, instead got %v", result, out.Versions)
}
return cleanupLockedObjects(s3client, bucket, []objToDelete{
{
key: obj,
versionId: getString(v.VersionId),
removeOnlyLeglHold: true,
},
})
})
}, withLock())
}
func Versioning_WORM_CopyObject_overwrite_locked_object(s *S3Conf) error {
testName := "Versioning_WORM_CopyObject_overwrite_locked_object"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
return forEachKey([]string{"my-obj", "my-dir/"}, func(obj string) error {
versions, err := createObjVersions(s3client, bucket, obj, 1)
if err != nil {
return err
}
v := versions[0]
v.IsLatest = getPtr(false)
// lock the object with legal hold
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.PutObjectLegalHold(ctx, &s3.PutObjectLegalHoldInput{
Bucket: &bucket,
Key: &obj,
LegalHold: &types.ObjectLockLegalHold{
Status: types.ObjectLockLegalHoldStatusOn,
},
})
cancel()
if err != nil {
return err
}
// create a source object version, a directory object for a
// directory object destination
srcObj := "source-object"
if strings.HasSuffix(obj, "/") {
srcObj = "source-dir/"
}
srcVersions, err := createObjVersions(s3client, bucket, srcObj, 1)
if err != nil {
return err
}
srcVersion := srcVersions[0]
// overwrite the locked object with a new version with CopyObject
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
copyResult, err := s3client.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: &bucket,
Key: &obj,
CopySource: getPtr(fmt.Sprintf("%s/%s", bucket, srcObj)),
})
cancel()
if err != nil {
return err
}
version := types.ObjectVersion{
ETag: copyResult.CopyObjectResult.ETag,
IsLatest: getPtr(true),
Key: &obj,
Size: srcVersion.Size,
VersionId: copyResult.VersionId,
StorageClass: types.ObjectVersionStorageClassStandard,
ChecksumType: copyResult.CopyObjectResult.ChecksumType,
}
// the destination and the source versions are listed separately,
// as the bucket also holds the versions of the other keys
for _, result := range [][]types.ObjectVersion{{version, v}, {srcVersion}} {
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.ListObjectVersions(ctx, &s3.ListObjectVersionsInput{
Bucket: &bucket,
Prefix: result[0].Key,
})
cancel()
if err != nil {
return err
}
if !compareVersions(result, out.Versions) {
return fmt.Errorf("expected the object versions to be %v, instead got %v", result, out.Versions)
}
}
return cleanupLockedObjects(s3client, bucket, []objToDelete{
{
key: obj,
versionId: getString(v.VersionId),
removeOnlyLeglHold: true,
},
})
})
}, withLock())
}
// A copy of a locked object onto itself creates a new version, leaving the
// locked one and its legal hold in place.
func Versioning_WORM_CopyObject_to_itself_locked_object(s *S3Conf) error {
testName := "Versioning_WORM_CopyObject_to_itself_locked_object"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
return forEachKey([]string{"my-obj", "my-dir/"}, func(obj string) error {
versions, err := createObjVersions(s3client, bucket, obj, 1)
if err != nil {
return err
}
v := versions[0]
v.IsLatest = getPtr(false)
lockedVersionId := getString(v.VersionId)
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.PutObjectLegalHold(ctx, &s3.PutObjectLegalHoldInput{
Bucket: &bucket,
Key: &obj,
LegalHold: &types.ObjectLockLegalHold{
Status: types.ObjectLockLegalHoldStatusOn,
},
})
cancel()
if err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
copyResult, err := s3client.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: &bucket,
Key: &obj,
CopySource: getPtr(fmt.Sprintf("%v/%v", bucket, obj)),
MetadataDirective: types.MetadataDirectiveReplace,
ContentType: getPtr("application/json"),
})
cancel()
if err != nil {
return err
}
if getString(copyResult.VersionId) == lockedVersionId {
return fmt.Errorf("expected a new versionId, instead got %v",
getString(copyResult.VersionId))
}
version := types.ObjectVersion{
ETag: copyResult.CopyObjectResult.ETag,
IsLatest: getPtr(true),
Key: &obj,
Size: v.Size,
VersionId: copyResult.VersionId,
StorageClass: types.ObjectVersionStorageClassStandard,
ChecksumType: copyResult.CopyObjectResult.ChecksumType,
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.ListObjectVersions(ctx, &s3.ListObjectVersionsInput{
Bucket: &bucket,
Prefix: &obj,
})
cancel()
if err != nil {
return err
}
if !compareVersions([]types.ObjectVersion{version, v}, out.Versions) {
return fmt.Errorf("expected the object versions to be %v, instead got %v",
[]types.ObjectVersion{version, v}, out.Versions)
}
// the legal hold stays on the version it was set on
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
lhRes, err := s3client.GetObjectLegalHold(ctx, &s3.GetObjectLegalHoldInput{
Bucket: &bucket,
Key: &obj,
VersionId: &lockedVersionId,
})
cancel()
if err != nil {
return err
}
if lhRes.LegalHold.Status != types.ObjectLockLegalHoldStatusOn {
return fmt.Errorf("expected the legal hold status to be %v, instead got %v",
types.ObjectLockLegalHoldStatusOn, lhRes.LegalHold.Status)
}
return cleanupLockedObjects(s3client, bucket, []objToDelete{
{
key: obj,
versionId: lockedVersionId,
removeOnlyLeglHold: true,
},
})
})
}, withLock())
}
func Versioning_WORM_CompleteMultipartUpload_overwrite_locked_object(s *S3Conf) error {
testName := "Versioning_WORM_CompleteMultipartUpload_overwrite_locked_object"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
obj := "my-obj"
versions, err := createObjVersions(s3client, bucket, obj, 1)
if err != nil {
return err
}
v := versions[0]
v.IsLatest = getPtr(false)
// lock the object with legal hold
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.PutObjectLegalHold(ctx, &s3.PutObjectLegalHoldInput{
Bucket: &bucket,
Key: &obj,
LegalHold: &types.ObjectLockLegalHold{
Status: types.ObjectLockLegalHoldStatusOn,
},
})
cancel()
if err != nil {
return err
}
dataLen := int64(5 * 1024 * 1024)
// overwrite the locked object with a new version
mp, err := createMp(s3client, bucket, obj)
if err != nil {
return err
}
parts, _, err := uploadParts(s3client, dataLen, 1, bucket, obj, *mp.UploadId)
if err != nil {
return err
}
part := parts[0]
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.CompleteMultipartUpload(ctx, &s3.CompleteMultipartUploadInput{
Bucket: &bucket,
Key: &obj,
MultipartUpload: &types.CompletedMultipartUpload{
Parts: []types.CompletedPart{
{
ETag: part.ETag,
PartNumber: part.PartNumber,
},
},
},
UploadId: mp.UploadId,
})
cancel()
if err != nil {
return err
}
version := types.ObjectVersion{
ETag: res.ETag,
IsLatest: getPtr(true),
Key: &obj,
Size: &dataLen,
VersionId: res.VersionId,
StorageClass: types.ObjectVersionStorageClassStandard,
}
result := []types.ObjectVersion{version, v}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.ListObjectVersions(ctx, &s3.ListObjectVersionsInput{
Bucket: &bucket,
})
cancel()
if err != nil {
return err
}
if !compareVersions(result, out.Versions) {
return fmt.Errorf("expected the object versions to be %v, instead got %v", result, out.Versions)
}
return cleanupLockedObjects(s3client, bucket, []objToDelete{
{
key: obj,
versionId: getString(v.VersionId),
removeOnlyLeglHold: true,
},
})
}, withLock())
}
func Versioning_WORM_remove_delete_marker_under_bucket_default_retention(s *S3Conf) error {
testName := "Versioning_WORM_remove_delete_marker_under_bucket_default_retention"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.PutObjectLockConfiguration(ctx, &s3.PutObjectLockConfigurationInput{
Bucket: &bucket,
ObjectLockConfiguration: &types.ObjectLockConfiguration{
ObjectLockEnabled: types.ObjectLockEnabledEnabled,
Rule: &types.ObjectLockRule{
DefaultRetention: &types.DefaultRetention{
Mode: types.ObjectLockRetentionModeGovernance,
Days: getPtr(int32(5)),
},
},
},
})
cancel()
if err != nil {
return err
}
return forEachKey([]string{"my-object", "my-dir/"}, func(obj string) error {
// a default retention rule gives every upload Object Lock
// parameters, which need a checksum of its body
versions, err := createObjVersions(s3client, bucket, obj, 3, withChecksumAlgo(types.ChecksumAlgorithmCrc32))
if err != nil {
return err
}
// Create a delete marker
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: &bucket,
Key: &obj,
})
cancel()
if err != nil {
return err
}
// Delete the delete marker
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: &bucket,
Key: &obj,
VersionId: out.VersionId,
})
cancel()
if err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
resp, err := s3client.ListObjectVersions(ctx, &s3.ListObjectVersionsInput{
Bucket: &bucket,
Prefix: &obj,
})
cancel()
if err != nil {
return err
}
if !compareVersions(versions, resp.Versions) {
return fmt.Errorf("expected the object vresions to be %v, instead got %v", versions, resp.Versions)
}
if len(resp.DeleteMarkers) != 0 {
return fmt.Errorf("expected empty delete markers list, instead got %v", resp.DeleteMarkers)
}
//
lockedVersions := make([]objToDelete, 0, len(versions))
for _, v := range versions {
lockedVersions = append(lockedVersions, objToDelete{
key: obj,
versionId: getString(v.VersionId),
isCompliance: false,
})
}
return cleanupLockedObjects(s3client, bucket, lockedVersions)
})
}, withLock())
}
func Versioning_WORM_trailing_slash_counterpart(s *S3Conf) error {
testName := "Versioning_WORM_trailing_slash_counterpart"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
rDate := time.Now().Add(time.Hour).UTC().Truncate(time.Second)
lockedObjs := []objToDelete{}
for _, keys := range [][2]string{{"my-dir/", "my-dir"}, {"my-obj", "my-obj/"}} {
obj, other := keys[0], keys[1]
res, err := putObjectWithData(objDataLen(obj, 10), &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
ChecksumAlgorithm: types.ChecksumAlgorithmCrc32,
ObjectLockLegalHoldStatus: types.ObjectLockLegalHoldStatusOn,
ObjectLockMode: types.ObjectLockModeGovernance,
ObjectLockRetainUntilDate: &rDate,
}, s3client)
if err != nil {
return err
}
versionId := getString(res.res.VersionId)
lockedObjs = append(lockedObjs, objToDelete{
key: obj,
versionId: versionId,
removeOnlyLeglHold: true,
})
// the version belongs to the object, not to the other key
err = checkObjectLockErr(s3client, bucket, other, versionId, s3err.GetAPIError(s3err.ErrNoSuchVersion))
if err != nil {
return fmt.Errorf("%v: %w", other, err)
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: &bucket,
Key: &other,
VersionId: &versionId,
})
cancel()
if err != nil {
return fmt.Errorf("%v: %w", other, err)
}
err = checkObjectLock(s3client, bucket, obj, versionId, rDate)
if err != nil {
return fmt.Errorf("%v: %w", obj, err)
}
}
return cleanupLockedObjects(s3client, bucket, lockedObjs)
}, withLock())
}
func Versioning_WORM_null_version_locked_with_legal_hold(s *S3Conf) error {
testName := "Versioning_WORM_null_version_locked_with_legal_hold"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
objs := []string{"my-obj", "my-dir/"}
// the objects are put before versioning is enabled: their
// current versions are the null versions
_, err := putObjects(s3client, objs, bucket)
if err != nil {
return err
}
err = putBucketVersioningStatus(s3client, bucket, types.BucketVersioningStatusEnabled)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.PutObjectLockConfiguration(ctx, &s3.PutObjectLockConfigurationInput{
Bucket: &bucket,
ObjectLockConfiguration: &types.ObjectLockConfiguration{
ObjectLockEnabled: types.ObjectLockEnabledEnabled,
},
})
cancel()
if err != nil {
return err
}
lockedObjs := []objToDelete{}
err = forEachKey(objs, func(obj string) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.PutObjectLegalHold(ctx, &s3.PutObjectLegalHoldInput{
Bucket: &bucket,
Key: &obj,
VersionId: getPtr(nullVersionId),
LegalHold: &types.ObjectLockLegalHold{
Status: types.ObjectLockLegalHoldStatusOn,
},
})
cancel()
if err != nil {
return err
}
lockedObjs = append(lockedObjs, objToDelete{
key: obj,
versionId: nullVersionId,
removeOnlyLeglHold: true,
})
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.GetObjectLegalHold(ctx, &s3.GetObjectLegalHoldInput{
Bucket: &bucket,
Key: &obj,
VersionId: getPtr(nullVersionId),
})
cancel()
if err != nil {
return err
}
if res.LegalHold == nil || res.LegalHold.Status != types.ObjectLockLegalHoldStatusOn {
return fmt.Errorf("expected the legal hold status to be %q, instead got %v",
types.ObjectLockLegalHoldStatusOn, res.LegalHold)
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: &bucket,
Key: &obj,
VersionId: getPtr(nullVersionId),
})
cancel()
return checkApiErr(err, s3err.GetAPIError(s3err.ErrObjectLocked))
})
if err != nil {
return err
}
return cleanupLockedObjects(s3client, bucket, lockedObjs)
})
}
func Versioning_WORM_PutObject_new_version_lock_settings(s *S3Conf) error {
testName := "Versioning_WORM_PutObject_new_version_lock_settings"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
obj := "my-obj"
rDate := time.Now().Add(time.Hour * 48)
first, err := putObjectWithData(10, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
ObjectLockLegalHoldStatus: types.ObjectLockLegalHoldStatusOn,
ObjectLockMode: types.ObjectLockModeGovernance,
ObjectLockRetainUntilDate: &rDate,
}, s3client, withPutObjectChecksumAlgo(types.ChecksumAlgorithmCrc32))
if err != nil {
return err
}
second, err := putObjectWithData(20, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
}, s3client)
if err != nil {
return err
}
// the new version isn't locked by the first version lock settings
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
cur, err := s3client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: &bucket,
Key: &obj,
})
cancel()
if err != nil {
return err
}
if getString(cur.VersionId) != getString(second.res.VersionId) {
return fmt.Errorf("expected the current versionId to be %v, instead got %v",
getString(second.res.VersionId), getString(cur.VersionId))
}
if cur.ObjectLockLegalHoldStatus != "" {
return fmt.Errorf("expected empty object legal hold status, instead got %q",
cur.ObjectLockLegalHoldStatus)
}
if cur.ObjectLockMode != "" {
return fmt.Errorf("expected empty object lock mode, instead got %q", cur.ObjectLockMode)
}
if cur.ObjectLockRetainUntilDate != nil {
return fmt.Errorf("expected nil object lock retain until date, instead got %v",
*cur.ObjectLockRetainUntilDate)
}
return cleanupLockedObjects(s3client, bucket, []objToDelete{
{
key: obj,
versionId: getString(first.res.VersionId),
removeLegalHold: true,
},
})
}, withLock())
}
func Versioning_WORM_DeleteObject_promoted_version_lock_settings(s *S3Conf) error {
testName := "Versioning_WORM_DeleteObject_promoted_version_lock_settings"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
obj := "my-obj"
rDate := time.Now().Add(time.Hour * 48)
first, err := putObjectWithData(10, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
}, s3client)
if err != nil {
return err
}
second, err := putObjectWithData(20, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
ObjectLockLegalHoldStatus: types.ObjectLockLegalHoldStatusOn,
ObjectLockMode: types.ObjectLockModeGovernance,
ObjectLockRetainUntilDate: &rDate,
}, s3client, withPutObjectChecksumAlgo(types.ChecksumAlgorithmCrc32))
if err != nil {
return err
}
// release the latest version and delete it, which makes the first
// version current again
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.PutObjectLegalHold(ctx, &s3.PutObjectLegalHoldInput{
Bucket: &bucket,
Key: &obj,
VersionId: second.res.VersionId,
LegalHold: &types.ObjectLockLegalHold{
Status: types.ObjectLockLegalHoldStatusOff,
},
})
cancel()
if err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: &bucket,
Key: &obj,
VersionId: second.res.VersionId,
BypassGovernanceRetention: getBoolPtr(true),
})
cancel()
if err != nil {
return err
}
// the first version holds none of the deleted version lock settings
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
cur, err := s3client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: &bucket,
Key: &obj,
})
cancel()
if err != nil {
return err
}
if getString(cur.VersionId) != getString(first.res.VersionId) {
return fmt.Errorf("expected the current versionId to be %v, instead got %v",
getString(first.res.VersionId), getString(cur.VersionId))
}
if cur.ObjectLockLegalHoldStatus != "" {
return fmt.Errorf("expected empty object legal hold status, instead got %q",
cur.ObjectLockLegalHoldStatus)
}
if cur.ObjectLockMode != "" {
return fmt.Errorf("expected empty object lock mode, instead got %q", cur.ObjectLockMode)
}
if cur.ObjectLockRetainUntilDate != nil {
return fmt.Errorf("expected nil object lock retain until date, instead got %v",
*cur.ObjectLockRetainUntilDate)
}
return nil
}, withLock())
}
func Versioning_AccessControl_GetObjectVersion(s *S3Conf) error {
testName := "Versioning_AccessControl_GetObjectVersion"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
testuser := getUser("user")
err := createUsers(s, []user{testuser})
if err != nil {
return err
}
userClient := s.getUserClient(testuser)
return forEachKey([]string{"my-obj", "my-dir/"}, func(obj string) error {
objData, err := putObjectWithData(objDataLen(obj, 10), &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
}, s3client)
if err != nil {
return err
}
doc := genPolicyDoc("Allow", fmt.Sprintf(`"%s"`, testuser.access), `"s3:GetObject"`, fmt.Sprintf(`"arn:aws:s3:::%s/*"`, bucket))
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{
Bucket: &bucket,
Policy: &doc,
})
cancel()
if err != nil {
return err
}
// querying with versionId should return access denied
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = userClient.GetObject(ctx, &s3.GetObjectInput{
Bucket: &bucket,
Key: &obj,
VersionId: objData.res.VersionId,
})
defer cancel()
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrAccessDenied)); err != nil {
return err
}
// grant the user s3:GetObjectVersion
doc = genPolicyDoc("Allow", fmt.Sprintf(`"%s"`, testuser.access), `"s3:GetObjectVersion"`, fmt.Sprintf(`"arn:aws:s3:::%s/*"`, bucket))
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{
Bucket: &bucket,
Policy: &doc,
})
cancel()
if err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = userClient.GetObject(ctx, &s3.GetObjectInput{
Bucket: &bucket,
Key: &obj,
VersionId: objData.res.VersionId,
})
defer cancel()
if err != nil {
return err
}
return nil
})
}, withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_AccessControl_HeadObjectVersion(s *S3Conf) error {
testName := "Versioning_AccessControl_HeadObjectVersion"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
testuser := getUser("user")
err := createUsers(s, []user{testuser})
if err != nil {
return err
}
userClient := s.getUserClient(testuser)
return forEachKey([]string{"my-obj", "my-dir/"}, func(obj string) error {
objData, err := putObjectWithData(objDataLen(obj, 10), &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
}, s3client)
if err != nil {
return err
}
doc := genPolicyDoc("Allow", fmt.Sprintf(`"%s"`, testuser.access), `"s3:GetObject"`, fmt.Sprintf(`"arn:aws:s3:::%s/*"`, bucket))
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{
Bucket: &bucket,
Policy: &doc,
})
cancel()
if err != nil {
return err
}
// querying with versionId should return access denied
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = userClient.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: &bucket,
Key: &obj,
VersionId: objData.res.VersionId,
})
cancel()
if err := checkSdkApiErr(err, http.StatusText(http.StatusForbidden)); err != nil {
return err
}
// grant the user s3:GetObjectVersion
doc = genPolicyDoc("Allow", fmt.Sprintf(`"%s"`, testuser.access), `"s3:GetObjectVersion"`, fmt.Sprintf(`"arn:aws:s3:::%s/*"`, bucket))
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{
Bucket: &bucket,
Policy: &doc,
})
cancel()
if err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = userClient.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: &bucket,
Key: &obj,
VersionId: objData.res.VersionId,
})
cancel()
if err != nil {
return err
}
return nil
})
}, withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_AccessControl_object_tagging_policy(s *S3Conf) error {
testName := "Versioning_AccessControl_PutObjectTagging_policy"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
testuser := getUser("user")
err := createUsers(s, []user{testuser})
if err != nil {
return err
}
userClient := s.getUserClient(testuser)
return forEachKey([]string{"my-object", "my-dir/"}, func(object string) error {
res, err := putObjectWithData(objDataLen(object, 10), &s3.PutObjectInput{
Bucket: &bucket,
Key: &object,
}, s3client)
if err != nil {
return err
}
putGetDeleteObjectTagging := func(versionId *string, denyAccess bool) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := userClient.PutObjectTagging(ctx, &s3.PutObjectTaggingInput{
Bucket: &bucket,
Key: &object,
VersionId: versionId,
Tagging: &types.Tagging{
TagSet: []types.Tag{
{Key: getPtr("key"), Value: getPtr("value")},
},
},
})
cancel()
if denyAccess {
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrAccessDenied)); err != nil {
return err
}
} else {
if err != nil {
return err
}
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = userClient.GetObjectTagging(ctx, &s3.GetObjectTaggingInput{
Bucket: &bucket,
Key: &object,
VersionId: versionId,
})
cancel()
if denyAccess {
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrAccessDenied)); err != nil {
return err
}
} else {
if err != nil {
return err
}
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = userClient.DeleteObjectTagging(ctx, &s3.DeleteObjectTaggingInput{
Bucket: &bucket,
Key: &object,
VersionId: versionId,
})
cancel()
if denyAccess {
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrAccessDenied)); err != nil {
return err
}
} else {
if err != nil {
return err
}
}
return nil
}
policy := genPolicyDoc("Allow", fmt.Sprintf(`"%s"`, testuser.access), `["s3:PutObjectVersionTagging", "s3:GetObjectVersionTagging", "s3:DeleteObjectVersionTagging"]`, fmt.Sprintf(`"arn:aws:s3:::%s/*"`, bucket))
err = putBucketPolicy(s3client, bucket, policy)
if err != nil {
return err
}
// deny without versionId
err = putGetDeleteObjectTagging(nil, true)
if err != nil {
return err
}
// allow with versionId
err = putGetDeleteObjectTagging(res.res.VersionId, false)
if err != nil {
return err
}
policy = genPolicyDoc("Allow", fmt.Sprintf(`"%s"`, testuser.access), `["s3:PutObjectTagging", "s3:GetObjectTagging", "s3:DeleteObjectTagging"]`, fmt.Sprintf(`"arn:aws:s3:::%s/*"`, bucket))
err = putBucketPolicy(s3client, bucket, policy)
if err != nil {
return err
}
// allow without versionId
err = putGetDeleteObjectTagging(nil, false)
if err != nil {
return err
}
// deny with versionId
err = putGetDeleteObjectTagging(res.res.VersionId, true)
if err != nil {
return err
}
return nil
})
}, withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_AccessControl_DeleteObject_policy(s *S3Conf) error {
testName := "Versioning_AccessControl_DeleteObject_policy"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
testuser := getUser("user")
err := createUsers(s, []user{testuser})
if err != nil {
return err
}
userClient := s.getUserClient(testuser)
return forEachKey([]string{"my-object", "my-dir/"}, func(obj string) error {
delObject := func(versionId *string, denyAccess bool) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := userClient.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: &bucket,
Key: &obj,
VersionId: versionId,
})
cancel()
if denyAccess {
return checkApiErr(err, s3err.GetAPIError(s3err.ErrAccessDenied))
}
return err
}
res, err := putObjectWithData(objDataLen(obj, 10), &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
}, s3client)
if err != nil {
return err
}
policy := genPolicyDoc("Allow", fmt.Sprintf(`"%s"`, testuser.access), `"s3:DeleteObject"`, fmt.Sprintf(`"arn:aws:s3:::%s/*"`, bucket))
err = putBucketPolicy(s3client, bucket, policy)
if err != nil {
return err
}
// deny with versionId
err = delObject(res.res.VersionId, true)
if err != nil {
return err
}
// allow without versionId
err = delObject(nil, false)
if err != nil {
return err
}
policy = genPolicyDoc("Allow", fmt.Sprintf(`"%s"`, testuser.access), `"s3:DeleteObjectVersion"`, fmt.Sprintf(`"arn:aws:s3:::%s/*"`, bucket))
err = putBucketPolicy(s3client, bucket, policy)
if err != nil {
return err
}
// recreate the object
res, err = putObjectWithData(objDataLen(obj, 10), &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
}, s3client)
if err != nil {
return err
}
// deny without versionId
err = delObject(nil, true)
if err != nil {
return err
}
// allow with versionId
err = delObject(res.res.VersionId, false)
if err != nil {
return err
}
return nil
})
}, withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_AccessControl_GetObjectAttributes_policy(s *S3Conf) error {
testName := "Versioning_AccessControl_GetObjectAttributes_policy"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
testuser := getUser("user")
err := createUsers(s, []user{testuser})
if err != nil {
return err
}
userClient := s.getUserClient(testuser)
return forEachKey([]string{"my-object", "my-dir/"}, func(obj string) error {
res, err := putObjectWithData(objDataLen(obj, 10), &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
}, s3client)
if err != nil {
return err
}
getObjectAttr := func(versionId *string, denyAccess bool) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := userClient.GetObjectAttributes(ctx, &s3.GetObjectAttributesInput{
Bucket: &bucket,
Key: &obj,
VersionId: versionId,
ObjectAttributes: types.ObjectAttributesChecksum.Values(),
})
cancel()
if denyAccess {
return checkApiErr(err, s3err.GetAPIError(s3err.ErrAccessDenied))
}
return nil
}
policy := genPolicyDoc("Allow", fmt.Sprintf(`"%s"`, testuser.access), `"s3:GetObjectAttributes"`, fmt.Sprintf(`"arn:aws:s3:::%s/*"`, bucket))
err = putBucketPolicy(s3client, bucket, policy)
if err != nil {
return err
}
// deny with versionId
err = getObjectAttr(res.res.VersionId, true)
if err != nil {
return err
}
// allow without versionId
err = getObjectAttr(nil, false)
if err != nil {
return err
}
policy = genPolicyDoc("Allow", fmt.Sprintf(`"%s"`, testuser.access), `"s3:GetObjectVersionAttributes"`, fmt.Sprintf(`"arn:aws:s3:::%s/*"`, bucket))
err = putBucketPolicy(s3client, bucket, policy)
if err != nil {
return err
}
// deny without versionId
err = getObjectAttr(nil, true)
if err != nil {
return err
}
// allow with versionId
err = getObjectAttr(res.res.VersionId, false)
if err != nil {
return err
}
return nil
})
}, withVersioning(types.BucketVersioningStatusEnabled))
}
func VersioningDisabled_GetBucketVersioning_not_configured(s *S3Conf) error {
testName := "VersioningDisabled_GetBucketVersioning_not_configured"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.GetBucketVersioning(ctx, &s3.GetBucketVersioningInput{
Bucket: &bucket,
})
cancel()
if err != nil {
return err
}
if res.Status != "" {
return fmt.Errorf("expected empty versioning status when versioning is not configured, instead got %v",
res.Status)
}
return nil
})
}
func VersioningDisabled_GetBucketVersioning_no_such_bucket(s *S3Conf) error {
testName := "VersioningDisabled_GetBucketVersioning_no_such_bucket"
return actionHandlerNoSetup(s, testName, func(s3client *s3.Client, bucket string) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.GetBucketVersioning(ctx, &s3.GetBucketVersioningInput{
Bucket: &bucket,
})
cancel()
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrNoSuchBucket)); err != nil {
return err
}
return nil
})
}
func VersioningDisabled_PutBucketVersioning_not_configured(s *S3Conf) error {
testName := "VersioningDisabled_PutBucketVersioning_not_configured"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
err := putBucketVersioningStatus(s3client, bucket, types.BucketVersioningStatusEnabled)
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrVersioningNotConfigured)); err != nil {
return err
}
return nil
})
}
func Versioning_concurrent_upload_object(s *S3Conf) error {
testName := "Versioninig_concurrent_upload_object"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
obj := "my-obj"
versionCount := 5
// Channel to collect errors
errCh := make(chan error, versionCount)
uploadVersion := func(wg *sync.WaitGroup) {
defer wg.Done()
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.PutObject(ctx, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
})
cancel()
if err != nil {
// Send error to the channel
errCh <- err
return
}
fmt.Printf("uploaded object successfully: versionId: %v\n", *res.VersionId)
}
wg := &sync.WaitGroup{}
wg.Add(versionCount)
for range versionCount {
go uploadVersion(wg)
}
wg.Wait()
close(errCh)
// Check if there were any errors
for err := range errCh {
if err != nil {
fmt.Printf("error uploading an object: %v\n", err.Error())
return err
}
}
// List object versions after all uploads
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.ListObjectVersions(ctx, &s3.ListObjectVersionsInput{
Bucket: &bucket,
})
cancel()
if err != nil {
return err
}
if len(res.Versions) != versionCount {
return fmt.Errorf("expected %v object versions, instead got %v",
versionCount, len(res.Versions))
}
return nil
}, withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_GetObjectTagging_invalid_versionId(s *S3Conf) error {
testName := "Versioning_GetObjectTagging_invalid_versionId"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
obj := "my-object"
_, err := putObjectWithData(4, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
}, s3client)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.GetObjectTagging(ctx, &s3.GetObjectTaggingInput{
Bucket: &bucket,
Key: &obj,
VersionId: getPtr("invalid_versionId"),
})
cancel()
return checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgVersionId, "invalid_versionId"))
})
}
func Versioning_PutObjectTagging_non_existing_object_version(s *S3Conf) error {
testName := "Versioning_PutObjectTagging_non_existing_object_version"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
return forEachKey([]string{"my-object", "my-dir/"}, func(obj string) error {
_, err := putObjectWithData(objDataLen(obj, 4), &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
}, s3client)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.PutObjectTagging(ctx, &s3.PutObjectTaggingInput{
Bucket: &bucket,
Key: &obj,
Tagging: &types.Tagging{
TagSet: []types.Tag{{Key: getPtr("key"), Value: getPtr("value")}},
},
VersionId: getPtr("01K97XE6PJQ1A4X5TJFDHK4EMC"),
})
cancel()
return checkApiErr(err, s3err.GetAPIError(s3err.ErrNoSuchVersion))
})
}, withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_PutGetDeleteObjectTagging_delete_marker(s *S3Conf) error {
testName := "Versioning_PutGetDeleteObjectTagging_delete_marker"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
return forEachKey([]string{"my-object", "my-dir/"}, func(obj string) error {
_, err := putObjectWithData(objDataLen(obj, 10), &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
}, s3client)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: &bucket,
Key: &obj,
})
cancel()
if err != nil {
return err
}
// PutObjectTagging
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.PutObjectTagging(ctx, &s3.PutObjectTaggingInput{
Bucket: &bucket,
Key: &obj,
VersionId: out.VersionId,
Tagging: &types.Tagging{
TagSet: []types.Tag{{Key: getPtr("key"), Value: getPtr("value")}},
},
})
cancel()
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrMethodNotAllowed)); err != nil {
return err
}
// GetObjectTagging
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.GetObjectTagging(ctx, &s3.GetObjectTaggingInput{
Bucket: &bucket,
Key: &obj,
VersionId: out.VersionId,
})
cancel()
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrMethodNotAllowed)); err != nil {
return err
}
// DeleteObjectTagging
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.DeleteObjectTagging(ctx, &s3.DeleteObjectTaggingInput{
Bucket: &bucket,
Key: &obj,
VersionId: out.VersionId,
})
cancel()
return checkApiErr(err, s3err.GetAPIError(s3err.ErrMethodNotAllowed))
})
}, withLock())
}
func Versioning_PutObjectTagging_invalid_versionId(s *S3Conf) error {
testName := "Versioning_PutObjectTagging_invalid_versionId"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
obj := "my-object"
_, err := putObjectWithData(4, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
}, s3client)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.PutObjectTagging(ctx, &s3.PutObjectTaggingInput{
Bucket: &bucket,
Key: &obj,
Tagging: &types.Tagging{
TagSet: []types.Tag{{Key: getPtr("key"), Value: getPtr("value")}},
},
VersionId: getPtr("invalid_versionId"),
})
cancel()
return checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgVersionId, "invalid_versionId"))
}, withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_GetObjectTagging_non_existing_object_version(s *S3Conf) error {
testName := "Versioning_GetObjectTagging_non_existing_object_version"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
return forEachKey([]string{"my-object", "my-dir/"}, func(obj string) error {
_, err := putObjectWithData(objDataLen(obj, 4), &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
}, s3client)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.GetObjectTagging(ctx, &s3.GetObjectTaggingInput{
Bucket: &bucket,
Key: &obj,
VersionId: getPtr("01K97XE6PJQ1A4X5TJFDHK4EMC"),
})
cancel()
return checkApiErr(err, s3err.GetAPIError(s3err.ErrNoSuchVersion))
})
}, withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_DeleteObjectTagging_invalid_versionId(s *S3Conf) error {
testName := "Versioning_DeleteObjectTagging_invalid_versionId"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
obj := "my-object"
_, err := putObjectWithData(4, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
}, s3client)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.DeleteObjectTagging(ctx, &s3.DeleteObjectTaggingInput{
Bucket: &bucket,
Key: &obj,
VersionId: getPtr("invalid_versionId"),
})
cancel()
return checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgVersionId, "invalid_versionId"))
})
}
func Versioning_DeleteObjectTagging_non_existing_object_version(s *S3Conf) error {
testName := "Versioning_DeleteObjectTagging_non_existing_object_version"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
return forEachKey([]string{"my-object", "my-dir/"}, func(obj string) error {
_, err := putObjectWithData(objDataLen(obj, 4), &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
}, s3client)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.DeleteObjectTagging(ctx, &s3.DeleteObjectTaggingInput{
Bucket: &bucket,
Key: &obj,
VersionId: getPtr("01K97XE6PJQ1A4X5TJFDHK4EMC"),
})
cancel()
return checkApiErr(err, s3err.GetAPIError(s3err.ErrNoSuchVersion))
})
}, withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_PutGetDeleteObjectTagging_success(s *S3Conf) error {
testName := "Versioning_PutGetDeleteObjectTagging_success"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
return forEachKey([]string{"my-object", "my-dir/"}, func(obj string) error {
versions, err := createObjVersions(s3client, bucket, obj, 5)
if err != nil {
return err
}
versionId := versions[2].VersionId
tagging := types.Tagging{
TagSet: []types.Tag{
{Key: getPtr("key"), Value: getPtr("value")},
},
}
compareVersionId := func(expected, input *string) error {
if getString(expected) != getString(input) {
return fmt.Errorf("expected the response versionId to be %s, instead got %s", getString(expected), getString(input))
}
return nil
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.PutObjectTagging(ctx, &s3.PutObjectTaggingInput{
Bucket: &bucket,
Key: &obj,
Tagging: &tagging,
VersionId: versionId,
})
cancel()
if err != nil {
return err
}
if err := compareVersionId(versionId, res.VersionId); err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.GetObjectTagging(ctx, &s3.GetObjectTaggingInput{
Bucket: &bucket,
Key: &obj,
VersionId: versionId,
})
cancel()
if err != nil {
return err
}
if !areTagsSame(tagging.TagSet, out.TagSet) {
return fmt.Errorf("expected the object version tags to be %v, instead got %v", tagging.TagSet, out.TagSet)
}
if err := compareVersionId(versionId, out.VersionId); err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
resp, err := s3client.DeleteObjectTagging(ctx, &s3.DeleteObjectTaggingInput{
Bucket: &bucket,
Key: &obj,
VersionId: versionId,
})
cancel()
if err != nil {
return err
}
if err := compareVersionId(versionId, resp.VersionId); err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
r, err := s3client.GetObjectTagging(ctx, &s3.GetObjectTaggingInput{
Bucket: &bucket,
Key: &obj,
VersionId: versionId,
})
cancel()
if err != nil {
return err
}
if len(r.TagSet) != 0 {
return fmt.Errorf("expected empty tag set, instead got %v", r.TagSet)
}
return nil
})
}, withVersioning(types.BucketVersioningStatusEnabled))
}
func Versioning_ObjectTagging_trailing_slash_counterpart(s *S3Conf) error {
testName := "Versioning_ObjectTagging_trailing_slash_counterpart"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
tagSet := []types.Tag{{Key: getPtr("key"), Value: getPtr("value")}}
for _, keys := range [][2]string{{"my-dir/", "my-dir"}, {"my-obj", "my-obj/"}} {
obj, other := keys[0], keys[1]
res, err := putObjectWithData(objDataLen(obj, 10), &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
Tagging: getPtr("key=value"),
}, s3client)
if err != nil {
return err
}
versionId := getString(res.res.VersionId)
// the version belongs to the object, not to the other key
err = checkObjectTaggingErr(s3client, bucket, other, versionId, s3err.GetAPIError(s3err.ErrNoSuchVersion))
if err != nil {
return fmt.Errorf("%v: %w", other, err)
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.GetObjectTagging(ctx, &s3.GetObjectTaggingInput{
Bucket: &bucket,
Key: &obj,
VersionId: &versionId,
})
cancel()
if err != nil {
return fmt.Errorf("%v: %w", obj, err)
}
if !areTagsSame(out.TagSet, tagSet) {
return fmt.Errorf("%v: expected the tag set to be %v, instead got %v",
obj, tagSet, out.TagSet)
}
}
return nil
}, withVersioning(types.BucketVersioningStatusEnabled))
}