2021-04-18 15:41:13 -04:00
// Copyright (c) 2015-2021 MinIO, Inc.
//
// This file is part of MinIO Object Storage stack
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
2016-08-16 22:24:23 -04:00
2016-08-18 19:23:42 -04:00
package cmd
2016-08-16 22:24:23 -04:00
import (
"bytes"
2018-03-15 16:27:16 -04:00
"context"
2018-09-20 22:22:09 -04:00
"crypto/md5"
2022-08-29 19:57:16 -04:00
"crypto/sha1"
2018-09-20 22:22:09 -04:00
"encoding/base64"
2018-11-27 16:23:32 -05:00
"encoding/hex"
2016-09-21 23:08:08 -04:00
"encoding/xml"
2016-10-07 11:02:37 -04:00
"fmt"
2022-08-29 19:57:16 -04:00
"hash"
"hash/crc32"
2016-10-01 11:23:26 -04:00
"io"
2016-08-16 22:24:23 -04:00
"net/http"
"net/http/httptest"
"net/url"
2022-01-02 12:15:06 -05:00
"path"
"runtime"
2016-10-01 11:23:26 -04:00
"strconv"
2022-01-02 12:15:06 -05:00
"strings"
2016-09-21 23:08:08 -04:00
"sync"
2016-08-16 22:24:23 -04:00
"testing"
2016-11-22 21:18:22 -05:00
2022-08-29 19:57:16 -04:00
"github.com/dustin/go-humanize"
2021-06-01 17:59:40 -04:00
"github.com/minio/minio/internal/auth"
2022-08-29 19:57:16 -04:00
"github.com/minio/minio/internal/hash/sha256"
2021-06-01 17:59:40 -04:00
xhttp "github.com/minio/minio/internal/http"
ioutilx "github.com/minio/minio/internal/ioutil"
2016-08-16 22:24:23 -04:00
)
2016-10-01 11:23:26 -04:00
// Type to capture different modifications to API request to simulate failure cases.
type Fault int
const (
None Fault = iota
MissingContentLength
TooBigObject
TooBigDecodedLength
BadSignature
BadMD5
2016-10-03 11:54:57 -04:00
MissingUploadID
2016-10-01 11:23:26 -04:00
)
2020-06-12 23:04:01 -04:00
// Wrapper for calling HeadObject API handler tests for both Erasure multiple disks and FS single drive setup.
2016-11-07 19:02:27 -05:00
func TestAPIHeadObjectHandler ( t * testing . T ) {
ExecObjectLayerAPITest ( t , testAPIHeadObjectHandler , [ ] string { "HeadObject" } )
}
func testAPIHeadObjectHandler ( obj ObjectLayer , instanceType , bucketName string , apiRouter http . Handler ,
2022-04-13 15:00:11 -04:00
credentials auth . Credentials , t * testing . T ,
) {
2016-11-07 19:02:27 -05:00
objectName := "test-object"
// set of byte data for PutObject.
// object has to be created before running tests for HeadObject.
// this is required even to assert the HeadObject data,
// since dataInserted === dataFetched back is a primary criteria for any object storage this assertion is critical.
bytesData := [ ] struct {
byteData [ ] byte
} {
2016-11-22 21:18:22 -05:00
{ generateBytesData ( 6 * humanize . MiByte ) } ,
2016-11-07 19:02:27 -05:00
}
// set of inputs for uploading the objects before tests for downloading is done.
putObjectInputs := [ ] struct {
bucketName string
objectName string
contentLength int64
textData [ ] byte
metaData map [ string ] string
} {
{ bucketName , objectName , int64 ( len ( bytesData [ 0 ] . byteData ) ) , bytesData [ 0 ] . byteData , make ( map [ string ] string ) } ,
}
// iterate through the above set of inputs and upload the object.
for i , input := range putObjectInputs {
// uploading the object.
2020-12-27 01:58:06 -05:00
_ , err := obj . PutObject ( context . Background ( ) , input . bucketName , input . objectName , mustGetPutObjReader ( t , bytes . NewReader ( input . textData ) , input . contentLength , input . metaData [ "" ] , "" ) , ObjectOptions { UserDefined : input . metaData } )
2016-11-07 19:02:27 -05:00
// if object upload fails stop the test.
if err != nil {
t . Fatalf ( "Put Object case %d: Error uploading object: <ERROR> %v" , i + 1 , err )
}
}
// test cases with inputs and expected result for HeadObject.
testCases := [ ] struct {
bucketName string
objectName string
accessKey string
secretKey string
// expected output.
expectedRespStatus int // expected response status body.
} {
// Test case - 1.
// Fetching stat info of object and validating it.
{
bucketName : bucketName ,
objectName : objectName ,
2016-12-26 13:21:23 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
2016-11-07 19:02:27 -05:00
expectedRespStatus : http . StatusOK ,
} ,
// Test case - 2.
// Case with non-existent object name.
{
bucketName : bucketName ,
objectName : "abcd" ,
2016-12-26 13:21:23 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
2016-11-07 19:02:27 -05:00
expectedRespStatus : http . StatusNotFound ,
} ,
// Test case - 3.
// Test case to induce a signature mismatch.
// Using invalid accessID.
{
bucketName : bucketName ,
objectName : objectName ,
accessKey : "Invalid-AccessID" ,
2016-12-26 13:21:23 -05:00
secretKey : credentials . SecretKey ,
2016-11-07 19:02:27 -05:00
expectedRespStatus : http . StatusForbidden ,
} ,
}
// Iterating over the cases, fetching the object validating the response.
for i , testCase := range testCases {
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
rec := httptest . NewRecorder ( )
// construct HTTP request for Get Object end point.
2020-07-20 15:52:49 -04:00
req , err := newTestSignedRequestV4 ( http . MethodHead , getHeadObjectURL ( "" , testCase . bucketName , testCase . objectName ) ,
2018-09-20 22:22:09 -04:00
0 , nil , testCase . accessKey , testCase . secretKey , nil )
2016-11-07 19:02:27 -05:00
if err != nil {
t . Fatalf ( "Test %d: %s: Failed to create HTTP request for Head Object: <ERROR> %v" , i + 1 , instanceType , err )
}
// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
// Call the ServeHTTP to execute the handler,`func (api objectAPIHandlers) GetObjectHandler` handles the request.
apiRouter . ServeHTTP ( rec , req )
// Assert the response code with the expected status.
if rec . Code != testCase . expectedRespStatus {
t . Fatalf ( "Case %d: Expected the response status to be `%d`, but instead found `%d`" , i + 1 , testCase . expectedRespStatus , rec . Code )
}
// Verify response of the V2 signed HTTP request.
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
recV2 := httptest . NewRecorder ( )
// construct HTTP request for Head Object endpoint.
2020-07-20 15:52:49 -04:00
reqV2 , err := newTestSignedRequestV2 ( http . MethodHead , getHeadObjectURL ( "" , testCase . bucketName , testCase . objectName ) ,
2018-09-20 22:22:09 -04:00
0 , nil , testCase . accessKey , testCase . secretKey , nil )
2016-11-07 19:02:27 -05:00
if err != nil {
t . Fatalf ( "Test %d: %s: Failed to create HTTP request for Head Object: <ERROR> %v" , i + 1 , instanceType , err )
}
// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
// Call the ServeHTTP to execute the handler.
apiRouter . ServeHTTP ( recV2 , reqV2 )
if recV2 . Code != testCase . expectedRespStatus {
t . Errorf ( "Test %d: %s: Expected the response status to be `%d`, but instead found `%d`" , i + 1 , instanceType , testCase . expectedRespStatus , recV2 . Code )
}
}
// Test for Anonymous/unsigned http request.
2020-07-20 15:52:49 -04:00
anonReq , err := newTestRequest ( http . MethodHead , getHeadObjectURL ( "" , bucketName , objectName ) , 0 , nil )
2016-11-07 19:02:27 -05:00
if err != nil {
2019-04-09 14:39:42 -04:00
t . Fatalf ( "MinIO %s: Failed to create an anonymous request for %s/%s: <ERROR> %v" ,
2016-11-07 19:02:27 -05:00
instanceType , bucketName , objectName , err )
}
// ExecObjectLayerAPIAnonTest - Calls the HTTP API handler using the anonymous request, validates the ErrAccessDeniedResponse,
// sets the bucket policy using the policy statement generated from `getWriteOnlyObjectStatement` so that the
// unsigned request goes through and its validated again.
2018-04-24 18:53:30 -04:00
ExecObjectLayerAPIAnonTest ( t , obj , "TestAPIHeadObjectHandler" , bucketName , objectName , instanceType , apiRouter , anonReq , getAnonReadOnlyObjectPolicy ( bucketName , objectName ) )
2016-11-07 19:02:27 -05:00
// HTTP request for testing when `objectLayer` is set to `nil`.
// There is no need to use an existing bucket and valid input for creating the request
// since the `objectLayer==nil` check is performed before any other checks inside the handlers.
// The only aim is to generate an HTTP request in a way that the relevant/registered end point is evoked/called.
nilBucket := "dummy-bucket"
nilObject := "dummy-object"
2020-07-20 15:52:49 -04:00
nilReq , err := newTestSignedRequestV4 ( http . MethodHead , getGetObjectURL ( "" , nilBucket , nilObject ) ,
2018-09-20 22:22:09 -04:00
0 , nil , "" , "" , nil )
2016-11-07 19:02:27 -05:00
if err != nil {
2019-04-09 14:39:42 -04:00
t . Errorf ( "MinIO %s: Failed to create HTTP request for testing the response when object Layer is set to `nil`." , instanceType )
2016-11-07 19:02:27 -05:00
}
// execute the object layer set to `nil` test.
// `ExecObjectLayerAPINilTest` manages the operation.
ExecObjectLayerAPINilTest ( t , nilBucket , nilObject , instanceType , apiRouter , nilReq )
}
2018-09-20 22:22:09 -04:00
func TestAPIHeadObjectHandlerWithEncryption ( t * testing . T ) {
globalPolicySys = NewPolicySys ( )
defer func ( ) { globalPolicySys = nil } ( )
defer DetectTestLeak ( t ) ( )
ExecObjectLayerAPITest ( t , testAPIHeadObjectHandlerWithEncryption , [ ] string { "NewMultipart" , "PutObjectPart" , "CompleteMultipart" , "GetObject" , "PutObject" , "HeadObject" } )
}
func testAPIHeadObjectHandlerWithEncryption ( obj ObjectLayer , instanceType , bucketName string , apiRouter http . Handler ,
2022-01-02 12:15:06 -05:00
credentials auth . Credentials , t * testing . T ,
) {
2018-09-20 22:22:09 -04:00
// Set SSL to on to do encryption tests
2020-12-22 00:42:38 -05:00
globalIsTLS = true
defer func ( ) { globalIsTLS = false } ( )
2018-09-20 22:22:09 -04:00
var (
oneMiB int64 = 1024 * 1024
key32Bytes = generateBytesData ( 32 * humanize . Byte )
key32BytesMd5 = md5 . Sum ( key32Bytes )
metaWithSSEC = map [ string ] string {
2020-12-22 12:19:32 -05:00
xhttp . AmzServerSideEncryptionCustomerAlgorithm : xhttp . AmzEncryptionAES ,
xhttp . AmzServerSideEncryptionCustomerKey : base64 . StdEncoding . EncodeToString ( key32Bytes ) ,
xhttp . AmzServerSideEncryptionCustomerKeyMD5 : base64 . StdEncoding . EncodeToString ( key32BytesMd5 [ : ] ) ,
2018-09-20 22:22:09 -04:00
}
mapCopy = func ( m map [ string ] string ) map [ string ] string {
r := make ( map [ string ] string , len ( m ) )
for k , v := range m {
r [ k ] = v
}
return r
}
)
type ObjectInput struct {
objectName string
partLengths [ ] int64
metaData map [ string ] string
}
objectLength := func ( oi ObjectInput ) ( sum int64 ) {
for _ , l := range oi . partLengths {
sum += l
}
return
}
// set of inputs for uploading the objects before tests for
// downloading is done. Data bytes are from DummyDataGen.
objectInputs := [ ] ObjectInput {
// Unencrypted objects
{ "nothing" , [ ] int64 { 0 } , nil } ,
{ "small-1" , [ ] int64 { 509 } , nil } ,
{ "mp-1" , [ ] int64 { 5 * oneMiB , 1 } , nil } ,
{ "mp-2" , [ ] int64 { 5487701 , 5487799 , 3 } , nil } ,
// Encrypted object
{ "enc-nothing" , [ ] int64 { 0 } , mapCopy ( metaWithSSEC ) } ,
{ "enc-small-1" , [ ] int64 { 509 } , mapCopy ( metaWithSSEC ) } ,
{ "enc-mp-1" , [ ] int64 { 5 * oneMiB , 1 } , mapCopy ( metaWithSSEC ) } ,
{ "enc-mp-2" , [ ] int64 { 5487701 , 5487799 , 3 } , mapCopy ( metaWithSSEC ) } ,
}
// iterate through the above set of inputs and upload the object.
for _ , input := range objectInputs {
uploadTestObject ( t , apiRouter , credentials , bucketName , input . objectName , input . partLengths , input . metaData , false )
}
for i , input := range objectInputs {
// initialize HTTP NewRecorder, this records any
// mutations to response writer inside the handler.
rec := httptest . NewRecorder ( )
// construct HTTP request for HEAD object.
2020-07-20 15:52:49 -04:00
req , err := newTestSignedRequestV4 ( http . MethodHead , getHeadObjectURL ( "" , bucketName , input . objectName ) ,
2018-09-20 22:22:09 -04:00
0 , nil , credentials . AccessKey , credentials . SecretKey , nil )
if err != nil {
t . Fatalf ( "Test %d: %s: Failed to create HTTP request for Head Object: <ERROR> %v" , i + 1 , instanceType , err )
}
// Since `apiRouter` satisfies `http.Handler` it has a
// ServeHTTP to execute the logic of the handler.
apiRouter . ServeHTTP ( rec , req )
isEnc := false
expected := 200
if strings . HasPrefix ( input . objectName , "enc-" ) {
isEnc = true
expected = 400
}
if rec . Code != expected {
t . Errorf ( "Test %d: expected code %d but got %d for object %s" , i + 1 , expected , rec . Code , input . objectName )
}
contentLength := rec . Header ( ) . Get ( "Content-Length" )
if isEnc {
// initialize HTTP NewRecorder, this records any
// mutations to response writer inside the handler.
rec := httptest . NewRecorder ( )
// construct HTTP request for HEAD object.
2020-07-20 15:52:49 -04:00
req , err := newTestSignedRequestV4 ( http . MethodHead , getHeadObjectURL ( "" , bucketName , input . objectName ) ,
2018-09-20 22:22:09 -04:00
0 , nil , credentials . AccessKey , credentials . SecretKey , input . metaData )
if err != nil {
t . Fatalf ( "Test %d: %s: Failed to create HTTP request for Head Object: <ERROR> %v" , i + 1 , instanceType , err )
}
// Since `apiRouter` satisfies `http.Handler` it has a
// ServeHTTP to execute the logic of the handler.
apiRouter . ServeHTTP ( rec , req )
if rec . Code != 200 {
t . Errorf ( "Test %d: Did not receive a 200 response: %d" , i + 1 , rec . Code )
}
contentLength = rec . Header ( ) . Get ( "Content-Length" )
}
if contentLength != fmt . Sprintf ( "%d" , objectLength ( input ) ) {
t . Errorf ( "Test %d: Content length is mismatching: got %s (expected: %d)" , i + 1 , contentLength , objectLength ( input ) )
}
}
}
2020-06-12 23:04:01 -04:00
// Wrapper for calling GetObject API handler tests for both Erasure multiple disks and FS single drive setup.
2016-10-04 03:09:21 -04:00
func TestAPIGetObjectHandler ( t * testing . T ) {
2018-09-20 22:22:09 -04:00
globalPolicySys = NewPolicySys ( )
defer func ( ) { globalPolicySys = nil } ( )
2016-11-07 00:53:50 -05:00
defer DetectTestLeak ( t ) ( )
2021-01-05 23:08:35 -05:00
ExecExtendedObjectLayerAPITest ( t , testAPIGetObjectHandler , [ ] string { "GetObject" } )
2016-08-16 22:24:23 -04:00
}
2016-10-04 03:09:21 -04:00
func testAPIGetObjectHandler ( obj ObjectLayer , instanceType , bucketName string , apiRouter http . Handler ,
2022-01-02 12:15:06 -05:00
credentials auth . Credentials , t * testing . T ,
) {
2016-08-16 22:24:23 -04:00
objectName := "test-object"
// set of byte data for PutObject.
2016-10-04 03:09:21 -04:00
// object has to be created before running tests for GetObject.
2016-08-16 22:24:23 -04:00
// this is required even to assert the GetObject data,
// since dataInserted === dataFetched back is a primary criteria for any object storage this assertion is critical.
bytesData := [ ] struct {
byteData [ ] byte
} {
2016-11-22 21:18:22 -05:00
{ generateBytesData ( 6 * humanize . MiByte ) } ,
2016-08-16 22:24:23 -04:00
}
// set of inputs for uploading the objects before tests for downloading is done.
putObjectInputs := [ ] struct {
bucketName string
objectName string
contentLength int64
textData [ ] byte
metaData map [ string ] string
} {
// case - 1.
{ bucketName , objectName , int64 ( len ( bytesData [ 0 ] . byteData ) ) , bytesData [ 0 ] . byteData , make ( map [ string ] string ) } ,
}
// iterate through the above set of inputs and upload the object.
for i , input := range putObjectInputs {
// uploading the object.
2020-12-27 01:58:06 -05:00
_ , err := obj . PutObject ( context . Background ( ) , input . bucketName , input . objectName , mustGetPutObjReader ( t , bytes . NewReader ( input . textData ) , input . contentLength , input . metaData [ "" ] , "" ) , ObjectOptions { UserDefined : input . metaData } )
2016-08-16 22:24:23 -04:00
// if object upload fails stop the test.
if err != nil {
t . Fatalf ( "Put Object case %d: Error uploading object: <ERROR> %v" , i + 1 , err )
}
}
2019-02-13 19:07:21 -05:00
ctx := context . Background ( )
2016-08-16 22:24:23 -04:00
// test cases with inputs and expected result for GetObject.
testCases := [ ] struct {
bucketName string
objectName string
byteRange string // range of bytes to be fetched from GetObject.
2016-10-14 23:52:46 -04:00
accessKey string
secretKey string
2016-08-16 22:24:23 -04:00
// expected output.
expectedContent [ ] byte // expected response body.
expectedRespStatus int // expected response status body.
} {
// Test case - 1.
// Fetching the entire object and validating its contents.
{
2016-10-14 23:52:46 -04:00
bucketName : bucketName ,
objectName : objectName ,
byteRange : "" ,
2016-12-26 13:21:23 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
2016-10-14 23:52:46 -04:00
2016-08-16 22:24:23 -04:00
expectedContent : bytesData [ 0 ] . byteData ,
expectedRespStatus : http . StatusOK ,
} ,
// Test case - 2.
// Case with non-existent object name.
{
2016-10-14 23:52:46 -04:00
bucketName : bucketName ,
objectName : "abcd" ,
byteRange : "" ,
2016-12-26 13:21:23 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
2016-10-14 23:52:46 -04:00
2019-02-13 19:07:21 -05:00
expectedContent : encodeResponse ( getAPIErrorResponse ( ctx ,
getAPIError ( ErrNoSuchKey ) ,
getGetObjectURL ( "" , bucketName , "abcd" ) , "" , "" ) ) ,
2016-08-16 22:24:23 -04:00
expectedRespStatus : http . StatusNotFound ,
} ,
// Test case - 3.
// Requesting from range 10-100.
{
2016-10-14 23:52:46 -04:00
bucketName : bucketName ,
objectName : objectName ,
byteRange : "bytes=10-100" ,
2016-12-26 13:21:23 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
2016-10-14 23:52:46 -04:00
2016-08-16 22:24:23 -04:00
expectedContent : bytesData [ 0 ] . byteData [ 10 : 101 ] ,
expectedRespStatus : http . StatusPartialContent ,
} ,
// Test case - 4.
// Test case with invalid range.
{
2016-10-14 23:52:46 -04:00
bucketName : bucketName ,
objectName : objectName ,
byteRange : "bytes=-0" ,
2016-12-26 13:21:23 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
2016-10-14 23:52:46 -04:00
2019-02-13 19:07:21 -05:00
expectedContent : encodeResponse ( getAPIErrorResponse ( ctx ,
getAPIError ( ErrInvalidRange ) ,
getGetObjectURL ( "" , bucketName , objectName ) , "" , "" ) ) ,
2016-08-16 22:24:23 -04:00
expectedRespStatus : http . StatusRequestedRangeNotSatisfiable ,
} ,
// Test case - 5.
// Test case with byte range exceeding the object size.
// Expected to read till end of the object.
{
2016-10-14 23:52:46 -04:00
bucketName : bucketName ,
objectName : objectName ,
byteRange : "bytes=10-1000000000000000" ,
2016-12-26 13:21:23 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
2016-10-14 23:52:46 -04:00
2016-08-16 22:24:23 -04:00
expectedContent : bytesData [ 0 ] . byteData [ 10 : ] ,
expectedRespStatus : http . StatusPartialContent ,
} ,
2016-10-14 23:52:46 -04:00
// Test case - 6.
// Test case to induce a signature mismatch.
// Using invalid accessID.
{
bucketName : bucketName ,
objectName : objectName ,
byteRange : "" ,
accessKey : "Invalid-AccessID" ,
2016-12-26 13:21:23 -05:00
secretKey : credentials . SecretKey ,
2016-10-14 23:52:46 -04:00
2019-02-13 19:07:21 -05:00
expectedContent : encodeResponse ( getAPIErrorResponse ( ctx ,
getAPIError ( ErrInvalidAccessKeyID ) ,
getGetObjectURL ( "" , bucketName , objectName ) , "" , "" ) ) ,
2016-10-14 23:52:46 -04:00
expectedRespStatus : http . StatusForbidden ,
} ,
2017-04-24 21:13:46 -04:00
// Test case - 7.
// Case with bad components in object name.
{
bucketName : bucketName ,
objectName : "../../etc" ,
byteRange : "" ,
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
2019-02-13 19:07:21 -05:00
expectedContent : encodeResponse ( getAPIErrorResponse ( ctx ,
getAPIError ( ErrInvalidObjectName ) ,
getGetObjectURL ( "" , bucketName , "../../etc" ) , "" , "" ) ) ,
2017-04-24 21:13:46 -04:00
expectedRespStatus : http . StatusBadRequest ,
} ,
// Test case - 8.
// Case with strange components but returning error as not found.
{
bucketName : bucketName ,
objectName : ". ./. ./etc" ,
byteRange : "" ,
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
2019-02-13 19:07:21 -05:00
expectedContent : encodeResponse ( getAPIErrorResponse ( ctx ,
getAPIError ( ErrNoSuchKey ) ,
2019-08-06 15:08:58 -04:00
SlashSeparator + bucketName + SlashSeparator + ". ./. ./etc" , "" , "" ) ) ,
2017-04-24 21:13:46 -04:00
expectedRespStatus : http . StatusNotFound ,
} ,
// Test case - 9.
// Case with bad components in object name.
{
bucketName : bucketName ,
objectName : ". ./../etc" ,
byteRange : "" ,
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
2019-02-13 19:07:21 -05:00
expectedContent : encodeResponse ( getAPIErrorResponse ( ctx ,
getAPIError ( ErrInvalidObjectName ) ,
2019-08-06 15:08:58 -04:00
SlashSeparator + bucketName + SlashSeparator + ". ./../etc" , "" , "" ) ) ,
2017-04-24 21:13:46 -04:00
expectedRespStatus : http . StatusBadRequest ,
} ,
// Test case - 10.
// Case with proper components
{
bucketName : bucketName ,
objectName : "etc/path/proper/.../etc" ,
byteRange : "" ,
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
2019-02-13 19:07:21 -05:00
expectedContent : encodeResponse ( getAPIErrorResponse ( ctx ,
getAPIError ( ErrNoSuchKey ) ,
getGetObjectURL ( "" , bucketName , "etc/path/proper/.../etc" ) ,
"" , "" ) ) ,
2017-04-24 21:13:46 -04:00
expectedRespStatus : http . StatusNotFound ,
} ,
2016-08-16 22:24:23 -04:00
}
2016-10-14 23:52:46 -04:00
2016-08-16 22:24:23 -04:00
// Iterating over the cases, fetching the object validating the response.
for i , testCase := range testCases {
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
rec := httptest . NewRecorder ( )
// construct HTTP request for Get Object end point.
2020-07-20 15:52:49 -04:00
req , err := newTestSignedRequestV4 ( http . MethodGet , getGetObjectURL ( "" , testCase . bucketName , testCase . objectName ) ,
2018-09-20 22:22:09 -04:00
0 , nil , testCase . accessKey , testCase . secretKey , nil )
2016-08-16 22:24:23 -04:00
if err != nil {
t . Fatalf ( "Test %d: Failed to create HTTP request for Get Object: <ERROR> %v" , i + 1 , err )
}
if testCase . byteRange != "" {
2020-09-01 19:58:13 -04:00
req . Header . Set ( "Range" , testCase . byteRange )
2016-08-16 22:24:23 -04:00
}
// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
// Call the ServeHTTP to execute the handler,`func (api objectAPIHandlers) GetObjectHandler` handles the request.
apiRouter . ServeHTTP ( rec , req )
// Assert the response code with the expected status.
if rec . Code != testCase . expectedRespStatus {
t . Fatalf ( "Case %d: Expected the response status to be `%d`, but instead found `%d`" , i + 1 , testCase . expectedRespStatus , rec . Code )
}
// read the response body.
2022-09-19 14:05:16 -04:00
actualContent , err := io . ReadAll ( rec . Body )
2016-08-16 22:24:23 -04:00
if err != nil {
2019-02-13 19:07:21 -05:00
t . Fatalf ( "Test %d: %s: Failed reading response body: <ERROR> %v" , i + 1 , instanceType , err )
2016-08-16 22:24:23 -04:00
}
2019-02-13 19:07:21 -05:00
if rec . Code == http . StatusOK || rec . Code == http . StatusPartialContent {
if ! bytes . Equal ( testCase . expectedContent , actualContent ) {
t . Errorf ( "Test %d: %s: Object content differs from expected value %s, got %s" , i + 1 , instanceType , testCase . expectedContent , string ( actualContent ) )
}
continue
}
2016-10-04 03:09:21 -04:00
// Verify whether the bucket obtained object is same as the one created.
2019-02-13 19:07:21 -05:00
actualError := & APIErrorResponse { }
if err = xml . Unmarshal ( actualContent , actualError ) ; err != nil {
t . Fatalf ( "Test %d: %s: Failed parsing response body: <ERROR> %v" , i + 1 , instanceType , err )
}
2021-08-08 01:43:01 -04:00
if path . Clean ( actualError . Resource ) != pathJoin ( SlashSeparator , testCase . bucketName , testCase . objectName ) {
t . Fatalf ( "Test %d: %s: Unexpected resource, expected %s, got %s" , i + 1 , instanceType , pathJoin ( SlashSeparator , testCase . bucketName , testCase . objectName ) , actualError . Resource )
2016-08-16 22:24:23 -04:00
}
2016-10-14 23:52:46 -04:00
// Verify response of the V2 signed HTTP request.
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
recV2 := httptest . NewRecorder ( )
// construct HTTP request for GET Object endpoint.
2020-07-20 15:52:49 -04:00
reqV2 , err := newTestSignedRequestV2 ( http . MethodGet , getGetObjectURL ( "" , testCase . bucketName , testCase . objectName ) ,
2018-09-20 22:22:09 -04:00
0 , nil , testCase . accessKey , testCase . secretKey , nil )
2016-10-14 23:52:46 -04:00
if err != nil {
t . Fatalf ( "Test %d: %s: Failed to create HTTP request for GetObject: <ERROR> %v" , i + 1 , instanceType , err )
}
if testCase . byteRange != "" {
2020-09-01 19:58:13 -04:00
reqV2 . Header . Set ( "Range" , testCase . byteRange )
2016-10-14 23:52:46 -04:00
}
// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
// Call the ServeHTTP to execute the handler.
apiRouter . ServeHTTP ( recV2 , reqV2 )
if recV2 . Code != testCase . expectedRespStatus {
t . Errorf ( "Test %d: %s: Expected the response status to be `%d`, but instead found `%d`" , i + 1 , instanceType , testCase . expectedRespStatus , recV2 . Code )
}
// read the response body.
2022-09-19 14:05:16 -04:00
actualContent , err = io . ReadAll ( recV2 . Body )
2016-10-14 23:52:46 -04:00
if err != nil {
2019-02-13 19:07:21 -05:00
t . Fatalf ( "Test %d: %s: Failed to read response body: <ERROR> %v" , i + 1 , instanceType , err )
}
if rec . Code == http . StatusOK || rec . Code == http . StatusPartialContent {
// Verify whether the bucket obtained object is same as the one created.
if ! bytes . Equal ( testCase . expectedContent , actualContent ) {
t . Errorf ( "Test %d: %s: Object content differs from expected value." , i + 1 , instanceType )
}
continue
}
actualError = & APIErrorResponse { }
if err = xml . Unmarshal ( actualContent , actualError ) ; err != nil {
2016-10-14 23:52:46 -04:00
t . Fatalf ( "Test %d: %s: Failed parsing response body: <ERROR> %v" , i + 1 , instanceType , err )
}
2019-02-13 19:07:21 -05:00
2021-08-08 01:43:01 -04:00
if path . Clean ( actualError . Resource ) != pathJoin ( SlashSeparator , testCase . bucketName , testCase . objectName ) {
t . Fatalf ( "Test %d: %s: Unexpected resource, expected %s, got %s" , i + 1 , instanceType , pathJoin ( SlashSeparator , testCase . bucketName , testCase . objectName ) , actualError . Resource )
2016-10-14 23:52:46 -04:00
}
2016-08-16 22:24:23 -04:00
}
2016-10-06 16:34:33 -04:00
2016-10-08 02:28:50 -04:00
// Test for Anonymous/unsigned http request.
2020-07-20 15:52:49 -04:00
anonReq , err := newTestRequest ( http . MethodGet , getGetObjectURL ( "" , bucketName , objectName ) , 0 , nil )
2016-10-08 02:28:50 -04:00
if err != nil {
2019-04-09 14:39:42 -04:00
t . Fatalf ( "MinIO %s: Failed to create an anonymous request for %s/%s: <ERROR> %v" ,
2016-10-08 02:28:50 -04:00
instanceType , bucketName , objectName , err )
}
// ExecObjectLayerAPIAnonTest - Calls the HTTP API handler using the anonymous request, validates the ErrAccessDeniedResponse,
// sets the bucket policy using the policy statement generated from `getWriteOnlyObjectStatement` so that the
// unsigned request goes through and its validated again.
2018-04-24 18:53:30 -04:00
ExecObjectLayerAPIAnonTest ( t , obj , "TestAPIGetObjectHandler" , bucketName , objectName , instanceType , apiRouter , anonReq , getAnonReadOnlyObjectPolicy ( bucketName , objectName ) )
2016-10-08 02:28:50 -04:00
2016-10-06 16:34:33 -04:00
// HTTP request for testing when `objectLayer` is set to `nil`.
// There is no need to use an existing bucket and valid input for creating the request
// since the `objectLayer==nil` check is performed before any other checks inside the handlers.
// The only aim is to generate an HTTP request in a way that the relevant/registered end point is evoked/called.
nilBucket := "dummy-bucket"
nilObject := "dummy-object"
2020-07-20 15:52:49 -04:00
nilReq , err := newTestSignedRequestV4 ( http . MethodGet , getGetObjectURL ( "" , nilBucket , nilObject ) ,
2018-09-20 22:22:09 -04:00
0 , nil , "" , "" , nil )
2016-10-06 16:34:33 -04:00
if err != nil {
2019-04-09 14:39:42 -04:00
t . Errorf ( "MinIO %s: Failed to create HTTP request for testing the response when object Layer is set to `nil`." , instanceType )
2016-10-06 16:34:33 -04:00
}
// execute the object layer set to `nil` test.
// `ExecObjectLayerAPINilTest` manages the operation.
ExecObjectLayerAPINilTest ( t , nilBucket , nilObject , instanceType , apiRouter , nilReq )
2016-08-16 22:24:23 -04:00
}
2020-06-12 23:04:01 -04:00
// Wrapper for calling GetObject API handler tests for both Erasure multiple disks and FS single drive setup.
2018-09-20 22:22:09 -04:00
func TestAPIGetObjectWithMPHandler ( t * testing . T ) {
globalPolicySys = NewPolicySys ( )
defer func ( ) { globalPolicySys = nil } ( )
defer DetectTestLeak ( t ) ( )
2021-01-05 23:08:35 -05:00
ExecExtendedObjectLayerAPITest ( t , testAPIGetObjectWithMPHandler , [ ] string { "NewMultipart" , "PutObjectPart" , "CompleteMultipart" , "GetObject" , "PutObject" } )
2018-09-20 22:22:09 -04:00
}
func testAPIGetObjectWithMPHandler ( obj ObjectLayer , instanceType , bucketName string , apiRouter http . Handler ,
2022-01-02 12:15:06 -05:00
credentials auth . Credentials , t * testing . T ,
) {
2018-09-20 22:22:09 -04:00
// Set SSL to on to do encryption tests
2020-12-22 00:42:38 -05:00
globalIsTLS = true
defer func ( ) { globalIsTLS = false } ( )
2018-09-20 22:22:09 -04:00
var (
oneMiB int64 = 1024 * 1024
key32Bytes = generateBytesData ( 32 * humanize . Byte )
key32BytesMd5 = md5 . Sum ( key32Bytes )
metaWithSSEC = map [ string ] string {
2020-12-22 12:19:32 -05:00
xhttp . AmzServerSideEncryptionCustomerAlgorithm : xhttp . AmzEncryptionAES ,
xhttp . AmzServerSideEncryptionCustomerKey : base64 . StdEncoding . EncodeToString ( key32Bytes ) ,
xhttp . AmzServerSideEncryptionCustomerKeyMD5 : base64 . StdEncoding . EncodeToString ( key32BytesMd5 [ : ] ) ,
2018-09-20 22:22:09 -04:00
}
mapCopy = func ( m map [ string ] string ) map [ string ] string {
r := make ( map [ string ] string , len ( m ) )
for k , v := range m {
r [ k ] = v
}
return r
}
)
type ObjectInput struct {
objectName string
partLengths [ ] int64
metaData map [ string ] string
}
objectLength := func ( oi ObjectInput ) ( sum int64 ) {
for _ , l := range oi . partLengths {
sum += l
}
return
}
// set of inputs for uploading the objects before tests for
// downloading is done. Data bytes are from DummyDataGen.
objectInputs := [ ] ObjectInput {
// // cases 0-3: small single part objects
{ "nothing" , [ ] int64 { 0 } , make ( map [ string ] string ) } ,
{ "small-0" , [ ] int64 { 11 } , make ( map [ string ] string ) } ,
{ "small-1" , [ ] int64 { 509 } , make ( map [ string ] string ) } ,
{ "small-2" , [ ] int64 { 5 * oneMiB } , make ( map [ string ] string ) } ,
// // // cases 4-7: multipart part objects
{ "mp-0" , [ ] int64 { 5 * oneMiB , 1 } , make ( map [ string ] string ) } ,
{ "mp-1" , [ ] int64 { 5 * oneMiB + 1 , 1 } , make ( map [ string ] string ) } ,
{ "mp-2" , [ ] int64 { 5487701 , 5487799 , 3 } , make ( map [ string ] string ) } ,
{ "mp-3" , [ ] int64 { 10499807 , 10499963 , 7 } , make ( map [ string ] string ) } ,
// cases 8-11: small single part objects with encryption
{ "enc-nothing" , [ ] int64 { 0 } , mapCopy ( metaWithSSEC ) } ,
{ "enc-small-0" , [ ] int64 { 11 } , mapCopy ( metaWithSSEC ) } ,
{ "enc-small-1" , [ ] int64 { 509 } , mapCopy ( metaWithSSEC ) } ,
{ "enc-small-2" , [ ] int64 { 5 * oneMiB } , mapCopy ( metaWithSSEC ) } ,
// cases 12-15: multipart part objects with encryption
{ "enc-mp-0" , [ ] int64 { 5 * oneMiB , 1 } , mapCopy ( metaWithSSEC ) } ,
{ "enc-mp-1" , [ ] int64 { 5 * oneMiB + 1 , 1 } , mapCopy ( metaWithSSEC ) } ,
{ "enc-mp-2" , [ ] int64 { 5487701 , 5487799 , 3 } , mapCopy ( metaWithSSEC ) } ,
{ "enc-mp-3" , [ ] int64 { 10499807 , 10499963 , 7 } , mapCopy ( metaWithSSEC ) } ,
}
// iterate through the above set of inputs and upload the object.
for _ , input := range objectInputs {
uploadTestObject ( t , apiRouter , credentials , bucketName , input . objectName , input . partLengths , input . metaData , false )
}
// function type for creating signed requests - used to repeat
// requests with V2 and V4 signing.
type testSignedReqFn func ( method , urlStr string , contentLength int64 ,
body io . ReadSeeker , accessKey , secretKey string , metamap map [ string ] string ) ( * http . Request ,
error )
mkGetReq := func ( oi ObjectInput , byteRange string , i int , mkSignedReq testSignedReqFn ) {
object := oi . objectName
rec := httptest . NewRecorder ( )
2020-07-20 15:52:49 -04:00
req , err := mkSignedReq ( http . MethodGet , getGetObjectURL ( "" , bucketName , object ) ,
2018-09-20 22:22:09 -04:00
0 , nil , credentials . AccessKey , credentials . SecretKey , oi . metaData )
if err != nil {
t . Fatalf ( "Object: %s Case %d ByteRange: %s: Failed to create HTTP request for Get Object: <ERROR> %v" ,
object , i + 1 , byteRange , err )
}
if byteRange != "" {
2020-09-01 19:58:13 -04:00
req . Header . Set ( "Range" , byteRange )
2018-09-20 22:22:09 -04:00
}
apiRouter . ServeHTTP ( rec , req )
// Check response code (we make only valid requests in
// this test)
if rec . Code != http . StatusPartialContent && rec . Code != http . StatusOK {
2022-09-19 14:05:16 -04:00
bd , err1 := io . ReadAll ( rec . Body )
2018-09-20 22:22:09 -04:00
t . Fatalf ( "%s Object: %s Case %d ByteRange: %s: Got response status `%d` and body: %s,%v" ,
instanceType , object , i + 1 , byteRange , rec . Code , string ( bd ) , err1 )
}
var off , length int64
var rs * HTTPRangeSpec
if byteRange != "" {
rs , err = parseRequestRangeSpec ( byteRange )
if err != nil {
t . Fatalf ( "Object: %s Case %d ByteRange: %s: Unexpected err: %v" , object , i + 1 , byteRange , err )
}
}
off , length , err = rs . GetOffsetLength ( objectLength ( oi ) )
if err != nil {
t . Fatalf ( "Object: %s Case %d ByteRange: %s: Unexpected err: %v" , object , i + 1 , byteRange , err )
}
readers := [ ] io . Reader { }
cumulativeSum := int64 ( 0 )
for _ , p := range oi . partLengths {
readers = append ( readers , NewDummyDataGen ( p , cumulativeSum ) )
cumulativeSum += p
}
refReader := io . LimitReader ( ioutilx . NewSkipReader ( io . MultiReader ( readers ... ) , off ) , length )
if ok , msg := cmpReaders ( refReader , rec . Body ) ; ! ok {
t . Fatalf ( "(%s) Object: %s Case %d ByteRange: %s --> data mismatch! (msg: %s)" , instanceType , oi . objectName , i + 1 , byteRange , msg )
}
}
// Iterate over each uploaded object and do a bunch of get
// requests on them.
caseNumber := 0
signFns := [ ] testSignedReqFn { newTestSignedRequestV2 , newTestSignedRequestV4 }
for _ , oi := range objectInputs {
objLen := objectLength ( oi )
for _ , sf := range signFns {
// Read whole object
mkGetReq ( oi , "" , caseNumber , sf )
caseNumber ++
// No range requests are possible if the
// object length is 0
if objLen == 0 {
continue
}
// Various ranges to query - all are valid!
rangeHdrs := [ ] string {
// Read first byte of object
fmt . Sprintf ( "bytes=%d-%d" , 0 , 0 ) ,
// Read second byte of object
fmt . Sprintf ( "bytes=%d-%d" , 1 , 1 ) ,
// Read last byte of object
fmt . Sprintf ( "bytes=-%d" , 1 ) ,
// Read all but first byte of object
"bytes=1-" ,
// Read first half of object
fmt . Sprintf ( "bytes=%d-%d" , 0 , objLen / 2 ) ,
// Read last half of object
fmt . Sprintf ( "bytes=-%d" , objLen / 2 ) ,
// Read middle half of object
fmt . Sprintf ( "bytes=%d-%d" , objLen / 4 , objLen * 3 / 4 ) ,
// Read 100MiB of the object from the beginning
fmt . Sprintf ( "bytes=%d-%d" , 0 , 100 * humanize . MiByte ) ,
// Read 100MiB of the object from the end
fmt . Sprintf ( "bytes=-%d" , 100 * humanize . MiByte ) ,
}
for _ , rangeHdr := range rangeHdrs {
mkGetReq ( oi , rangeHdr , caseNumber , sf )
caseNumber ++
}
}
}
// HTTP request for testing when `objectLayer` is set to `nil`.
// There is no need to use an existing bucket and valid input for creating the request
// since the `objectLayer==nil` check is performed before any other checks inside the handlers.
// The only aim is to generate an HTTP request in a way that the relevant/registered end point is evoked/called.
nilBucket := "dummy-bucket"
nilObject := "dummy-object"
2020-07-20 15:52:49 -04:00
nilReq , err := newTestSignedRequestV4 ( http . MethodGet , getGetObjectURL ( "" , nilBucket , nilObject ) ,
2018-09-20 22:22:09 -04:00
0 , nil , "" , "" , nil )
if err != nil {
2019-04-09 14:39:42 -04:00
t . Errorf ( "MinIO %s: Failed to create HTTP request for testing the response when object Layer is set to `nil`." , instanceType )
2018-09-20 22:22:09 -04:00
}
// execute the object layer set to `nil` test.
// `ExecObjectLayerAPINilTest` manages the operation.
ExecObjectLayerAPINilTest ( t , nilBucket , nilObject , instanceType , apiRouter , nilReq )
}
2020-12-08 16:12:42 -05:00
// Wrapper for calling GetObject API handler tests for both Erasure multiple disks and FS single drive setup.
func TestAPIGetObjectWithPartNumberHandler ( t * testing . T ) {
globalPolicySys = NewPolicySys ( )
defer func ( ) { globalPolicySys = nil } ( )
defer DetectTestLeak ( t ) ( )
2021-01-05 23:08:35 -05:00
ExecExtendedObjectLayerAPITest ( t , testAPIGetObjectWithPartNumberHandler , [ ] string { "NewMultipart" , "PutObjectPart" , "CompleteMultipart" , "GetObject" , "PutObject" } )
2020-12-08 16:12:42 -05:00
}
func testAPIGetObjectWithPartNumberHandler ( obj ObjectLayer , instanceType , bucketName string , apiRouter http . Handler ,
2022-01-02 12:15:06 -05:00
credentials auth . Credentials , t * testing . T ,
) {
2020-12-08 16:12:42 -05:00
// Set SSL to on to do encryption tests
2020-12-22 00:42:38 -05:00
globalIsTLS = true
defer func ( ) { globalIsTLS = false } ( )
2020-12-08 16:12:42 -05:00
var (
oneMiB int64 = 1024 * 1024
key32Bytes = generateBytesData ( 32 * humanize . Byte )
key32BytesMd5 = md5 . Sum ( key32Bytes )
metaWithSSEC = map [ string ] string {
2020-12-22 12:19:32 -05:00
xhttp . AmzServerSideEncryptionCustomerAlgorithm : xhttp . AmzEncryptionAES ,
xhttp . AmzServerSideEncryptionCustomerKey : base64 . StdEncoding . EncodeToString ( key32Bytes ) ,
xhttp . AmzServerSideEncryptionCustomerKeyMD5 : base64 . StdEncoding . EncodeToString ( key32BytesMd5 [ : ] ) ,
2020-12-08 16:12:42 -05:00
}
mapCopy = func ( m map [ string ] string ) map [ string ] string {
r := make ( map [ string ] string , len ( m ) )
for k , v := range m {
r [ k ] = v
}
return r
}
)
type ObjectInput struct {
objectName string
partLengths [ ] int64
metaData map [ string ] string
}
// set of inputs for uploading the objects before tests for
// downloading is done. Data bytes are from DummyDataGen.
objectInputs := [ ] ObjectInput {
// // cases 0-4: small single part objects
{ "nothing" , [ ] int64 { 0 } , make ( map [ string ] string ) } ,
{ "1byte" , [ ] int64 { 1 } , make ( map [ string ] string ) } ,
{ "small-0" , [ ] int64 { 11 } , make ( map [ string ] string ) } ,
{ "small-1" , [ ] int64 { 509 } , make ( map [ string ] string ) } ,
{ "small-2" , [ ] int64 { 5 * oneMiB } , make ( map [ string ] string ) } ,
// // // cases 5-8: multipart part objects
{ "mp-0" , [ ] int64 { 5 * oneMiB , 1 } , make ( map [ string ] string ) } ,
{ "mp-1" , [ ] int64 { 5 * oneMiB + 1 , 1 } , make ( map [ string ] string ) } ,
{ "mp-2" , [ ] int64 { 5487701 , 5487799 , 3 } , make ( map [ string ] string ) } ,
{ "mp-3" , [ ] int64 { 10499807 , 10499963 , 7 } , make ( map [ string ] string ) } ,
// cases 9-12: small single part objects with encryption
{ "enc-nothing" , [ ] int64 { 0 } , mapCopy ( metaWithSSEC ) } ,
{ "enc-small-0" , [ ] int64 { 11 } , mapCopy ( metaWithSSEC ) } ,
{ "enc-small-1" , [ ] int64 { 509 } , mapCopy ( metaWithSSEC ) } ,
{ "enc-small-2" , [ ] int64 { 5 * oneMiB } , mapCopy ( metaWithSSEC ) } ,
// cases 13-16: multipart part objects with encryption
{ "enc-mp-0" , [ ] int64 { 5 * oneMiB , 1 } , mapCopy ( metaWithSSEC ) } ,
{ "enc-mp-1" , [ ] int64 { 5 * oneMiB + 1 , 1 } , mapCopy ( metaWithSSEC ) } ,
{ "enc-mp-2" , [ ] int64 { 5487701 , 5487799 , 3 } , mapCopy ( metaWithSSEC ) } ,
{ "enc-mp-3" , [ ] int64 { 10499807 , 10499963 , 7 } , mapCopy ( metaWithSSEC ) } ,
}
// iterate through the above set of inputs and upload the object.
for _ , input := range objectInputs {
uploadTestObject ( t , apiRouter , credentials , bucketName , input . objectName , input . partLengths , input . metaData , false )
}
mkGetReqWithPartNumber := func ( oindex int , oi ObjectInput , partNumber int ) {
object := oi . objectName
queries := url . Values { }
queries . Add ( "partNumber" , strconv . Itoa ( partNumber ) )
targetURL := makeTestTargetURL ( "" , bucketName , object , queries )
req , err := newTestSignedRequestV4 ( http . MethodGet , targetURL ,
0 , nil , credentials . AccessKey , credentials . SecretKey , oi . metaData )
if err != nil {
t . Fatalf ( "Object: %s Object Index %d PartNumber: %d: Failed to create HTTP request for Get Object: <ERROR> %v" ,
object , oindex , partNumber , err )
}
2021-08-08 01:43:01 -04:00
rec := httptest . NewRecorder ( )
2020-12-08 16:12:42 -05:00
apiRouter . ServeHTTP ( rec , req )
// Check response code (we make only valid requests in this test)
if rec . Code != http . StatusPartialContent && rec . Code != http . StatusOK {
2022-09-19 14:05:16 -04:00
bd , err1 := io . ReadAll ( rec . Body )
2020-12-08 16:12:42 -05:00
t . Fatalf ( "%s Object: %s ObjectIndex %d PartNumber: %d: Got response status `%d` and body: %s,%v" ,
instanceType , object , oindex , partNumber , rec . Code , string ( bd ) , err1 )
}
oinfo , err := obj . GetObjectInfo ( context . Background ( ) , bucketName , object , ObjectOptions { } )
if err != nil {
t . Fatalf ( "Object: %s Object Index %d: Unexpected err: %v" , object , oindex , err )
}
rs := partNumberToRangeSpec ( oinfo , partNumber )
2021-01-22 15:09:24 -05:00
size , err := oinfo . GetActualSize ( )
if err != nil {
t . Fatalf ( "Object: %s Object Index %d: Unexpected err: %v" , object , oindex , err )
}
off , length , err := rs . GetOffsetLength ( size )
2020-12-08 16:12:42 -05:00
if err != nil {
t . Fatalf ( "Object: %s Object Index %d: Unexpected err: %v" , object , oindex , err )
}
readers := [ ] io . Reader { }
cumulativeSum := int64 ( 0 )
for _ , p := range oi . partLengths {
readers = append ( readers , NewDummyDataGen ( p , cumulativeSum ) )
cumulativeSum += p
}
2021-01-22 15:09:24 -05:00
2020-12-08 16:12:42 -05:00
refReader := io . LimitReader ( ioutilx . NewSkipReader ( io . MultiReader ( readers ... ) , off ) , length )
if ok , msg := cmpReaders ( refReader , rec . Body ) ; ! ok {
t . Fatalf ( "(%s) Object: %s ObjectIndex %d PartNumber: %d --> data mismatch! (msg: %s)" , instanceType , oi . objectName , oindex , partNumber , msg )
}
}
for idx , oi := range objectInputs {
for partNum := 1 ; partNum <= len ( oi . partLengths ) ; partNum ++ {
mkGetReqWithPartNumber ( idx , oi , partNum )
}
}
}
2020-06-12 23:04:01 -04:00
// Wrapper for calling PutObject API handler tests using streaming signature v4 for both Erasure multiple disks and FS single drive setup.
2016-09-04 16:37:14 -04:00
func TestAPIPutObjectStreamSigV4Handler ( t * testing . T ) {
2016-11-07 00:53:50 -05:00
defer DetectTestLeak ( t ) ( )
2021-01-05 23:08:35 -05:00
ExecExtendedObjectLayerAPITest ( t , testAPIPutObjectStreamSigV4Handler , [ ] string { "PutObject" } )
2016-09-04 16:37:14 -04:00
}
2016-09-10 17:47:27 -04:00
func testAPIPutObjectStreamSigV4Handler ( obj ObjectLayer , instanceType , bucketName string , apiRouter http . Handler ,
2022-01-02 12:15:06 -05:00
credentials auth . Credentials , t * testing . T ,
) {
2016-09-10 17:47:27 -04:00
objectName := "test-object"
2016-11-22 21:18:22 -05:00
bytesDataLen := 65 * humanize . KiByte
2016-09-04 16:37:14 -04:00
bytesData := bytes . Repeat ( [ ] byte { 'a' } , bytesDataLen )
2016-11-22 21:18:22 -05:00
oneKData := bytes . Repeat ( [ ] byte ( "a" ) , 1 * humanize . KiByte )
2016-10-10 04:42:32 -04:00
2018-03-15 16:03:41 -04:00
var err error
2016-10-10 04:42:32 -04:00
type streamFault int
const (
None streamFault = iota
malformedEncoding
unexpectedEOF
signatureMismatch
2016-10-12 02:46:51 -04:00
chunkDateMismatch
2016-10-26 05:06:22 -04:00
tooBigDecodedLength
2016-10-10 04:42:32 -04:00
)
2016-09-04 16:37:14 -04:00
// byte data for PutObject.
// test cases with inputs and expected result for GetObject.
testCases := [ ] struct {
bucketName string
objectName string
data [ ] byte
dataLen int
2016-09-16 05:45:42 -04:00
chunkSize int64
2016-09-04 16:37:14 -04:00
// expected output.
expectedContent [ ] byte // expected response body.
expectedRespStatus int // expected response status body.
2016-09-16 05:45:42 -04:00
// Access keys
accessKey string
secretKey string
shouldPass bool
removeAuthHeader bool
2016-10-10 04:42:32 -04:00
fault streamFault
2017-03-27 20:02:04 -04:00
// Custom content encoding.
contentEncoding string
2016-09-04 16:37:14 -04:00
} {
// Test case - 1.
// Fetching the entire object and validating its contents.
{
bucketName : bucketName ,
objectName : objectName ,
data : bytesData ,
dataLen : len ( bytesData ) ,
2016-11-22 21:18:22 -05:00
chunkSize : 64 * humanize . KiByte ,
2016-09-04 16:37:14 -04:00
expectedContent : [ ] byte { } ,
expectedRespStatus : http . StatusOK ,
2016-12-26 13:21:23 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
2016-09-16 05:45:42 -04:00
shouldPass : true ,
2019-02-13 07:59:36 -05:00
fault : None ,
2016-09-16 05:45:42 -04:00
} ,
// Test case - 2
// Small chunk size.
{
bucketName : bucketName ,
objectName : objectName ,
data : bytesData ,
dataLen : len ( bytesData ) ,
2016-11-22 21:18:22 -05:00
chunkSize : 1 * humanize . KiByte ,
2016-09-16 05:45:42 -04:00
expectedContent : [ ] byte { } ,
expectedRespStatus : http . StatusOK ,
2016-12-26 13:21:23 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
2016-09-16 05:45:42 -04:00
shouldPass : true ,
2019-02-13 07:59:36 -05:00
fault : None ,
2016-09-16 05:45:42 -04:00
} ,
// Test case - 3
2017-02-06 17:19:27 -05:00
// Empty data
{
bucketName : bucketName ,
objectName : objectName ,
data : [ ] byte { } ,
dataLen : 0 ,
chunkSize : 64 * humanize . KiByte ,
expectedContent : [ ] byte { } ,
expectedRespStatus : http . StatusOK ,
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
shouldPass : true ,
} ,
// Test case - 4
2016-09-16 05:45:42 -04:00
// Invalid access key id.
{
bucketName : bucketName ,
objectName : objectName ,
data : bytesData ,
dataLen : len ( bytesData ) ,
2016-11-22 21:18:22 -05:00
chunkSize : 64 * humanize . KiByte ,
2016-09-16 05:45:42 -04:00
expectedContent : [ ] byte { } ,
expectedRespStatus : http . StatusForbidden ,
accessKey : "" ,
secretKey : "" ,
shouldPass : false ,
2019-02-13 07:59:36 -05:00
fault : None ,
2016-09-16 05:45:42 -04:00
} ,
2017-02-06 17:19:27 -05:00
// Test case - 5
2016-09-16 05:45:42 -04:00
// Wrong auth header returns as bad request.
{
bucketName : bucketName ,
objectName : objectName ,
data : bytesData ,
dataLen : len ( bytesData ) ,
2016-11-22 21:18:22 -05:00
chunkSize : 64 * humanize . KiByte ,
2016-09-16 05:45:42 -04:00
expectedContent : [ ] byte { } ,
expectedRespStatus : http . StatusBadRequest ,
2016-12-26 13:21:23 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
2016-09-16 05:45:42 -04:00
shouldPass : false ,
removeAuthHeader : true ,
2019-02-13 07:59:36 -05:00
fault : None ,
2016-09-16 05:45:42 -04:00
} ,
2017-02-06 17:19:27 -05:00
// Test case - 6
2016-09-16 05:45:42 -04:00
// Large chunk size.. also passes.
{
bucketName : bucketName ,
objectName : objectName ,
data : bytesData ,
dataLen : len ( bytesData ) ,
2016-11-22 21:18:22 -05:00
chunkSize : 100 * humanize . KiByte ,
2016-09-16 05:45:42 -04:00
expectedContent : [ ] byte { } ,
expectedRespStatus : http . StatusOK ,
2016-12-26 13:21:23 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
2017-03-27 20:02:04 -04:00
shouldPass : true ,
2019-02-13 07:59:36 -05:00
fault : None ,
2016-09-04 16:37:14 -04:00
} ,
2017-02-06 17:19:27 -05:00
// Test case - 7
2016-10-10 04:42:32 -04:00
// Chunk with malformed encoding.
2023-05-05 22:53:12 -04:00
// Causes signature mismatch.
2016-10-10 04:42:32 -04:00
{
bucketName : bucketName ,
objectName : objectName ,
data : oneKData ,
dataLen : 1024 ,
chunkSize : 1024 ,
expectedContent : [ ] byte { } ,
2023-05-05 22:53:12 -04:00
expectedRespStatus : http . StatusForbidden ,
2016-12-26 13:21:23 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
2016-10-10 04:42:32 -04:00
shouldPass : false ,
fault : malformedEncoding ,
} ,
2017-02-06 17:19:27 -05:00
// Test case - 8
2016-10-10 04:42:32 -04:00
// Chunk with shorter than advertised chunk data.
{
bucketName : bucketName ,
objectName : objectName ,
data : oneKData ,
dataLen : 1024 ,
chunkSize : 1024 ,
expectedContent : [ ] byte { } ,
expectedRespStatus : http . StatusBadRequest ,
2016-12-26 13:21:23 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
2016-10-10 04:42:32 -04:00
shouldPass : false ,
fault : unexpectedEOF ,
} ,
2017-02-06 17:19:27 -05:00
// Test case - 9
2016-10-10 04:42:32 -04:00
// Chunk with first chunk data byte tampered.
{
bucketName : bucketName ,
objectName : objectName ,
data : oneKData ,
dataLen : 1024 ,
chunkSize : 1024 ,
expectedContent : [ ] byte { } ,
expectedRespStatus : http . StatusForbidden ,
2016-12-26 13:21:23 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
2016-10-10 04:42:32 -04:00
shouldPass : false ,
fault : signatureMismatch ,
} ,
2017-02-06 17:19:27 -05:00
// Test case - 10
2016-10-12 02:46:51 -04:00
// Different date (timestamps) used in seed signature calculation
// and chunks signature calculation.
{
bucketName : bucketName ,
objectName : objectName ,
data : oneKData ,
dataLen : 1024 ,
chunkSize : 1024 ,
expectedContent : [ ] byte { } ,
expectedRespStatus : http . StatusForbidden ,
2016-12-26 13:21:23 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
2016-10-12 02:46:51 -04:00
shouldPass : false ,
fault : chunkDateMismatch ,
} ,
2017-02-06 17:19:27 -05:00
// Test case - 11
2016-10-26 05:06:22 -04:00
// Set x-amz-decoded-content-length to a value too big to hold in int64.
{
bucketName : bucketName ,
objectName : objectName ,
data : oneKData ,
dataLen : 1024 ,
chunkSize : 1024 ,
expectedContent : [ ] byte { } ,
2021-09-09 17:37:07 -04:00
expectedRespStatus : http . StatusBadRequest ,
2016-12-26 13:21:23 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
2016-10-26 05:06:22 -04:00
shouldPass : false ,
fault : tooBigDecodedLength ,
} ,
2017-03-27 20:02:04 -04:00
// Test case - 12
// Set custom content encoding should succeed and save the encoding properly.
{
bucketName : bucketName ,
objectName : objectName ,
data : bytesData ,
dataLen : len ( bytesData ) ,
chunkSize : 100 * humanize . KiByte ,
expectedContent : [ ] byte { } ,
expectedRespStatus : http . StatusOK ,
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
shouldPass : true ,
contentEncoding : "aws-chunked,gzip" ,
2019-02-13 07:59:36 -05:00
fault : None ,
2017-03-27 20:02:04 -04:00
} ,
2016-09-04 16:37:14 -04:00
}
// Iterating over the cases, fetching the object validating the response.
for i , testCase := range testCases {
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
rec := httptest . NewRecorder ( )
// construct HTTP request for Put Object end point.
2016-10-12 02:46:51 -04:00
var req * http . Request
2023-03-06 11:56:10 -05:00
switch {
case testCase . fault == chunkDateMismatch :
2020-07-20 15:52:49 -04:00
req , err = newTestStreamingSignedBadChunkDateRequest ( http . MethodPut ,
2016-10-12 02:46:51 -04:00
getPutObjectURL ( "" , testCase . bucketName , testCase . objectName ) ,
int64 ( testCase . dataLen ) , testCase . chunkSize , bytes . NewReader ( testCase . data ) ,
testCase . accessKey , testCase . secretKey )
2023-03-06 11:56:10 -05:00
case testCase . contentEncoding == "" :
2020-07-20 15:52:49 -04:00
req , err = newTestStreamingSignedRequest ( http . MethodPut ,
2016-10-12 02:46:51 -04:00
getPutObjectURL ( "" , testCase . bucketName , testCase . objectName ) ,
int64 ( testCase . dataLen ) , testCase . chunkSize , bytes . NewReader ( testCase . data ) ,
testCase . accessKey , testCase . secretKey )
2023-03-06 11:56:10 -05:00
case testCase . contentEncoding != "" :
2020-07-20 15:52:49 -04:00
req , err = newTestStreamingSignedCustomEncodingRequest ( http . MethodPut ,
2017-03-27 20:02:04 -04:00
getPutObjectURL ( "" , testCase . bucketName , testCase . objectName ) ,
int64 ( testCase . dataLen ) , testCase . chunkSize , bytes . NewReader ( testCase . data ) ,
testCase . accessKey , testCase . secretKey , testCase . contentEncoding )
2016-10-12 02:46:51 -04:00
}
2016-09-04 16:37:14 -04:00
if err != nil {
t . Fatalf ( "Test %d: Failed to create HTTP request for Put Object: <ERROR> %v" , i + 1 , err )
}
2016-09-16 05:45:42 -04:00
// Removes auth header if test case requires it.
if testCase . removeAuthHeader {
req . Header . Del ( "Authorization" )
}
2016-10-10 04:42:32 -04:00
switch testCase . fault {
case malformedEncoding :
req , err = malformChunkSizeSigV4 ( req , testCase . chunkSize - 1 )
case signatureMismatch :
req , err = malformDataSigV4 ( req , 'z' )
case unexpectedEOF :
req , err = truncateChunkByHalfSigv4 ( req )
2016-10-26 05:06:22 -04:00
case tooBigDecodedLength :
// Set decoded length to a large value out of int64 range to simulate parse failure.
req . Header . Set ( "x-amz-decoded-content-length" , "9999999999999999999999" )
2016-10-10 04:42:32 -04:00
}
2016-10-31 02:32:46 -04:00
if err != nil {
t . Fatalf ( "Error injecting faults into the request: <ERROR> %v." , err )
}
2024-04-28 13:53:50 -04:00
2016-09-04 16:37:14 -04:00
// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
// Call the ServeHTTP to execute the handler,`func (api objectAPIHandlers) GetObjectHandler` handles the request.
apiRouter . ServeHTTP ( rec , req )
// Assert the response code with the expected status.
if rec . Code != testCase . expectedRespStatus {
2021-01-22 15:09:24 -05:00
t . Errorf ( "Test %d %s: Expected the response status to be `%d`, but instead found `%d`: fault case %d" ,
i + 1 , instanceType , testCase . expectedRespStatus , rec . Code , testCase . fault )
2016-09-04 16:37:14 -04:00
}
// read the response body.
2022-09-19 14:05:16 -04:00
actualContent , err := io . ReadAll ( rec . Body )
2016-09-04 16:37:14 -04:00
if err != nil {
t . Fatalf ( "Test %d: %s: Failed parsing response body: <ERROR> %v" , i + 1 , instanceType , err )
}
2018-09-10 12:42:43 -04:00
opts := ObjectOptions { }
2016-09-16 05:45:42 -04:00
if testCase . shouldPass {
2016-10-04 03:09:21 -04:00
// Verify whether the bucket obtained object is same as the one created.
2016-09-16 05:45:42 -04:00
if ! bytes . Equal ( testCase . expectedContent , actualContent ) {
t . Errorf ( "Test %d: %s: Object content differs from expected value.: %s" , i + 1 , instanceType , string ( actualContent ) )
2016-10-10 04:42:32 -04:00
continue
2016-09-16 05:45:42 -04:00
}
2018-09-10 12:42:43 -04:00
objInfo , err := obj . GetObjectInfo ( context . Background ( ) , testCase . bucketName , testCase . objectName , opts )
2017-02-20 15:07:03 -05:00
if err != nil {
t . Fatalf ( "Test %d: %s: Failed to fetch the copied object: <ERROR> %s" , i + 1 , instanceType , err )
}
if objInfo . ContentEncoding == streamingContentEncoding {
t . Fatalf ( "Test %d: %s: ContentEncoding is set to \"aws-chunked\" which is unexpected" , i + 1 , instanceType )
}
2017-03-27 20:02:04 -04:00
expectedContentEncoding := trimAwsChunkedContentEncoding ( testCase . contentEncoding )
if expectedContentEncoding != objInfo . ContentEncoding {
t . Fatalf ( "Test %d: %s: ContentEncoding is set to \"%s\" which is unexpected, expected \"%s\"" , i + 1 , instanceType , objInfo . ContentEncoding , expectedContentEncoding )
}
2016-09-16 05:45:42 -04:00
buffer := new ( bytes . Buffer )
2023-04-17 15:16:37 -04:00
r , err := obj . GetObjectNInfo ( context . Background ( ) , testCase . bucketName , testCase . objectName , nil , nil , opts )
2016-09-16 05:45:42 -04:00
if err != nil {
t . Fatalf ( "Test %d: %s: Failed to fetch the copied object: <ERROR> %s" , i + 1 , instanceType , err )
}
2021-01-22 15:09:24 -05:00
if _ , err = io . Copy ( buffer , r ) ; err != nil {
r . Close ( )
t . Fatalf ( "Test %d: %s: Failed to fetch the copied object: <ERROR> %s" , i + 1 , instanceType , err )
}
r . Close ( )
2017-02-06 17:19:27 -05:00
if ! bytes . Equal ( testCase . data , buffer . Bytes ( ) ) {
2016-09-16 05:45:42 -04:00
t . Errorf ( "Test %d: %s: Data Mismatch: Data fetched back from the uploaded object doesn't match the original one." , i + 1 , instanceType )
}
2016-09-04 16:37:14 -04:00
}
}
}
2020-06-12 23:04:01 -04:00
// Wrapper for calling PutObject API handler tests for both Erasure multiple disks and FS single drive setup.
2016-09-04 16:37:14 -04:00
func TestAPIPutObjectHandler ( t * testing . T ) {
2016-11-07 00:53:50 -05:00
defer DetectTestLeak ( t ) ( )
2021-01-05 23:08:35 -05:00
ExecExtendedObjectLayerAPITest ( t , testAPIPutObjectHandler , [ ] string { "PutObject" } )
2016-09-04 16:37:14 -04:00
}
2016-09-10 17:47:27 -04:00
func testAPIPutObjectHandler ( obj ObjectLayer , instanceType , bucketName string , apiRouter http . Handler ,
2022-01-02 12:15:06 -05:00
credentials auth . Credentials , t * testing . T ,
) {
2018-03-15 16:03:41 -04:00
var err error
2016-09-10 17:47:27 -04:00
objectName := "test-object"
2018-09-10 12:42:43 -04:00
opts := ObjectOptions { }
2016-09-04 16:37:14 -04:00
// byte data for PutObject.
2016-11-22 21:18:22 -05:00
bytesData := generateBytesData ( 6 * humanize . KiByte )
2016-09-04 16:37:14 -04:00
2022-08-29 19:57:16 -04:00
copySourceHeader := map [ string ] string { "X-Amz-Copy-Source" : "somewhere" }
invalidMD5Header := map [ string ] string { "Content-Md5" : "42" }
2024-02-22 01:26:06 -05:00
invalidStorageClassHeader := map [ string ] string { xhttp . AmzStorageClass : "INVALID" }
2022-08-29 19:57:16 -04:00
addCustomHeaders := func ( req * http . Request , customHeaders map [ string ] string ) {
for k , value := range customHeaders {
req . Header . Set ( k , value )
2016-10-26 05:06:22 -04:00
}
}
2022-08-29 19:57:16 -04:00
checksumData := func ( b [ ] byte , h hash . Hash ) string {
h . Reset ( )
_ , err := h . Write ( b )
if err != nil {
t . Fatal ( err )
}
return base64 . StdEncoding . EncodeToString ( h . Sum ( nil ) )
}
2016-09-04 16:37:14 -04:00
// test cases with inputs and expected result for GetObject.
testCases := [ ] struct {
bucketName string
objectName string
2022-08-29 19:57:16 -04:00
headers map [ string ] string
2016-09-04 16:37:14 -04:00
data [ ] byte
dataLen int
2016-10-14 23:52:46 -04:00
accessKey string
secretKey string
2016-10-26 05:06:22 -04:00
fault Fault
2016-09-04 16:37:14 -04:00
// expected output.
2016-10-14 23:52:46 -04:00
expectedRespStatus int // expected response status body.
2022-08-29 19:57:16 -04:00
wantAPICode string
wantHeaders map [ string ] string
2016-09-04 16:37:14 -04:00
} {
// Fetching the entire object and validating its contents.
2022-08-29 19:57:16 -04:00
0 : {
2016-10-14 23:52:46 -04:00
bucketName : bucketName ,
objectName : objectName ,
data : bytesData ,
dataLen : len ( bytesData ) ,
2016-12-26 13:21:23 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
2016-10-14 23:52:46 -04:00
2016-09-04 16:37:14 -04:00
expectedRespStatus : http . StatusOK ,
} ,
2016-10-14 23:52:46 -04:00
// Test Case with invalid accessID.
2022-08-29 19:57:16 -04:00
1 : {
2016-10-14 23:52:46 -04:00
bucketName : bucketName ,
objectName : objectName ,
data : bytesData ,
dataLen : len ( bytesData ) ,
2024-02-22 01:26:06 -05:00
accessKey : "Wrong-AccessID" ,
2016-12-26 13:21:23 -05:00
secretKey : credentials . SecretKey ,
2016-10-14 23:52:46 -04:00
expectedRespStatus : http . StatusForbidden ,
2022-08-29 19:57:16 -04:00
wantAPICode : "InvalidAccessKeyId" ,
2016-10-14 23:52:46 -04:00
} ,
2016-10-26 05:06:22 -04:00
// Test Case with invalid header key X-Amz-Copy-Source.
2022-08-29 19:57:16 -04:00
2 : {
2016-10-26 05:06:22 -04:00
bucketName : bucketName ,
objectName : objectName ,
headers : copySourceHeader ,
data : bytesData ,
dataLen : len ( bytesData ) ,
2016-12-26 13:21:23 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
2016-10-26 05:06:22 -04:00
expectedRespStatus : http . StatusBadRequest ,
2022-08-29 19:57:16 -04:00
wantAPICode : "InvalidArgument" ,
2016-10-26 05:06:22 -04:00
} ,
// Test Case with invalid Content-Md5 value
2022-08-29 19:57:16 -04:00
3 : {
2016-10-26 05:06:22 -04:00
bucketName : bucketName ,
objectName : objectName ,
headers : invalidMD5Header ,
data : bytesData ,
dataLen : len ( bytesData ) ,
2016-12-26 13:21:23 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
2016-10-26 05:06:22 -04:00
expectedRespStatus : http . StatusBadRequest ,
2022-08-29 19:57:16 -04:00
wantAPICode : "InvalidDigest" ,
2016-10-26 05:06:22 -04:00
} ,
// Test Case with object greater than maximum allowed size.
2022-08-29 19:57:16 -04:00
4 : {
2016-10-26 05:06:22 -04:00
bucketName : bucketName ,
objectName : objectName ,
data : bytesData ,
dataLen : len ( bytesData ) ,
2016-12-26 13:21:23 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
2016-10-26 05:06:22 -04:00
fault : TooBigObject ,
expectedRespStatus : http . StatusBadRequest ,
2022-08-29 19:57:16 -04:00
wantAPICode : "EntityTooLarge" ,
2016-10-26 05:06:22 -04:00
} ,
// Test Case with missing content length
2022-08-29 19:57:16 -04:00
5 : {
2016-10-26 05:06:22 -04:00
bucketName : bucketName ,
objectName : objectName ,
data : bytesData ,
dataLen : len ( bytesData ) ,
2016-12-26 13:21:23 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
2016-10-26 05:06:22 -04:00
fault : MissingContentLength ,
expectedRespStatus : http . StatusLengthRequired ,
2022-08-29 19:57:16 -04:00
wantAPICode : "MissingContentLength" ,
2016-10-26 05:06:22 -04:00
} ,
2017-12-26 23:36:16 -05:00
// Test Case with invalid header key X-Amz-Storage-Class
2022-08-29 19:57:16 -04:00
6 : {
2017-12-26 23:36:16 -05:00
bucketName : bucketName ,
objectName : objectName ,
2024-02-22 01:26:06 -05:00
headers : invalidStorageClassHeader ,
2017-12-26 23:36:16 -05:00
data : bytesData ,
dataLen : len ( bytesData ) ,
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
expectedRespStatus : http . StatusBadRequest ,
2022-08-29 19:57:16 -04:00
wantAPICode : "InvalidStorageClass" ,
} ,
// Invalid crc32
7 : {
bucketName : bucketName ,
objectName : objectName ,
headers : map [ string ] string { "x-amz-checksum-crc32" : "123" } ,
data : bytesData ,
dataLen : len ( bytesData ) ,
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
expectedRespStatus : http . StatusBadRequest ,
wantAPICode : "InvalidArgument" ,
} ,
// Wrong crc32
8 : {
bucketName : bucketName ,
objectName : objectName ,
headers : map [ string ] string { "x-amz-checksum-crc32" : "MTIzNA==" } ,
data : bytesData ,
dataLen : len ( bytesData ) ,
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
expectedRespStatus : http . StatusBadRequest ,
wantAPICode : "XAmzContentChecksumMismatch" ,
} ,
// Correct crc32
9 : {
bucketName : bucketName ,
objectName : objectName ,
headers : map [ string ] string { "x-amz-checksum-crc32" : checksumData ( bytesData , crc32 . New ( crc32 . IEEETable ) ) } ,
data : bytesData ,
dataLen : len ( bytesData ) ,
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
expectedRespStatus : http . StatusOK ,
wantHeaders : map [ string ] string { "x-amz-checksum-crc32" : checksumData ( bytesData , crc32 . New ( crc32 . IEEETable ) ) } ,
} ,
// Correct crc32c
10 : {
bucketName : bucketName ,
objectName : objectName ,
headers : map [ string ] string { "x-amz-checksum-crc32c" : checksumData ( bytesData , crc32 . New ( crc32 . MakeTable ( crc32 . Castagnoli ) ) ) } ,
data : bytesData ,
dataLen : len ( bytesData ) ,
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
expectedRespStatus : http . StatusOK ,
wantHeaders : map [ string ] string { "x-amz-checksum-crc32c" : checksumData ( bytesData , crc32 . New ( crc32 . MakeTable ( crc32 . Castagnoli ) ) ) } ,
} ,
// CRC32 as CRC32C
11 : {
bucketName : bucketName ,
objectName : objectName ,
headers : map [ string ] string { "x-amz-checksum-crc32c" : checksumData ( bytesData , crc32 . New ( crc32 . IEEETable ) ) } ,
data : bytesData ,
dataLen : len ( bytesData ) ,
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
expectedRespStatus : http . StatusBadRequest ,
wantAPICode : "XAmzContentChecksumMismatch" ,
} ,
// SHA1
12 : {
bucketName : bucketName ,
objectName : objectName ,
headers : map [ string ] string { "x-amz-checksum-sha1" : checksumData ( bytesData , sha1 . New ( ) ) } ,
data : bytesData ,
dataLen : len ( bytesData ) ,
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
expectedRespStatus : http . StatusOK ,
wantHeaders : map [ string ] string { "x-amz-checksum-sha1" : checksumData ( bytesData , sha1 . New ( ) ) } ,
} ,
// SHA256
13 : {
bucketName : bucketName ,
objectName : objectName ,
headers : map [ string ] string { "x-amz-checksum-sha256" : checksumData ( bytesData , sha256 . New ( ) ) } ,
data : bytesData ,
dataLen : len ( bytesData ) ,
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
expectedRespStatus : http . StatusOK ,
wantHeaders : map [ string ] string { "x-amz-checksum-sha256" : checksumData ( bytesData , sha256 . New ( ) ) } ,
2017-12-26 23:36:16 -05:00
} ,
2016-09-04 16:37:14 -04:00
}
// Iterating over the cases, fetching the object validating the response.
for i , testCase := range testCases {
2016-10-14 23:52:46 -04:00
var req , reqV2 * http . Request
2016-09-04 16:37:14 -04:00
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
rec := httptest . NewRecorder ( )
// construct HTTP request for Get Object end point.
2020-07-20 15:52:49 -04:00
req , err = newTestSignedRequestV4 ( http . MethodPut , getPutObjectURL ( "" , testCase . bucketName , testCase . objectName ) ,
2022-08-29 19:57:16 -04:00
int64 ( testCase . dataLen ) , bytes . NewReader ( testCase . data ) , testCase . accessKey , testCase . secretKey , testCase . headers )
2016-09-04 16:37:14 -04:00
if err != nil {
2022-08-29 19:57:16 -04:00
t . Fatalf ( "Test %d: Failed to create HTTP request for Put Object: <ERROR> %v" , i , err )
2016-09-04 16:37:14 -04:00
}
2016-10-26 05:06:22 -04:00
// Add test case specific headers to the request.
addCustomHeaders ( req , testCase . headers )
// Inject faults if specified in testCase.fault
switch testCase . fault {
case MissingContentLength :
req . ContentLength = - 1
req . TransferEncoding = [ ] string { }
case TooBigObject :
2017-03-03 13:14:17 -05:00
req . ContentLength = globalMaxObjectSize + 1
2016-10-26 05:06:22 -04:00
}
2016-09-04 16:37:14 -04:00
// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
// Call the ServeHTTP to execute the handler,`func (api objectAPIHandlers) GetObjectHandler` handles the request.
apiRouter . ServeHTTP ( rec , req )
// Assert the response code with the expected status.
if rec . Code != testCase . expectedRespStatus {
2022-08-29 19:57:16 -04:00
b , _ := io . ReadAll ( rec . Body )
t . Fatalf ( "Test %d: Expected the response status to be `%d`, but instead found `%d`: %s" , i , testCase . expectedRespStatus , rec . Code , string ( b ) )
}
if testCase . expectedRespStatus != http . StatusOK {
b , err := io . ReadAll ( rec . Body )
if err != nil {
t . Fatal ( err )
}
var apiErr APIErrorResponse
err = xml . Unmarshal ( b , & apiErr )
if err != nil {
t . Fatal ( err )
}
gotErr := apiErr . Code
wantErr := testCase . wantAPICode
if gotErr != wantErr {
t . Errorf ( "test %d: want api error %q, got %q" , i , wantErr , gotErr )
}
if testCase . wantHeaders != nil {
for k , v := range testCase . wantHeaders {
got := rec . Header ( ) . Get ( k )
if got != v {
t . Errorf ( "Want header %s = %s, got %#v" , k , v , rec . Header ( ) )
}
}
}
2016-09-04 16:37:14 -04:00
}
2016-10-14 23:52:46 -04:00
if testCase . expectedRespStatus == http . StatusOK {
buffer := new ( bytes . Buffer )
// Fetch the object to check whether the content is same as the one uploaded via PutObject.
2023-04-17 15:16:37 -04:00
gr , err := obj . GetObjectNInfo ( context . Background ( ) , testCase . bucketName , testCase . objectName , nil , nil , opts )
2016-10-14 23:52:46 -04:00
if err != nil {
2022-08-29 19:57:16 -04:00
t . Fatalf ( "Test %d: %s: Failed to fetch the copied object: <ERROR> %s" , i , instanceType , err )
2016-10-14 23:52:46 -04:00
}
2021-01-22 15:09:24 -05:00
if _ , err = io . Copy ( buffer , gr ) ; err != nil {
gr . Close ( )
2022-08-29 19:57:16 -04:00
t . Fatalf ( "Test %d: %s: Failed to fetch the copied object: <ERROR> %s" , i , instanceType , err )
2021-01-22 15:09:24 -05:00
}
gr . Close ( )
2016-10-14 23:52:46 -04:00
if ! bytes . Equal ( bytesData , buffer . Bytes ( ) ) {
2022-08-29 19:57:16 -04:00
t . Errorf ( "Test %d: %s: Data Mismatch: Data fetched back from the uploaded object doesn't match the original one." , i , instanceType )
2016-10-14 23:52:46 -04:00
}
buffer . Reset ( )
2016-09-04 16:37:14 -04:00
}
2016-10-14 23:52:46 -04:00
// Verify response of the V2 signed HTTP request.
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
recV2 := httptest . NewRecorder ( )
// construct HTTP request for PUT Object endpoint.
2020-07-20 15:52:49 -04:00
reqV2 , err = newTestSignedRequestV2 ( http . MethodPut , getPutObjectURL ( "" , testCase . bucketName , testCase . objectName ) ,
2022-08-29 19:57:16 -04:00
int64 ( testCase . dataLen ) , bytes . NewReader ( testCase . data ) , testCase . accessKey , testCase . secretKey , testCase . headers )
2016-09-04 16:37:14 -04:00
if err != nil {
2022-08-29 19:57:16 -04:00
t . Fatalf ( "Test %d: %s: Failed to create HTTP request for PutObject: <ERROR> %v" , i , instanceType , err )
2016-09-04 16:37:14 -04:00
}
2016-10-26 05:06:22 -04:00
// Add test case specific headers to the request.
addCustomHeaders ( reqV2 , testCase . headers )
// Inject faults if specified in testCase.fault
switch testCase . fault {
case MissingContentLength :
reqV2 . ContentLength = - 1
reqV2 . TransferEncoding = [ ] string { }
case TooBigObject :
2017-03-03 13:14:17 -05:00
reqV2 . ContentLength = globalMaxObjectSize + 1
2016-10-26 05:06:22 -04:00
}
2016-10-14 23:52:46 -04:00
// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
// Call the ServeHTTP to execute the handler.
apiRouter . ServeHTTP ( recV2 , reqV2 )
if recV2 . Code != testCase . expectedRespStatus {
2022-08-29 19:57:16 -04:00
b , _ := io . ReadAll ( rec . Body )
t . Errorf ( "Test %d: %s: Expected the response status to be `%d`, but instead found `%d`: %s" , i , instanceType , testCase . expectedRespStatus , recV2 . Code , string ( b ) )
2016-10-14 23:52:46 -04:00
}
if testCase . expectedRespStatus == http . StatusOK {
buffer := new ( bytes . Buffer )
// Fetch the object to check whether the content is same as the one uploaded via PutObject.
2023-04-17 15:16:37 -04:00
gr , err := obj . GetObjectNInfo ( context . Background ( ) , testCase . bucketName , testCase . objectName , nil , nil , opts )
2016-10-14 23:52:46 -04:00
if err != nil {
2022-08-29 19:57:16 -04:00
t . Fatalf ( "Test %d: %s: Failed to fetch the copied object: <ERROR> %s" , i , instanceType , err )
2016-10-14 23:52:46 -04:00
}
2021-01-22 15:09:24 -05:00
if _ , err = io . Copy ( buffer , gr ) ; err != nil {
gr . Close ( )
2022-08-29 19:57:16 -04:00
t . Fatalf ( "Test %d: %s: Failed to fetch the copied object: <ERROR> %s" , i , instanceType , err )
2021-01-22 15:09:24 -05:00
}
gr . Close ( )
2016-10-14 23:52:46 -04:00
if ! bytes . Equal ( bytesData , buffer . Bytes ( ) ) {
2022-08-29 19:57:16 -04:00
t . Errorf ( "Test %d: %s: Data Mismatch: Data fetched back from the uploaded object doesn't match the original one." , i , instanceType )
2016-10-14 23:52:46 -04:00
}
buffer . Reset ( )
2022-08-29 19:57:16 -04:00
if testCase . wantHeaders != nil {
for k , v := range testCase . wantHeaders {
got := recV2 . Header ( ) . Get ( k )
if got != v {
t . Errorf ( "Want header %s = %s, got %#v" , k , v , recV2 . Header ( ) )
}
}
}
2016-09-04 16:37:14 -04:00
}
}
2016-10-06 16:34:33 -04:00
2016-10-08 02:28:50 -04:00
// Test for Anonymous/unsigned http request.
2020-07-20 15:52:49 -04:00
anonReq , err := newTestRequest ( http . MethodPut , getPutObjectURL ( "" , bucketName , objectName ) ,
2016-10-08 02:28:50 -04:00
int64 ( len ( "hello" ) ) , bytes . NewReader ( [ ] byte ( "hello" ) ) )
if err != nil {
2019-04-09 14:39:42 -04:00
t . Fatalf ( "MinIO %s: Failed to create an anonymous request for %s/%s: <ERROR> %v" ,
2016-10-08 02:28:50 -04:00
instanceType , bucketName , objectName , err )
}
// ExecObjectLayerAPIAnonTest - Calls the HTTP API handler using the anonymous request, validates the ErrAccessDeniedResponse,
// sets the bucket policy using the policy statement generated from `getWriteOnlyObjectStatement` so that the
// unsigned request goes through and its validated again.
2018-04-24 18:53:30 -04:00
ExecObjectLayerAPIAnonTest ( t , obj , "TestAPIPutObjectHandler" , bucketName , objectName , instanceType , apiRouter , anonReq , getAnonWriteOnlyObjectPolicy ( bucketName , objectName ) )
2016-10-08 02:28:50 -04:00
2016-10-06 16:34:33 -04:00
// HTTP request to test the case of `objectLayer` being set to `nil`.
// There is no need to use an existing bucket or valid input for creating the request,
// since the `objectLayer==nil` check is performed before any other checks inside the handlers.
// The only aim is to generate an HTTP request in a way that the relevant/registered end point is evoked/called.
nilBucket := "dummy-bucket"
nilObject := "dummy-object"
2020-07-20 15:52:49 -04:00
nilReq , err := newTestSignedRequestV4 ( http . MethodPut , getPutObjectURL ( "" , nilBucket , nilObject ) ,
2018-09-20 22:22:09 -04:00
0 , nil , "" , "" , nil )
2016-10-06 16:34:33 -04:00
if err != nil {
2019-04-09 14:39:42 -04:00
t . Errorf ( "MinIO %s: Failed to create HTTP request for testing the response when object Layer is set to `nil`." , instanceType )
2016-10-06 16:34:33 -04:00
}
// execute the object layer set to `nil` test.
2017-01-31 12:38:34 -05:00
// `ExecObjectLayerAPINilTest` manages the operation.
ExecObjectLayerAPINilTest ( t , nilBucket , nilObject , instanceType , apiRouter , nilReq )
}
2017-03-03 19:32:04 -05:00
// Tests sanity of attempting to copying each parts at offsets from an existing
// file and create a new object. Also validates if the written is same as what we
// expected.
func TestAPICopyObjectPartHandlerSanity ( t * testing . T ) {
defer DetectTestLeak ( t ) ( )
2021-01-05 23:08:35 -05:00
ExecExtendedObjectLayerAPITest ( t , testAPICopyObjectPartHandlerSanity , [ ] string { "CopyObjectPart" } )
2017-03-03 19:32:04 -05:00
}
func testAPICopyObjectPartHandlerSanity ( obj ObjectLayer , instanceType , bucketName string , apiRouter http . Handler ,
2022-01-02 12:15:06 -05:00
credentials auth . Credentials , t * testing . T ,
) {
2017-03-03 19:32:04 -05:00
objectName := "test-object"
2018-03-15 16:03:41 -04:00
var err error
2018-09-10 12:42:43 -04:00
opts := ObjectOptions { }
2017-03-03 19:32:04 -05:00
// set of byte data for PutObject.
// object has to be created before running tests for Copy Object.
// this is required even to assert the copied object,
bytesData := [ ] struct {
byteData [ ] byte
} {
{ generateBytesData ( 6 * humanize . MiByte ) } ,
}
// set of inputs for uploading the objects before tests for downloading is done.
putObjectInputs := [ ] struct {
bucketName string
objectName string
contentLength int64
textData [ ] byte
metaData map [ string ] string
} {
// case - 1.
{ bucketName , objectName , int64 ( len ( bytesData [ 0 ] . byteData ) ) , bytesData [ 0 ] . byteData , make ( map [ string ] string ) } ,
}
// iterate through the above set of inputs and upload the object.
for i , input := range putObjectInputs {
// uploading the object.
2018-03-15 16:27:16 -04:00
_ , err = obj . PutObject ( context . Background ( ) , input . bucketName , input . objectName ,
2020-12-27 01:58:06 -05:00
mustGetPutObjReader ( t , bytes . NewReader ( input . textData ) , input . contentLength , input . metaData [ "" ] , "" ) , ObjectOptions { UserDefined : input . metaData } )
2017-03-03 19:32:04 -05:00
// if object upload fails stop the test.
if err != nil {
t . Fatalf ( "Put Object case %d: Error uploading object: <ERROR> %v" , i + 1 , err )
}
}
// Initiate Multipart upload for testing PutObjectPartHandler.
testObject := "testobject"
// PutObjectPart API HTTP Handler has to be tested in isolation,
// that is without any other handler being registered,
// That's why NewMultipartUpload is initiated using ObjectLayer.
2022-08-29 19:57:16 -04:00
res , err := obj . NewMultipartUpload ( context . Background ( ) , bucketName , testObject , opts )
2017-03-03 19:32:04 -05:00
if err != nil {
// Failed to create NewMultipartUpload, abort.
2019-04-09 14:39:42 -04:00
t . Fatalf ( "MinIO %s : <ERROR> %s" , instanceType , err )
2017-03-03 19:32:04 -05:00
}
2022-08-29 19:57:16 -04:00
uploadID := res . UploadID
2017-03-03 19:32:04 -05:00
a := 0
2018-09-27 23:36:17 -04:00
b := globalMinPartSize
2017-11-14 03:25:10 -05:00
var parts [ ] CompletePart
2017-03-03 19:32:04 -05:00
for partNumber := 1 ; partNumber <= 2 ; partNumber ++ {
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
rec := httptest . NewRecorder ( )
cpPartURL := getCopyObjectPartURL ( "" , bucketName , testObject , uploadID , fmt . Sprintf ( "%d" , partNumber ) )
// construct HTTP request for copy object.
var req * http . Request
2020-07-20 15:52:49 -04:00
req , err = newTestSignedRequestV4 ( http . MethodPut , cpPartURL , 0 , nil , credentials . AccessKey , credentials . SecretKey , nil )
2017-03-03 19:32:04 -05:00
if err != nil {
t . Fatalf ( "Test failed to create HTTP request for copy object part: <ERROR> %v" , err )
}
// "X-Amz-Copy-Source" header contains the information about the source bucket and the object to copied.
req . Header . Set ( "X-Amz-Copy-Source" , url . QueryEscape ( pathJoin ( bucketName , objectName ) ) )
req . Header . Set ( "X-Amz-Copy-Source-Range" , fmt . Sprintf ( "bytes=%d-%d" , a , b ) )
// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
// Call the ServeHTTP to execute the handler, `func (api objectAPIHandlers) CopyObjectHandler` handles the request.
2018-09-27 23:36:17 -04:00
a = globalMinPartSize + 1
2017-03-03 19:32:04 -05:00
b = len ( bytesData [ 0 ] . byteData ) - 1
apiRouter . ServeHTTP ( rec , req )
if rec . Code != http . StatusOK {
t . Fatalf ( "Test failed to create HTTP request for copy %d" , rec . Code )
}
resp := & CopyObjectPartResponse { }
if err = xmlDecoder ( rec . Body , resp , rec . Result ( ) . ContentLength ) ; err != nil {
t . Fatalf ( "Test failed to decode XML response: <ERROR> %v" , err )
}
2017-11-14 03:25:10 -05:00
parts = append ( parts , CompletePart {
2017-03-03 19:32:04 -05:00
PartNumber : partNumber ,
2017-03-15 23:48:49 -04:00
ETag : canonicalizeETag ( resp . ETag ) ,
2017-03-03 19:32:04 -05:00
} )
}
2018-11-14 20:36:41 -05:00
result , err := obj . CompleteMultipartUpload ( context . Background ( ) , bucketName , testObject , uploadID , parts , ObjectOptions { } )
2017-03-03 19:32:04 -05:00
if err != nil {
t . Fatalf ( "Test: %s complete multipart upload failed: <ERROR> %v" , instanceType , err )
}
if result . Size != int64 ( len ( bytesData [ 0 ] . byteData ) ) {
t . Fatalf ( "Test: %s expected size not written: expected %d, got %d" , instanceType , len ( bytesData [ 0 ] . byteData ) , result . Size )
}
var buf bytes . Buffer
2023-04-17 15:16:37 -04:00
r , err := obj . GetObjectNInfo ( context . Background ( ) , bucketName , testObject , nil , nil , ObjectOptions { } )
2021-01-22 15:09:24 -05:00
if err != nil {
2017-03-03 19:32:04 -05:00
t . Fatalf ( "Test: %s reading completed file failed: <ERROR> %v" , instanceType , err )
}
2021-01-22 15:09:24 -05:00
if _ , err = io . Copy ( & buf , r ) ; err != nil {
r . Close ( )
t . Fatalf ( "Test %s: Failed to fetch the copied object: <ERROR> %s" , instanceType , err )
}
r . Close ( )
2017-03-03 19:32:04 -05:00
if ! bytes . Equal ( buf . Bytes ( ) , bytesData [ 0 ] . byteData ) {
t . Fatalf ( "Test: %s returned data is not expected corruption detected:" , instanceType )
}
}
2020-06-12 23:04:01 -04:00
// Wrapper for calling Copy Object Part API handler tests for both Erasure multiple disks and single node setup.
2017-01-31 12:38:34 -05:00
func TestAPICopyObjectPartHandler ( t * testing . T ) {
defer DetectTestLeak ( t ) ( )
2021-01-05 23:08:35 -05:00
ExecExtendedObjectLayerAPITest ( t , testAPICopyObjectPartHandler , [ ] string { "CopyObjectPart" } )
2017-01-31 12:38:34 -05:00
}
func testAPICopyObjectPartHandler ( obj ObjectLayer , instanceType , bucketName string , apiRouter http . Handler ,
2022-01-02 12:15:06 -05:00
credentials auth . Credentials , t * testing . T ,
) {
2017-01-31 12:38:34 -05:00
objectName := "test-object"
2018-03-15 16:03:41 -04:00
var err error
2018-09-10 12:42:43 -04:00
opts := ObjectOptions { }
2017-01-31 12:38:34 -05:00
// set of byte data for PutObject.
// object has to be created before running tests for Copy Object.
// this is required even to assert the copied object,
bytesData := [ ] struct {
byteData [ ] byte
} {
{ generateBytesData ( 6 * humanize . KiByte ) } ,
}
// set of inputs for uploading the objects before tests for downloading is done.
putObjectInputs := [ ] struct {
bucketName string
objectName string
contentLength int64
textData [ ] byte
metaData map [ string ] string
} {
// case - 1.
{ bucketName , objectName , int64 ( len ( bytesData [ 0 ] . byteData ) ) , bytesData [ 0 ] . byteData , make ( map [ string ] string ) } ,
}
// iterate through the above set of inputs and upload the object.
for i , input := range putObjectInputs {
// uploading the object.
2020-12-27 01:58:06 -05:00
_ , err = obj . PutObject ( context . Background ( ) , input . bucketName , input . objectName , mustGetPutObjReader ( t , bytes . NewReader ( input . textData ) , input . contentLength , input . metaData [ "" ] , "" ) , ObjectOptions { UserDefined : input . metaData } )
2017-01-31 12:38:34 -05:00
// if object upload fails stop the test.
if err != nil {
t . Fatalf ( "Put Object case %d: Error uploading object: <ERROR> %v" , i + 1 , err )
}
}
// Initiate Multipart upload for testing PutObjectPartHandler.
testObject := "testobject"
// PutObjectPart API HTTP Handler has to be tested in isolation,
// that is without any other handler being registered,
// That's why NewMultipartUpload is initiated using ObjectLayer.
2022-08-29 19:57:16 -04:00
res , err := obj . NewMultipartUpload ( context . Background ( ) , bucketName , testObject , opts )
2017-01-31 12:38:34 -05:00
if err != nil {
// Failed to create NewMultipartUpload, abort.
2019-04-09 14:39:42 -04:00
t . Fatalf ( "MinIO %s : <ERROR> %s" , instanceType , err )
2017-01-31 12:38:34 -05:00
}
2022-08-29 19:57:16 -04:00
uploadID := res . UploadID
2017-01-31 12:38:34 -05:00
// test cases with inputs and expected result for Copy Object.
testCases := [ ] struct {
2020-06-19 16:53:45 -04:00
bucketName string
copySourceHeader string // data for "X-Amz-Copy-Source" header. Contains the object to be copied in the URL.
copySourceRange string // data for "X-Amz-Copy-Source-Range" header, contains the byte range offsets of data to be copied.
uploadID string // uploadID of the transaction.
invalidPartNumber bool // Sets an invalid multipart.
maximumPartNumber bool // Sets a maximum parts.
accessKey string
secretKey string
2017-01-31 12:38:34 -05:00
// expected output.
expectedRespStatus int
} {
2024-03-10 17:09:36 -04:00
// Test case - 1, copy part 1 from newObject1, ignore request headers.
2017-01-31 12:38:34 -05:00
{
bucketName : bucketName ,
uploadID : uploadID ,
2019-08-06 15:08:58 -04:00
copySourceHeader : url . QueryEscape ( SlashSeparator + bucketName + SlashSeparator + objectName ) ,
2017-01-31 12:38:34 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
expectedRespStatus : http . StatusOK ,
} ,
// Test case - 2.
// Test case with invalid source object.
{
bucketName : bucketName ,
uploadID : uploadID ,
2019-08-06 15:08:58 -04:00
copySourceHeader : url . QueryEscape ( SlashSeparator ) ,
2017-01-31 12:38:34 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
expectedRespStatus : http . StatusBadRequest ,
} ,
// Test case - 3.
// Test case with new object name is same as object to be copied.
// Fail with file not found.
{
bucketName : bucketName ,
uploadID : uploadID ,
2019-08-06 15:08:58 -04:00
copySourceHeader : url . QueryEscape ( SlashSeparator + bucketName + SlashSeparator + testObject ) ,
2017-01-31 12:38:34 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
expectedRespStatus : http . StatusNotFound ,
} ,
// Test case - 4.
// Test case with valid byte range.
{
bucketName : bucketName ,
uploadID : uploadID ,
2019-08-06 15:08:58 -04:00
copySourceHeader : url . QueryEscape ( SlashSeparator + bucketName + SlashSeparator + objectName ) ,
2017-01-31 12:38:34 -05:00
copySourceRange : "bytes=500-4096" ,
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
expectedRespStatus : http . StatusOK ,
} ,
// Test case - 5.
// Test case with invalid byte range.
{
bucketName : bucketName ,
uploadID : uploadID ,
2019-08-06 15:08:58 -04:00
copySourceHeader : url . QueryEscape ( SlashSeparator + bucketName + SlashSeparator + objectName ) ,
2017-01-31 12:38:34 -05:00
copySourceRange : "bytes=6145-" ,
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
2017-03-03 19:32:04 -05:00
expectedRespStatus : http . StatusBadRequest ,
2017-01-31 12:38:34 -05:00
} ,
// Test case - 6.
2024-01-18 02:03:17 -05:00
// Test case with invalid byte range for exceeding source size boundaries.
2017-03-03 19:32:04 -05:00
{
bucketName : bucketName ,
uploadID : uploadID ,
2019-08-06 15:08:58 -04:00
copySourceHeader : url . QueryEscape ( SlashSeparator + bucketName + SlashSeparator + objectName ) ,
2017-03-03 19:32:04 -05:00
copySourceRange : "bytes=0-6144" ,
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
expectedRespStatus : http . StatusBadRequest ,
} ,
// Test case - 7.
2017-01-31 12:38:34 -05:00
// Test case with object name missing from source.
// fail with BadRequest.
{
bucketName : bucketName ,
uploadID : uploadID ,
copySourceHeader : url . QueryEscape ( "//123" ) ,
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
expectedRespStatus : http . StatusBadRequest ,
} ,
2017-03-03 19:32:04 -05:00
// Test case - 8.
2017-01-31 12:38:34 -05:00
// Test case with non-existent source file.
// Case for the purpose of failing `api.ObjectAPI.GetObjectInfo`.
// Expecting the response status code to http.StatusNotFound (404).
{
bucketName : bucketName ,
uploadID : uploadID ,
2019-08-06 15:08:58 -04:00
copySourceHeader : url . QueryEscape ( SlashSeparator + bucketName + SlashSeparator + "non-existent-object" ) ,
2017-01-31 12:38:34 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
expectedRespStatus : http . StatusNotFound ,
} ,
2017-03-03 19:32:04 -05:00
// Test case - 9.
2017-01-31 12:38:34 -05:00
// Test case with non-existent source file.
// Case for the purpose of failing `api.ObjectAPI.PutObjectPart`.
// Expecting the response status code to http.StatusNotFound (404).
{
bucketName : "non-existent-destination-bucket" ,
uploadID : uploadID ,
2019-08-06 15:08:58 -04:00
copySourceHeader : url . QueryEscape ( SlashSeparator + bucketName + SlashSeparator + objectName ) ,
2017-01-31 12:38:34 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
expectedRespStatus : http . StatusNotFound ,
} ,
2017-03-03 19:32:04 -05:00
// Test case - 10.
2017-01-31 12:38:34 -05:00
// Case with invalid AccessKey.
{
bucketName : bucketName ,
uploadID : uploadID ,
2019-08-06 15:08:58 -04:00
copySourceHeader : url . QueryEscape ( SlashSeparator + bucketName + SlashSeparator + objectName ) ,
2017-01-31 12:38:34 -05:00
accessKey : "Invalid-AccessID" ,
secretKey : credentials . SecretKey ,
expectedRespStatus : http . StatusForbidden ,
} ,
2017-03-03 19:32:04 -05:00
// Test case - 11.
2017-01-31 12:38:34 -05:00
// Case with non-existent upload id.
{
bucketName : bucketName ,
uploadID : "-1" ,
2019-08-06 15:08:58 -04:00
copySourceHeader : url . QueryEscape ( SlashSeparator + bucketName + SlashSeparator + objectName ) ,
2017-01-31 12:38:34 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
expectedRespStatus : http . StatusNotFound ,
} ,
2017-03-03 19:32:04 -05:00
// Test case - 12.
2017-01-31 12:38:34 -05:00
// invalid part number.
{
bucketName : bucketName ,
uploadID : uploadID ,
2019-08-06 15:08:58 -04:00
copySourceHeader : url . QueryEscape ( SlashSeparator + bucketName + SlashSeparator + objectName ) ,
2017-01-31 12:38:34 -05:00
invalidPartNumber : true ,
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
expectedRespStatus : http . StatusOK ,
} ,
2017-03-03 19:32:04 -05:00
// Test case - 13.
2017-01-31 12:38:34 -05:00
// maximum part number.
{
bucketName : bucketName ,
uploadID : uploadID ,
2019-08-06 15:08:58 -04:00
copySourceHeader : url . QueryEscape ( SlashSeparator + bucketName + SlashSeparator + objectName ) ,
2017-01-31 12:38:34 -05:00
maximumPartNumber : true ,
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
expectedRespStatus : http . StatusOK ,
} ,
2024-03-10 17:09:36 -04:00
// Test case - 14, copy part 1 from newObject1 with null versionId
2019-01-10 16:10:10 -05:00
{
bucketName : bucketName ,
uploadID : uploadID ,
2019-08-06 15:08:58 -04:00
copySourceHeader : url . QueryEscape ( SlashSeparator + bucketName + SlashSeparator + objectName ) + "?versionId=null" ,
2019-01-10 16:10:10 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
expectedRespStatus : http . StatusOK ,
} ,
2024-03-10 17:09:36 -04:00
// Test case - 15, copy part 1 from newObject1 with non null versionId
2019-01-10 16:10:10 -05:00
{
bucketName : bucketName ,
uploadID : uploadID ,
2019-08-06 15:08:58 -04:00
copySourceHeader : url . QueryEscape ( SlashSeparator + bucketName + SlashSeparator + objectName ) + "?versionId=17" ,
2019-01-10 16:10:10 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
2020-11-29 00:15:45 -05:00
expectedRespStatus : http . StatusNotFound ,
2019-01-10 16:10:10 -05:00
} ,
2024-01-18 02:03:17 -05:00
// Test case - 16, Test case with invalid byte range empty value.
2022-02-03 19:58:27 -05:00
{
bucketName : bucketName ,
uploadID : uploadID ,
copySourceHeader : url . QueryEscape ( SlashSeparator + bucketName + SlashSeparator + objectName ) ,
copySourceRange : "empty" ,
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
expectedRespStatus : http . StatusBadRequest ,
} ,
2017-01-31 12:38:34 -05:00
}
for i , testCase := range testCases {
var req * http . Request
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
rec := httptest . NewRecorder ( )
2023-03-06 11:56:10 -05:00
switch {
case ! testCase . invalidPartNumber || ! testCase . maximumPartNumber :
2017-01-31 12:38:34 -05:00
// construct HTTP request for copy object.
2020-07-20 15:52:49 -04:00
req , err = newTestSignedRequestV4 ( http . MethodPut , getCopyObjectPartURL ( "" , testCase . bucketName , testObject , testCase . uploadID , "1" ) , 0 , nil , testCase . accessKey , testCase . secretKey , nil )
2023-03-06 11:56:10 -05:00
case testCase . invalidPartNumber :
2020-07-20 15:52:49 -04:00
req , err = newTestSignedRequestV4 ( http . MethodPut , getCopyObjectPartURL ( "" , testCase . bucketName , testObject , testCase . uploadID , "abc" ) , 0 , nil , testCase . accessKey , testCase . secretKey , nil )
2023-03-06 11:56:10 -05:00
case testCase . maximumPartNumber :
2020-07-20 15:52:49 -04:00
req , err = newTestSignedRequestV4 ( http . MethodPut , getCopyObjectPartURL ( "" , testCase . bucketName , testObject , testCase . uploadID , "99999" ) , 0 , nil , testCase . accessKey , testCase . secretKey , nil )
2017-01-31 12:38:34 -05:00
}
if err != nil {
t . Fatalf ( "Test %d: Failed to create HTTP request for copy Object: <ERROR> %v" , i + 1 , err )
}
// "X-Amz-Copy-Source" header contains the information about the source bucket and the object to copied.
if testCase . copySourceHeader != "" {
req . Header . Set ( "X-Amz-Copy-Source" , testCase . copySourceHeader )
}
if testCase . copySourceRange != "" {
2022-02-03 19:58:27 -05:00
if testCase . copySourceRange == "empty" {
req . Header . Set ( "X-Amz-Copy-Source-Range" , "" ) // specifically test for S3 errors in this scenario.
} else {
req . Header . Set ( "X-Amz-Copy-Source-Range" , testCase . copySourceRange )
}
2017-01-31 12:38:34 -05:00
}
2020-06-12 23:04:01 -04:00
2017-01-31 12:38:34 -05:00
// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
// Call the ServeHTTP to execute the handler, `func (api objectAPIHandlers) CopyObjectHandler` handles the request.
apiRouter . ServeHTTP ( rec , req )
// Assert the response code with the expected status.
if rec . Code != testCase . expectedRespStatus {
t . Fatalf ( "Test %d: %s: Expected the response status to be `%d`, but instead found `%d`" , i + 1 , instanceType , testCase . expectedRespStatus , rec . Code )
}
if rec . Code == http . StatusOK {
// See if the new part has been uploaded.
// testing whether the copy was successful.
var results ListPartsInfo
2019-01-05 17:16:43 -05:00
results , err = obj . ListObjectParts ( context . Background ( ) , testCase . bucketName , testObject , testCase . uploadID , 0 , 1 , ObjectOptions { } )
2017-01-31 12:38:34 -05:00
if err != nil {
t . Fatalf ( "Test %d: %s: Failed to look for copied object part: <ERROR> %s" , i + 1 , instanceType , err )
}
2022-05-30 13:58:37 -04:00
if len ( results . Parts ) != 1 {
2017-01-31 12:38:34 -05:00
t . Fatalf ( "Test %d: %s: Expected only one entry returned %d entries" , i + 1 , instanceType , len ( results . Parts ) )
}
}
}
// HTTP request for testing when `ObjectLayer` is set to `nil`.
// There is no need to use an existing bucket and valid input for creating the request
// since the `objectLayer==nil` check is performed before any other checks inside the handlers.
// The only aim is to generate an HTTP request in a way that the relevant/registered end point is evoked/called.
nilBucket := "dummy-bucket"
nilObject := "dummy-object"
2020-07-20 15:52:49 -04:00
nilReq , err := newTestSignedRequestV4 ( http . MethodPut , getCopyObjectPartURL ( "" , nilBucket , nilObject , "0" , "0" ) ,
2018-09-20 22:22:09 -04:00
0 , bytes . NewReader ( [ ] byte ( "testNilObjLayer" ) ) , "" , "" , nil )
2017-01-31 12:38:34 -05:00
if err != nil {
2019-04-09 14:39:42 -04:00
t . Errorf ( "MinIO %s: Failed to create http request for testing the response when object Layer is set to `nil`." , instanceType )
2017-01-31 12:38:34 -05:00
}
// Below is how CopyObjectPartHandler is registered.
2020-07-20 15:52:49 -04:00
// bucket.Methods(http.MethodPut).Path("/{object:.+}").HeadersRegexp("X-Amz-Copy-Source", ".*?(\\/|%2F).*?").HandlerFunc(api.CopyObjectPartHandler).Queries("partNumber", "{partNumber:[0-9]+}", "uploadId", "{uploadId:.*}")
2017-01-31 12:38:34 -05:00
// Its necessary to set the "X-Amz-Copy-Source" header for the request to be accepted by the handler.
2019-08-06 15:08:58 -04:00
nilReq . Header . Set ( "X-Amz-Copy-Source" , url . QueryEscape ( SlashSeparator + nilBucket + SlashSeparator + nilObject ) )
2017-01-31 12:38:34 -05:00
// execute the object layer set to `nil` test.
2016-10-06 16:34:33 -04:00
// `ExecObjectLayerAPINilTest` manages the operation.
ExecObjectLayerAPINilTest ( t , nilBucket , nilObject , instanceType , apiRouter , nilReq )
2016-09-04 16:37:14 -04:00
}
2020-06-12 23:04:01 -04:00
// Wrapper for calling Copy Object API handler tests for both Erasure multiple disks and single node setup.
2016-08-16 22:24:23 -04:00
func TestAPICopyObjectHandler ( t * testing . T ) {
2016-11-07 00:53:50 -05:00
defer DetectTestLeak ( t ) ( )
2021-01-05 23:08:35 -05:00
ExecExtendedObjectLayerAPITest ( t , testAPICopyObjectHandler , [ ] string { "CopyObject" } )
2016-08-16 22:24:23 -04:00
}
2016-09-10 17:47:27 -04:00
func testAPICopyObjectHandler ( obj ObjectLayer , instanceType , bucketName string , apiRouter http . Handler ,
2022-01-02 12:15:06 -05:00
credentials auth . Credentials , t * testing . T ,
) {
2019-01-10 16:10:10 -05:00
objectName := "test?object" // use file with ? to test URL parsing...
if runtime . GOOS == "windows" {
objectName = "test-object" // ...except on Windows
}
2016-10-08 02:28:50 -04:00
// object used for anonymous HTTP request test.
anonObject := "anon-object"
2018-03-15 16:03:41 -04:00
var err error
2018-09-10 12:42:43 -04:00
opts := ObjectOptions { }
2016-08-16 22:24:23 -04:00
// set of byte data for PutObject.
2016-10-04 03:09:21 -04:00
// object has to be created before running tests for Copy Object.
2016-08-16 22:24:23 -04:00
// this is required even to assert the copied object,
bytesData := [ ] struct {
byteData [ ] byte
2018-11-27 16:23:32 -05:00
md5sum string
2016-08-16 22:24:23 -04:00
} {
2018-11-27 16:23:32 -05:00
{ byteData : generateBytesData ( 6 * humanize . KiByte ) } ,
2016-08-16 22:24:23 -04:00
}
2018-11-27 16:23:32 -05:00
h := md5 . New ( )
h . Write ( bytesData [ 0 ] . byteData )
bytesData [ 0 ] . md5sum = hex . EncodeToString ( h . Sum ( nil ) )
2016-08-16 22:24:23 -04:00
buffers := [ ] * bytes . Buffer {
new ( bytes . Buffer ) ,
new ( bytes . Buffer ) ,
}
// set of inputs for uploading the objects before tests for downloading is done.
putObjectInputs := [ ] struct {
bucketName string
objectName string
contentLength int64
textData [ ] byte
2018-11-27 16:23:32 -05:00
md5sum string
2016-08-16 22:24:23 -04:00
metaData map [ string ] string
} {
// case - 1.
2018-11-27 16:23:32 -05:00
{ bucketName , objectName , int64 ( len ( bytesData [ 0 ] . byteData ) ) , bytesData [ 0 ] . byteData , bytesData [ 0 ] . md5sum , make ( map [ string ] string ) } ,
2016-10-08 02:28:50 -04:00
// case - 2.
// used for anonymous HTTP request test.
2018-11-27 16:23:32 -05:00
{ bucketName , anonObject , int64 ( len ( bytesData [ 0 ] . byteData ) ) , bytesData [ 0 ] . byteData , bytesData [ 0 ] . md5sum , make ( map [ string ] string ) } ,
2016-08-16 22:24:23 -04:00
}
2018-11-27 16:23:32 -05:00
2016-08-16 22:24:23 -04:00
// iterate through the above set of inputs and upload the object.
for i , input := range putObjectInputs {
// uploading the object.
2018-11-27 16:23:32 -05:00
var objInfo ObjectInfo
2020-12-27 01:58:06 -05:00
objInfo , err = obj . PutObject ( context . Background ( ) , input . bucketName , input . objectName , mustGetPutObjReader ( t , bytes . NewReader ( input . textData ) , input . contentLength , input . md5sum , "" ) , ObjectOptions { UserDefined : input . metaData } )
2016-08-16 22:24:23 -04:00
// if object upload fails stop the test.
if err != nil {
t . Fatalf ( "Put Object case %d: Error uploading object: <ERROR> %v" , i + 1 , err )
}
2018-11-27 16:23:32 -05:00
if objInfo . ETag != input . md5sum {
t . Fatalf ( "Put Object case %d: Checksum mismatched: <ERROR> got %s, expected %s" , i + 1 , input . md5sum , objInfo . ETag )
}
2016-08-16 22:24:23 -04:00
}
// test cases with inputs and expected result for Copy Object.
testCases := [ ] struct {
2017-04-12 15:34:57 -04:00
bucketName string
newObjectName string // name of the newly copied object.
copySourceHeader string // data for "X-Amz-Copy-Source" header. Contains the object to be copied in the URL.
copyModifiedHeader string // data for "X-Amz-Copy-Source-If-Modified-Since" header
copyUnmodifiedHeader string // data for "X-Amz-Copy-Source-If-Unmodified-Since" header
2021-01-22 15:09:24 -05:00
copySourceSame bool
2017-04-12 15:34:57 -04:00
metadataGarbage bool
metadataReplace bool
metadataCopy bool
metadata map [ string ] string
accessKey string
secretKey string
2016-08-16 22:24:23 -04:00
// expected output.
expectedRespStatus int
} {
2021-01-05 23:08:35 -05:00
0 : {
expectedRespStatus : http . StatusMethodNotAllowed ,
} ,
2016-12-26 19:29:26 -05:00
// Test case - 1, copy metadata from newObject1, ignore request headers.
2021-01-05 23:08:35 -05:00
1 : {
2016-10-14 23:52:46 -04:00
bucketName : bucketName ,
newObjectName : "newObject1" ,
2019-08-06 15:08:58 -04:00
copySourceHeader : url . QueryEscape ( SlashSeparator + bucketName + SlashSeparator + objectName ) ,
2016-12-26 13:21:23 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
2016-12-26 19:29:26 -05:00
metadata : map [ string ] string {
"Content-Type" : "application/json" ,
} ,
2016-08-16 22:24:23 -04:00
expectedRespStatus : http . StatusOK ,
} ,
// Test case - 2.
// Test case with invalid source object.
2021-01-05 23:08:35 -05:00
2 : {
2016-10-14 23:52:46 -04:00
bucketName : bucketName ,
newObjectName : "newObject1" ,
2019-08-06 15:08:58 -04:00
copySourceHeader : url . QueryEscape ( SlashSeparator ) ,
2016-12-26 13:21:23 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
2016-10-14 23:52:46 -04:00
2016-08-16 22:24:23 -04:00
expectedRespStatus : http . StatusBadRequest ,
} ,
// Test case - 3.
2024-02-22 13:05:17 -05:00
// Test case with invalid source object.
2021-01-05 23:08:35 -05:00
3 : {
2024-02-22 13:05:17 -05:00
bucketName : bucketName ,
newObjectName : "dir//newObject1" ,
copySourceHeader : url . QueryEscape ( SlashSeparator + bucketName + SlashSeparator + objectName ) ,
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
expectedRespStatus : http . StatusBadRequest ,
} ,
// Test case - 4.
// Test case with new object name is same as object to be copied.
4 : {
2016-10-14 23:52:46 -04:00
bucketName : bucketName ,
newObjectName : objectName ,
2019-08-06 15:08:58 -04:00
copySourceHeader : url . QueryEscape ( SlashSeparator + bucketName + SlashSeparator + objectName ) ,
2016-12-26 13:21:23 -05:00
accessKey : credentials . AccessKey ,
2021-01-22 15:09:24 -05:00
copySourceSame : true ,
2016-12-26 13:21:23 -05:00
secretKey : credentials . SecretKey ,
2016-10-14 23:52:46 -04:00
2016-08-16 22:24:23 -04:00
expectedRespStatus : http . StatusBadRequest ,
} ,
2016-12-26 19:29:26 -05:00
2024-02-22 13:05:17 -05:00
// Test case - 5.
2017-03-29 12:21:38 -04:00
// Test case with new object name is same as object to be copied.
// But source copy is without leading slash
2024-02-22 13:05:17 -05:00
5 : {
2017-03-29 12:21:38 -04:00
bucketName : bucketName ,
newObjectName : objectName ,
2021-01-22 15:09:24 -05:00
copySourceSame : true ,
2019-08-06 15:08:58 -04:00
copySourceHeader : url . QueryEscape ( bucketName + SlashSeparator + objectName ) ,
2017-03-29 12:21:38 -04:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
expectedRespStatus : http . StatusBadRequest ,
} ,
2024-02-22 13:05:17 -05:00
// Test case - 6.
2016-12-26 19:29:26 -05:00
// Test case with new object name is same as object to be copied
// but metadata is updated.
2024-02-22 13:05:17 -05:00
6 : {
2016-12-26 19:29:26 -05:00
bucketName : bucketName ,
newObjectName : objectName ,
2019-08-06 15:08:58 -04:00
copySourceHeader : url . QueryEscape ( SlashSeparator + bucketName + SlashSeparator + objectName ) ,
2016-12-26 19:29:26 -05:00
metadata : map [ string ] string {
"Content-Type" : "application/json" ,
} ,
metadataReplace : true ,
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
expectedRespStatus : http . StatusOK ,
} ,
2024-02-22 13:05:17 -05:00
// Test case - 7.
2016-12-26 19:29:26 -05:00
// Test case with invalid metadata-directive.
2024-02-22 13:05:17 -05:00
7 : {
2016-12-26 19:29:26 -05:00
bucketName : bucketName ,
newObjectName : "newObject1" ,
2019-08-06 15:08:58 -04:00
copySourceHeader : url . QueryEscape ( SlashSeparator + bucketName + SlashSeparator + objectName ) ,
2016-12-26 19:29:26 -05:00
metadata : map [ string ] string {
"Content-Type" : "application/json" ,
} ,
metadataGarbage : true ,
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
expectedRespStatus : http . StatusBadRequest ,
} ,
2024-02-22 13:05:17 -05:00
// Test case - 8.
2016-12-26 19:29:26 -05:00
// Test case with new object name is same as object to be copied
// fail with BadRequest.
2024-02-22 13:05:17 -05:00
8 : {
2016-12-26 19:29:26 -05:00
bucketName : bucketName ,
newObjectName : objectName ,
2019-08-06 15:08:58 -04:00
copySourceHeader : url . QueryEscape ( SlashSeparator + bucketName + SlashSeparator + objectName ) ,
2016-12-26 19:29:26 -05:00
metadata : map [ string ] string {
"Content-Type" : "application/json" ,
} ,
2021-01-22 15:09:24 -05:00
copySourceSame : true ,
metadataCopy : true ,
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
2016-12-26 19:29:26 -05:00
expectedRespStatus : http . StatusBadRequest ,
} ,
2024-02-22 13:05:17 -05:00
// Test case - 9.
2016-08-16 22:24:23 -04:00
// Test case with non-existent source file.
// Case for the purpose of failing `api.ObjectAPI.GetObjectInfo`.
// Expecting the response status code to http.StatusNotFound (404).
2024-02-22 13:05:17 -05:00
9 : {
2016-10-14 23:52:46 -04:00
bucketName : bucketName ,
newObjectName : objectName ,
2019-08-06 15:08:58 -04:00
copySourceHeader : url . QueryEscape ( SlashSeparator + bucketName + SlashSeparator + "non-existent-object" ) ,
2016-12-26 13:21:23 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
2016-10-14 23:52:46 -04:00
2016-08-16 22:24:23 -04:00
expectedRespStatus : http . StatusNotFound ,
} ,
2016-12-26 19:29:26 -05:00
2024-02-22 13:05:17 -05:00
// Test case - 10.
2016-08-16 22:24:23 -04:00
// Test case with non-existent source file.
// Case for the purpose of failing `api.ObjectAPI.PutObject`.
// Expecting the response status code to http.StatusNotFound (404).
2024-02-22 13:05:17 -05:00
10 : {
2016-10-14 23:52:46 -04:00
bucketName : "non-existent-destination-bucket" ,
newObjectName : objectName ,
2019-08-06 15:08:58 -04:00
copySourceHeader : url . QueryEscape ( SlashSeparator + bucketName + SlashSeparator + objectName ) ,
2016-12-26 13:21:23 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
2016-10-14 23:52:46 -04:00
2016-08-16 22:24:23 -04:00
expectedRespStatus : http . StatusNotFound ,
} ,
2016-12-26 19:29:26 -05:00
2024-02-22 13:05:17 -05:00
// Test case - 11.
2016-12-26 13:21:23 -05:00
// Case with invalid AccessKey.
2024-02-22 13:05:17 -05:00
11 : {
2016-10-14 23:52:46 -04:00
bucketName : bucketName ,
newObjectName : objectName ,
2019-08-06 15:08:58 -04:00
copySourceHeader : url . QueryEscape ( SlashSeparator + bucketName + SlashSeparator + objectName ) ,
2016-10-14 23:52:46 -04:00
accessKey : "Invalid-AccessID" ,
2016-12-26 13:21:23 -05:00
secretKey : credentials . SecretKey ,
2016-10-14 23:52:46 -04:00
expectedRespStatus : http . StatusForbidden ,
} ,
2024-02-22 13:05:17 -05:00
// Test case - 12, copy metadata from newObject1 with satisfying modified header.
12 : {
2017-04-12 15:34:57 -04:00
bucketName : bucketName ,
newObjectName : "newObject1" ,
2019-08-06 15:08:58 -04:00
copySourceHeader : url . QueryEscape ( SlashSeparator + bucketName + SlashSeparator + objectName ) ,
2017-04-12 15:34:57 -04:00
copyModifiedHeader : "Mon, 02 Jan 2006 15:04:05 GMT" ,
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
expectedRespStatus : http . StatusOK ,
} ,
2024-02-22 13:05:17 -05:00
// Test case - 13, copy metadata from newObject1 with unsatisfying modified header.
13 : {
2017-04-12 15:34:57 -04:00
bucketName : bucketName ,
newObjectName : "newObject1" ,
2019-08-06 15:08:58 -04:00
copySourceHeader : url . QueryEscape ( SlashSeparator + bucketName + SlashSeparator + objectName ) ,
2017-04-12 15:34:57 -04:00
copyModifiedHeader : "Mon, 02 Jan 2217 15:04:05 GMT" ,
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
expectedRespStatus : http . StatusPreconditionFailed ,
} ,
2024-02-22 13:05:17 -05:00
// Test case - 14, copy metadata from newObject1 with wrong modified header format
14 : {
2017-04-12 15:34:57 -04:00
bucketName : bucketName ,
newObjectName : "newObject1" ,
2019-08-06 15:08:58 -04:00
copySourceHeader : url . QueryEscape ( SlashSeparator + bucketName + SlashSeparator + objectName ) ,
2017-04-12 15:34:57 -04:00
copyModifiedHeader : "Mon, 02 Jan 2217 15:04:05 +00:00" ,
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
expectedRespStatus : http . StatusOK ,
} ,
2024-02-22 13:05:17 -05:00
// Test case - 15, copy metadata from newObject1 with satisfying unmodified header.
15 : {
2017-04-12 15:34:57 -04:00
bucketName : bucketName ,
newObjectName : "newObject1" ,
2019-08-06 15:08:58 -04:00
copySourceHeader : url . QueryEscape ( SlashSeparator + bucketName + SlashSeparator + objectName ) ,
2017-04-12 15:34:57 -04:00
copyUnmodifiedHeader : "Mon, 02 Jan 2217 15:04:05 GMT" ,
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
expectedRespStatus : http . StatusOK ,
} ,
2024-02-22 13:05:17 -05:00
// Test case - 16, copy metadata from newObject1 with unsatisfying unmodified header.
16 : {
2017-04-12 15:34:57 -04:00
bucketName : bucketName ,
newObjectName : "newObject1" ,
2019-08-06 15:08:58 -04:00
copySourceHeader : url . QueryEscape ( SlashSeparator + bucketName + SlashSeparator + objectName ) ,
2017-04-12 15:34:57 -04:00
copyUnmodifiedHeader : "Mon, 02 Jan 2007 15:04:05 GMT" ,
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
expectedRespStatus : http . StatusPreconditionFailed ,
} ,
2024-02-22 13:05:17 -05:00
// Test case - 17, copy metadata from newObject1 with incorrect unmodified header format.
17 : {
2017-04-12 15:34:57 -04:00
bucketName : bucketName ,
newObjectName : "newObject1" ,
2019-08-06 15:08:58 -04:00
copySourceHeader : url . QueryEscape ( SlashSeparator + bucketName + SlashSeparator + objectName ) ,
2017-04-12 15:34:57 -04:00
copyUnmodifiedHeader : "Mon, 02 Jan 2007 15:04:05 +00:00" ,
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
expectedRespStatus : http . StatusOK ,
} ,
2024-02-22 13:05:17 -05:00
// Test case - 18, copy metadata from newObject1 with null versionId
18 : {
2019-01-10 16:10:10 -05:00
bucketName : bucketName ,
newObjectName : "newObject1" ,
2019-08-06 15:08:58 -04:00
copySourceHeader : url . QueryEscape ( SlashSeparator + bucketName + SlashSeparator + objectName ) + "?versionId=null" ,
2019-01-10 16:10:10 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
expectedRespStatus : http . StatusOK ,
} ,
2024-02-22 13:05:17 -05:00
// Test case - 19, copy metadata from newObject1 with non null versionId
19 : {
2019-01-10 16:10:10 -05:00
bucketName : bucketName ,
newObjectName : "newObject1" ,
2019-08-06 15:08:58 -04:00
copySourceHeader : url . QueryEscape ( SlashSeparator + bucketName + SlashSeparator + objectName ) + "?versionId=17" ,
2019-01-10 16:10:10 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
2020-11-29 00:15:45 -05:00
expectedRespStatus : http . StatusNotFound ,
2019-01-10 16:10:10 -05:00
} ,
2016-08-16 22:24:23 -04:00
}
for i , testCase := range testCases {
2016-12-26 19:29:26 -05:00
var req * http . Request
var reqV2 * http . Request
2016-08-16 22:24:23 -04:00
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
rec := httptest . NewRecorder ( )
// construct HTTP request for copy object.
2020-07-20 15:52:49 -04:00
req , err = newTestSignedRequestV4 ( http . MethodPut , getCopyObjectURL ( "" , testCase . bucketName , testCase . newObjectName ) ,
2018-09-20 22:22:09 -04:00
0 , nil , testCase . accessKey , testCase . secretKey , nil )
2016-08-16 22:24:23 -04:00
if err != nil {
2021-01-05 23:08:35 -05:00
t . Fatalf ( "Test %d: Failed to create HTTP request for copy Object: <ERROR> %v" , i , err )
2016-08-16 22:24:23 -04:00
}
// "X-Amz-Copy-Source" header contains the information about the source bucket and the object to copied.
if testCase . copySourceHeader != "" {
req . Header . Set ( "X-Amz-Copy-Source" , testCase . copySourceHeader )
}
2017-04-12 15:34:57 -04:00
if testCase . copyModifiedHeader != "" {
req . Header . Set ( "X-Amz-Copy-Source-If-Modified-Since" , testCase . copyModifiedHeader )
}
if testCase . copyUnmodifiedHeader != "" {
req . Header . Set ( "X-Amz-Copy-Source-If-Unmodified-Since" , testCase . copyUnmodifiedHeader )
}
2016-12-26 19:29:26 -05:00
// Add custom metadata.
for k , v := range testCase . metadata {
req . Header . Set ( k , v )
}
if testCase . metadataReplace {
req . Header . Set ( "X-Amz-Metadata-Directive" , "REPLACE" )
}
if testCase . metadataCopy {
req . Header . Set ( "X-Amz-Metadata-Directive" , "COPY" )
}
if testCase . metadataGarbage {
req . Header . Set ( "X-Amz-Metadata-Directive" , "Unknown" )
}
2016-08-16 22:24:23 -04:00
// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
// Call the ServeHTTP to execute the handler, `func (api objectAPIHandlers) CopyObjectHandler` handles the request.
apiRouter . ServeHTTP ( rec , req )
// Assert the response code with the expected status.
if rec . Code != testCase . expectedRespStatus {
2021-01-22 15:09:24 -05:00
if testCase . copySourceSame {
// encryption will rotate creds, so fail only for non-encryption scenario.
if GlobalKMS == nil {
t . Errorf ( "Test %d: %s: Expected the response status to be `%d`, but instead found `%d`" , i , instanceType , testCase . expectedRespStatus , rec . Code )
continue
}
} else {
t . Errorf ( "Test %d: %s: Expected the response status to be `%d`, but instead found `%d`" , i , instanceType , testCase . expectedRespStatus , rec . Code )
continue
}
2016-08-16 22:24:23 -04:00
}
if rec . Code == http . StatusOK {
2018-11-27 16:23:32 -05:00
var cpObjResp CopyObjectResponse
if err = xml . Unmarshal ( rec . Body . Bytes ( ) , & cpObjResp ) ; err != nil {
2021-01-05 23:08:35 -05:00
t . Fatalf ( "Test %d: %s: Failed to parse the CopyObjectResult response: <ERROR> %s" , i , instanceType , err )
2018-11-27 16:23:32 -05:00
}
2016-08-16 22:24:23 -04:00
// See if the new object is formed.
// testing whether the copy was successful.
2021-01-05 23:08:35 -05:00
// Note that this goes directly to the file system,
// so encryption/compression may interfere at some point.
2021-01-22 15:09:24 -05:00
buffers [ 0 ] . Reset ( )
2023-04-17 15:16:37 -04:00
r , err := obj . GetObjectNInfo ( context . Background ( ) , testCase . bucketName , testCase . newObjectName , nil , nil , opts )
2021-01-22 15:09:24 -05:00
if err != nil {
t . Fatalf ( "Test %d: %s reading completed file failed: <ERROR> %v" , i , instanceType , err )
}
if _ , err = io . Copy ( buffers [ 0 ] , r ) ; err != nil {
r . Close ( )
t . Fatalf ( "Test %d %s: Failed to fetch the copied object: <ERROR> %s" , i , instanceType , err )
}
r . Close ( )
if ! bytes . Equal ( bytesData [ 0 ] . byteData , buffers [ 0 ] . Bytes ( ) ) {
t . Errorf ( "Test %d: %s: Data Mismatch: Data fetched back from the copied object doesn't match the original one." , i , instanceType )
2016-08-16 22:24:23 -04:00
}
}
2016-10-14 23:52:46 -04:00
// Verify response of the V2 signed HTTP request.
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
recV2 := httptest . NewRecorder ( )
2020-07-20 15:52:49 -04:00
reqV2 , err = newTestRequest ( http . MethodPut , getCopyObjectURL ( "" , testCase . bucketName , testCase . newObjectName ) , 0 , nil )
2016-10-14 23:52:46 -04:00
if err != nil {
2021-01-05 23:08:35 -05:00
t . Fatalf ( "Test %d: Failed to create HTTP request for copy Object: <ERROR> %v" , i , err )
2016-10-14 23:52:46 -04:00
}
// "X-Amz-Copy-Source" header contains the information about the source bucket and the object to copied.
if testCase . copySourceHeader != "" {
reqV2 . Header . Set ( "X-Amz-Copy-Source" , testCase . copySourceHeader )
}
2017-04-12 15:34:57 -04:00
if testCase . copyModifiedHeader != "" {
reqV2 . Header . Set ( "X-Amz-Copy-Source-If-Modified-Since" , testCase . copyModifiedHeader )
}
if testCase . copyUnmodifiedHeader != "" {
reqV2 . Header . Set ( "X-Amz-Copy-Source-If-Unmodified-Since" , testCase . copyUnmodifiedHeader )
}
2016-10-14 23:52:46 -04:00
2016-12-26 19:29:26 -05:00
// Add custom metadata.
for k , v := range testCase . metadata {
reqV2 . Header . Set ( k , v + "+x" )
}
if testCase . metadataReplace {
reqV2 . Header . Set ( "X-Amz-Metadata-Directive" , "REPLACE" )
}
if testCase . metadataCopy {
reqV2 . Header . Set ( "X-Amz-Metadata-Directive" , "COPY" )
}
if testCase . metadataGarbage {
reqV2 . Header . Set ( "X-Amz-Metadata-Directive" , "Unknown" )
}
2016-10-14 23:52:46 -04:00
err = signRequestV2 ( reqV2 , testCase . accessKey , testCase . secretKey )
if err != nil {
t . Fatalf ( "Failed to V2 Sign the HTTP request: %v." , err )
}
// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
// Call the ServeHTTP to execute the handler.
apiRouter . ServeHTTP ( recV2 , reqV2 )
if recV2 . Code != testCase . expectedRespStatus {
2021-01-22 15:09:24 -05:00
if testCase . copySourceSame {
// encryption will rotate creds, so fail only for non-encryption scenario.
if GlobalKMS == nil {
t . Errorf ( "Test %d: %s: Expected the response status to be `%d`, but instead found `%d`" , i , instanceType , testCase . expectedRespStatus , rec . Code )
}
} else {
t . Errorf ( "Test %d: %s: Expected the response status to be `%d`, but instead found `%d`" , i + 1 , instanceType , testCase . expectedRespStatus , rec . Code )
}
2016-10-14 23:52:46 -04:00
}
2016-08-16 22:24:23 -04:00
}
2016-10-06 16:34:33 -04:00
// HTTP request to test the case of `objectLayer` being set to `nil`.
// There is no need to use an existing bucket or valid input for creating the request,
// since the `objectLayer==nil` check is performed before any other checks inside the handlers.
// The only aim is to generate an HTTP request in a way that the relevant/registered end point is evoked/called.
nilBucket := "dummy-bucket"
nilObject := "dummy-object"
2020-07-20 15:52:49 -04:00
nilReq , err := newTestSignedRequestV4 ( http . MethodPut , getCopyObjectURL ( "" , nilBucket , nilObject ) ,
2018-09-20 22:22:09 -04:00
0 , nil , "" , "" , nil )
2016-10-06 16:34:33 -04:00
// Below is how CopyObjectHandler is registered.
2020-07-20 15:52:49 -04:00
// bucket.Methods(http.MethodPut).Path("/{object:.+}").HeadersRegexp("X-Amz-Copy-Source", ".*?(\\/|%2F).*?")
2016-10-06 16:34:33 -04:00
// Its necessary to set the "X-Amz-Copy-Source" header for the request to be accepted by the handler.
2019-08-06 15:08:58 -04:00
nilReq . Header . Set ( "X-Amz-Copy-Source" , url . QueryEscape ( SlashSeparator + nilBucket + SlashSeparator + nilObject ) )
2016-10-06 16:34:33 -04:00
if err != nil {
2019-04-09 14:39:42 -04:00
t . Errorf ( "MinIO %s: Failed to create HTTP request for testing the response when object Layer is set to `nil`." , instanceType )
2016-10-06 16:34:33 -04:00
}
2016-10-08 02:28:50 -04:00
2016-10-06 16:34:33 -04:00
// execute the object layer set to `nil` test.
// `ExecObjectLayerAPINilTest` manages the operation.
ExecObjectLayerAPINilTest ( t , nilBucket , nilObject , instanceType , apiRouter , nilReq )
2016-08-16 22:24:23 -04:00
}
2016-09-21 23:08:08 -04:00
2020-06-12 23:04:01 -04:00
// Wrapper for calling NewMultipartUpload tests for both Erasure multiple disks and single node setup.
2024-02-22 01:26:06 -05:00
// First register the HTTP handler for NewMultipartUpload, then a HTTP request for NewMultipart upload is made.
2016-10-04 03:09:21 -04:00
// The UploadID from the response body is parsed and its existence is asserted with an attempt to ListParts using it.
2016-09-21 23:08:08 -04:00
func TestAPINewMultipartHandler ( t * testing . T ) {
2016-11-07 00:53:50 -05:00
defer DetectTestLeak ( t ) ( )
2016-09-21 23:08:08 -04:00
ExecObjectLayerAPITest ( t , testAPINewMultipartHandler , [ ] string { "NewMultipart" } )
}
func testAPINewMultipartHandler ( obj ObjectLayer , instanceType , bucketName string , apiRouter http . Handler ,
2022-01-02 12:15:06 -05:00
credentials auth . Credentials , t * testing . T ,
) {
2016-09-21 23:08:08 -04:00
objectName := "test-object-new-multipart"
rec := httptest . NewRecorder ( )
2016-10-14 23:52:46 -04:00
// construct HTTP request for NewMultipart upload.
2020-07-20 15:52:49 -04:00
req , err := newTestSignedRequestV4 ( http . MethodPost , getNewMultipartURL ( "" , bucketName , objectName ) ,
2018-09-20 22:22:09 -04:00
0 , nil , credentials . AccessKey , credentials . SecretKey , nil )
2016-09-21 23:08:08 -04:00
if err != nil {
2016-10-14 23:52:46 -04:00
t . Fatalf ( "Failed to create HTTP request for NewMultipart Request: <ERROR> %v" , err )
2016-09-21 23:08:08 -04:00
}
// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
// Call the ServeHTTP to executes the registered handler.
apiRouter . ServeHTTP ( rec , req )
// Assert the response code with the expected status.
if rec . Code != http . StatusOK {
t . Fatalf ( "%s: Expected the response status to be `%d`, but instead found `%d`" , instanceType , http . StatusOK , rec . Code )
}
2016-10-14 23:52:46 -04:00
2016-09-21 23:08:08 -04:00
// decode the response body.
decoder := xml . NewDecoder ( rec . Body )
multipartResponse := & InitiateMultipartUploadResponse { }
err = decoder . Decode ( multipartResponse )
if err != nil {
t . Fatalf ( "Error decoding the recorded response Body" )
}
// verify the uploadID my making an attempt to list parts.
2019-01-05 17:16:43 -05:00
_ , err = obj . ListObjectParts ( context . Background ( ) , bucketName , objectName , multipartResponse . UploadID , 0 , 1 , ObjectOptions { } )
2016-09-21 23:08:08 -04:00
if err != nil {
t . Fatalf ( "Invalid UploadID: <ERROR> %s" , err )
}
2024-02-22 01:26:06 -05:00
// Testing the response for Invalid AccessID.
2016-10-14 23:52:46 -04:00
// Forcing the signature check to fail.
rec = httptest . NewRecorder ( )
// construct HTTP request for NewMultipart upload.
// Setting an invalid accessID.
2020-07-20 15:52:49 -04:00
req , err = newTestSignedRequestV4 ( http . MethodPost , getNewMultipartURL ( "" , bucketName , objectName ) ,
2018-09-20 22:22:09 -04:00
0 , nil , "Invalid-AccessID" , credentials . SecretKey , nil )
2016-10-14 23:52:46 -04:00
if err != nil {
t . Fatalf ( "Failed to create HTTP request for NewMultipart Request: <ERROR> %v" , err )
}
// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP method to execute the logic of the handler.
// Call the ServeHTTP to executes the registered handler.
apiRouter . ServeHTTP ( rec , req )
// Assert the response code with the expected status.
if rec . Code != http . StatusForbidden {
t . Fatalf ( "%s: Expected the response status to be `%d`, but instead found `%d`" , instanceType , http . StatusForbidden , rec . Code )
}
// Verify response of the V2 signed HTTP request.
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
recV2 := httptest . NewRecorder ( )
// construct HTTP request for NewMultipartUpload endpoint.
2020-07-20 15:52:49 -04:00
reqV2 , err := newTestSignedRequestV2 ( http . MethodPost , getNewMultipartURL ( "" , bucketName , objectName ) ,
2018-09-20 22:22:09 -04:00
0 , nil , credentials . AccessKey , credentials . SecretKey , nil )
2016-10-14 23:52:46 -04:00
if err != nil {
t . Fatalf ( "Failed to create HTTP request for NewMultipart Request: <ERROR> %v" , err )
}
// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
// Call the ServeHTTP to execute the handler.
apiRouter . ServeHTTP ( recV2 , reqV2 )
// Assert the response code with the expected status.
if recV2 . Code != http . StatusOK {
t . Fatalf ( "%s: Expected the response status to be `%d`, but instead found `%d`" , instanceType , http . StatusOK , recV2 . Code )
}
// decode the response body.
decoder = xml . NewDecoder ( recV2 . Body )
multipartResponse = & InitiateMultipartUploadResponse { }
err = decoder . Decode ( multipartResponse )
if err != nil {
t . Fatalf ( "Error decoding the recorded response Body" )
}
// verify the uploadID my making an attempt to list parts.
2019-01-05 17:16:43 -05:00
_ , err = obj . ListObjectParts ( context . Background ( ) , bucketName , objectName , multipartResponse . UploadID , 0 , 1 , ObjectOptions { } )
2016-10-14 23:52:46 -04:00
if err != nil {
t . Fatalf ( "Invalid UploadID: <ERROR> %s" , err )
}
2024-02-22 01:26:06 -05:00
// Testing the response for invalid AccessID.
2016-10-14 23:52:46 -04:00
// Forcing the V2 signature check to fail.
recV2 = httptest . NewRecorder ( )
// construct HTTP request for NewMultipartUpload endpoint.
// Setting invalid AccessID.
2020-07-20 15:52:49 -04:00
reqV2 , err = newTestSignedRequestV2 ( http . MethodPost , getNewMultipartURL ( "" , bucketName , objectName ) ,
2018-09-20 22:22:09 -04:00
0 , nil , "Invalid-AccessID" , credentials . SecretKey , nil )
2016-10-14 23:52:46 -04:00
if err != nil {
t . Fatalf ( "Failed to create HTTP request for NewMultipart Request: <ERROR> %v" , err )
}
// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
// Call the ServeHTTP to execute the handler.
apiRouter . ServeHTTP ( recV2 , reqV2 )
// Assert the response code with the expected status.
if recV2 . Code != http . StatusForbidden {
t . Fatalf ( "%s: Expected the response status to be `%d`, but instead found `%d`" , instanceType , http . StatusForbidden , recV2 . Code )
}
2016-10-08 02:28:50 -04:00
// Test for Anonymous/unsigned http request.
2020-07-20 15:52:49 -04:00
anonReq , err := newTestRequest ( http . MethodPost , getNewMultipartURL ( "" , bucketName , objectName ) , 0 , nil )
2016-10-08 02:28:50 -04:00
if err != nil {
2019-04-09 14:39:42 -04:00
t . Fatalf ( "MinIO %s: Failed to create an anonymous request for %s/%s: <ERROR> %v" ,
2016-10-08 02:28:50 -04:00
instanceType , bucketName , objectName , err )
}
// ExecObjectLayerAPIAnonTest - Calls the HTTP API handler using the anonymous request, validates the ErrAccessDeniedResponse,
// sets the bucket policy using the policy statement generated from `getWriteOnlyObjectStatement` so that the
// unsigned request goes through and its validated again.
2018-04-24 18:53:30 -04:00
ExecObjectLayerAPIAnonTest ( t , obj , "TestAPINewMultipartHandler" , bucketName , objectName , instanceType , apiRouter , anonReq , getAnonWriteOnlyObjectPolicy ( bucketName , objectName ) )
2016-10-08 02:28:50 -04:00
2016-10-06 16:34:33 -04:00
// HTTP request to test the case of `objectLayer` being set to `nil`.
// There is no need to use an existing bucket or valid input for creating the request,
// since the `objectLayer==nil` check is performed before any other checks inside the handlers.
// The only aim is to generate an HTTP request in a way that the relevant/registered end point is evoked/called.
nilBucket := "dummy-bucket"
nilObject := "dummy-object"
2020-07-20 15:52:49 -04:00
nilReq , err := newTestSignedRequestV4 ( http . MethodPost , getNewMultipartURL ( "" , nilBucket , nilObject ) ,
2018-09-20 22:22:09 -04:00
0 , nil , "" , "" , nil )
2016-10-06 16:34:33 -04:00
if err != nil {
2019-04-09 14:39:42 -04:00
t . Errorf ( "MinIO %s: Failed to create HTTP request for testing the response when object Layer is set to `nil`." , instanceType )
2016-10-06 16:34:33 -04:00
}
// execute the object layer set to `nil` test.
// `ExecObjectLayerAPINilTest` manages the operation.
ExecObjectLayerAPINilTest ( t , nilBucket , nilObject , instanceType , apiRouter , nilReq )
2016-09-21 23:08:08 -04:00
}
2020-06-12 23:04:01 -04:00
// Wrapper for calling NewMultipartUploadParallel tests for both Erasure multiple disks and single node setup.
2016-09-21 23:08:08 -04:00
// The objective of the test is to initialte multipart upload on the same object 10 times concurrently,
2016-10-04 03:09:21 -04:00
// The UploadID from the response body is parsed and its existence is asserted with an attempt to ListParts using it.
2016-09-21 23:08:08 -04:00
func TestAPINewMultipartHandlerParallel ( t * testing . T ) {
2016-11-07 00:53:50 -05:00
defer DetectTestLeak ( t ) ( )
2016-09-21 23:08:08 -04:00
ExecObjectLayerAPITest ( t , testAPINewMultipartHandlerParallel , [ ] string { "NewMultipart" } )
}
func testAPINewMultipartHandlerParallel ( obj ObjectLayer , instanceType , bucketName string , apiRouter http . Handler ,
2022-04-13 15:00:11 -04:00
credentials auth . Credentials , t * testing . T ,
) {
2016-09-21 23:08:08 -04:00
// used for storing the uploadID's parsed on concurrent HTTP requests for NewMultipart upload on the same object.
testUploads := struct {
sync . Mutex
uploads [ ] string
} { }
objectName := "test-object-new-multipart-parallel"
var wg sync . WaitGroup
for i := 0 ; i < 10 ; i ++ {
wg . Add ( 1 )
// Initiate NewMultipart upload on the same object 10 times concurrrently.
go func ( ) {
defer wg . Done ( )
rec := httptest . NewRecorder ( )
2016-10-14 23:52:46 -04:00
// construct HTTP request NewMultipartUpload.
2020-07-20 15:52:49 -04:00
req , err := newTestSignedRequestV4 ( http . MethodPost , getNewMultipartURL ( "" , bucketName , objectName ) , 0 , nil , credentials . AccessKey , credentials . SecretKey , nil )
2016-09-21 23:08:08 -04:00
if err != nil {
2018-10-23 12:44:20 -04:00
t . Errorf ( "Failed to create HTTP request for NewMultipart request: <ERROR> %v" , err )
return
2016-09-21 23:08:08 -04:00
}
// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
// Call the ServeHTTP to executes the registered handler.
apiRouter . ServeHTTP ( rec , req )
// Assert the response code with the expected status.
if rec . Code != http . StatusOK {
2019-04-09 14:39:42 -04:00
t . Errorf ( "MinIO %s: Expected the response status to be `%d`, but instead found `%d`" , instanceType , http . StatusOK , rec . Code )
2018-10-23 12:44:20 -04:00
return
2016-09-21 23:08:08 -04:00
}
// decode the response body.
decoder := xml . NewDecoder ( rec . Body )
multipartResponse := & InitiateMultipartUploadResponse { }
err = decoder . Decode ( multipartResponse )
if err != nil {
2019-04-09 14:39:42 -04:00
t . Errorf ( "MinIO %s: Error decoding the recorded response Body" , instanceType )
2018-10-23 12:44:20 -04:00
return
2016-09-21 23:08:08 -04:00
}
// push the obtained upload ID from the response into the array.
testUploads . Lock ( )
testUploads . uploads = append ( testUploads . uploads , multipartResponse . UploadID )
testUploads . Unlock ( )
} ( )
}
// Wait till all go routines finishes execution.
wg . Wait ( )
// Validate the upload ID by an attempt to list parts using it.
for _ , uploadID := range testUploads . uploads {
2019-01-05 17:16:43 -05:00
_ , err := obj . ListObjectParts ( context . Background ( ) , bucketName , objectName , uploadID , 0 , 1 , ObjectOptions { } )
2016-09-21 23:08:08 -04:00
if err != nil {
t . Fatalf ( "Invalid UploadID: <ERROR> %s" , err )
}
}
}
2016-10-04 03:09:21 -04:00
// The UploadID from the response body is parsed and its existence is asserted with an attempt to ListParts using it.
2016-09-21 23:08:08 -04:00
func TestAPICompleteMultipartHandler ( t * testing . T ) {
2016-11-07 00:53:50 -05:00
defer DetectTestLeak ( t ) ( )
2016-09-21 23:08:08 -04:00
ExecObjectLayerAPITest ( t , testAPICompleteMultipartHandler , [ ] string { "CompleteMultipart" } )
}
func testAPICompleteMultipartHandler ( obj ObjectLayer , instanceType , bucketName string , apiRouter http . Handler ,
2022-01-02 12:15:06 -05:00
credentials auth . Credentials , t * testing . T ,
) {
2016-10-14 23:52:46 -04:00
var err error
2018-09-10 12:42:43 -04:00
var opts ObjectOptions
2016-10-08 02:28:50 -04:00
// object used for the test.
2016-09-21 23:08:08 -04:00
objectName := "test-object-new-multipart"
2016-10-08 02:28:50 -04:00
// upload IDs collected.
2016-09-21 23:08:08 -04:00
var uploadIDs [ ] string
2016-10-08 02:28:50 -04:00
for i := 0 ; i < 2 ; i ++ {
// initiate new multipart uploadID.
2022-08-29 19:57:16 -04:00
res , err := obj . NewMultipartUpload ( context . Background ( ) , bucketName , objectName , opts )
2016-10-08 02:28:50 -04:00
if err != nil {
// Failed to create NewMultipartUpload, abort.
2019-04-09 14:39:42 -04:00
t . Fatalf ( "MinIO %s : <ERROR> %s" , instanceType , err )
2016-10-08 02:28:50 -04:00
}
2022-08-29 19:57:16 -04:00
uploadIDs = append ( uploadIDs , res . UploadID )
2016-10-08 02:28:50 -04:00
}
2017-05-05 11:28:08 -04:00
// Parts with size greater than 5 MiB.
// Generating a 6 MiB byte array.
2016-11-22 21:18:22 -05:00
validPart := bytes . Repeat ( [ ] byte ( "abcdef" ) , 1 * humanize . MiByte )
2016-11-21 16:51:05 -05:00
validPartMD5 := getMD5Hash ( validPart )
2016-09-21 23:08:08 -04:00
// Create multipart parts.
// Need parts to be uploaded before CompleteMultiPartUpload can be called tested.
parts := [ ] struct {
bucketName string
objName string
uploadID string
PartID int
inputReaderData string
inputMd5 string
2024-02-22 01:26:06 -05:00
inputDataSize int64
2016-09-21 23:08:08 -04:00
} {
// Case 1-4.
// Creating sequence of parts for same uploadID.
{ bucketName , objectName , uploadIDs [ 0 ] , 1 , "abcd" , "e2fc714c4727ee9395f324cd2e7f331f" , int64 ( len ( "abcd" ) ) } ,
{ bucketName , objectName , uploadIDs [ 0 ] , 2 , "efgh" , "1f7690ebdd9b4caf8fab49ca1757bf27" , int64 ( len ( "efgh" ) ) } ,
{ bucketName , objectName , uploadIDs [ 0 ] , 3 , "ijkl" , "09a0877d04abf8759f99adec02baf579" , int64 ( len ( "abcd" ) ) } ,
{ bucketName , objectName , uploadIDs [ 0 ] , 4 , "mnop" , "e132e96a5ddad6da8b07bba6f6131fef" , int64 ( len ( "abcd" ) ) } ,
2017-05-05 11:28:08 -04:00
// Part with size larger than 5 MiB.
2021-11-16 12:28:29 -05:00
{ bucketName , objectName , uploadIDs [ 0 ] , 5 , string ( validPart ) , validPartMD5 , int64 ( len ( validPart ) ) } ,
{ bucketName , objectName , uploadIDs [ 0 ] , 6 , string ( validPart ) , validPartMD5 , int64 ( len ( validPart ) ) } ,
2016-10-08 02:28:50 -04:00
2017-05-05 11:28:08 -04:00
// Part with size larger than 5 MiB.
2016-10-08 02:28:50 -04:00
// Parts uploaded for anonymous/unsigned API handler test.
2021-11-16 12:28:29 -05:00
{ bucketName , objectName , uploadIDs [ 1 ] , 1 , string ( validPart ) , validPartMD5 , int64 ( len ( validPart ) ) } ,
{ bucketName , objectName , uploadIDs [ 1 ] , 2 , string ( validPart ) , validPartMD5 , int64 ( len ( validPart ) ) } ,
2016-09-21 23:08:08 -04:00
}
// Iterating over creatPartCases to generate multipart chunks.
for _ , part := range parts {
2018-03-15 16:27:16 -04:00
_ , err = obj . PutObjectPart ( context . Background ( ) , part . bucketName , part . objName , part . uploadID , part . PartID ,
2024-02-22 01:26:06 -05:00
mustGetPutObjReader ( t , strings . NewReader ( part . inputReaderData ) , part . inputDataSize , part . inputMd5 , "" ) , opts )
2016-09-21 23:08:08 -04:00
if err != nil {
t . Fatalf ( "%s : %s" , instanceType , err )
}
}
// Parts to be sent as input for CompleteMultipartUpload.
inputParts := [ ] struct {
2017-11-14 03:25:10 -05:00
parts [ ] CompletePart
2016-09-21 23:08:08 -04:00
} {
// inputParts - 0.
// Case for replicating ETag mismatch.
{
2017-11-14 03:25:10 -05:00
[ ] CompletePart {
2016-09-21 23:08:08 -04:00
{ ETag : "abcd" , PartNumber : 1 } ,
} ,
} ,
// inputParts - 1.
// should error out with part too small.
{
2017-11-14 03:25:10 -05:00
[ ] CompletePart {
2016-09-21 23:08:08 -04:00
{ ETag : "e2fc714c4727ee9395f324cd2e7f331f" , PartNumber : 1 } ,
{ ETag : "1f7690ebdd9b4caf8fab49ca1757bf27" , PartNumber : 2 } ,
} ,
} ,
// inputParts - 2.
// Case with invalid Part number.
{
2017-11-14 03:25:10 -05:00
[ ] CompletePart {
2016-09-21 23:08:08 -04:00
{ ETag : "e2fc714c4727ee9395f324cd2e7f331f" , PartNumber : 10 } ,
} ,
} ,
// inputParts - 3.
// Case with valid parts,but parts are unsorted.
2017-05-05 11:28:08 -04:00
// Part size greater than 5 MiB.
2016-09-21 23:08:08 -04:00
{
2017-11-14 03:25:10 -05:00
[ ] CompletePart {
2016-09-21 23:08:08 -04:00
{ ETag : validPartMD5 , PartNumber : 6 } ,
{ ETag : validPartMD5 , PartNumber : 5 } ,
} ,
} ,
// inputParts - 4.
// Case with valid part.
2017-05-05 11:28:08 -04:00
// Part size greater than 5 MiB.
2016-09-21 23:08:08 -04:00
{
2017-11-14 03:25:10 -05:00
[ ] CompletePart {
2016-09-21 23:08:08 -04:00
{ ETag : validPartMD5 , PartNumber : 5 } ,
{ ETag : validPartMD5 , PartNumber : 6 } ,
} ,
} ,
2016-10-08 02:28:50 -04:00
// inputParts - 5.
// Used for the case of testing for anonymous API request.
2017-05-05 11:28:08 -04:00
// Part size greater than 5 MiB.
2016-10-08 02:28:50 -04:00
{
2017-11-14 03:25:10 -05:00
[ ] CompletePart {
2016-10-08 02:28:50 -04:00
{ ETag : validPartMD5 , PartNumber : 1 } ,
{ ETag : validPartMD5 , PartNumber : 2 } ,
} ,
} ,
2016-09-21 23:08:08 -04:00
}
2016-10-06 16:34:33 -04:00
2016-10-26 11:46:14 -04:00
// on successful complete multipart operation the s3MD5 for the parts uploaded will be returned.
2019-05-08 21:35:40 -04:00
s3MD5 := getCompleteMultipartMD5 ( inputParts [ 3 ] . parts )
2016-10-06 16:34:33 -04:00
2016-09-21 23:08:08 -04:00
// generating the response body content for the success case.
2022-08-29 19:57:16 -04:00
successResponse := generateCompleteMultpartUploadResponse ( bucketName , objectName , getGetObjectURL ( "" , bucketName , objectName ) , ObjectInfo { ETag : s3MD5 } )
2016-09-21 23:08:08 -04:00
encodedSuccessResponse := encodeResponse ( successResponse )
2018-11-12 14:07:43 -05:00
ctx := context . Background ( )
2016-09-21 23:08:08 -04:00
testCases := [ ] struct {
2016-10-14 23:52:46 -04:00
bucket string
object string
uploadID string
2017-11-14 03:25:10 -05:00
parts [ ] CompletePart
2016-10-14 23:52:46 -04:00
accessKey string
secretKey string
2016-09-21 23:08:08 -04:00
// Expected output of CompleteMultipartUpload.
expectedContent [ ] byte
// Expected HTTP Response status.
expectedRespStatus int
} {
// Test case - 1.
// Upload and PartNumber exists, But a deliberate ETag mismatch is introduced.
{
2016-10-14 23:52:46 -04:00
bucket : bucketName ,
object : objectName ,
uploadID : uploadIDs [ 0 ] ,
parts : inputParts [ 0 ] . parts ,
2016-12-26 13:21:23 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
2016-10-14 23:52:46 -04:00
2019-02-13 19:07:21 -05:00
expectedContent : encodeResponse ( getAPIErrorResponse ( ctx ,
toAPIError ( ctx , InvalidPart { } ) ,
getGetObjectURL ( "" , bucketName , objectName ) , "" , "" ) ) ,
2016-09-21 23:08:08 -04:00
expectedRespStatus : http . StatusBadRequest ,
} ,
// Test case - 2.
2017-11-14 03:25:10 -05:00
// No parts specified in CompletePart{}.
2016-09-21 23:08:08 -04:00
// Should return ErrMalformedXML in the response body.
{
2016-10-14 23:52:46 -04:00
bucket : bucketName ,
object : objectName ,
uploadID : uploadIDs [ 0 ] ,
2017-11-14 03:25:10 -05:00
parts : [ ] CompletePart { } ,
2016-12-26 13:21:23 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
2016-10-14 23:52:46 -04:00
2019-02-13 19:07:21 -05:00
expectedContent : encodeResponse ( getAPIErrorResponse ( ctx ,
getAPIError ( ErrMalformedXML ) ,
getGetObjectURL ( "" , bucketName , objectName ) , "" , "" ) ) ,
2016-09-21 23:08:08 -04:00
expectedRespStatus : http . StatusBadRequest ,
} ,
// Test case - 3.
2016-10-04 03:09:21 -04:00
// Non-Existent uploadID.
2016-09-21 23:08:08 -04:00
// 404 Not Found response status expected.
{
2016-10-14 23:52:46 -04:00
bucket : bucketName ,
object : objectName ,
uploadID : "abc" ,
parts : inputParts [ 0 ] . parts ,
2016-12-26 13:21:23 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
2016-10-14 23:52:46 -04:00
2019-02-13 19:07:21 -05:00
expectedContent : encodeResponse ( getAPIErrorResponse ( ctx ,
toAPIError ( ctx , InvalidUploadID { UploadID : "abc" } ) ,
getGetObjectURL ( "" , bucketName , objectName ) , "" , "" ) ) ,
2016-09-21 23:08:08 -04:00
expectedRespStatus : http . StatusNotFound ,
} ,
// Test case - 4.
// Case with part size being less than minimum allowed size.
{
2016-10-14 23:52:46 -04:00
bucket : bucketName ,
object : objectName ,
uploadID : uploadIDs [ 0 ] ,
parts : inputParts [ 1 ] . parts ,
2016-12-26 13:21:23 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
2016-10-14 23:52:46 -04:00
2019-02-21 01:20:15 -05:00
expectedContent : encodeResponse ( getAPIErrorResponse ( ctx ,
toAPIError ( ctx , PartTooSmall { PartNumber : 1 } ) ,
getGetObjectURL ( "" , bucketName , objectName ) , "" , "" ) ) ,
2016-09-21 23:08:08 -04:00
expectedRespStatus : http . StatusBadRequest ,
} ,
// Test case - 5.
// TestCase with invalid Part Number.
{
2016-10-14 23:52:46 -04:00
bucket : bucketName ,
object : objectName ,
uploadID : uploadIDs [ 0 ] ,
parts : inputParts [ 2 ] . parts ,
2016-12-26 13:21:23 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
2016-10-14 23:52:46 -04:00
2019-02-13 19:07:21 -05:00
expectedContent : encodeResponse ( getAPIErrorResponse ( ctx ,
toAPIError ( ctx , InvalidPart { } ) ,
getGetObjectURL ( "" , bucketName , objectName ) , "" , "" ) ) ,
2016-09-21 23:08:08 -04:00
expectedRespStatus : http . StatusBadRequest ,
} ,
// Test case - 6.
// Parts are not sorted according to the part number.
// This should return ErrInvalidPartOrder in the response body.
{
2016-10-14 23:52:46 -04:00
bucket : bucketName ,
object : objectName ,
uploadID : uploadIDs [ 0 ] ,
parts : inputParts [ 3 ] . parts ,
2016-12-26 13:21:23 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
2016-10-14 23:52:46 -04:00
2019-02-13 19:07:21 -05:00
expectedContent : encodeResponse ( getAPIErrorResponse ( ctx ,
getAPIError ( ErrInvalidPartOrder ) ,
getGetObjectURL ( "" , bucketName , objectName ) , "" , "" ) ) ,
2016-09-21 23:08:08 -04:00
expectedRespStatus : http . StatusBadRequest ,
} ,
// Test case - 7.
// Test case with proper parts.
2024-01-18 02:03:17 -05:00
// Should succeeded and the content in the response body is asserted.
2016-09-21 23:08:08 -04:00
{
2016-10-14 23:52:46 -04:00
bucket : bucketName ,
object : objectName ,
uploadID : uploadIDs [ 0 ] ,
parts : inputParts [ 4 ] . parts ,
accessKey : "Invalid-AccessID" ,
2016-12-26 13:21:23 -05:00
secretKey : credentials . SecretKey ,
2016-10-14 23:52:46 -04:00
2019-02-13 19:07:21 -05:00
expectedContent : encodeResponse ( getAPIErrorResponse ( ctx ,
getAPIError ( ErrInvalidAccessKeyID ) ,
getGetObjectURL ( "" , bucketName , objectName ) , "" , "" ) ) ,
2016-10-14 23:52:46 -04:00
expectedRespStatus : http . StatusForbidden ,
} ,
// Test case - 8.
// Test case with proper parts.
2024-01-18 02:03:17 -05:00
// Should succeeded and the content in the response body is asserted.
2016-10-14 23:52:46 -04:00
{
bucket : bucketName ,
object : objectName ,
uploadID : uploadIDs [ 0 ] ,
parts : inputParts [ 4 ] . parts ,
2016-12-26 13:21:23 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
2016-10-14 23:52:46 -04:00
2016-09-21 23:08:08 -04:00
expectedContent : encodedSuccessResponse ,
expectedRespStatus : http . StatusOK ,
} ,
}
for i , testCase := range testCases {
var req * http . Request
2016-10-06 16:34:33 -04:00
var completeBytes , actualContent [ ] byte
2016-09-21 23:08:08 -04:00
// Complete multipart upload parts.
2017-11-14 03:25:10 -05:00
completeUploads := & CompleteMultipartUpload {
2016-09-21 23:08:08 -04:00
Parts : testCase . parts ,
}
2016-10-06 16:34:33 -04:00
completeBytes , err = xml . Marshal ( completeUploads )
2016-09-21 23:08:08 -04:00
if err != nil {
t . Fatalf ( "Error XML encoding of parts: <ERROR> %s." , err )
}
2017-11-14 03:25:10 -05:00
// Indicating that all parts are uploaded and initiating CompleteMultipartUpload.
2020-07-20 15:52:49 -04:00
req , err = newTestSignedRequestV4 ( http . MethodPost , getCompleteMultipartUploadURL ( "" , bucketName , objectName , testCase . uploadID ) ,
2018-09-20 22:22:09 -04:00
int64 ( len ( completeBytes ) ) , bytes . NewReader ( completeBytes ) , testCase . accessKey , testCase . secretKey , nil )
2016-09-21 23:08:08 -04:00
if err != nil {
2016-10-14 23:52:46 -04:00
t . Fatalf ( "Failed to create HTTP request for CompleteMultipartUpload: <ERROR> %v" , err )
2016-09-21 23:08:08 -04:00
}
2016-10-14 23:52:46 -04:00
2016-09-21 23:08:08 -04:00
rec := httptest . NewRecorder ( )
// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
// Call the ServeHTTP to executes the registered handler.
apiRouter . ServeHTTP ( rec , req )
// Assert the response code with the expected status.
if rec . Code != testCase . expectedRespStatus {
2019-04-09 14:39:42 -04:00
t . Errorf ( "Case %d: MinIO %s: Expected the response status to be `%d`, but instead found `%d`" , i + 1 , instanceType , testCase . expectedRespStatus , rec . Code )
2016-09-21 23:08:08 -04:00
}
// read the response body.
2022-09-19 14:05:16 -04:00
actualContent , err = io . ReadAll ( rec . Body )
2016-09-21 23:08:08 -04:00
if err != nil {
2019-04-09 14:39:42 -04:00
t . Fatalf ( "Test %d : MinIO %s: Failed parsing response body: <ERROR> %v" , i + 1 , instanceType , err )
2016-09-21 23:08:08 -04:00
}
2019-02-13 19:07:21 -05:00
if rec . Code == http . StatusOK {
// Verify whether the bucket obtained object is same as the one created.
if ! bytes . Equal ( testCase . expectedContent , actualContent ) {
2024-01-18 02:03:17 -05:00
t . Errorf ( "Test %d : MinIO %s: CompleteMultipart response content differs from expected value. got %s, expected %s" , i + 1 , instanceType ,
2020-10-01 23:24:34 -04:00
string ( actualContent ) , string ( testCase . expectedContent ) )
2019-02-13 19:07:21 -05:00
}
continue
}
actualError := & APIErrorResponse { }
if err = xml . Unmarshal ( actualContent , actualError ) ; err != nil {
2019-04-09 14:39:42 -04:00
t . Errorf ( "MinIO %s: error response failed to parse error XML" , instanceType )
2019-02-13 19:07:21 -05:00
}
if actualError . BucketName != bucketName {
2019-04-09 14:39:42 -04:00
t . Errorf ( "MinIO %s: error response bucket name differs from expected value" , instanceType )
2016-09-21 23:08:08 -04:00
}
2016-10-14 23:52:46 -04:00
2019-02-13 19:07:21 -05:00
if actualError . Key != objectName {
2021-03-09 15:58:22 -05:00
t . Errorf ( "MinIO %s: error response object name (%s) differs from expected value (%s)" , instanceType , actualError . Key , objectName )
2019-02-13 19:07:21 -05:00
}
2016-09-21 23:08:08 -04:00
}
2016-10-06 16:34:33 -04:00
2016-10-08 02:28:50 -04:00
// Testing for anonymous API request.
var completeBytes [ ] byte
// Complete multipart upload parts.
2017-11-14 03:25:10 -05:00
completeUploads := & CompleteMultipartUpload {
2016-10-08 02:28:50 -04:00
Parts : inputParts [ 5 ] . parts ,
}
completeBytes , err = xml . Marshal ( completeUploads )
if err != nil {
t . Fatalf ( "Error XML encoding of parts: <ERROR> %s." , err )
}
// create unsigned HTTP request for CompleteMultipart upload.
2020-07-20 15:52:49 -04:00
anonReq , err := newTestRequest ( http . MethodPost , getCompleteMultipartUploadURL ( "" , bucketName , objectName , uploadIDs [ 1 ] ) ,
2016-10-08 02:28:50 -04:00
int64 ( len ( completeBytes ) ) , bytes . NewReader ( completeBytes ) )
if err != nil {
2019-04-09 14:39:42 -04:00
t . Fatalf ( "MinIO %s: Failed to create an anonymous request for %s/%s: <ERROR> %v" ,
2016-10-08 02:28:50 -04:00
instanceType , bucketName , objectName , err )
}
// ExecObjectLayerAPIAnonTest - Calls the HTTP API handler using the anonymous request, validates the ErrAccessDeniedResponse,
// sets the bucket policy using the policy statement generated from `getWriteOnlyObjectStatement` so that the
// unsigned request goes through and its validated again.
2018-02-09 18:19:30 -05:00
ExecObjectLayerAPIAnonTest ( t , obj , "TestAPICompleteMultipartHandler" , bucketName , objectName , instanceType ,
2018-04-24 18:53:30 -04:00
apiRouter , anonReq , getAnonWriteOnlyObjectPolicy ( bucketName , objectName ) )
2016-10-08 02:28:50 -04:00
2016-10-06 16:34:33 -04:00
// HTTP request to test the case of `objectLayer` being set to `nil`.
// There is no need to use an existing bucket or valid input for creating the request,
// since the `objectLayer==nil` check is performed before any other checks inside the handlers.
// The only aim is to generate an HTTP request in a way that the relevant/registered end point is evoked/called.
2017-11-14 03:25:10 -05:00
// Indicating that all parts are uploaded and initiating CompleteMultipartUpload.
2016-10-06 16:34:33 -04:00
nilBucket := "dummy-bucket"
nilObject := "dummy-object"
2020-07-20 15:52:49 -04:00
nilReq , err := newTestSignedRequestV4 ( http . MethodPost , getCompleteMultipartUploadURL ( "" , nilBucket , nilObject , "dummy-uploadID" ) ,
2018-09-20 22:22:09 -04:00
0 , nil , "" , "" , nil )
2016-10-06 16:34:33 -04:00
if err != nil {
2019-04-09 14:39:42 -04:00
t . Errorf ( "MinIO %s: Failed to create HTTP request for testing the response when object Layer is set to `nil`." , instanceType )
2016-10-06 16:34:33 -04:00
}
// execute the object layer set to `nil` test.
// `ExecObjectLayerAPINilTest` manages the operation.
ExecObjectLayerAPINilTest ( t , nilBucket , nilObject , instanceType , apiRouter , nilReq )
2016-09-21 23:08:08 -04:00
}
2016-11-08 19:25:00 -05:00
// The UploadID from the response body is parsed and its existence is asserted with an attempt to ListParts using it.
func TestAPIAbortMultipartHandler ( t * testing . T ) {
defer DetectTestLeak ( t ) ( )
ExecObjectLayerAPITest ( t , testAPIAbortMultipartHandler , [ ] string { "AbortMultipart" } )
}
func testAPIAbortMultipartHandler ( obj ObjectLayer , instanceType , bucketName string , apiRouter http . Handler ,
2022-01-02 12:15:06 -05:00
credentials auth . Credentials , t * testing . T ,
) {
2016-11-08 19:25:00 -05:00
var err error
2018-09-10 12:42:43 -04:00
opts := ObjectOptions { }
2016-11-08 19:25:00 -05:00
// object used for the test.
objectName := "test-object-new-multipart"
// upload IDs collected.
var uploadIDs [ ] string
for i := 0 ; i < 2 ; i ++ {
// initiate new multipart uploadID.
2022-08-29 19:57:16 -04:00
res , err := obj . NewMultipartUpload ( context . Background ( ) , bucketName , objectName , opts )
2016-11-08 19:25:00 -05:00
if err != nil {
// Failed to create NewMultipartUpload, abort.
2019-04-09 14:39:42 -04:00
t . Fatalf ( "MinIO %s : <ERROR> %s" , instanceType , err )
2016-11-08 19:25:00 -05:00
}
2022-08-29 19:57:16 -04:00
uploadIDs = append ( uploadIDs , res . UploadID )
2016-11-08 19:25:00 -05:00
}
2017-05-05 11:28:08 -04:00
// Parts with size greater than 5 MiB.
// Generating a 6 MiB byte array.
2016-11-22 21:18:22 -05:00
validPart := bytes . Repeat ( [ ] byte ( "abcdef" ) , 1 * humanize . MiByte )
2016-11-21 16:51:05 -05:00
validPartMD5 := getMD5Hash ( validPart )
2016-11-08 19:25:00 -05:00
// Create multipart parts.
// Need parts to be uploaded before AbortMultiPartUpload can be called tested.
parts := [ ] struct {
bucketName string
objName string
uploadID string
PartID int
inputReaderData string
inputMd5 string
2024-02-22 01:26:06 -05:00
inputDataSize int64
2016-11-08 19:25:00 -05:00
} {
// Case 1-4.
// Creating sequence of parts for same uploadID.
{ bucketName , objectName , uploadIDs [ 0 ] , 1 , "abcd" , "e2fc714c4727ee9395f324cd2e7f331f" , int64 ( len ( "abcd" ) ) } ,
{ bucketName , objectName , uploadIDs [ 0 ] , 2 , "efgh" , "1f7690ebdd9b4caf8fab49ca1757bf27" , int64 ( len ( "efgh" ) ) } ,
{ bucketName , objectName , uploadIDs [ 0 ] , 3 , "ijkl" , "09a0877d04abf8759f99adec02baf579" , int64 ( len ( "abcd" ) ) } ,
{ bucketName , objectName , uploadIDs [ 0 ] , 4 , "mnop" , "e132e96a5ddad6da8b07bba6f6131fef" , int64 ( len ( "abcd" ) ) } ,
2017-05-05 11:28:08 -04:00
// Part with size larger than 5 MiB.
2021-11-16 12:28:29 -05:00
{ bucketName , objectName , uploadIDs [ 0 ] , 5 , string ( validPart ) , validPartMD5 , int64 ( len ( validPart ) ) } ,
{ bucketName , objectName , uploadIDs [ 0 ] , 6 , string ( validPart ) , validPartMD5 , int64 ( len ( validPart ) ) } ,
2016-11-08 19:25:00 -05:00
2017-05-05 11:28:08 -04:00
// Part with size larger than 5 MiB.
2016-11-08 19:25:00 -05:00
// Parts uploaded for anonymous/unsigned API handler test.
2021-11-16 12:28:29 -05:00
{ bucketName , objectName , uploadIDs [ 1 ] , 1 , string ( validPart ) , validPartMD5 , int64 ( len ( validPart ) ) } ,
{ bucketName , objectName , uploadIDs [ 1 ] , 2 , string ( validPart ) , validPartMD5 , int64 ( len ( validPart ) ) } ,
2016-11-08 19:25:00 -05:00
}
// Iterating over createPartCases to generate multipart chunks.
for _ , part := range parts {
2018-03-15 16:27:16 -04:00
_ , err = obj . PutObjectPart ( context . Background ( ) , part . bucketName , part . objName , part . uploadID , part . PartID ,
2024-02-22 01:26:06 -05:00
mustGetPutObjReader ( t , strings . NewReader ( part . inputReaderData ) , part . inputDataSize , part . inputMd5 , "" ) , opts )
2016-11-08 19:25:00 -05:00
if err != nil {
t . Fatalf ( "%s : %s" , instanceType , err )
}
}
testCases := [ ] struct {
bucket string
object string
uploadID string
accessKey string
secretKey string
// Expected HTTP Response status.
expectedRespStatus int
} {
// Test case - 1.
// Abort existing upload ID.
{
bucket : bucketName ,
object : objectName ,
uploadID : uploadIDs [ 0 ] ,
2016-12-26 13:21:23 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
2016-11-08 19:25:00 -05:00
expectedRespStatus : http . StatusNoContent ,
} ,
// Test case - 2.
2024-02-22 01:26:06 -05:00
// Abort non-existing upload ID.
2016-11-08 19:25:00 -05:00
{
bucket : bucketName ,
object : objectName ,
uploadID : "nonexistent-upload-id" ,
2016-12-26 13:21:23 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
2016-11-08 19:25:00 -05:00
expectedRespStatus : http . StatusNotFound ,
} ,
// Test case - 3.
// Abort with unknown Access key.
{
bucket : bucketName ,
object : objectName ,
uploadID : uploadIDs [ 0 ] ,
accessKey : "Invalid-AccessID" ,
2016-12-26 13:21:23 -05:00
secretKey : credentials . SecretKey ,
2016-11-08 19:25:00 -05:00
expectedRespStatus : http . StatusForbidden ,
} ,
}
for i , testCase := range testCases {
var req * http . Request
// Indicating that all parts are uploaded and initiating abortMultipartUpload.
2020-07-20 15:52:49 -04:00
req , err = newTestSignedRequestV4 ( http . MethodDelete , getAbortMultipartUploadURL ( "" , testCase . bucket , testCase . object , testCase . uploadID ) ,
2018-09-20 22:22:09 -04:00
0 , nil , testCase . accessKey , testCase . secretKey , nil )
2016-11-08 19:25:00 -05:00
if err != nil {
t . Fatalf ( "Failed to create HTTP request for AbortMultipartUpload: <ERROR> %v" , err )
}
rec := httptest . NewRecorder ( )
// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
// Call the ServeHTTP to executes the registered handler.
apiRouter . ServeHTTP ( rec , req )
// Assert the response code with the expected status.
if rec . Code != testCase . expectedRespStatus {
2019-04-09 14:39:42 -04:00
t . Errorf ( "Case %d: MinIO %s: Expected the response status to be `%d`, but instead found `%d`" , i + 1 , instanceType , testCase . expectedRespStatus , rec . Code )
2016-11-08 19:25:00 -05:00
}
}
// create unsigned HTTP request for Abort multipart upload.
2020-07-20 15:52:49 -04:00
anonReq , err := newTestRequest ( http . MethodDelete , getAbortMultipartUploadURL ( "" , bucketName , objectName , uploadIDs [ 1 ] ) ,
2016-11-08 19:25:00 -05:00
0 , nil )
if err != nil {
2019-04-09 14:39:42 -04:00
t . Fatalf ( "MinIO %s: Failed to create an anonymous request for %s/%s: <ERROR> %v" ,
2016-11-08 19:25:00 -05:00
instanceType , bucketName , objectName , err )
}
// ExecObjectLayerAPIAnonTest - Calls the HTTP API handler using the anonymous request, validates the ErrAccessDeniedResponse,
// sets the bucket policy using the policy statement generated from `getWriteOnlyObjectStatement` so that the
// unsigned request goes through and its validated again.
2018-02-09 18:19:30 -05:00
ExecObjectLayerAPIAnonTest ( t , obj , "TestAPIAbortMultipartHandler" , bucketName , objectName , instanceType ,
2018-04-24 18:53:30 -04:00
apiRouter , anonReq , getAnonWriteOnlyObjectPolicy ( bucketName , objectName ) )
2016-11-08 19:25:00 -05:00
// HTTP request to test the case of `objectLayer` being set to `nil`.
// There is no need to use an existing bucket or valid input for creating the request,
// since the `objectLayer==nil` check is performed before any other checks inside the handlers.
// The only aim is to generate an HTTP request in a way that the relevant/registered end point is evoked/called.
// Indicating that all parts are uploaded and initiating abortMultipartUpload.
nilBucket := "dummy-bucket"
nilObject := "dummy-object"
2020-07-20 15:52:49 -04:00
nilReq , err := newTestSignedRequestV4 ( http . MethodDelete , getAbortMultipartUploadURL ( "" , nilBucket , nilObject , "dummy-uploadID" ) ,
2018-09-20 22:22:09 -04:00
0 , nil , "" , "" , nil )
2016-11-08 19:25:00 -05:00
if err != nil {
2019-04-09 14:39:42 -04:00
t . Errorf ( "MinIO %s: Failed to create HTTP request for testing the response when object Layer is set to `nil`." , instanceType )
2016-11-08 19:25:00 -05:00
}
// execute the object layer set to `nil` test.
// `ExecObjectLayerAPINilTest` manages the operation.
ExecObjectLayerAPINilTest ( t , nilBucket , nilObject , instanceType , apiRouter , nilReq )
}
2020-06-12 23:04:01 -04:00
// Wrapper for calling Delete Object API handler tests for both Erasure multiple disks and FS single drive setup.
2016-10-08 02:28:50 -04:00
func TestAPIDeleteObjectHandler ( t * testing . T ) {
2016-11-07 00:53:50 -05:00
defer DetectTestLeak ( t ) ( )
2016-10-08 02:28:50 -04:00
ExecObjectLayerAPITest ( t , testAPIDeleteObjectHandler , [ ] string { "DeleteObject" } )
2016-09-21 23:08:08 -04:00
}
2016-10-08 02:28:50 -04:00
func testAPIDeleteObjectHandler ( obj ObjectLayer , instanceType , bucketName string , apiRouter http . Handler ,
2022-01-02 12:15:06 -05:00
credentials auth . Credentials , t * testing . T ,
) {
2018-03-15 16:03:41 -04:00
var err error
2016-09-21 23:08:08 -04:00
objectName := "test-object"
2016-10-08 02:28:50 -04:00
// Object used for anonymous API request test.
anonObjectName := "test-anon-obj"
2016-09-21 23:08:08 -04:00
// set of byte data for PutObject.
2016-10-04 03:09:21 -04:00
// object has to be created before running tests for Deleting the object.
2016-09-21 23:08:08 -04:00
bytesData := [ ] struct {
byteData [ ] byte
} {
2016-11-22 21:18:22 -05:00
{ generateBytesData ( 6 * humanize . MiByte ) } ,
2016-09-21 23:08:08 -04:00
}
// set of inputs for uploading the objects before tests for deleting them is done.
putObjectInputs := [ ] struct {
bucketName string
objectName string
contentLength int64
textData [ ] byte
metaData map [ string ] string
} {
// case - 1.
{ bucketName , objectName , int64 ( len ( bytesData [ 0 ] . byteData ) ) , bytesData [ 0 ] . byteData , make ( map [ string ] string ) } ,
2016-10-08 02:28:50 -04:00
// case - 2.
{ bucketName , anonObjectName , int64 ( len ( bytesData [ 0 ] . byteData ) ) , bytesData [ 0 ] . byteData , make ( map [ string ] string ) } ,
2016-09-21 23:08:08 -04:00
}
// iterate through the above set of inputs and upload the object.
for i , input := range putObjectInputs {
// uploading the object.
2020-12-27 01:58:06 -05:00
_ , err = obj . PutObject ( context . Background ( ) , input . bucketName , input . objectName , mustGetPutObjReader ( t , bytes . NewReader ( input . textData ) , input . contentLength , input . metaData [ "" ] , "" ) , ObjectOptions { UserDefined : input . metaData } )
2016-09-21 23:08:08 -04:00
// if object upload fails stop the test.
if err != nil {
t . Fatalf ( "Put Object case %d: Error uploading object: <ERROR> %v" , i + 1 , err )
}
}
// test cases with inputs and expected result for DeleteObject.
testCases := [ ] struct {
bucketName string
objectName string
2016-10-14 23:52:46 -04:00
accessKey string
secretKey string
2016-09-21 23:08:08 -04:00
expectedRespStatus int // expected response status body.
} {
// Test case - 1.
// Deleting an existing object.
2024-01-18 02:03:17 -05:00
// Expected to return HTTP response status code 204.
2016-09-21 23:08:08 -04:00
{
bucketName : bucketName ,
objectName : objectName ,
2016-12-26 13:21:23 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
2016-09-21 23:08:08 -04:00
expectedRespStatus : http . StatusNoContent ,
} ,
// Test case - 2.
// Attempt to delete an object which is already deleted.
// Still should return http response status 204.
{
bucketName : bucketName ,
objectName : objectName ,
2016-12-26 13:21:23 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
2016-09-21 23:08:08 -04:00
expectedRespStatus : http . StatusNoContent ,
} ,
2016-10-14 23:52:46 -04:00
// Test case - 3.
// Setting Invalid AccessKey to force signature check inside the handler to fail.
// Should return HTTP response status 403 forbidden.
{
bucketName : bucketName ,
objectName : objectName ,
accessKey : "Invalid-AccessKey" ,
2016-12-26 13:21:23 -05:00
secretKey : credentials . SecretKey ,
2016-10-14 23:52:46 -04:00
expectedRespStatus : http . StatusForbidden ,
} ,
2016-09-21 23:08:08 -04:00
}
// Iterating over the cases, call DeleteObjectHandler and validate the HTTP response.
for i , testCase := range testCases {
2016-10-14 23:52:46 -04:00
var req , reqV2 * http . Request
2016-09-21 23:08:08 -04:00
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
rec := httptest . NewRecorder ( )
2016-10-14 23:52:46 -04:00
// construct HTTP request for Delete Object end point.
2020-07-20 15:52:49 -04:00
req , err = newTestSignedRequestV4 ( http . MethodDelete , getDeleteObjectURL ( "" , testCase . bucketName , testCase . objectName ) ,
2018-09-20 22:22:09 -04:00
0 , nil , testCase . accessKey , testCase . secretKey , nil )
2016-09-21 23:08:08 -04:00
if err != nil {
2016-10-14 23:52:46 -04:00
t . Fatalf ( "Test %d: Failed to create HTTP request for Delete Object: <ERROR> %v" , i + 1 , err )
2016-09-21 23:08:08 -04:00
}
// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
// Call the ServeHTTP to execute the handler,`func (api objectAPIHandlers) DeleteObjectHandler` handles the request.
apiRouter . ServeHTTP ( rec , req )
// Assert the response code with the expected status.
if rec . Code != testCase . expectedRespStatus {
2019-04-09 14:39:42 -04:00
t . Fatalf ( "MinIO %s: Case %d: Expected the response status to be `%d`, but instead found `%d`" , instanceType , i + 1 , testCase . expectedRespStatus , rec . Code )
2016-09-21 23:08:08 -04:00
}
2016-10-14 23:52:46 -04:00
// Verify response of the V2 signed HTTP request.
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
recV2 := httptest . NewRecorder ( )
// construct HTTP request for Delete Object endpoint.
2020-07-20 15:52:49 -04:00
reqV2 , err = newTestSignedRequestV2 ( http . MethodDelete , getDeleteObjectURL ( "" , testCase . bucketName , testCase . objectName ) ,
2018-09-20 22:22:09 -04:00
0 , nil , testCase . accessKey , testCase . secretKey , nil )
2016-10-14 23:52:46 -04:00
if err != nil {
t . Fatalf ( "Failed to create HTTP request for NewMultipart Request: <ERROR> %v" , err )
}
// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
// Call the ServeHTTP to execute the handler.
apiRouter . ServeHTTP ( recV2 , reqV2 )
// Assert the response code with the expected status.
if recV2 . Code != testCase . expectedRespStatus {
2019-04-09 14:39:42 -04:00
t . Errorf ( "Case %d: MinIO %s: Expected the response status to be `%d`, but instead found `%d`" , i + 1 ,
2016-10-14 23:52:46 -04:00
instanceType , testCase . expectedRespStatus , recV2 . Code )
}
2016-09-21 23:08:08 -04:00
}
2016-10-06 16:34:33 -04:00
2016-10-08 02:28:50 -04:00
// Test for Anonymous/unsigned http request.
2020-07-20 15:52:49 -04:00
anonReq , err := newTestRequest ( http . MethodDelete , getDeleteObjectURL ( "" , bucketName , anonObjectName ) , 0 , nil )
2016-10-08 02:28:50 -04:00
if err != nil {
2019-04-09 14:39:42 -04:00
t . Fatalf ( "MinIO %s: Failed to create an anonymous request for %s/%s: <ERROR> %v" ,
2016-10-08 02:28:50 -04:00
instanceType , bucketName , anonObjectName , err )
}
// ExecObjectLayerAPIAnonTest - Calls the HTTP API handler using the anonymous request, validates the ErrAccessDeniedResponse,
// sets the bucket policy using the policy statement generated from `getWriteOnlyObjectStatement` so that the
// unsigned request goes through and its validated again.
2018-04-24 18:53:30 -04:00
ExecObjectLayerAPIAnonTest ( t , obj , "TestAPIDeleteObjectHandler" , bucketName , anonObjectName , instanceType , apiRouter , anonReq , getAnonWriteOnlyObjectPolicy ( bucketName , anonObjectName ) )
2016-10-08 02:28:50 -04:00
2016-10-06 16:34:33 -04:00
// HTTP request to test the case of `objectLayer` being set to `nil`.
// There is no need to use an existing bucket or valid input for creating the request,
// since the `objectLayer==nil` check is performed before any other checks inside the handlers.
// The only aim is to generate an HTTP request in a way that the relevant/registered end point is evoked/called.
nilBucket := "dummy-bucket"
nilObject := "dummy-object"
2020-07-20 15:52:49 -04:00
nilReq , err := newTestSignedRequestV4 ( http . MethodDelete , getDeleteObjectURL ( "" , nilBucket , nilObject ) ,
2018-09-20 22:22:09 -04:00
0 , nil , "" , "" , nil )
2016-10-06 16:34:33 -04:00
if err != nil {
2019-04-09 14:39:42 -04:00
t . Errorf ( "MinIO %s: Failed to create HTTP request for testing the response when object Layer is set to `nil`." , instanceType )
2016-10-06 16:34:33 -04:00
}
// execute the object layer set to `nil` test.
// `ExecObjectLayerAPINilTest` manages the operation.
ExecObjectLayerAPINilTest ( t , nilBucket , nilObject , instanceType , apiRouter , nilReq )
2016-09-21 23:08:08 -04:00
}
2016-10-01 11:23:26 -04:00
2016-10-07 11:02:37 -04:00
// TestAPIPutObjectPartHandlerStreaming - Tests validate the response of PutObjectPart HTTP handler
// when the request signature type is `streaming signature`.
func TestAPIPutObjectPartHandlerStreaming ( t * testing . T ) {
2016-11-07 00:53:50 -05:00
defer DetectTestLeak ( t ) ( )
2021-01-05 23:08:35 -05:00
ExecExtendedObjectLayerAPITest ( t , testAPIPutObjectPartHandlerStreaming , [ ] string { "NewMultipart" , "PutObjectPart" } )
2016-10-01 11:23:26 -04:00
}
func testAPIPutObjectPartHandlerStreaming ( obj ObjectLayer , instanceType , bucketName string , apiRouter http . Handler ,
2022-04-13 15:00:11 -04:00
credentials auth . Credentials , t * testing . T ,
) {
2016-10-01 11:23:26 -04:00
testObject := "testobject"
rec := httptest . NewRecorder ( )
2020-07-20 15:52:49 -04:00
req , err := newTestSignedRequestV4 ( http . MethodPost , getNewMultipartURL ( "" , bucketName , "testobject" ) ,
2018-09-20 22:22:09 -04:00
0 , nil , credentials . AccessKey , credentials . SecretKey , nil )
2016-10-01 11:23:26 -04:00
if err != nil {
t . Fatalf ( "[%s] - Failed to create a signed request to initiate multipart upload for %s/%s: <ERROR> %v" ,
instanceType , bucketName , testObject , err )
}
apiRouter . ServeHTTP ( rec , req )
2024-01-18 02:03:17 -05:00
// Get uploadID of the multipart upload initiated.
2016-10-01 11:23:26 -04:00
var mpartResp InitiateMultipartUploadResponse
2022-09-19 14:05:16 -04:00
mpartRespBytes , err := io . ReadAll ( rec . Result ( ) . Body )
2016-10-01 11:23:26 -04:00
if err != nil {
t . Fatalf ( "[%s] Failed to read NewMultipartUpload response <ERROR> %v" , instanceType , err )
}
err = xml . Unmarshal ( mpartRespBytes , & mpartResp )
if err != nil {
t . Fatalf ( "[%s] Failed to unmarshal NewMultipartUpload response <ERROR> %v" , instanceType , err )
}
noAPIErr := APIError { }
missingDateHeaderErr := getAPIError ( ErrMissingDateHeader )
2021-09-09 17:37:07 -04:00
internalErr := getAPIError ( ErrBadRequest )
2016-10-01 11:23:26 -04:00
testCases := [ ] struct {
fault Fault
expectedErr APIError
} {
{ BadSignature , missingDateHeaderErr } ,
{ None , noAPIErr } ,
{ TooBigDecodedLength , internalErr } ,
}
for i , test := range testCases {
rec = httptest . NewRecorder ( )
2020-07-20 15:52:49 -04:00
req , err = newTestStreamingSignedRequest ( http . MethodPut ,
2016-10-01 11:23:26 -04:00
getPutObjectPartURL ( "" , bucketName , testObject , mpartResp . UploadID , "1" ) ,
2016-12-26 13:21:23 -05:00
5 , 1 , bytes . NewReader ( [ ] byte ( "hello" ) ) , credentials . AccessKey , credentials . SecretKey )
2016-10-31 02:32:46 -04:00
if err != nil {
t . Fatalf ( "Failed to create new streaming signed HTTP request: <ERROR> %v." , err )
}
2016-10-01 11:23:26 -04:00
switch test . fault {
case BadSignature :
// Reset date field in header to make streaming signature fail.
req . Header . Set ( "x-amz-date" , "" )
case TooBigDecodedLength :
// Set decoded length to a large value out of int64 range to simulate parse failure.
req . Header . Set ( "x-amz-decoded-content-length" , "9999999999999999999999" )
}
apiRouter . ServeHTTP ( rec , req )
if test . expectedErr != noAPIErr {
2022-09-19 14:05:16 -04:00
errBytes , err := io . ReadAll ( rec . Result ( ) . Body )
2016-10-01 11:23:26 -04:00
if err != nil {
t . Fatalf ( "Test %d %s Failed to read error response from upload part request %s/%s: <ERROR> %v" ,
i + 1 , instanceType , bucketName , testObject , err )
}
var errXML APIErrorResponse
err = xml . Unmarshal ( errBytes , & errXML )
if err != nil {
t . Fatalf ( "Test %d %s Failed to unmarshal error response from upload part request %s/%s: <ERROR> %v" ,
i + 1 , instanceType , bucketName , testObject , err )
}
if test . expectedErr . Code != errXML . Code {
t . Errorf ( "Test %d %s expected to fail with error %s, but received %s" , i + 1 , instanceType ,
test . expectedErr . Code , errXML . Code )
}
2023-03-06 11:56:10 -05:00
} else if rec . Code != http . StatusOK {
t . Errorf ( "Test %d %s expected to succeed, but failed with HTTP status code %d" ,
i + 1 , instanceType , rec . Code )
2016-10-01 11:23:26 -04:00
}
}
}
2016-10-07 11:02:37 -04:00
// TestAPIPutObjectPartHandler - Tests validate the response of PutObjectPart HTTP handler
2022-08-26 15:52:29 -04:00
//
// for variety of inputs.
2016-10-07 11:02:37 -04:00
func TestAPIPutObjectPartHandler ( t * testing . T ) {
2016-11-07 00:53:50 -05:00
defer DetectTestLeak ( t ) ( )
2021-01-05 23:08:35 -05:00
ExecExtendedObjectLayerAPITest ( t , testAPIPutObjectPartHandler , [ ] string { "PutObjectPart" } )
2016-10-01 11:23:26 -04:00
}
func testAPIPutObjectPartHandler ( obj ObjectLayer , instanceType , bucketName string , apiRouter http . Handler ,
2022-01-02 12:15:06 -05:00
credentials auth . Credentials , t * testing . T ,
) {
2016-10-01 11:23:26 -04:00
// Initiate Multipart upload for testing PutObjectPartHandler.
testObject := "testobject"
2018-09-10 12:42:43 -04:00
var opts ObjectOptions
2016-10-07 11:02:37 -04:00
// PutObjectPart API HTTP Handler has to be tested in isolation,
// that is without any other handler being registered,
// That's why NewMultipartUpload is initiated using ObjectLayer.
2022-08-29 19:57:16 -04:00
res , err := obj . NewMultipartUpload ( context . Background ( ) , bucketName , testObject , opts )
2016-10-01 11:23:26 -04:00
if err != nil {
2016-10-07 11:02:37 -04:00
// Failed to create NewMultipartUpload, abort.
2019-04-09 14:39:42 -04:00
t . Fatalf ( "MinIO %s : <ERROR> %s" , instanceType , err )
2016-10-01 11:23:26 -04:00
}
2022-08-29 19:57:16 -04:00
uploadID := res . UploadID
2016-10-01 11:23:26 -04:00
2016-10-07 14:16:11 -04:00
uploadIDCopy := uploadID
2016-10-01 11:23:26 -04:00
// SignatureMismatch for various signing types
testCases := [ ] struct {
2016-10-14 23:52:46 -04:00
objectName string
2022-08-29 19:57:16 -04:00
content string
2016-10-14 23:52:46 -04:00
partNumber string
fault Fault
accessKey string
secretKey string
2022-08-29 19:57:16 -04:00
expectedAPIError APIErrorCode
2016-10-01 11:23:26 -04:00
} {
2016-10-07 11:02:37 -04:00
// Success case.
2022-08-29 19:57:16 -04:00
0 : {
2016-10-14 23:52:46 -04:00
objectName : testObject ,
2022-08-29 19:57:16 -04:00
content : "hello" ,
2016-10-14 23:52:46 -04:00
partNumber : "1" ,
fault : None ,
2016-12-26 13:21:23 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
2016-10-14 23:52:46 -04:00
2022-08-29 19:57:16 -04:00
expectedAPIError : - 1 ,
2016-10-07 11:02:37 -04:00
} ,
// Case where part number is invalid.
2022-08-29 19:57:16 -04:00
1 : {
2016-10-14 23:52:46 -04:00
objectName : testObject ,
2022-08-29 19:57:16 -04:00
content : "hello" ,
2016-10-14 23:52:46 -04:00
partNumber : "9999999999999999999" ,
fault : None ,
2016-12-26 13:21:23 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
2016-10-14 23:52:46 -04:00
2022-08-29 19:57:16 -04:00
expectedAPIError : ErrInvalidPart ,
2016-10-07 11:02:37 -04:00
} ,
// Case where the part number has exceeded the max allowed parts in an upload.
2022-08-29 19:57:16 -04:00
2 : {
2016-10-14 23:52:46 -04:00
objectName : testObject ,
2022-08-29 19:57:16 -04:00
content : "hello" ,
2017-03-03 13:14:17 -05:00
partNumber : strconv . Itoa ( globalMaxPartID + 1 ) ,
2016-10-14 23:52:46 -04:00
fault : None ,
2016-12-26 13:21:23 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
2016-10-14 23:52:46 -04:00
2022-08-29 19:57:16 -04:00
expectedAPIError : ErrInvalidMaxParts ,
2016-10-07 11:02:37 -04:00
} ,
// Case where the content length is not set in the HTTP request.
2022-08-29 19:57:16 -04:00
3 : {
2016-10-14 23:52:46 -04:00
objectName : testObject ,
2022-08-29 19:57:16 -04:00
content : "hello" ,
2016-10-14 23:52:46 -04:00
partNumber : "1" ,
fault : MissingContentLength ,
2016-12-26 13:21:23 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
2016-10-14 23:52:46 -04:00
2022-08-29 19:57:16 -04:00
expectedAPIError : ErrMissingContentLength ,
2016-10-07 11:02:37 -04:00
} ,
// case where the object size is set to a value greater than the max allowed size.
2022-08-29 19:57:16 -04:00
4 : {
2016-10-14 23:52:46 -04:00
objectName : testObject ,
2022-08-29 19:57:16 -04:00
content : "hello" ,
2016-10-14 23:52:46 -04:00
partNumber : "1" ,
fault : TooBigObject ,
2016-12-26 13:21:23 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
2016-10-14 23:52:46 -04:00
2022-08-29 19:57:16 -04:00
expectedAPIError : ErrEntityTooLarge ,
2016-10-07 11:02:37 -04:00
} ,
// case where a signature mismatch is introduced and the response is validated.
2022-08-29 19:57:16 -04:00
5 : {
2016-10-14 23:52:46 -04:00
objectName : testObject ,
2022-08-29 19:57:16 -04:00
content : "hello" ,
2016-10-14 23:52:46 -04:00
partNumber : "1" ,
fault : BadSignature ,
2016-12-26 13:21:23 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
2016-10-14 23:52:46 -04:00
2022-08-29 19:57:16 -04:00
expectedAPIError : ErrSignatureDoesNotMatch ,
2016-10-07 11:02:37 -04:00
} ,
// Case where incorrect checksum is set and the error response
// is asserted with the expected error response.
2022-08-29 19:57:16 -04:00
6 : {
2016-10-14 23:52:46 -04:00
objectName : testObject ,
2022-08-29 19:57:16 -04:00
content : "hello" ,
2016-10-14 23:52:46 -04:00
partNumber : "1" ,
fault : BadMD5 ,
2016-12-26 13:21:23 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
2016-10-14 23:52:46 -04:00
2022-08-29 19:57:16 -04:00
expectedAPIError : ErrInvalidDigest ,
2016-10-07 11:02:37 -04:00
} ,
// case where the a non-existent uploadID is set.
2022-08-29 19:57:16 -04:00
7 : {
2016-10-14 23:52:46 -04:00
objectName : testObject ,
2022-08-29 19:57:16 -04:00
content : "hello" ,
2016-10-14 23:52:46 -04:00
partNumber : "1" ,
fault : MissingUploadID ,
2016-12-26 13:21:23 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
2016-10-14 23:52:46 -04:00
2022-08-29 19:57:16 -04:00
expectedAPIError : ErrNoSuchUpload ,
2016-10-07 11:02:37 -04:00
} ,
2016-10-14 23:52:46 -04:00
// case with invalid AccessID.
// Forcing the signature check inside the handler to fail.
2022-08-29 19:57:16 -04:00
8 : {
2016-10-14 23:52:46 -04:00
objectName : testObject ,
2022-08-29 19:57:16 -04:00
content : "hello" ,
2016-10-14 23:52:46 -04:00
partNumber : "1" ,
fault : None ,
accessKey : "Invalid-AccessID" ,
2016-12-26 13:21:23 -05:00
secretKey : credentials . SecretKey ,
2016-10-14 23:52:46 -04:00
2022-08-29 19:57:16 -04:00
expectedAPIError : ErrInvalidAccessKeyID ,
2016-10-14 23:52:46 -04:00
} ,
2023-05-24 11:00:47 -04:00
// Case where part number is invalid.
9 : {
objectName : testObject ,
content : "hello" ,
partNumber : "0" ,
fault : None ,
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
expectedAPIError : ErrInvalidPart ,
} ,
10 : {
objectName : testObject ,
content : "hello" ,
partNumber : "-10" ,
fault : None ,
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
expectedAPIError : ErrInvalidPart ,
} ,
2016-10-14 23:52:46 -04:00
}
reqV2Str := "V2 Signed HTTP request"
reqV4Str := "V4 Signed HTTP request"
type inputReqRec struct {
req * http . Request
rec * httptest . ResponseRecorder
reqType string
2016-10-01 11:23:26 -04:00
}
for i , test := range testCases {
2016-10-07 11:02:37 -04:00
// Using sub-tests introduced in Go 1.7.
2022-08-29 19:57:16 -04:00
t . Run ( fmt . Sprintf ( "MinIO-%s-Test-%d." , instanceType , i ) , func ( t * testing . T ) {
// collection of input HTTP request, ResponseRecorder and request type.
// Used to make a collection of V4 and V4 HTTP request.
2016-10-14 23:52:46 -04:00
var reqV4 , reqV2 * http . Request
var recV4 , recV2 * httptest . ResponseRecorder
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
recV4 = httptest . NewRecorder ( )
recV2 = httptest . NewRecorder ( )
2016-10-07 11:02:37 -04:00
// setting a non-existent uploadID.
// deliberately introducing the invalid value to be able to assert the response with the expected error response.
if test . fault == MissingUploadID {
uploadID = "upload1"
2016-10-01 11:23:26 -04:00
}
2016-10-07 11:02:37 -04:00
// constructing a v4 signed HTTP request.
2020-07-20 15:52:49 -04:00
reqV4 , err = newTestSignedRequestV4 ( http . MethodPut ,
2016-10-07 11:02:37 -04:00
getPutObjectPartURL ( "" , bucketName , test . objectName , uploadID , test . partNumber ) ,
2022-08-29 19:57:16 -04:00
int64 ( len ( test . content ) ) , bytes . NewReader ( [ ] byte ( test . content ) ) , test . accessKey , test . secretKey , nil )
2016-10-01 11:23:26 -04:00
if err != nil {
2016-10-14 23:52:46 -04:00
t . Fatalf ( "Failed to create a signed V4 request to upload part for %s/%s: <ERROR> %v" ,
2016-10-07 11:02:37 -04:00
bucketName , test . objectName , err )
2016-10-01 11:23:26 -04:00
}
2016-10-14 23:52:46 -04:00
// Verify response of the V2 signed HTTP request.
// construct HTTP request for PutObject Part Object endpoint.
2020-07-20 15:52:49 -04:00
reqV2 , err = newTestSignedRequestV2 ( http . MethodPut ,
2016-10-14 23:52:46 -04:00
getPutObjectPartURL ( "" , bucketName , test . objectName , uploadID , test . partNumber ) ,
2022-08-29 19:57:16 -04:00
int64 ( len ( test . content ) ) , bytes . NewReader ( [ ] byte ( test . content ) ) , test . accessKey , test . secretKey , nil )
2016-10-14 23:52:46 -04:00
if err != nil {
2022-08-29 19:57:16 -04:00
t . Fatalf ( "Test %d %s Failed to create a V2 signed request to upload part for %s/%s: <ERROR> %v" , i , instanceType ,
2016-10-14 23:52:46 -04:00
bucketName , test . objectName , err )
2016-10-01 11:23:26 -04:00
}
2016-10-07 11:02:37 -04:00
2016-10-14 23:52:46 -04:00
// collection of input HTTP request, ResponseRecorder and request type.
reqRecs := [ ] inputReqRec {
{
req : reqV4 ,
rec : recV4 ,
reqType : reqV4Str ,
} ,
{
req : reqV2 ,
rec : recV2 ,
reqType : reqV2Str ,
} ,
}
2016-10-07 11:02:37 -04:00
2016-10-14 23:52:46 -04:00
for _ , reqRec := range reqRecs {
// Response recorder to record the response of the handler.
rec := reqRec . rec
// HTTP request used to call the handler.
req := reqRec . req
// HTTP request type string for V4/V2 requests.
reqType := reqRec . reqType
2022-08-29 19:57:16 -04:00
// Clone so we don't retain values we do not want.
req . Header = req . Header . Clone ( )
2016-10-14 23:52:46 -04:00
// introduce faults in the request.
// deliberately introducing the invalid value to be able to assert the response with the expected error response.
switch test . fault {
case MissingContentLength :
req . ContentLength = - 1
// Setting the content length to a value greater than the max allowed size of a part.
// Used in test case 4.
case TooBigObject :
2017-03-03 13:14:17 -05:00
req . ContentLength = globalMaxObjectSize + 1
2016-10-14 23:52:46 -04:00
// Malformed signature.
// Used in test case 6.
case BadSignature :
req . Header . Set ( "authorization" , req . Header . Get ( "authorization" ) + "a" )
// Setting an invalid Content-MD5 to force a Md5 Mismatch error.
// Used in tesr case 7.
case BadMD5 :
req . Header . Set ( "Content-MD5" , "badmd5" )
2016-10-07 11:02:37 -04:00
}
2016-10-14 23:52:46 -04:00
// invoke the PutObjectPart HTTP handler.
apiRouter . ServeHTTP ( rec , req )
// validate the error response.
2022-08-29 19:57:16 -04:00
want := getAPIError ( test . expectedAPIError )
if test . expectedAPIError == - 1 {
want . HTTPStatusCode = 200
want . Code = "<no error>"
want . Description = "<no error>"
}
if rec . Code != http . StatusOK {
2016-10-14 23:52:46 -04:00
var errBytes [ ] byte
// read the response body.
2022-09-19 14:05:16 -04:00
errBytes , err = io . ReadAll ( rec . Result ( ) . Body )
2016-10-14 23:52:46 -04:00
if err != nil {
t . Fatalf ( "%s, Failed to read error response from upload part request \"%s\"/\"%s\": <ERROR> %v." ,
reqType , bucketName , test . objectName , err )
}
// parse the XML error response.
var errXML APIErrorResponse
err = xml . Unmarshal ( errBytes , & errXML )
if err != nil {
t . Fatalf ( "%s, Failed to unmarshal error response from upload part request \"%s\"/\"%s\": <ERROR> %v." ,
reqType , bucketName , test . objectName , err )
}
2016-10-26 11:46:14 -04:00
// Validate whether the error has occurred for the expected reason.
2022-08-29 19:57:16 -04:00
if want . Code != errXML . Code {
t . Errorf ( "%s, Expected to fail with error \"%s\", but received \"%s\": %q." ,
reqType , want . Code , errXML . Code , errXML . Message )
2016-10-14 23:52:46 -04:00
}
// Validate the HTTP response status code with the expected one.
2022-08-29 19:57:16 -04:00
if want . HTTPStatusCode != rec . Code {
t . Errorf ( "%s, Expected the HTTP response status code to be %d, got %d." , reqType , want . HTTPStatusCode , rec . Code )
2016-10-14 23:52:46 -04:00
}
2022-08-29 19:57:16 -04:00
} else if want . HTTPStatusCode != http . StatusOK {
t . Errorf ( "got 200 ok, want %d" , rec . Code )
2016-10-07 11:02:37 -04:00
}
}
} )
2016-10-01 11:23:26 -04:00
}
2016-10-07 14:16:11 -04:00
// Test for Anonymous/unsigned http request.
2020-07-20 15:52:49 -04:00
anonReq , err := newTestRequest ( http . MethodPut , getPutObjectPartURL ( "" , bucketName , testObject , uploadIDCopy , "1" ) ,
2016-10-07 14:16:11 -04:00
int64 ( len ( "hello" ) ) , bytes . NewReader ( [ ] byte ( "hello" ) ) )
if err != nil {
2019-04-09 14:39:42 -04:00
t . Fatalf ( "MinIO %s: Failed to create an anonymous request for %s/%s: <ERROR> %v" ,
2016-10-07 14:16:11 -04:00
instanceType , bucketName , testObject , err )
}
// ExecObjectLayerAPIAnonTest - Calls the HTTP API handler using the anonymous request, validates the ErrAccessDeniedResponse,
// sets the bucket policy using the policy statement generated from `getWriteOnlyObjectStatement` so that the
// unsigned request goes through and its validated again.
2018-04-24 18:53:30 -04:00
ExecObjectLayerAPIAnonTest ( t , obj , "TestAPIPutObjectPartHandler" , bucketName , testObject , instanceType , apiRouter , anonReq , getAnonWriteOnlyObjectPolicy ( bucketName , testObject ) )
2016-10-07 14:16:11 -04:00
2016-10-07 11:02:37 -04:00
// HTTP request for testing when `ObjectLayer` is set to `nil`.
2016-10-06 16:34:33 -04:00
// There is no need to use an existing bucket and valid input for creating the request
// since the `objectLayer==nil` check is performed before any other checks inside the handlers.
// The only aim is to generate an HTTP request in a way that the relevant/registered end point is evoked/called.
nilBucket := "dummy-bucket"
nilObject := "dummy-object"
2016-10-01 11:23:26 -04:00
2020-07-20 15:52:49 -04:00
nilReq , err := newTestSignedRequestV4 ( http . MethodPut , getPutObjectPartURL ( "" , nilBucket , nilObject , "0" , "0" ) ,
2018-09-20 22:22:09 -04:00
0 , bytes . NewReader ( [ ] byte ( "testNilObjLayer" ) ) , "" , "" , nil )
2016-10-01 11:23:26 -04:00
if err != nil {
2019-04-09 14:39:42 -04:00
t . Errorf ( "MinIO %s: Failed to create http request for testing the response when object Layer is set to `nil`." , instanceType )
2016-10-01 11:23:26 -04:00
}
2016-10-06 16:34:33 -04:00
// execute the object layer set to `nil` test.
// `ExecObjectLayerAPINilTest` manages the operation.
ExecObjectLayerAPINilTest ( t , nilBucket , nilObject , instanceType , apiRouter , nilReq )
}
2016-10-07 11:02:37 -04:00
// TestAPIListObjectPartsHandlerPreSign - Tests validate the response of ListObjectParts HTTP handler
2022-08-26 15:52:29 -04:00
//
// when signature type of the HTTP request is `Presigned`.
2016-10-07 11:02:37 -04:00
func TestAPIListObjectPartsHandlerPreSign ( t * testing . T ) {
2016-11-07 00:53:50 -05:00
defer DetectTestLeak ( t ) ( )
2016-10-07 11:02:37 -04:00
ExecObjectLayerAPITest ( t , testAPIListObjectPartsHandlerPreSign ,
[ ] string { "PutObjectPart" , "NewMultipart" , "ListObjectParts" } )
2016-10-01 11:23:26 -04:00
}
2016-10-03 11:54:57 -04:00
2016-10-04 01:05:33 -04:00
func testAPIListObjectPartsHandlerPreSign ( obj ObjectLayer , instanceType , bucketName string , apiRouter http . Handler ,
2022-04-13 15:00:11 -04:00
credentials auth . Credentials , t * testing . T ,
) {
2016-10-04 01:05:33 -04:00
testObject := "testobject"
rec := httptest . NewRecorder ( )
2020-07-20 15:52:49 -04:00
req , err := newTestSignedRequestV4 ( http . MethodPost , getNewMultipartURL ( "" , bucketName , testObject ) ,
2018-09-20 22:22:09 -04:00
0 , nil , credentials . AccessKey , credentials . SecretKey , nil )
2016-10-04 01:05:33 -04:00
if err != nil {
t . Fatalf ( "[%s] - Failed to create a signed request to initiate multipart upload for %s/%s: <ERROR> %v" ,
instanceType , bucketName , testObject , err )
}
apiRouter . ServeHTTP ( rec , req )
2024-01-18 02:03:17 -05:00
// Get uploadID of the multipart upload initiated.
2016-10-04 01:05:33 -04:00
var mpartResp InitiateMultipartUploadResponse
2022-09-19 14:05:16 -04:00
mpartRespBytes , err := io . ReadAll ( rec . Result ( ) . Body )
2016-10-04 01:05:33 -04:00
if err != nil {
t . Fatalf ( "[%s] Failed to read NewMultipartUpload response <ERROR> %v" , instanceType , err )
}
err = xml . Unmarshal ( mpartRespBytes , & mpartResp )
if err != nil {
t . Fatalf ( "[%s] Failed to unmarshal NewMultipartUpload response <ERROR> %v" , instanceType , err )
}
// Upload a part for listing purposes.
rec = httptest . NewRecorder ( )
2020-07-20 15:52:49 -04:00
req , err = newTestSignedRequestV4 ( http . MethodPut ,
2016-10-04 01:05:33 -04:00
getPutObjectPartURL ( "" , bucketName , testObject , mpartResp . UploadID , "1" ) ,
2018-09-20 22:22:09 -04:00
int64 ( len ( "hello" ) ) , bytes . NewReader ( [ ] byte ( "hello" ) ) , credentials . AccessKey , credentials . SecretKey , nil )
2016-10-04 01:05:33 -04:00
if err != nil {
t . Fatalf ( "[%s] - Failed to create a signed request to initiate multipart upload for %s/%s: <ERROR> %v" ,
instanceType , bucketName , testObject , err )
}
apiRouter . ServeHTTP ( rec , req )
if rec . Code != http . StatusOK {
t . Fatalf ( "[%s] - Failed to PutObjectPart bucket: %s object: %s HTTP status code: %d" ,
instanceType , bucketName , testObject , rec . Code )
}
rec = httptest . NewRecorder ( )
2020-07-20 15:52:49 -04:00
req , err = newTestRequest ( http . MethodGet ,
2016-10-04 01:05:33 -04:00
getListMultipartURLWithParams ( "" , bucketName , testObject , mpartResp . UploadID , "" , "" , "" ) ,
0 , nil )
if err != nil {
t . Fatalf ( "[%s] - Failed to create an unsigned request to list object parts for bucket %s, uploadId %s" ,
instanceType , bucketName , mpartResp . UploadID )
}
2017-09-24 17:20:12 -04:00
req . Header = http . Header { }
2016-12-26 13:21:23 -05:00
err = preSignV2 ( req , credentials . AccessKey , credentials . SecretKey , int64 ( 10 * 60 * 60 ) )
2016-11-11 00:57:15 -05:00
if err != nil {
t . Fatalf ( "[%s] - Failed to presignV2 an unsigned request to list object parts for bucket %s, uploadId %s" ,
instanceType , bucketName , mpartResp . UploadID )
}
apiRouter . ServeHTTP ( rec , req )
if rec . Code != http . StatusOK {
t . Errorf ( "Test %d %s expected to succeed but failed with HTTP status code %d" ,
1 , instanceType , rec . Code )
}
rec = httptest . NewRecorder ( )
2020-07-20 15:52:49 -04:00
req , err = newTestRequest ( http . MethodGet ,
2016-11-11 00:57:15 -05:00
getListMultipartURLWithParams ( "" , bucketName , testObject , mpartResp . UploadID , "" , "" , "" ) ,
0 , nil )
if err != nil {
t . Fatalf ( "[%s] - Failed to create an unsigned request to list object parts for bucket %s, uploadId %s" ,
instanceType , bucketName , mpartResp . UploadID )
}
2016-12-26 13:21:23 -05:00
err = preSignV4 ( req , credentials . AccessKey , credentials . SecretKey , int64 ( 10 * 60 * 60 ) )
2016-10-04 01:05:33 -04:00
if err != nil {
t . Fatalf ( "[%s] - Failed to presignV2 an unsigned request to list object parts for bucket %s, uploadId %s" ,
instanceType , bucketName , mpartResp . UploadID )
}
apiRouter . ServeHTTP ( rec , req )
if rec . Code != http . StatusOK {
t . Errorf ( "Test %d %s expected to succeed but failed with HTTP status code %d" ,
1 , instanceType , rec . Code )
}
}
2016-10-07 11:02:37 -04:00
// TestAPIListObjectPartsHandler - Tests validate the response of ListObjectParts HTTP handler
2022-08-26 15:52:29 -04:00
//
// for variety of success/failure cases.
2016-10-07 11:02:37 -04:00
func TestAPIListObjectPartsHandler ( t * testing . T ) {
2016-11-07 00:53:50 -05:00
defer DetectTestLeak ( t ) ( )
2021-01-05 23:08:35 -05:00
ExecExtendedObjectLayerAPITest ( t , testAPIListObjectPartsHandler , [ ] string { "ListObjectParts" } )
2016-10-04 01:05:33 -04:00
}
2016-10-03 11:54:57 -04:00
func testAPIListObjectPartsHandler ( obj ObjectLayer , instanceType , bucketName string , apiRouter http . Handler ,
2022-04-13 15:00:11 -04:00
credentials auth . Credentials , t * testing . T ,
) {
2016-10-03 11:54:57 -04:00
testObject := "testobject"
2018-09-10 12:42:43 -04:00
var opts ObjectOptions
2016-10-07 23:16:57 -04:00
// PutObjectPart API HTTP Handler has to be tested in isolation,
// that is without any other handler being registered,
// That's why NewMultipartUpload is initiated using ObjectLayer.
2022-08-29 19:57:16 -04:00
res , err := obj . NewMultipartUpload ( context . Background ( ) , bucketName , testObject , opts )
2016-10-03 11:54:57 -04:00
if err != nil {
2016-10-07 23:16:57 -04:00
// Failed to create NewMultipartUpload, abort.
2019-04-09 14:39:42 -04:00
t . Fatalf ( "MinIO %s : <ERROR> %s" , instanceType , err )
2016-10-03 11:54:57 -04:00
}
2022-08-29 19:57:16 -04:00
uploadID := res . UploadID
2016-10-07 23:16:57 -04:00
uploadIDCopy := uploadID
2016-10-03 11:54:57 -04:00
2016-10-07 23:16:57 -04:00
// create an object Part, will be used to test list object parts.
2018-11-14 20:36:41 -05:00
_ , err = obj . PutObjectPart ( context . Background ( ) , bucketName , testObject , uploadID , 1 , mustGetPutObjReader ( t , bytes . NewReader ( [ ] byte ( "hello" ) ) , int64 ( len ( "hello" ) ) , "5d41402abc4b2a76b9719d911017c592" , "" ) , opts )
2016-10-07 23:16:57 -04:00
if err != nil {
2019-04-09 14:39:42 -04:00
t . Fatalf ( "MinIO %s : %s." , instanceType , err )
2016-10-03 11:54:57 -04:00
}
2016-10-07 23:16:57 -04:00
// expected error types for invalid inputs to ListObjectParts handler.
2016-10-03 11:54:57 -04:00
noAPIErr := APIError { }
2016-10-07 23:16:57 -04:00
// expected error when the signature check fails.
2016-10-03 11:54:57 -04:00
signatureMismatchErr := getAPIError ( ErrSignatureDoesNotMatch )
2016-10-07 23:16:57 -04:00
// expected error the when the uploadID is invalid.
2016-10-03 11:54:57 -04:00
noSuchUploadErr := getAPIError ( ErrNoSuchUpload )
2016-10-07 23:16:57 -04:00
// expected error the part number marker use in the ListObjectParts request is invalid.
2016-10-03 11:54:57 -04:00
invalidPartMarkerErr := getAPIError ( ErrInvalidPartNumberMarker )
2016-10-07 23:16:57 -04:00
// expected error when the maximum number of parts requested to listed in invalid.
2016-10-03 11:54:57 -04:00
invalidMaxPartsErr := getAPIError ( ErrInvalidMaxParts )
2016-10-07 23:16:57 -04:00
2016-10-03 11:54:57 -04:00
testCases := [ ] struct {
fault Fault
partNumberMarker string
maxParts string
expectedErr APIError
} {
2016-10-07 23:16:57 -04:00
// Test case - 1.
// case where a signature mismatch is introduced and the response is validated.
{
fault : BadSignature ,
partNumberMarker : "" ,
maxParts : "" ,
expectedErr : signatureMismatchErr ,
} ,
// Test case - 2.
// Marker is set to invalid value of -1, error response is asserted.
{
fault : None ,
partNumberMarker : "-1" ,
maxParts : "" ,
expectedErr : invalidPartMarkerErr ,
} ,
// Test case - 3.
// Max Parts is set a negative value, error response is validated.
{
fault : None ,
partNumberMarker : "" ,
maxParts : "-1" ,
expectedErr : invalidMaxPartsErr ,
} ,
// Test case - 4.
// Invalid UploadID is set and the error response is validated.
{
fault : MissingUploadID ,
partNumberMarker : "" ,
maxParts : "" ,
expectedErr : noSuchUploadErr ,
} ,
2016-10-03 11:54:57 -04:00
}
2016-10-14 23:52:46 -04:00
// string to represent V2 signed HTTP request.
reqV2Str := "V2 Signed HTTP request"
// string to represent V4 signed HTTP request.
reqV4Str := "V4 Signed HTTP request"
// Collection of HTTP request and ResponseRecorder and request type string.
type inputReqRec struct {
req * http . Request
rec * httptest . ResponseRecorder
reqType string
}
2016-10-03 11:54:57 -04:00
for i , test := range testCases {
2016-10-14 23:52:46 -04:00
var reqV4 , reqV2 * http . Request
2016-10-07 23:16:57 -04:00
// Using sub-tests introduced in Go 1.7.
2019-04-09 14:39:42 -04:00
t . Run ( fmt . Sprintf ( "MinIO %s: Test case %d failed." , instanceType , i + 1 ) , func ( t * testing . T ) {
2016-10-14 23:52:46 -04:00
recV2 := httptest . NewRecorder ( )
recV4 := httptest . NewRecorder ( )
2016-10-07 23:16:57 -04:00
// setting a non-existent uploadID.
// deliberately introducing the invalid value to be able to assert the response with the expected error response.
if test . fault == MissingUploadID {
uploadID = "upload1"
2016-10-03 11:54:57 -04:00
}
2016-10-07 23:16:57 -04:00
// constructing a v4 signed HTTP request for ListMultipartUploads.
2020-07-20 15:52:49 -04:00
reqV4 , err = newTestSignedRequestV4 ( http . MethodGet ,
2016-10-07 23:16:57 -04:00
getListMultipartURLWithParams ( "" , bucketName , testObject , uploadID , test . maxParts , test . partNumberMarker , "" ) ,
2018-09-20 22:22:09 -04:00
0 , nil , credentials . AccessKey , credentials . SecretKey , nil )
2016-10-03 11:54:57 -04:00
if err != nil {
2016-10-14 23:52:46 -04:00
t . Fatalf ( "Failed to create a V4 signed request to list object parts for %s/%s: <ERROR> %v." ,
2016-10-07 23:16:57 -04:00
bucketName , testObject , err )
2016-10-03 11:54:57 -04:00
}
2016-10-14 23:52:46 -04:00
// Verify response of the V2 signed HTTP request.
// construct HTTP request for PutObject Part Object endpoint.
2020-07-20 15:52:49 -04:00
reqV2 , err = newTestSignedRequestV2 ( http . MethodGet ,
2016-10-14 23:52:46 -04:00
getListMultipartURLWithParams ( "" , bucketName , testObject , uploadID , test . maxParts , test . partNumberMarker , "" ) ,
2018-09-20 22:22:09 -04:00
0 , nil , credentials . AccessKey , credentials . SecretKey , nil )
2016-10-14 23:52:46 -04:00
if err != nil {
t . Fatalf ( "Failed to create a V2 signed request to list object parts for %s/%s: <ERROR> %v." ,
bucketName , testObject , err )
2016-10-03 11:54:57 -04:00
}
2016-10-14 23:52:46 -04:00
// collection of input HTTP request, ResponseRecorder and request type.
reqRecs := [ ] inputReqRec {
{
req : reqV4 ,
rec : recV4 ,
reqType : reqV4Str ,
} ,
{
req : reqV2 ,
rec : recV2 ,
reqType : reqV2Str ,
} ,
}
for _ , reqRec := range reqRecs {
// Response recorder to record the response of the handler.
rec := reqRec . rec
// HTTP request used to call the handler.
req := reqRec . req
// HTTP request type string for V4/V2 requests.
reqType := reqRec . reqType
// Malformed signature.
if test . fault == BadSignature {
req . Header . Set ( "authorization" , req . Header . Get ( "authorization" ) + "a" )
2016-10-07 23:16:57 -04:00
}
2016-10-14 23:52:46 -04:00
// invoke the PutObjectPart HTTP handler with the given HTTP request.
apiRouter . ServeHTTP ( rec , req )
// validate the error response.
if test . expectedErr != noAPIErr {
var errBytes [ ] byte
// read the response body.
2022-09-19 14:05:16 -04:00
errBytes , err = io . ReadAll ( rec . Result ( ) . Body )
2016-10-14 23:52:46 -04:00
if err != nil {
t . Fatalf ( "%s,Failed to read error response list object parts request %s/%s: <ERROR> %v" , reqType , bucketName , testObject , err )
}
// parse the error response.
var errXML APIErrorResponse
err = xml . Unmarshal ( errBytes , & errXML )
if err != nil {
t . Fatalf ( "%s, Failed to unmarshal error response from list object partsest %s/%s: <ERROR> %v" ,
reqType , bucketName , testObject , err )
}
2016-10-26 11:46:14 -04:00
// Validate whether the error has occurred for the expected reason.
2016-10-14 23:52:46 -04:00
if test . expectedErr . Code != errXML . Code {
t . Errorf ( "%s, Expected to fail with %s but received %s" ,
reqType , test . expectedErr . Code , errXML . Code )
}
// in case error is not expected response status should be 200OK.
2023-03-06 11:56:10 -05:00
} else if rec . Code != http . StatusOK {
t . Errorf ( "%s, Expected to succeed with response HTTP status 200OK, but failed with HTTP status code %d." , reqType , rec . Code )
2016-10-07 23:16:57 -04:00
}
2016-10-03 11:54:57 -04:00
}
2016-10-07 23:16:57 -04:00
} )
}
// Test for Anonymous/unsigned http request.
2020-07-20 15:52:49 -04:00
anonReq , err := newTestRequest ( http . MethodGet ,
2016-10-07 23:16:57 -04:00
getListMultipartURLWithParams ( "" , bucketName , testObject , uploadIDCopy , "" , "" , "" ) , 0 , nil )
if err != nil {
2019-04-09 14:39:42 -04:00
t . Fatalf ( "MinIO %s: Failed to create an anonymous request for %s/%s: <ERROR> %v" ,
2016-10-07 23:16:57 -04:00
instanceType , bucketName , testObject , err )
2016-10-03 11:54:57 -04:00
}
2016-10-06 16:34:33 -04:00
2016-10-07 23:16:57 -04:00
// ExecObjectLayerAPIAnonTest - Calls the HTTP API handler using the anonymous request, validates the ErrAccessDeniedResponse,
// sets the bucket policy using the policy statement generated from `getWriteOnlyObjectStatement` so that the
// unsigned request goes through and its validated again.
2018-04-24 18:53:30 -04:00
ExecObjectLayerAPIAnonTest ( t , obj , "TestAPIListObjectPartsHandler" , bucketName , testObject , instanceType , apiRouter , anonReq , getAnonWriteOnlyObjectPolicy ( bucketName , testObject ) )
2016-10-07 23:16:57 -04:00
2016-10-06 16:34:33 -04:00
// HTTP request for testing when `objectLayer` is set to `nil`.
// There is no need to use an existing bucket and valid input for creating the request
// since the `objectLayer==nil` check is performed before any other checks inside the handlers.
// The only aim is to generate an HTTP request in a way that the relevant/registered end point is evoked/called.
nilBucket := "dummy-bucket"
nilObject := "dummy-object"
2020-07-20 15:52:49 -04:00
nilReq , err := newTestSignedRequestV4 ( http . MethodGet ,
2016-10-06 16:34:33 -04:00
getListMultipartURLWithParams ( "" , nilBucket , nilObject , "dummy-uploadID" , "0" , "0" , "" ) ,
2018-09-20 22:22:09 -04:00
0 , nil , "" , "" , nil )
2016-10-06 16:34:33 -04:00
if err != nil {
2019-04-09 14:39:42 -04:00
t . Errorf ( "MinIO %s:Failed to create http request for testing the response when object Layer is set to `nil`." , instanceType )
2016-10-06 16:34:33 -04:00
}
// execute the object layer set to `nil` test.
// `ExecObjectLayerAPINilTest` sets the Object Layer to `nil` and calls the handler.
ExecObjectLayerAPINilTest ( t , nilBucket , nilObject , instanceType , apiRouter , nilReq )
2016-10-03 11:54:57 -04:00
}