session_test.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  1. package modem
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "io"
  7. "sync"
  8. "testing"
  9. "time"
  10. )
  11. type transportStep struct {
  12. write string
  13. chunks []string
  14. }
  15. type transcriptTransport struct {
  16. mu sync.Mutex
  17. steps []transportStep
  18. chunks [][]byte
  19. pendingWrite string
  20. pendingChunks []string
  21. readTimeout time.Duration
  22. resetCount int
  23. closed bool
  24. unexpected error
  25. writePartial bool
  26. writeEvents chan string
  27. }
  28. func (transport *transcriptTransport) Write(payload []byte) (int, error) {
  29. transport.mu.Lock()
  30. defer transport.mu.Unlock()
  31. if transport.closed {
  32. return 0, io.ErrClosedPipe
  33. }
  34. if transport.pendingWrite != "" {
  35. if string(payload) != transport.pendingWrite {
  36. transport.unexpected = fmt.Errorf(
  37. "partial write %q, want %q",
  38. payload,
  39. transport.pendingWrite,
  40. )
  41. return 0, transport.unexpected
  42. }
  43. for _, chunk := range transport.pendingChunks {
  44. transport.chunks = append(transport.chunks, []byte(chunk))
  45. }
  46. transport.pendingWrite = ""
  47. transport.pendingChunks = nil
  48. return len(payload), nil
  49. }
  50. if len(transport.steps) == 0 {
  51. transport.unexpected = fmt.Errorf("unexpected write %q", payload)
  52. return 0, transport.unexpected
  53. }
  54. step := transport.steps[0]
  55. transport.steps = transport.steps[1:]
  56. if string(payload) != step.write {
  57. transport.unexpected = fmt.Errorf("write %q, want %q", payload, step.write)
  58. return 0, transport.unexpected
  59. }
  60. if transport.writeEvents != nil {
  61. select {
  62. case transport.writeEvents <- string(payload):
  63. default:
  64. }
  65. }
  66. if transport.writePartial && len(payload) > 1 {
  67. transport.writePartial = false
  68. count := len(payload) / 2
  69. transport.pendingWrite = step.write[count:]
  70. transport.pendingChunks = append([]string(nil), step.chunks...)
  71. return count, nil
  72. }
  73. for _, chunk := range step.chunks {
  74. transport.chunks = append(transport.chunks, []byte(chunk))
  75. }
  76. return len(payload), nil
  77. }
  78. func (transport *transcriptTransport) enqueue(chunks ...string) {
  79. transport.mu.Lock()
  80. for _, chunk := range chunks {
  81. transport.chunks = append(transport.chunks, []byte(chunk))
  82. }
  83. transport.mu.Unlock()
  84. }
  85. func (transport *transcriptTransport) Read(buffer []byte) (int, error) {
  86. transport.mu.Lock()
  87. if transport.closed {
  88. transport.mu.Unlock()
  89. return 0, io.EOF
  90. }
  91. if len(transport.chunks) > 0 {
  92. chunk := transport.chunks[0]
  93. count := copy(buffer, chunk)
  94. if count == len(chunk) {
  95. transport.chunks = transport.chunks[1:]
  96. } else {
  97. transport.chunks[0] = chunk[count:]
  98. }
  99. transport.mu.Unlock()
  100. return count, nil
  101. }
  102. timeout := transport.readTimeout
  103. transport.mu.Unlock()
  104. if timeout <= 0 || timeout > 2*time.Millisecond {
  105. timeout = time.Millisecond
  106. }
  107. time.Sleep(timeout)
  108. return 0, nil
  109. }
  110. func (transport *transcriptTransport) Drain() error { return nil }
  111. func (transport *transcriptTransport) ResetInputBuffer() error {
  112. transport.mu.Lock()
  113. transport.chunks = nil
  114. transport.resetCount++
  115. transport.mu.Unlock()
  116. return nil
  117. }
  118. func (transport *transcriptTransport) SetReadTimeout(timeout time.Duration) error {
  119. transport.mu.Lock()
  120. transport.readTimeout = timeout
  121. transport.mu.Unlock()
  122. return nil
  123. }
  124. func (transport *transcriptTransport) Close() error {
  125. transport.mu.Lock()
  126. transport.closed = true
  127. transport.mu.Unlock()
  128. return nil
  129. }
  130. func TestSessionSeparatesInterleavedURCs(t *testing.T) {
  131. transport := &transcriptTransport{steps: []transportStep{{
  132. write: "AT+CSQ\r",
  133. chunks: []string{
  134. "\r\nAT+CSQ\r\n+CMTI: \"SM\",7\r\n",
  135. "+CSQ: 24,99\r\nOK\r\n",
  136. },
  137. }}}
  138. session := newTestSession(t, transport)
  139. response, err := session.Execute(context.Background(), "AT+CSQ")
  140. if err != nil {
  141. t.Fatalf("Execute: %v", err)
  142. }
  143. if got := response.Text(); got != "+CSQ: 24,99" {
  144. t.Fatalf("response = %q", got)
  145. }
  146. if len(response.URCs) != 1 || response.URCs[0] != `+CMTI: "SM",7` {
  147. t.Fatalf("URCs = %#v", response.URCs)
  148. }
  149. urc, err := session.WaitURC(context.Background(), func(line string) bool {
  150. return line == `+CMTI: "SM",7`
  151. })
  152. if err != nil || urc == "" {
  153. t.Fatalf("WaitURC = %q, %v", urc, err)
  154. }
  155. }
  156. func TestSessionKeepsExpectedRegistrationLineInResponse(t *testing.T) {
  157. transport := &transcriptTransport{steps: []transportStep{{
  158. write: "AT+CEREG?\r",
  159. chunks: []string{"\r\n+CEREG: 0,5\r\nOK\r\n"},
  160. }}}
  161. session := newTestSession(t, transport)
  162. response, err := session.Execute(context.Background(), "AT+CEREG?")
  163. if err != nil {
  164. t.Fatalf("Execute: %v", err)
  165. }
  166. if response.Text() != "+CEREG: 0,5" || len(response.URCs) != 0 {
  167. t.Fatalf("response = %#v", response)
  168. }
  169. }
  170. func TestSessionQueuesCUSDThatArrivesBeforeOK(t *testing.T) {
  171. transport := &transcriptTransport{steps: []transportStep{{
  172. write: "AT+CUSD=1,\"*100#\",15\r",
  173. chunks: []string{"\r\n+CUSD: 0,\"004F004B\",72\r\nOK\r\n"},
  174. }}}
  175. session := newTestSession(t, transport)
  176. response, err := session.Execute(context.Background(), `AT+CUSD=1,"*100#",15`)
  177. if err != nil {
  178. t.Fatalf("Execute: %v", err)
  179. }
  180. if len(response.URCs) != 1 {
  181. t.Fatalf("URCs = %#v", response.URCs)
  182. }
  183. urc, err := session.WaitURC(context.Background(), func(line string) bool {
  184. return len(line) >= 6 && line[:6] == "+CUSD:"
  185. })
  186. if err != nil || urc != `+CUSD: 0,"004F004B",72` {
  187. t.Fatalf("WaitURC = %q, %v", urc, err)
  188. }
  189. }
  190. func TestSessionReturnsTypedCommandError(t *testing.T) {
  191. transport := &transcriptTransport{steps: []transportStep{{
  192. write: "AT+CPIN?\r",
  193. chunks: []string{"\r\n+CME ERROR: 10\r\n"},
  194. }}}
  195. session := newTestSession(t, transport)
  196. _, err := session.Execute(context.Background(), "AT+CPIN?")
  197. var commandErr *CommandError
  198. if !errors.As(err, &commandErr) || commandErr.Final != "+CME ERROR: 10" {
  199. t.Fatalf("error = %#v", err)
  200. }
  201. }
  202. func TestSessionTimeoutResetsInputAndRejectsCommandInjection(t *testing.T) {
  203. transport := &transcriptTransport{steps: []transportStep{{write: "AT\r"}}}
  204. session, err := NewSession(transport, SessionOptions{
  205. ReadTimeout: time.Millisecond,
  206. CommandTimeout: 15 * time.Millisecond,
  207. })
  208. if err != nil {
  209. t.Fatal(err)
  210. }
  211. _, err = session.Execute(context.Background(), "AT")
  212. if !errors.Is(err, ErrCommandTimeout) {
  213. t.Fatalf("error = %v", err)
  214. }
  215. if transport.resetCount != 1 {
  216. t.Fatalf("reset count = %d", transport.resetCount)
  217. }
  218. if _, err := session.Execute(context.Background(), "AT\rAT+CFUN=0"); err == nil {
  219. t.Fatal("expected command delimiter rejection")
  220. }
  221. }
  222. func TestSessionHandlesPartialWrites(t *testing.T) {
  223. transport := &transcriptTransport{
  224. writePartial: true,
  225. steps: []transportStep{{
  226. write: "AT+CSQ\r",
  227. chunks: []string{"\r\n+CSQ: 1,99\r\nOK\r\n"},
  228. }},
  229. }
  230. session := newTestSession(t, transport)
  231. response, err := session.Execute(context.Background(), "AT+CSQ")
  232. if err != nil {
  233. t.Fatalf("Execute: %v", err)
  234. }
  235. if response.Text() != "+CSQ: 1,99" {
  236. t.Fatalf("response = %#v", response)
  237. }
  238. transport.mu.Lock()
  239. defer transport.mu.Unlock()
  240. if transport.pendingWrite != "" || len(transport.steps) != 0 ||
  241. transport.unexpected != nil {
  242. t.Fatalf(
  243. "unfinished transcript: pending=%q steps=%d err=%v",
  244. transport.pendingWrite,
  245. len(transport.steps),
  246. transport.unexpected,
  247. )
  248. }
  249. }
  250. func TestSessionExecutePromptQueuesURCsAndReturnsCMGS(t *testing.T) {
  251. const pdu = "00010005912143F50008044F60597D"
  252. transport := &transcriptTransport{steps: []transportStep{
  253. {
  254. write: "AT+CMGS=14\r",
  255. chunks: []string{
  256. "\r\nAT+CMGS=14\r\n+CMTI: \"SM\",7\r\n> ",
  257. },
  258. },
  259. {write: pdu},
  260. {
  261. write: string([]byte{0x1a}),
  262. chunks: []string{
  263. "\r\n" + pdu + "\r\n+CMGS: 42\r\n",
  264. "+CMTI: \"SM\",8\r\nOK\r\n",
  265. },
  266. },
  267. }}
  268. session := newTestSession(t, transport)
  269. response, err := session.ExecutePrompt(
  270. context.Background(),
  271. "AT+CMGS=14",
  272. []byte(pdu),
  273. )
  274. if err != nil {
  275. t.Fatalf("ExecutePrompt: %v", err)
  276. }
  277. if response.Text() != "+CMGS: 42" || !response.OK() {
  278. t.Fatalf("response = %#v", response)
  279. }
  280. if len(response.URCs) != 2 {
  281. t.Fatalf("URCs = %#v", response.URCs)
  282. }
  283. for _, wanted := range []string{`+CMTI: "SM",7`, `+CMTI: "SM",8`} {
  284. line, waitErr := session.WaitURC(
  285. context.Background(),
  286. func(line string) bool { return line == wanted },
  287. )
  288. if waitErr != nil || line != wanted {
  289. t.Fatalf("WaitURC(%q) = %q, %v", wanted, line, waitErr)
  290. }
  291. }
  292. }
  293. func TestSessionExecutePromptTimeoutDoesNotWritePayload(t *testing.T) {
  294. transport := &transcriptTransport{
  295. steps: []transportStep{{write: "AT+CMGS=5\r"}},
  296. }
  297. session, err := NewSession(transport, SessionOptions{
  298. ReadTimeout: time.Millisecond,
  299. CommandTimeout: 15 * time.Millisecond,
  300. })
  301. if err != nil {
  302. t.Fatal(err)
  303. }
  304. _, err = session.ExecutePrompt(
  305. context.Background(),
  306. "AT+CMGS=5",
  307. []byte("001122"),
  308. )
  309. if !errors.Is(err, ErrCommandTimeout) {
  310. t.Fatalf("error = %v", err)
  311. }
  312. transport.mu.Lock()
  313. defer transport.mu.Unlock()
  314. if transport.resetCount != 1 || len(transport.steps) != 0 ||
  315. transport.unexpected != nil {
  316. t.Fatalf(
  317. "transport = reset %d, steps %d, error %v",
  318. transport.resetCount,
  319. len(transport.steps),
  320. transport.unexpected,
  321. )
  322. }
  323. }
  324. func TestSessionExecutePromptSerializesConcurrentCommand(t *testing.T) {
  325. events := make(chan string, 4)
  326. transport := &transcriptTransport{
  327. writeEvents: events,
  328. steps: []transportStep{
  329. {write: "AT+CMGS=\"12345\"\r"},
  330. {write: "HELLO"},
  331. {
  332. write: string([]byte{0x1a}),
  333. chunks: []string{"\r\n+CMGS: 9\r\nOK\r\n"},
  334. },
  335. {
  336. write: "AT+CSQ\r",
  337. chunks: []string{"\r\n+CSQ: 20,99\r\nOK\r\n"},
  338. },
  339. },
  340. }
  341. session := newTestSession(t, transport)
  342. promptResult := make(chan error, 1)
  343. go func() {
  344. _, err := session.ExecutePrompt(
  345. context.Background(),
  346. `AT+CMGS="12345"`,
  347. []byte("HELLO"),
  348. )
  349. promptResult <- err
  350. }()
  351. if first := <-events; first != "AT+CMGS=\"12345\"\r" {
  352. t.Fatalf("first write = %q", first)
  353. }
  354. normalStarted := make(chan struct{})
  355. normalResult := make(chan error, 1)
  356. go func() {
  357. close(normalStarted)
  358. _, err := session.Execute(context.Background(), "AT+CSQ")
  359. normalResult <- err
  360. }()
  361. <-normalStarted
  362. transport.enqueue("\r\n> ")
  363. if err := <-promptResult; err != nil {
  364. t.Fatalf("ExecutePrompt: %v", err)
  365. }
  366. if err := <-normalResult; err != nil {
  367. t.Fatalf("concurrent Execute: %v", err)
  368. }
  369. writes := []string{<-events, <-events, <-events}
  370. want := []string{"HELLO", string([]byte{0x1a}), "AT+CSQ\r"}
  371. for index := range want {
  372. if writes[index] != want[index] {
  373. t.Fatalf("write[%d] = %q, want %q", index, writes[index], want[index])
  374. }
  375. }
  376. }
  377. func TestSessionExecutePromptRejectsUnsafeInput(t *testing.T) {
  378. transport := &transcriptTransport{}
  379. session := newTestSession(t, transport)
  380. if _, err := session.ExecutePrompt(
  381. context.Background(),
  382. "AT+CSQ",
  383. []byte("payload"),
  384. ); err == nil {
  385. t.Fatal("expected non-CMGS prompt command rejection")
  386. }
  387. if _, err := session.ExecutePrompt(
  388. context.Background(),
  389. "AT+CMGS=1",
  390. []byte{'A', 0x1a},
  391. ); err == nil {
  392. t.Fatal("expected Ctrl-Z payload rejection")
  393. }
  394. }
  395. func newTestSession(t *testing.T, transport Transport) *Session {
  396. t.Helper()
  397. session, err := NewSession(transport, SessionOptions{
  398. ReadTimeout: time.Millisecond,
  399. CommandTimeout: time.Second,
  400. })
  401. if err != nil {
  402. t.Fatal(err)
  403. }
  404. return session
  405. }