diff --git a/cmd/object-handlers_test.go b/cmd/object-handlers_test.go index 793c774b5..9e7837188 100644 --- a/cmd/object-handlers_test.go +++ b/cmd/object-handlers_test.go @@ -1731,6 +1731,182 @@ func testAPICompleteMultipartHandler(obj ObjectLayer, instanceType, bucketName s ExecObjectLayerAPINilTest(t, nilBucket, nilObject, instanceType, apiRouter, nilReq) } +// 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, + credentials credential, t *testing.T) { + + var err error + // register event notifier. + err = initEventNotifier(obj) + + if err != nil { + t.Fatal("Notifier initialization failed.") + } + + // Calculates MD5 sum of the given byte array. + findMD5 := func(toBeHashed []byte) string { + hasher := md5.New() + hasher.Write(toBeHashed) + return hex.EncodeToString(hasher.Sum(nil)) + } + + // object used for the test. + objectName := "test-object-new-multipart" + + // uploadID obtained from NewMultipart upload. + var uploadID string + // upload IDs collected. + var uploadIDs []string + + for i := 0; i < 2; i++ { + // initiate new multipart uploadID. + uploadID, err = obj.NewMultipartUpload(bucketName, objectName, nil) + if err != nil { + // Failed to create NewMultipartUpload, abort. + t.Fatalf("Minio %s : %s", instanceType, err) + } + + uploadIDs = append(uploadIDs, uploadID) + } + + // Parts with size greater than 5 MB. + // Generating a 6MB byte array. + validPart := bytes.Repeat([]byte("abcdef"), 1024*1024) + validPartMD5 := findMD5(validPart) + // 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 + intputDataSize int64 + }{ + // 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"))}, + // Part with size larger than 5Mb. + {bucketName, objectName, uploadIDs[0], 5, string(validPart), validPartMD5, int64(len(string(validPart)))}, + {bucketName, objectName, uploadIDs[0], 6, string(validPart), validPartMD5, int64(len(string(validPart)))}, + + // Part with size larger than 5Mb. + // Parts uploaded for anonymous/unsigned API handler test. + {bucketName, objectName, uploadIDs[1], 1, string(validPart), validPartMD5, int64(len(string(validPart)))}, + {bucketName, objectName, uploadIDs[1], 2, string(validPart), validPartMD5, int64(len(string(validPart)))}, + } + // Iterating over createPartCases to generate multipart chunks. + for _, part := range parts { + _, err = obj.PutObjectPart(part.bucketName, part.objName, part.uploadID, part.PartID, part.intputDataSize, + bytes.NewBufferString(part.inputReaderData), part.inputMd5, "") + 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], + accessKey: credentials.AccessKeyID, + secretKey: credentials.SecretAccessKey, + expectedRespStatus: http.StatusNoContent, + }, + // Test case - 2. + // Abort non-existng upload ID. + { + bucket: bucketName, + object: objectName, + uploadID: "nonexistent-upload-id", + accessKey: credentials.AccessKeyID, + secretKey: credentials.SecretAccessKey, + expectedRespStatus: http.StatusNotFound, + }, + // Test case - 3. + // Abort with unknown Access key. + { + bucket: bucketName, + object: objectName, + uploadID: uploadIDs[0], + accessKey: "Invalid-AccessID", + secretKey: credentials.SecretAccessKey, + expectedRespStatus: http.StatusForbidden, + }, + } + + for i, testCase := range testCases { + var req *http.Request + // Indicating that all parts are uploaded and initiating abortMultipartUpload. + req, err = newTestSignedRequestV4("DELETE", getAbortMultipartUploadURL("", testCase.bucket, testCase.object, testCase.uploadID), + 0, nil, testCase.accessKey, testCase.secretKey) + if err != nil { + t.Fatalf("Failed to create HTTP request for AbortMultipartUpload: %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 { + t.Errorf("Case %d: Minio %s: Expected the response status to be `%d`, but instead found `%d`", i+1, instanceType, testCase.expectedRespStatus, rec.Code) + } + } + + // create unsigned HTTP request for Abort multipart upload. + anonReq, err := newTestRequest("DELETE", getAbortMultipartUploadURL("", bucketName, objectName, uploadIDs[1]), + 0, nil) + if err != nil { + t.Fatalf("Minio %s: Failed to create an anonymous request for %s/%s: %v", + 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. + ExecObjectLayerAPIAnonTest(t, "TestAPIAbortMultipartHandler", bucketName, objectName, instanceType, + apiRouter, anonReq, getWriteOnlyObjectStatement) + + // 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" + + nilReq, err := newTestSignedRequestV4("DELETE", getAbortMultipartUploadURL("", nilBucket, nilObject, "dummy-uploadID"), + 0, nil, "", "") + + if err != nil { + t.Errorf("Minio %s: Failed to create HTTP request for testing the response when object Layer is set to `nil`.", instanceType) + } + // execute the object layer set to `nil` test. + // `ExecObjectLayerAPINilTest` manages the operation. + ExecObjectLayerAPINilTest(t, nilBucket, nilObject, instanceType, apiRouter, nilReq) +} + // Wrapper for calling Delete Object API handler tests for both XL multiple disks and FS single drive setup. func TestAPIDeleteObjectHandler(t *testing.T) { defer DetectTestLeak(t)() diff --git a/cmd/test-utils_test.go b/cmd/test-utils_test.go index cd0092c76..e68ac8c81 100644 --- a/cmd/test-utils_test.go +++ b/cmd/test-utils_test.go @@ -1745,7 +1745,7 @@ func ExecObjectLayerAPIAnonTest(t *testing.T, testName, bucketName, objectName, var expectedHTTPStatus int // expectedHTTPStatus returns 204 (http.StatusNoContent) on success. - if testName == "TestAPIDeleteObjectHandler" { + if testName == "TestAPIDeleteObjectHandler" || testName == "TestAPIAbortMultipartHandler" { expectedHTTPStatus = http.StatusNoContent } else if strings.Contains(testName, "BucketPolicyHandler") || testName == "ListBucketsHandler" { // BucketPolicyHandlers and `ListBucketsHandler` doesn't support anonymous request, policy changes should allow unsigned requests. @@ -1993,6 +1993,9 @@ func registerBucketLevelFunc(bucket *router.Router, api objectAPIHandlers, apiFu case "CompleteMultipart": // Register Complete Multipart Upload handler. bucket.Methods("POST").Path("/{object:.+}").HandlerFunc(api.CompleteMultipartUploadHandler).Queries("uploadId", "{uploadId:.*}") + case "AbortMultipart": + // Register AbortMultipart Handler. + bucket.Methods("DELETE").Path("/{object:.+}").HandlerFunc(api.AbortMultipartUploadHandler).Queries("uploadId", "{uploadId:.*}") case "GetBucketNotification": // Register GetBucketNotification Handler. bucket.Methods("GET").HandlerFunc(api.GetBucketNotificationHandler).Queries("notification", "")