// SPDX-License-Identifier: GPL-2.0-or-later // Copyright (C) 2026 Bryan Joshua Pedini package main import ( "context" "database/sql" "errors" "fmt" "io" "log/slog" "net" "net/http" "os" "strconv" "strings" "sync" "syscall" "testing" "time" ) // serverHarness runs app.run in server mode with an ephemeral listener and a fake Docker client type serverHarness struct { t *testing.T path string docker *fakeDocker stderr *syncBuffer stdout *syncBuffer cancel context.CancelFunc exited chan struct{} code int addr string network string reqAddr string } func startServer(t *testing.T, env map[string]string, configure func(a *app)) *serverHarness { t.Helper() h := &serverHarness{ t: t, path: testDBPath(t), docker: &fakeDocker{}, stderr: &syncBuffer{}, stdout: &syncBuffer{}, exited: make(chan struct{}), } if env == nil { env = map[string]string{} } if _, ok := env["DB_PATH"]; !ok { env["DB_PATH"] = h.path } h.path = env["DB_PATH"] a := newApp(envMap(env), h.stdout, h.stderr) ready := make(chan string, 1) a.newDocker = func(*slog.Logger) (dockerConn, error) { return h.docker, nil } a.listen = func(network, address string) (net.Listener, error) { h.network, h.reqAddr = network, address ln, err := net.Listen("tcp", "127.0.0.1:0") if err == nil { ready <- ln.Addr().String() } return ln, err } if configure != nil { configure(a) } ctx, cancel := context.WithCancel(context.Background()) h.cancel = cancel go func() { h.code = a.run(ctx, nil) close(h.exited) }() select { case h.addr = <-ready: case <-h.exited: case <-time.After(10 * time.Second): t.Fatal("server did not start") } t.Cleanup(func() { cancel() select { case <-h.exited: case <-time.After(10 * time.Second): } }) return h } func (h *serverHarness) wait() int { h.t.Helper() select { case <-h.exited: return h.code case <-time.After(20 * time.Second): h.t.Fatal("server did not exit") return -1 } } func (h *serverHarness) get(path string, header http.Header) (*http.Response, string) { h.t.Helper() req, _ := http.NewRequest("GET", "http://"+h.addr+path, nil) if header != nil { req.Header = header } resp, err := http.DefaultClient.Do(req) if err != nil { h.t.Fatal(err) } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) return resp, string(body) } func TestServerGracefulShutdown(t *testing.T) { t.Parallel() path := testDBPath(t) tok := strings.TrimSpace(cli(t, path, nil, nil, "token", "new", "srv").stdout) h := startServer(t, map[string]string{"DB_PATH": path, "HTTP_ADDR": "127.0.0.1", "HTTP_PORT": "8080", "LOG_LEVEL": "DEBUG"}, nil) if h.network != "tcp4" || h.reqAddr != "127.0.0.1:8080" { t.Fatalf("listen(%q, %q)", h.network, h.reqAddr) } h.docker.containers = makeContainers(1) resp, body := h.get("/api/v1/containers", bearer(tok)) if resp.StatusCode != 200 || !strings.HasPrefix(body, `[{"Id":"`) { t.Fatalf("got %d %s", resp.StatusCode, body) } resp, body = h.get("/api/v1/containers", nil) if resp.StatusCode != 401 || body != `{"error":"unauthorized"}` { t.Fatalf("got %d %s", resp.StatusCode, body) } h.cancel() if code := h.wait(); code != 0 { t.Fatalf("exit code %d, stderr:\n%s", code, h.stderr.String()) } if h.stdout.String() != "" { t.Fatalf("server wrote to stdout: %q", h.stdout.String()) } out := h.stderr.String() for _, want := range []string{ `level=DEBUG msg="configuration resolved"`, `level=DEBUG msg=dispatch mode=server`, `level=DEBUG msg="database opened"`, `level=DEBUG msg="docker client created" host=unix:///var/run/docker.sock`, `level=DEBUG msg="routes registered" route="GET /api/v1/containers"`, `level=INFO msg="server listening" addr=` + h.addr + ` version=latest commit=undefined`, `level=INFO msg="this request has been made" method=GET path=/api/v1/containers status=200`, `level=INFO msg="this request has been made" method=GET path=/api/v1/containers status=401`, `level=DEBUG msg="shutdown requested, draining http server" timeout=15s`, `level=DEBUG msg="http server shut down"`, `level=INFO msg="server stopped"`, `level=DEBUG msg="database closed"`, } { if !strings.Contains(out, want) { t.Errorf("missing %q in:\n%s", want, out) } } if strings.Contains(out, tok) { t.Fatal("token leaked into server logs") } if !h.docker.closed { t.Fatal("docker client not closed") } // The audit row is in the database after shutdown db := openTestDBAt(t, path) if n := countRows(t, db, "operations"); n != 1 { t.Fatalf("operations = %d", n) } } func openTestDBAt(t *testing.T, path string) *sql.DB { t.Helper() db, err := OpenDatabase(context.Background(), path, traceDiscard()) if err != nil { t.Fatal(err) } t.Cleanup(func() { db.Close() }) return db } func TestServerInfoLevelOutput(t *testing.T) { t.Parallel() h := startServer(t, nil, nil) h.get("/nope", nil) h.cancel() if code := h.wait(); code != 0 { t.Fatalf("exit %d", code) } lines := strings.Split(strings.TrimSpace(h.stderr.String()), "\n") if len(lines) != 3 || !strings.Contains(lines[0], `level=INFO msg="server listening"`) || !strings.Contains(lines[1], `level=INFO msg="this request has been made" method=GET path=/nope status=404 duration=`) || !strings.Contains(lines[1], ` origin=127.0.0.1`) || !strings.Contains(lines[2], `level=INFO msg="server stopped"`) { t.Fatalf("unexpected INFO output:\n%s", h.stderr.String()) } } func TestServerErrorLevelIsSilent(t *testing.T) { t.Parallel() h := startServer(t, map[string]string{"LOG_LEVEL": "error"}, nil) h.get("/nope", nil) h.cancel() if code := h.wait(); code != 0 || h.stderr.String() != "" { t.Fatalf("exit %d stderr %q", code, h.stderr.String()) } } // failingListener returns a permanent error from Accept type failingListener struct { net.Listener } func (failingListener) Accept() (net.Conn, error) { return nil, errors.New("accept exploded") } func (failingListener) Close() error { return nil } func (failingListener) Addr() net.Addr { return &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 1} } func TestServerErrorPropagation(t *testing.T) { t.Parallel() stderr := &syncBuffer{} a := newApp(envMap(map[string]string{"DB_PATH": testDBPath(t)}), io.Discard, stderr) a.newDocker = func(*slog.Logger) (dockerConn, error) { return &fakeDocker{}, nil } a.listen = func(string, string) (net.Listener, error) { return failingListener{}, nil } code := a.run(context.Background(), nil) if code != 1 || !strings.HasSuffix(stderr.String(), "dpsapi: http server: accept exploded\n") { t.Fatalf("code=%d stderr=%q", code, stderr.String()) } } func TestServerListenFailurePortInUse(t *testing.T) { t.Parallel() busy, err := net.Listen("tcp4", "127.0.0.1:0") if err != nil { t.Fatal(err) } defer busy.Close() port := busy.Addr().(*net.TCPAddr).Port stderr := &syncBuffer{} a := newApp(envMap(map[string]string{ "DB_PATH": testDBPath(t), "HTTP_ADDR": "127.0.0.1", "HTTP_PORT": strconv.Itoa(port), }), io.Discard, stderr) a.newDocker = func(*slog.Logger) (dockerConn, error) { return &fakeDocker{}, nil } code := a.run(context.Background(), nil) want := fmt.Sprintf("dpsapi: listen tcp4 127.0.0.1:%d: bind: address already in use\n", port) if code != 1 || stderr.String() != want { t.Fatalf("code=%d stderr=%q want %q", code, stderr.String(), want) } } func TestServerShutdownFailure(t *testing.T) { t.Parallel() path := testDBPath(t) tok := strings.TrimSpace(cli(t, path, nil, nil, "token", "new").stdout) release := make(chan struct{}) entered := make(chan struct{}) h := startServer(t, map[string]string{"DB_PATH": path}, func(a *app) { a.shutdownTimeout = 50 * time.Millisecond }) var once sync.Once h.docker.onList = func(ctx context.Context) { once.Do(func() { close(entered) }) <-release } defer close(release) // Start a request that stays in flight go func() { req, _ := http.NewRequest("GET", "http://"+h.addr+"/api/v1/containers", nil) req.Header = bearer(tok) if resp, err := http.DefaultClient.Do(req); err == nil { resp.Body.Close() } }() select { case <-entered: case <-time.After(10 * time.Second): t.Fatal("request did not reach Docker") } h.cancel() code := h.wait() if code != 1 || !strings.HasSuffix(h.stderr.String(), "dpsapi: http server shutdown: context deadline exceeded\n") { t.Fatalf("code=%d stderr=%q", code, h.stderr.String()) } } func TestServerStartupDatabaseFailure(t *testing.T) { t.Parallel() stderr := &syncBuffer{} a := newApp(envMap(map[string]string{"DB_PATH": t.TempDir()}), io.Discard, stderr) listened := false a.listen = func(string, string) (net.Listener, error) { listened = true; return nil, errors.New("unreachable") } code := a.run(context.Background(), nil) if code != 1 || listened || !strings.HasPrefix(stderr.String(), "dpsapi: initialize database schema: ") { t.Fatalf("code=%d listened=%v stderr=%q", code, listened, stderr.String()) } } func TestServerDockerClientFailure(t *testing.T) { t.Parallel() stderr := &syncBuffer{} a := newApp(envMap(map[string]string{"DB_PATH": testDBPath(t)}), io.Discard, stderr) a.newDocker = func(*slog.Logger) (dockerConn, error) { return nil, errors.New("create docker client: bad host") } code := a.run(context.Background(), nil) if code != 1 || stderr.String() != "dpsapi: create docker client: bad host\n" { t.Fatalf("code=%d stderr=%q", code, stderr.String()) } } func TestServerProductionDockerFactory(t *testing.T) { t.Parallel() // The default factory builds a lazy client for the production socket without connecting a := newApp(envMap(nil), io.Discard, io.Discard) d, err := a.newDocker(traceDiscard()) if err != nil { t.Fatal(err) } defer d.Close() if _, ok := d.(*DockerClient); !ok { t.Fatalf("factory returned %T", d) } } // TestRunSIGTERM drives the real entry point and delivers a real SIGTERM to the test process // It is deliberately not parallel; the signal is sent only once the server is confirmed to be serving, // so signal.NotifyContext is registered and the signal cannot terminate the test binary func TestRunSIGTERM(t *testing.T) { probe, err := net.Listen("tcp4", "127.0.0.1:0") if err != nil { t.Fatal(err) } port := probe.Addr().(*net.TCPAddr).Port probe.Close() stderr := &syncBuffer{} env := envMap(map[string]string{"DB_PATH": testDBPath(t), "HTTP_ADDR": "127.0.0.1", "HTTP_PORT": strconv.Itoa(port)}) done := make(chan int, 1) go func() { done <- run(nil, env, io.Discard, stderr) }() // Wait until our server answers (a 404 JSON body proves it is ours) deadline := time.Now().Add(10 * time.Second) for { resp, err := http.Get(fmt.Sprintf("http://127.0.0.1:%d/ready-probe", port)) if err == nil { body, _ := io.ReadAll(resp.Body) resp.Body.Close() if string(body) == bodyNotFound { break } } select { case code := <-done: t.Fatalf("server exited early with %d: %s", code, stderr.String()) default: } if time.Now().After(deadline) { t.Fatalf("server not ready: %s", stderr.String()) } time.Sleep(20 * time.Millisecond) } if err := syscall.Kill(os.Getpid(), syscall.SIGTERM); err != nil { t.Fatal(err) } select { case code := <-done: if code != 0 { t.Fatalf("exit code %d: %s", code, stderr.String()) } case <-time.After(20 * time.Second): t.Fatal("server did not stop on SIGTERM") } if !strings.Contains(stderr.String(), `msg="server stopped"`) { t.Fatalf("stderr:\n%s", stderr.String()) } } func TestExitCodeMapping(t *testing.T) { t.Parallel() for _, c := range []struct { err error code int }{ {nil, 0}, {usageErrorf("x"), 2}, {configErrorf("x"), 2}, {ErrTokenNotFound, 1}, {ErrTokenInactive, 1}, {errors.New("anything"), 1}, } { a := newApp(envMap(nil), io.Discard, io.Discard) if got := a.exitCode(c.err); got != c.code { t.Errorf("exitCode(%v) = %d, want %d", c.err, got, c.code) } } }