package main import ( "context" "encoding/json" "fmt" "net" "net/http" "os" "os/exec" "path/filepath" "testing" "time" ) func TestServerHealthViaSubprocess(t *testing.T) { dir := t.TempDir() port := freePort(t) cfgPath := writeServerConfig(t, dir, port) bin := buildServerBinary(t) ctx, cancel := context.WithCancel(context.Background()) defer cancel() cmd := exec.CommandContext(ctx, bin, "-config", cfgPath) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Start(); err != nil { t.Fatalf("start server: %v", err) } t.Cleanup(func() { cancel() _ = cmd.Wait() }) base := fmt.Sprintf("http://127.0.0.1:%d", port) waitForHealth(t, base+"/api/v1/health", 15*time.Second) resp, err := http.Get(base + "/api/v1/health") if err != nil { t.Fatalf("health GET: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("health status: got %d want 200", resp.StatusCode) } var body struct { Status string `json:"status"` Service string `json:"service"` Version string `json:"version"` } if err := json.NewDecoder(resp.Body).Decode(&body); err != nil { t.Fatalf("decode health: %v", err) } if body.Status != "ok" { t.Fatalf("status: got %q want ok", body.Status) } if body.Service != "forge-mesh-server" { t.Fatalf("service: got %q", body.Service) } if body.Version == "" { t.Fatal("expected version in health response") } } func TestServerUsesConfigDataDirAndPort(t *testing.T) { dir := t.TempDir() port := freePort(t) cfgPath := writeServerConfig(t, dir, port) cfgData, err := os.ReadFile(cfgPath) if err != nil { t.Fatal(err) } if !containsAll(string(cfgData), dir, fmt.Sprintf("127.0.0.1:%d", port)) { t.Fatalf("config missing data_dir or listen_addr: %s", cfgData) } dbPath := filepath.Join(dir, "forge-mesh.db") artifacts := filepath.Join(dir, "artifacts") bin := buildServerBinary(t) ctx, cancel := context.WithTimeout(context.Background(), 12*time.Second) defer cancel() cmd := exec.CommandContext(ctx, bin, "-config", cfgPath) if err := cmd.Start(); err != nil { t.Fatalf("start: %v", err) } defer func() { cancel() _ = cmd.Wait() }() waitForHealth(t, fmt.Sprintf("http://127.0.0.1:%d/api/v1/health", port), 12*time.Second) for _, path := range []string{dbPath, artifacts, filepath.Join(dir, "signing.key")} { if _, err := os.Stat(path); err != nil { t.Fatalf("expected server to create %s: %v", path, err) } } } func buildServerBinary(t *testing.T) string { t.Helper() out := filepath.Join(t.TempDir(), "forge-mesh-server") cmd := exec.Command("go", "build", "-o", out, "./cmd/server") cmd.Dir = repoRoot(t) if outBytes, err := cmd.CombinedOutput(); err != nil { t.Fatalf("build server: %v\n%s", err, outBytes) } return out } func writeServerConfig(t *testing.T, dir string, port int) string { t.Helper() cfgPath := filepath.Join(dir, "config.json") content := fmt.Sprintf(`{ "listen_addr": "127.0.0.1:%d", "data_dir": %q, "database_path": %q, "auth": { "basic_username": "admin", "basic_password": "changeme", "fleet_secret": "test-fleet-secret" }, "forge": { "signing_key_path": %q, "artifacts_dir": %q } }`, port, dir, filepath.Join(dir, "forge-mesh.db"), filepath.Join(dir, "signing.key"), filepath.Join(dir, "artifacts")) if err := os.WriteFile(cfgPath, []byte(content), 0o644); err != nil { t.Fatal(err) } return cfgPath } func freePort(t *testing.T) int { t.Helper() ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } defer ln.Close() return ln.Addr().(*net.TCPAddr).Port } func waitForHealth(t *testing.T, url string, timeout time.Duration) { t.Helper() deadline := time.Now().Add(timeout) for time.Now().Before(deadline) { resp, err := http.Get(url) if err == nil { resp.Body.Close() if resp.StatusCode == http.StatusOK { return } } time.Sleep(200 * time.Millisecond) } t.Fatalf("server did not become healthy at %s within %s", url, timeout) } func repoRoot(t *testing.T) string { t.Helper() wd, err := os.Getwd() if err != nil { t.Fatal(err) } for { if _, err := os.Stat(filepath.Join(wd, "go.mod")); err == nil { return wd } parent := filepath.Dir(wd) if parent == wd { t.Fatal("go.mod not found") } wd = parent } } func containsAll(s string, parts ...string) bool { for _, p := range parts { if !contains(s, p) { return false } } return true } func contains(s, sub string) bool { return len(sub) == 0 || (len(s) >= len(sub) && indexOf(s, sub) >= 0) } func indexOf(s, sub string) int { for i := 0; i+len(sub) <= len(s); i++ { if s[i:i+len(sub)] == sub { return i } } return -1 }