package control import ( "bytes" "encoding/json" "errors" "net/http" "net/http/httptest" "testing" cfgpkg "github.com/jan/fm-rds-tx/internal/config" ) func TestHealthz(t *testing.T) { srv := NewServer(cfgpkg.Default()) rec := httptest.NewRecorder() srv.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/healthz", nil)) if rec.Code != 200 { t.Fatalf("status: %d", rec.Code) } } func TestStatus(t *testing.T) { srv := NewServer(cfgpkg.Default()) rec := httptest.NewRecorder() srv.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/status", nil)) if rec.Code != 200 { t.Fatalf("status: %d", rec.Code) } var body map[string]any json.Unmarshal(rec.Body.Bytes(), &body) if body["service"] != "fm-rds-tx" { t.Fatal("missing service") } if _, ok := body["preEmphasisTauUS"]; !ok { t.Fatal("missing preEmphasisTauUS") } } func TestStatusReportsRuntimeIndicator(t *testing.T) { srv := NewServer(cfgpkg.Default()) srv.SetTXController(&fakeTXController{stats: map[string]any{"runtimeIndicator": "degraded"}}) rec := httptest.NewRecorder() srv.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/status", nil)) if rec.Code != 200 { t.Fatalf("status: %d", rec.Code) } var body map[string]any json.Unmarshal(rec.Body.Bytes(), &body) if body["runtimeIndicator"] != "degraded" { t.Fatalf("expected runtimeIndicator degraded, got %v", body["runtimeIndicator"]) } } func TestDryRunEndpoint(t *testing.T) { srv := NewServer(cfgpkg.Default()) rec := httptest.NewRecorder() srv.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/dry-run", nil)) if rec.Code != 200 { t.Fatalf("status: %d", rec.Code) } var body map[string]any json.Unmarshal(rec.Body.Bytes(), &body) if body["mode"] != "dry-run" { t.Fatal("wrong mode") } } func TestConfigPatch(t *testing.T) { srv := NewServer(cfgpkg.Default()) body := []byte(`{"toneLeftHz":900,"radioText":"hello world","preEmphasisTauUS":75}`) rec := httptest.NewRecorder() srv.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodPost, "/config", bytes.NewReader(body))) if rec.Code != 200 { t.Fatalf("status: %d body=%s", rec.Code, rec.Body.String()) } } func TestRuntimeWithoutDriver(t *testing.T) { srv := NewServer(cfgpkg.Default()) rec := httptest.NewRecorder() srv.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/runtime", nil)) if rec.Code != 200 { t.Fatalf("status: %d", rec.Code) } } func TestTXStartWithoutController(t *testing.T) { srv := NewServer(cfgpkg.Default()) rec := httptest.NewRecorder() srv.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodPost, "/tx/start", nil)) if rec.Code != http.StatusServiceUnavailable { t.Fatalf("expected 503, got %d", rec.Code) } } func TestConfigPatchUpdatesSnapshot(t *testing.T) { srv := NewServer(cfgpkg.Default()) srv.SetTXController(&fakeTXController{}) rec := httptest.NewRecorder() body := []byte(`{"outputDrive":1.2}`) srv.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodPost, "/config", bytes.NewReader(body))) if rec.Code != 200 { t.Fatalf("status: %d", rec.Code) } var resp map[string]any if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal response: %v", err) } if live, ok := resp["live"].(bool); !ok || !live { t.Fatalf("expected live true, got %v", resp["live"]) } rec = httptest.NewRecorder() srv.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/config", nil)) var cfg cfgpkg.Config if err := json.NewDecoder(rec.Body).Decode(&cfg); err != nil { t.Fatalf("decode config: %v", err) } if cfg.FM.OutputDrive != 1.2 { t.Fatalf("expected snapshot to reflect new drive, got %v", cfg.FM.OutputDrive) } } func TestConfigPatchEngineRejectsDoesNotUpdateSnapshot(t *testing.T) { srv := NewServer(cfgpkg.Default()) srv.SetTXController(&fakeTXController{updateErr: errors.New("boom")}) body := []byte(`{"outputDrive":2.2}`) rec := httptest.NewRecorder() srv.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodPost, "/config", bytes.NewReader(body))) if rec.Code != http.StatusBadRequest { t.Fatalf("expected 400, got %d", rec.Code) } rec = httptest.NewRecorder() srv.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/config", nil)) var cfg cfgpkg.Config if err := json.NewDecoder(rec.Body).Decode(&cfg); err != nil { t.Fatalf("decode config: %v", err) } if cfg.FM.OutputDrive != cfgpkg.Default().FM.OutputDrive { t.Fatalf("expected snapshot untouched, got %v", cfg.FM.OutputDrive) } } type fakeTXController struct { updateErr error stats map[string]any } func (f *fakeTXController) StartTX() error { return nil } func (f *fakeTXController) StopTX() error { return nil } func (f *fakeTXController) TXStats() map[string]any { if f.stats != nil { return f.stats } return map[string]any{} } func (f *fakeTXController) UpdateConfig(_ LivePatch) error { return f.updateErr }