package app_test import ( "bytes" "errors" "fmt" "net" "net/http" "os" "os/exec" "strconv" "syscall" "testing" "time" "sneak.berlin/go/pixa/internal/app" ) // These tests run pixad in a child process: the test binary itself, // started again with runPixadInChildEnvVar set, which makes TestMain run // pixad instead of the tests. const runPixadInChildEnvVar = "TEST_RUN_PIXAD_IN_CHILD" const validTestSigningKey = "test-signing-key-for-app-tests-0123456789" func TestMain(m *testing.M) { if os.Getenv(runPixadInChildEnvVar) == "1" { os.Args = []string{"pixad"} app.Run("test") os.Exit(0) } os.Exit(m.Run()) } // pixadCommand returns a command that runs pixad with only the given // environment, in an empty directory with an empty HOME, so that no // config file is found. func pixadCommand(t *testing.T, env ...string) (*exec.Cmd, *bytes.Buffer) { t.Helper() testBinary := os.Args[0] cmd := exec.CommandContext(t.Context(), testBinary) //nolint:gosec // not tainted cmd.Dir = t.TempDir() cmd.Env = append([]string{ runPixadInChildEnvVar + "=1", "HOME=" + t.TempDir(), }, env...) var stderr bytes.Buffer cmd.Stderr = &stderr return cmd, &stderr } // TestRunPrintsStartupErrorOnce checks that an invalid setting is // printed once, as the line that names it, and nothing else goes to // stderr. func TestRunPrintsStartupErrorOnce(t *testing.T) { t.Parallel() cmd, stderr := pixadCommand(t, "PIXA_SIGNING_KEY=short") err := cmd.Run() var exitErr *exec.ExitError if !errors.As(err, &exitErr) || exitErr.ExitCode() != 1 { t.Fatalf("pixad: got %v, want exit status 1; stderr:\n%s", err, stderr) } want := `config key "signing_key" (environment variable PIXA_SIGNING_KEY): ` + "value too short: must be at least 32 characters, got 5\n" if stderr.String() != want { t.Errorf("stderr:\n%s\nwant:\n%s", stderr, want) } } // TestRunWritesNothingToStderrOnStartAndStop starts pixad, waits until // it answers its health check, stops it with SIGTERM, and checks that it // wrote nothing to stderr. func TestRunWritesNothingToStderrOnStartAndStop(t *testing.T) { t.Parallel() port := freePort(t) cmd, stderr := pixadCommand(t, "PORT="+strconv.Itoa(port), "PIXA_STATE_DIR="+t.TempDir(), "PIXA_SIGNING_KEY="+validTestSigningKey, ) err := cmd.Start() if err != nil { t.Fatalf("starting pixad: %v", err) } waitForHealthCheck(t, port) err = cmd.Process.Signal(syscall.SIGTERM) if err != nil { t.Fatalf("signalling pixad: %v", err) } err = cmd.Wait() if err != nil { t.Fatalf("pixad: got %v, want exit status 0; stderr:\n%s", err, stderr) } if stderr.Len() != 0 { t.Errorf("stderr is not empty:\n%s", stderr) } } // freePort returns a TCP port on the loopback address that nothing // listens on. func freePort(t *testing.T) int { t.Helper() var listenConfig net.ListenConfig listener, err := listenConfig.Listen(t.Context(), "tcp", "127.0.0.1:0") if err != nil { t.Fatalf("finding a free port: %v", err) } port := listener.Addr().(*net.TCPAddr).Port //nolint:forcetypeassert // a TCP listener err = listener.Close() if err != nil { t.Fatalf("closing listener: %v", err) } return port } // waitForHealthCheck waits up to 30 seconds for pixad to answer its // health check on port. func waitForHealthCheck(t *testing.T, port int) { t.Helper() url := fmt.Sprintf("http://127.0.0.1:%d/.well-known/healthcheck.json", port) deadline := time.Now().Add(30 * time.Second) for time.Now().Before(deadline) { req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, url, nil) if err != nil { t.Fatalf("health check request: %v", err) } resp, err := http.DefaultClient.Do(req) if err == nil { _ = resp.Body.Close() if resp.StatusCode == http.StatusOK { return } } time.Sleep(100 * time.Millisecond) } t.Fatalf("pixad did not answer its health check on port %d", port) }