package device import ( "context" "encoding/hex" "errors" "fmt" "strings" "testing" "time" "vocat/internal/modem" ) // tlv builds one BER-TLV element from a (possibly multi-byte) tag and a body // assembled from the given parts. func tlv(tag []byte, parts ...[]byte) []byte { var body []byte for _, part := range parts { body = append(body, part...) } out := append([]byte(nil), tag...) switch { case len(body) < 0x80: out = append(out, byte(len(body))) case len(body) < 0x100: out = append(out, 0x81, byte(len(body))) default: out = append(out, 0x82, byte(len(body)>>8), byte(len(body))) } return append(out, body...) } func esimTestProfile(t *testing.T, iccidDigits, provider, name string, state byte) []byte { t.Helper() bcd, err := encodeICCID(iccidDigits) if err != nil { t.Fatalf("encodeICCID: %v", err) } aid, _ := hex.DecodeString("A0000005591010FFFFFFFF8900001000") // Icon deliberately contains 0x5A and 0xE3 bytes to prove the parser never // descends into primitive (non-constructed) leaves. icon := []byte{0x89, 0x50, 0x4E, 0x47, 0x5A, 0xE3, 0x05, 0x9F, 0x70, 0x01} return tlv([]byte{0xE3}, tlv([]byte{0x5A}, bcd), tlv([]byte{0x4F}, aid), tlv([]byte{0x9F, 0x70}, []byte{state}), tlv([]byte{0x91}, []byte(provider)), tlv([]byte{0x92}, []byte(name)), tlv([]byte{0x94}, icon), ) } func TestParseProfilesInfoRealShape(t *testing.T) { // BF2D root (this card echoes the request tag) -> A0 list -> E3 records. body := tlv([]byte{0xA0}, esimTestProfile(t, "89441000400128014257", "Vodafone UK", "Vodafone UK eSIM", 0x00), esimTestProfile(t, "89441000430011604140", "Vodafone UK", "Vodafone UK eSIM", 0x01), esimTestProfile(t, "89852351225001058508", "Webbing", "WEBBING", 0x00), ) payload := tlv([]byte{0xBF, 0x2D}, body) profiles := parseProfilesInfo(payload) if len(profiles) != 3 { t.Fatalf("expected 3 profiles, got %d: %#v", len(profiles), profiles) } if profiles[0].ICCID != "89441000400128014257" || profiles[0].State != 0 { t.Fatalf("profile[0] = %#v", profiles[0]) } if profiles[1].ICCID != "89441000430011604140" || profiles[1].State != 1 || profiles[1].StateText != "已启用" { t.Fatalf("profile[1] = %#v", profiles[1]) } if profiles[2].ServiceProvider != "Webbing" || profiles[2].Name != "WEBBING" || profiles[2].State != 0 { t.Fatalf("profile[2] = %#v", profiles[2]) } info := &EsimInfo{Profiles: profiles} enabled := info.EnabledProfile() if enabled == nil || enabled.Name != "Vodafone UK eSIM" { t.Fatalf("enabled profile = %#v", enabled) } } func TestParseProfilesInfoSkipsNestedMetadataE3WithoutICCID(t *testing.T) { real := esimTestProfile(t, "89441000400316048687", "Vodafone UK", "Vodafone UK eSIM", 0x01) duplicate := esimTestProfile(t, "89441000400316048687", "Duplicate", "Duplicate", 0x00) metadata := tlv([]byte{0xE3}, tlv([]byte{0x80}, []byte{0x01})) empty := tlv([]byte{0xE3}) payload := tlv([]byte{0xBF, 0x2D}, tlv([]byte{0xA0}, metadata, real, empty, duplicate)) profiles := parseProfilesInfo(payload) if len(profiles) != 1 { t.Fatalf("profiles = %#v, want one addressable profile", profiles) } if profiles[0].ICCID != "89441000400316048687" || profiles[0].Name != "Vodafone UK eSIM" { t.Fatalf("profile = %#v", profiles[0]) } } func TestICCIDRoundTrip(t *testing.T) { for _, digits := range []string{"89441000400128014257", "8985235122500105850", "1"} { bcd, err := encodeICCID(digits) if err != nil { t.Fatalf("encodeICCID(%q): %v", digits, err) } if len(bcd) != 10 { t.Fatalf("encodeICCID(%q) length = %d, want fixed 10 octets", digits, len(bcd)) } if got := decodeICCID(bcd); got != digits { t.Fatalf("round trip %q -> %q", digits, got) } } if _, err := encodeICCID("894410004001280142571"); err == nil { t.Fatal("21-digit ICCID was accepted") } } func TestEnableProfileRequestPads18DigitICCIDToTenOctets(t *testing.T) { request, err := buildEnableProfileRequest("894921007608519523") if err != nil { t.Fatal(err) } if got := strings.ToUpper(hex.EncodeToString(request)); got != "BF3111A00C5A0A989412006780155932FF8101FF" { t.Fatalf("EnableProfile request = %s", got) } } func TestDeleteProfileRequestAndResult(t *testing.T) { request, err := buildDeleteProfileRequest("89441000400128014257") if err != nil { t.Fatal(err) } if got := strings.ToUpper(hex.EncodeToString(request)); got != "BF330C5A0A98440100041082102475" { t.Fatalf("DeleteProfile request = %s", got) } result, ok := deleteProfileResult([]byte{0xBF, 0x33, 0x03, 0x80, 0x01, 0x00}) if !ok || result != 0 { t.Fatalf("DeleteProfile result = (%d, %v)", result, ok) } activeResponse := []byte{0xBF, 0x33, 0x03, 0x80, 0x01, 0x02} if err := deleteProfileResponseError(2, activeResponse); !errors.Is(err, ErrESIMDeleteProfileNotDisabled) { t.Fatalf("DeleteProfile result 2 error = %v", err) } } func TestSetNicknameRequestAndResult(t *testing.T) { request, err := buildSetNicknameRequest("89441000400128014257", "Test") if err != nil { t.Fatal(err) } if got := strings.ToUpper(hex.EncodeToString(request)); got != "BF29125A0A98440100041082102475900454657374" { t.Fatalf("SetNickname request = %s", got) } result, ok := setNicknameResult([]byte{0xBF, 0x29, 0x03, 0x80, 0x01, 0x00}) if !ok || result != 0 { t.Fatalf("SetNickname result = (%d, %v)", result, ok) } if _, err := buildSetNicknameRequest("89441000400128014257", strings.Repeat("名", 65)); !errors.Is(err, ErrESIMNicknameTooLong) { t.Fatalf("long nickname error = %v", err) } } func TestDisableProfileRequestAndResult(t *testing.T) { request, err := buildDisableProfileRequest("89441000400128014257") if err != nil { t.Fatal(err) } if got := strings.ToUpper(hex.EncodeToString(request)); got != "BF3211A00C5A0A984401000410821024758101FF" { t.Fatalf("DisableProfile request = %s", got) } result, ok := disableProfileResult([]byte{0xBF, 0x32, 0x03, 0x80, 0x01, 0x00}) if !ok || result != 0 { t.Fatalf("DisableProfile result = (%d, %v)", result, ok) } busyResponse := []byte{0xBF, 0x32, 0x03, 0x80, 0x01, 0x05} if err := disableProfileResponseError(5, busyResponse); !errors.Is(err, ErrESIMDisableCATBusy) { t.Fatalf("DisableProfile result 5 error = %v", err) } } func TestEnableProfileResultErrors(t *testing.T) { undefinedResponse := []byte{0xBF, 0x31, 0x03, 0x80, 0x01, 0x7F} result, ok := enableProfileResult(undefinedResponse) if !ok || result != 0x7F { t.Fatalf("EnableProfile result = (%d, %v)", result, ok) } if err := enableProfileResponseError(byte(result), undefinedResponse); !errors.Is(err, ErrESIMEnableUndefined) { t.Fatalf("EnableProfile undefinedError = %v", err) } policyResponse := []byte{0xBF, 0x31, 0x03, 0x80, 0x01, 0x03} if err := enableProfileResponseError(3, policyResponse); !errors.Is(err, ErrESIMEnableDisallowedPolicy) { t.Fatalf("EnableProfile policy error = %v", err) } } func TestVerifySwitchedICCIDReadsLiveModem(t *testing.T) { client := &transcriptClient{steps: []clientStep{{ command: "AT+CCID", response: okResponse("+CCID: 89492026266006792824F"), }}} manager, id := newStartedTestManager(t, client) ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() if err := manager.verifySwitchedICCID(ctx, id, "89492026266006792824"); err != nil { t.Fatalf("verifySwitchedICCID: %v", err) } client.assertDone(t) } func TestEUMManufacturerForWatchData(t *testing.T) { if got := eumManufacturerForEID("35840574202500000125000001855764"); got != "WatchData Technologies Ltd." { t.Fatalf("manufacturer = %q", got) } } func TestEUMManufacturerForEastcompeace(t *testing.T) { if got := eumManufacturerForEID("89086030202200000025000015085962"); got != "Eastcompeace Technology Co., Ltd." { t.Fatalf("manufacturer = %q", got) } } func TestEUMManufacturerForModernThalesPrefix(t *testing.T) { if got := eumManufacturerForEID("89033023427100000000056707807049"); got != "Thales DIS France SAS" { t.Fatalf("manufacturer = %q", got) } } func TestEuiccSASTrimsCardPadding(t *testing.T) { payload := derConstruct(0xBF22, derEncode(0x0C, []byte(" SAS-UP-TEST "))) if got := euiccSAS(payload); got != "SAS-UP-TEST" { t.Fatalf("SAS = %q", got) } } func TestTransientEuiccCMEClassification(t *testing.T) { err := fmt.Errorf("select ISD-R: %w", &modem.CommandError{ Command: `AT+CSIM=42,"01A40400"`, Final: "+CME ERROR: 0", }) if !isTransientEuiccCME(err) { t.Fatal("wrapped +CME ERROR: 0 must be retryable") } if isTransientEuiccCME(&modem.CommandError{Final: "+CME ERROR: 13"}) { t.Fatal("SIM failure must not be classified as a transient SELECT error") } } func TestEUICCChannelStuckWrapsTransientCME(t *testing.T) { cause := &modem.CommandError{ Command: `AT+CSIM=10,"0070000001"`, Final: "+CME ERROR: 0", } err := fmt.Errorf("%w: %v", ErrEUICCChannelStuck, cause) if !errors.Is(err, ErrEUICCChannelStuck) { t.Fatal("wrapped hot-swap channel failure must retain its sentinel") } } func TestWaitForESIMRecovery(t *testing.T) { done := make(chan struct{}) manager := &Manager{esimRecoveries: map[string]chan struct{}{"dev": done}} go func() { time.Sleep(10 * time.Millisecond) close(done) }() ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() if err := manager.waitForESIMRecovery(ctx, "dev"); err != nil { t.Fatalf("waitForESIMRecovery: %v", err) } blocked := make(chan struct{}) manager.esimRecoveries["blocked"] = blocked timeoutContext, cancelTimeout := context.WithTimeout(context.Background(), time.Millisecond) defer cancelTimeout() if err := manager.waitForESIMRecovery(timeoutContext, "blocked"); err == nil { t.Fatal("waitForESIMRecovery must honor caller cancellation") } } func TestESIMListProfilesReturnsCacheDuringRecovery(t *testing.T) { done := make(chan struct{}) manager := &Manager{ esimRecoveries: map[string]chan struct{}{"dev": done}, esimCache: map[string]EsimInfo{ "dev": {Profiles: []EsimProfile{{ICCID: "old", State: 1}}}, }, } info, err := manager.ESIMListProfiles(context.Background(), "dev") if err != nil { t.Fatalf("ESIMListProfiles during recovery: %v", err) } if len(info.Profiles) != 1 || info.Profiles[0].ICCID != "old" { t.Fatalf("cached profiles = %#v", info.Profiles) } // The returned value must not alias the manager cache. info.Profiles[0].ICCID = "changed" cached, _ := manager.cachedESIMInfo("dev") if cached.Profiles[0].ICCID != "old" { t.Fatalf("caller mutated cache: %#v", cached.Profiles) } } func TestMarkCachedProfileEnabled(t *testing.T) { manager := &Manager{esimCache: map[string]EsimInfo{ "dev": {Profiles: []EsimProfile{ {ICCID: "old", State: 1, StateText: "old state"}, {ICCID: "target", State: 0, StateText: "target state"}, }}, }} manager.markCachedProfileEnabled("dev", "target") info, ok := manager.cachedESIMInfo("dev") if !ok || info.Profiles[0].State != 0 || info.Profiles[0].StateText != "已禁用" { t.Fatalf("old profile state = %#v", info.Profiles[0]) } if info.Profiles[1].State != 1 || info.Profiles[1].StateText != "已启用" { t.Fatalf("target profile state = %#v", info.Profiles[1]) } }