sms_test.go 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  1. package device
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "strings"
  7. "testing"
  8. "vocat/internal/modem"
  9. )
  10. func TestManagerSendSMSDirectGSM7ReturnsAcceptanceEvidence(t *testing.T) {
  11. client := &transcriptClient{
  12. steps: []clientStep{
  13. {command: "AT+CMGF=1", response: okResponse()},
  14. {command: `AT+CSCS="GSM"`, response: okResponse()},
  15. {command: "AT+CSMP=49,167,0,0", response: okResponse()},
  16. },
  17. promptSteps: []promptClientStep{{
  18. command: `AT+CMGS="+12345"`,
  19. payload: "HELLO",
  20. response: okResponse("+CMGS: 23"),
  21. }},
  22. }
  23. manager, id := newStartedTestManager(t, client)
  24. result, err := manager.SendSMS(
  25. context.Background(),
  26. id,
  27. "+12 345",
  28. "HELLO",
  29. )
  30. if err != nil {
  31. t.Fatalf("SendSMS: %v", err)
  32. }
  33. if result.To != "+12345" ||
  34. result.Encoding != SMSEncodingGSM7Text ||
  35. !result.AcceptedByModem ||
  36. !result.ReferenceKnown ||
  37. result.MessageReference != 23 ||
  38. result.DeliveryConfirmed ||
  39. result.DeliveryStatus != "unknown" ||
  40. result.SubmissionStatus != "accepted_by_modem" ||
  41. result.PartsTotal != 1 ||
  42. result.PartsAttempted != 1 ||
  43. result.PartsAccepted != 1 ||
  44. !result.AllPartsAccepted ||
  45. len(result.PartResults) != 1 {
  46. t.Fatalf("result = %#v", result)
  47. }
  48. if len(result.ModemEvidence) != 1 || result.ModemEvidence[0] != "+CMGS: 23" {
  49. t.Fatalf("evidence = %#v", result.ModemEvidence)
  50. }
  51. client.assertDone(t)
  52. }
  53. func TestManagerSendMultipartSMSReturnsEveryPartReference(t *testing.T) {
  54. var concatReference = -1
  55. validatePart := func(sequence int, wantText string) func(string) error {
  56. return func(payload string) error {
  57. message, err := decodeSMSPDU(payload)
  58. if err != nil {
  59. return fmt.Errorf("decode part %d: %w", sequence, err)
  60. }
  61. if message.Text != wantText || message.Concat == nil ||
  62. message.Concat.Total != 2 ||
  63. message.Concat.Sequence != sequence {
  64. return fmt.Errorf("part %d decoded as %#v", sequence, message)
  65. }
  66. if concatReference < 0 {
  67. concatReference = message.Concat.Reference
  68. } else if message.Concat.Reference != concatReference {
  69. return fmt.Errorf(
  70. "part %d concat reference %d, want %d",
  71. sequence,
  72. message.Concat.Reference,
  73. concatReference,
  74. )
  75. }
  76. return nil
  77. }
  78. }
  79. client := &transcriptClient{
  80. steps: []clientStep{{command: "AT+CMGF=0", response: okResponse()}},
  81. promptSteps: []promptClientStep{
  82. {
  83. command: "AT+CMGS=150",
  84. validateBody: validatePart(1, strings.Repeat("A", 153)),
  85. response: okResponse("+CMGS: 31"),
  86. },
  87. {
  88. command: "AT+CMGS=24",
  89. validateBody: validatePart(2, strings.Repeat("A", 8)),
  90. response: okResponse("+CMGS: 32"),
  91. },
  92. },
  93. }
  94. manager, id := newStartedTestManager(t, client)
  95. result, err := manager.SendSMS(
  96. context.Background(),
  97. id,
  98. "+12345",
  99. strings.Repeat("A", 161),
  100. )
  101. if err != nil {
  102. t.Fatalf("SendSMS: %v", err)
  103. }
  104. if result.PartsTotal != 2 ||
  105. result.PartsAttempted != 2 ||
  106. result.PartsAccepted != 2 ||
  107. !result.AcceptedByModem ||
  108. !result.AllPartsAccepted ||
  109. result.ReferenceKnown ||
  110. result.ConcatReference == nil ||
  111. *result.ConcatReference != concatReference ||
  112. len(result.PartResults) != 2 ||
  113. result.PartResults[0].MessageReference != 31 ||
  114. result.PartResults[1].MessageReference != 32 ||
  115. !result.PartResults[0].AcceptedByModem ||
  116. !result.PartResults[1].AcceptedByModem ||
  117. result.DeliveryConfirmed {
  118. t.Fatalf("result = %#v", result)
  119. }
  120. client.assertDone(t)
  121. }
  122. func TestManagerSendMultipartSMSStopsAndPreservesPartialEvidence(t *testing.T) {
  123. validatePart := func(sequence int, wantText string) func(string) error {
  124. return func(payload string) error {
  125. message, err := decodeSMSPDU(payload)
  126. if err != nil {
  127. return err
  128. }
  129. if message.Concat == nil ||
  130. message.Concat.Sequence != sequence ||
  131. message.Text != wantText {
  132. return fmt.Errorf("part %d decoded as %#v", sequence, message)
  133. }
  134. return nil
  135. }
  136. }
  137. secondError := &modem.CommandError{
  138. Command: "AT+CMGS=24",
  139. Final: "+CMS ERROR: 500",
  140. }
  141. client := &transcriptClient{
  142. steps: []clientStep{{command: "AT+CMGF=0", response: okResponse()}},
  143. promptSteps: []promptClientStep{
  144. {
  145. command: "AT+CMGS=150",
  146. validateBody: validatePart(1, strings.Repeat("A", 153)),
  147. response: okResponse("+CMGS: 41"),
  148. },
  149. {
  150. command: "AT+CMGS=24",
  151. validateBody: validatePart(2, strings.Repeat("A", 8)),
  152. response: modem.Response{
  153. Final: "+CMS ERROR: 500",
  154. },
  155. err: secondError,
  156. },
  157. },
  158. }
  159. manager, id := newStartedTestManager(t, client)
  160. result, err := manager.SendSMS(
  161. context.Background(),
  162. id,
  163. "+12345",
  164. strings.Repeat("A", 161),
  165. )
  166. var commandErr *modem.CommandError
  167. if !errors.As(err, &commandErr) {
  168. t.Fatalf("error = %v", err)
  169. }
  170. if result.PartsTotal != 2 ||
  171. result.PartsAttempted != 2 ||
  172. result.PartsAccepted != 1 ||
  173. result.AcceptedByModem ||
  174. result.AllPartsAccepted ||
  175. result.SubmissionStatus != "partially_accepted_by_modem" ||
  176. len(result.PartResults) != 2 ||
  177. !result.PartResults[0].AcceptedByModem ||
  178. result.PartResults[0].MessageReference != 41 ||
  179. result.PartResults[1].AcceptedByModem ||
  180. result.PartResults[1].SubmissionStatus != "rejected_by_modem" ||
  181. result.DeliveryConfirmed {
  182. t.Fatalf("result = %#v", result)
  183. }
  184. client.assertDone(t)
  185. }
  186. func TestManagerSendSMSUsesUCS2PDUForChinese(t *testing.T) {
  187. client := &transcriptClient{
  188. steps: []clientStep{
  189. {command: "AT+CMGF=0", response: okResponse()},
  190. },
  191. promptSteps: []promptClientStep{{
  192. command: "AT+CMGS=14",
  193. payload: "00210005912143F50008044F60597D",
  194. response: okResponse("+CMGS: 0"),
  195. }},
  196. }
  197. manager, id := newStartedTestManager(t, client)
  198. result, err := manager.SendSMS(
  199. context.Background(),
  200. id,
  201. "+12345",
  202. "你好",
  203. )
  204. if err != nil {
  205. t.Fatalf("SendSMS: %v", err)
  206. }
  207. if result.Encoding != SMSEncodingUCS2PDU ||
  208. !result.ReferenceKnown ||
  209. result.MessageReference != 0 ||
  210. !result.AllPartsAccepted ||
  211. result.DeliveryConfirmed {
  212. t.Fatalf("result = %#v", result)
  213. }
  214. client.assertDone(t)
  215. }
  216. func TestManagerSendSMSTimeoutNeverClaimsAcceptanceOrDelivery(t *testing.T) {
  217. client := &transcriptClient{
  218. steps: []clientStep{
  219. {command: "AT+CMGF=1", response: okResponse()},
  220. {command: `AT+CSCS="GSM"`, response: okResponse()},
  221. {command: "AT+CSMP=49,167,0,0", response: okResponse()},
  222. },
  223. promptSteps: []promptClientStep{{
  224. command: `AT+CMGS="12345"`,
  225. payload: "HELLO",
  226. response: modem.Response{
  227. Lines: []string{"+CMGS: 77"},
  228. },
  229. err: modem.ErrCommandTimeout,
  230. }},
  231. }
  232. manager, id := newStartedTestManager(t, client)
  233. result, err := manager.SendSMS(
  234. context.Background(),
  235. id,
  236. "12345",
  237. "HELLO",
  238. )
  239. if !errors.Is(err, modem.ErrCommandTimeout) {
  240. t.Fatalf("error = %v", err)
  241. }
  242. if result.AcceptedByModem || result.DeliveryConfirmed ||
  243. !result.ReferenceKnown || result.MessageReference != 77 ||
  244. result.SubmissionStatus != "reference_returned_without_final" ||
  245. result.DeliveryStatus != "unknown" {
  246. t.Fatalf("result = %#v", result)
  247. }
  248. client.mu.Lock()
  249. closeCount := client.closeCount
  250. client.mu.Unlock()
  251. if closeCount != 1 {
  252. t.Fatalf("close count = %d, want 1 after uncertain timeout", closeCount)
  253. }
  254. client.assertDone(t)
  255. }
  256. func TestManagerListReadAndDeleteSMS(t *testing.T) {
  257. const gsmPDU = "000405912143F500004210203040500005C82293F904"
  258. const ucs2PDU = "000405912143F5000842102030405000044F60597D"
  259. client := &transcriptClient{steps: []clientStep{
  260. {command: "AT+CMGF=0", response: okResponse()},
  261. {command: `AT+CPMS="SM"`, response: okResponse()},
  262. {
  263. command: "AT+CMGL=4",
  264. response: okResponse(
  265. "+CMGL: 7,0,,23",
  266. gsmPDU,
  267. "+CMGL: 8,1,,22",
  268. ucs2PDU,
  269. ),
  270. },
  271. {command: `AT+CPMS="ME"`, response: okResponse()},
  272. {command: "AT+CMGL=4", response: okResponse()},
  273. {command: "AT+CMGF=0", response: okResponse()},
  274. {
  275. command: "AT+CMGR=7",
  276. response: okResponse(
  277. "+CMGR: 0,,23",
  278. gsmPDU,
  279. ),
  280. },
  281. {command: "AT+CMGD=7", response: okResponse()},
  282. }}
  283. manager, id := newStartedTestManager(t, client)
  284. messages, err := manager.ListSMS(context.Background(), id)
  285. if err != nil {
  286. t.Fatalf("ListSMS: %v", err)
  287. }
  288. if len(messages) != 2 ||
  289. messages[0].Index != 7 ||
  290. messages[0].Storage != "SM" ||
  291. messages[0].StorageStatus != SMSStatusReceivedUnread ||
  292. messages[0].Text != "HELLO" ||
  293. messages[1].Index != 8 ||
  294. messages[1].Storage != "SM" ||
  295. messages[1].StorageStatus != SMSStatusReceivedRead ||
  296. messages[1].Text != "你好" {
  297. t.Fatalf("messages = %#v", messages)
  298. }
  299. message, err := manager.ReadSMS(context.Background(), id, 7)
  300. if err != nil {
  301. t.Fatalf("ReadSMS: %v", err)
  302. }
  303. if message.Index != 7 ||
  304. message.StorageStatus != SMSStatusReceivedUnread ||
  305. message.Text != "HELLO" {
  306. t.Fatalf("message = %#v", message)
  307. }
  308. if err := manager.DeleteSMS(context.Background(), id, 7); err != nil {
  309. t.Fatalf("DeleteSMS: %v", err)
  310. }
  311. client.assertDone(t)
  312. }
  313. func TestManagerSendSMSRequiresMessageReference(t *testing.T) {
  314. client := &transcriptClient{
  315. steps: []clientStep{
  316. {command: "AT+CMGF=1", response: okResponse()},
  317. {command: `AT+CSCS="GSM"`, response: okResponse()},
  318. {command: "AT+CSMP=49,167,0,0", response: okResponse()},
  319. },
  320. promptSteps: []promptClientStep{{
  321. command: `AT+CMGS="12345"`,
  322. payload: "HELLO",
  323. response: okResponse(),
  324. }},
  325. }
  326. manager, id := newStartedTestManager(t, client)
  327. result, err := manager.SendSMS(
  328. context.Background(),
  329. id,
  330. "12345",
  331. "HELLO",
  332. )
  333. if !errors.Is(err, ErrSMSReferenceMissing) {
  334. t.Fatalf("error = %v", err)
  335. }
  336. if result.AcceptedByModem ||
  337. result.ReferenceKnown ||
  338. result.DeliveryConfirmed ||
  339. result.SubmissionStatus != "unconfirmed_without_reference" {
  340. t.Fatalf("result = %#v", result)
  341. }
  342. client.assertDone(t)
  343. }