package main import ( "fmt" "net" "net/http" "os" "os/exec" "path/filepath" "syscall" "testing" "time" "sourcecraft.dev/bigbes/sr-ht-ecore/ecoretest" ) // TestStartupSmoke builds the daemon, runs it against a synthesized minimal // config.ini (ecore's test keyset, an empty repos root), waits for /healthz to // answer 200, then sends SIGINT and asserts a clean (exit code 0) warm // shutdown. It exercises the real startup path end to end: config validation, // crypto init via server.New, web.New (which resolves the embedded hashed // artefacts and parses every page template), the middleware chain, and // core-go's signal-driven Run loop. // // server.New's Run installs its warm-shutdown handler on SIGINT (the SourceHut // fleet convention); the deployed systemd unit sets KillSignal=SIGINT so // `systemctl stop` triggers the same graceful path this test verifies. func TestStartupSmoke(t *testing.T) { if testing.Short() { t.Skip("smoke test builds a binary; skipped in -short mode") } if _, err := exec.LookPath("go"); err != nil { t.Skipf("go toolchain not available: %v", err) } // Build the daemon from the current package directory. binDir := t.TempDir() bin := filepath.Join(binDir, "comparesrht") build := exec.Command("go", "build", "-o", bin, ".") if out, err := build.CombinedOutput(); err != nil { t.Fatalf("go build: %v\n%s", err, out) } // Synthesize a config.ini around ecore's test keyset and an empty repos // root. The keys are constants rather than generated ones because they // secure nothing here — nothing outside this test ever sees them, and a // constant cannot be malformed by accident, which matters for a subprocess // whose only way of complaining is a log.Fatal on startup. confDir := t.TempDir() // becomes the process cwd; LoadConfig finds config.ini here reposDir := t.TempDir() confBody := fmt.Sprintf(`[sr.ht] network-key=%s site-name=%s environment=development [webhooks] private-key=%s [compare.sr.ht] origin=%s [meta.sr.ht] origin=%s [git.sr.ht] origin=%s repos=%s `, ecoretest.NetworkKey, ecoretest.SiteName, ecoretest.WebhookKey, ecoretest.Origin(service), ecoretest.Origin("meta.sr.ht"), ecoretest.Origin("git.sr.ht"), reposDir) if err := os.WriteFile(filepath.Join(confDir, "config.ini"), []byte(confBody), 0o644); err != nil { t.Fatalf("write config.ini: %v", err) } addr := freeAddr(t) cmd := exec.Command(bin, "-b", addr) cmd.Dir = confDir // so config.LoadConfig picks up ./config.ini cmd.Stdout = os.Stderr cmd.Stderr = os.Stderr if err := cmd.Start(); err != nil { t.Fatalf("start daemon: %v", err) } // Guarantee the child is reaped even if an assertion below fails early. defer func() { if cmd.ProcessState == nil { _ = cmd.Process.Kill() _ = cmd.Wait() } }() healthz := "http://" + addr + "/healthz" if !waitForOK(healthz, 10*time.Second) { t.Fatalf("daemon did not become healthy at %s within timeout", healthz) } // Warm shutdown: core-go's Run listens for SIGINT. if err := cmd.Process.Signal(syscall.SIGINT); err != nil { t.Fatalf("signal daemon: %v", err) } done := make(chan error, 1) go func() { done <- cmd.Wait() }() select { case err := <-done: if err != nil { t.Fatalf("daemon exited uncleanly after SIGINT: %v", err) } case <-time.After(10 * time.Second): _ = cmd.Process.Kill() t.Fatal("daemon did not shut down within 10s of SIGINT") } } // freeAddr reserves and immediately releases a loopback TCP port, returning its // host:port. The daemon binds with SO_REUSEPORT so the brief release window is // harmless. func freeAddr(t *testing.T) string { t.Helper() l, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("reserve port: %v", err) } addr := l.Addr().String() _ = l.Close() return addr } // waitForOK polls url until it returns HTTP 200 or the deadline elapses. func waitForOK(url string, timeout time.Duration) bool { deadline := time.Now().Add(timeout) client := &http.Client{Timeout: time.Second} for time.Now().Before(deadline) { resp, err := client.Get(url) if err == nil { _ = resp.Body.Close() if resp.StatusCode == http.StatusOK { return true } } time.Sleep(100 * time.Millisecond) } return false }