1package customizations_test
2
3import (
4	"bytes"
5	"context"
6	"net/http"
7	"strings"
8	"testing"
9
10	"github.com/google/go-cmp/cmp"
11
12	"github.com/aws/aws-sdk-go-v2/aws"
13	"github.com/aws/aws-sdk-go-v2/internal/awstesting/unit"
14	"github.com/aws/aws-sdk-go-v2/service/s3"
15)
16
17func TestPutObject_PresignURL(t *testing.T) {
18	cases := map[string]struct {
19		input                  s3.PutObjectInput
20		options                s3.PresignOptions
21		expectPresignedURLHost string
22		expectRequestURIQuery  []string
23		expectSignedHeader     http.Header
24		expectMethod           string
25		expectError            string
26	}{
27		"standard case": {
28			input: s3.PutObjectInput{
29				Bucket: aws.String("mock-bucket"),
30				Key:    aws.String("mockkey"),
31				Body:   strings.NewReader("hello-world"),
32			},
33			expectPresignedURLHost: "https://mock-bucket.s3.us-west-2.amazonaws.com/mockkey?",
34			expectRequestURIQuery: []string{
35				"X-Amz-Expires=900",
36				"X-Amz-Credential",
37				"X-Amz-Date",
38				"x-id=PutObject",
39				"X-Amz-Signature",
40			},
41			expectMethod: "PUT",
42			expectSignedHeader: http.Header{
43				"Content-Length": []string{"11"},
44				"Content-Type":   []string{"application/octet-stream"},
45				"Host":           []string{"mock-bucket.s3.us-west-2.amazonaws.com"},
46			},
47		},
48		"seekable payload": {
49			input: s3.PutObjectInput{
50				Bucket: aws.String("mock-bucket"),
51				Key:    aws.String("mockkey"),
52				Body:   bytes.NewReader([]byte("hello-world")),
53			},
54			expectPresignedURLHost: "https://mock-bucket.s3.us-west-2.amazonaws.com/mockkey?",
55			expectRequestURIQuery: []string{
56				"X-Amz-Expires=900",
57				"X-Amz-Credential",
58				"X-Amz-Date",
59				"x-id=PutObject",
60				"X-Amz-Signature",
61			},
62			expectMethod: "PUT",
63			expectSignedHeader: http.Header{
64				"Content-Length": []string{"11"},
65				"Content-Type":   []string{"application/octet-stream"},
66				"Host":           []string{"mock-bucket.s3.us-west-2.amazonaws.com"},
67			},
68		},
69		"unseekable payload": {
70			// unseekable payload succeeds as we disable content sha256 computation for streaming input
71			input: s3.PutObjectInput{
72				Bucket: aws.String("mock-bucket"),
73				Key:    aws.String("mockkey"),
74				Body:   bytes.NewBuffer([]byte(`hello-world`)),
75			},
76			expectPresignedURLHost: "https://mock-bucket.s3.us-west-2.amazonaws.com/mockkey?",
77			expectRequestURIQuery: []string{
78				"X-Amz-Expires=900",
79				"X-Amz-Credential",
80				"X-Amz-Date",
81				"x-id=PutObject",
82				"X-Amz-Signature",
83			},
84			expectMethod: "PUT",
85			expectSignedHeader: http.Header{
86				"Content-Length": []string{"11"},
87				"Content-Type":   []string{"application/octet-stream"},
88				"Host":           []string{"mock-bucket.s3.us-west-2.amazonaws.com"},
89			},
90		},
91		"empty body": {
92			input: s3.PutObjectInput{
93				Bucket: aws.String("mock-bucket"),
94				Key:    aws.String("mockkey"),
95				Body:   bytes.NewReader([]byte(``)),
96			},
97			expectPresignedURLHost: "https://mock-bucket.s3.us-west-2.amazonaws.com/mockkey?",
98			expectRequestURIQuery: []string{
99				"X-Amz-Expires=900",
100				"X-Amz-Credential",
101				"X-Amz-Date",
102				"x-id=PutObject",
103				"X-Amz-Signature",
104			},
105			expectMethod: "PUT",
106			expectSignedHeader: http.Header{
107				"Host": []string{"mock-bucket.s3.us-west-2.amazonaws.com"},
108			},
109		},
110		"nil body": {
111			input: s3.PutObjectInput{
112				Bucket: aws.String("mock-bucket"),
113				Key:    aws.String("mockkey"),
114			},
115			expectPresignedURLHost: "https://mock-bucket.s3.us-west-2.amazonaws.com/mockkey?",
116			expectRequestURIQuery: []string{
117				"X-Amz-Expires=900",
118				"X-Amz-Credential",
119				"X-Amz-Date",
120				"x-id=PutObject",
121				"X-Amz-Signature",
122			},
123			expectMethod: "PUT",
124			expectSignedHeader: http.Header{
125				"Host": []string{"mock-bucket.s3.us-west-2.amazonaws.com"},
126			},
127		},
128	}
129
130	for name, c := range cases {
131		t.Run(name, func(t *testing.T) {
132			ctx := context.Background()
133			cfg := aws.Config{
134				Region:      "us-west-2",
135				Credentials: unit.StubCredentialsProvider{},
136				Retryer: func() aws.Retryer {
137					return aws.NopRetryer{}
138				},
139			}
140			presignClient := s3.NewPresignClient(s3.NewFromConfig(cfg), func(options *s3.PresignOptions) {
141				options = &c.options
142			})
143
144			req, err := presignClient.PresignPutObject(ctx, &c.input)
145			if err != nil {
146				if len(c.expectError) == 0 {
147					t.Fatalf("expected no error, got %v", err)
148				}
149				// if expect error, match error and skip rest
150				if e, a := c.expectError, err.Error(); !strings.Contains(a, e) {
151					t.Fatalf("expected error to be %s, got %s", e, a)
152				}
153			} else {
154				if len(c.expectError) != 0 {
155					t.Fatalf("expected error to be %v, got none", c.expectError)
156				}
157			}
158
159			if e, a := c.expectPresignedURLHost, req.URL; !strings.Contains(a, e) {
160				t.Fatalf("expected presigned url to contain host %s, got %s", e, a)
161			}
162
163			if len(c.expectRequestURIQuery) != 0 {
164				for _, label := range c.expectRequestURIQuery {
165					if e, a := label, req.URL; !strings.Contains(a, e) {
166						t.Fatalf("expected presigned url to contain %v label in url: %v", label, req.URL)
167					}
168				}
169			}
170
171			if e, a := c.expectSignedHeader, req.SignedHeader; len(cmp.Diff(e, a)) != 0 {
172				t.Fatalf("expected signed header to be %s, got %s, \n diff : %s", e, a, cmp.Diff(e, a))
173			}
174
175			if e, a := c.expectMethod, req.Method; !strings.EqualFold(e, a) {
176				t.Fatalf("expected presigning Method to be %s, got %s", e, a)
177			}
178
179		})
180	}
181}
182
183func TestUploadPart_PresignURL(t *testing.T) {
184	cases := map[string]struct {
185		input                  s3.UploadPartInput
186		options                s3.PresignOptions
187		expectPresignedURLHost string
188		expectRequestURIQuery  []string
189		expectSignedHeader     http.Header
190		expectMethod           string
191		expectError            string
192	}{
193		"standard case": {
194			input: s3.UploadPartInput{
195				Bucket:     aws.String("mock-bucket"),
196				Key:        aws.String("mockkey"),
197				PartNumber: 1,
198				UploadId:   aws.String("123456"),
199				Body:       strings.NewReader("hello-world"),
200			},
201			expectPresignedURLHost: "https://mock-bucket.s3.us-west-2.amazonaws.com/mockkey?",
202			expectRequestURIQuery: []string{
203				"X-Amz-Expires=900",
204				"X-Amz-Credential",
205				"X-Amz-Date",
206				"partNumber=1",
207				"uploadId=123456",
208				"x-id=UploadPart",
209				"X-Amz-Signature",
210			},
211			expectMethod: "PUT",
212			expectSignedHeader: http.Header{
213				"Content-Length": []string{"11"},
214				"Content-Type":   []string{"application/octet-stream"},
215				"Host":           []string{"mock-bucket.s3.us-west-2.amazonaws.com"},
216			},
217		},
218		"seekable payload": {
219			input: s3.UploadPartInput{
220				Bucket:     aws.String("mock-bucket"),
221				Key:        aws.String("mockkey"),
222				PartNumber: 1,
223				UploadId:   aws.String("123456"),
224				Body:       bytes.NewReader([]byte("hello-world")),
225			},
226			expectPresignedURLHost: "https://mock-bucket.s3.us-west-2.amazonaws.com/mockkey?",
227			expectRequestURIQuery: []string{
228				"X-Amz-Expires=900",
229				"X-Amz-Credential",
230				"X-Amz-Date",
231				"partNumber=1",
232				"uploadId=123456",
233				"x-id=UploadPart",
234				"X-Amz-Signature",
235			},
236			expectMethod: "PUT",
237			expectSignedHeader: http.Header{
238				"Content-Length": []string{"11"},
239				"Content-Type":   []string{"application/octet-stream"},
240				"Host":           []string{"mock-bucket.s3.us-west-2.amazonaws.com"},
241			},
242		},
243		"unseekable payload": {
244			// unseekable payload succeeds as we disable content sha256 computation for streaming input
245			input: s3.UploadPartInput{
246				Bucket:     aws.String("mock-bucket"),
247				Key:        aws.String("mockkey"),
248				PartNumber: 1,
249				UploadId:   aws.String("123456"),
250				Body:       bytes.NewBuffer([]byte(`hello-world`)),
251			},
252			expectPresignedURLHost: "https://mock-bucket.s3.us-west-2.amazonaws.com/mockkey?",
253			expectRequestURIQuery: []string{
254				"X-Amz-Expires=900",
255				"X-Amz-Credential",
256				"X-Amz-Date",
257				"partNumber=1",
258				"uploadId=123456",
259				"x-id=UploadPart",
260				"X-Amz-Signature",
261			},
262			expectMethod: "PUT",
263			expectSignedHeader: http.Header{
264				"Content-Length": []string{"11"},
265				"Content-Type":   []string{"application/octet-stream"},
266				"Host":           []string{"mock-bucket.s3.us-west-2.amazonaws.com"},
267			},
268		},
269		"empty body": {
270			input: s3.UploadPartInput{
271				Bucket:     aws.String("mock-bucket"),
272				Key:        aws.String("mockkey"),
273				PartNumber: 1,
274				UploadId:   aws.String("123456"),
275				Body:       bytes.NewReader([]byte(``)),
276			},
277			expectPresignedURLHost: "https://mock-bucket.s3.us-west-2.amazonaws.com/mockkey?",
278			expectRequestURIQuery: []string{
279				"X-Amz-Expires=900",
280				"X-Amz-Credential",
281				"X-Amz-Date",
282				"partNumber=1",
283				"uploadId=123456",
284				"x-id=UploadPart",
285				"X-Amz-Signature",
286			},
287			expectMethod: "PUT",
288			expectSignedHeader: http.Header{
289				"Host": []string{"mock-bucket.s3.us-west-2.amazonaws.com"},
290			},
291		},
292		"nil body": {
293			input: s3.UploadPartInput{
294				Bucket:     aws.String("mock-bucket"),
295				Key:        aws.String("mockkey"),
296				PartNumber: 1,
297				UploadId:   aws.String("123456"),
298			},
299			expectPresignedURLHost: "https://mock-bucket.s3.us-west-2.amazonaws.com/mockkey?",
300			expectRequestURIQuery: []string{
301				"X-Amz-Expires=900",
302				"X-Amz-Credential",
303				"X-Amz-Date",
304				"partNumber=1",
305				"uploadId=123456",
306				"x-id=UploadPart",
307				"X-Amz-Signature",
308			},
309			expectMethod: "PUT",
310			expectSignedHeader: http.Header{
311				"Host": []string{"mock-bucket.s3.us-west-2.amazonaws.com"},
312			},
313		},
314	}
315
316	for name, c := range cases {
317		t.Run(name, func(t *testing.T) {
318			ctx := context.Background()
319			cfg := aws.Config{
320				Region:      "us-west-2",
321				Credentials: unit.StubCredentialsProvider{},
322				Retryer: func() aws.Retryer {
323					return aws.NopRetryer{}
324				},
325			}
326			presignClient := s3.NewPresignClient(s3.NewFromConfig(cfg), func(options *s3.PresignOptions) {
327				options = &c.options
328			})
329
330			req, err := presignClient.PresignUploadPart(ctx, &c.input)
331			if err != nil {
332				if len(c.expectError) == 0 {
333					t.Fatalf("expected no error, got %v", err)
334				}
335				// if expect error, match error and skip rest
336				if e, a := c.expectError, err.Error(); !strings.Contains(a, e) {
337					t.Fatalf("expected error to be %s, got %s", e, a)
338				}
339			} else {
340				if len(c.expectError) != 0 {
341					t.Fatalf("expected error to be %v, got none", c.expectError)
342				}
343			}
344
345			if e, a := c.expectPresignedURLHost, req.URL; !strings.Contains(a, e) {
346				t.Fatalf("expected presigned url to contain host %s, got %s", e, a)
347			}
348
349			if len(c.expectRequestURIQuery) != 0 {
350				for _, label := range c.expectRequestURIQuery {
351					if e, a := label, req.URL; !strings.Contains(a, e) {
352						t.Fatalf("expected presigned url to contain %v label in url: %v", label, req.URL)
353					}
354				}
355			}
356
357			if e, a := c.expectSignedHeader, req.SignedHeader; len(cmp.Diff(e, a)) != 0 {
358				t.Fatalf("expected signed header to be %s, got %s, \n diff : %s", e, a, cmp.Diff(e, a))
359			}
360
361			if e, a := c.expectMethod, req.Method; !strings.EqualFold(e, a) {
362				t.Fatalf("expected presigning Method to be %s, got %s", e, a)
363			}
364
365		})
366	}
367}
368