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 }