github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/cmd/bucket-lifecycle-handlers_test.go (about)

     1  // Copyright (c) 2015-2021 MinIO, Inc.
     2  //
     3  // This file is part of MinIO Object Storage stack
     4  //
     5  // This program is free software: you can redistribute it and/or modify
     6  // it under the terms of the GNU Affero General Public License as published by
     7  // the Free Software Foundation, either version 3 of the License, or
     8  // (at your option) any later version.
     9  //
    10  // This program is distributed in the hope that it will be useful
    11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13  // GNU Affero General Public License for more details.
    14  //
    15  // You should have received a copy of the GNU Affero General Public License
    16  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17  
    18  package cmd
    19  
    20  import (
    21  	"bytes"
    22  	"encoding/xml"
    23  	"net/http"
    24  	"net/http/httptest"
    25  	"testing"
    26  
    27  	"github.com/minio/minio/internal/auth"
    28  )
    29  
    30  // Test S3 Bucket lifecycle APIs with wrong credentials
    31  func TestBucketLifecycleWrongCredentials(t *testing.T) {
    32  	ExecObjectLayerAPITest(t, testBucketLifecycleHandlersWrongCredentials, []string{"GetBucketLifecycle", "PutBucketLifecycle", "DeleteBucketLifecycle"})
    33  }
    34  
    35  // Test for authentication
    36  func testBucketLifecycleHandlersWrongCredentials(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
    37  	credentials auth.Credentials, t *testing.T,
    38  ) {
    39  	// test cases with sample input and expected output.
    40  	testCases := []struct {
    41  		method     string
    42  		bucketName string
    43  		accessKey  string
    44  		secretKey  string
    45  		// Sent body
    46  		body []byte
    47  		// Expected response
    48  		expectedRespStatus int
    49  		lifecycleResponse  []byte
    50  		errorResponse      APIErrorResponse
    51  		shouldPass         bool
    52  	}{
    53  		// GET empty credentials
    54  		{
    55  			method: http.MethodGet, bucketName: bucketName,
    56  			accessKey:          "",
    57  			secretKey:          "",
    58  			expectedRespStatus: http.StatusForbidden,
    59  			lifecycleResponse:  []byte(""),
    60  			errorResponse: APIErrorResponse{
    61  				Resource: SlashSeparator + bucketName + SlashSeparator,
    62  				Code:     "AccessDenied",
    63  				Message:  "Access Denied.",
    64  			},
    65  			shouldPass: false,
    66  		},
    67  		// GET wrong credentials
    68  		{
    69  			method: http.MethodGet, bucketName: bucketName,
    70  			accessKey:          "abcd",
    71  			secretKey:          "abcd",
    72  			expectedRespStatus: http.StatusForbidden,
    73  			lifecycleResponse:  []byte(""),
    74  			errorResponse: APIErrorResponse{
    75  				Resource: SlashSeparator + bucketName + SlashSeparator,
    76  				Code:     "InvalidAccessKeyId",
    77  				Message:  "The Access Key Id you provided does not exist in our records.",
    78  			},
    79  			shouldPass: false,
    80  		},
    81  		// PUT empty credentials
    82  		{
    83  			method:             http.MethodPut,
    84  			bucketName:         bucketName,
    85  			accessKey:          "",
    86  			secretKey:          "",
    87  			expectedRespStatus: http.StatusForbidden,
    88  			lifecycleResponse:  []byte(""),
    89  			errorResponse: APIErrorResponse{
    90  				Resource: SlashSeparator + bucketName + SlashSeparator,
    91  				Code:     "AccessDenied",
    92  				Message:  "Access Denied.",
    93  			},
    94  			shouldPass: false,
    95  		},
    96  		// PUT wrong credentials
    97  		{
    98  			method:             http.MethodPut,
    99  			bucketName:         bucketName,
   100  			accessKey:          "abcd",
   101  			secretKey:          "abcd",
   102  			expectedRespStatus: http.StatusForbidden,
   103  			lifecycleResponse:  []byte(""),
   104  			errorResponse: APIErrorResponse{
   105  				Resource: SlashSeparator + bucketName + SlashSeparator,
   106  				Code:     "InvalidAccessKeyId",
   107  				Message:  "The Access Key Id you provided does not exist in our records.",
   108  			},
   109  			shouldPass: false,
   110  		},
   111  		// DELETE empty credentials
   112  		{
   113  			method:             http.MethodDelete,
   114  			bucketName:         bucketName,
   115  			accessKey:          "",
   116  			secretKey:          "",
   117  			expectedRespStatus: http.StatusForbidden,
   118  			lifecycleResponse:  []byte(""),
   119  			errorResponse: APIErrorResponse{
   120  				Resource: SlashSeparator + bucketName + SlashSeparator,
   121  				Code:     "AccessDenied",
   122  				Message:  "Access Denied.",
   123  			},
   124  			shouldPass: false,
   125  		},
   126  		// DELETE wrong credentials
   127  		{
   128  			method:             http.MethodDelete,
   129  			bucketName:         bucketName,
   130  			accessKey:          "abcd",
   131  			secretKey:          "abcd",
   132  			expectedRespStatus: http.StatusForbidden,
   133  			lifecycleResponse:  []byte(""),
   134  			errorResponse: APIErrorResponse{
   135  				Resource: SlashSeparator + bucketName + SlashSeparator,
   136  				Code:     "InvalidAccessKeyId",
   137  				Message:  "The Access Key Id you provided does not exist in our records.",
   138  			},
   139  			shouldPass: false,
   140  		},
   141  	}
   142  
   143  	testBucketLifecycle(obj, instanceType, bucketName, apiRouter, t, testCases)
   144  }
   145  
   146  // Test S3 Bucket lifecycle APIs
   147  func TestBucketLifecycle(t *testing.T) {
   148  	ExecObjectLayerAPITest(t, testBucketLifecycleHandlers, []string{"GetBucketLifecycle", "PutBucketLifecycle", "DeleteBucketLifecycle"})
   149  }
   150  
   151  // Simple tests of bucket lifecycle: PUT, GET, DELETE.
   152  // Tests are related and the order is important.
   153  func testBucketLifecycleHandlers(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
   154  	creds auth.Credentials, t *testing.T,
   155  ) {
   156  	// test cases with sample input and expected output.
   157  	testCases := []struct {
   158  		method     string
   159  		bucketName string
   160  		accessKey  string
   161  		secretKey  string
   162  		// Sent body
   163  		body []byte
   164  		// Expected response
   165  		expectedRespStatus int
   166  		lifecycleResponse  []byte
   167  		errorResponse      APIErrorResponse
   168  		shouldPass         bool
   169  	}{
   170  		// Test case - 1.
   171  		// Filter contains more than (Prefix,Tag,And) rule
   172  		{
   173  			method:             http.MethodPut,
   174  			bucketName:         bucketName,
   175  			accessKey:          creds.AccessKey,
   176  			secretKey:          creds.SecretKey,
   177  			body:               []byte(`<LifecycleConfiguration><Rule><ID>id</ID><Filter><Prefix>logs/</Prefix><Tag><Key>Key1</Key><Value>Value1</Value></Tag></Filter><Status>Enabled</Status><Expiration><Days>365</Days></Expiration></Rule></LifecycleConfiguration>`),
   178  			expectedRespStatus: http.StatusBadRequest,
   179  			lifecycleResponse:  []byte(``),
   180  			errorResponse: APIErrorResponse{
   181  				Resource: SlashSeparator + bucketName + SlashSeparator,
   182  				Code:     "InvalidArgument",
   183  				Message:  "Filter must have exactly one of Prefix, Tag, or And specified",
   184  			},
   185  
   186  			shouldPass: false,
   187  		},
   188  		// Date contains wrong format
   189  		{
   190  			method:             http.MethodPut,
   191  			bucketName:         bucketName,
   192  			accessKey:          creds.AccessKey,
   193  			secretKey:          creds.SecretKey,
   194  			body:               []byte(`<LifecycleConfiguration><Rule><ID>id</ID><Filter><Prefix>logs/</Prefix><Tag><Key>Key1</Key><Value>Value1</Value></Tag></Filter><Status>Enabled</Status><Expiration><Date>365</Date></Expiration></Rule></LifecycleConfiguration>`),
   195  			expectedRespStatus: http.StatusBadRequest,
   196  			lifecycleResponse:  []byte(``),
   197  			errorResponse: APIErrorResponse{
   198  				Resource: SlashSeparator + bucketName + SlashSeparator,
   199  				Code:     "InvalidArgument",
   200  				Message:  "Date must be provided in ISO 8601 format",
   201  			},
   202  
   203  			shouldPass: false,
   204  		},
   205  		{
   206  			method:             http.MethodPut,
   207  			bucketName:         bucketName,
   208  			accessKey:          creds.AccessKey,
   209  			secretKey:          creds.SecretKey,
   210  			body:               []byte(`<?xml version="1.0" encoding="UTF-8"?><LifecycleConfiguration><Rule><ID>id</ID><Filter><Prefix>logs/</Prefix></Filter><Status>Enabled</Status><Expiration><Days>365</Days></Expiration></Rule></LifecycleConfiguration>`),
   211  			expectedRespStatus: http.StatusOK,
   212  			lifecycleResponse:  []byte(``),
   213  			errorResponse:      APIErrorResponse{},
   214  			shouldPass:         true,
   215  		},
   216  		{
   217  			method:             http.MethodGet,
   218  			accessKey:          creds.AccessKey,
   219  			secretKey:          creds.SecretKey,
   220  			bucketName:         bucketName,
   221  			body:               []byte(``),
   222  			expectedRespStatus: http.StatusOK,
   223  			lifecycleResponse:  []byte(`<LifecycleConfiguration><Rule><ID>id</ID><Status>Enabled</Status><Filter><Prefix>logs/</Prefix></Filter><Expiration><Days>365</Days></Expiration></Rule></LifecycleConfiguration>`),
   224  			errorResponse:      APIErrorResponse{},
   225  			shouldPass:         true,
   226  		},
   227  		{
   228  			method:             http.MethodDelete,
   229  			accessKey:          creds.AccessKey,
   230  			secretKey:          creds.SecretKey,
   231  			bucketName:         bucketName,
   232  			body:               []byte(``),
   233  			expectedRespStatus: http.StatusNoContent,
   234  			lifecycleResponse:  []byte(``),
   235  			errorResponse:      APIErrorResponse{},
   236  			shouldPass:         true,
   237  		},
   238  		{
   239  			method:             http.MethodGet,
   240  			accessKey:          creds.AccessKey,
   241  			secretKey:          creds.SecretKey,
   242  			bucketName:         bucketName,
   243  			body:               []byte(``),
   244  			expectedRespStatus: http.StatusNotFound,
   245  			lifecycleResponse:  []byte(``),
   246  			errorResponse: APIErrorResponse{
   247  				Resource: SlashSeparator + bucketName + SlashSeparator,
   248  				Code:     "NoSuchLifecycleConfiguration",
   249  				Message:  "The lifecycle configuration does not exist",
   250  			},
   251  			shouldPass: false,
   252  		},
   253  	}
   254  
   255  	testBucketLifecycle(obj, instanceType, bucketName, apiRouter, t, testCases)
   256  }
   257  
   258  // testBucketLifecycle is a generic testing of lifecycle requests
   259  func testBucketLifecycle(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
   260  	t *testing.T, testCases []struct {
   261  		method             string
   262  		bucketName         string
   263  		accessKey          string
   264  		secretKey          string
   265  		body               []byte
   266  		expectedRespStatus int
   267  		lifecycleResponse  []byte
   268  		errorResponse      APIErrorResponse
   269  		shouldPass         bool
   270  	},
   271  ) {
   272  	for i, testCase := range testCases {
   273  		// initialize httptest Recorder, this records any mutations to response writer inside the handler.
   274  		rec := httptest.NewRecorder()
   275  		// construct HTTP request
   276  		req, err := newTestSignedRequestV4(testCase.method, getBucketLifecycleURL("", testCase.bucketName),
   277  			int64(len(testCase.body)), bytes.NewReader(testCase.body), testCase.accessKey, testCase.secretKey, nil)
   278  		if err != nil {
   279  			t.Fatalf("Test %d: %s: Failed to create HTTP request for GetBucketLocationHandler: <ERROR> %v", i+1, instanceType, err)
   280  		}
   281  		// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
   282  		// Call the ServeHTTP to execute the handler.
   283  		apiRouter.ServeHTTP(rec, req)
   284  		if rec.Code != testCase.expectedRespStatus {
   285  			t.Errorf("Test %d: %s: Expected the response status to be `%d`, but instead found `%d`", i+1, instanceType, testCase.expectedRespStatus, rec.Code)
   286  		}
   287  		if testCase.shouldPass && !bytes.Equal(testCase.lifecycleResponse, rec.Body.Bytes()) {
   288  			t.Errorf("Test %d: %s: Expected the response to be `%s`, but instead found `%s`", i+1, instanceType, string(testCase.lifecycleResponse), rec.Body.String())
   289  		}
   290  		errorResponse := APIErrorResponse{}
   291  		err = xml.Unmarshal(rec.Body.Bytes(), &errorResponse)
   292  		if err != nil && !testCase.shouldPass {
   293  			t.Fatalf("Test %d: %s: Unable to marshal response body %s", i+1, instanceType, rec.Body.String())
   294  		}
   295  		if errorResponse.Resource != testCase.errorResponse.Resource {
   296  			t.Errorf("Test %d: %s: Expected the error resource to be `%s`, but instead found `%s`", i+1, instanceType, testCase.errorResponse.Resource, errorResponse.Resource)
   297  		}
   298  		if errorResponse.Message != testCase.errorResponse.Message {
   299  			t.Errorf("Test %d: %s: Expected the error message to be `%s`, but instead found `%s`", i+1, instanceType, testCase.errorResponse.Message, errorResponse.Message)
   300  		}
   301  		if errorResponse.Code != testCase.errorResponse.Code {
   302  			t.Errorf("Test %d: %s: Expected the error code to be `%s`, but instead found `%s`", i+1, instanceType, testCase.errorResponse.Code, errorResponse.Code)
   303  		}
   304  	}
   305  }