| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907 |
- package vowifi
- import (
- "context"
- "errors"
- "fmt"
- "reflect"
- "strings"
- "sync"
- "testing"
- "time"
- )
- func TestClassifyErrorEAPAuthenticationRejected(t *testing.T) {
- err := fmt.Errorf("tunnel setup: %w", ErrEAPAuthenticationRejected)
- if got := classifyError(PhaseTunnelReady, err); got != "eap_authentication_rejected" {
- t.Fatalf("classifyError = %q", got)
- }
- }
- type fakeEnvironment struct {
- mu sync.Mutex
- calls []string
- failCounts map[string]int
- blockAt string
- blocked chan struct{}
- blockOnce sync.Once
- identity SIMIdentity
- akaEvidence AKAEvidence
- radioSnapshot RadioSnapshot
- proxy ProxyRoute
- tunnelEvidence TunnelEvidence
- imsEvidence IMSEvidence
- smsEvidence SMSEvidence
- phoneRecords []PhoneRecord
- tunnelRequests []TunnelRequest
- tunnelFailures chan error
- imsFailures chan error
- }
- func newFakeEnvironment() *fakeEnvironment {
- return &fakeEnvironment{
- failCounts: make(map[string]int),
- blocked: make(chan struct{}),
- identity: SIMIdentity{
- ICCID: "8944100000000000000",
- IMSI: "234150000000000",
- IMEI: "860000000000000",
- HomeMCC: "234",
- HomeMNC: "15",
- HomeCountryCode: "GB",
- },
- akaEvidence: AKAEvidence{
- Ready: true,
- Application: "usim",
- },
- radioSnapshot: RadioSnapshot{
- CellularDataEnabled: true,
- OperatingMode: 1,
- PureAirplanePolicy: false,
- },
- proxy: ProxyRoute{Mode: ProxyModeDirect},
- tunnelEvidence: TunnelEvidence{
- Established: true,
- Name: "vowifi0",
- ResponderAUTH: ResponderAUTHVerified,
- IKEEncryption: "aes-cbc-128",
- IKEIntegrity: "hmac-sha2-256",
- IKEDHGroup: "modp2048",
- ESPEncryption: "aes-cbc-128",
- ESPIntegrity: "hmac-sha1-96",
- },
- imsEvidence: IMSEvidence{
- Registered: true,
- RegistrationState: "registered",
- AssociatedMSISDN: "[email protected]",
- PAssociatedURI: []string{"sip:[email protected]"},
- Transport: "tcp",
- LastSIPCode: 200,
- },
- smsEvidence: SMSEvidence{Ready: true},
- }
- }
- func (environment *fakeEnvironment) record(ctx context.Context, call string) error {
- environment.mu.Lock()
- environment.calls = append(environment.calls, call)
- block := environment.blockAt == call
- if remaining := environment.failCounts[call]; remaining > 0 {
- environment.failCounts[call] = remaining - 1
- environment.mu.Unlock()
- return errors.New(call + " failed")
- }
- environment.mu.Unlock()
- if block {
- environment.blockOnce.Do(func() { close(environment.blocked) })
- <-ctx.Done()
- return ctx.Err()
- }
- select {
- case <-ctx.Done():
- return ctx.Err()
- default:
- return nil
- }
- }
- func (environment *fakeEnvironment) callsSnapshot() []string {
- environment.mu.Lock()
- defer environment.mu.Unlock()
- return append([]string(nil), environment.calls...)
- }
- func (environment *fakeEnvironment) callCount(call string) int {
- count := 0
- for _, recorded := range environment.callsSnapshot() {
- if recorded == call {
- count++
- }
- }
- return count
- }
- func (environment *fakeEnvironment) setFailure(call string, count int) {
- environment.mu.Lock()
- environment.failCounts[call] = count
- environment.mu.Unlock()
- }
- type fakeSIM struct{ environment *fakeEnvironment }
- func (fake fakeSIM) ReadIdentity(ctx context.Context, _ string) (SIMIdentity, error) {
- if err := fake.environment.record(ctx, "sim.identity"); err != nil {
- return SIMIdentity{}, err
- }
- return fake.environment.identity, nil
- }
- type fakeAKA struct{ environment *fakeEnvironment }
- func (fake fakeAKA) CheckReady(ctx context.Context, _ SIMIdentity) (AKAEvidence, error) {
- if err := fake.environment.record(ctx, "aka.ready"); err != nil {
- return AKAEvidence{}, err
- }
- return fake.environment.akaEvidence, nil
- }
- func (fake fakeAKA) Authenticate(ctx context.Context, _ SIMIdentity, _ AKAChallenge) (AKAResult, error) {
- if err := fake.environment.record(ctx, "aka.authenticate"); err != nil {
- return AKAResult{}, err
- }
- return AKAResult{
- RES: []byte{0x01, 0x02, 0x03, 0x04},
- CK: make([]byte, 16),
- IK: make([]byte, 16),
- }, nil
- }
- type fakeRadio struct{ environment *fakeEnvironment }
- func (fake fakeRadio) Snapshot(ctx context.Context, _ string) (RadioSnapshot, error) {
- if err := fake.environment.record(ctx, "radio.snapshot"); err != nil {
- return RadioSnapshot{}, err
- }
- return fake.environment.radioSnapshot, nil
- }
- func (fake fakeRadio) StopCellularData(ctx context.Context, _ string) error {
- return fake.environment.record(ctx, "radio.stop_data")
- }
- func (fake fakeRadio) EnterVoWiFiRFOff(ctx context.Context, _ string) error {
- return fake.environment.record(ctx, "radio.rf_off")
- }
- func (fake fakeRadio) Restore(ctx context.Context, _ string, _ RadioSnapshot) error {
- return fake.environment.record(ctx, "radio.restore")
- }
- type fakeProxy struct{ environment *fakeEnvironment }
- func (fake fakeProxy) Resolve(ctx context.Context, _ ProxyRequest) (ProxyRoute, error) {
- if err := fake.environment.record(ctx, "proxy.resolve"); err != nil {
- return ProxyRoute{}, err
- }
- return fake.environment.proxy, nil
- }
- type fakeTunnelProvider struct{ environment *fakeEnvironment }
- func (fake fakeTunnelProvider) Start(ctx context.Context, request TunnelRequest) (TunnelSession, error) {
- if err := fake.environment.record(ctx, "tunnel.start"); err != nil {
- return nil, err
- }
- fake.environment.mu.Lock()
- fake.environment.tunnelRequests = append(fake.environment.tunnelRequests, request)
- fake.environment.mu.Unlock()
- return &fakeTunnelSession{environment: fake.environment}, nil
- }
- type fakeTunnelSession struct{ environment *fakeEnvironment }
- func (fake *fakeTunnelSession) Evidence() TunnelEvidence {
- _ = fake.environment.record(context.Background(), "tunnel.evidence")
- return fake.environment.tunnelEvidence
- }
- func (fake *fakeTunnelSession) Close(ctx context.Context) error {
- return fake.environment.record(ctx, "tunnel.close")
- }
- func (fake *fakeTunnelSession) Failures() <-chan error {
- return fake.environment.tunnelFailures
- }
- type fakeIMSProvider struct{ environment *fakeEnvironment }
- func (fake fakeIMSProvider) Start(ctx context.Context, _ IMSRequest) (IMSSession, error) {
- if err := fake.environment.record(ctx, "ims.start"); err != nil {
- return nil, err
- }
- return &fakeIMSSession{environment: fake.environment}, nil
- }
- type fakeIMSSession struct{ environment *fakeEnvironment }
- func (fake *fakeIMSSession) Evidence() IMSEvidence {
- _ = fake.environment.record(context.Background(), "ims.evidence")
- return fake.environment.imsEvidence
- }
- func (fake *fakeIMSSession) EnableSMS(ctx context.Context) (SMSEvidence, error) {
- if err := fake.environment.record(ctx, "ims.sms"); err != nil {
- return SMSEvidence{}, err
- }
- return fake.environment.smsEvidence, nil
- }
- func (fake *fakeIMSSession) Close(ctx context.Context) error {
- return fake.environment.record(ctx, "ims.close")
- }
- func (fake *fakeIMSSession) Failures() <-chan error {
- return fake.environment.imsFailures
- }
- type fakePhones struct{ environment *fakeEnvironment }
- func (fake fakePhones) SaveAssociatedNumber(ctx context.Context, record PhoneRecord) error {
- if err := fake.environment.record(ctx, "phone.save"); err != nil {
- return err
- }
- fake.environment.mu.Lock()
- fake.environment.phoneRecords = append(fake.environment.phoneRecords, record)
- fake.environment.mu.Unlock()
- return nil
- }
- func newTestOrchestrator(t *testing.T, environment *fakeEnvironment, allowMissingAUTH bool) *Orchestrator {
- t.Helper()
- return newTestOrchestratorWithOptions(t, environment, Options{
- DeviceID: "EC20",
- AllowMissingResponderAUTH: allowMissingAUTH,
- CleanupTimeout: time.Second,
- })
- }
- func newTestOrchestratorWithOptions(
- t *testing.T,
- environment *fakeEnvironment,
- options Options,
- ) *Orchestrator {
- t.Helper()
- orchestrator, err := New(Dependencies{
- SIM: fakeSIM{environment},
- AKA: fakeAKA{environment},
- Radio: fakeRadio{environment},
- Proxy: fakeProxy{environment},
- Tunnel: fakeTunnelProvider{environment},
- IMS: fakeIMSProvider{environment},
- Phones: fakePhones{environment},
- }, options)
- if err != nil {
- t.Fatalf("New() error = %v", err)
- }
- return orchestrator
- }
- func TestEnableKeepsIMSAndNumberWhenSMSCapabilityIsOptional(t *testing.T) {
- environment := newFakeEnvironment()
- environment.setFailure("ims.sms", 1)
- orchestrator := newTestOrchestratorWithOptions(t, environment, Options{
- DeviceID: "EC20",
- AllowIMSWithoutSMS: true,
- CleanupTimeout: time.Second,
- })
- state, err := orchestrator.Enable(context.Background())
- if err != nil {
- t.Fatalf("Enable() error = %v", err)
- }
- if state.Phase != PhaseIMSReady || !state.Active || !state.TunnelReady ||
- !state.IMSReady || state.SMSReady {
- t.Fatalf("Enable() state = %+v", state)
- }
- if state.PhoneNumber != "+447700900123" {
- t.Fatalf("phone number = %q", state.PhoneNumber)
- }
- if state.LastReason != "ims_registered_sms_unavailable" ||
- len(state.Warnings) == 0 {
- t.Fatalf("optional SMS evidence = %+v", state)
- }
- if environment.callCount("ims.close") != 0 ||
- environment.callCount("tunnel.close") != 0 {
- t.Fatal("optional SMS failure tore down a valid IMS registration")
- }
- }
- func TestEnableUsesEvidenceBackedOrderAndDisableRollsBackInReverse(t *testing.T) {
- environment := newFakeEnvironment()
- orchestrator := newTestOrchestrator(t, environment, false)
- state, err := orchestrator.Enable(context.Background())
- if err != nil {
- t.Fatalf("Enable() error = %v", err)
- }
- if state.Phase != PhaseSMSReady ||
- !state.Enabled ||
- !state.Active ||
- !state.SIMReady ||
- !state.AccessReady ||
- !state.TunnelReady ||
- !state.IMSReady ||
- !state.SMSReady {
- t.Fatalf("Enable() state = %+v", state)
- }
- if state.PhoneNumber != "+447700900123" ||
- state.PhoneNumberSource != PhoneSourceAssociatedMSISDN {
- t.Fatalf("phone projection = %q (%q)", state.PhoneNumber, state.PhoneNumberSource)
- }
- if state.Security.ResponderAUTH != ResponderAUTHVerified || state.Security.HighRisk {
- t.Fatalf("security audit = %+v", state.Security)
- }
- if state.PureAirplanePolicy {
- t.Fatal("VoWiFi RF off must not enable the independent pure-airplane policy")
- }
- wantEnableCalls := []string{
- "sim.identity",
- "aka.ready",
- "radio.snapshot",
- "radio.rf_off",
- "radio.stop_data",
- "proxy.resolve",
- "tunnel.start",
- "tunnel.evidence",
- "ims.start",
- "ims.evidence",
- "phone.save",
- "ims.sms",
- }
- if calls := environment.callsSnapshot(); !reflect.DeepEqual(calls, wantEnableCalls) {
- t.Fatalf("enable calls = %#v, want %#v", calls, wantEnableCalls)
- }
- if len(environment.tunnelRequests) != 1 {
- t.Fatalf("tunnel request count = %d", len(environment.tunnelRequests))
- }
- request := environment.tunnelRequests[0]
- if request.EPDG != "epdg.epc.mnc015.mcc234.pub.3gppnetwork.org" {
- t.Fatalf("EPDG = %q", request.EPDG)
- }
- if request.Proxy.Mode != ProxyModeDirect || request.Security.AllowMissingResponderAUTH {
- t.Fatalf("tunnel request = %+v", request)
- }
- state, err = orchestrator.Disable(context.Background())
- if err != nil {
- t.Fatalf("Disable() error = %v", err)
- }
- if state.Phase != PhaseIdle || state.Enabled || state.Active ||
- state.TunnelReady || state.IMSReady || state.SMSReady {
- t.Fatalf("Disable() state = %+v", state)
- }
- if state.PhoneNumber != "+447700900123" {
- t.Fatal("disabling the runtime must not erase the ICCID-associated number projection")
- }
- calls := environment.callsSnapshot()
- wantCleanup := []string{"ims.close", "tunnel.close", "radio.restore"}
- if !reflect.DeepEqual(calls[len(calls)-len(wantCleanup):], wantCleanup) {
- t.Fatalf("cleanup tail = %#v, want %#v", calls, wantCleanup)
- }
- }
- func TestEnableFailuresCleanUpEveryAcquiredLayer(t *testing.T) {
- tests := []struct {
- name string
- failCall string
- mutate func(*fakeEnvironment)
- wantError error
- wantCleanupTail []string
- }{
- {name: "identity", failCall: "sim.identity"},
- {name: "aka", failCall: "aka.ready"},
- {name: "radio snapshot", failCall: "radio.snapshot"},
- {
- name: "stop data can partially mutate",
- failCall: "radio.stop_data",
- wantCleanupTail: []string{"radio.restore"},
- },
- {
- name: "rf off",
- failCall: "radio.rf_off",
- wantCleanupTail: []string{"radio.restore"},
- },
- {
- name: "proxy",
- failCall: "proxy.resolve",
- wantCleanupTail: []string{"radio.restore"},
- },
- {
- name: "tunnel start",
- failCall: "tunnel.start",
- wantCleanupTail: []string{"radio.restore"},
- },
- {
- name: "tunnel evidence",
- mutate: func(environment *fakeEnvironment) {
- environment.tunnelEvidence.Established = false
- environment.tunnelEvidence.ResponderAUTH = ResponderAUTHUnknown
- },
- wantError: ErrTunnelNotEstablished,
- wantCleanupTail: []string{"tunnel.close", "radio.restore"},
- },
- {
- name: "IMS start",
- failCall: "ims.start",
- wantCleanupTail: []string{"tunnel.close", "radio.restore"},
- },
- {
- name: "IMS registration evidence",
- mutate: func(environment *fakeEnvironment) {
- environment.imsEvidence.Registered = false
- },
- wantError: ErrIMSNotRegistered,
- wantCleanupTail: []string{"ims.close", "tunnel.close", "radio.restore"},
- },
- {
- name: "SMS activation",
- failCall: "ims.sms",
- wantCleanupTail: []string{"ims.close", "tunnel.close", "radio.restore"},
- },
- {
- name: "SMS evidence",
- mutate: func(environment *fakeEnvironment) {
- environment.smsEvidence.Ready = false
- },
- wantError: ErrSMSNotReady,
- wantCleanupTail: []string{"ims.close", "tunnel.close", "radio.restore"},
- },
- }
- for _, test := range tests {
- t.Run(test.name, func(t *testing.T) {
- environment := newFakeEnvironment()
- if test.failCall != "" {
- environment.setFailure(test.failCall, 1)
- }
- if test.mutate != nil {
- test.mutate(environment)
- }
- orchestrator := newTestOrchestrator(t, environment, false)
- state, err := orchestrator.Enable(context.Background())
- if err == nil {
- t.Fatal("Enable() unexpectedly succeeded")
- }
- if test.wantError != nil && !errors.Is(err, test.wantError) {
- t.Fatalf("Enable() error = %v, want errors.Is(%v)", err, test.wantError)
- }
- if state.Phase != PhaseFailed || state.Active ||
- state.TunnelReady || state.IMSReady || state.SMSReady {
- t.Fatalf("failed state = %+v", state)
- }
- if len(test.wantCleanupTail) > 0 {
- calls := environment.callsSnapshot()
- if len(calls) < len(test.wantCleanupTail) {
- t.Fatalf("calls = %#v", calls)
- }
- tail := calls[len(calls)-len(test.wantCleanupTail):]
- if !reflect.DeepEqual(tail, test.wantCleanupTail) {
- t.Fatalf("cleanup tail = %#v, want %#v", tail, test.wantCleanupTail)
- }
- }
- })
- }
- }
- func TestResponderAUTHPolicyIsStrictByDefaultAndAuditsExplicitCompatibility(t *testing.T) {
- t.Run("strict", func(t *testing.T) {
- environment := newFakeEnvironment()
- environment.tunnelEvidence.ResponderAUTH = ResponderAUTHMissing
- orchestrator := newTestOrchestrator(t, environment, false)
- state, err := orchestrator.Enable(context.Background())
- if !errors.Is(err, ErrResponderAUTHRequired) {
- t.Fatalf("Enable() error = %v", err)
- }
- if state.Phase != PhaseFailed || state.Security.HighRisk ||
- state.Security.CompatibilityOverride {
- t.Fatalf("strict security state = %+v", state.Security)
- }
- })
- t.Run("explicit compatibility", func(t *testing.T) {
- environment := newFakeEnvironment()
- environment.tunnelEvidence.ResponderAUTH = ResponderAUTHMissing
- orchestrator := newTestOrchestrator(t, environment, true)
- state, err := orchestrator.Enable(context.Background())
- if err != nil {
- t.Fatalf("Enable() error = %v", err)
- }
- if state.Phase != PhaseSMSReady ||
- !state.Security.HighRisk ||
- !state.Security.CompatibilityOverride ||
- state.Security.Level != AuditLevelHigh ||
- state.Security.Code != AuditCodeMissingResponderAUTH {
- t.Fatalf("compatibility security state = %+v", state.Security)
- }
- if !environment.tunnelRequests[0].Security.AllowMissingResponderAUTH {
- t.Fatal("explicit compatibility policy was not passed to the tunnel provider")
- }
- })
- t.Run("invalid is never compatible", func(t *testing.T) {
- environment := newFakeEnvironment()
- environment.tunnelEvidence.ResponderAUTH = ResponderAUTHInvalid
- orchestrator := newTestOrchestrator(t, environment, true)
- state, err := orchestrator.Enable(context.Background())
- if !errors.Is(err, ErrResponderAUTHRequired) || state.Phase != PhaseFailed {
- t.Fatalf("Enable() = (%+v, %v)", state, err)
- }
- })
- }
- func TestPhoneNumberIsNeverInferredFromIMSI(t *testing.T) {
- environment := newFakeEnvironment()
- environment.identity.IMSI = "234159999999999"
- environment.imsEvidence.AssociatedMSISDN = ""
- environment.imsEvidence.PAssociatedURI = []string{
- "sip:[email protected]",
- }
- orchestrator := newTestOrchestrator(t, environment, false)
- state, err := orchestrator.Enable(context.Background())
- if err != nil {
- t.Fatalf("Enable() error = %v", err)
- }
- if state.PhoneNumber != "" || environment.callCount("phone.save") != 0 {
- t.Fatalf("number was inferred: state=%+v records=%+v", state, environment.phoneRecords)
- }
- if len(state.Warnings) != 1 || !strings.Contains(state.Warnings[0], "not inferred from IMSI") {
- t.Fatalf("warnings = %#v", state.Warnings)
- }
- }
- func TestPhoneStoreFailureDoesNotMisreportOrTearDownWorkingIMS(t *testing.T) {
- environment := newFakeEnvironment()
- environment.setFailure("phone.save", 1)
- orchestrator := newTestOrchestrator(t, environment, false)
- state, err := orchestrator.Enable(context.Background())
- if err != nil {
- t.Fatalf("Enable() error = %v", err)
- }
- if state.Phase != PhaseSMSReady || !state.IMSReady || state.PhoneNumber != "" {
- t.Fatalf("state = %+v", state)
- }
- if len(state.Warnings) != 1 || !strings.Contains(state.Warnings[0], "could not be persisted") {
- t.Fatalf("warnings = %#v", state.Warnings)
- }
- }
- func TestDisableCancelsAnInFlightEnableAndRestoresRadio(t *testing.T) {
- environment := newFakeEnvironment()
- environment.blockAt = "tunnel.start"
- orchestrator := newTestOrchestrator(t, environment, false)
- enableResult := make(chan error, 1)
- go func() {
- _, err := orchestrator.Enable(context.Background())
- enableResult <- err
- }()
- select {
- case <-environment.blocked:
- case <-time.After(2 * time.Second):
- t.Fatal("Enable() did not reach blocking tunnel provider")
- }
- disableContext, cancel := context.WithTimeout(context.Background(), 2*time.Second)
- defer cancel()
- state, err := orchestrator.Disable(disableContext)
- if err != nil {
- t.Fatalf("Disable() error = %v", err)
- }
- if state.Phase != PhaseIdle || state.Enabled || state.Active {
- t.Fatalf("Disable() state = %+v", state)
- }
- select {
- case err := <-enableResult:
- if !errors.Is(err, context.Canceled) {
- t.Fatalf("Enable() error = %v, want context.Canceled", err)
- }
- case <-time.After(2 * time.Second):
- t.Fatal("Enable() did not exit after Disable() cancellation")
- }
- if environment.callCount("radio.restore") != 1 {
- t.Fatalf("radio.restore count = %d", environment.callCount("radio.restore"))
- }
- }
- func TestConcurrentEnableStartsOnlyOneRuntime(t *testing.T) {
- environment := newFakeEnvironment()
- orchestrator := newTestOrchestrator(t, environment, false)
- const goroutines = 24
- start := make(chan struct{})
- results := make(chan error, goroutines)
- var group sync.WaitGroup
- for index := 0; index < goroutines; index++ {
- group.Add(1)
- go func() {
- defer group.Done()
- <-start
- _, err := orchestrator.Enable(context.Background())
- results <- err
- }()
- }
- close(start)
- group.Wait()
- close(results)
- successes := 0
- alreadyEnabled := 0
- for err := range results {
- switch {
- case err == nil:
- successes++
- case errors.Is(err, ErrAlreadyEnabled):
- alreadyEnabled++
- default:
- t.Fatalf("unexpected Enable() error = %v", err)
- }
- }
- if successes != 1 || alreadyEnabled != goroutines-1 {
- t.Fatalf("successes=%d alreadyEnabled=%d", successes, alreadyEnabled)
- }
- if environment.callCount("tunnel.start") != 1 {
- t.Fatalf("tunnel.start count = %d", environment.callCount("tunnel.start"))
- }
- }
- func TestRetryAfterFailureCreatesANewAttempt(t *testing.T) {
- environment := newFakeEnvironment()
- environment.setFailure("tunnel.start", 1)
- orchestrator := newTestOrchestrator(t, environment, false)
- first, err := orchestrator.Enable(context.Background())
- if err == nil || first.Phase != PhaseFailed || first.Attempt != 1 {
- t.Fatalf("first Enable() = (%+v, %v)", first, err)
- }
- second, err := orchestrator.Retry(context.Background())
- if err != nil {
- t.Fatalf("Retry() error = %v", err)
- }
- if second.Phase != PhaseSMSReady || second.Attempt != 2 {
- t.Fatalf("Retry() state = %+v", second)
- }
- if environment.callCount("tunnel.start") != 2 {
- t.Fatalf("tunnel.start count = %d", environment.callCount("tunnel.start"))
- }
- }
- func TestReconnectClosesThenRebuildsTheRuntime(t *testing.T) {
- environment := newFakeEnvironment()
- orchestrator := newTestOrchestrator(t, environment, false)
- if _, err := orchestrator.Enable(context.Background()); err != nil {
- t.Fatal(err)
- }
- state, err := orchestrator.Reconnect(context.Background())
- if err != nil {
- t.Fatalf("Reconnect() error = %v", err)
- }
- if state.Phase != PhaseSMSReady || state.Attempt != 2 {
- t.Fatalf("Reconnect() state = %+v", state)
- }
- if environment.callCount("tunnel.start") != 2 ||
- environment.callCount("tunnel.close") != 1 ||
- environment.callCount("radio.restore") != 1 {
- t.Fatalf("calls = %#v", environment.callsSnapshot())
- }
- }
- // A non-fatal teardown error (for example the network rejecting SIP
- // deregistration during IMS close) must not stop a reconnect from rebuilding
- // the runtime; Disable still releases the local IMS, tunnel, and radio layers.
- func TestReconnectToleratesCleanupFailureAndRebuilds(t *testing.T) {
- environment := newFakeEnvironment()
- orchestrator := newTestOrchestrator(t, environment, false)
- if _, err := orchestrator.Enable(context.Background()); err != nil {
- t.Fatal(err)
- }
- environment.setFailure("ims.close", 1)
- state, err := orchestrator.Reconnect(context.Background())
- if err != nil {
- t.Fatalf("Reconnect() error = %v", err)
- }
- if state.Phase != PhaseSMSReady || state.Attempt != 2 {
- t.Fatalf("Reconnect() state = %+v", state)
- }
- if environment.callCount("ims.close") != 1 ||
- environment.callCount("tunnel.close") != 1 ||
- environment.callCount("radio.restore") != 1 ||
- environment.callCount("tunnel.start") != 2 {
- t.Fatalf("calls = %#v", environment.callsSnapshot())
- }
- }
- func TestRuntimeTunnelFailureRevokesReadinessAndCleansEveryLayer(t *testing.T) {
- environment := newFakeEnvironment()
- environment.tunnelFailures = make(chan error, 1)
- orchestrator := newTestOrchestrator(t, environment, false)
- if _, err := orchestrator.Enable(context.Background()); err != nil {
- t.Fatal(err)
- }
- environment.tunnelFailures <- errors.New("ESP relay stopped")
- deadline := time.Now().Add(2 * time.Second)
- for {
- state := orchestrator.State()
- if state.Phase == PhaseFailed {
- if state.Active || state.TunnelReady || state.IMSReady || state.SMSReady {
- t.Fatalf("stale runtime readiness survived failure: %+v", state)
- }
- if !state.Enabled || state.LastErrorClass != "tunnel_runtime" ||
- state.LastReason != "runtime_tunnel_failed" ||
- !strings.Contains(state.LastError, "ESP relay stopped") {
- t.Fatalf("runtime failure evidence = %+v", state)
- }
- break
- }
- if time.Now().After(deadline) {
- t.Fatalf("timed out waiting for runtime failure; state = %+v", state)
- }
- time.Sleep(10 * time.Millisecond)
- }
- calls := environment.callsSnapshot()
- wantTail := []string{"ims.close", "tunnel.close", "radio.restore"}
- if len(calls) < len(wantTail) ||
- !reflect.DeepEqual(calls[len(calls)-len(wantTail):], wantTail) {
- t.Fatalf("runtime failure cleanup tail = %#v", calls)
- }
- }
- func TestRuntimeIMSFailureRevokesRegistrationEvidence(t *testing.T) {
- environment := newFakeEnvironment()
- environment.imsFailures = make(chan error, 1)
- orchestrator := newTestOrchestrator(t, environment, false)
- if _, err := orchestrator.Enable(context.Background()); err != nil {
- t.Fatal(err)
- }
- environment.imsFailures <- errors.New("registration refresh failed")
- deadline := time.Now().Add(2 * time.Second)
- for {
- state := orchestrator.State()
- if state.Phase == PhaseFailed {
- if state.TunnelReady || state.IMSReady || state.SMSReady ||
- state.LastErrorClass != "ims_runtime" ||
- state.LastReason != "runtime_ims_failed" {
- t.Fatalf("IMS runtime failure evidence = %+v", state)
- }
- return
- }
- if time.Now().After(deadline) {
- t.Fatalf("timed out waiting for IMS runtime failure; state = %+v", state)
- }
- time.Sleep(10 * time.Millisecond)
- }
- }
- func TestSubscriptionPublishesOrderedEvidencePhases(t *testing.T) {
- environment := newFakeEnvironment()
- orchestrator := newTestOrchestrator(t, environment, false)
- updates, unsubscribe := orchestrator.Subscribe(32)
- defer unsubscribe()
- if _, err := orchestrator.Enable(context.Background()); err != nil {
- t.Fatal(err)
- }
- var phases []Phase
- deadline := time.After(2 * time.Second)
- for {
- select {
- case state := <-updates:
- if len(phases) == 0 || phases[len(phases)-1] != state.Phase {
- phases = append(phases, state.Phase)
- }
- if state.Phase == PhaseSMSReady {
- want := []Phase{
- PhaseIdle,
- PhaseSIMReady,
- PhaseAccessReady,
- PhaseTunnelReady,
- PhaseIMSReady,
- PhaseSMSReady,
- }
- if !reflect.DeepEqual(phases, want) {
- t.Fatalf("phases = %#v, want %#v", phases, want)
- }
- return
- }
- case <-deadline:
- t.Fatalf("timed out waiting for phases; got %#v", phases)
- }
- }
- }
- func TestCleanupAttemptsEveryLayerAndReportsAllErrors(t *testing.T) {
- environment := newFakeEnvironment()
- environment.setFailure("ims.sms", 1)
- environment.setFailure("ims.close", 1)
- environment.setFailure("tunnel.close", 1)
- environment.setFailure("radio.restore", 1)
- orchestrator := newTestOrchestrator(t, environment, false)
- state, err := orchestrator.Enable(context.Background())
- if err == nil {
- t.Fatal("Enable() unexpectedly succeeded")
- }
- if len(state.CleanupErrors) != 3 {
- t.Fatalf("cleanup errors = %#v", state.CleanupErrors)
- }
- calls := environment.callsSnapshot()
- wantTail := []string{"ims.close", "tunnel.close", "radio.restore"}
- if !reflect.DeepEqual(calls[len(calls)-3:], wantTail) {
- t.Fatalf("cleanup tail = %#v", calls[len(calls)-3:])
- }
- for _, text := range []string{"close IMS", "close tunnel", "restore radio"} {
- if !strings.Contains(err.Error(), text) {
- t.Fatalf("error %q does not contain %q", err, text)
- }
- }
- }
- func TestDisableCleanupWarningStillSettlesIdle(t *testing.T) {
- environment := newFakeEnvironment()
- orchestrator := newTestOrchestrator(t, environment, false)
- if _, err := orchestrator.Enable(context.Background()); err != nil {
- t.Fatalf("Enable() error = %v", err)
- }
- environment.setFailure("ims.close", 1)
- state, err := orchestrator.Disable(context.Background())
- if !errors.Is(err, ErrCleanupIncomplete) {
- t.Fatalf("Disable() error = %v, want ErrCleanupIncomplete", err)
- }
- if state.Phase != PhaseIdle || state.Enabled || state.Active ||
- state.SIMReady || state.AccessReady || state.TunnelReady ||
- state.IMSReady || state.SMSReady {
- t.Fatalf("Disable() warning state = %+v", state)
- }
- if state.LastErrorClass != "cleanup_warning" ||
- state.LastReason != "disabled_with_cleanup_errors" ||
- len(state.CleanupErrors) != 1 {
- t.Fatalf("Disable() warning evidence = %+v", state)
- }
- }
- func TestNewRejectsMissingProvidersAndInvalidOptions(t *testing.T) {
- environment := newFakeEnvironment()
- dependencies := Dependencies{
- SIM: fakeSIM{environment},
- AKA: fakeAKA{environment},
- Radio: fakeRadio{environment},
- Proxy: fakeProxy{environment},
- Tunnel: fakeTunnelProvider{environment},
- IMS: fakeIMSProvider{environment},
- Phones: fakePhones{environment},
- }
- if _, err := New(Dependencies{}, Options{DeviceID: "EC20"}); err == nil {
- t.Fatal("New() accepted missing providers")
- }
- if _, err := New(dependencies, Options{}); err == nil {
- t.Fatal("New() accepted empty device ID")
- }
- }
|