package loader import ( "bytes" "crypto/tls" "errors" "fmt" "net/http" "os" "os/exec" "path/filepath" "runtime" "strings" "testing" "time" "github.com/go-acme/lego/v4/platform/wait" ) const ( cmdNamePebble = "pebble" cmdNameChallSrv = "pebble-challtestsrv" ) type CmdOption struct { HealthCheckURL string Args []string Env []string Dir string } type EnvLoader struct { PebbleOptions *CmdOption LegoOptions []string ChallSrv *CmdOption lego string } func (l *EnvLoader) MainTest(m *testing.M) int { if _, e2e := os.LookupEnv("LEGO_E2E_TESTS"); !e2e { fmt.Fprintln(os.Stderr, "skipping test: e2e tests are disabled. (no 'LEGO_E2E_TESTS' env var)") fmt.Println("PASS") return 0 } if _, err := exec.LookPath("git"); err != nil { fmt.Fprintln(os.Stderr, "skipping because git command not found") fmt.Println("PASS") return 0 } if l.PebbleOptions != nil { if _, err := exec.LookPath(cmdNamePebble); err != nil { fmt.Fprintln(os.Stderr, "skipping because pebble binary not found") fmt.Println("PASS") return 0 } } if l.ChallSrv != nil { if _, err := exec.LookPath(cmdNameChallSrv); err != nil { fmt.Fprintln(os.Stderr, "skipping because challtestsrv binary not found") fmt.Println("PASS") return 0 } } pebbleTearDown := l.launchPebble() defer pebbleTearDown() challSrvTearDown := l.launchChallSrv() defer challSrvTearDown() legoBinary, tearDown, err := buildLego() defer tearDown() if err != nil { fmt.Fprintln(os.Stderr, err) return 1 } l.lego = legoBinary if l.PebbleOptions != nil && l.PebbleOptions.HealthCheckURL != "" { pebbleHealthCheck(l.PebbleOptions) } return m.Run() } func (l *EnvLoader) RunLego(arg ...string) ([]byte, error) { cmd := exec.Command(l.lego, arg...) cmd.Env = l.LegoOptions fmt.Printf("$ %s\n", strings.Join(cmd.Args, " ")) return cmd.CombinedOutput() } func (l *EnvLoader) launchPebble() func() { if l.PebbleOptions == nil { return func() {} } pebble, outPebble := l.cmdPebble() go func() { err := pebble.Run() if err != nil { fmt.Println(err) } }() return func() { err := pebble.Process.Kill() if err != nil { fmt.Println(err) } fmt.Println(outPebble.String()) } } func (l *EnvLoader) cmdPebble() (*exec.Cmd, *bytes.Buffer) { cmd := exec.Command(cmdNamePebble, l.PebbleOptions.Args...) cmd.Env = l.PebbleOptions.Env dir, err := filepath.Abs(l.PebbleOptions.Dir) if err != nil { panic(err) } cmd.Dir = dir fmt.Printf("$ %s\n", strings.Join(cmd.Args, " ")) var b bytes.Buffer cmd.Stdout = &b cmd.Stderr = &b return cmd, &b } func pebbleHealthCheck(options *CmdOption) { client := &http.Client{Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}} err := wait.For("pebble", 10*time.Second, 500*time.Millisecond, func() (bool, error) { resp, err := client.Get(options.HealthCheckURL) if err != nil { return false, err } if resp.StatusCode != http.StatusOK { return false, nil } return true, nil }) if err != nil { panic(err) } } func (l *EnvLoader) launchChallSrv() func() { if l.ChallSrv == nil { return func() {} } challtestsrv, outChalSrv := l.cmdChallSrv() go func() { err := challtestsrv.Run() if err != nil { fmt.Println(err) } }() return func() { err := challtestsrv.Process.Kill() if err != nil { fmt.Println(err) } fmt.Println(outChalSrv.String()) } } func (l *EnvLoader) cmdChallSrv() (*exec.Cmd, *bytes.Buffer) { cmd := exec.Command(cmdNameChallSrv, l.ChallSrv.Args...) fmt.Printf("$ %s\n", strings.Join(cmd.Args, " ")) var b bytes.Buffer cmd.Stdout = &b cmd.Stderr = &b return cmd, &b } func buildLego() (string, func(), error) { here, err := os.Getwd() if err != nil { return "", func() {}, err } defer func() { _ = os.Chdir(here) }() buildPath, err := os.MkdirTemp("", "lego_test") if err != nil { return "", func() {}, err } projectRoot, err := getProjectRoot() if err != nil { return "", func() {}, err } mainFolder := filepath.Join(projectRoot, "cmd", "lego") err = os.Chdir(mainFolder) if err != nil { return "", func() {}, err } binary := filepath.Join(buildPath, "lego") err = build(binary) if err != nil { return "", func() {}, err } err = os.Chdir(here) if err != nil { return "", func() {}, err } return binary, func() { _ = os.RemoveAll(buildPath) CleanLegoFiles() }, nil } func getProjectRoot() (string, error) { git := exec.Command("git", "rev-parse", "--show-toplevel") output, err := git.CombinedOutput() if err != nil { fmt.Fprintf(os.Stderr, "%s\n", output) return "", err } return strings.TrimSpace(string(output)), nil } func build(binary string) error { toolPath, err := goToolPath() if err != nil { return err } cmd := exec.Command(toolPath, "build", "-o", binary) output, err := cmd.CombinedOutput() if err != nil { fmt.Fprintf(os.Stderr, "%s\n", output) return err } return nil } func goToolPath() (string, error) { // inspired by go1.11.1/src/internal/testenv/testenv.go if os.Getenv("GO_GCFLAGS") != "" { return "", errors.New("'go build' not compatible with setting $GO_GCFLAGS") } if runtime.GOOS == "darwin" && strings.HasPrefix(runtime.GOARCH, "arm") { return "", fmt.Errorf("skipping test: 'go build' not available on %s/%s", runtime.GOOS, runtime.GOARCH) } return goTool() } func goTool() (string, error) { var exeSuffix string if runtime.GOOS == "windows" { exeSuffix = ".exe" } path := filepath.Join(runtime.GOROOT(), "bin", "go"+exeSuffix) if _, err := os.Stat(path); err == nil { return path, nil } goBin, err := exec.LookPath("go" + exeSuffix) if err != nil { return "", fmt.Errorf("cannot find go tool: %w", err) } return goBin, nil } func CleanLegoFiles() { cmd := exec.Command("rm", "-rf", ".lego") fmt.Printf("$ %s\n", strings.Join(cmd.Args, " ")) output, err := cmd.CombinedOutput() if err != nil { fmt.Println(string(output)) } }