package device import ( "context" "errors" "fmt" "strings" "testing" "vocat/internal/modem" ) func TestManagerSendSMSDirectGSM7ReturnsAcceptanceEvidence(t *testing.T) { client := &transcriptClient{ steps: []clientStep{ {command: "AT+CMGF=1", response: okResponse()}, {command: `AT+CSCS="GSM"`, response: okResponse()}, {command: "AT+CSMP=49,167,0,0", response: okResponse()}, }, promptSteps: []promptClientStep{{ command: `AT+CMGS="+12345"`, payload: "HELLO", response: okResponse("+CMGS: 23"), }}, } manager, id := newStartedTestManager(t, client) result, err := manager.SendSMS( context.Background(), id, "+12 345", "HELLO", ) if err != nil { t.Fatalf("SendSMS: %v", err) } if result.To != "+12345" || result.Encoding != SMSEncodingGSM7Text || !result.AcceptedByModem || !result.ReferenceKnown || result.MessageReference != 23 || result.DeliveryConfirmed || result.DeliveryStatus != "unknown" || result.SubmissionStatus != "accepted_by_modem" || result.PartsTotal != 1 || result.PartsAttempted != 1 || result.PartsAccepted != 1 || !result.AllPartsAccepted || len(result.PartResults) != 1 { t.Fatalf("result = %#v", result) } if len(result.ModemEvidence) != 1 || result.ModemEvidence[0] != "+CMGS: 23" { t.Fatalf("evidence = %#v", result.ModemEvidence) } client.assertDone(t) } func TestManagerSendMultipartSMSReturnsEveryPartReference(t *testing.T) { var concatReference = -1 validatePart := func(sequence int, wantText string) func(string) error { return func(payload string) error { message, err := decodeSMSPDU(payload) if err != nil { return fmt.Errorf("decode part %d: %w", sequence, err) } if message.Text != wantText || message.Concat == nil || message.Concat.Total != 2 || message.Concat.Sequence != sequence { return fmt.Errorf("part %d decoded as %#v", sequence, message) } if concatReference < 0 { concatReference = message.Concat.Reference } else if message.Concat.Reference != concatReference { return fmt.Errorf( "part %d concat reference %d, want %d", sequence, message.Concat.Reference, concatReference, ) } return nil } } client := &transcriptClient{ steps: []clientStep{{command: "AT+CMGF=0", response: okResponse()}}, promptSteps: []promptClientStep{ { command: "AT+CMGS=150", validateBody: validatePart(1, strings.Repeat("A", 153)), response: okResponse("+CMGS: 31"), }, { command: "AT+CMGS=24", validateBody: validatePart(2, strings.Repeat("A", 8)), response: okResponse("+CMGS: 32"), }, }, } manager, id := newStartedTestManager(t, client) result, err := manager.SendSMS( context.Background(), id, "+12345", strings.Repeat("A", 161), ) if err != nil { t.Fatalf("SendSMS: %v", err) } if result.PartsTotal != 2 || result.PartsAttempted != 2 || result.PartsAccepted != 2 || !result.AcceptedByModem || !result.AllPartsAccepted || result.ReferenceKnown || result.ConcatReference == nil || *result.ConcatReference != concatReference || len(result.PartResults) != 2 || result.PartResults[0].MessageReference != 31 || result.PartResults[1].MessageReference != 32 || !result.PartResults[0].AcceptedByModem || !result.PartResults[1].AcceptedByModem || result.DeliveryConfirmed { t.Fatalf("result = %#v", result) } client.assertDone(t) } func TestManagerSendMultipartSMSStopsAndPreservesPartialEvidence(t *testing.T) { validatePart := func(sequence int, wantText string) func(string) error { return func(payload string) error { message, err := decodeSMSPDU(payload) if err != nil { return err } if message.Concat == nil || message.Concat.Sequence != sequence || message.Text != wantText { return fmt.Errorf("part %d decoded as %#v", sequence, message) } return nil } } secondError := &modem.CommandError{ Command: "AT+CMGS=24", Final: "+CMS ERROR: 500", } client := &transcriptClient{ steps: []clientStep{{command: "AT+CMGF=0", response: okResponse()}}, promptSteps: []promptClientStep{ { command: "AT+CMGS=150", validateBody: validatePart(1, strings.Repeat("A", 153)), response: okResponse("+CMGS: 41"), }, { command: "AT+CMGS=24", validateBody: validatePart(2, strings.Repeat("A", 8)), response: modem.Response{ Final: "+CMS ERROR: 500", }, err: secondError, }, }, } manager, id := newStartedTestManager(t, client) result, err := manager.SendSMS( context.Background(), id, "+12345", strings.Repeat("A", 161), ) var commandErr *modem.CommandError if !errors.As(err, &commandErr) { t.Fatalf("error = %v", err) } if result.PartsTotal != 2 || result.PartsAttempted != 2 || result.PartsAccepted != 1 || result.AcceptedByModem || result.AllPartsAccepted || result.SubmissionStatus != "partially_accepted_by_modem" || len(result.PartResults) != 2 || !result.PartResults[0].AcceptedByModem || result.PartResults[0].MessageReference != 41 || result.PartResults[1].AcceptedByModem || result.PartResults[1].SubmissionStatus != "rejected_by_modem" || result.DeliveryConfirmed { t.Fatalf("result = %#v", result) } client.assertDone(t) } func TestManagerSendSMSUsesUCS2PDUForChinese(t *testing.T) { client := &transcriptClient{ steps: []clientStep{ {command: "AT+CMGF=0", response: okResponse()}, }, promptSteps: []promptClientStep{{ command: "AT+CMGS=14", payload: "00210005912143F50008044F60597D", response: okResponse("+CMGS: 0"), }}, } manager, id := newStartedTestManager(t, client) result, err := manager.SendSMS( context.Background(), id, "+12345", "你好", ) if err != nil { t.Fatalf("SendSMS: %v", err) } if result.Encoding != SMSEncodingUCS2PDU || !result.ReferenceKnown || result.MessageReference != 0 || !result.AllPartsAccepted || result.DeliveryConfirmed { t.Fatalf("result = %#v", result) } client.assertDone(t) } func TestManagerSendSMSTimeoutNeverClaimsAcceptanceOrDelivery(t *testing.T) { client := &transcriptClient{ steps: []clientStep{ {command: "AT+CMGF=1", response: okResponse()}, {command: `AT+CSCS="GSM"`, response: okResponse()}, {command: "AT+CSMP=49,167,0,0", response: okResponse()}, }, promptSteps: []promptClientStep{{ command: `AT+CMGS="12345"`, payload: "HELLO", response: modem.Response{ Lines: []string{"+CMGS: 77"}, }, err: modem.ErrCommandTimeout, }}, } manager, id := newStartedTestManager(t, client) result, err := manager.SendSMS( context.Background(), id, "12345", "HELLO", ) if !errors.Is(err, modem.ErrCommandTimeout) { t.Fatalf("error = %v", err) } if result.AcceptedByModem || result.DeliveryConfirmed || !result.ReferenceKnown || result.MessageReference != 77 || result.SubmissionStatus != "reference_returned_without_final" || result.DeliveryStatus != "unknown" { t.Fatalf("result = %#v", result) } client.mu.Lock() closeCount := client.closeCount client.mu.Unlock() if closeCount != 1 { t.Fatalf("close count = %d, want 1 after uncertain timeout", closeCount) } client.assertDone(t) } func TestManagerListReadAndDeleteSMS(t *testing.T) { const gsmPDU = "000405912143F500004210203040500005C82293F904" const ucs2PDU = "000405912143F5000842102030405000044F60597D" client := &transcriptClient{steps: []clientStep{ {command: "AT+CMGF=0", response: okResponse()}, {command: `AT+CPMS="SM"`, response: okResponse()}, { command: "AT+CMGL=4", response: okResponse( "+CMGL: 7,0,,23", gsmPDU, "+CMGL: 8,1,,22", ucs2PDU, ), }, {command: `AT+CPMS="ME"`, response: okResponse()}, {command: "AT+CMGL=4", response: okResponse()}, {command: "AT+CMGF=0", response: okResponse()}, { command: "AT+CMGR=7", response: okResponse( "+CMGR: 0,,23", gsmPDU, ), }, {command: "AT+CMGD=7", response: okResponse()}, }} manager, id := newStartedTestManager(t, client) messages, err := manager.ListSMS(context.Background(), id) if err != nil { t.Fatalf("ListSMS: %v", err) } if len(messages) != 2 || messages[0].Index != 7 || messages[0].Storage != "SM" || messages[0].StorageStatus != SMSStatusReceivedUnread || messages[0].Text != "HELLO" || messages[1].Index != 8 || messages[1].Storage != "SM" || messages[1].StorageStatus != SMSStatusReceivedRead || messages[1].Text != "你好" { t.Fatalf("messages = %#v", messages) } message, err := manager.ReadSMS(context.Background(), id, 7) if err != nil { t.Fatalf("ReadSMS: %v", err) } if message.Index != 7 || message.StorageStatus != SMSStatusReceivedUnread || message.Text != "HELLO" { t.Fatalf("message = %#v", message) } if err := manager.DeleteSMS(context.Background(), id, 7); err != nil { t.Fatalf("DeleteSMS: %v", err) } client.assertDone(t) } func TestManagerSendSMSRequiresMessageReference(t *testing.T) { client := &transcriptClient{ steps: []clientStep{ {command: "AT+CMGF=1", response: okResponse()}, {command: `AT+CSCS="GSM"`, response: okResponse()}, {command: "AT+CSMP=49,167,0,0", response: okResponse()}, }, promptSteps: []promptClientStep{{ command: `AT+CMGS="12345"`, payload: "HELLO", response: okResponse(), }}, } manager, id := newStartedTestManager(t, client) result, err := manager.SendSMS( context.Background(), id, "12345", "HELLO", ) if !errors.Is(err, ErrSMSReferenceMissing) { t.Fatalf("error = %v", err) } if result.AcceptedByModem || result.ReferenceKnown || result.DeliveryConfirmed || result.SubmissionStatus != "unconfirmed_without_reference" { t.Fatalf("result = %#v", result) } client.assertDone(t) }