Files
VoCat/internal/server/at_guard_test.go
T
2026-08-19 21:47:26 +08:00

145 lines
3.7 KiB
Go

package server
import (
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"
"vocat/internal/device"
"vocat/internal/modem"
)
func TestValidateATCommandBlocksTrafficMessagingAndDialActions(t *testing.T) {
t.Parallel()
for _, command := range []string{
"AT+CGATT=1",
"AT+CGACT=1,1",
"AT+CGDATA=\"PPP\",1",
"AT+QNETDEVCTL=1,1,1",
"AT+QIACT=1",
"AT+CMGS=42",
"AT+CMSS=7",
"AT+CMGC=12",
"AT+QCMGS=42",
"AT+CUSD=1,\"*100#\"",
"ATD12345;",
"ATA",
"ATH",
"AT+CSQ; +CGACT = 1,1",
"AT+CSQ;+CMSS=7",
"AT+CSQ;D12345;",
} {
if err := validateATCommand(command, false); err == nil {
t.Errorf("validateATCommand(%q) permitted a guarded mutation", command)
}
}
}
func TestValidateATCommandForceBypassesGuard(t *testing.T) {
t.Parallel()
for _, command := range []string{
"AT+CGATT=1",
"AT+CFUN=1",
"AT+CGACT=1,1",
"AT+CUSD=1,\"*100#\"",
"ATD12345;",
} {
if err := validateATCommand(command, true); err != nil {
t.Errorf("validateATCommand(%q, true): %v", command, err)
}
}
}
func TestValidateATCommandForceKeepsSyntaxChecks(t *testing.T) {
t.Parallel()
for _, command := range []string{
"A",
"",
"AT\r",
"AT\n",
string(make([]byte, 513)),
} {
if err := validateATCommand(command, true); err == nil {
t.Errorf("validateATCommand(%q, true) skipped syntax check", command)
}
}
}
func TestValidateATCommandAllowsReadOnlyStatusQueries(t *testing.T) {
t.Parallel()
for _, command := range []string{
"AT",
"AT+CPIN?",
"AT+CGATT?",
"AT+CGACT?",
"AT+CFUN?",
"AT+CIMI",
"AT+CCID",
} {
if err := validateATCommand(command, false); err != nil {
t.Errorf("validateATCommand(%q): %v", command, err)
}
}
}
// The AT terminal must present ERROR / +CME ERROR as a normal response, not as
// a 502. Before the CommandError branch was restored, every unsupported or
// SIM-less command was folded into "the device operation failed", hiding the
// real reason from the user.
func TestHandleATSurfacesCommandErrorAsResponse(t *testing.T) {
controller := fakeDeviceController{
entry: device.Device{ID: "dev1"},
atHandler: func(command string) (modem.Response, error) {
return modem.Response{}, &modem.CommandError{
Command: command,
Final: "+CME ERROR: 10",
Lines: []string{"+CME ERROR: 10"},
}
},
}
server := &Server{devices: controller, logger: regionTestLogger(), maxRequestBodyBytes: 1 << 20}
recorder := httptest.NewRecorder()
request := httptest.NewRequest(
http.MethodPost,
"/api/devices/dev1/actions/at",
strings.NewReader(`{"cmd":"AT+CPIN?","timeout_ms":5000}`),
)
request.Header.Set("Content-Type", "application/json")
if !server.handleAT(recorder, request, "dev1") {
t.Fatal("handleAT returned false")
}
if recorder.Code != http.StatusOK {
t.Fatalf("status = %d, want 200 (body=%s)", recorder.Code, recorder.Body.String())
}
data := decodeData(t, recorder)
response, _ := data["response"].(string)
if !strings.Contains(response, "+CME ERROR: 10") {
t.Fatalf("response = %q, want +CME ERROR text", response)
}
}
func TestHandleATMapsNonCommandErrorTo502(t *testing.T) {
controller := fakeDeviceController{
entry: device.Device{ID: "dev1"},
atErr: errors.New("transport wedged"),
}
server := &Server{devices: controller, logger: regionTestLogger(), maxRequestBodyBytes: 1 << 20}
recorder := httptest.NewRecorder()
request := httptest.NewRequest(
http.MethodPost,
"/api/devices/dev1/actions/at",
strings.NewReader(`{"cmd":"AT+CSQ"}`),
)
request.Header.Set("Content-Type", "application/json")
if !server.handleAT(recorder, request, "dev1") {
t.Fatal("handleAT returned false")
}
if recorder.Code != http.StatusBadGateway {
t.Fatalf("status = %d, want 502", recorder.Code)
}
}