main_test.go 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. package main
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "io"
  7. "log/slog"
  8. "testing"
  9. "time"
  10. "vocat/internal/device"
  11. "vocat/internal/modem"
  12. "vocat/internal/store"
  13. )
  14. // fakeModemClient is a minimal scripted modem.Client for exercising the region
  15. // enforcement orchestration (flight-mode flips) without hardware.
  16. type fakeModemClient struct {
  17. steps []fakeStep
  18. index int
  19. }
  20. type fakeStep struct {
  21. command string
  22. lines []string
  23. }
  24. func (client *fakeModemClient) Execute(_ context.Context, command string) (modem.Response, error) {
  25. if client.index >= len(client.steps) {
  26. return modem.Response{}, fmt.Errorf("unexpected command %q", command)
  27. }
  28. step := client.steps[client.index]
  29. client.index++
  30. if command != step.command {
  31. return modem.Response{}, fmt.Errorf("command %q, want %q", command, step.command)
  32. }
  33. return modem.Response{Command: command, Lines: step.lines, Final: "OK"}, nil
  34. }
  35. func (client *fakeModemClient) WaitURC(context.Context, func(string) bool) (string, error) {
  36. return "", errors.New("no URC scripted")
  37. }
  38. func (client *fakeModemClient) Close() error { return nil }
  39. func (client *fakeModemClient) assertExhausted(t *testing.T) {
  40. t.Helper()
  41. if client.index != len(client.steps) {
  42. t.Fatalf("consumed %d of %d scripted commands", client.index, len(client.steps))
  43. }
  44. }
  45. type fakeDiscoverer struct{ candidates []modem.Candidate }
  46. func (discoverer fakeDiscoverer) Discover(context.Context) ([]modem.Candidate, error) {
  47. return discoverer.candidates, nil
  48. }
  49. type fakeOpener struct{ client modem.Client }
  50. func (opener fakeOpener) Open(context.Context, modem.Port) (modem.Client, error) {
  51. return opener.client, nil
  52. }
  53. const regionTestDeviceID = "quectel-region-test"
  54. func newRegionTestManager(t *testing.T, client modem.Client) *device.Manager {
  55. t.Helper()
  56. manager, err := device.NewManager(device.Options{
  57. Discoverer: fakeDiscoverer{candidates: []modem.Candidate{{
  58. ID: regionTestDeviceID,
  59. Product: "EC20",
  60. ATPort: modem.Port{Path: "/dev/ttyUSB2", Role: modem.PortRoleAT},
  61. }}},
  62. Opener: fakeOpener{client: client},
  63. CommandTimeout: time.Second,
  64. LongTimeout: time.Second,
  65. })
  66. if err != nil {
  67. t.Fatalf("NewManager: %v", err)
  68. }
  69. if err := manager.Start(context.Background()); err != nil {
  70. t.Fatalf("Start: %v", err)
  71. }
  72. t.Cleanup(func() { _ = manager.Stop(context.Background()) })
  73. return manager
  74. }
  75. func newRegionTestStore(t *testing.T) *store.Store {
  76. t.Helper()
  77. database, err := store.Open(context.Background(), ":memory:")
  78. if err != nil {
  79. t.Fatalf("store.Open: %v", err)
  80. }
  81. t.Cleanup(func() { _ = database.Close() })
  82. return database
  83. }
  84. func regionTestLogger() *slog.Logger {
  85. return slog.New(slog.NewTextHandler(io.Discard, nil))
  86. }
  87. func TestEnforceCardRegionForcesAirplaneAndPersistsPolicy(t *testing.T) {
  88. client := &fakeModemClient{steps: []fakeStep{
  89. {command: "AT+CFUN?", lines: []string{"+CFUN: 1"}},
  90. {command: "AT+CFUN=4"},
  91. {command: "AT+CFUN?", lines: []string{"+CFUN: 4"}},
  92. }}
  93. manager := newRegionTestManager(t, client)
  94. database := newRegionTestStore(t)
  95. snapshot := &device.Snapshot{
  96. DeviceID: regionTestDeviceID,
  97. SIMReady: true,
  98. IMSI: "460001234567890",
  99. ICCID: "89860012345678901234",
  100. }
  101. enforceCardRegion(context.Background(), regionTestLogger(), database, manager, regionTestDeviceID, snapshot)
  102. client.assertExhausted(t)
  103. policy, err := database.CardPolicy(context.Background(), snapshot.ICCID)
  104. if err != nil {
  105. t.Fatalf("CardPolicy: %v", err)
  106. }
  107. if policy.Source != cardPolicySourceRegionBlock {
  108. t.Fatalf("policy source = %q, want %q", policy.Source, cardPolicySourceRegionBlock)
  109. }
  110. if policy.NetworkEnabled || policy.VoWiFiEnabled || !policy.AirplaneEnabled {
  111. t.Fatalf("policy switches = %#v, want all service off and airplane on", policy)
  112. }
  113. }
  114. func TestEnforceCardRegionSkipsRadioWhenAlreadyOff(t *testing.T) {
  115. client := &fakeModemClient{}
  116. manager := newRegionTestManager(t, client)
  117. database := newRegionTestStore(t)
  118. snapshot := &device.Snapshot{
  119. DeviceID: regionTestDeviceID,
  120. SIMReady: true,
  121. IMSI: "461001234567890",
  122. ICCID: "89860012345678901234",
  123. FlightMode: true,
  124. }
  125. enforceCardRegion(context.Background(), regionTestLogger(), database, manager, regionTestDeviceID, snapshot)
  126. client.assertExhausted(t)
  127. if _, err := database.CardPolicy(context.Background(), snapshot.ICCID); err != nil {
  128. t.Fatalf("expected a persisted block policy even with the radio already off: %v", err)
  129. }
  130. }
  131. func TestEnforceCardRegionLiftsBlockForAllowedSIM(t *testing.T) {
  132. client := &fakeModemClient{steps: []fakeStep{
  133. {command: "AT+CFUN?", lines: []string{"+CFUN: 4"}},
  134. {command: "AT+CFUN=1"},
  135. {command: "AT+CFUN?", lines: []string{"+CFUN: 1"}},
  136. }}
  137. manager := newRegionTestManager(t, client)
  138. database := newRegionTestStore(t)
  139. if err := database.UpsertCardPolicy(context.Background(), store.CardPolicy{
  140. ICCID: "89860012345678901234",
  141. AirplaneEnabled: true,
  142. IPVersion: "IPV4V6",
  143. Source: cardPolicySourceRegionBlock,
  144. }); err != nil {
  145. t.Fatalf("seed block policy: %v", err)
  146. }
  147. snapshot := &device.Snapshot{
  148. DeviceID: regionTestDeviceID,
  149. SIMReady: true,
  150. IMSI: "310260123456789",
  151. ICCID: "89012601234567890123",
  152. FlightMode: true,
  153. }
  154. enforceCardRegion(context.Background(), regionTestLogger(), database, manager, regionTestDeviceID, snapshot)
  155. client.assertExhausted(t)
  156. if _, err := database.CardPolicy(context.Background(), "89860012345678901234"); !errors.Is(err, store.ErrNotFound) {
  157. t.Fatalf("expected the auto block policy to be cleared, got err=%v", err)
  158. }
  159. }
  160. func TestEnforceCardRegionLeavesAllowedSIMWithoutPriorBlockAlone(t *testing.T) {
  161. client := &fakeModemClient{}
  162. manager := newRegionTestManager(t, client)
  163. database := newRegionTestStore(t)
  164. snapshot := &device.Snapshot{
  165. DeviceID: regionTestDeviceID,
  166. SIMReady: true,
  167. IMSI: "310260123456789",
  168. ICCID: "89012601234567890123",
  169. }
  170. enforceCardRegion(context.Background(), regionTestLogger(), database, manager, regionTestDeviceID, snapshot)
  171. client.assertExhausted(t)
  172. }
  173. func TestEnforceCardRegionIgnoresUnknownOrNotReadySIM(t *testing.T) {
  174. client := &fakeModemClient{}
  175. manager := newRegionTestManager(t, client)
  176. database := newRegionTestStore(t)
  177. // Not ready: no action at all.
  178. notReady := &device.Snapshot{DeviceID: regionTestDeviceID, SIMReady: false, IMSI: "460001234567890"}
  179. enforceCardRegion(context.Background(), regionTestLogger(), database, manager, regionTestDeviceID, notReady)
  180. // Ready but IMSI unknown: hold state, neither block nor lift.
  181. unknown := &device.Snapshot{DeviceID: regionTestDeviceID, SIMReady: true, IMSI: ""}
  182. enforceCardRegion(context.Background(), regionTestLogger(), database, manager, regionTestDeviceID, unknown)
  183. client.assertExhausted(t)
  184. policies, err := database.ListCardPolicies(context.Background())
  185. if err != nil {
  186. t.Fatalf("ListCardPolicies: %v", err)
  187. }
  188. if len(policies) != 0 {
  189. t.Fatalf("expected no card policies, got %d", len(policies))
  190. }
  191. }