Go-based FM stereo transmitter with RDS, Windows-first and cross-platform
Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.

305 wiersze
9.6KB

  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/audio"
  11. "github.com/jan/fm-rds-tx/internal/output"
  12. )
  13. func TestHealthz(t *testing.T) {
  14. srv := NewServer(cfgpkg.Default())
  15. rec := httptest.NewRecorder()
  16. srv.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/healthz", nil))
  17. if rec.Code != 200 {
  18. t.Fatalf("status: %d", rec.Code)
  19. }
  20. }
  21. func TestStatus(t *testing.T) {
  22. srv := NewServer(cfgpkg.Default())
  23. rec := httptest.NewRecorder()
  24. srv.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/status", nil))
  25. if rec.Code != 200 {
  26. t.Fatalf("status: %d", rec.Code)
  27. }
  28. var body map[string]any
  29. json.Unmarshal(rec.Body.Bytes(), &body)
  30. if body["service"] != "fm-rds-tx" {
  31. t.Fatal("missing service")
  32. }
  33. if _, ok := body["preEmphasisTauUS"]; !ok {
  34. t.Fatal("missing preEmphasisTauUS")
  35. }
  36. }
  37. func TestStatusReportsRuntimeIndicator(t *testing.T) {
  38. srv := NewServer(cfgpkg.Default())
  39. srv.SetTXController(&fakeTXController{stats: map[string]any{"runtimeIndicator": "degraded", "runtimeAlert": "late buffers"}})
  40. rec := httptest.NewRecorder()
  41. srv.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/status", nil))
  42. if rec.Code != 200 {
  43. t.Fatalf("status: %d", rec.Code)
  44. }
  45. var body map[string]any
  46. json.Unmarshal(rec.Body.Bytes(), &body)
  47. if body["runtimeIndicator"] != "degraded" {
  48. t.Fatalf("expected runtimeIndicator degraded, got %v", body["runtimeIndicator"])
  49. }
  50. if body["runtimeAlert"] != "late buffers" {
  51. t.Fatalf("expected runtimeAlert late buffers, got %v", body["runtimeAlert"])
  52. }
  53. }
  54. func TestStatusReportsQueueStats(t *testing.T) {
  55. cfg := cfgpkg.Default()
  56. queueStats := output.QueueStats{
  57. Capacity: cfg.Runtime.FrameQueueCapacity,
  58. Depth: 1,
  59. FillLevel: 0.25,
  60. Health: output.QueueHealthLow,
  61. }
  62. srv := NewServer(cfg)
  63. srv.SetTXController(&fakeTXController{stats: map[string]any{"queue": queueStats}})
  64. rec := httptest.NewRecorder()
  65. srv.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/status", nil))
  66. if rec.Code != 200 {
  67. t.Fatalf("status: %d", rec.Code)
  68. }
  69. var body map[string]any
  70. if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
  71. t.Fatalf("unmarshal queue stats: %v", err)
  72. }
  73. queueRaw, ok := body["queue"]
  74. if !ok {
  75. t.Fatalf("missing queue in status")
  76. }
  77. queueMap, ok := queueRaw.(map[string]any)
  78. if !ok {
  79. t.Fatalf("queue stats type mismatch: %T", queueRaw)
  80. }
  81. if queueMap["capacity"] != float64(queueStats.Capacity) {
  82. t.Fatalf("queue capacity mismatch: want %v got %v", queueStats.Capacity, queueMap["capacity"])
  83. }
  84. if queueMap["health"] != string(queueStats.Health) {
  85. t.Fatalf("queue health mismatch: want %s got %v", queueStats.Health, queueMap["health"])
  86. }
  87. }
  88. func TestDryRunEndpoint(t *testing.T) {
  89. srv := NewServer(cfgpkg.Default())
  90. rec := httptest.NewRecorder()
  91. srv.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/dry-run", nil))
  92. if rec.Code != 200 {
  93. t.Fatalf("status: %d", rec.Code)
  94. }
  95. var body map[string]any
  96. json.Unmarshal(rec.Body.Bytes(), &body)
  97. if body["mode"] != "dry-run" {
  98. t.Fatal("wrong mode")
  99. }
  100. }
  101. func TestConfigPatch(t *testing.T) {
  102. srv := NewServer(cfgpkg.Default())
  103. body := []byte(`{"toneLeftHz":900,"radioText":"hello world","preEmphasisTauUS":75}`)
  104. rec := httptest.NewRecorder()
  105. srv.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodPost, "/config", bytes.NewReader(body)))
  106. if rec.Code != 200 {
  107. t.Fatalf("status: %d body=%s", rec.Code, rec.Body.String())
  108. }
  109. }
  110. func TestRuntimeWithoutDriver(t *testing.T) {
  111. srv := NewServer(cfgpkg.Default())
  112. rec := httptest.NewRecorder()
  113. srv.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/runtime", nil))
  114. if rec.Code != 200 {
  115. t.Fatalf("status: %d", rec.Code)
  116. }
  117. }
  118. func TestRuntimeFaultResetRejectsGet(t *testing.T) {
  119. srv := NewServer(cfgpkg.Default())
  120. rec := httptest.NewRecorder()
  121. req := httptest.NewRequest(http.MethodGet, "/runtime/fault/reset", nil)
  122. srv.Handler().ServeHTTP(rec, req)
  123. if rec.Code != http.StatusMethodNotAllowed {
  124. t.Fatalf("expected 405 for fault reset GET, got %d", rec.Code)
  125. }
  126. }
  127. func TestRuntimeFaultResetRequiresController(t *testing.T) {
  128. srv := NewServer(cfgpkg.Default())
  129. rec := httptest.NewRecorder()
  130. req := httptest.NewRequest(http.MethodPost, "/runtime/fault/reset", nil)
  131. srv.Handler().ServeHTTP(rec, req)
  132. if rec.Code != http.StatusServiceUnavailable {
  133. t.Fatalf("expected 503 without controller, got %d", rec.Code)
  134. }
  135. }
  136. func TestRuntimeFaultResetControllerError(t *testing.T) {
  137. srv := NewServer(cfgpkg.Default())
  138. srv.SetTXController(&fakeTXController{resetErr: errors.New("boom")})
  139. rec := httptest.NewRecorder()
  140. req := httptest.NewRequest(http.MethodPost, "/runtime/fault/reset", nil)
  141. srv.Handler().ServeHTTP(rec, req)
  142. if rec.Code != http.StatusConflict {
  143. t.Fatalf("expected 409 when controller rejects, got %d", rec.Code)
  144. }
  145. }
  146. func TestRuntimeFaultResetSuccess(t *testing.T) {
  147. srv := NewServer(cfgpkg.Default())
  148. srv.SetTXController(&fakeTXController{})
  149. rec := httptest.NewRecorder()
  150. req := httptest.NewRequest(http.MethodPost, "/runtime/fault/reset", nil)
  151. srv.Handler().ServeHTTP(rec, req)
  152. if rec.Code != 200 {
  153. t.Fatalf("expected 200 on success, got %d", rec.Code)
  154. }
  155. var body map[string]any
  156. if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
  157. t.Fatalf("unmarshal response: %v", err)
  158. }
  159. if ok, _ := body["ok"].(bool); !ok {
  160. t.Fatalf("expected ok true, got %v", body["ok"])
  161. }
  162. }
  163. func TestAudioStreamRequiresSource(t *testing.T) {
  164. srv := NewServer(cfgpkg.Default())
  165. rec := httptest.NewRecorder()
  166. req := httptest.NewRequest(http.MethodPost, "/audio/stream", bytes.NewReader(nil))
  167. srv.Handler().ServeHTTP(rec, req)
  168. if rec.Code != http.StatusServiceUnavailable {
  169. t.Fatalf("expected 503 when audio stream missing, got %d", rec.Code)
  170. }
  171. }
  172. func TestAudioStreamPushesPCM(t *testing.T) {
  173. cfg := cfgpkg.Default()
  174. srv := NewServer(cfg)
  175. stream := audio.NewStreamSource(256, 44100)
  176. srv.SetStreamSource(stream)
  177. pcm := []byte{0, 0, 0, 0}
  178. rec := httptest.NewRecorder()
  179. req := httptest.NewRequest(http.MethodPost, "/audio/stream", bytes.NewReader(pcm))
  180. srv.Handler().ServeHTTP(rec, req)
  181. if rec.Code != 200 {
  182. t.Fatalf("expected 200, got %d", rec.Code)
  183. }
  184. var body map[string]any
  185. if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
  186. t.Fatalf("unmarshal response: %v", err)
  187. }
  188. if ok, _ := body["ok"].(bool); !ok {
  189. t.Fatalf("expected ok true, got %v", body["ok"])
  190. }
  191. frames, _ := body["frames"].(float64)
  192. if frames != 1 {
  193. t.Fatalf("expected 1 frame, got %v", frames)
  194. }
  195. stats, ok := body["stats"].(map[string]any)
  196. if !ok {
  197. t.Fatalf("missing stats: %v", body["stats"])
  198. }
  199. if avail, _ := stats["available"].(float64); avail < 1 {
  200. t.Fatalf("expected stats.available >= 1, got %v", avail)
  201. }
  202. }
  203. func TestAudioStreamRejectsNonPost(t *testing.T) {
  204. srv := NewServer(cfgpkg.Default())
  205. rec := httptest.NewRecorder()
  206. req := httptest.NewRequest(http.MethodGet, "/audio/stream", nil)
  207. srv.Handler().ServeHTTP(rec, req)
  208. if rec.Code != http.StatusMethodNotAllowed {
  209. t.Fatalf("expected 405 for audio stream GET, got %d", rec.Code)
  210. }
  211. }
  212. func TestTXStartWithoutController(t *testing.T) {
  213. srv := NewServer(cfgpkg.Default())
  214. rec := httptest.NewRecorder()
  215. srv.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodPost, "/tx/start", nil))
  216. if rec.Code != http.StatusServiceUnavailable {
  217. t.Fatalf("expected 503, got %d", rec.Code)
  218. }
  219. }
  220. func TestConfigPatchUpdatesSnapshot(t *testing.T) {
  221. srv := NewServer(cfgpkg.Default())
  222. srv.SetTXController(&fakeTXController{})
  223. rec := httptest.NewRecorder()
  224. body := []byte(`{"outputDrive":1.2}`)
  225. srv.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodPost, "/config", bytes.NewReader(body)))
  226. if rec.Code != 200 {
  227. t.Fatalf("status: %d", rec.Code)
  228. }
  229. var resp map[string]any
  230. if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
  231. t.Fatalf("unmarshal response: %v", err)
  232. }
  233. if live, ok := resp["live"].(bool); !ok || !live {
  234. t.Fatalf("expected live true, got %v", resp["live"])
  235. }
  236. rec = httptest.NewRecorder()
  237. srv.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/config", nil))
  238. var cfg cfgpkg.Config
  239. if err := json.NewDecoder(rec.Body).Decode(&cfg); err != nil {
  240. t.Fatalf("decode config: %v", err)
  241. }
  242. if cfg.FM.OutputDrive != 1.2 {
  243. t.Fatalf("expected snapshot to reflect new drive, got %v", cfg.FM.OutputDrive)
  244. }
  245. }
  246. func TestConfigPatchEngineRejectsDoesNotUpdateSnapshot(t *testing.T) {
  247. srv := NewServer(cfgpkg.Default())
  248. srv.SetTXController(&fakeTXController{updateErr: errors.New("boom")})
  249. body := []byte(`{"outputDrive":2.2}`)
  250. rec := httptest.NewRecorder()
  251. srv.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodPost, "/config", bytes.NewReader(body)))
  252. if rec.Code != http.StatusBadRequest {
  253. t.Fatalf("expected 400, got %d", rec.Code)
  254. }
  255. rec = httptest.NewRecorder()
  256. srv.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/config", nil))
  257. var cfg cfgpkg.Config
  258. if err := json.NewDecoder(rec.Body).Decode(&cfg); err != nil {
  259. t.Fatalf("decode config: %v", err)
  260. }
  261. if cfg.FM.OutputDrive != cfgpkg.Default().FM.OutputDrive {
  262. t.Fatalf("expected snapshot untouched, got %v", cfg.FM.OutputDrive)
  263. }
  264. }
  265. type fakeTXController struct {
  266. updateErr error
  267. resetErr error
  268. stats map[string]any
  269. }
  270. func (f *fakeTXController) StartTX() error { return nil }
  271. func (f *fakeTXController) StopTX() error { return nil }
  272. func (f *fakeTXController) TXStats() map[string]any {
  273. if f.stats != nil {
  274. return f.stats
  275. }
  276. return map[string]any{}
  277. }
  278. func (f *fakeTXController) UpdateConfig(_ LivePatch) error { return f.updateErr }
  279. func (f *fakeTXController) ResetFault() error { return f.resetErr }