1/*
2 * Copyright
3 *  2015, 2016, 2017 Minio, Inc.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *     http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18package minio
19
20import (
21	"bytes"
22	"encoding/xml"
23	"io/ioutil"
24	"net/http"
25	"net/url"
26	"path"
27	"reflect"
28	"testing"
29
30	"github.com/minio/minio-go/pkg/credentials"
31	"github.com/minio/minio-go/pkg/s3signer"
32)
33
34// Test validates `newBucketLocationCache`.
35func TestNewBucketLocationCache(t *testing.T) {
36	expectedBucketLocationcache := &bucketLocationCache{
37		items: make(map[string]string),
38	}
39	actualBucketLocationCache := newBucketLocationCache()
40
41	if !reflect.DeepEqual(actualBucketLocationCache, expectedBucketLocationcache) {
42		t.Errorf("Unexpected return value")
43	}
44}
45
46// Tests validate bucketLocationCache operations.
47func TestBucketLocationCacheOps(t *testing.T) {
48	testBucketLocationCache := newBucketLocationCache()
49	expectedBucketName := "minio-bucket"
50	expectedLocation := "us-east-1"
51	testBucketLocationCache.Set(expectedBucketName, expectedLocation)
52	actualLocation, ok := testBucketLocationCache.Get(expectedBucketName)
53	if !ok {
54		t.Errorf("Bucket location cache not set")
55	}
56	if expectedLocation != actualLocation {
57		t.Errorf("Bucket location cache not set to expected value")
58	}
59	testBucketLocationCache.Delete(expectedBucketName)
60	_, ok = testBucketLocationCache.Get(expectedBucketName)
61	if ok {
62		t.Errorf("Bucket location cache not deleted as expected")
63	}
64}
65
66// Tests validate http request generation for 'getBucketLocation'.
67func TestGetBucketLocationRequest(t *testing.T) {
68	// Generates expected http request for getBucketLocation.
69	// Used for asserting with the actual request generated.
70	createExpectedRequest := func(c *Client, bucketName string, req *http.Request) (*http.Request, error) {
71		// Set location query.
72		urlValues := make(url.Values)
73		urlValues.Set("location", "")
74
75		// Set get bucket location always as path style.
76		targetURL := c.endpointURL
77		targetURL.Path = path.Join(bucketName, "") + "/"
78		targetURL.RawQuery = urlValues.Encode()
79
80		// Get a new HTTP request for the method.
81		var err error
82		req, err = http.NewRequest("GET", targetURL.String(), nil)
83		if err != nil {
84			return nil, err
85		}
86
87		// Set UserAgent for the request.
88		c.setUserAgent(req)
89
90		// Get credentials from the configured credentials provider.
91		value, err := c.credsProvider.Get()
92		if err != nil {
93			return nil, err
94		}
95
96		var (
97			signerType      = value.SignerType
98			accessKeyID     = value.AccessKeyID
99			secretAccessKey = value.SecretAccessKey
100			sessionToken    = value.SessionToken
101		)
102
103		// Custom signer set then override the behavior.
104		if c.overrideSignerType != credentials.SignatureDefault {
105			signerType = c.overrideSignerType
106		}
107
108		// If signerType returned by credentials helper is anonymous,
109		// then do not sign regardless of signerType override.
110		if value.SignerType == credentials.SignatureAnonymous {
111			signerType = credentials.SignatureAnonymous
112		}
113
114		// Set sha256 sum for signature calculation only
115		// with signature version '4'.
116		switch {
117		case signerType.IsV4():
118			contentSha256 := emptySHA256Hex
119			if c.secure {
120				contentSha256 = unsignedPayload
121			}
122			req.Header.Set("X-Amz-Content-Sha256", contentSha256)
123			req = s3signer.SignV4(*req, accessKeyID, secretAccessKey, sessionToken, "us-east-1")
124		case signerType.IsV2():
125			req = s3signer.SignV2(*req, accessKeyID, secretAccessKey, false)
126		}
127
128		return req, nil
129
130	}
131	// Info for 'Client' creation.
132	// Will be used as arguments for 'NewClient'.
133	type infoForClient struct {
134		endPoint       string
135		accessKey      string
136		secretKey      string
137		enableInsecure bool
138	}
139	// dataset for 'NewClient' call.
140	info := []infoForClient{
141		// endpoint localhost.
142		// both access-key and secret-key are empty.
143		{"localhost:9000", "", "", false},
144		// both access-key are secret-key exists.
145		{"localhost:9000", "my-access-key", "my-secret-key", false},
146		// one of acess-key and secret-key are empty.
147		{"localhost:9000", "", "my-secret-key", false},
148
149		// endpoint amazon s3.
150		{"s3.amazonaws.com", "", "", false},
151		{"s3.amazonaws.com", "my-access-key", "my-secret-key", false},
152		{"s3.amazonaws.com", "my-acess-key", "", false},
153
154		// endpoint google cloud storage.
155		{"storage.googleapis.com", "", "", false},
156		{"storage.googleapis.com", "my-access-key", "my-secret-key", false},
157		{"storage.googleapis.com", "", "my-secret-key", false},
158
159		// endpoint custom domain running Minio server.
160		{"play.minio.io", "", "", false},
161		{"play.minio.io", "my-access-key", "my-secret-key", false},
162		{"play.minio.io", "my-acess-key", "", false},
163	}
164	testCases := []struct {
165		bucketName string
166		// data for new client creation.
167		info infoForClient
168		// error in the output.
169		err error
170		// flag indicating whether tests should pass.
171		shouldPass bool
172	}{
173		// Client is constructed using the info struct.
174		// case with empty location.
175		{"my-bucket", info[0], nil, true},
176		// case with location set to standard 'us-east-1'.
177		{"my-bucket", info[0], nil, true},
178		// case with location set to a value different from 'us-east-1'.
179		{"my-bucket", info[0], nil, true},
180
181		{"my-bucket", info[1], nil, true},
182		{"my-bucket", info[1], nil, true},
183		{"my-bucket", info[1], nil, true},
184
185		{"my-bucket", info[2], nil, true},
186		{"my-bucket", info[2], nil, true},
187		{"my-bucket", info[2], nil, true},
188
189		{"my-bucket", info[3], nil, true},
190		{"my-bucket", info[3], nil, true},
191		{"my-bucket", info[3], nil, true},
192
193		{"my-bucket", info[4], nil, true},
194		{"my-bucket", info[4], nil, true},
195		{"my-bucket", info[4], nil, true},
196
197		{"my-bucket", info[5], nil, true},
198		{"my-bucket", info[5], nil, true},
199		{"my-bucket", info[5], nil, true},
200
201		{"my-bucket", info[6], nil, true},
202		{"my-bucket", info[6], nil, true},
203		{"my-bucket", info[6], nil, true},
204
205		{"my-bucket", info[7], nil, true},
206		{"my-bucket", info[7], nil, true},
207		{"my-bucket", info[7], nil, true},
208
209		{"my-bucket", info[8], nil, true},
210		{"my-bucket", info[8], nil, true},
211		{"my-bucket", info[8], nil, true},
212
213		{"my-bucket", info[9], nil, true},
214		{"my-bucket", info[9], nil, true},
215		{"my-bucket", info[9], nil, true},
216
217		{"my-bucket", info[10], nil, true},
218		{"my-bucket", info[10], nil, true},
219		{"my-bucket", info[10], nil, true},
220
221		{"my-bucket", info[11], nil, true},
222		{"my-bucket", info[11], nil, true},
223		{"my-bucket", info[11], nil, true},
224	}
225	for i, testCase := range testCases {
226		// cannot create a newclient with empty endPoint value.
227		// validates and creates a new client only if the endPoint value is not empty.
228		client := &Client{}
229		var err error
230		if testCase.info.endPoint != "" {
231
232			client, err = New(testCase.info.endPoint, testCase.info.accessKey, testCase.info.secretKey, testCase.info.enableInsecure)
233			if err != nil {
234				t.Fatalf("Test %d: Failed to create new Client: %s", i+1, err.Error())
235			}
236		}
237
238		actualReq, err := client.getBucketLocationRequest(testCase.bucketName)
239		if err != nil && testCase.shouldPass {
240			t.Errorf("Test %d: Expected to pass, but failed with: <ERROR> %s", i+1, err.Error())
241		}
242		if err == nil && !testCase.shouldPass {
243			t.Errorf("Test %d: Expected to fail with <ERROR> \"%s\", but passed instead", i+1, testCase.err.Error())
244		}
245		// Failed as expected, but does it fail for the expected reason.
246		if err != nil && !testCase.shouldPass {
247			if err.Error() != testCase.err.Error() {
248				t.Errorf("Test %d: Expected to fail with error \"%s\", but instead failed with error \"%s\" instead", i+1, testCase.err.Error(), err.Error())
249			}
250		}
251
252		// Test passes as expected, but the output values are verified for correctness here.
253		if err == nil && testCase.shouldPass {
254			expectedReq := &http.Request{}
255			expectedReq, err = createExpectedRequest(client, testCase.bucketName, expectedReq)
256			if err != nil {
257				t.Fatalf("Test %d: Expected request Creation failed", i+1)
258			}
259			if expectedReq.Method != actualReq.Method {
260				t.Errorf("Test %d: The expected Request method doesn't match with the actual one", i+1)
261			}
262			if expectedReq.URL.String() != actualReq.URL.String() {
263				t.Errorf("Test %d: Expected the request URL to be '%s', but instead found '%s'", i+1, expectedReq.URL.String(), actualReq.URL.String())
264			}
265			if expectedReq.ContentLength != actualReq.ContentLength {
266				t.Errorf("Test %d: Expected the request body Content-Length to be '%d', but found '%d' instead", i+1, expectedReq.ContentLength, actualReq.ContentLength)
267			}
268
269			if expectedReq.Header.Get("X-Amz-Content-Sha256") != actualReq.Header.Get("X-Amz-Content-Sha256") {
270				t.Errorf("Test %d: 'X-Amz-Content-Sha256' header of the expected request doesn't match with that of the actual request", i+1)
271			}
272			if expectedReq.Header.Get("User-Agent") != actualReq.Header.Get("User-Agent") {
273				t.Errorf("Test %d: Expected 'User-Agent' header to be \"%s\",but found \"%s\" instead", i+1, expectedReq.Header.Get("User-Agent"), actualReq.Header.Get("User-Agent"))
274			}
275		}
276	}
277}
278
279// generates http response with bucket location set in the body.
280func generateLocationResponse(resp *http.Response, bodyContent []byte) (*http.Response, error) {
281	resp.StatusCode = http.StatusOK
282	resp.Body = ioutil.NopCloser(bytes.NewBuffer(bodyContent))
283	return resp, nil
284}
285
286// Tests the processing of GetPolicy response from server.
287func TestProcessBucketLocationResponse(t *testing.T) {
288	// LocationResponse - format for location response.
289	type LocationResponse struct {
290		XMLName  xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ LocationConstraint" json:"-"`
291		Location string   `xml:",chardata"`
292	}
293
294	APIErrors := []APIError{
295		{
296			Code:           "AccessDenied",
297			Description:    "Access Denied",
298			HTTPStatusCode: http.StatusUnauthorized,
299		},
300	}
301	testCases := []struct {
302		bucketName    string
303		inputLocation string
304		isAPIError    bool
305		apiErr        APIError
306		// expected results.
307		expectedResult string
308		err            error
309		// flag indicating whether tests should pass.
310		shouldPass bool
311	}{
312		{"my-bucket", "", true, APIErrors[0], "us-east-1", nil, true},
313		{"my-bucket", "", false, APIError{}, "us-east-1", nil, true},
314		{"my-bucket", "EU", false, APIError{}, "eu-west-1", nil, true},
315		{"my-bucket", "eu-central-1", false, APIError{}, "eu-central-1", nil, true},
316		{"my-bucket", "us-east-1", false, APIError{}, "us-east-1", nil, true},
317	}
318
319	for i, testCase := range testCases {
320		inputResponse := &http.Response{}
321		var err error
322		if testCase.isAPIError {
323			inputResponse = generateErrorResponse(inputResponse, testCase.apiErr, testCase.bucketName)
324		} else {
325			inputResponse, err = generateLocationResponse(inputResponse, encodeResponse(LocationResponse{
326				Location: testCase.inputLocation,
327			}))
328			if err != nil {
329				t.Fatalf("Test %d: Creation of valid response failed", i+1)
330			}
331		}
332		actualResult, err := processBucketLocationResponse(inputResponse, "my-bucket")
333		if err != nil && testCase.shouldPass {
334			t.Errorf("Test %d: Expected to pass, but failed with: <ERROR> %s", i+1, err.Error())
335		}
336		if err == nil && !testCase.shouldPass {
337			t.Errorf("Test %d: Expected to fail with <ERROR> \"%s\", but passed instead", i+1, testCase.err.Error())
338		}
339		// Failed as expected, but does it fail for the expected reason.
340		if err != nil && !testCase.shouldPass {
341			if err.Error() != testCase.err.Error() {
342				t.Errorf("Test %d: Expected to fail with error \"%s\", but instead failed with error \"%s\" instead", i+1, testCase.err.Error(), err.Error())
343			}
344		}
345		if err == nil && testCase.shouldPass {
346			if !reflect.DeepEqual(testCase.expectedResult, actualResult) {
347				t.Errorf("Test %d: The expected BucketPolicy doesn't match the actual BucketPolicy", i+1)
348			}
349		}
350	}
351}
352