Test that pixad writes only its own errors to stderr (closes #359)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
Both tests run pixad as a child process. They fail against this code: every start writes the dependency-injection library's event log to stderr, and a startup error is printed twice inside its frames. Model: opus-5-5
This commit is contained in:
1 parent
092176d917
commit
fa6e7d698e
1 file changed
+164
@@ -0,0 +1,164 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
Reference in new issue
Block a user