Upstream versity/versitygw state of the touched files before eecdc6b996
This commit is contained in:
1 parent
e0fef79b7b
commit
6704314fea
5 files changed
+2892
-2
No files matched your search
+1560
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,381 @@
|
||||
// Copyright 2026 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 s3err
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type InvalidArgErrorCode int
|
||||
|
||||
const (
|
||||
InvalidArgMaxBuckets InvalidArgErrorCode = iota
|
||||
InvalidArgNegativeMaxKeys
|
||||
InvalidArgObjectAttributes
|
||||
InvalidArgPartNumber
|
||||
InvalidArgCompleteMpPartNumber
|
||||
InvalidArgCopySourceRange
|
||||
InvalidArgCopySourceBucket
|
||||
InvalidArgCopySourceObject
|
||||
InvalidArgCopySourceEncoding
|
||||
InvalidArgURLEncodedTagging
|
||||
InvalidArgAuthHeader
|
||||
InvalidArgAuthorizationType
|
||||
InvalidArgPOSTFileRequired
|
||||
InvalidArgSHA256Payload
|
||||
InvalidArgCopySource
|
||||
InvalidArgRetainUntilDate
|
||||
InvalidArgPastObjectLockRetainDate
|
||||
InvalidArgObjectLockRetentionDays
|
||||
InvalidArgObjectLockRetentionYears
|
||||
InvalidArgObjectLockRetentionDaysTooLarge
|
||||
InvalidArgObjectLockRetentionYearsTooLarge
|
||||
InvalidArgMissingObjectLockRetainDate
|
||||
InvalidArgMissingObjectLockMode
|
||||
InvalidArgLegalHoldStatus
|
||||
InvalidArgObjectLockMode
|
||||
InvalidArgAnonymousObjectLock
|
||||
InvalidArgMetadataDirective
|
||||
InvalidArgTaggingDirective
|
||||
InvalidArgVersionId
|
||||
InvalidArgEmptyVersionId
|
||||
InvalidArgChecksumPart
|
||||
InvalidArgMissingUploadId
|
||||
InvalidArgUploadIdMarker
|
||||
InvalidArgCannedAcl
|
||||
InvalidArgOnlyAws4HmacSha256
|
||||
InvalidArgDateHeader
|
||||
InvalidArgIndexDocumentSuffix
|
||||
InvalidArgMissingIndexDocumentSuffix
|
||||
InvalidArgErrorDocumentKey
|
||||
InvalidArgAwsChunkedUnsignedPayload
|
||||
InvalidArgSSECMissingAlgorithm
|
||||
InvalidArgSSECMissingKey
|
||||
InvalidArgSSECMissingKeyMD5
|
||||
InvalidArgSSECInvalidKey
|
||||
InvalidArgSSECKeyMD5Mismatch
|
||||
InvalidArgSSECInvalidAlgorithm
|
||||
InvalidArgSSECIncompatibleEncryption
|
||||
InvalidArgSSEInvalidEncryptionMethod
|
||||
InvalidArgSSECRequiresTLS
|
||||
)
|
||||
|
||||
var invalidArgErrResponses = map[InvalidArgErrorCode]InvalidArgumentError{
|
||||
InvalidArgMaxBuckets: {
|
||||
Description: "Argument max-buckets must be an integer between 1 and 10000.",
|
||||
ArgumentName: "max-buckets",
|
||||
},
|
||||
InvalidArgNegativeMaxKeys: {
|
||||
Description: "max-keys cannot be negative",
|
||||
ArgumentName: "maxKeys",
|
||||
},
|
||||
InvalidArgObjectAttributes: {
|
||||
Description: "Invalid attribute name specified.",
|
||||
ArgumentName: "x-amz-object-attributes",
|
||||
},
|
||||
InvalidArgPartNumber: {
|
||||
Description: "Part number must be an integer between 1 and 10000, inclusive.",
|
||||
ArgumentName: "partNumber",
|
||||
},
|
||||
InvalidArgCompleteMpPartNumber: {
|
||||
Description: "PartNumber must be >= 1",
|
||||
ArgumentName: "PartNumber",
|
||||
},
|
||||
InvalidArgCopySourceRange: {
|
||||
Description: "The x-amz-copy-source-range value must be of the form bytes=first-last where first and last are the zero-based offsets of the first and last bytes to copy",
|
||||
ArgumentName: "x-amz-copy-source-range",
|
||||
},
|
||||
InvalidArgCopySourceBucket: {
|
||||
Description: "Invalid copy source bucket name",
|
||||
ArgumentName: "x-amz-copy-source",
|
||||
},
|
||||
InvalidArgCopySourceObject: {
|
||||
Description: "Invalid copy source object key",
|
||||
ArgumentName: "x-amz-copy-source",
|
||||
},
|
||||
InvalidArgCopySourceEncoding: {
|
||||
Description: "Invalid copy source encoding",
|
||||
ArgumentName: "x-amz-copy-source",
|
||||
},
|
||||
InvalidArgURLEncodedTagging: {
|
||||
Description: "The header 'x-amz-tagging' shall be encoded as UTF-8 then URLEncoded URL query parameters without tag name duplicates.",
|
||||
ArgumentName: "x-amz-tagging",
|
||||
},
|
||||
InvalidArgAuthHeader: {
|
||||
Description: "Authorization header is invalid -- one and only one ' ' (space) required.",
|
||||
ArgumentName: "Authorization",
|
||||
},
|
||||
InvalidArgAuthorizationType: {
|
||||
Description: "Unsupported Authorization Type",
|
||||
ArgumentName: "Authorization",
|
||||
},
|
||||
InvalidArgPOSTFileRequired: {
|
||||
Description: "POST requires exactly one file upload per request.",
|
||||
ArgumentName: "file",
|
||||
},
|
||||
InvalidArgSHA256Payload: {
|
||||
Description: "x-amz-content-sha256 must be UNSIGNED-PAYLOAD, STREAMING-UNSIGNED-PAYLOAD-TRAILER, STREAMING-AWS4-HMAC-SHA256-PAYLOAD, STREAMING-AWS4-HMAC-SHA256-PAYLOAD-TRAILER, STREAMING-AWS4-ECDSA-P256-SHA256-PAYLOAD, STREAMING-AWS4-ECDSA-P256-SHA256-PAYLOAD-TRAILER or a valid sha256 value.",
|
||||
ArgumentName: "x-amz-content-sha256",
|
||||
},
|
||||
InvalidArgAwsChunkedUnsignedPayload: {
|
||||
Description: "aws-chunked encoding is not supported when x-amz-content-sha256 UNSIGNED-PAYLOAD is supplied.",
|
||||
ArgumentName: "Content-Encoding",
|
||||
},
|
||||
InvalidArgCopySource: {
|
||||
Description: "You can only specify a copy source header for copy requests.",
|
||||
ArgumentName: "x-amz-copy-source",
|
||||
},
|
||||
InvalidArgRetainUntilDate: {
|
||||
Description: "The retain until date must be provided in ISO 8601 format",
|
||||
ArgumentName: "x-amz-object-lock-retain-until-date",
|
||||
},
|
||||
InvalidArgPastObjectLockRetainDate: {
|
||||
Description: "The retain until date must be in the future!",
|
||||
ArgumentName: "x-amz-object-lock-retain-until-date",
|
||||
},
|
||||
InvalidArgMissingObjectLockRetainDate: {
|
||||
Description: "x-amz-object-lock-retain-until-date and x-amz-object-lock-mode must both be supplied",
|
||||
ArgumentName: "x-amz-object-lock-retain-until-date",
|
||||
},
|
||||
InvalidArgMissingObjectLockMode: {
|
||||
Description: "x-amz-object-lock-retain-until-date and x-amz-object-lock-mode must both be supplied",
|
||||
ArgumentName: "x-amz-object-lock-mode",
|
||||
},
|
||||
InvalidArgObjectLockRetentionDays: {
|
||||
Description: "Default retention period must be a positive integer value.",
|
||||
ArgumentName: "Days",
|
||||
},
|
||||
InvalidArgObjectLockRetentionYears: {
|
||||
Description: "Default retention period must be a positive integer value.",
|
||||
ArgumentName: "Years",
|
||||
},
|
||||
InvalidArgObjectLockRetentionDaysTooLarge: {
|
||||
Description: "Default retention period too large.",
|
||||
ArgumentName: "Days",
|
||||
},
|
||||
InvalidArgObjectLockRetentionYearsTooLarge: {
|
||||
Description: "Default retention period too large.",
|
||||
ArgumentName: "Years",
|
||||
},
|
||||
InvalidArgLegalHoldStatus: {
|
||||
Description: "Legal Hold must be either of 'ON' or 'OFF'",
|
||||
ArgumentName: "x-amz-object-lock-legal-hold",
|
||||
},
|
||||
InvalidArgObjectLockMode: {
|
||||
Description: "Unknown wormMode directive.",
|
||||
ArgumentName: "x-amz-object-lock-mode",
|
||||
},
|
||||
InvalidArgAnonymousObjectLock: {
|
||||
Description: "Put Object requests with Object Lock parameters require AWS Signature Version 4",
|
||||
ArgumentName: "Authorization",
|
||||
},
|
||||
InvalidArgMetadataDirective: {
|
||||
Description: "Unknown metadata directive.",
|
||||
ArgumentName: "x-amz-metadata-directive",
|
||||
},
|
||||
InvalidArgTaggingDirective: {
|
||||
Description: "Unknown tagging directive.",
|
||||
ArgumentName: "x-amz-tagging-directive",
|
||||
},
|
||||
InvalidArgVersionId: {
|
||||
Description: "Invalid version id specified",
|
||||
ArgumentName: "versionId",
|
||||
},
|
||||
InvalidArgEmptyVersionId: {
|
||||
Description: "Version id cannot be the empty string",
|
||||
ArgumentName: "versionId",
|
||||
},
|
||||
InvalidArgChecksumPart: {
|
||||
Description: "Invalid Base64 or multiple checksums present in request",
|
||||
ArgumentName: "Checksum",
|
||||
},
|
||||
InvalidArgMissingUploadId: {
|
||||
Description: "This operation does not accept partNumber without uploadId",
|
||||
ArgumentName: "partNumber",
|
||||
},
|
||||
InvalidArgUploadIdMarker: {
|
||||
Description: "Invalid uploadId marker",
|
||||
ArgumentName: "upload-id-marker",
|
||||
},
|
||||
InvalidArgCannedAcl: {
|
||||
Description: "",
|
||||
ArgumentName: "x-amz-acl",
|
||||
},
|
||||
InvalidArgOnlyAws4HmacSha256: {
|
||||
Description: "Only AWS4-HMAC-SHA256 is supported",
|
||||
ArgumentName: "X-Amz-Algorithm",
|
||||
},
|
||||
InvalidArgDateHeader: {
|
||||
Description: "X-Amz-Date must be formated via ISO8601 Long format",
|
||||
ArgumentName: "X-Amz-Date",
|
||||
},
|
||||
InvalidArgIndexDocumentSuffix: {
|
||||
Description: "The IndexDocument Suffix is not well formed",
|
||||
ArgumentName: "IndexDocument",
|
||||
},
|
||||
InvalidArgMissingIndexDocumentSuffix: {
|
||||
Description: "A value for IndexDocument Suffix must be provided if RedirectAllRequestsTo is empty",
|
||||
ArgumentName: "IndexDocument",
|
||||
},
|
||||
InvalidArgErrorDocumentKey: {
|
||||
Description: "The ErrorDocument Key is not well formed",
|
||||
ArgumentName: "ErrorDocument",
|
||||
},
|
||||
InvalidArgSSECMissingAlgorithm: {
|
||||
Description: "Requests specifying Server Side Encryption with Customer provided keys must provide a valid encryption algorithm.",
|
||||
ArgumentName: "x-amz-server-side-encryption",
|
||||
},
|
||||
InvalidArgSSECMissingKey: {
|
||||
Description: "Requests specifying Server Side Encryption with Customer provided keys must provide an appropriate secret key.",
|
||||
ArgumentName: "x-amz-server-side-encryption",
|
||||
},
|
||||
InvalidArgSSECMissingKeyMD5: {
|
||||
Description: "Requests specifying Server Side Encryption with Customer provided keys must provide the client calculated MD5 of the secret key.",
|
||||
ArgumentName: "x-amz-server-side-encryption",
|
||||
},
|
||||
InvalidArgSSECInvalidKey: {
|
||||
Description: "The secret key was invalid for the specified algorithm.",
|
||||
ArgumentName: "x-amz-server-side-encryption",
|
||||
},
|
||||
InvalidArgSSECKeyMD5Mismatch: {
|
||||
Description: "The calculated MD5 hash of the key did not match the hash that was provided.",
|
||||
ArgumentName: "x-amz-server-side-encryption",
|
||||
},
|
||||
InvalidArgSSECInvalidAlgorithm: {
|
||||
Description: "The Encryption request you specified is not valid. Supported value: AES256.",
|
||||
ArgumentName: "x-amz-server-side-encryption",
|
||||
},
|
||||
InvalidArgSSECIncompatibleEncryption: {
|
||||
Description: "Server Side Encryption with Customer provided key is incompatible with the encryption method specified",
|
||||
ArgumentName: "x-amz-server-side-encryption",
|
||||
},
|
||||
InvalidArgSSEInvalidEncryptionMethod: {
|
||||
Description: "The encryption method specified is not valid.",
|
||||
ArgumentName: "x-amz-server-side-encryption",
|
||||
},
|
||||
InvalidArgSSECRequiresTLS: {
|
||||
Description: "Requests specifying Server Side Encryption with Customer provided keys must be made over a secure connection.",
|
||||
ArgumentName: "x-amz-server-side-encryption",
|
||||
},
|
||||
}
|
||||
|
||||
// InvalidArgumentError is returned when a request argument is invalid.
|
||||
// Produces <ArgumentName> and <ArgumentValue> fields in the XML response, and
|
||||
// a <Region> field when the expected gateway region is known.
|
||||
type InvalidArgumentError struct {
|
||||
Description string
|
||||
ArgumentName string
|
||||
ArgumentValue string
|
||||
Region string `xml:",omitempty"`
|
||||
}
|
||||
|
||||
func (e InvalidArgumentError) BaseError() APIError {
|
||||
return APIError{
|
||||
Code: "InvalidArgument",
|
||||
Description: e.Description,
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
}
|
||||
}
|
||||
|
||||
// InvalidArgumentError http status code is always 400
|
||||
func (e InvalidArgumentError) StatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
func (e InvalidArgumentError) Error() string {
|
||||
var bytesBuffer bytes.Buffer
|
||||
bytesBuffer.WriteString(xml.Header)
|
||||
enc := xml.NewEncoder(&bytesBuffer)
|
||||
_ = enc.Encode(e)
|
||||
return bytesBuffer.String()
|
||||
}
|
||||
|
||||
func (e InvalidArgumentError) XMLBody(requestID, hostID string) []byte {
|
||||
return encodeResponse(struct {
|
||||
XMLName xml.Name `xml:"Error"`
|
||||
Code string
|
||||
Message string
|
||||
ArgumentName string `xml:"ArgumentName,omitempty"`
|
||||
ArgumentValue string `xml:"ArgumentValue,omitempty"`
|
||||
Region string `xml:"Region,omitempty"`
|
||||
RequestId string `xml:"RequestId,omitempty"`
|
||||
HostId string `xml:"HostId,omitempty"`
|
||||
}{
|
||||
Code: "InvalidArgument",
|
||||
Message: e.Description,
|
||||
ArgumentName: e.ArgumentName,
|
||||
ArgumentValue: e.ArgumentValue,
|
||||
Region: e.Region,
|
||||
RequestId: requestID,
|
||||
HostId: hostID,
|
||||
})
|
||||
}
|
||||
|
||||
func (e InvalidArgumentError) HTMLBody(requestID, hostID string) []byte {
|
||||
fields := []ErrorField{
|
||||
{Name: "ArgumentName", Value: e.ArgumentName},
|
||||
{Name: "ArgumentValue", Value: e.ArgumentValue},
|
||||
}
|
||||
// Region is only set for the credential region mismatch; every other
|
||||
// invalid argument error leaves it out.
|
||||
if e.Region != "" {
|
||||
fields = append(fields, ErrorField{Name: "Region", Value: e.Region})
|
||||
}
|
||||
|
||||
return e.BaseError().encodeHTMLResponse(requestID, hostID, fields...)
|
||||
}
|
||||
|
||||
// ExpectedRegion implements RegionMismatchError. Only the credential region
|
||||
// mismatch sets Region; every other invalid argument error returns "".
|
||||
func (e InvalidArgumentError) ExpectedRegion() string { return e.Region }
|
||||
|
||||
func GetInvalidArgumentErr(code InvalidArgErrorCode, value string) InvalidArgumentError {
|
||||
err := invalidArgErrResponses[code]
|
||||
err.ArgumentValue = value
|
||||
return err
|
||||
}
|
||||
|
||||
func GetInvalidArgMaxLimiter(name, value string) InvalidArgumentError {
|
||||
return InvalidArgumentError{
|
||||
ArgumentName: name,
|
||||
ArgumentValue: value,
|
||||
Description: fmt.Sprintf("Provided %s not an integer or within integer range", value),
|
||||
}
|
||||
}
|
||||
|
||||
func GetInvalidArgNegativeMaxLimiter(name, value string) InvalidArgumentError {
|
||||
return InvalidArgumentError{
|
||||
ArgumentName: name,
|
||||
ArgumentValue: value,
|
||||
Description: fmt.Sprintf("Argument %s must be an integer between 0 and 2147483647", value),
|
||||
}
|
||||
}
|
||||
|
||||
func GetInvalidArgExceedingRange(size int64) InvalidArgumentError {
|
||||
return InvalidArgumentError{
|
||||
ArgumentName: "x-amz-copy-source-range",
|
||||
ArgumentValue: fmt.Sprint(size),
|
||||
Description: fmt.Sprintf("Range specified is not valid for source object of size: %d", size),
|
||||
}
|
||||
}
|
||||
|
||||
func GetInvalidArgObjectOwnership(value string) InvalidArgumentError {
|
||||
return InvalidArgumentError{
|
||||
ArgumentName: "x-amz-object-ownership",
|
||||
// no ArgumentValue is returned for this error
|
||||
Description: fmt.Sprintf("Invalid x-amz-object-ownership header: %s", value),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,674 @@
|
||||
// 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"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
||||
"github.com/google/uuid"
|
||||
"github.com/versity/versitygw/s3err"
|
||||
)
|
||||
|
||||
func ListMultipartUploads_non_existing_bucket(s *S3Conf) error {
|
||||
testName := "ListMultipartUploads_non_existing_bucket"
|
||||
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
||||
bucketName := getBucketName()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
_, err := s3client.ListMultipartUploads(ctx, &s3.ListMultipartUploadsInput{
|
||||
Bucket: &bucketName,
|
||||
})
|
||||
cancel()
|
||||
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrNoSuchBucket)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func ListMultipartUploads_empty_result(s *S3Conf) error {
|
||||
testName := "ListMultipartUploads_empty_result"
|
||||
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
out, err := s3client.ListMultipartUploads(ctx, &s3.ListMultipartUploadsInput{
|
||||
Bucket: &bucket,
|
||||
})
|
||||
cancel()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(out.Uploads) != 0 {
|
||||
return fmt.Errorf("expected empty uploads, instead got %+v",
|
||||
out.Uploads)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func ListMultipartUploads_invalid_max_uploads(s *S3Conf) error {
|
||||
testName := "ListMultipartUploads_invalid_max_uploads"
|
||||
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
||||
maxUploads := int32(-3)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
_, err := s3client.ListMultipartUploads(ctx, &s3.ListMultipartUploadsInput{
|
||||
Bucket: &bucket,
|
||||
MaxUploads: &maxUploads,
|
||||
})
|
||||
cancel()
|
||||
if err := checkApiErr(err, s3err.GetInvalidArgNegativeMaxLimiter("max-uploads", fmt.Sprint(maxUploads))); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func ListMultipartUploads_max_uploads(s *S3Conf) error {
|
||||
testName := "ListMultipartUploads_max_uploads"
|
||||
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
||||
uploads := []types.MultipartUpload{}
|
||||
for i := 1; i < 6; i++ {
|
||||
out, err := createMp(s3client, bucket, fmt.Sprintf("obj%v", i))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uploads = append(uploads, types.MultipartUpload{
|
||||
UploadId: out.UploadId,
|
||||
Key: out.Key,
|
||||
StorageClass: types.StorageClassStandard,
|
||||
})
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
maxUploads := int32(2)
|
||||
out, err := s3client.ListMultipartUploads(ctx, &s3.ListMultipartUploadsInput{
|
||||
Bucket: &bucket,
|
||||
MaxUploads: &maxUploads,
|
||||
})
|
||||
cancel()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if out.IsTruncated == nil {
|
||||
return fmt.Errorf("unexpected nil is-truncated")
|
||||
}
|
||||
if out.MaxUploads == nil {
|
||||
return fmt.Errorf("unexpected nil max-uploads")
|
||||
}
|
||||
if !*out.IsTruncated {
|
||||
return fmt.Errorf("expected the output to be truncated")
|
||||
}
|
||||
if *out.MaxUploads != 2 {
|
||||
return fmt.Errorf("expected max-uploads to be 2, instead got %v",
|
||||
out.MaxUploads)
|
||||
}
|
||||
if ok := compareMultipartUploads(out.Uploads, uploads[:2]); !ok {
|
||||
return fmt.Errorf("expected multipart uploads to be %v, instead got %v",
|
||||
uploads[:2], out.Uploads)
|
||||
}
|
||||
if getString(out.NextKeyMarker) != getString(uploads[1].Key) {
|
||||
return fmt.Errorf("expected next-key-marker to be %v, instead got %v",
|
||||
getString(uploads[1].Key), getString(out.NextKeyMarker))
|
||||
}
|
||||
if getString(out.NextUploadIdMarker) != getString(uploads[1].UploadId) {
|
||||
return fmt.Errorf("expected next-upload-id-marker to be %v, instead got %v",
|
||||
getString(uploads[1].UploadId), getString(out.NextUploadIdMarker))
|
||||
}
|
||||
|
||||
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
|
||||
out, err = s3client.ListMultipartUploads(ctx, &s3.ListMultipartUploadsInput{
|
||||
Bucket: &bucket,
|
||||
KeyMarker: out.NextKeyMarker,
|
||||
})
|
||||
cancel()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ok := compareMultipartUploads(out.Uploads, uploads[2:]); !ok {
|
||||
return fmt.Errorf("expected multipart uploads to be %v, instead got %v",
|
||||
uploads[2:], out.Uploads)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func ListMultipartUploads_exceeding_max_uploads(s *S3Conf) error {
|
||||
testName := "ListMultipartUploads_exceeding_max_uploads"
|
||||
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
||||
maxUploads := int32(1343235)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
res, err := s3client.ListMultipartUploads(ctx, &s3.ListMultipartUploadsInput{
|
||||
Bucket: &bucket,
|
||||
MaxUploads: &maxUploads,
|
||||
})
|
||||
cancel()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if res.MaxUploads == nil {
|
||||
return fmt.Errorf("unexpected nil max-uploads")
|
||||
}
|
||||
if *res.MaxUploads != 1000 {
|
||||
return fmt.Errorf("expected max-uploads to be %v, instaed got %v",
|
||||
1000, *res.MaxUploads)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func ListMultipartUploads_ignore_upload_id_marker(s *S3Conf) error {
|
||||
testName := "ListMultipartUploads_ignore_upload_id_marker"
|
||||
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
||||
uploads := []types.MultipartUpload{}
|
||||
for i := 1; i < 6; i++ {
|
||||
out, err := createMp(s3client, bucket, fmt.Sprintf("obj%v", i))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uploads = append(uploads, types.MultipartUpload{
|
||||
UploadId: out.UploadId,
|
||||
Key: out.Key,
|
||||
StorageClass: types.StorageClassStandard,
|
||||
})
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
out, err := s3client.ListMultipartUploads(ctx, &s3.ListMultipartUploadsInput{
|
||||
Bucket: &bucket,
|
||||
UploadIdMarker: uploads[2].UploadId,
|
||||
})
|
||||
cancel()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !compareMultipartUploads(out.Uploads, uploads) {
|
||||
return fmt.Errorf("expected multipart uploads to be %v, instead got %v",
|
||||
uploads, out.Uploads)
|
||||
}
|
||||
|
||||
// should ignore invalid uploaId marker
|
||||
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
|
||||
out, err = s3client.ListMultipartUploads(ctx, &s3.ListMultipartUploadsInput{
|
||||
Bucket: &bucket,
|
||||
UploadIdMarker: getPtr("invalid_uploadId_marker"),
|
||||
})
|
||||
cancel()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !compareMultipartUploads(out.Uploads, uploads) {
|
||||
return fmt.Errorf("expected multipart uploads to be %v, instead got %v",
|
||||
uploads, out.Uploads)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func ListMultipartUploads_invalid_uploadId_marker(s *S3Conf) error {
|
||||
testName := "ListMultipartUploads_invalid_uploadId_marker"
|
||||
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
||||
uploads := make([]types.MultipartUpload, 0, 5)
|
||||
for i := range 5 {
|
||||
out, err := createMp(s3client, bucket, fmt.Sprintf("obj-%v", i))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uploads = append(uploads, types.MultipartUpload{
|
||||
UploadId: out.UploadId,
|
||||
Key: out.Key,
|
||||
StorageClass: types.StorageClassStandard,
|
||||
})
|
||||
}
|
||||
|
||||
// invalid UUID
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
_, err := s3client.ListMultipartUploads(ctx, &s3.ListMultipartUploadsInput{
|
||||
Bucket: &bucket,
|
||||
KeyMarker: getPtr("obj-2"),
|
||||
UploadIdMarker: getPtr("invalid_uploadId_marker"),
|
||||
})
|
||||
cancel()
|
||||
if err := checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgUploadIdMarker, "invalid_uploadId_marker")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// valid UUID, but not from the list
|
||||
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
|
||||
uploadIDMarker := uuid.New().String()
|
||||
_, err = s3client.ListMultipartUploads(ctx, &s3.ListMultipartUploadsInput{
|
||||
Bucket: &bucket,
|
||||
KeyMarker: getPtr("obj-2"),
|
||||
UploadIdMarker: &uploadIDMarker,
|
||||
})
|
||||
cancel()
|
||||
if err := checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgUploadIdMarker, uploadIDMarker)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// uploadId marker and key marker mismatch
|
||||
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
|
||||
_, err = s3client.ListMultipartUploads(ctx, &s3.ListMultipartUploadsInput{
|
||||
Bucket: &bucket,
|
||||
KeyMarker: getPtr("obj-2"),
|
||||
UploadIdMarker: uploads[4].UploadId,
|
||||
})
|
||||
cancel()
|
||||
return checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgUploadIdMarker, *uploads[4].UploadId))
|
||||
})
|
||||
}
|
||||
|
||||
func ListMultipartUploads_keyMarker_not_from_list(s *S3Conf) error {
|
||||
testName := "ListMultipartUploads_keyMarker_not_from_list"
|
||||
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
||||
uploads := make([]types.MultipartUpload, 0, 9)
|
||||
for _, mp := range []struct {
|
||||
key string
|
||||
count int
|
||||
}{
|
||||
{"bar", 3},
|
||||
{"baz", 4},
|
||||
{"foo", 2},
|
||||
} {
|
||||
for range mp.count {
|
||||
out, err := createMp(s3client, bucket, mp.key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uploads = append(uploads, types.MultipartUpload{
|
||||
Key: out.Key,
|
||||
UploadId: out.UploadId,
|
||||
StorageClass: types.StorageClassStandard,
|
||||
})
|
||||
// add an artificial delay, since some backends
|
||||
// (e.g. azure, or posix on filesystems with coarse
|
||||
// mtime resolution) can report the same identical
|
||||
// creation/initiated time for uploads created back
|
||||
// to back, making the resulting order non-deterministic
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
// without uploadId marker
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
out, err := s3client.ListMultipartUploads(ctx, &s3.ListMultipartUploadsInput{
|
||||
Bucket: &bucket,
|
||||
KeyMarker: getPtr("bat"),
|
||||
})
|
||||
cancel()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !compareMultipartUploads(uploads[3:], out.Uploads) {
|
||||
return fmt.Errorf("expected the mp list to be %v, instead got %v", uploads[3:], out.Uploads)
|
||||
}
|
||||
|
||||
// should start the listing after the specified uploadId marker
|
||||
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
|
||||
out, err = s3client.ListMultipartUploads(ctx, &s3.ListMultipartUploadsInput{
|
||||
Bucket: &bucket,
|
||||
KeyMarker: getPtr("bat"),
|
||||
UploadIdMarker: uploads[4].UploadId,
|
||||
})
|
||||
cancel()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !compareMultipartUploads(uploads[5:], out.Uploads) {
|
||||
return fmt.Errorf("expected the mp list to be %v, instead got %v", uploads[5:], out.Uploads)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func ListMultipartUploads_delimiter_truncated(s *S3Conf) error {
|
||||
testName := "ListMultipartUploads_delimiter_truncated"
|
||||
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
||||
uploads := make([]types.MultipartUpload, 0, 6)
|
||||
for _, key := range []string{
|
||||
"abc/something",
|
||||
"foo/bar/baz",
|
||||
"foo/quxx",
|
||||
"xyz/hello",
|
||||
"zzz/bca",
|
||||
"some/very/nested/mp/object",
|
||||
} {
|
||||
out, err := createMp(s3client, bucket, key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uploads = append(uploads, types.MultipartUpload{
|
||||
Key: out.Key,
|
||||
UploadId: out.UploadId,
|
||||
StorageClass: types.StorageClassStandard,
|
||||
})
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
out, err := s3client.ListMultipartUploads(ctx, &s3.ListMultipartUploadsInput{
|
||||
Bucket: &bucket,
|
||||
Delimiter: getPtr("/"),
|
||||
MaxUploads: getPtr(int32(2)),
|
||||
})
|
||||
cancel()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(out.Uploads) != 0 {
|
||||
return fmt.Errorf("expected empty uplodas list, instead got %v", out.Uploads)
|
||||
}
|
||||
expectedCps := []string{"abc/", "foo/"}
|
||||
if !comparePrefixes(expectedCps, out.CommonPrefixes) {
|
||||
return fmt.Errorf("expected the common prefixes to be %v, instead got %v", expectedCps, out.CommonPrefixes)
|
||||
}
|
||||
if getString(out.NextKeyMarker) != "foo/" {
|
||||
return fmt.Errorf("expected the next key marker to be 'foo/', instead got %s", getString(out.NextKeyMarker))
|
||||
}
|
||||
if getString(out.NextUploadIdMarker) != getString(uploads[1].UploadId) {
|
||||
return fmt.Errorf("expected the next upload id marker to be %s, instead got %s", getString(uploads[1].UploadId), getString(out.NextUploadIdMarker))
|
||||
}
|
||||
if !*out.IsTruncated {
|
||||
return fmt.Errorf("expected a truncated response")
|
||||
}
|
||||
|
||||
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
|
||||
out2, err := s3client.ListMultipartUploads(ctx, &s3.ListMultipartUploadsInput{
|
||||
Bucket: &bucket,
|
||||
Delimiter: getPtr("/"),
|
||||
UploadIdMarker: out.NextUploadIdMarker,
|
||||
KeyMarker: out.NextKeyMarker,
|
||||
})
|
||||
cancel()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(out2.Uploads) != 0 {
|
||||
return fmt.Errorf("expected empty uplodas list, instead got %v", out2.Uploads)
|
||||
}
|
||||
expectedCps = []string{"foo/", "some/", "xyz/", "zzz/"}
|
||||
if !comparePrefixes(expectedCps, out2.CommonPrefixes) {
|
||||
return fmt.Errorf("expected the common prefixes to be %v, instead got %v", expectedCps, out2.CommonPrefixes)
|
||||
}
|
||||
if getString(out2.KeyMarker) != "foo/" {
|
||||
return fmt.Errorf("expected key marker to be 'foo/', instead got %s", getString(out2.KeyMarker))
|
||||
}
|
||||
if getString(out2.UploadIdMarker) != getString(uploads[1].UploadId) {
|
||||
return fmt.Errorf("expected the upload id marker to be %s, instead got %s", getString(uploads[1].UploadId), getString(out2.UploadIdMarker))
|
||||
}
|
||||
if getString(out2.NextKeyMarker) != "" {
|
||||
return fmt.Errorf("expected empty next key marker, instead got %s", getString(out2.NextKeyMarker))
|
||||
}
|
||||
if getString(out2.NextUploadIdMarker) != "" {
|
||||
return fmt.Errorf("expected empty next upload id marker, instead got %s", getString(out2.NextUploadIdMarker))
|
||||
}
|
||||
if *out2.IsTruncated {
|
||||
return fmt.Errorf("expected a non-truncated response")
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func ListMultipartUploads_prefix(s *S3Conf) error {
|
||||
testName := "ListMultipartUploads_prefix"
|
||||
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
||||
prefix := "foo"
|
||||
uploads := make([]types.MultipartUpload, 0, 8)
|
||||
for _, key := range []string{
|
||||
"abc/something",
|
||||
"foo/bar/baz",
|
||||
"foo/quxx",
|
||||
"hello/world",
|
||||
"xyz/hello",
|
||||
"zzz/bca",
|
||||
"some/very/nested/mp/object",
|
||||
"foo/xyz",
|
||||
} {
|
||||
out, err := createMp(s3client, bucket, key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if strings.HasPrefix(key, prefix) {
|
||||
uploads = append(uploads, types.MultipartUpload{
|
||||
Key: out.Key,
|
||||
UploadId: out.UploadId,
|
||||
StorageClass: types.StorageClassStandard,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
out, err := s3client.ListMultipartUploads(ctx, &s3.ListMultipartUploadsInput{
|
||||
Bucket: &bucket,
|
||||
Prefix: &prefix,
|
||||
})
|
||||
cancel()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if getString(out.Prefix) != prefix {
|
||||
return fmt.Errorf("expected the prefix to be %s, instead got %s", prefix, getString(out.Prefix))
|
||||
}
|
||||
if !compareMultipartUploads(out.Uploads, uploads) {
|
||||
return fmt.Errorf("expected the uploads list to be %v, instead got %v", uploads, out.Uploads)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func ListMultipartUploads_both_delimiter_and_prefix(s *S3Conf) error {
|
||||
testName := "ListMultipartUploads_both_delimiter_and_prefix"
|
||||
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
||||
for _, key := range []string{
|
||||
"foo/abc/bbb/aaa/c",
|
||||
"abc/something",
|
||||
"foo/bar/baz",
|
||||
"foo/quxx",
|
||||
"hello/world",
|
||||
"foo/random/object",
|
||||
"foo/random/another/object",
|
||||
"xyz/hello",
|
||||
"zzz/bca",
|
||||
"some/very/nested/mp/object",
|
||||
"foo/xyz",
|
||||
} {
|
||||
_, err := createMp(s3client, bucket, key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
out, err := s3client.ListMultipartUploads(ctx, &s3.ListMultipartUploadsInput{
|
||||
Bucket: &bucket,
|
||||
Delimiter: getPtr("/"),
|
||||
Prefix: getPtr("foo/"),
|
||||
})
|
||||
cancel()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
expectedCps := []string{"foo/abc/", "foo/bar/", "foo/random/"}
|
||||
if !comparePrefixes(expectedCps, out.CommonPrefixes) {
|
||||
return fmt.Errorf("expected the common prefixes to be %v, instead got %v", expectedCps, out.CommonPrefixes)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func ListMultipartUploads_delimiter_no_matches(s *S3Conf) error {
|
||||
testName := "ListMultipartUploads_delimiter_no_matches"
|
||||
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
||||
uploads := make([]types.MultipartUpload, 0, 8)
|
||||
for _, key := range []string{
|
||||
"abc/something",
|
||||
"foo/bar/baz",
|
||||
"foo/quxx",
|
||||
"hello/world",
|
||||
"xyz/hello",
|
||||
"zzz/bca",
|
||||
"some/very/nested/mp/object",
|
||||
"foo/xyz",
|
||||
} {
|
||||
out, err := createMp(s3client, bucket, key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uploads = append(uploads, types.MultipartUpload{
|
||||
Key: out.Key,
|
||||
UploadId: out.UploadId,
|
||||
StorageClass: types.StorageClassStandard,
|
||||
})
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
out, err := s3client.ListMultipartUploads(ctx, &s3.ListMultipartUploadsInput{
|
||||
Bucket: &bucket,
|
||||
Delimiter: getPtr("delim"),
|
||||
})
|
||||
cancel()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sort.SliceStable(uploads, func(i, j int) bool {
|
||||
return *uploads[i].Key < *uploads[j].Key
|
||||
})
|
||||
|
||||
if !compareMultipartUploads(uploads, out.Uploads) {
|
||||
return fmt.Errorf("expected the uploads to be %v, instead got %v", uploads, out.Uploads)
|
||||
}
|
||||
if len(out.CommonPrefixes) != 0 {
|
||||
return fmt.Errorf("expected empty common prefixes, instead got %v", out.CommonPrefixes)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func ListMultipartUploads_with_checksums(s *S3Conf) error {
|
||||
testName := "ListMultipartUploads_with_checksums"
|
||||
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
||||
uploads := []types.MultipartUpload{}
|
||||
for _, el := range []struct {
|
||||
obj string
|
||||
algo types.ChecksumAlgorithm
|
||||
t types.ChecksumType
|
||||
}{
|
||||
{
|
||||
obj: "obj-1",
|
||||
algo: types.ChecksumAlgorithmCrc32,
|
||||
t: types.ChecksumTypeComposite,
|
||||
},
|
||||
{
|
||||
obj: "obj-2",
|
||||
algo: types.ChecksumAlgorithmCrc32c,
|
||||
t: types.ChecksumTypeFullObject,
|
||||
},
|
||||
{
|
||||
obj: "obj-3",
|
||||
algo: types.ChecksumAlgorithmSha1,
|
||||
t: types.ChecksumTypeComposite,
|
||||
},
|
||||
{
|
||||
obj: "obj-4",
|
||||
algo: types.ChecksumAlgorithmSha256,
|
||||
t: types.ChecksumTypeComposite,
|
||||
},
|
||||
{
|
||||
obj: "obj-5",
|
||||
algo: types.ChecksumAlgorithmCrc64nvme,
|
||||
t: types.ChecksumTypeFullObject,
|
||||
},
|
||||
{
|
||||
obj: "obj-6",
|
||||
algo: types.ChecksumAlgorithmSha512,
|
||||
t: types.ChecksumTypeComposite,
|
||||
},
|
||||
{
|
||||
obj: "obj-7",
|
||||
algo: types.ChecksumAlgorithmMd5,
|
||||
t: types.ChecksumTypeComposite,
|
||||
},
|
||||
{
|
||||
obj: "obj-8",
|
||||
algo: types.ChecksumAlgorithmXxhash64,
|
||||
t: types.ChecksumTypeComposite,
|
||||
},
|
||||
{
|
||||
obj: "obj-9",
|
||||
algo: types.ChecksumAlgorithmXxhash3,
|
||||
t: types.ChecksumTypeComposite,
|
||||
},
|
||||
// obj-91 to preserve the lexicographical order
|
||||
{
|
||||
obj: "obj-91",
|
||||
algo: types.ChecksumAlgorithmXxhash128,
|
||||
t: types.ChecksumTypeComposite,
|
||||
},
|
||||
} {
|
||||
key := el.obj
|
||||
mp, err := createMp(s3client, bucket, key, withChecksum(el.algo), withChecksumType(el.t))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uploads = append(uploads, types.MultipartUpload{
|
||||
Key: &key,
|
||||
UploadId: mp.UploadId,
|
||||
StorageClass: types.StorageClassStandard,
|
||||
ChecksumAlgorithm: el.algo,
|
||||
ChecksumType: el.t,
|
||||
})
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
res, err := s3client.ListMultipartUploads(ctx, &s3.ListMultipartUploadsInput{
|
||||
Bucket: &bucket,
|
||||
})
|
||||
cancel()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !compareMultipartUploads(res.Uploads, uploads) {
|
||||
return fmt.Errorf("expected the final multipart uploads to be %v, instead got %v",
|
||||
uploads, res.Uploads)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -205,6 +205,8 @@ func TestPutObject(ts *TestState) {
|
||||
ts.Run(PutObject_with_metadata)
|
||||
}
|
||||
ts.Run(PutObject_success)
|
||||
ts.Run(PutObject_sse_c_invalid_headers)
|
||||
ts.Run(PutObject_unsupported_sse_not_implemented)
|
||||
ts.Run(PutObject_default_content_type)
|
||||
ts.Run(PutObject_overwrite_resets_attributes)
|
||||
ts.Run(PutObject_overwrite_multipart_object)
|
||||
@@ -238,6 +240,7 @@ func TestHeadObject(ts *TestState) {
|
||||
ts.Run(HeadObject_ranged_with_checksum_mode)
|
||||
}
|
||||
ts.Run(HeadObject_success)
|
||||
ts.Run(HeadObject_sse_c_invalid_headers)
|
||||
ts.Run(HeadObject_overrides_success)
|
||||
ts.Run(HeadObject_overrides_presign_success)
|
||||
ts.Run(HeadObject_overrides_fail_public)
|
||||
@@ -417,6 +420,7 @@ func TestCopyObject(ts *TestState) {
|
||||
}
|
||||
ts.Run(CopyObject_with_special_characters)
|
||||
ts.Run(CopyObject_success)
|
||||
ts.Run(CopyObject_sse_c_invalid_copy_source_headers)
|
||||
ts.Run(CopyObject_cross_bucket_server_side_copy)
|
||||
ts.Run(CopyObject_incorrect_source_bucket_expected_owner)
|
||||
}
|
||||
@@ -903,6 +907,28 @@ func TestNotImplementedActions(ts *TestState) {
|
||||
ts.Run(GetObjectAcl_not_implemented)
|
||||
}
|
||||
|
||||
// TestSSECNotImplemented is for backends that reject SSE-C rather than forward it.
|
||||
func TestSSECNotImplemented(ts *TestState) {
|
||||
ts.Run(PutObject_sse_c_not_implemented)
|
||||
ts.Run(PostObject_sse_c_not_implemented)
|
||||
ts.Run(GetObject_sse_c_not_implemented)
|
||||
ts.Run(HeadObject_sse_c_not_implemented)
|
||||
ts.Run(GetObjectAttributes_sse_c_not_implemented)
|
||||
ts.Run(CopyObject_sse_c_not_implemented)
|
||||
ts.Run(CreateMultipartUpload_sse_c_not_implemented)
|
||||
ts.Run(UploadPart_sse_c_not_implemented)
|
||||
ts.Run(UploadPartCopy_sse_c_not_implemented)
|
||||
ts.Run(CompleteMultipartUpload_sse_c_not_implemented)
|
||||
}
|
||||
|
||||
func TestSSECTransport(ts *TestState) {
|
||||
if ts.conf.tlsStatus {
|
||||
TestSSECNotImplemented(ts)
|
||||
return
|
||||
}
|
||||
ts.Run(PutObject_sse_c_requires_tls)
|
||||
}
|
||||
|
||||
func TestWORMProtection(ts *TestState) {
|
||||
ts.Run(WORMProtection_bucket_object_lock_configuration_compliance_mode)
|
||||
ts.Run(WORMProtection_bucket_object_lock_configuration_governance_mode)
|
||||
@@ -1003,6 +1029,9 @@ func TestFullFlow(ts *TestState) {
|
||||
if ts.conf.versioningEnabled {
|
||||
TestVersioning(ts)
|
||||
}
|
||||
if ts.conf.azureTests {
|
||||
TestSSECTransport(ts)
|
||||
}
|
||||
TestGatewayIAM(ts)
|
||||
TestServer(ts)
|
||||
}
|
||||
@@ -1033,6 +1062,7 @@ func TestPosix(ts *TestState) {
|
||||
if !ts.conf.versioningEnabled {
|
||||
TestVersioningDisabled(ts)
|
||||
}
|
||||
TestSSECTransport(ts)
|
||||
}
|
||||
|
||||
func TestScoutfs(ts *TestState) {
|
||||
@@ -1134,6 +1164,7 @@ func TestScoutfs(ts *TestState) {
|
||||
ts.Run(ObjectTagging_trailing_slash_counterpart)
|
||||
ts.Run(ObjectLock_trailing_slash_counterpart)
|
||||
ts.Run(DeleteObject_directory_not_empty)
|
||||
TestSSECTransport(ts)
|
||||
}
|
||||
|
||||
func TestGatewayIAM(ts *TestState) {
|
||||
@@ -2162,7 +2193,6 @@ func TestRouter(ts *TestState) {
|
||||
ts.Run(RouterObjectAnnotationNotImplemented)
|
||||
ts.Run(RouterRenameObjectNotImplemented)
|
||||
ts.Run(RouterObjectTorrentMethodNotAllowed)
|
||||
ts.Run(RouterInvalidURLEscape)
|
||||
}
|
||||
|
||||
func TestPostObject(ts *TestState) {
|
||||
@@ -3540,6 +3570,21 @@ func GetIntTests() IntTests {
|
||||
"PutObject_with_slashes": PutObject_with_slashes,
|
||||
"PutObject_race_with_delete": PutObject_race_with_delete,
|
||||
"CreateMultipartUpload_dir_obj": CreateMultipartUpload_dir_obj,
|
||||
"PutObject_sse_c_not_implemented": PutObject_sse_c_not_implemented,
|
||||
"PutObject_unsupported_sse_not_implemented": PutObject_unsupported_sse_not_implemented,
|
||||
"PutObject_sse_c_requires_tls": PutObject_sse_c_requires_tls,
|
||||
"PostObject_sse_c_not_implemented": PostObject_sse_c_not_implemented,
|
||||
"GetObject_sse_c_not_implemented": GetObject_sse_c_not_implemented,
|
||||
"HeadObject_sse_c_not_implemented": HeadObject_sse_c_not_implemented,
|
||||
"GetObjectAttributes_sse_c_not_implemented": GetObjectAttributes_sse_c_not_implemented,
|
||||
"CopyObject_sse_c_not_implemented": CopyObject_sse_c_not_implemented,
|
||||
"CreateMultipartUpload_sse_c_not_implemented": CreateMultipartUpload_sse_c_not_implemented,
|
||||
"UploadPart_sse_c_not_implemented": UploadPart_sse_c_not_implemented,
|
||||
"UploadPartCopy_sse_c_not_implemented": UploadPartCopy_sse_c_not_implemented,
|
||||
"CompleteMultipartUpload_sse_c_not_implemented": CompleteMultipartUpload_sse_c_not_implemented,
|
||||
"PutObject_sse_c_invalid_headers": PutObject_sse_c_invalid_headers,
|
||||
"HeadObject_sse_c_invalid_headers": HeadObject_sse_c_invalid_headers,
|
||||
"CopyObject_sse_c_invalid_copy_source_headers": CopyObject_sse_c_invalid_copy_source_headers,
|
||||
"IAM_user_access_denied": IAM_user_access_denied,
|
||||
"IAM_userplus_access_denied": IAM_userplus_access_denied,
|
||||
"IAM_userplus_CreateBucket": IAM_userplus_CreateBucket,
|
||||
@@ -3750,7 +3795,6 @@ func GetIntTests() IntTests {
|
||||
"RouterObjectAnnotationNotImplemented": RouterObjectAnnotationNotImplemented,
|
||||
"RouterRenameObjectNotImplemented": RouterRenameObjectNotImplemented,
|
||||
"RouterObjectTorrentMethodNotAllowed": RouterObjectTorrentMethodNotAllowed,
|
||||
"RouterInvalidURLEscape": RouterInvalidURLEscape,
|
||||
"UnsignedStreaminPayloadTrailer_malformed_trailer": UnsignedStreaminPayloadTrailer_malformed_trailer,
|
||||
"UnsignedStreamingPayloadTrailer_aborted_connection": UnsignedStreamingPayloadTrailer_aborted_connection,
|
||||
"UnsignedStreamingPayloadTrailer_missing_invalid_dec_content_length": UnsignedStreamingPayloadTrailer_missing_invalid_dec_content_length,
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
#!/usr/bin/env bats
|
||||
|
||||
# Copyright 2026 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.
|
||||
|
||||
load ./bats-support/load
|
||||
load ./bats-assert/load
|
||||
|
||||
source ./tests/drivers/create_bucket/create_bucket_rest.sh
|
||||
source ./tests/drivers/list_multipart_uploads/list_multipart_uploads_rest.sh
|
||||
source ./tests/drivers/file.sh
|
||||
source ./tests/setup.sh
|
||||
|
||||
# tags: curl,ListMultipartUploads,multipart
|
||||
@test "REST - ListMultipartUploads - Initiator and Owner data shown" {
|
||||
if [ "$DIRECT" != "true" ]; then
|
||||
skip "https://github.com/versity/versitygw/issues/2142"
|
||||
fi
|
||||
run setup_bucket_and_file_v3 "$BUCKET_ONE_NAME"
|
||||
assert_success
|
||||
read -r bucket_name test_file <<< "$output"
|
||||
|
||||
run create_multipart_upload_rest "$bucket_name" "$test_file" "" "parse_upload_id"
|
||||
assert_success
|
||||
upload_id=$output
|
||||
|
||||
local owner_id initiator_id
|
||||
if [ "$DIRECT" == "true" ]; then
|
||||
owner_id="$AWS_CANONICAL_ID"
|
||||
initiator_id="arn:aws:iam::$DIRECT_AWS_USER_ID:user/$DIRECT_S3_ROOT_ACCOUNT_NAME"
|
||||
else
|
||||
owner_id="$AWS_ACCESS_KEY_ID"
|
||||
initiator_id="$AWS_ACCESS_KEY_ID"
|
||||
fi
|
||||
|
||||
run list_multipart_uploads_check_user_data "$bucket_name" "$upload_id" "$owner_id" "$initiator_id"
|
||||
assert_success
|
||||
}
|
||||
|
||||
# tags: curl,ListMultipartUploads,multipart,uploadId,next-key-marker,next-upload-id-marker
|
||||
@test "REST - ListMultipartUploads - NextKeyMarker and NextUploadIdMarker set to last item" {
|
||||
if [ "$DIRECT" != "true" ]; then
|
||||
skip "https://github.com/versity/versitygw/issues/2144"
|
||||
fi
|
||||
run setup_bucket_and_files_v3 "$BUCKET_ONE_NAME" 2
|
||||
assert_success
|
||||
read -r bucket_name test_file_one test_file_two <<< "$output"
|
||||
|
||||
local upload_id_one upload_id_two
|
||||
run create_multipart_upload_rest "$bucket_name" "$test_file_one" "" "parse_upload_id"
|
||||
assert_success
|
||||
upload_id_one=$output
|
||||
|
||||
run create_multipart_upload_rest "$bucket_name" "$test_file_two" "" "parse_upload_id"
|
||||
assert_success
|
||||
upload_id_two=$output
|
||||
|
||||
local last_key last_upload_id
|
||||
if [[ "$test_file_one" < "$test_file_two" ]]; then
|
||||
last_key="$test_file_two"
|
||||
last_upload_id="$upload_id_two"
|
||||
else
|
||||
last_key="$test_file_one"
|
||||
last_upload_id="$upload_id_one"
|
||||
fi
|
||||
|
||||
run list_multipart_uploads_check_next_values "$bucket_name" "$last_key" "$last_upload_id"
|
||||
assert_success
|
||||
}
|
||||
|
||||
# tags: curl,ListMultipartUploads,multipart,uploadId,key-marker
|
||||
@test "REST - ListMultipartUploads - uploadId and key combo work" {
|
||||
if [ "$DIRECT" != "true" ]; then
|
||||
skip "https://github.com/versity/versitygw/issues/2149"
|
||||
fi
|
||||
run setup_bucket_and_files_v3 "$BUCKET_ONE_NAME" 2
|
||||
assert_success
|
||||
read -r bucket_name test_file_one test_file_two <<< "$output"
|
||||
|
||||
local upload_id_one upload_id_two
|
||||
run create_multipart_upload_rest "$bucket_name" "$test_file_one" "" "parse_upload_id"
|
||||
assert_success
|
||||
upload_id_one=$output
|
||||
|
||||
run create_multipart_upload_rest "$bucket_name" "$test_file_two" "" "parse_upload_id"
|
||||
assert_success
|
||||
upload_id_two=$output
|
||||
|
||||
local first_key first_upload_id second_key second_upload_id
|
||||
if [[ "$test_file_one" < "$test_file_two" ]]; then
|
||||
first_key="$test_file_one"
|
||||
first_upload_id="$upload_id_one"
|
||||
second_key="$test_file_two"
|
||||
second_upload_id="$upload_id_two"
|
||||
else
|
||||
first_key="$test_file_two"
|
||||
first_upload_id="$upload_id_two"
|
||||
second_key="$test_file_one"
|
||||
second_upload_id="$upload_id_one"
|
||||
fi
|
||||
|
||||
run list_multipart_uploads_get_check_next_values "$bucket_name" "$second_key" "$second_upload_id" "$first_key" "$first_upload_id"
|
||||
assert_success
|
||||
}
|
||||
|
||||
# tags: curl,ListMultipartUploads,multipart,uploadId
|
||||
@test "REST - ListMultipartUploads - uploadId without key should be ignored" {
|
||||
if [ "$DIRECT" != "true" ]; then
|
||||
skip "https://github.com/versity/versitygw/issues/2150"
|
||||
fi
|
||||
run setup_bucket_and_file_v3 "$BUCKET_ONE_NAME"
|
||||
assert_success
|
||||
read -r bucket_name test_file <<< "$output"
|
||||
|
||||
run create_multipart_upload_rest "$bucket_name" "$test_file" "" "parse_upload_id"
|
||||
assert_success
|
||||
upload_id_one=$output
|
||||
|
||||
run create_multipart_upload_rest "$bucket_name" "$test_file" "" "parse_upload_id"
|
||||
assert_success
|
||||
upload_id_two=$output
|
||||
|
||||
local first_upload_id second_upload_id
|
||||
if [[ "$upload_id_one" < "$upload_id_two" ]]; then
|
||||
first_upload_id="$upload_id_one"
|
||||
second_upload_id="$upload_id_two"
|
||||
else
|
||||
first_upload_id="$upload_id_two"
|
||||
second_upload_id="$upload_id_one"
|
||||
fi
|
||||
|
||||
run list_multipart_uploads_check_no_upload_id_in_response "$bucket_name" "$second_upload_id" "$first_upload_id"
|
||||
assert_success
|
||||
}
|
||||
|
||||
# tags: curl,ListMultipartUploads,multipart,prefix,delimiter
|
||||
@test "REST - ListMultipartUploads - prefix and delimiter" {
|
||||
file_names=("a-b-1.txt" "a-b-2.txt" "a-b/c-1.txt" "a-b/c-2.txt" "a-b/d.txt" "a/c.txt")
|
||||
local prefix="a-"
|
||||
run create_test_files_and_folders "${file_names[@]}"
|
||||
assert_success
|
||||
|
||||
run setup_bucket_v3 "$BUCKET_ONE_NAME"
|
||||
assert_success
|
||||
bucket_name="$output"
|
||||
|
||||
for file_name in "${file_names[@]}"; do
|
||||
run create_multipart_upload_rest "$bucket_name" "$file_name" "" ""
|
||||
assert_success
|
||||
done
|
||||
|
||||
run list_multipart_uploads_rest "$bucket_name"
|
||||
assert_success
|
||||
log 5 "uploads: $output"
|
||||
|
||||
run list_uploads_with_prefix_and_delimiter_check_results "$bucket_name" "$prefix" "/" "a-b/" "--" "a-b-1.txt" "a-b-2.txt"
|
||||
assert_success
|
||||
}
|
||||
|
||||
# tags: curl,ListMultipartUploads,multipart,encoding-type
|
||||
@test "REST - ListMultipartUploads - encoding type" {
|
||||
if [ "$DIRECT" != "true" ]; then
|
||||
skip "https://github.com/versity/versitygw/issues/2156"
|
||||
fi
|
||||
run setup_bucket_v3 "$BUCKET_ONE_NAME"
|
||||
assert_success
|
||||
bucket_name="$output"
|
||||
|
||||
run create_test_files_with_prefix " "
|
||||
assert_success
|
||||
base_file_name="$output"
|
||||
file_name="${base_file_name}(test)+"
|
||||
log 5 "file name: '$file_name'"
|
||||
|
||||
run send_rest_go_command_callback "200" "parse_upload_id" "-method" "POST" "-query" "uploads" "-bucketName" "$bucket_name" "-objectKey" "$file_name"
|
||||
assert_success
|
||||
upload_id=$output
|
||||
|
||||
run list_multipart_uploads_check_encoding "$bucket_name" "&encoding-type=url" "${base_file_name/ /+}%28test%29%2B"
|
||||
assert_success
|
||||
|
||||
run list_multipart_uploads_check_encoding "$bucket_name" "" "$file_name"
|
||||
assert_success
|
||||
}
|
||||
|
||||
# tags: curl,ListMultipartUploads,multipart,encoding-type,invalid-query
|
||||
@test "REST - ListMultipartUploads - invalid encoding type" {
|
||||
if [ "$DIRECT" != "true" ]; then
|
||||
skip "https://github.com/versity/versitygw/issues/2156"
|
||||
fi
|
||||
run setup_bucket_and_file_v3 "$BUCKET_ONE_NAME"
|
||||
assert_success
|
||||
read -r bucket_name test_file <<< "$output"
|
||||
|
||||
run create_multipart_upload_rest "$bucket_name" "$test_file" "" "parse_upload_id"
|
||||
assert_success
|
||||
upload_id_one=$output
|
||||
|
||||
local invalid_encoding="abc"
|
||||
run send_rest_go_command_expect_error_with_arg_name_value "400" "InvalidArgument" "Invalid Encoding Method specified" \
|
||||
"encoding-type" "$invalid_encoding" "-bucketName" "$bucket_name" "-query" "uploads&encoding-type=$invalid_encoding"
|
||||
assert_success
|
||||
}
|
||||
|
||||
# tags: curl,ListMultipartUploads,multipart,uploadId,invalid-query
|
||||
@test "REST - ListMultipartUploads - uploads/uploadId combo" {
|
||||
if [ "$DIRECT" != "true" ]; then
|
||||
skip "https://github.com/versity/versitygw/issues/2155"
|
||||
fi
|
||||
run setup_bucket_and_file_v3 "$BUCKET_ONE_NAME"
|
||||
assert_success
|
||||
read -r bucket_name test_file <<< "$output"
|
||||
|
||||
run create_multipart_upload_rest "$bucket_name" "$test_file" "" "parse_upload_id"
|
||||
assert_success
|
||||
upload_id_one=$output
|
||||
|
||||
run send_rest_go_command_expect_error "400" "InvalidArgument" "Conflicting query string parameters" "-query" "uploads&uploadId=$upload_id_one" "-bucketName" "$bucket_name"
|
||||
assert_success
|
||||
}
|
||||
Reference in new issue
Block a user