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: "InvalidARNError : resource ARN not supported for FIPS region",
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			expectedSigningName:        "s3-outposts",
222			expectedSigningRegion:      "us-gov-east-1",
223			expectedReqURL:             "https://s3-outposts.us-gov-east-1.amazonaws.com/v20180820/bucket/myaccesspoint",
224			expectedHeaderForAccountID: true,
225			expectedHeaderForOutpostID: "op-01234567890123456",
226		},
227		"Outpost AccessPoint Fips region in Arn": {
228			bucket: "arn:aws-us-gov:s3-outposts:us-gov-east-1-fips:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint",
229			options: s3control.Options{
230				Region:       "us-gov-east-1-fips",
231				UseARNRegion: true,
232			},
233			expectedErr: "InvalidARNError : resource ARN not supported for FIPS region",
234		},
235		"Outpost AccessPoint Fips region with valid ARN region": {
236			bucket: "arn:aws-us-gov:s3-outposts:us-gov-east-1:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint",
237			options: s3control.Options{
238				Region:       "us-gov-east-1-fips",
239				UseARNRegion: true,
240			},
241			expectedReqURL:             "https://s3-outposts.us-gov-east-1.amazonaws.com/v20180820/bucket/myaccesspoint",
242			expectedSigningName:        "s3-outposts",
243			expectedSigningRegion:      "us-gov-east-1",
244			expectedHeaderForAccountID: true,
245			expectedHeaderForOutpostID: "op-01234567890123456",
246		},
247		"Outpost AccessPoint with DualStack": {
248			bucket: "arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint",
249			options: s3control.Options{
250				Region:       "us-west-2",
251				UseARNRegion: true,
252				UseDualstack: true,
253			},
254			expectedErr: "ConfigurationError : client configured for S3 Dual-stack but is not supported with resource ARN",
255		},
256		"Invalid outpost resource format": {
257			bucket: "arn:aws:s3-outposts:us-west-2:123456789012:outpost",
258			options: s3control.Options{
259				Region: "us-west-2",
260			},
261			expectedErr: "outpost resource-id not set",
262		},
263		"Missing access point for outpost resource": {
264			bucket: "arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456",
265			options: s3control.Options{
266				Region: "us-west-2",
267			},
268			expectedErr: "incomplete outpost resource type",
269		},
270		"access point": {
271			bucket: "myaccesspoint",
272			options: s3control.Options{
273				Region: "us-west-2",
274			},
275			expectedReqURL:             "https://123456789012.s3-control.us-west-2.amazonaws.com/v20180820/bucket/myaccesspoint",
276			expectedHeaderForAccountID: true,
277			expectedSigningRegion:      "us-west-2",
278			expectedSigningName:        "s3",
279		},
280		"outpost access point with unsupported sub-resource": {
281			bucket: "arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:accesspoint:mybucket:object:foo",
282			options: s3control.Options{
283				Region: "us-west-2",
284			},
285			expectedErr: "sub resource not supported",
286		},
287		"Missing outpost identifiers in outpost access point arn": {
288			bucket: "arn:aws:s3-outposts:us-west-2:123456789012:accesspoint:myendpoint",
289			options: s3control.Options{
290				Region: "us-west-2",
291			},
292			expectedErr: "invalid Amazon s3-outposts ARN",
293		},
294		"Outpost Bucket with no S3UseARNRegion flag set": {
295			bucket: "arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:bucket:mybucket",
296			options: s3control.Options{
297				Region: "us-west-2",
298			},
299			expectedReqURL:             "https://s3-outposts.us-west-2.amazonaws.com/v20180820/bucket/mybucket",
300			expectedSigningName:        "s3-outposts",
301			expectedSigningRegion:      "us-west-2",
302			expectedHeaderForOutpostID: "op-01234567890123456",
303			expectedHeaderForAccountID: true,
304		},
305		"Outpost Bucket Cross-Region Enabled": {
306			bucket: "arn:aws:s3-outposts:us-east-1:123456789012:outpost:op-01234567890123456:bucket:mybucket",
307			options: s3control.Options{
308				Region:       "us-west-2",
309				UseARNRegion: true,
310			},
311			expectedReqURL:             "https://s3-outposts.us-east-1.amazonaws.com/v20180820/bucket/mybucket",
312			expectedSigningName:        "s3-outposts",
313			expectedSigningRegion:      "us-east-1",
314			expectedHeaderForOutpostID: "op-01234567890123456",
315			expectedHeaderForAccountID: true,
316		},
317		"Outpost Bucket Cross-Region Disabled": {
318			bucket: "arn:aws:s3-outposts:us-east-1:123456789012:outpost:op-01234567890123456:bucket:mybucket",
319			options: s3control.Options{
320				Region: "us-west-2",
321			},
322			expectedErr: "client region does not match provided ARN region",
323		},
324		"Outpost Bucket other partition": {
325			bucket: "arn:aws-cn:s3-outposts:cn-north-1:123456789012:outpost:op-01234567890123456:bucket:mybucket",
326			options: s3control.Options{
327				Region:       "us-west-2",
328				UseARNRegion: true,
329			},
330			expectedErr: "ConfigurationError : client partition does not match provided ARN partition",
331		},
332		"Outpost Bucket us-gov region": {
333			bucket: "arn:aws-us-gov:s3-outposts:us-gov-east-1:123456789012:outpost:op-01234567890123456:bucket:mybucket",
334			options: s3control.Options{
335				Region:       "us-gov-east-1",
336				UseARNRegion: true,
337			},
338			expectedReqURL:             "https://s3-outposts.us-gov-east-1.amazonaws.com/v20180820/bucket/mybucket",
339			expectedSigningName:        "s3-outposts",
340			expectedSigningRegion:      "us-gov-east-1",
341			expectedHeaderForOutpostID: "op-01234567890123456",
342			expectedHeaderForAccountID: true,
343		},
344		"Outpost Bucket Fips region": {
345			bucket: "arn:aws-us-gov:s3-outposts:us-gov-east-1:123456789012:outpost:op-01234567890123456:bucket:mybucket",
346			options: s3control.Options{
347				Region: "us-gov-west-1-fips",
348			},
349			expectedErr: "ConfigurationError : client region does not match provided ARN region",
350		},
351		"Outpost Bucket Fips region in Arn": {
352			bucket: "arn:aws-us-gov:s3-outposts:fips-us-gov-east-1:123456789012:outpost:op-01234567890123456:bucket:mybucket",
353			options: s3control.Options{
354				Region:       "us-gov-east-1-fips",
355				UseARNRegion: true,
356			},
357			expectedErr: "InvalidARNError : resource ARN not supported for FIPS region",
358		},
359		"Outpost Bucket Fips region with valid ARN region": {
360			bucket: "arn:aws-us-gov:s3-outposts:us-gov-east-1:123456789012:outpost:op-01234567890123456:bucket:mybucket",
361			options: s3control.Options{
362				Region:       "us-gov-east-1-fips",
363				UseARNRegion: true,
364			},
365			expectedReqURL:             "https://s3-outposts.us-gov-east-1.amazonaws.com/v20180820/bucket/mybucket",
366			expectedSigningName:        "s3-outposts",
367			expectedSigningRegion:      "us-gov-east-1",
368			expectedHeaderForOutpostID: "op-01234567890123456",
369			expectedHeaderForAccountID: true,
370		},
371		"Outpost Bucket with DualStack": {
372			bucket: "arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:bucket:mybucket",
373			options: s3control.Options{
374				Region:       "us-west-2",
375				UseDualstack: true,
376			},
377			expectedErr: "ConfigurationError : client configured for S3 Dual-stack but is not supported with resource ARN",
378		},
379		"Missing bucket id": {
380			bucket: "arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:bucket",
381			options: s3control.Options{
382				Region: "us-west-2",
383			},
384			expectedErr: "invalid Amazon s3-outposts ARN",
385		},
386		"Invalid ARN": {
387			bucket: "arn:aws:s3-outposts:us-west-2:123456789012:bucket:mybucket",
388			options: s3control.Options{
389				Region: "us-west-2",
390			},
391			expectedErr: "invalid Amazon s3-outposts ARN, unknown resource type",
392		},
393	}
394
395	for name, c := range cases {
396		t.Run(name, func(t *testing.T) {
397
398			// options
399			opts := c.options.Copy()
400			opts.Credentials = unit.StubCredentialsProvider{}
401			opts.HTTPClient = smithyhttp.NopClient{}
402			opts.Retryer = aws.NopRetryer{}
403
404			// build an s3control client
405			svc := s3control.New(opts)
406			// setup a request retriever middleware
407			fm := requestRetrieverMiddleware{}
408
409			ctx := context.Background()
410
411			// call an operation
412			_, err := svc.GetBucket(ctx, &s3control.GetBucketInput{
413				Bucket:    ptr.String(c.bucket),
414				AccountId: ptr.String("123456789012"),
415			}, func(options *s3control.Options) {
416				// append request retriever middleware for request inspection
417				options.APIOptions = append(options.APIOptions,
418					func(stack *middleware.Stack) error {
419						// adds AFTER operation serializer middleware
420						stack.Serialize.Insert(&fm, "OperationSerializer", middleware.After)
421						return nil
422					})
423			})
424
425			// inspect any errors
426			if len(c.expectedErr) != 0 {
427				if err == nil {
428					t.Fatalf("expected error, got none")
429				}
430				if a, e := err.Error(), c.expectedErr; !strings.Contains(a, e) {
431					t.Fatalf("expect error code to contain %q, got %q", e, a)
432				}
433				return
434			}
435			if err != nil {
436				t.Fatalf("expect no error, got %v", err)
437			}
438
439			// build the captured request
440			req := fm.request.Build(ctx)
441			// verify the built request is as expected
442			if e, a := c.expectedReqURL, req.URL.String(); e != a {
443				t.Fatalf("expect url %s, got %s", e, a)
444			}
445
446			if e, a := c.expectedSigningRegion, fm.signingRegion; !strings.EqualFold(e, a) {
447				t.Fatalf("expect signing region as %s, got %s", e, a)
448			}
449
450			if e, a := c.expectedSigningName, fm.signingName; !strings.EqualFold(e, a) {
451				t.Fatalf("expect signing name as %s, got %s", e, a)
452			}
453
454			if c.expectedHeaderForAccountID {
455				if e, a := "123456789012", req.Header.Get("x-amz-account-id"); e != a {
456					t.Fatalf("expect account id header value to be %v, got %v", e, a)
457				}
458			}
459
460			if e, a := c.expectedHeaderForOutpostID, req.Header.Get("x-amz-outpost-id"); e != a {
461				t.Fatalf("expect outpost id header value to be %v, got %v", e, a)
462			}
463		})
464
465	}
466}
467
468type requestRetrieverMiddleware struct {
469	request       *smithyhttp.Request
470	signingRegion string
471	signingName   string
472}
473
474func TestCustomEndpoint_SpecialOperations(t *testing.T) {
475	cases := map[string]testCaseForEndpointCustomization{
476		"CreateBucketOperation": {
477			options: s3control.Options{
478				Region: "us-west-2",
479			},
480			operation: func(ctx context.Context, svc *s3control.Client, fm *requestRetrieverMiddleware) (interface{}, error) {
481				return svc.CreateBucket(ctx, &s3control.CreateBucketInput{
482					Bucket:    aws.String("mockBucket"),
483					OutpostId: aws.String("op-01234567890123456"),
484				}, func(options *s3control.Options) {
485					// append request retriever middleware for request inspection
486					options.APIOptions = append(options.APIOptions,
487						func(stack *middleware.Stack) error {
488							// adds AFTER operation serializer middleware
489							stack.Serialize.Insert(fm, "OperationSerializer", middleware.After)
490							return nil
491						})
492				})
493			},
494			expectedReqURL:             "https://s3-outposts.us-west-2.amazonaws.com/v20180820/bucket/mockBucket",
495			expectedSigningName:        "s3-outposts",
496			expectedSigningRegion:      "us-west-2",
497			expectedHeaderForOutpostID: "op-01234567890123456",
498			expectedHeaderForAccountID: false,
499		},
500		"ListRegionalBucketsOperation": {
501			options: s3control.Options{
502				Region: "us-west-2",
503			},
504			operation: func(ctx context.Context, svc *s3control.Client, fm *requestRetrieverMiddleware) (interface{}, error) {
505				return svc.ListRegionalBuckets(ctx, &s3control.ListRegionalBucketsInput{
506					AccountId: aws.String("123456789012"),
507					OutpostId: aws.String("op-01234567890123456"),
508				}, func(options *s3control.Options) {
509					// append request retriever middleware for request inspection
510					options.APIOptions = append(options.APIOptions,
511						func(stack *middleware.Stack) error {
512							// adds AFTER operation serializer middleware
513							stack.Serialize.Insert(fm, "OperationSerializer", middleware.After)
514							return nil
515						})
516				})
517			},
518			expectedReqURL:             "https://s3-outposts.us-west-2.amazonaws.com/v20180820/bucket",
519			expectedSigningName:        "s3-outposts",
520			expectedSigningRegion:      "us-west-2",
521			expectedHeaderForOutpostID: "op-01234567890123456",
522			expectedHeaderForAccountID: true,
523		},
524		"CreateAccessPoint bucket arn": {
525			options: s3control.Options{
526				Region: "us-west-2",
527			},
528			operation: func(ctx context.Context, svc *s3control.Client, fm *requestRetrieverMiddleware) (interface{}, error) {
529				return svc.CreateAccessPoint(ctx, &s3control.CreateAccessPointInput{
530					AccountId: aws.String("123456789012"),
531					Bucket:    aws.String("arn:aws:s3:us-west-2:123456789012:bucket:mockBucket"),
532					Name:      aws.String("mockName"),
533				}, func(options *s3control.Options) {
534					// append request retriever middleware for request inspection
535					options.APIOptions = append(options.APIOptions,
536						func(stack *middleware.Stack) error {
537							// adds AFTER operation serializer middleware
538							stack.Serialize.Insert(fm, "OperationSerializer", middleware.After)
539							return nil
540						})
541				})
542			},
543			expectedErr: "invalid Amazon s3 ARN, unknown resource type",
544		},
545		"CreateAccessPoint outpost bucket arn": {
546			options: s3control.Options{
547				Region: "us-west-2",
548			},
549			operation: func(ctx context.Context, svc *s3control.Client, fm *requestRetrieverMiddleware) (interface{}, error) {
550				return svc.CreateAccessPoint(ctx, &s3control.CreateAccessPointInput{
551					AccountId: aws.String("123456789012"),
552					Bucket:    aws.String("arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:bucket:mockBucket"),
553					Name:      aws.String("mockName"),
554				}, func(options *s3control.Options) {
555					// append request retriever middleware for request inspection
556					options.APIOptions = append(options.APIOptions,
557						func(stack *middleware.Stack) error {
558							// adds AFTER operation serializer middleware
559							stack.Serialize.Insert(fm, "OperationSerializer", middleware.After)
560							return nil
561						})
562				})
563			},
564			expectedReqURL:             "https://s3-outposts.us-west-2.amazonaws.com/v20180820/accesspoint/mockName",
565			expectedSigningName:        "s3-outposts",
566			expectedSigningRegion:      "us-west-2",
567			expectedHeaderForOutpostID: "op-01234567890123456",
568		},
569	}
570
571	for name, c := range cases {
572		t.Run(name, func(t *testing.T) {
573			runValidations(t, c)
574		})
575	}
576}
577
578func runValidations(t *testing.T, c testCaseForEndpointCustomization) {
579	// options
580	opts := c.options.Copy()
581	opts.Credentials = unit.StubCredentialsProvider{}
582	opts.HTTPClient = smithyhttp.NopClient{}
583	opts.Retryer = aws.NopRetryer{}
584
585	// build an s3control client
586	svc := s3control.New(opts)
587	// setup a request retriever middleware
588	fm := requestRetrieverMiddleware{}
589
590	ctx := context.Background()
591
592	// call an operation
593	_, err := c.operation(ctx, svc, &fm)
594
595	// inspect any errors
596	if len(c.expectedErr) != 0 {
597		if err == nil {
598			t.Fatalf("expected error, got none")
599		}
600		if a, e := err.Error(), c.expectedErr; !strings.Contains(a, e) {
601			t.Fatalf("expect error code to contain %q, got %q", e, a)
602		}
603		return
604	}
605	if err != nil {
606		t.Fatalf("expect no error, got %v", err)
607	}
608
609	// build the captured request
610	req := fm.request.Build(ctx)
611	// verify the built request is as expected
612	if e, a := c.expectedReqURL, req.URL.String(); e != a {
613		t.Fatalf("expect url %s, got %s", e, a)
614	}
615
616	if e, a := c.expectedSigningRegion, fm.signingRegion; !strings.EqualFold(e, a) {
617		t.Fatalf("expect signing region as %s, got %s", e, a)
618	}
619
620	if e, a := c.expectedSigningName, fm.signingName; !strings.EqualFold(e, a) {
621		t.Fatalf("expect signing name as %s, got %s", e, a)
622	}
623
624	if c.expectedHeaderForAccountID {
625		if e, a := "123456789012", req.Header.Get("x-amz-account-id"); e != a {
626			t.Fatalf("expect account id header value to be %v, got %v", e, a)
627		}
628	}
629
630	if e, a := c.expectedHeaderForOutpostID, req.Header.Get("x-amz-outpost-id"); e != a {
631		t.Fatalf("expect outpost id header value to be %v, got %v", e, a)
632	}
633}
634
635type testCaseForEndpointCustomization struct {
636	options                    s3control.Options
637	operation                  func(context.Context, *s3control.Client, *requestRetrieverMiddleware) (interface{}, error)
638	expectedReqURL             string
639	expectedSigningName        string
640	expectedSigningRegion      string
641	expectedHeaderForOutpostID string
642	expectedErr                string
643	expectedHeaderForAccountID bool
644}
645
646func TestVPC_CustomEndpoint(t *testing.T) {
647	account := "123456789012"
648	cases := map[string]testCaseForEndpointCustomization{
649		"standard GetAccesspoint with custom endpoint url": {
650			options: s3control.Options{
651				EndpointResolver: s3control.EndpointResolverFromURL("https://beta.example.com"),
652				Region:           "us-west-2",
653			},
654			operation: func(ctx context.Context, svc *s3control.Client, fm *requestRetrieverMiddleware) (interface{}, error) {
655				return svc.GetAccessPoint(ctx, &s3control.GetAccessPointInput{
656					AccountId: aws.String(account),
657					Name:      aws.String("apname"),
658				}, addRequestRetriever(fm))
659			},
660			expectedReqURL:        "https://123456789012.beta.example.com/v20180820/accesspoint/apname",
661			expectedSigningName:   "s3",
662			expectedSigningRegion: "us-west-2",
663		},
664		"Outpost Accesspoint ARN with GetAccesspoint and custom endpoint url": {
665			options: s3control.Options{
666				EndpointResolver: s3control.EndpointResolverFromURL(
667					"https://beta.example.com",
668				),
669				Region: "us-west-2",
670			},
671			operation: func(ctx context.Context, svc *s3control.Client, fm *requestRetrieverMiddleware) (interface{}, error) {
672				return svc.GetAccessPoint(ctx, &s3control.GetAccessPointInput{
673					AccountId: aws.String(account),
674					Name:      aws.String("arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint"),
675				}, addRequestRetriever(fm))
676			},
677			expectedReqURL:             "https://beta.example.com/v20180820/accesspoint/myaccesspoint",
678			expectedSigningName:        "s3-outposts",
679			expectedSigningRegion:      "us-west-2",
680			expectedHeaderForOutpostID: "op-01234567890123456",
681		},
682		"standard CreateBucket with custom endpoint url": {
683			options: s3control.Options{
684				EndpointResolver: s3control.EndpointResolverFromURL("https://beta.example.com"),
685				Region:           "us-west-2",
686			},
687			operation: func(ctx context.Context, svc *s3control.Client, fm *requestRetrieverMiddleware) (interface{}, error) {
688				return svc.CreateBucket(ctx, &s3control.CreateBucketInput{
689					Bucket:    aws.String("bucketname"),
690					OutpostId: aws.String("op-01234567890123456"),
691				}, addRequestRetriever(fm))
692			},
693			expectedReqURL:             "https://beta.example.com/v20180820/bucket/bucketname",
694			expectedSigningName:        "s3-outposts",
695			expectedSigningRegion:      "us-west-2",
696			expectedHeaderForOutpostID: "op-01234567890123456",
697		},
698		"Outpost Accesspoint for GetBucket with custom endpoint url": {
699			options: s3control.Options{
700				EndpointResolver: s3control.EndpointResolverFromURL("https://beta.example.com"),
701				Region:           "us-west-2",
702			},
703			operation: func(ctx context.Context, svc *s3control.Client, fm *requestRetrieverMiddleware) (interface{}, error) {
704				return svc.GetBucket(ctx, &s3control.GetBucketInput{
705					Bucket: aws.String("arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:bucket:mybucket"),
706				}, addRequestRetriever(fm))
707			},
708			expectedReqURL:             "https://beta.example.com/v20180820/bucket/mybucket",
709			expectedSigningName:        "s3-outposts",
710			expectedSigningRegion:      "us-west-2",
711			expectedHeaderForOutpostID: "op-01234567890123456",
712		},
713		"GetAccesspoint with dualstack and custom endpoint url": {
714			options: s3control.Options{
715				EndpointResolver: s3control.EndpointResolverFromURL("https://beta.example.com"),
716				Region:           "us-west-2",
717				UseDualstack:     true,
718			},
719			operation: func(ctx context.Context, svc *s3control.Client, fm *requestRetrieverMiddleware) (interface{}, error) {
720				return svc.GetAccessPoint(ctx, &s3control.GetAccessPointInput{
721					AccountId: aws.String(account),
722					Name:      aws.String("apname"),
723				}, addRequestRetriever(fm))
724			},
725			expectedReqURL:        "https://123456789012.beta.example.com/v20180820/accesspoint/apname",
726			expectedSigningName:   "s3",
727			expectedSigningRegion: "us-west-2",
728		},
729		"GetAccesspoint with Outposts accesspoint ARN and dualstack": {
730			options: s3control.Options{
731				EndpointResolver: s3control.EndpointResolverFromURL("https://beta.example.com"),
732				Region:           "us-west-2",
733				UseDualstack:     true,
734			},
735			operation: func(ctx context.Context, svc *s3control.Client, fm *requestRetrieverMiddleware) (interface{}, error) {
736				return svc.GetAccessPoint(ctx, &s3control.GetAccessPointInput{
737					AccountId: aws.String(account),
738					Name:      aws.String("arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint"),
739				}, addRequestRetriever(fm))
740			},
741			expectedErr: "client configured for S3 Dual-stack but is not supported with resource ARN",
742		},
743		"standard CreateBucket with dualstack": {
744			options: s3control.Options{
745				EndpointResolver: s3control.EndpointResolverFromURL("https://beta.example.com"),
746				Region:           "us-west-2",
747				UseDualstack:     true,
748			},
749			operation: func(ctx context.Context, svc *s3control.Client, fm *requestRetrieverMiddleware) (interface{}, error) {
750				return svc.CreateBucket(ctx, &s3control.CreateBucketInput{
751					Bucket:    aws.String("bucketname"),
752					OutpostId: aws.String("op-1234567890123456"),
753				}, addRequestRetriever(fm))
754			},
755			expectedErr: " dualstack is not supported for outposts request",
756		},
757		"GetBucket with Outpost bucket ARN": {
758			options: s3control.Options{
759				EndpointResolver: s3control.EndpointResolverFromURL("https://beta.example.com"),
760				Region:           "us-west-2",
761				UseDualstack:     true,
762			},
763			operation: func(ctx context.Context, svc *s3control.Client, fm *requestRetrieverMiddleware) (interface{}, error) {
764				return svc.GetBucket(ctx, &s3control.GetBucketInput{
765					Bucket: aws.String("arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:bucket:mybucket"),
766				}, addRequestRetriever(fm))
767			},
768			expectedErr: "client configured for S3 Dual-stack but is not supported with resource ARN",
769		},
770	}
771
772	for name, c := range cases {
773		t.Run(name, func(t *testing.T) {
774			runValidations(t, c)
775		})
776	}
777}
778
779func TestInputIsNotModified(t *testing.T) {
780	inputBucket := "arn:aws:s3-outposts:us-east-1:123456789012:outpost:op-01234567890123456:bucket:mybucket"
781
782	// build options
783	opts := s3control.Options{}
784	opts.Credentials = unit.StubCredentialsProvider{}
785	opts.HTTPClient = smithyhttp.NopClient{}
786	opts.Retryer = aws.NopRetryer{}
787	opts.Region = "us-west-2"
788	opts.UseARNRegion = true
789
790	ctx := context.Background()
791	fm := requestRetrieverMiddleware{}
792	svc := s3control.New(opts)
793	params := s3control.DeleteBucketInput{Bucket: ptr.String(inputBucket)}
794	_, err := svc.DeleteBucket(ctx, &params, func(options *s3control.Options) {
795		// append request retriever middleware for request inspection
796		options.APIOptions = append(options.APIOptions,
797			func(stack *middleware.Stack) error {
798				// adds AFTER operation serializer middleware
799				stack.Serialize.Insert(&fm, "OperationSerializer", middleware.After)
800				return nil
801			})
802	})
803
804	if err != nil {
805		t.Fatalf("expect no error, got %v", err.Error())
806	}
807
808	// check if req params were modified
809	if e, a := *params.Bucket, inputBucket; !strings.EqualFold(e, a) {
810		t.Fatalf("expected no modification for operation input, "+
811			"expected %v, got %v as bucket input", e, a)
812	}
813
814	if params.AccountId != nil {
815		t.Fatalf("expected original input to be unmodified, but account id was backfilled")
816	}
817
818	req := fm.request.Build(ctx)
819	modifiedAccountID := req.Header.Get("x-amz-account-id")
820	if len(modifiedAccountID) == 0 {
821		t.Fatalf("expected account id to be backfilled/modified, was not")
822	}
823	if e, a := "123456789012", modifiedAccountID; !strings.EqualFold(e, a) {
824		t.Fatalf("unexpected diff in account id backfilled from arn, expected %v, got %v", e, a)
825	}
826}
827
828func (*requestRetrieverMiddleware) ID() string { return "S3:requestRetrieverMiddleware" }
829
830func (rm *requestRetrieverMiddleware) HandleSerialize(
831	ctx context.Context, in middleware.SerializeInput, next middleware.SerializeHandler,
832) (
833	out middleware.SerializeOutput, metadata middleware.Metadata, err error,
834) {
835	req, ok := in.Request.(*smithyhttp.Request)
836	if !ok {
837		return out, metadata, fmt.Errorf("unknown request type %T", req)
838	}
839	rm.request = req
840
841	rm.signingName = awsmiddleware.GetSigningName(ctx)
842	rm.signingRegion = awsmiddleware.GetSigningRegion(ctx)
843
844	return next.HandleSerialize(ctx, in)
845}
846
847var addRequestRetriever = func(fm *requestRetrieverMiddleware) func(options *s3control.Options) {
848	return func(options *s3control.Options) {
849		// append request retriever middleware for request inspection
850		options.APIOptions = append(options.APIOptions,
851			func(stack *middleware.Stack) error {
852				// adds AFTER operation serializer middleware
853				stack.Serialize.Insert(fm, "OperationSerializer", middleware.After)
854				return nil
855			})
856	}
857}
858