...

Source file src/cloud.google.com/go/httpreplay/cmd/httpr/integration_test.go

Documentation: cloud.google.com/go/httpreplay/cmd/httpr

     1  // Copyright 2018 Google LLC
     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  //      http://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  //go:build linux
    16  // +build linux
    17  
    18  package main_test
    19  
    20  import (
    21  	"context"
    22  	"crypto/tls"
    23  	"crypto/x509"
    24  	"errors"
    25  	"fmt"
    26  	"io"
    27  	"net"
    28  	"net/http"
    29  	"net/url"
    30  	"os"
    31  	"os/exec"
    32  	"strings"
    33  	"testing"
    34  	"time"
    35  
    36  	"cloud.google.com/go/internal/testutil"
    37  	"cloud.google.com/go/storage"
    38  	"golang.org/x/oauth2"
    39  	"google.golang.org/api/option"
    40  )
    41  
    42  const initial = "initial state"
    43  
    44  func TestIntegration_HTTPR(t *testing.T) {
    45  	if testing.Short() {
    46  		t.Skip("Integration tests skipped in short mode")
    47  	}
    48  	if testutil.ProjID() == "" {
    49  		t.Fatal("set GCLOUD_TESTS_GOLANG_PROJECT_ID and GCLOUD_TESTS_GOLANG_KEY")
    50  	}
    51  	// Get a unique temporary filename.
    52  	f, err := os.CreateTemp("", "httpreplay")
    53  	if err != nil {
    54  		t.Fatal(err)
    55  	}
    56  	replayFilename := f.Name()
    57  	if err := f.Close(); err != nil {
    58  		t.Fatal(err)
    59  	}
    60  	defer os.Remove(replayFilename)
    61  
    62  	if err := exec.Command("go", "build").Run(); err != nil {
    63  		t.Fatalf("running 'go build': %v", err)
    64  	}
    65  	defer os.Remove("./httpr")
    66  	want := runRecord(t, replayFilename)
    67  	got := runReplay(t, replayFilename)
    68  	if got != want {
    69  		t.Fatalf("got %q, want %q", got, want)
    70  	}
    71  }
    72  
    73  func runRecord(t *testing.T, filename string) string {
    74  	cmd, tr, cport, err := start(t, "-record", filename)
    75  	if err != nil {
    76  		t.Fatal(err)
    77  	}
    78  	defer stop(t, cmd)
    79  
    80  	ctx := context.Background()
    81  	hc := &http.Client{
    82  		Transport: &oauth2.Transport{
    83  			Base:   tr,
    84  			Source: testutil.TokenSource(ctx, storage.ScopeFullControl),
    85  		},
    86  	}
    87  	res, err := http.Post(
    88  		fmt.Sprintf("http://localhost:%s/initial", cport),
    89  		"text/plain",
    90  		strings.NewReader(initial))
    91  	if err != nil {
    92  		t.Fatal(err)
    93  	}
    94  	if res.StatusCode != 200 {
    95  		t.Fatalf("from POST: %s", res.Status)
    96  	}
    97  	info, err := getBucketInfo(ctx, hc)
    98  	if err != nil {
    99  		t.Fatal(err)
   100  	}
   101  	return info
   102  }
   103  
   104  func runReplay(t *testing.T, filename string) string {
   105  	cmd, tr, cport, err := start(t, "-replay", filename)
   106  	if err != nil {
   107  		t.Fatal(err)
   108  	}
   109  	defer stop(t, cmd)
   110  
   111  	hc := &http.Client{Transport: tr}
   112  	res, err := http.Get(fmt.Sprintf("http://localhost:%s/initial", cport))
   113  	if err != nil {
   114  		t.Fatal(err)
   115  	}
   116  	if res.StatusCode != 200 {
   117  		t.Fatalf("from GET: %s", res.Status)
   118  	}
   119  	bytes, err := io.ReadAll(res.Body)
   120  	res.Body.Close()
   121  	if err != nil {
   122  		t.Fatal(err)
   123  	}
   124  	if got, want := string(bytes), initial; got != want {
   125  		t.Errorf("initial: got %q, want %q", got, want)
   126  	}
   127  	info, err := getBucketInfo(context.Background(), hc)
   128  	if err != nil {
   129  		t.Fatal(err)
   130  	}
   131  	return info
   132  }
   133  
   134  // Start the proxy binary and wait for it to come up.
   135  // Return a transport that talks to the proxy, as well as the control port.
   136  // modeFlag must be either "-record" or "-replay".
   137  func start(t *testing.T, modeFlag, filename string) (*exec.Cmd, *http.Transport, string, error) {
   138  	pport, err := pickPort()
   139  	if err != nil {
   140  		return nil, nil, "", err
   141  	}
   142  	cport, err := pickPort()
   143  	if err != nil {
   144  		return nil, nil, "", err
   145  	}
   146  	cmd := exec.Command("./httpr",
   147  		"-port", pport,
   148  		"-control-port", cport,
   149  		modeFlag,
   150  		filename,
   151  		"-debug-headers",
   152  	)
   153  	if err := cmd.Start(); err != nil {
   154  		return nil, nil, "", err
   155  	}
   156  	// Wait for the server to come up.
   157  	serverUp := false
   158  	for i := 0; i < 10; i++ {
   159  		if conn, err := net.Dial("tcp", "localhost:"+cport); err == nil {
   160  			conn.Close()
   161  			serverUp = true
   162  			break
   163  		}
   164  		time.Sleep(time.Second)
   165  	}
   166  	if !serverUp {
   167  		return nil, nil, "", errors.New("server never came up")
   168  	}
   169  	tr, err := proxyTransport(t, pport, cport)
   170  	if err != nil {
   171  		return nil, nil, "", err
   172  	}
   173  	return cmd, tr, cport, nil
   174  }
   175  
   176  func stop(t *testing.T, cmd *exec.Cmd) {
   177  	if err := cmd.Process.Signal(os.Interrupt); err != nil {
   178  		t.Fatal(err)
   179  	}
   180  }
   181  
   182  // pickPort picks an unused port.
   183  func pickPort() (string, error) {
   184  	l, err := net.Listen("tcp", ":0")
   185  	if err != nil {
   186  		return "", err
   187  	}
   188  	addr := l.Addr().String()
   189  	_, port, err := net.SplitHostPort(addr)
   190  	if err != nil {
   191  		return "", err
   192  	}
   193  	l.Close()
   194  	return port, nil
   195  }
   196  
   197  func proxyTransport(t *testing.T, pport, cport string) (*http.Transport, error) {
   198  	var caCert []byte
   199  	var err error
   200  	// GET localhost authority.cer can be flaky; retry 10 times before marking as failing.
   201  	testutil.Retry(t, 10, 1*time.Second, func(r *testutil.R) {
   202  		if caCert, err = getBody(fmt.Sprintf("http://localhost:%s/authority.cer", cport)); err != nil {
   203  			r.Errorf("proxyTransport Retry: %v", err)
   204  		}
   205  	})
   206  	if err != nil {
   207  		return nil, err
   208  	}
   209  	caCertPool := x509.NewCertPool()
   210  	if !caCertPool.AppendCertsFromPEM([]byte(caCert)) {
   211  		return nil, errors.New("bad CA Cert")
   212  	}
   213  	return &http.Transport{
   214  		Proxy:           http.ProxyURL(&url.URL{Host: "localhost:" + pport}),
   215  		TLSClientConfig: &tls.Config{RootCAs: caCertPool},
   216  	}, nil
   217  }
   218  
   219  func getBucketInfo(ctx context.Context, hc *http.Client) (string, error) {
   220  	ctx, cc := context.WithTimeout(ctx, 10*time.Second)
   221  	defer cc()
   222  
   223  	client, err := storage.NewClient(ctx, option.WithHTTPClient(hc))
   224  	if err != nil {
   225  		return "", err
   226  	}
   227  	defer client.Close()
   228  	b := client.Bucket(testutil.ProjID())
   229  	attrs, err := b.Attrs(ctx)
   230  	if err != nil {
   231  		return "", err
   232  	}
   233  	return fmt.Sprintf("name:%s reqpays:%v location:%s sclass:%s",
   234  		attrs.Name, attrs.RequesterPays, attrs.Location, attrs.StorageClass), nil
   235  }
   236  
   237  func getBody(url string) ([]byte, error) {
   238  	res, err := http.Get(url)
   239  	if err != nil {
   240  		return nil, err
   241  	}
   242  	if res.StatusCode != 200 {
   243  		return nil, fmt.Errorf("response: %s", res.Status)
   244  	}
   245  	defer res.Body.Close()
   246  	return io.ReadAll(res.Body)
   247  }
   248  

View as plain text