1066 lines
34 KiB
Go
1066 lines
34 KiB
Go
/*
|
|
*
|
|
* Mint, (C) 2017 Minio, Inc.
|
|
*
|
|
* 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 main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"encoding/xml"
|
|
"errors"
|
|
"fmt"
|
|
"math/rand"
|
|
"net/http"
|
|
"os"
|
|
"reflect"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/aws/aws-sdk-go/aws"
|
|
"github.com/aws/aws-sdk-go/aws/awserr"
|
|
"github.com/aws/aws-sdk-go/aws/credentials"
|
|
"github.com/aws/aws-sdk-go/aws/session"
|
|
"github.com/aws/aws-sdk-go/service/s3"
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
const letterBytes = "abcdefghijklmnopqrstuvwxyz01234569"
|
|
const (
|
|
letterIdxBits = 6 // 6 bits to represent a letter index
|
|
letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
|
|
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
|
|
)
|
|
|
|
// different kinds of test failures
|
|
const (
|
|
PASS = "PASS" // Indicate that a test passed
|
|
FAIL = "FAIL" // Indicate that a test failed
|
|
)
|
|
|
|
type errorResponse struct {
|
|
XMLName xml.Name `xml:"Error" json:"-"`
|
|
Code string
|
|
Message string
|
|
BucketName string
|
|
Key string
|
|
RequestID string `xml:"RequestId"`
|
|
HostID string `xml:"HostId"`
|
|
|
|
// Region where the bucket is located. This header is returned
|
|
// only in HEAD bucket and ListObjects response.
|
|
Region string
|
|
|
|
// Headers of the returned S3 XML error
|
|
Headers http.Header `xml:"-" json:"-"`
|
|
}
|
|
|
|
type mintJSONFormatter struct {
|
|
}
|
|
|
|
func (f *mintJSONFormatter) Format(entry *log.Entry) ([]byte, error) {
|
|
data := make(log.Fields, len(entry.Data))
|
|
for k, v := range entry.Data {
|
|
switch v := v.(type) {
|
|
case error:
|
|
// Otherwise errors are ignored by `encoding/json`
|
|
// https://github.com/sirupsen/logrus/issues/137
|
|
data[k] = v.Error()
|
|
default:
|
|
data[k] = v
|
|
}
|
|
}
|
|
|
|
serialized, err := json.Marshal(data)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Failed to marshal fields to JSON, %w", err)
|
|
}
|
|
return append(serialized, '\n'), nil
|
|
}
|
|
|
|
// log successful test runs
|
|
func successLogger(function string, args map[string]interface{}, startTime time.Time) *log.Entry {
|
|
// calculate the test case duration
|
|
duration := time.Since(startTime)
|
|
// log with the fields as per mint
|
|
fields := log.Fields{"name": "aws-sdk-go", "function": function, "args": args, "duration": duration.Nanoseconds() / 1000000, "status": PASS}
|
|
return log.WithFields(fields)
|
|
}
|
|
|
|
// log failed test runs
|
|
func failureLog(function string, args map[string]interface{}, startTime time.Time, alert string, message string, err error) *log.Entry {
|
|
// calculate the test case duration
|
|
duration := time.Since(startTime)
|
|
var fields log.Fields
|
|
// log with the fields as per mint
|
|
if err != nil {
|
|
fields = log.Fields{"name": "aws-sdk-go", "function": function, "args": args,
|
|
"duration": duration.Nanoseconds() / 1000000, "status": FAIL, "alert": alert, "message": message, "error": err}
|
|
} else {
|
|
fields = log.Fields{"name": "aws-sdk-go", "function": function, "args": args,
|
|
"duration": duration.Nanoseconds() / 1000000, "status": FAIL, "alert": alert, "message": message}
|
|
}
|
|
return log.WithFields(fields)
|
|
}
|
|
|
|
func randString(n int, src rand.Source, prefix string) string {
|
|
b := make([]byte, n)
|
|
// A rand.Int63() generates 63 random bits, enough for letterIdxMax letters!
|
|
for i, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 0; {
|
|
if remain == 0 {
|
|
cache, remain = src.Int63(), letterIdxMax
|
|
}
|
|
if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
|
|
b[i] = letterBytes[idx]
|
|
i--
|
|
}
|
|
cache >>= letterIdxBits
|
|
remain--
|
|
}
|
|
return prefix + string(b[0:30-len(prefix)])
|
|
}
|
|
|
|
func isObjectTaggingImplemented(s3Client *s3.S3) bool {
|
|
bucket := randString(60, rand.NewSource(time.Now().UnixNano()), "aws-sdk-go-test-")
|
|
object := randString(60, rand.NewSource(time.Now().UnixNano()), "")
|
|
startTime := time.Now()
|
|
function := "isObjectTaggingImplemented"
|
|
args := map[string]interface{}{
|
|
"bucketName": bucket,
|
|
"objectName": object,
|
|
}
|
|
defer cleanup(s3Client, bucket, object, function, args, startTime, true)
|
|
|
|
_, err := s3Client.CreateBucket(&s3.CreateBucketInput{
|
|
Bucket: aws.String(bucket),
|
|
})
|
|
if err != nil {
|
|
failureLog(function, args, startTime, "", "AWS SDK Go CreateBucket Failed", err).Fatal()
|
|
return false
|
|
}
|
|
|
|
_, err = s3Client.PutObject(&s3.PutObjectInput{
|
|
Body: aws.ReadSeekCloser(strings.NewReader("testfile")),
|
|
Bucket: aws.String(bucket),
|
|
Key: aws.String(object),
|
|
})
|
|
|
|
if err != nil {
|
|
failureLog(function, args, startTime, "", fmt.Sprintf("AWS SDK Go PUT expected to success but got %v", err), err).Fatal()
|
|
return false
|
|
}
|
|
|
|
_, err = s3Client.GetObjectTagging(&s3.GetObjectTaggingInput{
|
|
Bucket: aws.String(bucket),
|
|
Key: aws.String(object),
|
|
})
|
|
if err != nil {
|
|
if awsErr, ok := err.(awserr.Error); ok {
|
|
if awsErr.Code() == "NotImplemented" {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func cleanup(s3Client *s3.S3, bucket string, object string, function string,
|
|
args map[string]interface{}, startTime time.Time, deleteBucket bool) {
|
|
|
|
// Deleting the object, just in case it was created. Will not check for errors.
|
|
s3Client.DeleteObject(&s3.DeleteObjectInput{
|
|
Bucket: aws.String(bucket),
|
|
Key: aws.String(object),
|
|
})
|
|
|
|
if deleteBucket {
|
|
_, err := s3Client.DeleteBucket(&s3.DeleteBucketInput{
|
|
Bucket: aws.String(bucket),
|
|
})
|
|
if err != nil {
|
|
failureLog(function, args, startTime, "", "AWS SDK Go DeleteBucket Failed", err).Fatal()
|
|
return
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
func testPresignedPutInvalidHash(s3Client *s3.S3) {
|
|
startTime := time.Now()
|
|
function := "PresignedPut"
|
|
bucket := randString(60, rand.NewSource(time.Now().UnixNano()), "aws-sdk-go-test-")
|
|
object := "presignedTest"
|
|
expiry := 1 * time.Minute
|
|
args := map[string]interface{}{
|
|
"bucketName": bucket,
|
|
"objectName": object,
|
|
"expiry": expiry,
|
|
}
|
|
|
|
_, err := s3Client.CreateBucket(&s3.CreateBucketInput{
|
|
Bucket: aws.String(bucket),
|
|
})
|
|
if err != nil {
|
|
failureLog(function, args, startTime, "", "AWS SDK Go CreateBucket Failed", err).Fatal()
|
|
return
|
|
}
|
|
defer cleanup(s3Client, bucket, object, function, args, startTime, true)
|
|
|
|
req, _ := s3Client.PutObjectRequest(&s3.PutObjectInput{
|
|
Bucket: aws.String(bucket),
|
|
Key: aws.String(object),
|
|
ContentType: aws.String("application/octet-stream"),
|
|
})
|
|
|
|
req.HTTPRequest.Header.Set("X-Amz-Content-Sha256", "invalid-sha256")
|
|
url, err := req.Presign(expiry)
|
|
if err != nil {
|
|
failureLog(function, args, startTime, "", "AWS SDK Go presigned Put request creation failed", err).Fatal()
|
|
return
|
|
}
|
|
|
|
rreq, err := http.NewRequest("PUT", url, bytes.NewReader([]byte("")))
|
|
if err != nil {
|
|
failureLog(function, args, startTime, "", "AWS SDK Go presigned PUT request failed", err).Fatal()
|
|
return
|
|
}
|
|
|
|
rreq.Header.Add("X-Amz-Content-Sha256", "invalid-sha256")
|
|
rreq.Header.Add("Content-Type", "application/octet-stream")
|
|
|
|
resp, err := http.DefaultClient.Do(rreq)
|
|
if err != nil {
|
|
failureLog(function, args, startTime, "", "AWS SDK Go presigned put request failed", err).Fatal()
|
|
return
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
dec := xml.NewDecoder(resp.Body)
|
|
errResp := errorResponse{}
|
|
err = dec.Decode(&errResp)
|
|
if err != nil {
|
|
failureLog(function, args, startTime, "", "AWS SDK Go unmarshalling xml failed", err).Fatal()
|
|
return
|
|
}
|
|
|
|
if errResp.Code != "XAmzContentSHA256Mismatch" {
|
|
failureLog(function, args, startTime, "", fmt.Sprintf("AWS SDK Go presigned PUT expected to fail with XAmzContentSHA256Mismatch but got %v", errResp.Code), errors.New("AWS S3 error code mismatch")).Fatal()
|
|
return
|
|
}
|
|
|
|
successLogger(function, args, startTime).Info()
|
|
}
|
|
|
|
func testListObjects(s3Client *s3.S3) {
|
|
startTime := time.Now()
|
|
function := "testListObjects"
|
|
bucket := randString(60, rand.NewSource(time.Now().UnixNano()), "aws-sdk-go-test-")
|
|
object1 := "testObject1"
|
|
object2 := "testObject2"
|
|
expiry := 1 * time.Minute
|
|
args := map[string]interface{}{
|
|
"bucketName": bucket,
|
|
"objectName1": object1,
|
|
"objectName2": object2,
|
|
"expiry": expiry,
|
|
}
|
|
|
|
getKeys := func(objects []*s3.Object) []string {
|
|
var rv []string
|
|
for _, obj := range objects {
|
|
rv = append(rv, *obj.Key)
|
|
}
|
|
return rv
|
|
}
|
|
_, err := s3Client.CreateBucket(&s3.CreateBucketInput{
|
|
Bucket: aws.String(bucket),
|
|
})
|
|
if err != nil {
|
|
failureLog(function, args, startTime, "", "AWS SDK Go CreateBucket Failed", err).Fatal()
|
|
return
|
|
}
|
|
defer cleanup(s3Client, bucket, object1, function, args, startTime, true)
|
|
defer cleanup(s3Client, bucket, object2, function, args, startTime, false)
|
|
|
|
listInput := &s3.ListObjectsV2Input{
|
|
Bucket: aws.String(bucket),
|
|
MaxKeys: aws.Int64(1000),
|
|
Prefix: aws.String(""),
|
|
}
|
|
result, err := s3Client.ListObjectsV2(listInput)
|
|
if err != nil {
|
|
failureLog(function, args, startTime, "", fmt.Sprintf("AWS SDK Go listobjects expected to success but got %v", err), err).Fatal()
|
|
return
|
|
}
|
|
if *result.KeyCount != 0 {
|
|
failureLog(function, args, startTime, "", fmt.Sprintf("AWS SDK Go listobjects with prefix '' expected 0 key but got %v, %v", result.KeyCount, getKeys(result.Contents)), errors.New("AWS S3 key count mismatch")).Fatal()
|
|
return
|
|
}
|
|
putInput1 := &s3.PutObjectInput{
|
|
Body: aws.ReadSeekCloser(strings.NewReader("filetoupload")),
|
|
Bucket: aws.String(bucket),
|
|
Key: aws.String(object1),
|
|
}
|
|
_, err = s3Client.PutObject(putInput1)
|
|
if err != nil {
|
|
failureLog(function, args, startTime, "", fmt.Sprintf("AWS SDK Go PUT expected to success but got %v", err), err).Fatal()
|
|
return
|
|
}
|
|
putInput2 := &s3.PutObjectInput{
|
|
Body: aws.ReadSeekCloser(strings.NewReader("filetoupload")),
|
|
Bucket: aws.String(bucket),
|
|
Key: aws.String(object2),
|
|
}
|
|
_, err = s3Client.PutObject(putInput2)
|
|
if err != nil {
|
|
failureLog(function, args, startTime, "", fmt.Sprintf("AWS SDK Go PUT expected to success but got %v", err), err).Fatal()
|
|
return
|
|
}
|
|
result, err = s3Client.ListObjectsV2(listInput)
|
|
if err != nil {
|
|
failureLog(function, args, startTime, "", fmt.Sprintf("AWS SDK Go listobjects expected to success but got %v", err), err).Fatal()
|
|
return
|
|
}
|
|
if *result.KeyCount != 2 {
|
|
failureLog(function, args, startTime, "", fmt.Sprintf("AWS SDK Go listobjects with prefix '' expected 2 key but got %v, %v", *result.KeyCount, getKeys(result.Contents)), errors.New("AWS S3 key count mismatch")).Fatal()
|
|
return
|
|
}
|
|
|
|
successLogger(function, args, startTime).Info()
|
|
}
|
|
|
|
func testSelectObject(s3Client *s3.S3) {
|
|
startTime := time.Now()
|
|
function := "testSelectObject"
|
|
bucket := randString(60, rand.NewSource(time.Now().UnixNano()), "aws-sdk-go-test-")
|
|
object1 := "object1.csv"
|
|
object2 := "object2.csv"
|
|
args := map[string]interface{}{
|
|
"bucketName": bucket,
|
|
"objectName1": object1,
|
|
"objectName2": object2,
|
|
}
|
|
|
|
_, err := s3Client.CreateBucket(&s3.CreateBucketInput{
|
|
Bucket: aws.String(bucket),
|
|
})
|
|
if err != nil {
|
|
failureLog(function, args, startTime, "", "AWS SDK Go CreateBucket Failed", err).Fatal()
|
|
return
|
|
}
|
|
|
|
// Test comma field separator
|
|
inputCsv1 := `year,gender,ethnicity,firstname,count,rank
|
|
2011,FEMALE,ASIAN AND PACIFIC ISLANDER,SOPHIA,119,1
|
|
2011,FEMALE,ASIAN AND PACIFIC ISLANDER,CHLOE,106,2
|
|
2011,FEMALE,ASIAN AND PACIFIC ISLANDER,EMILY,93,3
|
|
2011,FEMALE,ASIAN AND PACIFIC ISLANDER,OLIVIA,89,4
|
|
2011,FEMALE,ASIAN AND PACIFIC ISLANDER,EMMA,75,5
|
|
2011,FEMALE,ASIAN AND PACIFIC ISLANDER,ISABELLA,67,6
|
|
2011,FEMALE,ASIAN AND PACIFIC ISLANDER,TIFFANY,54,7
|
|
2011,FEMALE,ASIAN AND PACIFIC ISLANDER,ASHLEY,52,8
|
|
2011,FEMALE,ASIAN AND PACIFIC ISLANDER,FIONA,48,9
|
|
2011,FEMALE,ASIAN AND PACIFIC ISLANDER,ANGELA,47,10
|
|
`
|
|
|
|
outputCSV1 := `2011
|
|
2011
|
|
2011
|
|
2011
|
|
2011
|
|
2011
|
|
2011
|
|
2011
|
|
2011
|
|
2011
|
|
`
|
|
|
|
putInput1 := &s3.PutObjectInput{
|
|
Body: aws.ReadSeekCloser(strings.NewReader(inputCsv1)),
|
|
Bucket: aws.String(bucket),
|
|
Key: aws.String(object1),
|
|
}
|
|
_, err = s3Client.PutObject(putInput1)
|
|
if err != nil {
|
|
failureLog(function, args, startTime, "", fmt.Sprintf("AWS SDK Go Select object failed %v", err), err).Fatal()
|
|
return
|
|
}
|
|
|
|
defer cleanup(s3Client, bucket, object1, function, args, startTime, true)
|
|
|
|
params := &s3.SelectObjectContentInput{
|
|
Bucket: &bucket,
|
|
Key: &object1,
|
|
ExpressionType: aws.String(s3.ExpressionTypeSql),
|
|
Expression: aws.String("SELECT s._1 FROM S3Object s"),
|
|
RequestProgress: &s3.RequestProgress{},
|
|
InputSerialization: &s3.InputSerialization{
|
|
CompressionType: aws.String("NONE"),
|
|
CSV: &s3.CSVInput{
|
|
FileHeaderInfo: aws.String(s3.FileHeaderInfoIgnore),
|
|
FieldDelimiter: aws.String(","),
|
|
RecordDelimiter: aws.String("\n"),
|
|
},
|
|
},
|
|
OutputSerialization: &s3.OutputSerialization{
|
|
CSV: &s3.CSVOutput{},
|
|
},
|
|
}
|
|
|
|
resp, err := s3Client.SelectObjectContent(params)
|
|
if err != nil {
|
|
failureLog(function, args, startTime, "", fmt.Sprintf("AWS SDK Go Select object failed %v", err), err).Fatal()
|
|
return
|
|
}
|
|
defer resp.EventStream.Close()
|
|
|
|
payload := ""
|
|
for event := range resp.EventStream.Events() {
|
|
switch v := event.(type) {
|
|
case *s3.RecordsEvent:
|
|
// s3.RecordsEvent.Records is a byte slice of select records
|
|
payload = string(v.Payload)
|
|
}
|
|
}
|
|
|
|
if err := resp.EventStream.Err(); err != nil {
|
|
failureLog(function, args, startTime, "", fmt.Sprintf("AWS SDK Go Select object failed %v", err), err).Fatal()
|
|
return
|
|
}
|
|
|
|
if payload != outputCSV1 {
|
|
failureLog(function, args, startTime, "", fmt.Sprintf("AWS SDK Go Select object output mismatch %v", payload), errors.New("AWS S3 select object mismatch")).Fatal()
|
|
return
|
|
}
|
|
|
|
// Test unicode field separator
|
|
inputCsv2 := `"year"╦"gender"╦"ethnicity"╦"firstname"╦"count"╦"rank"
|
|
"2011"╦"FEMALE"╦"ASIAN AND PACIFIC ISLANDER"╦"SOPHIA"╦"119"╦"1"
|
|
"2011"╦"FEMALE"╦"ASIAN AND PACIFIC ISLANDER"╦"CHLOE"╦"106"╦"2"
|
|
"2011"╦"FEMALE"╦"ASIAN AND PACIFIC ISLANDER"╦"EMILY"╦"93"╦"3"
|
|
"2011"╦"FEMALE"╦"ASIAN AND PACIFIC ISLANDER"╦"OLIVIA"╦"89"╦"4"
|
|
"2011"╦"FEMALE"╦"ASIAN AND PACIFIC ISLANDER"╦"EMMA"╦"75"╦"5"
|
|
"2011"╦"FEMALE"╦"ASIAN AND PACIFIC ISLANDER"╦"ISABELLA"╦"67"╦"6"
|
|
"2011"╦"FEMALE"╦"ASIAN AND PACIFIC ISLANDER"╦"TIFFANY"╦"54"╦"7"
|
|
"2011"╦"FEMALE"╦"ASIAN AND PACIFIC ISLANDER"╦"ASHLEY"╦"52"╦"8"
|
|
"2011"╦"FEMALE"╦"ASIAN AND PACIFIC ISLANDER"╦"FIONA"╦"48"╦"9"
|
|
"2011"╦"FEMALE"╦"ASIAN AND PACIFIC ISLANDER"╦"ANGELA"╦"47"╦"10"
|
|
`
|
|
|
|
outputCSV2 := `2011
|
|
2011
|
|
2011
|
|
2011
|
|
2011
|
|
2011
|
|
2011
|
|
2011
|
|
2011
|
|
2011
|
|
`
|
|
|
|
putInput2 := &s3.PutObjectInput{
|
|
Body: aws.ReadSeekCloser(strings.NewReader(inputCsv2)),
|
|
Bucket: aws.String(bucket),
|
|
Key: aws.String(object2),
|
|
}
|
|
_, err = s3Client.PutObject(putInput2)
|
|
if err != nil {
|
|
failureLog(function, args, startTime, "", fmt.Sprintf("AWS SDK Go Select object upload failed: %v", err), err).Fatal()
|
|
return
|
|
}
|
|
|
|
defer cleanup(s3Client, bucket, object2, function, args, startTime, false)
|
|
|
|
params2 := &s3.SelectObjectContentInput{
|
|
Bucket: &bucket,
|
|
Key: &object2,
|
|
ExpressionType: aws.String(s3.ExpressionTypeSql),
|
|
Expression: aws.String("SELECT s._1 FROM S3Object s"),
|
|
RequestProgress: &s3.RequestProgress{},
|
|
InputSerialization: &s3.InputSerialization{
|
|
CompressionType: aws.String("NONE"),
|
|
CSV: &s3.CSVInput{
|
|
FileHeaderInfo: aws.String(s3.FileHeaderInfoIgnore),
|
|
FieldDelimiter: aws.String("╦"),
|
|
RecordDelimiter: aws.String("\n"),
|
|
},
|
|
},
|
|
OutputSerialization: &s3.OutputSerialization{
|
|
CSV: &s3.CSVOutput{},
|
|
},
|
|
}
|
|
|
|
resp, err = s3Client.SelectObjectContent(params2)
|
|
if err != nil {
|
|
failureLog(function, args, startTime, "", fmt.Sprintf("AWS SDK Go Select object failed for unicode separator %v", err), err).Fatal()
|
|
return
|
|
}
|
|
defer resp.EventStream.Close()
|
|
|
|
for event := range resp.EventStream.Events() {
|
|
switch v := event.(type) {
|
|
case *s3.RecordsEvent:
|
|
// s3.RecordsEvent.Records is a byte slice of select records
|
|
payload = string(v.Payload)
|
|
}
|
|
}
|
|
|
|
if err := resp.EventStream.Err(); err != nil {
|
|
failureLog(function, args, startTime, "", fmt.Sprintf("AWS SDK Go Select object failed for unicode separator %v", err), err).Fatal()
|
|
return
|
|
}
|
|
|
|
if payload != outputCSV2 {
|
|
failureLog(function, args, startTime, "", fmt.Sprintf("AWS SDK Go Select object output mismatch %v", payload), errors.New("AWS S3 select object mismatch")).Fatal()
|
|
return
|
|
}
|
|
|
|
successLogger(function, args, startTime).Info()
|
|
}
|
|
|
|
func testObjectTagging(s3Client *s3.S3) {
|
|
startTime := time.Now()
|
|
function := "testObjectTagging"
|
|
bucket := randString(60, rand.NewSource(time.Now().UnixNano()), "aws-sdk-go-test-")
|
|
object := randString(60, rand.NewSource(time.Now().UnixNano()), "")
|
|
args := map[string]interface{}{
|
|
"bucketName": bucket,
|
|
"objectName": object,
|
|
}
|
|
|
|
_, err := s3Client.CreateBucket(&s3.CreateBucketInput{
|
|
Bucket: aws.String(bucket),
|
|
})
|
|
if err != nil {
|
|
failureLog(function, args, startTime, "", "AWS SDK Go CreateBucket Failed", err).Fatal()
|
|
return
|
|
}
|
|
defer cleanup(s3Client, bucket, object, function, args, startTime, true)
|
|
|
|
taginput := "Tag1=Value1"
|
|
tagInputSet := []*s3.Tag{
|
|
{
|
|
Key: aws.String("Tag1"),
|
|
Value: aws.String("Value1"),
|
|
},
|
|
}
|
|
_, err = s3Client.PutObject(&s3.PutObjectInput{
|
|
Body: aws.ReadSeekCloser(strings.NewReader("testfile")),
|
|
Bucket: aws.String(bucket),
|
|
Key: aws.String(object),
|
|
Tagging: &taginput,
|
|
})
|
|
|
|
if err != nil {
|
|
failureLog(function, args, startTime, "", fmt.Sprintf("AWS SDK Go PUT expected to success but got %v", err), err).Fatal()
|
|
return
|
|
}
|
|
|
|
tagop, err := s3Client.GetObjectTagging(&s3.GetObjectTaggingInput{
|
|
Bucket: aws.String(bucket),
|
|
Key: aws.String(object),
|
|
})
|
|
if err != nil {
|
|
if awsErr, ok := err.(awserr.Error); ok {
|
|
failureLog(function, args, startTime, "", fmt.Sprintf("AWS SDK Go PUTObjectTagging expected to success but got %v", awsErr.Code()), err).Fatal()
|
|
return
|
|
}
|
|
}
|
|
if !reflect.DeepEqual(tagop.TagSet, tagInputSet) {
|
|
failureLog(function, args, startTime, "", fmt.Sprintf("AWS SDK Go PUTObject Tag input did not match with GetObjectTagging output %v", nil), nil).Fatal()
|
|
return
|
|
}
|
|
|
|
taginputSet1 := []*s3.Tag{
|
|
{
|
|
Key: aws.String("Key4"),
|
|
Value: aws.String("Value4"),
|
|
},
|
|
}
|
|
_, err = s3Client.PutObjectTagging(&s3.PutObjectTaggingInput{
|
|
Bucket: aws.String(bucket),
|
|
Key: aws.String(object),
|
|
Tagging: &s3.Tagging{
|
|
TagSet: taginputSet1,
|
|
},
|
|
})
|
|
if err != nil {
|
|
if awsErr, ok := err.(awserr.Error); ok {
|
|
failureLog(function, args, startTime, "", fmt.Sprintf("AWS SDK Go PUTObjectTagging expected to success but got %v", awsErr.Code()), err).Fatal()
|
|
return
|
|
}
|
|
}
|
|
|
|
tagop, err = s3Client.GetObjectTagging(&s3.GetObjectTaggingInput{
|
|
Bucket: aws.String(bucket),
|
|
Key: aws.String(object),
|
|
})
|
|
if err != nil {
|
|
if awsErr, ok := err.(awserr.Error); ok {
|
|
failureLog(function, args, startTime, "", fmt.Sprintf("AWS SDK Go PUTObjectTagging expected to success but got %v", awsErr.Code()), err).Fatal()
|
|
return
|
|
}
|
|
}
|
|
if !reflect.DeepEqual(tagop.TagSet, taginputSet1) {
|
|
failureLog(function, args, startTime, "", fmt.Sprintf("AWS SDK Go PUTObjectTagging input did not match with GetObjectTagging output %v", nil), nil).Fatal()
|
|
return
|
|
}
|
|
successLogger(function, args, startTime).Info()
|
|
}
|
|
|
|
func testObjectTaggingErrors(s3Client *s3.S3) {
|
|
startTime := time.Now()
|
|
function := "testObjectTaggingErrors"
|
|
bucket := randString(60, rand.NewSource(time.Now().UnixNano()), "aws-sdk-go-test-")
|
|
object := randString(60, rand.NewSource(time.Now().UnixNano()), "")
|
|
args := map[string]interface{}{
|
|
"bucketName": bucket,
|
|
"objectName": object,
|
|
}
|
|
|
|
_, err := s3Client.CreateBucket(&s3.CreateBucketInput{
|
|
Bucket: aws.String(bucket),
|
|
})
|
|
if err != nil {
|
|
failureLog(function, args, startTime, "", "AWS SDK Go CreateBucket Failed", err).Fatal()
|
|
return
|
|
}
|
|
defer cleanup(s3Client, bucket, object, function, args, startTime, true)
|
|
|
|
_, err = s3Client.PutObject(&s3.PutObjectInput{
|
|
Body: aws.ReadSeekCloser(strings.NewReader("testfile")),
|
|
Bucket: aws.String(bucket),
|
|
Key: aws.String(object),
|
|
})
|
|
|
|
if err != nil {
|
|
failureLog(function, args, startTime, "", fmt.Sprintf("AWS SDK Go PUT expected to success but got %v", err), err).Fatal()
|
|
return
|
|
}
|
|
|
|
// case 1 : Too many tags > 10
|
|
input := &s3.PutObjectTaggingInput{
|
|
Bucket: aws.String(bucket),
|
|
Key: aws.String(object),
|
|
Tagging: &s3.Tagging{
|
|
TagSet: []*s3.Tag{
|
|
{
|
|
Key: aws.String("Key1"),
|
|
Value: aws.String("Value3"),
|
|
},
|
|
{
|
|
Key: aws.String("Key2"),
|
|
Value: aws.String("Value4"),
|
|
},
|
|
{
|
|
Key: aws.String("Key3"),
|
|
Value: aws.String("Value3"),
|
|
},
|
|
{
|
|
Key: aws.String("Key4"),
|
|
Value: aws.String("Value3"),
|
|
},
|
|
{
|
|
Key: aws.String("Key5"),
|
|
Value: aws.String("Value3"),
|
|
},
|
|
{
|
|
Key: aws.String("Key6"),
|
|
Value: aws.String("Value3"),
|
|
},
|
|
{
|
|
Key: aws.String("Key7"),
|
|
Value: aws.String("Value3"),
|
|
},
|
|
{
|
|
Key: aws.String("Key8"),
|
|
Value: aws.String("Value3"),
|
|
},
|
|
{
|
|
Key: aws.String("Key9"),
|
|
Value: aws.String("Value3"),
|
|
},
|
|
{
|
|
Key: aws.String("Key10"),
|
|
Value: aws.String("Value3"),
|
|
},
|
|
{
|
|
Key: aws.String("Key11"),
|
|
Value: aws.String("Value3"),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
_, err = s3Client.PutObjectTagging(input)
|
|
if err == nil {
|
|
failureLog(function, args, startTime, "", "AWS SDK Go PUT expected to fail but succeeded", err).Fatal()
|
|
return
|
|
}
|
|
|
|
if aerr, ok := err.(awserr.Error); ok {
|
|
if aerr.Code() != "BadRequest" && aerr.Message() != "BadRequest: Object tags cannot be greater than 10" {
|
|
failureLog(function, args, startTime, "", fmt.Sprintf("AWS SDK Go PUT expected to fail but got %v", err), err).Fatal()
|
|
return
|
|
}
|
|
}
|
|
|
|
// case 2 : Duplicate Tag Keys
|
|
input = &s3.PutObjectTaggingInput{
|
|
Bucket: aws.String(bucket),
|
|
Key: aws.String(object),
|
|
Tagging: &s3.Tagging{
|
|
TagSet: []*s3.Tag{
|
|
{
|
|
Key: aws.String("Key1"),
|
|
Value: aws.String("Value3"),
|
|
},
|
|
{
|
|
Key: aws.String("Key1"),
|
|
Value: aws.String("Value4"),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
_, err = s3Client.PutObjectTagging(input)
|
|
if err == nil {
|
|
failureLog(function, args, startTime, "", "AWS SDK Go PUT expected to fail but succeeded", err).Fatal()
|
|
return
|
|
}
|
|
|
|
if aerr, ok := err.(awserr.Error); ok {
|
|
if aerr.Code() != "InvalidTag" && aerr.Message() != "InvalidTag: Cannot provide multiple Tags with the same key" {
|
|
failureLog(function, args, startTime, "", fmt.Sprintf("AWS SDK Go PUT expected to fail but got %v", err), err).Fatal()
|
|
return
|
|
}
|
|
}
|
|
|
|
// case 3 : Too long Tag Key
|
|
input = &s3.PutObjectTaggingInput{
|
|
Bucket: aws.String(bucket),
|
|
Key: aws.String(object),
|
|
Tagging: &s3.Tagging{
|
|
TagSet: []*s3.Tag{
|
|
{
|
|
Key: aws.String("Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1"),
|
|
Value: aws.String("Value3"),
|
|
},
|
|
{
|
|
Key: aws.String("Key1"),
|
|
Value: aws.String("Value4"),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
_, err = s3Client.PutObjectTagging(input)
|
|
if err == nil {
|
|
failureLog(function, args, startTime, "", "AWS SDK Go PUT expected to fail but succeeded", err).Fatal()
|
|
return
|
|
}
|
|
|
|
if aerr, ok := err.(awserr.Error); ok {
|
|
if aerr.Code() != "InvalidTag" && aerr.Message() != "InvalidTag: The TagKey you have provided is invalid" {
|
|
failureLog(function, args, startTime, "", fmt.Sprintf("AWS SDK Go PUT expected to fail but got %v", err), err).Fatal()
|
|
return
|
|
}
|
|
}
|
|
|
|
// case 4 : Too long Tag value
|
|
input = &s3.PutObjectTaggingInput{
|
|
Bucket: aws.String(bucket),
|
|
Key: aws.String(object),
|
|
Tagging: &s3.Tagging{
|
|
TagSet: []*s3.Tag{
|
|
{
|
|
Key: aws.String("Key1"),
|
|
Value: aws.String("Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1Key1"),
|
|
},
|
|
{
|
|
Key: aws.String("Key1"),
|
|
Value: aws.String("Value4"),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
_, err = s3Client.PutObjectTagging(input)
|
|
if err == nil {
|
|
failureLog(function, args, startTime, "", "AWS SDK Go PUT expected to fail but succeeded", err).Fatal()
|
|
return
|
|
}
|
|
|
|
if aerr, ok := err.(awserr.Error); ok {
|
|
if aerr.Code() != "InvalidTag" && aerr.Message() != "InvalidTag: The TagValue you have provided is invalid" {
|
|
failureLog(function, args, startTime, "", fmt.Sprintf("AWS SDK Go PUT expected to fail but got %v", err), err).Fatal()
|
|
return
|
|
}
|
|
}
|
|
|
|
successLogger(function, args, startTime).Info()
|
|
}
|
|
|
|
// Tests bucket re-create errors.
|
|
func testCreateBucketError(s3Client *s3.S3) {
|
|
region := s3Client.Config.Region
|
|
// Amazon S3 returns error in all AWS Regions except in the North Virginia Region.
|
|
// More details in https://docs.aws.amazon.com/sdk-for-go/api/service/s3/#S3.CreateBucket
|
|
s3Client.Config.Region = aws.String("us-west-1")
|
|
|
|
// initialize logging params
|
|
startTime := time.Now()
|
|
function := "testMakeBucketError"
|
|
bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "aws-sdk-go-test-")
|
|
args := map[string]interface{}{
|
|
"bucketName": bucketName,
|
|
}
|
|
_, err := s3Client.CreateBucket(&s3.CreateBucketInput{
|
|
Bucket: aws.String(bucketName),
|
|
})
|
|
if err != nil {
|
|
failureLog(function, args, startTime, "", "AWS SDK Go CreateBucket Failed", err).Fatal()
|
|
return
|
|
}
|
|
defer cleanup(s3Client, bucketName, "", function, args, startTime, true)
|
|
|
|
_, errCreating := s3Client.CreateBucket(&s3.CreateBucketInput{
|
|
Bucket: aws.String(bucketName),
|
|
})
|
|
if errCreating == nil {
|
|
failureLog(function, args, startTime, "", "AWS SDK Go CreateBucket Should Return Error for Existing bucket", err).Fatal()
|
|
return
|
|
}
|
|
// Verify valid error response from server.
|
|
if errCreating.(s3.RequestFailure).Code() != "BucketAlreadyExists" &&
|
|
errCreating.(s3.RequestFailure).Code() != "BucketAlreadyOwnedByYou" {
|
|
failureLog(function, args, startTime, "", "Invalid error returned by server", err).Fatal()
|
|
return
|
|
}
|
|
|
|
// Restore region in s3Client
|
|
s3Client.Config.Region = region
|
|
successLogger(function, args, startTime).Info()
|
|
}
|
|
|
|
func testListMultipartUploads(s3Client *s3.S3) {
|
|
startTime := time.Now()
|
|
function := "testListMultipartUploads"
|
|
bucket := randString(60, rand.NewSource(time.Now().UnixNano()), "aws-sdk-go-test-")
|
|
object := randString(60, rand.NewSource(time.Now().UnixNano()), "")
|
|
args := map[string]interface{}{
|
|
"bucketName": bucket,
|
|
"objectName": object,
|
|
}
|
|
_, errCreating := s3Client.CreateBucket(&s3.CreateBucketInput{
|
|
Bucket: aws.String(bucket),
|
|
})
|
|
if errCreating != nil {
|
|
failureLog(function, args, startTime, "", "AWS SDK Go CreateBucket Failed", errCreating).Fatal()
|
|
return
|
|
}
|
|
defer cleanup(s3Client, bucket, object, function, args, startTime, true)
|
|
|
|
multipartUpload, err := s3Client.CreateMultipartUpload(&s3.CreateMultipartUploadInput{
|
|
Bucket: aws.String(bucket),
|
|
Key: aws.String(object),
|
|
})
|
|
if err != nil {
|
|
failureLog(function, args, startTime, "", "AWS SDK Go createMultipartupload API failed", err).Fatal()
|
|
return
|
|
}
|
|
parts := make(map[*int64]*string)
|
|
for i := 0; i < 5; i++ {
|
|
result, errUpload := s3Client.UploadPart(&s3.UploadPartInput{
|
|
Bucket: aws.String(bucket),
|
|
Key: aws.String(object),
|
|
UploadId: multipartUpload.UploadId,
|
|
PartNumber: aws.Int64(int64(i + 1)),
|
|
Body: aws.ReadSeekCloser(strings.NewReader("fileToUpload")),
|
|
})
|
|
if errUpload != nil {
|
|
_, _ = s3Client.AbortMultipartUpload(&s3.AbortMultipartUploadInput{
|
|
Bucket: aws.String(bucket),
|
|
Key: aws.String(object),
|
|
UploadId: multipartUpload.UploadId,
|
|
})
|
|
failureLog(function, args, startTime, "", "AWS SDK Go uploadPart API failed for", errUpload).Fatal()
|
|
return
|
|
}
|
|
parts[aws.Int64(int64(i+1))] = result.ETag
|
|
}
|
|
|
|
listParts, errParts := s3Client.ListParts(&s3.ListPartsInput{
|
|
Bucket: aws.String(bucket),
|
|
Key: aws.String(object),
|
|
UploadId: multipartUpload.UploadId,
|
|
})
|
|
if errParts != nil {
|
|
failureLog(function, args, startTime, "", "AWS SDK Go ListPartsInput API failed for", err).Fatal()
|
|
return
|
|
}
|
|
|
|
if len(parts) != len(listParts.Parts) {
|
|
failureLog(function, args, startTime, "", fmt.Sprintf("AWS SDK Go ListParts.Parts len mismatch want: %v got: %v", len(parts), len(listParts.Parts)), err).Fatal()
|
|
return
|
|
}
|
|
|
|
for _, part := range listParts.Parts {
|
|
if tag, ok := parts[part.PartNumber]; ok {
|
|
if tag != part.ETag {
|
|
failureLog(function, args, startTime, "", fmt.Sprintf("AWS SDK Go ListParts.Parts output mismatch want: %v got: %v", tag, part.ETag), err).Fatal()
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
successLogger(function, args, startTime).Info()
|
|
}
|
|
|
|
func testSSECopyObject(s3Client *s3.S3) {
|
|
// initialize logging params
|
|
startTime := time.Now()
|
|
function := "testSSECopyObjectSourceEncrypted"
|
|
bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "aws-sdk-go-test-")
|
|
object := randString(60, rand.NewSource(time.Now().UnixNano()), "")
|
|
args := map[string]interface{}{
|
|
"bucketName": bucketName,
|
|
"objectName": object,
|
|
}
|
|
_, err := s3Client.CreateBucket(&s3.CreateBucketInput{
|
|
Bucket: aws.String(bucketName),
|
|
})
|
|
if err != nil {
|
|
failureLog(function, args, startTime, "", "AWS SDK Go CreateBucket Failed", err).Fatal()
|
|
return
|
|
}
|
|
defer cleanup(s3Client, bucketName, object+"-enc", function, args, startTime, true)
|
|
defer cleanup(s3Client, bucketName, object+"-noenc", function, args, startTime, false)
|
|
var wrongSuccess = errors.New("Succeeded instead of failing. ")
|
|
|
|
// create encrypted object
|
|
sseCustomerKey := aws.String("32byteslongsecretkeymustbegiven2")
|
|
_, errPutEnc := s3Client.PutObject(&s3.PutObjectInput{
|
|
Body: aws.ReadSeekCloser(strings.NewReader("fileToUpload")),
|
|
Bucket: aws.String(bucketName),
|
|
Key: aws.String(object + "-enc"),
|
|
SSECustomerAlgorithm: aws.String("AES256"),
|
|
SSECustomerKey: sseCustomerKey,
|
|
})
|
|
if errPutEnc != nil {
|
|
failureLog(function, args, startTime, "", fmt.Sprintf("AWS SDK Go PUT expected to succeed but got %v", errPutEnc), errPutEnc).Fatal()
|
|
return
|
|
}
|
|
|
|
// copy the encrypted object
|
|
_, errCopyEnc := s3Client.CopyObject(&s3.CopyObjectInput{
|
|
SSECustomerAlgorithm: aws.String("AES256"),
|
|
SSECustomerKey: sseCustomerKey,
|
|
CopySource: aws.String(bucketName + "/" + object + "-enc"),
|
|
Bucket: aws.String(bucketName),
|
|
Key: aws.String(object + "-copy"),
|
|
})
|
|
if errCopyEnc == nil {
|
|
failureLog(function, args, startTime, "", "AWS SDK Go CopyObject expected to fail, but it succeeds ", wrongSuccess).Fatal()
|
|
return
|
|
}
|
|
var invalidSSECustomerAlgorithm = "Requests specifying Server Side Encryption with Customer provided keys must provide a valid encryption algorithm"
|
|
if !strings.Contains(errCopyEnc.Error(), invalidSSECustomerAlgorithm) {
|
|
failureLog(function, args, startTime, "", fmt.Sprintf("AWS SDK Go CopyObject expected error %v got %v", invalidSSECustomerAlgorithm, errCopyEnc), errCopyEnc).Fatal()
|
|
return
|
|
}
|
|
|
|
// create non-encrypted object
|
|
_, errPut := s3Client.PutObject(&s3.PutObjectInput{
|
|
Body: aws.ReadSeekCloser(strings.NewReader("fileToUpload")),
|
|
Bucket: aws.String(bucketName),
|
|
Key: aws.String(object + "-noenc"),
|
|
})
|
|
if errPut != nil {
|
|
failureLog(function, args, startTime, "", fmt.Sprintf("AWS SDK Go PUT expected to succeed but got %v", errPut), errPut).Fatal()
|
|
return
|
|
}
|
|
|
|
// copy the non-encrypted object
|
|
_, errCopy := s3Client.CopyObject(&s3.CopyObjectInput{
|
|
CopySourceSSECustomerAlgorithm: aws.String("AES256"),
|
|
CopySourceSSECustomerKey: sseCustomerKey,
|
|
SSECustomerAlgorithm: aws.String("AES256"),
|
|
SSECustomerKey: sseCustomerKey,
|
|
CopySource: aws.String(bucketName + "/" + object + "-noenc"),
|
|
Bucket: aws.String(bucketName),
|
|
Key: aws.String(object + "-copy"),
|
|
})
|
|
if errCopy == nil {
|
|
failureLog(function, args, startTime, "", "AWS SDK Go CopyObject expected to fail, but it succeeds ", wrongSuccess).Fatal()
|
|
return
|
|
}
|
|
var invalidEncryptionParameters = "The encryption parameters are not applicable to this object."
|
|
if !strings.Contains(errCopy.Error(), invalidEncryptionParameters) {
|
|
failureLog(function, args, startTime, "", fmt.Sprintf("AWS SDK Go CopyObject expected error %v got %v", invalidEncryptionParameters, errCopy), errCopy).Fatal()
|
|
return
|
|
}
|
|
|
|
successLogger(function, args, startTime).Info()
|
|
}
|
|
|
|
func main() {
|
|
endpoint := os.Getenv("SERVER_ENDPOINT")
|
|
accessKey := os.Getenv("ACCESS_KEY")
|
|
secretKey := os.Getenv("SECRET_KEY")
|
|
secure := os.Getenv("ENABLE_HTTPS")
|
|
sdkEndpoint := "http://" + endpoint
|
|
if secure == "1" {
|
|
sdkEndpoint = "https://" + endpoint
|
|
}
|
|
|
|
creds := credentials.NewStaticCredentials(accessKey, secretKey, "")
|
|
newSession := session.New()
|
|
s3Config := &aws.Config{
|
|
Credentials: creds,
|
|
Endpoint: aws.String(sdkEndpoint),
|
|
Region: aws.String("us-east-1"),
|
|
S3ForcePathStyle: aws.Bool(true),
|
|
}
|
|
|
|
// Create an S3 service object in the default region.
|
|
s3Client := s3.New(newSession, s3Config)
|
|
|
|
// Output to stdout instead of the default stderr
|
|
log.SetOutput(os.Stdout)
|
|
// create custom formatter
|
|
mintFormatter := mintJSONFormatter{}
|
|
// set custom formatter
|
|
log.SetFormatter(&mintFormatter)
|
|
// log Info or above -- success cases are Info level, failures are Fatal level
|
|
log.SetLevel(log.InfoLevel)
|
|
// execute tests
|
|
testPresignedPutInvalidHash(s3Client)
|
|
testListObjects(s3Client)
|
|
testSelectObject(s3Client)
|
|
testCreateBucketError(s3Client)
|
|
testListMultipartUploads(s3Client)
|
|
if secure == "1" {
|
|
testSSECopyObject(s3Client)
|
|
}
|
|
if isObjectTaggingImplemented(s3Client) {
|
|
testObjectTagging(s3Client)
|
|
testObjectTaggingErrors(s3Client)
|
|
}
|
|
}
|