1package customizations_test
2
3import (
4	"context"
5	"fmt"
6	"strconv"
7	"strings"
8	"testing"
9
10	"github.com/aws/smithy-go/ptr"
11
12	"github.com/aws/aws-sdk-go-v2/aws"
13	awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware"
14	"github.com/aws/aws-sdk-go-v2/internal/awstesting/unit"
15	"github.com/aws/aws-sdk-go-v2/service/s3control"
16
17	"github.com/aws/smithy-go/middleware"
18	smithyhttp "github.com/aws/smithy-go/transport/http"
19)
20
21type s3controlEndpointTest struct {
22	bucket    string
23	accountID string
24	url       string
25	err       string
26}
27
28func TestUpdateEndpointBuild(t *testing.T) {
29	cases := map[string]map[string]struct {
30		tests          []s3controlEndpointTest
31		useDualstack   bool
32		customEndpoint *aws.Endpoint
33	}{
34		"default endpoint": {
35			"default": {
36				tests: []s3controlEndpointTest{
37					{"abc", "123456789012", "https://123456789012.s3-control.mock-region.amazonaws.com/v20180820/bucket/abc", ""},
38					{"a.b.c", "123456789012", "https://123456789012.s3-control.mock-region.amazonaws.com/v20180820/bucket/a.b.c", ""},
39					{"a$b$c", "123456789012", "https://123456789012.s3-control.mock-region.amazonaws.com/v20180820/bucket/a%24b%24c", ""},
40				},
41			},
42			"DualStack": {
43				useDualstack: true,
44				tests: []s3controlEndpointTest{
45					{"abc", "123456789012", "https://123456789012.s3-control.dualstack.mock-region.amazonaws.com/v20180820/bucket/abc", ""},
46					{"a.b.c", "123456789012", "https://123456789012.s3-control.dualstack.mock-region.amazonaws.com/v20180820/bucket/a.b.c", ""},
47					{"a$b$c", "123456789012", "https://123456789012.s3-control.dualstack.mock-region.amazonaws.com/v20180820/bucket/a%24b%24c", ""},
48				},
49			},
50		},
51
52		"immutable endpoint": {
53			"default": {
54				customEndpoint: &aws.Endpoint{
55					URL:               "https://example.region.amazonaws.com",
56					HostnameImmutable: true,
57				},
58				tests: []s3controlEndpointTest{
59					{"abc", "123456789012", "https://example.region.amazonaws.com/v20180820/bucket/abc", ""},
60					{"a.b.c", "123456789012", "https://example.region.amazonaws.com/v20180820/bucket/a.b.c", ""},
61					{"a$b$c", "123456789012", "https://example.region.amazonaws.com/v20180820/bucket/a%24b%24c", ""},
62				},
63			},
64			"DualStack": {
65				useDualstack: true,
66				customEndpoint: &aws.Endpoint{
67					URL:               "https://example.region.amazonaws.com",
68					HostnameImmutable: true,
69				},
70				tests: []s3controlEndpointTest{
71					{"abc", "123456789012", "https://example.region.amazonaws.com/v20180820/bucket/abc", ""},
72					{"a.b.c", "123456789012", "https://example.region.amazonaws.com/v20180820/bucket/a.b.c", ""},
73					{"a$b$c", "123456789012", "https://example.region.amazonaws.com/v20180820/bucket/a%24b%24c", ""},
74				},
75			},
76		},
77	}
78
79	for suitName, cs := range cases {
80		t.Run(suitName, func(t *testing.T) {
81			for unitName, c := range cs {
82				t.Run(unitName, func(t *testing.T) {
83
84					options := s3control.Options{
85						Credentials: unit.StubCredentialsProvider{},
86						Retryer:     aws.NopRetryer{},
87						Region:      "mock-region",
88
89						HTTPClient: smithyhttp.NopClient{},
90
91						UseDualstack: c.useDualstack,
92					}
93
94					if c.customEndpoint != nil {
95						options.EndpointResolver = s3control.EndpointResolverFunc(
96							func(region string, options s3control.EndpointResolverOptions) (aws.Endpoint, error) {
97								return *c.customEndpoint, nil
98							})
99					}
100
101					svc := s3control.New(options)
102					for i, test := range c.tests {
103						t.Run(strconv.Itoa(i), func(t *testing.T) {
104							fm := requestRetrieverMiddleware{}
105							_, err := svc.DeleteBucket(context.Background(),
106								&s3control.DeleteBucketInput{
107									Bucket:    &test.bucket,
108									AccountId: &test.accountID,
109								},
110								func(options *s3control.Options) {
111									options.APIOptions = append(options.APIOptions,
112										func(stack *middleware.Stack) error {
113											stack.Serialize.Insert(&fm,
114												"OperationSerializer", middleware.Before)
115											return nil
116										})
117
118								},
119							)
120
121							if test.err != "" {
122								if err == nil {
123									t.Fatalf("test %d: expected error, got none", i)
124								}
125								if a, e := err.Error(), test.err; !strings.Contains(a, e) {
126									t.Fatalf("expect error code to contain %q, got %q", e, a)
127								}
128								return
129							}
130							if err != nil {
131								t.Fatalf("expect no error, got %v", err)
132							}
133
134							req := fm.request.Build(context.Background())
135							if e, a := test.url, req.URL.String(); e != a {
136								t.Fatalf("expect URL %s, got %s", e, a)
137							}
138						})
139					}
140				})
141			}
142		})
143	}
144}
145
146func TestEndpointWithARN(t *testing.T) {
147	// test cases
148	cases := map[string]struct {
149		options                    s3control.Options
150		bucket                     string
151		expectedErr                string
152		expectedReqURL             string
153		expectedSigningName        string
154		expectedSigningRegion      string
155		expectedHeaderForOutpostID string
156		expectedHeaderForAccountID bool
157	}{
158		"Outpost AccessPoint with no S3UseARNRegion flag set": {
159			bucket: "arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint",
160			options: s3control.Options{
161				Region: "us-west-2",
162			},
163			expectedReqURL:             "https://s3-outposts.us-west-2.amazonaws.com/v20180820/bucket/myaccesspoint",
164			expectedSigningName:        "s3-outposts",
165			expectedSigningRegion:      "us-west-2",
166			expectedHeaderForAccountID: true,
167			expectedHeaderForOutpostID: "op-01234567890123456",
168		},
169		"Outpost AccessPoint Cross-Region Enabled": {
170			bucket: "arn:aws:s3-outposts:us-east-1:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint",
171			options: s3control.Options{
172				Region:       "us-west-2",
173				UseARNRegion: true,
174			},
175			expectedReqURL:             "https://s3-outposts.us-east-1.amazonaws.com/v20180820/bucket/myaccesspoint",
176			expectedSigningName:        "s3-outposts",
177			expectedSigningRegion:      "us-east-1",
178			expectedHeaderForAccountID: true,
179			expectedHeaderForOutpostID: "op-01234567890123456",
180		},
181		"Outpost AccessPoint Cross-Region Disabled": {
182			bucket: "arn:aws:s3-outposts:us-east-1:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint",
183			options: s3control.Options{
184				Region: "us-west-2",
185			},
186			expectedErr: "client region does not match provided ARN region",
187		},
188		"Outpost AccessPoint other partition": {
189			bucket: "arn:aws-cn:s3-outposts:cn-north-1:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint",
190			options: s3control.Options{
191				Region:       "us-west-2",
192				UseARNRegion: true,
193			},
194			expectedErr: "ConfigurationError : client partition does not match provided ARN partition",
195		},
196		"Outpost AccessPoint us-gov region": {
197			bucket: "arn:aws-us-gov:s3-outposts:us-gov-east-1:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint",
198			options: s3control.Options{
199				Region:       "us-gov-east-1",
200				UseARNRegion: true,
201			},
202			expectedReqURL:             "https://s3-outposts.us-gov-east-1.amazonaws.com/v20180820/bucket/myaccesspoint",
203			expectedSigningName:        "s3-outposts",
204			expectedSigningRegion:      "us-gov-east-1",
205			expectedHeaderForAccountID: true,
206			expectedHeaderForOutpostID: "op-01234567890123456",
207		},
208		"Outpost AccessPoint with client region as FIPS": {
209			bucket: "arn:aws-us-gov:s3-outposts:us-gov-east-1:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint",
210			options: s3control.Options{
211				Region: "us-gov-east-1-fips",
212			},
213			expectedErr: "use of ARN is not supported when client or request is configured for FIPS",
214		},
215		"Outpost AccessPoint with client FIPS region and use arn region enabled": {
216			bucket: "arn:aws-us-gov:s3-outposts:us-gov-east-1:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint",
217			options: s3control.Options{
218				Region:       "us-gov-east-1-fips",
219				UseARNRegion: true,
220			},
221			expectedErr: "use of ARN is not supported when client or request is configured for FIPS",
222		},
223		"Outpost AccessPoint client FIPS region in Arn": {
224			bucket: "arn:aws-us-gov:s3-outposts:us-gov-east-1:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint",
225			options: s3control.Options{
226				Region:       "us-gov-east-1-fips",
227				UseARNRegion: true,
228			},
229			expectedErr: "use of ARN is not supported when client or request is configured for FIPS",
230		},
231		"Outpost AccessPoint client FIPS region with valid ARN region": {
232			bucket: "arn:aws-us-gov:s3-outposts:us-gov-east-1:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint",
233			options: s3control.Options{
234				Region:       "us-gov-east-1-fips",
235				UseARNRegion: true,
236			},
237			expectedErr: "use of ARN is not supported when client or request is configured for FIPS",
238		},
239		"Outpost AccessPoint with DualStack": {
240			bucket: "arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint",
241			options: s3control.Options{
242				Region:       "us-west-2",
243				UseARNRegion: true,
244				UseDualstack: true,
245			},
246			expectedErr: "ConfigurationError : client configured for S3 Dual-stack but is not supported with resource ARN",
247		},
248		"Invalid outpost resource format": {
249			bucket: "arn:aws:s3-outposts:us-west-2:123456789012:outpost",
250			options: s3control.Options{
251				Region: "us-west-2",
252			},
253			expectedErr: "outpost resource-id not set",
254		},
255		"Missing access point for outpost resource": {
256			bucket: "arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456",
257			options: s3control.Options{
258				Region: "us-west-2",
259			},
260			expectedErr: "incomplete outpost resource type",
261		},
262		"access point": {
263			bucket: "myaccesspoint",
264			options: s3control.Options{
265				Region: "us-west-2",
266			},
267			expectedReqURL:             "https://123456789012.s3-control.us-west-2.amazonaws.com/v20180820/bucket/myaccesspoint",
268			expectedHeaderForAccountID: true,
269			expectedSigningRegion:      "us-west-2",
270			expectedSigningName:        "s3",
271		},
272		"outpost access point with unsupported sub-resource": {
273			bucket: "arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:accesspoint:mybucket:object:foo",
274			options: s3control.Options{
275				Region: "us-west-2",
276			},
277			expectedErr: "sub resource not supported",
278		},
279		"Missing outpost identifiers in outpost access point arn": {
280			bucket: "arn:aws:s3-outposts:us-west-2:123456789012:accesspoint:myendpoint",
281			options: s3control.Options{
282				Region: "us-west-2",
283			},
284			expectedErr: "invalid Amazon s3-outposts ARN",
285		},
286		"Outpost Bucket with no S3UseARNRegion flag set": {
287			bucket: "arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:bucket:mybucket",
288			options: s3control.Options{
289				Region: "us-west-2",
290			},
291			expectedReqURL:             "https://s3-outposts.us-west-2.amazonaws.com/v20180820/bucket/mybucket",
292			expectedSigningName:        "s3-outposts",
293			expectedSigningRegion:      "us-west-2",
294			expectedHeaderForOutpostID: "op-01234567890123456",
295			expectedHeaderForAccountID: true,
296		},
297		"Outpost Bucket Cross-Region Enabled": {
298			bucket: "arn:aws:s3-outposts:us-east-1:123456789012:outpost:op-01234567890123456:bucket:mybucket",
299			options: s3control.Options{
300				Region:       "us-west-2",
301				UseARNRegion: true,
302			},
303			expectedReqURL:             "https://s3-outposts.us-east-1.amazonaws.com/v20180820/bucket/mybucket",
304			expectedSigningName:        "s3-outposts",
305			expectedSigningRegion:      "us-east-1",
306			expectedHeaderForOutpostID: "op-01234567890123456",
307			expectedHeaderForAccountID: true,
308		},
309		"Outpost Bucket Cross-Region Disabled": {
310			bucket: "arn:aws:s3-outposts:us-east-1:123456789012:outpost:op-01234567890123456:bucket:mybucket",
311			options: s3control.Options{
312				Region: "us-west-2",
313			},
314			expectedErr: "client region does not match provided ARN region",
315		},
316		"Outpost Bucket other partition": {
317			bucket: "arn:aws-cn:s3-outposts:cn-north-1:123456789012:outpost:op-01234567890123456:bucket:mybucket",
318			options: s3control.Options{
319				Region:       "us-west-2",
320				UseARNRegion: true,
321			},
322			expectedErr: "ConfigurationError : client partition does not match provided ARN partition",
323		},
324		"Outpost Bucket us-gov region": {
325			bucket: "arn:aws-us-gov:s3-outposts:us-gov-east-1:123456789012:outpost:op-01234567890123456:bucket:mybucket",
326			options: s3control.Options{
327				Region:       "us-gov-east-1",
328				UseARNRegion: true,
329			},
330			expectedReqURL:             "https://s3-outposts.us-gov-east-1.amazonaws.com/v20180820/bucket/mybucket",
331			expectedSigningName:        "s3-outposts",
332			expectedSigningRegion:      "us-gov-east-1",
333			expectedHeaderForOutpostID: "op-01234567890123456",
334			expectedHeaderForAccountID: true,
335		},
336		"Outpost Bucket client FIPS region, cross-region ARN": {
337			bucket: "arn:aws-us-gov:s3-outposts:us-gov-east-1:123456789012:outpost:op-01234567890123456:bucket:mybucket",
338			options: s3control.Options{
339				Region: "us-gov-west-1-fips",
340			},
341			expectedErr: "ConfigurationError : client region does not match provided ARN region",
342		},
343		"Outpost Bucket client FIPS region with non cross-region ARN region": {
344			bucket: "arn:aws-us-gov:s3-outposts:us-gov-east-1:123456789012:outpost:op-01234567890123456:bucket:mybucket",
345			options: s3control.Options{
346				Region:       "us-gov-east-1-fips",
347				UseARNRegion: true,
348			},
349			expectedErr: "use of ARN is not supported when client or request is configured for FIPS",
350		},
351		"Outpost Bucket with DualStack": {
352			bucket: "arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:bucket:mybucket",
353			options: s3control.Options{
354				Region:       "us-west-2",
355				UseDualstack: true,
356			},
357			expectedErr: "ConfigurationError : client configured for S3 Dual-stack but is not supported with resource ARN",
358		},
359		"Missing bucket id": {
360			bucket: "arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:bucket",
361			options: s3control.Options{
362				Region: "us-west-2",
363			},
364			expectedErr: "invalid Amazon s3-outposts ARN",
365		},
366		"Invalid ARN": {
367			bucket: "arn:aws:s3-outposts:us-west-2:123456789012:bucket:mybucket",
368			options: s3control.Options{
369				Region: "us-west-2",
370			},
371			expectedErr: "invalid Amazon s3-outposts ARN, unknown resource type",
372		},
373		"Invalid Outpost Bucket ARN with FIPS pseudo-region (prefix)": {
374			bucket: "arn:aws:s3-outposts:fips-us-east-1:123456789012:outpost:op-01234567890123456:bucket:mybucket",
375			options: s3control.Options{
376				Region:       "us-west-2",
377				UseARNRegion: true,
378			},
379			expectedErr: "FIPS region not allowed in ARN",
380		},
381		"Invalid Outpost Bucket ARN with FIPS pseudo-region (suffix)": {
382			bucket: "arn:aws:s3-outposts:us-east-1-fips:123456789012:outpost:op-01234567890123456:bucket:mybucket",
383			options: s3control.Options{
384				Region:       "us-west-2",
385				UseARNRegion: true,
386			},
387			expectedErr: "FIPS region not allowed in ARN",
388		},
389		"Invalid Outpost AccessPoint ARN with FIPS pseudo-region (prefix)": {
390			bucket: "arn:aws-us-gov:s3-outposts:fips-us-east-1:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint",
391			options: s3control.Options{
392				Region:       "us-west-2",
393				UseARNRegion: true,
394			},
395			expectedErr: "FIPS region not allowed in ARN",
396		},
397		"Invalid Outpost AccessPoint ARN with FIPS pseudo-region (suffix)": {
398			bucket: "arn:aws-us-gov:s3-outposts:us-east-1-fips:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint",
399			options: s3control.Options{
400				Region:       "us-west-2",
401				UseARNRegion: true,
402			},
403			expectedErr: "FIPS region not allowed in ARN",
404		},
405	}
406
407	for name, c := range cases {
408		t.Run(name, func(t *testing.T) {
409
410			// options
411			opts := c.options.Copy()
412			opts.Credentials = unit.StubCredentialsProvider{}
413			opts.HTTPClient = smithyhttp.NopClient{}
414			opts.Retryer = aws.NopRetryer{}
415
416			// build an s3control client
417			svc := s3control.New(opts)
418			// setup a request retriever middleware
419			fm := requestRetrieverMiddleware{}
420
421			ctx := context.Background()
422
423			// call an operation
424			_, err := svc.GetBucket(ctx, &s3control.GetBucketInput{
425				Bucket:    ptr.String(c.bucket),
426				AccountId: ptr.String("123456789012"),
427			}, func(options *s3control.Options) {
428				// append request retriever middleware for request inspection
429				options.APIOptions = append(options.APIOptions,
430					func(stack *middleware.Stack) error {
431						// adds AFTER operation serializer middleware
432						stack.Serialize.Insert(&fm, "OperationSerializer", middleware.After)
433						return nil
434					})
435			})
436
437			// inspect any errors
438			if len(c.expectedErr) != 0 {
439				if err == nil {
440					t.Fatalf("expected error, got none")
441				}
442				if a, e := err.Error(), c.expectedErr; !strings.Contains(a, e) {
443					t.Fatalf("expect error code to contain %q, got %q", e, a)
444				}
445				return
446			}
447			if err != nil {
448				t.Fatalf("expect no error, got %v", err)
449			}
450
451			// build the captured request
452			req := fm.request.Build(ctx)
453			// verify the built request is as expected
454			if e, a := c.expectedReqURL, req.URL.String(); e != a {
455				t.Fatalf("expect url %s, got %s", e, a)
456			}
457
458			if e, a := c.expectedSigningRegion, fm.signingRegion; !strings.EqualFold(e, a) {
459				t.Fatalf("expect signing region as %s, got %s", e, a)
460			}
461
462			if e, a := c.expectedSigningName, fm.signingName; !strings.EqualFold(e, a) {
463				t.Fatalf("expect signing name as %s, got %s", e, a)
464			}
465
466			if c.expectedHeaderForAccountID {
467				if e, a := "123456789012", req.Header.Get("x-amz-account-id"); e != a {
468					t.Fatalf("expect account id header value to be %v, got %v", e, a)
469				}
470			}
471
472			if e, a := c.expectedHeaderForOutpostID, req.Header.Get("x-amz-outpost-id"); e != a {
473				t.Fatalf("expect outpost id header value to be %v, got %v", e, a)
474			}
475		})
476
477	}
478}
479
480type requestRetrieverMiddleware struct {
481	request       *smithyhttp.Request
482	signingRegion string
483	signingName   string
484}
485
486func TestCustomEndpoint_SpecialOperations(t *testing.T) {
487	cases := map[string]testCaseForEndpointCustomization{
488		"CreateBucketOperation": {
489			options: s3control.Options{
490				Region: "us-west-2",
491			},
492			operation: func(ctx context.Context, svc *s3control.Client, fm *requestRetrieverMiddleware) (interface{}, error) {
493				return svc.CreateBucket(ctx, &s3control.CreateBucketInput{
494					Bucket:    aws.String("mockBucket"),
495					OutpostId: aws.String("op-01234567890123456"),
496				}, func(options *s3control.Options) {
497					// append request retriever middleware for request inspection
498					options.APIOptions = append(options.APIOptions,
499						func(stack *middleware.Stack) error {
500							// adds AFTER operation serializer middleware
501							stack.Serialize.Insert(fm, "OperationSerializer", middleware.After)
502							return nil
503						})
504				})
505			},
506			expectedReqURL:             "https://s3-outposts.us-west-2.amazonaws.com/v20180820/bucket/mockBucket",
507			expectedSigningName:        "s3-outposts",
508			expectedSigningRegion:      "us-west-2",
509			expectedHeaderForOutpostID: "op-01234567890123456",
510			expectedHeaderForAccountID: false,
511		},
512		"ListRegionalBucketsOperation": {
513			options: s3control.Options{
514				Region: "us-west-2",
515			},
516			operation: func(ctx context.Context, svc *s3control.Client, fm *requestRetrieverMiddleware) (interface{}, error) {
517				return svc.ListRegionalBuckets(ctx, &s3control.ListRegionalBucketsInput{
518					AccountId: aws.String("123456789012"),
519					OutpostId: aws.String("op-01234567890123456"),
520				}, func(options *s3control.Options) {
521					// append request retriever middleware for request inspection
522					options.APIOptions = append(options.APIOptions,
523						func(stack *middleware.Stack) error {
524							// adds AFTER operation serializer middleware
525							stack.Serialize.Insert(fm, "OperationSerializer", middleware.After)
526							return nil
527						})
528				})
529			},
530			expectedReqURL:             "https://s3-outposts.us-west-2.amazonaws.com/v20180820/bucket",
531			expectedSigningName:        "s3-outposts",
532			expectedSigningRegion:      "us-west-2",
533			expectedHeaderForOutpostID: "op-01234567890123456",
534			expectedHeaderForAccountID: true,
535		},
536		"CreateAccessPoint bucket arn": {
537			options: s3control.Options{
538				Region: "us-west-2",
539			},
540			operation: func(ctx context.Context, svc *s3control.Client, fm *requestRetrieverMiddleware) (interface{}, error) {
541				return svc.CreateAccessPoint(ctx, &s3control.CreateAccessPointInput{
542					AccountId: aws.String("123456789012"),
543					Bucket:    aws.String("arn:aws:s3:us-west-2:123456789012:bucket:mockBucket"),
544					Name:      aws.String("mockName"),
545				}, func(options *s3control.Options) {
546					// append request retriever middleware for request inspection
547					options.APIOptions = append(options.APIOptions,
548						func(stack *middleware.Stack) error {
549							// adds AFTER operation serializer middleware
550							stack.Serialize.Insert(fm, "OperationSerializer", middleware.After)
551							return nil
552						})
553				})
554			},
555			expectedErr: "invalid Amazon s3 ARN, unknown resource type",
556		},
557		"CreateAccessPoint outpost bucket arn": {
558			options: s3control.Options{
559				Region: "us-west-2",
560			},
561			operation: func(ctx context.Context, svc *s3control.Client, fm *requestRetrieverMiddleware) (interface{}, error) {
562				return svc.CreateAccessPoint(ctx, &s3control.CreateAccessPointInput{
563					AccountId: aws.String("123456789012"),
564					Bucket:    aws.String("arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:bucket:mockBucket"),
565					Name:      aws.String("mockName"),
566				}, func(options *s3control.Options) {
567					// append request retriever middleware for request inspection
568					options.APIOptions = append(options.APIOptions,
569						func(stack *middleware.Stack) error {
570							// adds AFTER operation serializer middleware
571							stack.Serialize.Insert(fm, "OperationSerializer", middleware.After)
572							return nil
573						})
574				})
575			},
576			expectedReqURL:             "https://s3-outposts.us-west-2.amazonaws.com/v20180820/accesspoint/mockName",
577			expectedSigningName:        "s3-outposts",
578			expectedSigningRegion:      "us-west-2",
579			expectedHeaderForOutpostID: "op-01234567890123456",
580		},
581	}
582
583	for name, c := range cases {
584		t.Run(name, func(t *testing.T) {
585			runValidations(t, c)
586		})
587	}
588}
589
590func runValidations(t *testing.T, c testCaseForEndpointCustomization) {
591	// options
592	opts := c.options.Copy()
593	opts.Credentials = unit.StubCredentialsProvider{}
594	opts.HTTPClient = smithyhttp.NopClient{}
595	opts.Retryer = aws.NopRetryer{}
596
597	// build an s3control client
598	svc := s3control.New(opts)
599	// setup a request retriever middleware
600	fm := requestRetrieverMiddleware{}
601
602	ctx := context.Background()
603
604	// call an operation
605	_, err := c.operation(ctx, svc, &fm)
606
607	// inspect any errors
608	if len(c.expectedErr) != 0 {
609		if err == nil {
610			t.Fatalf("expected error, got none")
611		}
612		if a, e := err.Error(), c.expectedErr; !strings.Contains(a, e) {
613			t.Fatalf("expect error code to contain %q, got %q", e, a)
614		}
615		return
616	}
617	if err != nil {
618		t.Fatalf("expect no error, got %v", err)
619	}
620
621	// build the captured request
622	req := fm.request.Build(ctx)
623	// verify the built request is as expected
624	if e, a := c.expectedReqURL, req.URL.String(); e != a {
625		t.Fatalf("expect url %s, got %s", e, a)
626	}
627
628	if e, a := c.expectedSigningRegion, fm.signingRegion; !strings.EqualFold(e, a) {
629		t.Fatalf("expect signing region as %s, got %s", e, a)
630	}
631
632	if e, a := c.expectedSigningName, fm.signingName; !strings.EqualFold(e, a) {
633		t.Fatalf("expect signing name as %s, got %s", e, a)
634	}
635
636	if c.expectedHeaderForAccountID {
637		if e, a := "123456789012", req.Header.Get("x-amz-account-id"); e != a {
638			t.Fatalf("expect account id header value to be %v, got %v", e, a)
639		}
640	}
641
642	if e, a := c.expectedHeaderForOutpostID, req.Header.Get("x-amz-outpost-id"); e != a {
643		t.Fatalf("expect outpost id header value to be %v, got %v", e, a)
644	}
645}
646
647type testCaseForEndpointCustomization struct {
648	options                    s3control.Options
649	operation                  func(context.Context, *s3control.Client, *requestRetrieverMiddleware) (interface{}, error)
650	expectedReqURL             string
651	expectedSigningName        string
652	expectedSigningRegion      string
653	expectedHeaderForOutpostID string
654	expectedErr                string
655	expectedHeaderForAccountID bool
656}
657
658func TestVPC_CustomEndpoint(t *testing.T) {
659	account := "123456789012"
660	cases := map[string]testCaseForEndpointCustomization{
661		"standard GetAccesspoint with custom endpoint url": {
662			options: s3control.Options{
663				EndpointResolver: s3control.EndpointResolverFromURL("https://beta.example.com"),
664				Region:           "us-west-2",
665			},
666			operation: func(ctx context.Context, svc *s3control.Client, fm *requestRetrieverMiddleware) (interface{}, error) {
667				return svc.GetAccessPoint(ctx, &s3control.GetAccessPointInput{
668					AccountId: aws.String(account),
669					Name:      aws.String("apname"),
670				}, addRequestRetriever(fm))
671			},
672			expectedReqURL:        "https://123456789012.beta.example.com/v20180820/accesspoint/apname",
673			expectedSigningName:   "s3",
674			expectedSigningRegion: "us-west-2",
675		},
676		"Outpost Accesspoint ARN with GetAccesspoint and custom endpoint url": {
677			options: s3control.Options{
678				EndpointResolver: s3control.EndpointResolverFromURL(
679					"https://beta.example.com",
680				),
681				Region: "us-west-2",
682			},
683			operation: func(ctx context.Context, svc *s3control.Client, fm *requestRetrieverMiddleware) (interface{}, error) {
684				return svc.GetAccessPoint(ctx, &s3control.GetAccessPointInput{
685					AccountId: aws.String(account),
686					Name:      aws.String("arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint"),
687				}, addRequestRetriever(fm))
688			},
689			expectedReqURL:             "https://beta.example.com/v20180820/accesspoint/myaccesspoint",
690			expectedSigningName:        "s3-outposts",
691			expectedSigningRegion:      "us-west-2",
692			expectedHeaderForOutpostID: "op-01234567890123456",
693		},
694		"standard CreateBucket with custom endpoint url": {
695			options: s3control.Options{
696				EndpointResolver: s3control.EndpointResolverFromURL("https://beta.example.com"),
697				Region:           "us-west-2",
698			},
699			operation: func(ctx context.Context, svc *s3control.Client, fm *requestRetrieverMiddleware) (interface{}, error) {
700				return svc.CreateBucket(ctx, &s3control.CreateBucketInput{
701					Bucket:    aws.String("bucketname"),
702					OutpostId: aws.String("op-01234567890123456"),
703				}, addRequestRetriever(fm))
704			},
705			expectedReqURL:             "https://beta.example.com/v20180820/bucket/bucketname",
706			expectedSigningName:        "s3-outposts",
707			expectedSigningRegion:      "us-west-2",
708			expectedHeaderForOutpostID: "op-01234567890123456",
709		},
710		"Outpost Accesspoint for GetBucket with custom endpoint url": {
711			options: s3control.Options{
712				EndpointResolver: s3control.EndpointResolverFromURL("https://beta.example.com"),
713				Region:           "us-west-2",
714			},
715			operation: func(ctx context.Context, svc *s3control.Client, fm *requestRetrieverMiddleware) (interface{}, error) {
716				return svc.GetBucket(ctx, &s3control.GetBucketInput{
717					Bucket: aws.String("arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:bucket:mybucket"),
718				}, addRequestRetriever(fm))
719			},
720			expectedReqURL:             "https://beta.example.com/v20180820/bucket/mybucket",
721			expectedSigningName:        "s3-outposts",
722			expectedSigningRegion:      "us-west-2",
723			expectedHeaderForOutpostID: "op-01234567890123456",
724		},
725		"GetAccesspoint with dualstack and custom endpoint url": {
726			options: s3control.Options{
727				EndpointResolver: s3control.EndpointResolverFromURL("https://beta.example.com"),
728				Region:           "us-west-2",
729				UseDualstack:     true,
730			},
731			operation: func(ctx context.Context, svc *s3control.Client, fm *requestRetrieverMiddleware) (interface{}, error) {
732				return svc.GetAccessPoint(ctx, &s3control.GetAccessPointInput{
733					AccountId: aws.String(account),
734					Name:      aws.String("apname"),
735				}, addRequestRetriever(fm))
736			},
737			expectedReqURL:        "https://123456789012.beta.example.com/v20180820/accesspoint/apname",
738			expectedSigningName:   "s3",
739			expectedSigningRegion: "us-west-2",
740		},
741		"GetAccesspoint with Outposts accesspoint ARN and dualstack": {
742			options: s3control.Options{
743				EndpointResolver: s3control.EndpointResolverFromURL("https://beta.example.com"),
744				Region:           "us-west-2",
745				UseDualstack:     true,
746			},
747			operation: func(ctx context.Context, svc *s3control.Client, fm *requestRetrieverMiddleware) (interface{}, error) {
748				return svc.GetAccessPoint(ctx, &s3control.GetAccessPointInput{
749					AccountId: aws.String(account),
750					Name:      aws.String("arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint"),
751				}, addRequestRetriever(fm))
752			},
753			expectedErr: "client configured for S3 Dual-stack but is not supported with resource ARN",
754		},
755		"standard CreateBucket with dualstack": {
756			options: s3control.Options{
757				EndpointResolver: s3control.EndpointResolverFromURL("https://beta.example.com"),
758				Region:           "us-west-2",
759				UseDualstack:     true,
760			},
761			operation: func(ctx context.Context, svc *s3control.Client, fm *requestRetrieverMiddleware) (interface{}, error) {
762				return svc.CreateBucket(ctx, &s3control.CreateBucketInput{
763					Bucket:    aws.String("bucketname"),
764					OutpostId: aws.String("op-1234567890123456"),
765				}, addRequestRetriever(fm))
766			},
767			expectedErr: " dualstack is not supported for outposts request",
768		},
769		"GetBucket with Outpost bucket ARN": {
770			options: s3control.Options{
771				EndpointResolver: s3control.EndpointResolverFromURL("https://beta.example.com"),
772				Region:           "us-west-2",
773				UseDualstack:     true,
774			},
775			operation: func(ctx context.Context, svc *s3control.Client, fm *requestRetrieverMiddleware) (interface{}, error) {
776				return svc.GetBucket(ctx, &s3control.GetBucketInput{
777					Bucket: aws.String("arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:bucket:mybucket"),
778				}, addRequestRetriever(fm))
779			},
780			expectedErr: "client configured for S3 Dual-stack but is not supported with resource ARN",
781		},
782	}
783
784	for name, c := range cases {
785		t.Run(name, func(t *testing.T) {
786			runValidations(t, c)
787		})
788	}
789}
790
791func TestInputIsNotModified(t *testing.T) {
792	inputBucket := "arn:aws:s3-outposts:us-east-1:123456789012:outpost:op-01234567890123456:bucket:mybucket"
793
794	// build options
795	opts := s3control.Options{}
796	opts.Credentials = unit.StubCredentialsProvider{}
797	opts.HTTPClient = smithyhttp.NopClient{}
798	opts.Retryer = aws.NopRetryer{}
799	opts.Region = "us-west-2"
800	opts.UseARNRegion = true
801
802	ctx := context.Background()
803	fm := requestRetrieverMiddleware{}
804	svc := s3control.New(opts)
805	params := s3control.DeleteBucketInput{Bucket: ptr.String(inputBucket)}
806	_, err := svc.DeleteBucket(ctx, &params, func(options *s3control.Options) {
807		// append request retriever middleware for request inspection
808		options.APIOptions = append(options.APIOptions,
809			func(stack *middleware.Stack) error {
810				// adds AFTER operation serializer middleware
811				stack.Serialize.Insert(&fm, "OperationSerializer", middleware.After)
812				return nil
813			})
814	})
815
816	if err != nil {
817		t.Fatalf("expect no error, got %v", err.Error())
818	}
819
820	// check if req params were modified
821	if e, a := *params.Bucket, inputBucket; !strings.EqualFold(e, a) {
822		t.Fatalf("expected no modification for operation input, "+
823			"expected %v, got %v as bucket input", e, a)
824	}
825
826	if params.AccountId != nil {
827		t.Fatalf("expected original input to be unmodified, but account id was backfilled")
828	}
829
830	req := fm.request.Build(ctx)
831	modifiedAccountID := req.Header.Get("x-amz-account-id")
832	if len(modifiedAccountID) == 0 {
833		t.Fatalf("expected account id to be backfilled/modified, was not")
834	}
835	if e, a := "123456789012", modifiedAccountID; !strings.EqualFold(e, a) {
836		t.Fatalf("unexpected diff in account id backfilled from arn, expected %v, got %v", e, a)
837	}
838}
839
840func (*requestRetrieverMiddleware) ID() string { return "S3:requestRetrieverMiddleware" }
841
842func (rm *requestRetrieverMiddleware) HandleSerialize(
843	ctx context.Context, in middleware.SerializeInput, next middleware.SerializeHandler,
844) (
845	out middleware.SerializeOutput, metadata middleware.Metadata, err error,
846) {
847	req, ok := in.Request.(*smithyhttp.Request)
848	if !ok {
849		return out, metadata, fmt.Errorf("unknown request type %T", req)
850	}
851	rm.request = req
852
853	rm.signingName = awsmiddleware.GetSigningName(ctx)
854	rm.signingRegion = awsmiddleware.GetSigningRegion(ctx)
855
856	return next.HandleSerialize(ctx, in)
857}
858
859var addRequestRetriever = func(fm *requestRetrieverMiddleware) func(options *s3control.Options) {
860	return func(options *s3control.Options) {
861		// append request retriever middleware for request inspection
862		options.APIOptions = append(options.APIOptions,
863			func(stack *middleware.Stack) error {
864				// adds AFTER operation serializer middleware
865				stack.Serialize.Insert(fm, "OperationSerializer", middleware.After)
866				return nil
867			})
868	}
869}
870