Go-based FM stereo transmitter with RDS, Windows-first and cross-platform
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

201 строка
6.1KB

  1. package control
  2. import (
  3. "bytes"
  4. "encoding/json"
  5. "errors"
  6. "net/http"
  7. "net/http/httptest"
  8. "testing"
  9. cfgpkg "github.com/jan/fm-rds-tx/internal/config"
  10. "github.com/jan/fm-rds-tx/internal/output"
  11. )
  12. func TestHealthz(t *testing.T) {
  13. srv := NewServer(cfgpkg.Default())
  14. rec := httptest.NewRecorder()
  15. srv.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/healthz", nil))
  16. if rec.Code != 200 {
  17. t.Fatalf("status: %d", rec.Code)
  18. }
  19. }
  20. func TestStatus(t *testing.T) {
  21. srv := NewServer(cfgpkg.Default())
  22. rec := httptest.NewRecorder()
  23. srv.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/status", nil))
  24. if rec.Code != 200 {
  25. t.Fatalf("status: %d", rec.Code)
  26. }
  27. var body map[string]any
  28. json.Unmarshal(rec.Body.Bytes(), &body)
  29. if body["service"] != "fm-rds-tx" {
  30. t.Fatal("missing service")
  31. }
  32. if _, ok := body["preEmphasisTauUS"]; !ok {
  33. t.Fatal("missing preEmphasisTauUS")
  34. }
  35. }
  36. func TestStatusReportsRuntimeIndicator(t *testing.T) {
  37. srv := NewServer(cfgpkg.Default())
  38. srv.SetTXController(&fakeTXController{stats: map[string]any{"runtimeIndicator": "degraded", "runtimeAlert": "late buffers"}})
  39. rec := httptest.NewRecorder()
  40. srv.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/status", nil))
  41. if rec.Code != 200 {
  42. t.Fatalf("status: %d", rec.Code)
  43. }
  44. var body map[string]any
  45. json.Unmarshal(rec.Body.Bytes(), &body)
  46. if body["runtimeIndicator"] != "degraded" {
  47. t.Fatalf("expected runtimeIndicator degraded, got %v", body["runtimeIndicator"])
  48. }
  49. if body["runtimeAlert"] != "late buffers" {
  50. t.Fatalf("expected runtimeAlert late buffers, got %v", body["runtimeAlert"])
  51. }
  52. }
  53. func TestStatusReportsQueueStats(t *testing.T) {
  54. cfg := cfgpkg.Default()
  55. queueStats := output.QueueStats{
  56. Capacity: cfg.Runtime.FrameQueueCapacity,
  57. Depth: 1,
  58. FillLevel: 0.25,
  59. Health: output.QueueHealthLow,
  60. }
  61. srv := NewServer(cfg)
  62. srv.SetTXController(&fakeTXController{stats: map[string]any{"queue": queueStats}})
  63. rec := httptest.NewRecorder()
  64. srv.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/status", nil))
  65. if rec.Code != 200 {
  66. t.Fatalf("status: %d", rec.Code)
  67. }
  68. var body map[string]any
  69. if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
  70. t.Fatalf("unmarshal queue stats: %v", err)
  71. }
  72. queueRaw, ok := body["queue"]
  73. if !ok {
  74. t.Fatalf("missing queue in status")
  75. }
  76. queueMap, ok := queueRaw.(map[string]any)
  77. if !ok {
  78. t.Fatalf("queue stats type mismatch: %T", queueRaw)
  79. }
  80. if queueMap["capacity"] != float64(queueStats.Capacity) {
  81. t.Fatalf("queue capacity mismatch: want %v got %v", queueStats.Capacity, queueMap["capacity"])
  82. }
  83. if queueMap["health"] != string(queueStats.Health) {
  84. t.Fatalf("queue health mismatch: want %s got %v", queueStats.Health, queueMap["health"])
  85. }
  86. }
  87. func TestDryRunEndpoint(t *testing.T) {
  88. srv := NewServer(cfgpkg.Default())
  89. rec := httptest.NewRecorder()
  90. srv.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/dry-run", nil))
  91. if rec.Code != 200 {
  92. t.Fatalf("status: %d", rec.Code)
  93. }
  94. var body map[string]any
  95. json.Unmarshal(rec.Body.Bytes(), &body)
  96. if body["mode"] != "dry-run" {
  97. t.Fatal("wrong mode")
  98. }
  99. }
  100. func TestConfigPatch(t *testing.T) {
  101. srv := NewServer(cfgpkg.Default())
  102. body := []byte(`{"toneLeftHz":900,"radioText":"hello world","preEmphasisTauUS":75}`)
  103. rec := httptest.NewRecorder()
  104. srv.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodPost, "/config", bytes.NewReader(body)))
  105. if rec.Code != 200 {
  106. t.Fatalf("status: %d body=%s", rec.Code, rec.Body.String())
  107. }
  108. }
  109. func TestRuntimeWithoutDriver(t *testing.T) {
  110. srv := NewServer(cfgpkg.Default())
  111. rec := httptest.NewRecorder()
  112. srv.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/runtime", nil))
  113. if rec.Code != 200 {
  114. t.Fatalf("status: %d", rec.Code)
  115. }
  116. }
  117. func TestTXStartWithoutController(t *testing.T) {
  118. srv := NewServer(cfgpkg.Default())
  119. rec := httptest.NewRecorder()
  120. srv.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodPost, "/tx/start", nil))
  121. if rec.Code != http.StatusServiceUnavailable {
  122. t.Fatalf("expected 503, got %d", rec.Code)
  123. }
  124. }
  125. func TestConfigPatchUpdatesSnapshot(t *testing.T) {
  126. srv := NewServer(cfgpkg.Default())
  127. srv.SetTXController(&fakeTXController{})
  128. rec := httptest.NewRecorder()
  129. body := []byte(`{"outputDrive":1.2}`)
  130. srv.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodPost, "/config", bytes.NewReader(body)))
  131. if rec.Code != 200 {
  132. t.Fatalf("status: %d", rec.Code)
  133. }
  134. var resp map[string]any
  135. if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
  136. t.Fatalf("unmarshal response: %v", err)
  137. }
  138. if live, ok := resp["live"].(bool); !ok || !live {
  139. t.Fatalf("expected live true, got %v", resp["live"])
  140. }
  141. rec = httptest.NewRecorder()
  142. srv.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/config", nil))
  143. var cfg cfgpkg.Config
  144. if err := json.NewDecoder(rec.Body).Decode(&cfg); err != nil {
  145. t.Fatalf("decode config: %v", err)
  146. }
  147. if cfg.FM.OutputDrive != 1.2 {
  148. t.Fatalf("expected snapshot to reflect new drive, got %v", cfg.FM.OutputDrive)
  149. }
  150. }
  151. func TestConfigPatchEngineRejectsDoesNotUpdateSnapshot(t *testing.T) {
  152. srv := NewServer(cfgpkg.Default())
  153. srv.SetTXController(&fakeTXController{updateErr: errors.New("boom")})
  154. body := []byte(`{"outputDrive":2.2}`)
  155. rec := httptest.NewRecorder()
  156. srv.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodPost, "/config", bytes.NewReader(body)))
  157. if rec.Code != http.StatusBadRequest {
  158. t.Fatalf("expected 400, got %d", rec.Code)
  159. }
  160. rec = httptest.NewRecorder()
  161. srv.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/config", nil))
  162. var cfg cfgpkg.Config
  163. if err := json.NewDecoder(rec.Body).Decode(&cfg); err != nil {
  164. t.Fatalf("decode config: %v", err)
  165. }
  166. if cfg.FM.OutputDrive != cfgpkg.Default().FM.OutputDrive {
  167. t.Fatalf("expected snapshot untouched, got %v", cfg.FM.OutputDrive)
  168. }
  169. }
  170. type fakeTXController struct {
  171. updateErr error
  172. stats map[string]any
  173. }
  174. func (f *fakeTXController) StartTX() error { return nil }
  175. func (f *fakeTXController) StopTX() error { return nil }
  176. func (f *fakeTXController) TXStats() map[string]any {
  177. if f.stats != nil {
  178. return f.stats
  179. }
  180. return map[string]any{}
  181. }
  182. func (f *fakeTXController) UpdateConfig(_ LivePatch) error { return f.updateErr }