| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428 |
- package modem
- import (
- "context"
- "errors"
- "fmt"
- "io"
- "sync"
- "testing"
- "time"
- )
- type transportStep struct {
- write string
- chunks []string
- }
- type transcriptTransport struct {
- mu sync.Mutex
- steps []transportStep
- chunks [][]byte
- pendingWrite string
- pendingChunks []string
- readTimeout time.Duration
- resetCount int
- closed bool
- unexpected error
- writePartial bool
- writeEvents chan string
- }
- func (transport *transcriptTransport) Write(payload []byte) (int, error) {
- transport.mu.Lock()
- defer transport.mu.Unlock()
- if transport.closed {
- return 0, io.ErrClosedPipe
- }
- if transport.pendingWrite != "" {
- if string(payload) != transport.pendingWrite {
- transport.unexpected = fmt.Errorf(
- "partial write %q, want %q",
- payload,
- transport.pendingWrite,
- )
- return 0, transport.unexpected
- }
- for _, chunk := range transport.pendingChunks {
- transport.chunks = append(transport.chunks, []byte(chunk))
- }
- transport.pendingWrite = ""
- transport.pendingChunks = nil
- return len(payload), nil
- }
- if len(transport.steps) == 0 {
- transport.unexpected = fmt.Errorf("unexpected write %q", payload)
- return 0, transport.unexpected
- }
- step := transport.steps[0]
- transport.steps = transport.steps[1:]
- if string(payload) != step.write {
- transport.unexpected = fmt.Errorf("write %q, want %q", payload, step.write)
- return 0, transport.unexpected
- }
- if transport.writeEvents != nil {
- select {
- case transport.writeEvents <- string(payload):
- default:
- }
- }
- if transport.writePartial && len(payload) > 1 {
- transport.writePartial = false
- count := len(payload) / 2
- transport.pendingWrite = step.write[count:]
- transport.pendingChunks = append([]string(nil), step.chunks...)
- return count, nil
- }
- for _, chunk := range step.chunks {
- transport.chunks = append(transport.chunks, []byte(chunk))
- }
- return len(payload), nil
- }
- func (transport *transcriptTransport) enqueue(chunks ...string) {
- transport.mu.Lock()
- for _, chunk := range chunks {
- transport.chunks = append(transport.chunks, []byte(chunk))
- }
- transport.mu.Unlock()
- }
- func (transport *transcriptTransport) Read(buffer []byte) (int, error) {
- transport.mu.Lock()
- if transport.closed {
- transport.mu.Unlock()
- return 0, io.EOF
- }
- if len(transport.chunks) > 0 {
- chunk := transport.chunks[0]
- count := copy(buffer, chunk)
- if count == len(chunk) {
- transport.chunks = transport.chunks[1:]
- } else {
- transport.chunks[0] = chunk[count:]
- }
- transport.mu.Unlock()
- return count, nil
- }
- timeout := transport.readTimeout
- transport.mu.Unlock()
- if timeout <= 0 || timeout > 2*time.Millisecond {
- timeout = time.Millisecond
- }
- time.Sleep(timeout)
- return 0, nil
- }
- func (transport *transcriptTransport) Drain() error { return nil }
- func (transport *transcriptTransport) ResetInputBuffer() error {
- transport.mu.Lock()
- transport.chunks = nil
- transport.resetCount++
- transport.mu.Unlock()
- return nil
- }
- func (transport *transcriptTransport) SetReadTimeout(timeout time.Duration) error {
- transport.mu.Lock()
- transport.readTimeout = timeout
- transport.mu.Unlock()
- return nil
- }
- func (transport *transcriptTransport) Close() error {
- transport.mu.Lock()
- transport.closed = true
- transport.mu.Unlock()
- return nil
- }
- func TestSessionSeparatesInterleavedURCs(t *testing.T) {
- transport := &transcriptTransport{steps: []transportStep{{
- write: "AT+CSQ\r",
- chunks: []string{
- "\r\nAT+CSQ\r\n+CMTI: \"SM\",7\r\n",
- "+CSQ: 24,99\r\nOK\r\n",
- },
- }}}
- session := newTestSession(t, transport)
- response, err := session.Execute(context.Background(), "AT+CSQ")
- if err != nil {
- t.Fatalf("Execute: %v", err)
- }
- if got := response.Text(); got != "+CSQ: 24,99" {
- t.Fatalf("response = %q", got)
- }
- if len(response.URCs) != 1 || response.URCs[0] != `+CMTI: "SM",7` {
- t.Fatalf("URCs = %#v", response.URCs)
- }
- urc, err := session.WaitURC(context.Background(), func(line string) bool {
- return line == `+CMTI: "SM",7`
- })
- if err != nil || urc == "" {
- t.Fatalf("WaitURC = %q, %v", urc, err)
- }
- }
- func TestSessionKeepsExpectedRegistrationLineInResponse(t *testing.T) {
- transport := &transcriptTransport{steps: []transportStep{{
- write: "AT+CEREG?\r",
- chunks: []string{"\r\n+CEREG: 0,5\r\nOK\r\n"},
- }}}
- session := newTestSession(t, transport)
- response, err := session.Execute(context.Background(), "AT+CEREG?")
- if err != nil {
- t.Fatalf("Execute: %v", err)
- }
- if response.Text() != "+CEREG: 0,5" || len(response.URCs) != 0 {
- t.Fatalf("response = %#v", response)
- }
- }
- func TestSessionQueuesCUSDThatArrivesBeforeOK(t *testing.T) {
- transport := &transcriptTransport{steps: []transportStep{{
- write: "AT+CUSD=1,\"*100#\",15\r",
- chunks: []string{"\r\n+CUSD: 0,\"004F004B\",72\r\nOK\r\n"},
- }}}
- session := newTestSession(t, transport)
- response, err := session.Execute(context.Background(), `AT+CUSD=1,"*100#",15`)
- if err != nil {
- t.Fatalf("Execute: %v", err)
- }
- if len(response.URCs) != 1 {
- t.Fatalf("URCs = %#v", response.URCs)
- }
- urc, err := session.WaitURC(context.Background(), func(line string) bool {
- return len(line) >= 6 && line[:6] == "+CUSD:"
- })
- if err != nil || urc != `+CUSD: 0,"004F004B",72` {
- t.Fatalf("WaitURC = %q, %v", urc, err)
- }
- }
- func TestSessionReturnsTypedCommandError(t *testing.T) {
- transport := &transcriptTransport{steps: []transportStep{{
- write: "AT+CPIN?\r",
- chunks: []string{"\r\n+CME ERROR: 10\r\n"},
- }}}
- session := newTestSession(t, transport)
- _, err := session.Execute(context.Background(), "AT+CPIN?")
- var commandErr *CommandError
- if !errors.As(err, &commandErr) || commandErr.Final != "+CME ERROR: 10" {
- t.Fatalf("error = %#v", err)
- }
- }
- func TestSessionTimeoutResetsInputAndRejectsCommandInjection(t *testing.T) {
- transport := &transcriptTransport{steps: []transportStep{{write: "AT\r"}}}
- session, err := NewSession(transport, SessionOptions{
- ReadTimeout: time.Millisecond,
- CommandTimeout: 15 * time.Millisecond,
- })
- if err != nil {
- t.Fatal(err)
- }
- _, err = session.Execute(context.Background(), "AT")
- if !errors.Is(err, ErrCommandTimeout) {
- t.Fatalf("error = %v", err)
- }
- if transport.resetCount != 1 {
- t.Fatalf("reset count = %d", transport.resetCount)
- }
- if _, err := session.Execute(context.Background(), "AT\rAT+CFUN=0"); err == nil {
- t.Fatal("expected command delimiter rejection")
- }
- }
- func TestSessionHandlesPartialWrites(t *testing.T) {
- transport := &transcriptTransport{
- writePartial: true,
- steps: []transportStep{{
- write: "AT+CSQ\r",
- chunks: []string{"\r\n+CSQ: 1,99\r\nOK\r\n"},
- }},
- }
- session := newTestSession(t, transport)
- response, err := session.Execute(context.Background(), "AT+CSQ")
- if err != nil {
- t.Fatalf("Execute: %v", err)
- }
- if response.Text() != "+CSQ: 1,99" {
- t.Fatalf("response = %#v", response)
- }
- transport.mu.Lock()
- defer transport.mu.Unlock()
- if transport.pendingWrite != "" || len(transport.steps) != 0 ||
- transport.unexpected != nil {
- t.Fatalf(
- "unfinished transcript: pending=%q steps=%d err=%v",
- transport.pendingWrite,
- len(transport.steps),
- transport.unexpected,
- )
- }
- }
- func TestSessionExecutePromptQueuesURCsAndReturnsCMGS(t *testing.T) {
- const pdu = "00010005912143F50008044F60597D"
- transport := &transcriptTransport{steps: []transportStep{
- {
- write: "AT+CMGS=14\r",
- chunks: []string{
- "\r\nAT+CMGS=14\r\n+CMTI: \"SM\",7\r\n> ",
- },
- },
- {write: pdu},
- {
- write: string([]byte{0x1a}),
- chunks: []string{
- "\r\n" + pdu + "\r\n+CMGS: 42\r\n",
- "+CMTI: \"SM\",8\r\nOK\r\n",
- },
- },
- }}
- session := newTestSession(t, transport)
- response, err := session.ExecutePrompt(
- context.Background(),
- "AT+CMGS=14",
- []byte(pdu),
- )
- if err != nil {
- t.Fatalf("ExecutePrompt: %v", err)
- }
- if response.Text() != "+CMGS: 42" || !response.OK() {
- t.Fatalf("response = %#v", response)
- }
- if len(response.URCs) != 2 {
- t.Fatalf("URCs = %#v", response.URCs)
- }
- for _, wanted := range []string{`+CMTI: "SM",7`, `+CMTI: "SM",8`} {
- line, waitErr := session.WaitURC(
- context.Background(),
- func(line string) bool { return line == wanted },
- )
- if waitErr != nil || line != wanted {
- t.Fatalf("WaitURC(%q) = %q, %v", wanted, line, waitErr)
- }
- }
- }
- func TestSessionExecutePromptTimeoutDoesNotWritePayload(t *testing.T) {
- transport := &transcriptTransport{
- steps: []transportStep{{write: "AT+CMGS=5\r"}},
- }
- session, err := NewSession(transport, SessionOptions{
- ReadTimeout: time.Millisecond,
- CommandTimeout: 15 * time.Millisecond,
- })
- if err != nil {
- t.Fatal(err)
- }
- _, err = session.ExecutePrompt(
- context.Background(),
- "AT+CMGS=5",
- []byte("001122"),
- )
- if !errors.Is(err, ErrCommandTimeout) {
- t.Fatalf("error = %v", err)
- }
- transport.mu.Lock()
- defer transport.mu.Unlock()
- if transport.resetCount != 1 || len(transport.steps) != 0 ||
- transport.unexpected != nil {
- t.Fatalf(
- "transport = reset %d, steps %d, error %v",
- transport.resetCount,
- len(transport.steps),
- transport.unexpected,
- )
- }
- }
- func TestSessionExecutePromptSerializesConcurrentCommand(t *testing.T) {
- events := make(chan string, 4)
- transport := &transcriptTransport{
- writeEvents: events,
- steps: []transportStep{
- {write: "AT+CMGS=\"12345\"\r"},
- {write: "HELLO"},
- {
- write: string([]byte{0x1a}),
- chunks: []string{"\r\n+CMGS: 9\r\nOK\r\n"},
- },
- {
- write: "AT+CSQ\r",
- chunks: []string{"\r\n+CSQ: 20,99\r\nOK\r\n"},
- },
- },
- }
- session := newTestSession(t, transport)
- promptResult := make(chan error, 1)
- go func() {
- _, err := session.ExecutePrompt(
- context.Background(),
- `AT+CMGS="12345"`,
- []byte("HELLO"),
- )
- promptResult <- err
- }()
- if first := <-events; first != "AT+CMGS=\"12345\"\r" {
- t.Fatalf("first write = %q", first)
- }
- normalStarted := make(chan struct{})
- normalResult := make(chan error, 1)
- go func() {
- close(normalStarted)
- _, err := session.Execute(context.Background(), "AT+CSQ")
- normalResult <- err
- }()
- <-normalStarted
- transport.enqueue("\r\n> ")
- if err := <-promptResult; err != nil {
- t.Fatalf("ExecutePrompt: %v", err)
- }
- if err := <-normalResult; err != nil {
- t.Fatalf("concurrent Execute: %v", err)
- }
- writes := []string{<-events, <-events, <-events}
- want := []string{"HELLO", string([]byte{0x1a}), "AT+CSQ\r"}
- for index := range want {
- if writes[index] != want[index] {
- t.Fatalf("write[%d] = %q, want %q", index, writes[index], want[index])
- }
- }
- }
- func TestSessionExecutePromptRejectsUnsafeInput(t *testing.T) {
- transport := &transcriptTransport{}
- session := newTestSession(t, transport)
- if _, err := session.ExecutePrompt(
- context.Background(),
- "AT+CSQ",
- []byte("payload"),
- ); err == nil {
- t.Fatal("expected non-CMGS prompt command rejection")
- }
- if _, err := session.ExecutePrompt(
- context.Background(),
- "AT+CMGS=1",
- []byte{'A', 0x1a},
- ); err == nil {
- t.Fatal("expected Ctrl-Z payload rejection")
- }
- }
- func newTestSession(t *testing.T, transport Transport) *Session {
- t.Helper()
- session, err := NewSession(transport, SessionOptions{
- ReadTimeout: time.Millisecond,
- CommandTimeout: time.Second,
- })
- if err != nil {
- t.Fatal(err)
- }
- return session
- }
|