diff --git a/.travis.yml b/.travis.yml index eee4f7cd56..b59e81ce62 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,6 +23,9 @@ before_install: stripe-mock/stripe-mock_${STRIPE_MOCK_VERSION}/stripe-mock -https-port 12112 > /dev/null & STRIPE_MOCK_PID=$! + # stripe-mock must be in PATH for `scripts/test_with_stripe_mock.go` to work. + - export PATH="${PATH}:${PWD}/stripe-mock/stripe-mock_${STRIPE_MOCK_VERSION}" + cache: directories: - stripe-mock diff --git a/Makefile b/Makefile index c37283072c..fe270566c0 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ lint: golint -set_exit_status ./... test: - go test -race ./... + go run scripts/test_with_stripe_mock.go -race ./... vet: go vet ./... @@ -21,7 +21,7 @@ vet: coverage: # go currently cannot create coverage profiles when testing multiple packages, so we test each package # independently. This issue should be fixed in Go 1.10 (https://github.com/golang/go/issues/6909). - go list ./... | xargs -n1 -I {} -P 4 go test -covermode=count -coverprofile=../../../{}/profile.coverprofile {} + go list ./... | xargs -n1 -I {} -P 4 go run scripts/test_with_stripe_mock.go -covermode=count -coverprofile=../../../{}/profile.coverprofile {} clean: find . -name \*.coverprofile -delete diff --git a/scripts/test_with_stripe_mock.go b/scripts/test_with_stripe_mock.go new file mode 100644 index 0000000000..ca0e90b2e6 --- /dev/null +++ b/scripts/test_with_stripe_mock.go @@ -0,0 +1,176 @@ +// A script that wraps the run of the project test suite and starts stripe-mock +// with a custom OpenAPI + fixtures bundle if one was found in the appropriate +// spot (see `pathSpec` below). +// +// The script passes all its arguments to a Go's test command, and defaults to +// `./...`. For example, both of the following are valid invocations: +// +// go run test_with_stripe_mock.go +// go run test_with_stripe_mock.go ./charge +// +// The reason that we need a separate script for this is because Go's testing +// infrastructure doesn't provide any kind of global hook that we can use to do +// this work in only one place before any tests are run. +package main + +import ( + "bytes" + "fmt" + "os" + "os/exec" + "regexp" + "time" +) + +const ( + defaultStripeMockPort = "12112" + + pathSpec = "./testing/openapi/spec3.json" + pathFixtures = "./testing/openapi/fixtures3.json" +) + +var portMatch = regexp.MustCompile(` port: (\d+)`) + +func main() { + var port string + var stripeMockProcess *os.Process + + // + // Maybe start stripe-mock + // + + if _, err := os.Stat(pathSpec); os.IsNotExist(err) { + port = os.Getenv("STRIPE_MOCK_PORT") + if port == "" { + port = defaultStripeMockPort + } + fmt.Printf("No custom spec file found, assuming stripe-mock is already running on port %v\n", port) + } else { + var err error + port, stripeMockProcess, err = startStripeMock() + if err != nil { + exitWithError(err) + } + } + + // Be somewhat careful with this `defer`. This will work most of the time + // including a SIGINT issued from the terminal, but deferred invocations + // are not called when using `os.Exit` like we do in `exitWithError`. + + // + // Run tests + // + + err := runTests(port) + if err != nil { + stopStripeMock(stripeMockProcess) + exitWithError(err) + } + + // + // Stop stripe-mock + // + + // Try to cleanly stop stripe-mock, but in case we don't, it'll die anyway + // because it's executing as a subprocess. + stopStripeMock(stripeMockProcess) +} + +// +// Private functions +// + +func exitWithError(err error) { + fmt.Fprintf(os.Stderr, "%v", err) + os.Exit(1) +} + +func runTests(port string) error { + args := append([]string{"test"}, os.Args[1:]...) + + // Defaults to `./...`, but also allows a specific package (or other CLI + // flags like `-test.v`) to be passed. + if len(args) == 1 { + args = append(args, "./...") + } + + cmd := exec.Command("go", args...) + + // Inherit this script's environment so that it's still possible to pass + // the test package flags like `GOCACHE=off`. + cmd.Env = append( + os.Environ(), + "STRIPE_MOCK_PORT="+port, + ) + + cmd.Stderr = os.Stderr + cmd.Stdout = os.Stdout + + err := cmd.Run() + if err != nil { + return fmt.Errorf("Error running tests: %v", err) + } + + return nil +} + +func startStripeMock() (string, *os.Process, error) { + fmt.Printf("Starting stripe-mock...\n") + + cmd := exec.Command( + "stripe-mock", + "-https-port", "0", // stripe-mock will select a port + "-spec", pathSpec, + "-fixtures", pathFixtures, + ) + + cmd.Stderr = os.Stderr + + stdout, err := cmd.StdoutPipe() + if err != nil { + return "", nil, fmt.Errorf("Error starting stripe-mock: %v", err) + } + + err = cmd.Start() + if err != nil { + return "", nil, fmt.Errorf("Error starting stripe-mock: %v", err) + } + + b := make([]byte, 1024) + var port string + + // We store the entire captured output because the string we're looking for + // may have appeared across a read boundary. + var stdoutBuffer bytes.Buffer + + for { + n, err := stdout.Read(b) + if err != nil { + return "", nil, err + } + stdoutBuffer.Write(b[0:n]) + + // Look for port in "Listening for HTTP on port: 50602" + matches := portMatch.FindStringSubmatch(stdoutBuffer.String()) + if len(matches) > 0 { + port = matches[1] + break + } + + time.Sleep(100 * time.Millisecond) + } + + fmt.Printf("Started stripe-mock; PID = %v, port = %v\n", cmd.Process.Pid, port) + return port, cmd.Process, nil +} + +func stopStripeMock(process *os.Process) { + if process == nil { + return + } + + fmt.Printf("Stopping stripe-mock...\n") + process.Signal(os.Interrupt) + process.Wait() + fmt.Printf("Stopped stripe-mock\n") +} diff --git a/testing/openapi/README.md b/testing/openapi/README.md new file mode 100644 index 0000000000..e33c189cac --- /dev/null +++ b/testing/openapi/README.md @@ -0,0 +1,9 @@ +## Using custom OpenAPI specification and fixtures files + +You can place custom OpenAPI specification and fixtures files in this +directory. The files must be in JSON format, and must be named `spec3.json` +and `fixtures3.json` respectively. + +If those files are present, the test suite will start its own stripe-mock +process on a random available port. In order for this to work, `stripe-mock` +must be on the `PATH` in the environment used to run the test suite.