esim_test.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. package device
  2. import (
  3. "context"
  4. "encoding/hex"
  5. "errors"
  6. "fmt"
  7. "strings"
  8. "testing"
  9. "time"
  10. "vocat/internal/modem"
  11. )
  12. // tlv builds one BER-TLV element from a (possibly multi-byte) tag and a body
  13. // assembled from the given parts.
  14. func tlv(tag []byte, parts ...[]byte) []byte {
  15. var body []byte
  16. for _, part := range parts {
  17. body = append(body, part...)
  18. }
  19. out := append([]byte(nil), tag...)
  20. switch {
  21. case len(body) < 0x80:
  22. out = append(out, byte(len(body)))
  23. case len(body) < 0x100:
  24. out = append(out, 0x81, byte(len(body)))
  25. default:
  26. out = append(out, 0x82, byte(len(body)>>8), byte(len(body)))
  27. }
  28. return append(out, body...)
  29. }
  30. func esimTestProfile(t *testing.T, iccidDigits, provider, name string, state byte) []byte {
  31. t.Helper()
  32. bcd, err := encodeICCID(iccidDigits)
  33. if err != nil {
  34. t.Fatalf("encodeICCID: %v", err)
  35. }
  36. aid, _ := hex.DecodeString("A0000005591010FFFFFFFF8900001000")
  37. // Icon deliberately contains 0x5A and 0xE3 bytes to prove the parser never
  38. // descends into primitive (non-constructed) leaves.
  39. icon := []byte{0x89, 0x50, 0x4E, 0x47, 0x5A, 0xE3, 0x05, 0x9F, 0x70, 0x01}
  40. return tlv([]byte{0xE3},
  41. tlv([]byte{0x5A}, bcd),
  42. tlv([]byte{0x4F}, aid),
  43. tlv([]byte{0x9F, 0x70}, []byte{state}),
  44. tlv([]byte{0x91}, []byte(provider)),
  45. tlv([]byte{0x92}, []byte(name)),
  46. tlv([]byte{0x94}, icon),
  47. )
  48. }
  49. func TestParseProfilesInfoRealShape(t *testing.T) {
  50. // BF2D root (this card echoes the request tag) -> A0 list -> E3 records.
  51. body := tlv([]byte{0xA0},
  52. esimTestProfile(t, "89441000400128014257", "Vodafone UK", "Vodafone UK eSIM", 0x00),
  53. esimTestProfile(t, "89441000430011604140", "Vodafone UK", "Vodafone UK eSIM", 0x01),
  54. esimTestProfile(t, "89852351225001058508", "Webbing", "WEBBING", 0x00),
  55. )
  56. payload := tlv([]byte{0xBF, 0x2D}, body)
  57. profiles := parseProfilesInfo(payload)
  58. if len(profiles) != 3 {
  59. t.Fatalf("expected 3 profiles, got %d: %#v", len(profiles), profiles)
  60. }
  61. if profiles[0].ICCID != "89441000400128014257" || profiles[0].State != 0 {
  62. t.Fatalf("profile[0] = %#v", profiles[0])
  63. }
  64. if profiles[1].ICCID != "89441000430011604140" || profiles[1].State != 1 || profiles[1].StateText != "已启用" {
  65. t.Fatalf("profile[1] = %#v", profiles[1])
  66. }
  67. if profiles[2].ServiceProvider != "Webbing" || profiles[2].Name != "WEBBING" || profiles[2].State != 0 {
  68. t.Fatalf("profile[2] = %#v", profiles[2])
  69. }
  70. info := &EsimInfo{Profiles: profiles}
  71. enabled := info.EnabledProfile()
  72. if enabled == nil || enabled.Name != "Vodafone UK eSIM" {
  73. t.Fatalf("enabled profile = %#v", enabled)
  74. }
  75. }
  76. func TestParseProfilesInfoSkipsNestedMetadataE3WithoutICCID(t *testing.T) {
  77. real := esimTestProfile(t, "89441000400316048687", "Vodafone UK", "Vodafone UK eSIM", 0x01)
  78. duplicate := esimTestProfile(t, "89441000400316048687", "Duplicate", "Duplicate", 0x00)
  79. metadata := tlv([]byte{0xE3}, tlv([]byte{0x80}, []byte{0x01}))
  80. empty := tlv([]byte{0xE3})
  81. payload := tlv([]byte{0xBF, 0x2D}, tlv([]byte{0xA0}, metadata, real, empty, duplicate))
  82. profiles := parseProfilesInfo(payload)
  83. if len(profiles) != 1 {
  84. t.Fatalf("profiles = %#v, want one addressable profile", profiles)
  85. }
  86. if profiles[0].ICCID != "89441000400316048687" || profiles[0].Name != "Vodafone UK eSIM" {
  87. t.Fatalf("profile = %#v", profiles[0])
  88. }
  89. }
  90. func TestICCIDRoundTrip(t *testing.T) {
  91. for _, digits := range []string{"89441000400128014257", "8985235122500105850", "1"} {
  92. bcd, err := encodeICCID(digits)
  93. if err != nil {
  94. t.Fatalf("encodeICCID(%q): %v", digits, err)
  95. }
  96. if len(bcd) != 10 {
  97. t.Fatalf("encodeICCID(%q) length = %d, want fixed 10 octets", digits, len(bcd))
  98. }
  99. if got := decodeICCID(bcd); got != digits {
  100. t.Fatalf("round trip %q -> %q", digits, got)
  101. }
  102. }
  103. if _, err := encodeICCID("894410004001280142571"); err == nil {
  104. t.Fatal("21-digit ICCID was accepted")
  105. }
  106. }
  107. func TestEnableProfileRequestPads18DigitICCIDToTenOctets(t *testing.T) {
  108. request, err := buildEnableProfileRequest("894921007608519523")
  109. if err != nil {
  110. t.Fatal(err)
  111. }
  112. if got := strings.ToUpper(hex.EncodeToString(request)); got != "BF3111A00C5A0A989412006780155932FF8101FF" {
  113. t.Fatalf("EnableProfile request = %s", got)
  114. }
  115. }
  116. func TestDeleteProfileRequestAndResult(t *testing.T) {
  117. request, err := buildDeleteProfileRequest("89441000400128014257")
  118. if err != nil {
  119. t.Fatal(err)
  120. }
  121. if got := strings.ToUpper(hex.EncodeToString(request)); got != "BF330C5A0A98440100041082102475" {
  122. t.Fatalf("DeleteProfile request = %s", got)
  123. }
  124. result, ok := deleteProfileResult([]byte{0xBF, 0x33, 0x03, 0x80, 0x01, 0x00})
  125. if !ok || result != 0 {
  126. t.Fatalf("DeleteProfile result = (%d, %v)", result, ok)
  127. }
  128. activeResponse := []byte{0xBF, 0x33, 0x03, 0x80, 0x01, 0x02}
  129. if err := deleteProfileResponseError(2, activeResponse); !errors.Is(err, ErrESIMDeleteProfileNotDisabled) {
  130. t.Fatalf("DeleteProfile result 2 error = %v", err)
  131. }
  132. }
  133. func TestSetNicknameRequestAndResult(t *testing.T) {
  134. request, err := buildSetNicknameRequest("89441000400128014257", "Test")
  135. if err != nil {
  136. t.Fatal(err)
  137. }
  138. if got := strings.ToUpper(hex.EncodeToString(request)); got != "BF29125A0A98440100041082102475900454657374" {
  139. t.Fatalf("SetNickname request = %s", got)
  140. }
  141. result, ok := setNicknameResult([]byte{0xBF, 0x29, 0x03, 0x80, 0x01, 0x00})
  142. if !ok || result != 0 {
  143. t.Fatalf("SetNickname result = (%d, %v)", result, ok)
  144. }
  145. if _, err := buildSetNicknameRequest("89441000400128014257", strings.Repeat("名", 65)); !errors.Is(err, ErrESIMNicknameTooLong) {
  146. t.Fatalf("long nickname error = %v", err)
  147. }
  148. }
  149. func TestDisableProfileRequestAndResult(t *testing.T) {
  150. request, err := buildDisableProfileRequest("89441000400128014257")
  151. if err != nil {
  152. t.Fatal(err)
  153. }
  154. if got := strings.ToUpper(hex.EncodeToString(request)); got != "BF3211A00C5A0A984401000410821024758101FF" {
  155. t.Fatalf("DisableProfile request = %s", got)
  156. }
  157. result, ok := disableProfileResult([]byte{0xBF, 0x32, 0x03, 0x80, 0x01, 0x00})
  158. if !ok || result != 0 {
  159. t.Fatalf("DisableProfile result = (%d, %v)", result, ok)
  160. }
  161. busyResponse := []byte{0xBF, 0x32, 0x03, 0x80, 0x01, 0x05}
  162. if err := disableProfileResponseError(5, busyResponse); !errors.Is(err, ErrESIMDisableCATBusy) {
  163. t.Fatalf("DisableProfile result 5 error = %v", err)
  164. }
  165. }
  166. func TestEnableProfileResultErrors(t *testing.T) {
  167. undefinedResponse := []byte{0xBF, 0x31, 0x03, 0x80, 0x01, 0x7F}
  168. result, ok := enableProfileResult(undefinedResponse)
  169. if !ok || result != 0x7F {
  170. t.Fatalf("EnableProfile result = (%d, %v)", result, ok)
  171. }
  172. if err := enableProfileResponseError(byte(result), undefinedResponse); !errors.Is(err, ErrESIMEnableUndefined) {
  173. t.Fatalf("EnableProfile undefinedError = %v", err)
  174. }
  175. policyResponse := []byte{0xBF, 0x31, 0x03, 0x80, 0x01, 0x03}
  176. if err := enableProfileResponseError(3, policyResponse); !errors.Is(err, ErrESIMEnableDisallowedPolicy) {
  177. t.Fatalf("EnableProfile policy error = %v", err)
  178. }
  179. }
  180. func TestVerifySwitchedICCIDReadsLiveModem(t *testing.T) {
  181. client := &transcriptClient{steps: []clientStep{{
  182. command: "AT+CCID",
  183. response: okResponse("+CCID: 89492026266006792824F"),
  184. }}}
  185. manager, id := newStartedTestManager(t, client)
  186. ctx, cancel := context.WithTimeout(context.Background(), time.Second)
  187. defer cancel()
  188. if err := manager.verifySwitchedICCID(ctx, id, "89492026266006792824"); err != nil {
  189. t.Fatalf("verifySwitchedICCID: %v", err)
  190. }
  191. client.assertDone(t)
  192. }
  193. func TestEUMManufacturerForWatchData(t *testing.T) {
  194. if got := eumManufacturerForEID("35840574202500000125000001855764"); got != "WatchData Technologies Ltd." {
  195. t.Fatalf("manufacturer = %q", got)
  196. }
  197. }
  198. func TestEUMManufacturerForEastcompeace(t *testing.T) {
  199. if got := eumManufacturerForEID("89086030202200000025000015085962"); got != "Eastcompeace Technology Co., Ltd." {
  200. t.Fatalf("manufacturer = %q", got)
  201. }
  202. }
  203. func TestEUMManufacturerForModernThalesPrefix(t *testing.T) {
  204. if got := eumManufacturerForEID("89033023427100000000056707807049"); got != "Thales DIS France SAS" {
  205. t.Fatalf("manufacturer = %q", got)
  206. }
  207. }
  208. func TestEuiccSASTrimsCardPadding(t *testing.T) {
  209. payload := derConstruct(0xBF22, derEncode(0x0C, []byte(" SAS-UP-TEST ")))
  210. if got := euiccSAS(payload); got != "SAS-UP-TEST" {
  211. t.Fatalf("SAS = %q", got)
  212. }
  213. }
  214. func TestTransientEuiccCMEClassification(t *testing.T) {
  215. err := fmt.Errorf("select ISD-R: %w", &modem.CommandError{
  216. Command: `AT+CSIM=42,"01A40400"`,
  217. Final: "+CME ERROR: 0",
  218. })
  219. if !isTransientEuiccCME(err) {
  220. t.Fatal("wrapped +CME ERROR: 0 must be retryable")
  221. }
  222. if isTransientEuiccCME(&modem.CommandError{Final: "+CME ERROR: 13"}) {
  223. t.Fatal("SIM failure must not be classified as a transient SELECT error")
  224. }
  225. }
  226. func TestEUICCChannelStuckWrapsTransientCME(t *testing.T) {
  227. cause := &modem.CommandError{
  228. Command: `AT+CSIM=10,"0070000001"`,
  229. Final: "+CME ERROR: 0",
  230. }
  231. err := fmt.Errorf("%w: %v", ErrEUICCChannelStuck, cause)
  232. if !errors.Is(err, ErrEUICCChannelStuck) {
  233. t.Fatal("wrapped hot-swap channel failure must retain its sentinel")
  234. }
  235. }
  236. func TestWaitForESIMRecovery(t *testing.T) {
  237. done := make(chan struct{})
  238. manager := &Manager{esimRecoveries: map[string]chan struct{}{"dev": done}}
  239. go func() {
  240. time.Sleep(10 * time.Millisecond)
  241. close(done)
  242. }()
  243. ctx, cancel := context.WithTimeout(context.Background(), time.Second)
  244. defer cancel()
  245. if err := manager.waitForESIMRecovery(ctx, "dev"); err != nil {
  246. t.Fatalf("waitForESIMRecovery: %v", err)
  247. }
  248. blocked := make(chan struct{})
  249. manager.esimRecoveries["blocked"] = blocked
  250. timeoutContext, cancelTimeout := context.WithTimeout(context.Background(), time.Millisecond)
  251. defer cancelTimeout()
  252. if err := manager.waitForESIMRecovery(timeoutContext, "blocked"); err == nil {
  253. t.Fatal("waitForESIMRecovery must honor caller cancellation")
  254. }
  255. }
  256. func TestESIMListProfilesReturnsCacheDuringRecovery(t *testing.T) {
  257. done := make(chan struct{})
  258. manager := &Manager{
  259. esimRecoveries: map[string]chan struct{}{"dev": done},
  260. esimCache: map[string]EsimInfo{
  261. "dev": {Profiles: []EsimProfile{{ICCID: "old", State: 1}}},
  262. },
  263. }
  264. info, err := manager.ESIMListProfiles(context.Background(), "dev")
  265. if err != nil {
  266. t.Fatalf("ESIMListProfiles during recovery: %v", err)
  267. }
  268. if len(info.Profiles) != 1 || info.Profiles[0].ICCID != "old" {
  269. t.Fatalf("cached profiles = %#v", info.Profiles)
  270. }
  271. // The returned value must not alias the manager cache.
  272. info.Profiles[0].ICCID = "changed"
  273. cached, _ := manager.cachedESIMInfo("dev")
  274. if cached.Profiles[0].ICCID != "old" {
  275. t.Fatalf("caller mutated cache: %#v", cached.Profiles)
  276. }
  277. }
  278. func TestMarkCachedProfileEnabled(t *testing.T) {
  279. manager := &Manager{esimCache: map[string]EsimInfo{
  280. "dev": {Profiles: []EsimProfile{
  281. {ICCID: "old", State: 1, StateText: "old state"},
  282. {ICCID: "target", State: 0, StateText: "target state"},
  283. }},
  284. }}
  285. manager.markCachedProfileEnabled("dev", "target")
  286. info, ok := manager.cachedESIMInfo("dev")
  287. if !ok || info.Profiles[0].State != 0 || info.Profiles[0].StateText != "已禁用" {
  288. t.Fatalf("old profile state = %#v", info.Profiles[0])
  289. }
  290. if info.Profiles[1].State != 1 || info.Profiles[1].StateText != "已启用" {
  291. t.Fatalf("target profile state = %#v", info.Profiles[1])
  292. }
  293. }