github.com/thiagoyeds/go-cloud@v0.26.0/internal/testing/setup/setup.go (about) 1 // Copyright 2019 The Go Cloud Development Kit Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // https://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package setup // import "gocloud.dev/internal/testing/setup" 16 17 import ( 18 "context" 19 "flag" 20 "io/ioutil" 21 "net/http" 22 "os" 23 "path/filepath" 24 "testing" 25 "time" 26 27 "github.com/aws/aws-sdk-go/aws" 28 awscreds "github.com/aws/aws-sdk-go/aws/credentials" 29 "github.com/aws/aws-sdk-go/aws/session" 30 31 awsv2 "github.com/aws/aws-sdk-go-v2/aws" 32 awsv2config "github.com/aws/aws-sdk-go-v2/config" 33 awsv2creds "github.com/aws/aws-sdk-go-v2/credentials" 34 35 "gocloud.dev/gcp" 36 "gocloud.dev/internal/useragent" 37 38 "github.com/google/go-replayers/grpcreplay" 39 "github.com/google/go-replayers/httpreplay" 40 hrgoog "github.com/google/go-replayers/httpreplay/google" 41 "golang.org/x/oauth2/google" 42 "google.golang.org/api/option" 43 "google.golang.org/grpc" 44 grpccreds "google.golang.org/grpc/credentials" 45 "google.golang.org/grpc/credentials/oauth" 46 47 "github.com/Azure/azure-pipeline-go/pipeline" 48 "github.com/Azure/azure-storage-blob-go/azblob" 49 ) 50 51 // Record is true iff the tests are being run in "record" mode. 52 var Record = flag.Bool("record", false, "whether to run tests against cloud resources and record the interactions") 53 54 // FakeGCPCredentials gets fake GCP credentials. 55 func FakeGCPCredentials(ctx context.Context) (*google.Credentials, error) { 56 return google.CredentialsFromJSON(ctx, []byte(`{"type": "service_account", "project_id": "my-project-id"}`)) 57 } 58 59 func awsSession(region string, client *http.Client) (*session.Session, error) { 60 // Provide fake creds if running in replay mode. 61 var creds *awscreds.Credentials 62 if !*Record { 63 creds = awscreds.NewStaticCredentials("FAKE_ID", "FAKE_SECRET", "FAKE_TOKEN") 64 } 65 return session.NewSession(&aws.Config{ 66 HTTPClient: client, 67 Region: aws.String(region), 68 Credentials: creds, 69 MaxRetries: aws.Int(0), 70 }) 71 } 72 73 func awsV2Config(ctx context.Context, region string, client *http.Client) (awsv2.Config, error) { 74 // Provide fake creds if running in replay mode. 75 var creds awsv2.CredentialsProvider 76 if !*Record { 77 creds = awsv2creds.NewStaticCredentialsProvider("FAKE_KEY", "FAKE_SECRET", "FAKE_SESSION") 78 } 79 return awsv2config.LoadDefaultConfig( 80 ctx, 81 awsv2config.WithHTTPClient(client), 82 awsv2config.WithRegion(region), 83 awsv2config.WithCredentialsProvider(creds), 84 awsv2config.WithRetryer(func() awsv2.Retryer { return awsv2.NopRetryer{} }), 85 ) 86 } 87 88 // NewRecordReplayClient creates a new http.Client for tests. This client's 89 // activity is being either recorded to files (when *Record is set) or replayed 90 // from files. rf is a modifier function that will be invoked with the address 91 // of the httpreplay.Recorder object used to obtain the client; this function 92 // can mutate the recorder to add service-specific header filters, for example. 93 // An initState is returned for tests that need a state to have deterministic 94 // results, for example, a seed to generate random sequences. 95 func NewRecordReplayClient(ctx context.Context, t *testing.T, rf func(r *httpreplay.Recorder)) (c *http.Client, cleanup func(), initState int64) { 96 httpreplay.DebugHeaders() 97 path := filepath.Join("testdata", t.Name()+".replay") 98 if *Record { 99 t.Logf("Recording into golden file %s", path) 100 if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { 101 t.Fatal(err) 102 } 103 state := time.Now() 104 b, _ := state.MarshalBinary() 105 rec, err := httpreplay.NewRecorder(path, b) 106 if err != nil { 107 t.Fatal(err) 108 } 109 rf(rec) 110 cleanup = func() { 111 if err := rec.Close(); err != nil { 112 t.Fatal(err) 113 } 114 } 115 116 return rec.Client(), cleanup, state.UnixNano() 117 } 118 t.Logf("Replaying from golden file %s", path) 119 rep, err := httpreplay.NewReplayer(path) 120 if err != nil { 121 t.Fatal(err) 122 } 123 recState := new(time.Time) 124 if err := recState.UnmarshalBinary(rep.Initial()); err != nil { 125 t.Fatal(err) 126 } 127 return rep.Client(), func() { rep.Close() }, recState.UnixNano() 128 } 129 130 // NewAWSSession creates a new session for testing against AWS. 131 // If the test is in --record mode, the test will call out to AWS, and the 132 // results are recorded in a replay file. 133 // Otherwise, the session reads a replay file and runs the test as a replay, 134 // which never makes an outgoing HTTP call and uses fake credentials. 135 // An initState is returned for tests that need a state to have deterministic 136 // results, for example, a seed to generate random sequences. 137 func NewAWSSession(ctx context.Context, t *testing.T, region string) (sess *session.Session, 138 rt http.RoundTripper, cleanup func(), initState int64) { 139 client, cleanup, state := NewRecordReplayClient(ctx, t, func(r *httpreplay.Recorder) { 140 r.RemoveQueryParams("X-Amz-Credential", "X-Amz-Signature", "X-Amz-Security-Token") 141 r.RemoveRequestHeaders("Authorization", "Duration", "X-Amz-Security-Token") 142 r.ClearHeaders("X-Amz-Date") 143 r.ClearQueryParams("X-Amz-Date") 144 r.ClearHeaders("User-Agent") // AWS includes the Go version 145 }) 146 sess, err := awsSession(region, client) 147 if err != nil { 148 t.Fatal(err) 149 } 150 return sess, client.Transport, cleanup, state 151 } 152 153 // NewAWSv2Config creates a new aws.Config for testing against AWS. 154 // If the test is in --record mode, the test will call out to AWS, and the 155 // results are recorded in a replay file. 156 // Otherwise, the session reads a replay file and runs the test as a replay, 157 // which never makes an outgoing HTTP call and uses fake credentials. 158 // An initState is returned for tests that need a state to have deterministic 159 // results, for example, a seed to generate random sequences. 160 func NewAWSv2Config(ctx context.Context, t *testing.T, region string) (cfg awsv2.Config, rt http.RoundTripper, cleanup func(), initState int64) { 161 client, cleanup, state := NewRecordReplayClient(ctx, t, func(r *httpreplay.Recorder) { 162 r.RemoveQueryParams("X-Amz-Credential", "X-Amz-Signature", "X-Amz-Security-Token") 163 r.RemoveRequestHeaders("Authorization", "Duration", "X-Amz-Security-Token") 164 r.ClearHeaders("Amz-Sdk-Invocation-Id") 165 r.ClearHeaders("X-Amz-Date") 166 r.ClearQueryParams("X-Amz-Date") 167 r.ClearHeaders("User-Agent") // AWS includes the Go version 168 }) 169 cfg, err := awsV2Config(ctx, region, client) 170 if err != nil { 171 t.Fatal(err) 172 } 173 return cfg, client.Transport, cleanup, state 174 } 175 176 // NewGCPClient creates a new HTTPClient for testing against GCP. 177 // NewGCPClient creates a new HTTPClient for testing against GCP. 178 // If the test is in --record mode, the client will call out to GCP, and the 179 // results are recorded in a replay file. 180 // Otherwise, the session reads a replay file and runs the test as a replay, 181 // which never makes an outgoing HTTP call and uses fake credentials. 182 func NewGCPClient(ctx context.Context, t *testing.T) (client *gcp.HTTPClient, rt http.RoundTripper, done func()) { 183 c, cleanup, _ := NewRecordReplayClient(ctx, t, func(r *httpreplay.Recorder) { 184 r.ClearQueryParams("Expires") 185 r.ClearQueryParams("Signature") 186 r.ClearHeaders("Expires") 187 r.ClearHeaders("Signature") 188 }) 189 transport := c.Transport 190 if *Record { 191 creds, err := gcp.DefaultCredentials(ctx) 192 if err != nil { 193 t.Fatalf("failed to get default credentials: %v", err) 194 } 195 c, err = hrgoog.RecordClient(ctx, c, option.WithTokenSource(gcp.CredentialsTokenSource(creds))) 196 if err != nil { 197 t.Fatal(err) 198 } 199 } 200 return &gcp.HTTPClient{Client: *c}, transport, cleanup 201 } 202 203 // NewGCPgRPCConn creates a new connection for testing against GCP via gRPC. 204 // If the test is in --record mode, the client will call out to GCP, and the 205 // results are recorded in a replay file. 206 // Otherwise, the session reads a replay file and runs the test as a replay, 207 // which never makes an outgoing RPC and uses fake credentials. 208 func NewGCPgRPCConn(ctx context.Context, t *testing.T, endPoint, api string) (*grpc.ClientConn, func()) { 209 filename := t.Name() + ".replay" 210 if *Record { 211 opts, done := newGCPRecordDialOptions(t, filename) 212 opts = append(opts, useragent.GRPCDialOption(api)) 213 // Add credentials for real RPCs. 214 creds, err := gcp.DefaultCredentials(ctx) 215 if err != nil { 216 t.Fatal(err) 217 } 218 opts = append(opts, grpc.WithTransportCredentials(grpccreds.NewClientTLSFromCert(nil, ""))) 219 opts = append(opts, grpc.WithPerRPCCredentials(oauth.TokenSource{TokenSource: gcp.CredentialsTokenSource(creds)})) 220 conn, err := grpc.DialContext(ctx, endPoint, opts...) 221 if err != nil { 222 t.Fatal(err) 223 } 224 return conn, done 225 } 226 rep, done := newGCPReplayer(t, filename) 227 conn, err := rep.Connection() 228 if err != nil { 229 t.Fatal(err) 230 } 231 return conn, done 232 } 233 234 // contentTypeInjectPolicy and contentTypeInjector are somewhat of a hack to 235 // overcome an impedance mismatch between the Azure pipeline library and 236 // httpreplay - the tool we use to record/replay HTTP traffic for tests. 237 // azure-pipeline-go does not set the Content-Type header in its requests, 238 // setting X-Ms-Blob-Content-Type instead; however, httpreplay expects 239 // Content-Type to be non-empty in some cases. This injector makes sure that 240 // the content type is copied into the right header when that is originally 241 // empty. It's only used for testing. 242 type contentTypeInjectPolicy struct { 243 node pipeline.Policy 244 } 245 246 func (p *contentTypeInjectPolicy) Do(ctx context.Context, request pipeline.Request) (pipeline.Response, error) { 247 if len(request.Header.Get("Content-Type")) == 0 { 248 cType := request.Header.Get("X-Ms-Blob-Content-Type") 249 request.Header.Set("Content-Type", cType) 250 } 251 response, err := p.node.Do(ctx, request) 252 return response, err 253 } 254 255 type contentTypeInjector struct { 256 } 257 258 func (f contentTypeInjector) New(node pipeline.Policy, opts *pipeline.PolicyOptions) pipeline.Policy { 259 return &contentTypeInjectPolicy{node: node} 260 } 261 262 // NewAzureTestPipeline creates a new connection for testing against Azure Blob. 263 func NewAzureTestPipeline(ctx context.Context, t *testing.T, api string, credential azblob.Credential, accountName string) (pipeline.Pipeline, func(), *http.Client) { 264 client, done, _ := NewRecordReplayClient(ctx, t, func(r *httpreplay.Recorder) { 265 r.RemoveQueryParams("se", "sig") 266 r.RemoveQueryParams("X-Ms-Date") 267 r.ClearQueryParams("blockid") 268 r.ClearHeaders("X-Ms-Date") 269 r.ClearHeaders("X-Ms-Version") 270 r.ClearHeaders("User-Agent") // includes the full Go version 271 // Yes, it's true, Azure does not appear to be internally 272 // consistent about casing for BLock(l|L)ist. 273 r.ScrubBody("<Block(l|L)ist><Latest>.*</Latest></Block(l|L)ist>") 274 }) 275 f := []pipeline.Factory{ 276 // Sets User-Agent for recorder. 277 azblob.NewTelemetryPolicyFactory(azblob.TelemetryOptions{ 278 Value: useragent.AzureUserAgentPrefix(api), 279 }), 280 contentTypeInjector{}, 281 credential, 282 pipeline.MethodFactoryMarker(), 283 } 284 // Create a pipeline that uses client to make requests. 285 p := pipeline.NewPipeline(f, pipeline.Options{ 286 HTTPSender: pipeline.FactoryFunc(func(next pipeline.Policy, po *pipeline.PolicyOptions) pipeline.PolicyFunc { 287 return func(ctx context.Context, request pipeline.Request) (pipeline.Response, error) { 288 r, err := client.Do(request.WithContext(ctx)) 289 if err != nil { 290 err = pipeline.NewError(err, "HTTP request failed") 291 } 292 return pipeline.NewHTTPResponse(r), err 293 } 294 }), 295 }) 296 297 return p, done, client 298 } 299 300 // NewAzureKeyVaultTestClient creates a *http.Client for Azure KeyVault test 301 // recordings. 302 func NewAzureKeyVaultTestClient(ctx context.Context, t *testing.T) (*http.Client, func()) { 303 client, cleanup, _ := NewRecordReplayClient(ctx, t, func(r *httpreplay.Recorder) { 304 r.RemoveQueryParams("se", "sig") 305 r.RemoveQueryParams("X-Ms-Date") 306 r.ClearHeaders("X-Ms-Date") 307 r.ClearHeaders("User-Agent") // includes the full Go version 308 }) 309 return client, cleanup 310 } 311 312 // FakeGCPDefaultCredentials sets up the environment with fake GCP credentials. 313 // It returns a cleanup function. 314 func FakeGCPDefaultCredentials(t *testing.T) func() { 315 const envVar = "GOOGLE_APPLICATION_CREDENTIALS" 316 jsonCred := []byte(`{"client_id": "foo.apps.googleusercontent.com", "client_secret": "bar", "refresh_token": "baz", "type": "authorized_user"}`) 317 f, err := ioutil.TempFile("", "fake-gcp-creds") 318 if err != nil { 319 t.Fatal(err) 320 } 321 if err := ioutil.WriteFile(f.Name(), jsonCred, 0666); err != nil { 322 t.Fatal(err) 323 } 324 oldEnvVal := os.Getenv(envVar) 325 os.Setenv(envVar, f.Name()) 326 return func() { 327 os.Remove(f.Name()) 328 os.Setenv(envVar, oldEnvVal) 329 } 330 } 331 332 // newGCPRecordDialOptions return grpc.DialOptions that are to be appended to a 333 // GRPC dial request. These options allow a recorder to intercept RPCs and save 334 // RPCs to the file at filename, or read the RPCs from the file and return them. 335 func newGCPRecordDialOptions(t *testing.T, filename string) (opts []grpc.DialOption, done func()) { 336 path := filepath.Join("testdata", filename) 337 os.MkdirAll(filepath.Dir(path), os.ModePerm) 338 t.Logf("Recording into golden file %s", path) 339 r, err := grpcreplay.NewRecorder(path, nil) 340 if err != nil { 341 t.Fatal(err) 342 } 343 opts = r.DialOptions() 344 done = func() { 345 if err := r.Close(); err != nil { 346 t.Errorf("unable to close recorder: %v", err) 347 } 348 } 349 return opts, done 350 } 351 352 // newGCPReplayer returns a Replayer for GCP gRPC connections, as well as a function 353 // to call when done with the Replayer. 354 func newGCPReplayer(t *testing.T, filename string) (*grpcreplay.Replayer, func()) { 355 path := filepath.Join("testdata", filename) 356 t.Logf("Replaying from golden file %s", path) 357 r, err := grpcreplay.NewReplayer(path, nil) 358 if err != nil { 359 t.Fatal(err) 360 } 361 done := func() { 362 if err := r.Close(); err != nil { 363 t.Errorf("unable to close recorder: %v", err) 364 } 365 } 366 return r, done 367 } 368 369 // HasDockerTestEnvironment returns true when either: 370 // 1) Not on Github Actions. 371 // 2) On Github's Linux environment, where Docker is available. 372 func HasDockerTestEnvironment() bool { 373 s := os.Getenv("RUNNER_OS") 374 return s == "" || s == "Linux" 375 }