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  }