diff --git a/cmd/bucket-handlers_test.go b/cmd/bucket-handlers_test.go index 3febf730e..24fb82783 100644 --- a/cmd/bucket-handlers_test.go +++ b/cmd/bucket-handlers_test.go @@ -19,8 +19,10 @@ package cmd import ( "bytes" "encoding/xml" + "io/ioutil" "net/http" "net/http/httptest" + "strconv" "testing" ) @@ -607,3 +609,165 @@ func testListBucketsHandler(obj ObjectLayer, instanceType, bucketName string, ap // `ExecObjectLayerAPINilTest` manages the operation. ExecObjectLayerAPINilTest(t, "", "", instanceType, apiRouter, nilReq) } + +// Wrapper for calling DeleteMultipleObjects HTTP handler tests for both XL multiple disks and single node setup. +func TestAPIDeleteMultipleObjectsHandler(t *testing.T) { + ExecObjectLayerAPITest(t, testAPIDeleteMultipleObjectsHandler, []string{"DeleteMultipleObjects"}) +} + +func testAPIDeleteMultipleObjectsHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler, + credentials credential, t *testing.T) { + initBucketPolicies(obj) + + var err error + // register event notifier. + err = initEventNotifier(obj) + if err != nil { + t.Fatal("Notifier initialization failed.") + } + + contentBytes := []byte("hello") + sha256sum := "" + var objectNames []string + for i := 0; i < 10; i++ { + objectName := "test-object-" + strconv.Itoa(i) + // uploading the object. + _, err = obj.PutObject(bucketName, objectName, int64(len(contentBytes)), bytes.NewBuffer(contentBytes), + make(map[string]string), sha256sum) + // if object upload fails stop the test. + if err != nil { + t.Fatalf("Put Object %d: Error uploading object: %v", i, err) + } + + // object used for the test. + objectNames = append(objectNames, objectName) + } + + getObjectIdentifierList := func(objectNames []string) (objectIdentifierList []ObjectIdentifier) { + for _, objectName := range objectNames { + objectIdentifierList = append(objectIdentifierList, ObjectIdentifier{objectName}) + } + + return objectIdentifierList + } + + requestList := []DeleteObjectsRequest{ + {Quiet: false, Objects: getObjectIdentifierList(objectNames[:5])}, + {Quiet: true, Objects: getObjectIdentifierList(objectNames[5:])}, + } + + // generate multi objects delete response. + successRequest0 := encodeResponse(requestList[0]) + successResponse0 := generateMultiDeleteResponse(requestList[0].Quiet, requestList[0].Objects, nil) + encodedSuccessResponse0 := encodeResponse(successResponse0) + + successRequest1 := encodeResponse(requestList[1]) + successResponse1 := generateMultiDeleteResponse(requestList[1].Quiet, requestList[1].Objects, nil) + encodedSuccessResponse1 := encodeResponse(successResponse1) + + // generate multi objects delete response for errors. + // errorRequest := encodeResponse(requestList[1]) + errorResponse := generateMultiDeleteResponse(requestList[1].Quiet, requestList[1].Objects, nil) + encodedErrorResponse := encodeResponse(errorResponse) + + testCases := []struct { + bucket string + objects []byte + accessKey string + secretKey string + expectedContent []byte + expectedRespStatus int + }{ + // Test case - 1. + // Delete objects with invalid access key. + { + bucket: bucketName, + objects: successRequest0, + accessKey: "Invalid-AccessID", + secretKey: credentials.SecretAccessKey, + expectedContent: nil, + expectedRespStatus: http.StatusForbidden, + }, + // Test case - 2. + // Delete valid objects with quiet flag off. + { + bucket: bucketName, + objects: successRequest0, + accessKey: credentials.AccessKeyID, + secretKey: credentials.SecretAccessKey, + expectedContent: encodedSuccessResponse0, + expectedRespStatus: http.StatusOK, + }, + // Test case - 3. + // Delete valid objects with quiet flag on. + { + bucket: bucketName, + objects: successRequest1, + accessKey: credentials.AccessKeyID, + secretKey: credentials.SecretAccessKey, + expectedContent: encodedSuccessResponse1, + expectedRespStatus: http.StatusOK, + }, + // Test case - 4. + // Delete previously deleted objects. + { + bucket: bucketName, + objects: successRequest1, + accessKey: credentials.AccessKeyID, + secretKey: credentials.SecretAccessKey, + expectedContent: encodedErrorResponse, + expectedRespStatus: http.StatusOK, + }, + } + + for i, testCase := range testCases { + var req *http.Request + var actualContent []byte + + // Indicating that all parts are uploaded and initiating completeMultipartUpload. + req, err = newTestSignedRequestV4("POST", getDeleteMultipleObjectsURL("", bucketName), + int64(len(testCase.objects)), bytes.NewReader(testCase.objects), testCase.accessKey, testCase.secretKey) + if err != nil { + t.Fatalf("Failed to create HTTP request for DeleteMultipleObjects: %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) + } + + // read the response body. + actualContent, err = ioutil.ReadAll(rec.Body) + if err != nil { + t.Fatalf("Test %d : Minio %s: Failed parsing response body: %v", i+1, instanceType, err) + } + + // Verify whether the bucket obtained object is same as the one created. + if testCase.expectedContent != nil && !bytes.Equal(testCase.expectedContent, actualContent) { + t.Errorf("Test %d : Minio %s: Object content differs from expected value.", i+1, instanceType) + } + } + + // Currently anonymous user cannot delete multiple objects in Minio server, hence no test case is required. + + // 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 completeMultipartUpload. + nilBucket := "dummy-bucket" + nilObject := "" + + nilReq, err := newTestSignedRequestV4("POST", getDeleteMultipleObjectsURL("", nilBucket), 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) +} diff --git a/cmd/test-utils_test.go b/cmd/test-utils_test.go index 7ea72b799..f84456cb3 100644 --- a/cmd/test-utils_test.go +++ b/cmd/test-utils_test.go @@ -1416,6 +1416,13 @@ func getDeleteBucketURL(endPoint, bucketName string) string { return makeTestTargetURL(endPoint, bucketName, "", url.Values{}) } +// return URL for deleting the bucket. +func getDeleteMultipleObjectsURL(endPoint, bucketName string) string { + queryValue := url.Values{} + queryValue.Set("delete", "") + return makeTestTargetURL(endPoint, bucketName, "", queryValue) +} + // return URL For fetching location of the bucket. func getBucketLocationURL(endPoint, bucketName string) string { queryValue := url.Values{} @@ -1970,6 +1977,9 @@ func registerBucketLevelFunc(bucket *router.Router, api objectAPIHandlers, apiFu case "HeadBucket": // Register HeadBucket handler. bucket.Methods("HEAD").HandlerFunc(api.HeadBucketHandler) + case "DeleteMultipleObjects": + // Register DeleteMultipleObjects handler. + bucket.Methods("POST").HandlerFunc(api.DeleteMultipleObjectsHandler).Queries("delete", "") case "NewMultipart": // Register New Multipart upload handler. bucket.Methods("POST").Path("/{object:.+}").HandlerFunc(api.NewMultipartUploadHandler).Queries("uploads", "")