device_vowifi_test.go 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. package server
  2. import (
  3. "bytes"
  4. "context"
  5. "io"
  6. "log/slog"
  7. "net/http"
  8. "net/http/httptest"
  9. "testing"
  10. "vocat/internal/store"
  11. "vocat/internal/vowifi"
  12. vowifiruntime "vocat/internal/vowifi/runtime"
  13. )
  14. type fakeVoWiFiController struct {
  15. state vowifi.State
  16. enabled []bool
  17. reconnects int
  18. err error
  19. }
  20. func (controller *fakeVoWiFiController) State(string) (vowifi.State, error) {
  21. return controller.state, controller.err
  22. }
  23. func (controller *fakeVoWiFiController) RequestEnabled(
  24. _ string,
  25. enabled bool,
  26. ) (vowifi.State, error) {
  27. controller.enabled = append(controller.enabled, enabled)
  28. return controller.state, controller.err
  29. }
  30. func (controller *fakeVoWiFiController) RequestReconnect(string) (vowifi.State, error) {
  31. controller.reconnects++
  32. return controller.state, controller.err
  33. }
  34. func TestShouldDeferModemSMSSync(t *testing.T) {
  35. tests := []struct {
  36. name string
  37. state vowifi.State
  38. err error
  39. want bool
  40. }{
  41. {
  42. name: "cellular session",
  43. state: vowifi.State{Phase: vowifi.PhaseIdle},
  44. },
  45. {
  46. name: "vowifi SIM setup",
  47. state: vowifi.State{Enabled: true, Phase: vowifi.PhaseSIMReady},
  48. want: true,
  49. },
  50. {
  51. name: "vowifi IMS registration",
  52. state: vowifi.State{Enabled: true, Phase: vowifi.PhaseIMSReady},
  53. want: true,
  54. },
  55. {
  56. name: "stable vowifi catch-up",
  57. state: vowifi.State{Enabled: true, Phase: vowifi.PhaseSMSReady, SMSReady: true},
  58. },
  59. {
  60. name: "failed vowifi cellular fallback",
  61. state: vowifi.State{Enabled: true, Phase: vowifi.PhaseFailed},
  62. },
  63. {
  64. name: "unknown vowifi state",
  65. state: vowifi.State{Enabled: true, Phase: vowifi.PhaseSIMReady},
  66. err: context.Canceled,
  67. },
  68. }
  69. for _, test := range tests {
  70. t.Run(test.name, func(t *testing.T) {
  71. if got := shouldDeferModemSMSSync(test.state, test.err); got != test.want {
  72. t.Fatalf("shouldDeferModemSMSSync() = %v, want %v", got, test.want)
  73. }
  74. })
  75. }
  76. }
  77. func TestVoWiFiEnableUpdatesPolicyAndQueuesRuntime(t *testing.T) {
  78. database, err := store.Open(context.Background(), ":memory:")
  79. if err != nil {
  80. t.Fatal(err)
  81. }
  82. t.Cleanup(func() { _ = database.Close() })
  83. config := store.Device{ID: "ec20", Name: "EC20"}
  84. if err := database.UpsertDevice(context.Background(), config); err != nil {
  85. t.Fatal(err)
  86. }
  87. controller := &fakeVoWiFiController{
  88. state: vowifi.State{DeviceID: "ec20", Phase: vowifi.PhaseIdle},
  89. }
  90. server := &Server{
  91. store: database,
  92. vowifi: controller,
  93. logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
  94. maxRequestBodyBytes: 4096,
  95. }
  96. request := httptest.NewRequest(
  97. http.MethodPatch,
  98. "/api/devices/ec20/vowifi",
  99. bytes.NewBufferString(`{"enabled":true}`),
  100. )
  101. request.Header.Set("Content-Type", "application/json")
  102. response := httptest.NewRecorder()
  103. server.handleVoWiFiEnabled(response, request, config, true)
  104. if response.Code != http.StatusAccepted {
  105. t.Fatalf("status = %d, body = %s", response.Code, response.Body.String())
  106. }
  107. if len(controller.enabled) != 1 || !controller.enabled[0] {
  108. t.Fatalf("queued enables = %#v", controller.enabled)
  109. }
  110. stored, err := database.Device(context.Background(), "ec20")
  111. if err != nil {
  112. t.Fatal(err)
  113. }
  114. if !stored.VoWiFiEnabled {
  115. t.Fatal("VoWiFi policy was not persisted")
  116. }
  117. }
  118. func TestVoWiFiRepeatedEnableWhileStartingIsAccepted(t *testing.T) {
  119. database, err := store.Open(context.Background(), ":memory:")
  120. if err != nil {
  121. t.Fatal(err)
  122. }
  123. t.Cleanup(func() { _ = database.Close() })
  124. config := store.Device{ID: "ec20", Name: "EC20", VoWiFiEnabled: true}
  125. if err := database.UpsertDevice(context.Background(), config); err != nil {
  126. t.Fatal(err)
  127. }
  128. controller := &fakeVoWiFiController{
  129. // A reconnect briefly enters Stopping/Enabled=false while the persisted
  130. // policy remains enabled. Repeating "enable" is still the same intent.
  131. state: vowifi.State{DeviceID: "ec20", Phase: vowifi.PhaseStopping, Enabled: false},
  132. err: vowifiruntime.ErrOperationInProgress,
  133. }
  134. server := &Server{
  135. store: database,
  136. vowifi: controller,
  137. logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
  138. maxRequestBodyBytes: 4096,
  139. }
  140. request := httptest.NewRequest(
  141. http.MethodPatch,
  142. "/api/devices/ec20/vowifi",
  143. bytes.NewBufferString(`{"enabled":true}`),
  144. )
  145. request.Header.Set("Content-Type", "application/json")
  146. response := httptest.NewRecorder()
  147. server.handleVoWiFiEnabled(response, request, config, true)
  148. if response.Code != http.StatusAccepted {
  149. t.Fatalf("status = %d, body = %s", response.Code, response.Body.String())
  150. }
  151. stored, err := database.Device(context.Background(), "ec20")
  152. if err != nil {
  153. t.Fatal(err)
  154. }
  155. if !stored.VoWiFiEnabled {
  156. t.Fatal("idempotent enable reverted the desired policy")
  157. }
  158. }
  159. func TestVoWiFiReconnectRequiresEnabledPolicy(t *testing.T) {
  160. database, err := store.Open(context.Background(), ":memory:")
  161. if err != nil {
  162. t.Fatal(err)
  163. }
  164. t.Cleanup(func() { _ = database.Close() })
  165. controller := &fakeVoWiFiController{}
  166. server := &Server{
  167. store: database,
  168. vowifi: controller,
  169. logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
  170. }
  171. request := httptest.NewRequest(
  172. http.MethodPost,
  173. "/api/devices/ec20/vowifi/actions/reconnect",
  174. nil,
  175. )
  176. response := httptest.NewRecorder()
  177. server.handleVoWiFiReconnect(
  178. response,
  179. request,
  180. store.Device{ID: "ec20", Name: "EC20"},
  181. true,
  182. )
  183. if response.Code != http.StatusConflict {
  184. t.Fatalf("status = %d, body = %s", response.Code, response.Body.String())
  185. }
  186. if controller.reconnects != 0 {
  187. t.Fatalf("reconnects = %d", controller.reconnects)
  188. }
  189. }