|
- package control
-
- import (
- "bytes"
- "encoding/json"
- "errors"
- "net/http"
- "net/http/httptest"
- "testing"
-
- cfgpkg "github.com/jan/fm-rds-tx/internal/config"
- "github.com/jan/fm-rds-tx/internal/audio"
- "github.com/jan/fm-rds-tx/internal/output"
- )
-
- 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", "runtimeAlert": "late buffers"}})
- 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"])
- }
- if body["runtimeAlert"] != "late buffers" {
- t.Fatalf("expected runtimeAlert late buffers, got %v", body["runtimeAlert"])
- }
- }
-
- func TestStatusReportsQueueStats(t *testing.T) {
- cfg := cfgpkg.Default()
- queueStats := output.QueueStats{
- Capacity: cfg.Runtime.FrameQueueCapacity,
- Depth: 1,
- FillLevel: 0.25,
- Health: output.QueueHealthLow,
- }
- srv := NewServer(cfg)
- srv.SetTXController(&fakeTXController{stats: map[string]any{"queue": queueStats}})
- 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
- if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
- t.Fatalf("unmarshal queue stats: %v", err)
- }
- queueRaw, ok := body["queue"]
- if !ok {
- t.Fatalf("missing queue in status")
- }
- queueMap, ok := queueRaw.(map[string]any)
- if !ok {
- t.Fatalf("queue stats type mismatch: %T", queueRaw)
- }
- if queueMap["capacity"] != float64(queueStats.Capacity) {
- t.Fatalf("queue capacity mismatch: want %v got %v", queueStats.Capacity, queueMap["capacity"])
- }
- if queueMap["health"] != string(queueStats.Health) {
- t.Fatalf("queue health mismatch: want %s got %v", queueStats.Health, queueMap["health"])
- }
- }
-
- 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 TestRuntimeFaultResetRejectsGet(t *testing.T) {
- srv := NewServer(cfgpkg.Default())
- rec := httptest.NewRecorder()
- req := httptest.NewRequest(http.MethodGet, "/runtime/fault/reset", nil)
- srv.Handler().ServeHTTP(rec, req)
- if rec.Code != http.StatusMethodNotAllowed {
- t.Fatalf("expected 405 for fault reset GET, got %d", rec.Code)
- }
- }
-
- func TestRuntimeFaultResetRequiresController(t *testing.T) {
- srv := NewServer(cfgpkg.Default())
- rec := httptest.NewRecorder()
- req := httptest.NewRequest(http.MethodPost, "/runtime/fault/reset", nil)
- srv.Handler().ServeHTTP(rec, req)
- if rec.Code != http.StatusServiceUnavailable {
- t.Fatalf("expected 503 without controller, got %d", rec.Code)
- }
- }
-
- func TestRuntimeFaultResetControllerError(t *testing.T) {
- srv := NewServer(cfgpkg.Default())
- srv.SetTXController(&fakeTXController{resetErr: errors.New("boom")})
- rec := httptest.NewRecorder()
- req := httptest.NewRequest(http.MethodPost, "/runtime/fault/reset", nil)
- srv.Handler().ServeHTTP(rec, req)
- if rec.Code != http.StatusConflict {
- t.Fatalf("expected 409 when controller rejects, got %d", rec.Code)
- }
- }
-
- func TestRuntimeFaultResetSuccess(t *testing.T) {
- srv := NewServer(cfgpkg.Default())
- srv.SetTXController(&fakeTXController{})
- rec := httptest.NewRecorder()
- req := httptest.NewRequest(http.MethodPost, "/runtime/fault/reset", nil)
- srv.Handler().ServeHTTP(rec, req)
- if rec.Code != 200 {
- t.Fatalf("expected 200 on success, got %d", rec.Code)
- }
- var body map[string]any
- if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
- t.Fatalf("unmarshal response: %v", err)
- }
- if ok, _ := body["ok"].(bool); !ok {
- t.Fatalf("expected ok true, got %v", body["ok"])
- }
- }
-
- func TestAudioStreamRequiresSource(t *testing.T) {
- srv := NewServer(cfgpkg.Default())
- rec := httptest.NewRecorder()
- req := httptest.NewRequest(http.MethodPost, "/audio/stream", bytes.NewReader(nil))
- srv.Handler().ServeHTTP(rec, req)
- if rec.Code != http.StatusServiceUnavailable {
- t.Fatalf("expected 503 when audio stream missing, got %d", rec.Code)
- }
- }
-
- func TestAudioStreamPushesPCM(t *testing.T) {
- cfg := cfgpkg.Default()
- srv := NewServer(cfg)
- stream := audio.NewStreamSource(256, 44100)
- srv.SetStreamSource(stream)
- pcm := []byte{0, 0, 0, 0}
- rec := httptest.NewRecorder()
- req := httptest.NewRequest(http.MethodPost, "/audio/stream", bytes.NewReader(pcm))
- srv.Handler().ServeHTTP(rec, req)
- if rec.Code != 200 {
- t.Fatalf("expected 200, got %d", rec.Code)
- }
- var body map[string]any
- if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
- t.Fatalf("unmarshal response: %v", err)
- }
- if ok, _ := body["ok"].(bool); !ok {
- t.Fatalf("expected ok true, got %v", body["ok"])
- }
- frames, _ := body["frames"].(float64)
- if frames != 1 {
- t.Fatalf("expected 1 frame, got %v", frames)
- }
- stats, ok := body["stats"].(map[string]any)
- if !ok {
- t.Fatalf("missing stats: %v", body["stats"])
- }
- if avail, _ := stats["available"].(float64); avail < 1 {
- t.Fatalf("expected stats.available >= 1, got %v", avail)
- }
- }
-
- func TestAudioStreamRejectsNonPost(t *testing.T) {
- srv := NewServer(cfgpkg.Default())
- rec := httptest.NewRecorder()
- req := httptest.NewRequest(http.MethodGet, "/audio/stream", nil)
- srv.Handler().ServeHTTP(rec, req)
- if rec.Code != http.StatusMethodNotAllowed {
- t.Fatalf("expected 405 for audio stream GET, got %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
- resetErr 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 }
- func (f *fakeTXController) ResetFault() error { return f.resetErr }
|