package main import ( "bufio" "context" "errors" "io" "net" "net/http" "os" "os/exec" "path/filepath" "strings" "syscall" "testing" "time" ) func TestMain(m *testing.M) { if os.Getenv("ZDDPROBE_AS_MAIN") == "1" { main() os.Exit(0) } os.Exit(m.Run()) } // launch starts this test binary as the probe with argv, as the platform // starts an instance: argv only, no shell. It returns the command and a // reader on the probe's stdout. func launch(t *testing.T, env []string, argv ...string) (*exec.Cmd, *bufio.Reader) { t.Helper() cmd := exec.Command(os.Args[0], argv...) cmd.Env = append(append(os.Environ(), "ZDDPROBE_AS_MAIN=1"), env...) cmd.Stderr = os.Stderr out, err := cmd.StdoutPipe() if err != nil { t.Fatal(err) } if err := cmd.Start(); err != nil { t.Fatal(err) } t.Cleanup(func() { _ = cmd.Process.Kill(); _, _ = cmd.Process.Wait() }) return cmd, bufio.NewReader(out) } func unixClient(sock string) *http.Client { return &http.Client{ Timeout: 10 * time.Second, Transport: &http.Transport{ DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { var d net.Dialer return d.DialContext(ctx, "unix", sock) }, }, } } // getOverUnix retries until the socket answers, with a 10 s cap, and returns // the body. func getOverUnix(t *testing.T, sock, path string) string { t.Helper() c := unixClient(sock) deadline := time.Now().Add(10 * time.Second) for { resp, err := c.Get("http://probe" + path) if err == nil { defer resp.Body.Close() b, err := io.ReadAll(resp.Body) if err != nil { t.Fatal(err) } return string(b) } if time.Now().After(deadline) { t.Fatalf("probe never answered on %s: %v", sock, err) } time.Sleep(50 * time.Millisecond) } } func TestProbeServesWithThePlatformsRunArgv(t *testing.T) { dir := t.TempDir() sock, db := filepath.Join(dir, "app.sock"), filepath.Join(dir, "app.db") cmd, stdout := launch(t, []string{"ZDDPROBE_TRACE=1", "CARLOS_HOST=shop.example.com"}, "--socket", sock, "--db", db) // backend_exec.go spawnUnitFrom if body := getOverUnix(t, sock, "/healthz"); !strings.HasPrefix(body, "ok ") { t.Fatalf("body %q", body) } if line, err := stdout.ReadString('\n'); err != nil || line != "serving\n" { t.Fatalf("trace line %q, %v", line, err) } // SIGTERM drains: a request inside the handler still completes, then exit 0. type result struct { status int body string err error } done := make(chan result, 1) go func() { resp, err := unixClient(sock).Get("http://probe/inflight") if err != nil { done <- result{err: err} return } defer resp.Body.Close() b, err := io.ReadAll(resp.Body) done <- result{resp.StatusCode, string(b), err} }() if line, err := stdout.ReadString('\n'); err != nil || line != "serving\n" { t.Fatalf("trace line %q, %v", line, err) } if err := cmd.Process.Signal(syscall.SIGTERM); err != nil { t.Fatal(err) } r := <-done if r.err != nil || r.status != 200 || !strings.HasPrefix(r.body, "ok ") { t.Fatalf("in-flight request: %d %q %v", r.status, r.body, r.err) } if err := cmd.Wait(); err != nil { t.Fatalf("exit after SIGTERM: %v", err) } } func TestProbeServesWithVetsArgv(t *testing.T) { sock := filepath.Join(t.TempDir(), "app.sock") // Vet's argv: no --db and no CARLOS_HOST. Rule 5 is a platform-launch // check and does not apply. launch(t, []string{"CARLOS_HOST="}, "-socket", sock) // internal/vet/vet.go bootHealthz: no --db, no CARLOS_HOST if body := getOverUnix(t, sock, "/"); !strings.HasPrefix(body, "ok ") { t.Fatalf("body %q", body) } } func TestProbeIsSilentWithoutTrace(t *testing.T) { sock := filepath.Join(t.TempDir(), "app.sock") cmd, stdout := launch(t, []string{"ZDDPROBE_TRACE=", "CARLOS_HOST=shop.example.com"}, "--socket", sock) getOverUnix(t, sock, "/") if err := cmd.Process.Signal(syscall.SIGTERM); err != nil { t.Fatal(err) } rest, _ := io.ReadAll(stdout) if len(rest) != 0 { t.Fatalf("stdout %q without ZDDPROBE_TRACE", rest) } if err := cmd.Wait(); err != nil { t.Fatalf("exit after SIGTERM: %v", err) } } func TestProbeRequiresSocket(t *testing.T) { cmd, _ := launch(t, []string{"CARLOS_HOST=shop.example.com"}, "--db", "x.db") if err := cmd.Wait(); err == nil { t.Fatal("probe without --socket exited 0") } } // The flat flag-off name, .sock, is served like app.sock. func TestProbeServesOnTheFlatHostSocketName(t *testing.T) { sock := filepath.Join(t.TempDir(), "shop.example.com.sock") launch(t, []string{"CARLOS_HOST=shop.example.com"}, "--socket", sock, "--db", "x.db") if body := getOverUnix(t, sock, "/"); !strings.HasPrefix(body, "ok ") { t.Fatalf("body %q", body) } } // A vet boot is not checked whatever its socket is called: vet picks the // name, and CARLOS_HOST is not set. func TestProbeServesVetsArgvWithAnyName(t *testing.T) { for _, form := range []string{"-socket", "--socket"} { t.Run(form, func(t *testing.T) { sock := filepath.Join(t.TempDir(), "probe.3.sock") launch(t, []string{"CARLOS_HOST="}, form, sock) if body := getOverUnix(t, sock, "/"); !strings.HasPrefix(body, "ok ") { t.Fatalf("body %q", body) } }) } } // Rev 19, rule 5: a platform launch (it passes --db) whose socket file name // is not a flag-off one, or that has no CARLOS_HOST, exits 1 with a line on // stderr, before it listens, so a probe on such a launch fails its health // check as the tenant did. -db and --db are the same flag. func TestProbeRefusesALaunchARealAppMightRefuse(t *testing.T) { for _, tc := range []struct { name, host, file, want string }{ {"generation file name", "shop.example.com", "app.3.sock", `--socket file name is "app.3.sock"`}, {"hashed file name", "shop.example.com", "0123456789ab.3.sock", "want app.sock or shop.example.com.sock"}, {"another host's name", "shop.example.com", "other.example.com.sock", `"other.example.com.sock"`}, {"no CARLOS_HOST, nested name", "", "app.sock", "CARLOS_HOST is empty"}, {"no CARLOS_HOST, flat name", "", "shop.example.com.sock", "CARLOS_HOST is empty"}, } { for _, form := range []string{"--db", "-db"} { t.Run(tc.name+" "+form, func(t *testing.T) { refused(t, tc.host, tc.file, form, tc.want) }) } } } func refused(t *testing.T, host, file, dbFlag, want string) { t.Helper() sock := filepath.Join(t.TempDir(), file) cmd := exec.Command(os.Args[0], "--socket", sock, dbFlag, "x.db") cmd.Env = append(os.Environ(), "ZDDPROBE_AS_MAIN=1", "CARLOS_HOST="+host) var stderr strings.Builder cmd.Stderr = &stderr err := cmd.Run() var exit *exec.ExitError if !errors.As(err, &exit) || exit.ExitCode() != 1 { t.Fatalf("exit = %v, want status 1", err) } if !strings.Contains(stderr.String(), want) { t.Fatalf("stderr %q, want it to say %q", stderr.String(), want) } if _, err := os.Lstat(sock); !os.IsNotExist(err) { t.Fatalf("the probe listened before refusing (lstat %s: %v)", sock, err) } } func TestCheckLaunch(t *testing.T) { for _, tc := range []struct { sock, host string ok bool }{ {"/run/carlos/instances/shop.example.com/g3/app.sock", "shop.example.com", true}, {"/run/carlos/instances/shop.example.com/app.sock", "shop.example.com", true}, {"/run/carlos/g/3/shop.example.com.sock", "shop.example.com", true}, {"/run/carlos/shop.example.com.sock", "shop.example.com", true}, {"/run/carlos/instances/shop.example.com/app.3.sock", "shop.example.com", false}, {"/run/carlos/g/0123456789ab.3.sock", "shop.example.com", false}, {"/run/carlos/shop.example.com.sock", "", false}, {"/run/carlos/.sock", "", false}, } { if err := checkLaunch(tc.sock, tc.host); (err == nil) != tc.ok { t.Errorf("checkLaunch(%q, %q) = %v, want ok %v", tc.sock, tc.host, err, tc.ok) } } }