package integration import ( "context" "encoding/json" "testing" "time" "vocat/internal/device" "vocat/internal/store" "vocat/internal/vowifi" ) func testStore(t *testing.T) *store.Store { t.Helper() database, err := store.Open(context.Background(), ":memory:") if err != nil { t.Fatal(err) } t.Cleanup(func() { _ = database.Close() }) return database } func TestProxyResolverUsesDeviceBinding(t *testing.T) { database := testStore(t) if err := database.UpsertDevice(context.Background(), store.Device{ID: "ec20", Name: "EC20"}); err != nil { t.Fatal(err) } if err := database.UpsertUpstreamProxy(context.Background(), store.UpstreamProxy{ ID: "clash", Name: "Clash", Addr: "192.168.2.143:7897", Enabled: true, Password: "must-not-be-lost", Username: "proxy-user", }); err != nil { t.Fatal(err) } if err := database.UpsertDeviceProxyBinding(context.Background(), store.DeviceProxyBinding{ DeviceID: "ec20", UpstreamProxyID: "clash", }); err != nil { t.Fatal(err) } route, err := (ProxyResolver{Store: database}).Resolve( context.Background(), vowifi.ProxyRequest{DeviceID: "ec20", HomeMCC: "234", HomeMNC: "15"}, ) if err != nil { t.Fatal(err) } if route.Mode != vowifi.ProxyModeSOCKS5 || route.Address != "192.168.2.143:7897" || route.Username != "proxy-user" || route.Password != "must-not-be-lost" { t.Fatalf("route = %#v", route) } } func TestProxyResolverDoesNotUseCountryRuleWithoutDeviceBinding(t *testing.T) { database := testStore(t) if err := database.UpsertUpstreamProxy(context.Background(), store.UpstreamProxy{ ID: "legacy", Name: "Legacy", Addr: "127.0.0.1:1080", Enabled: true, }); err != nil { t.Fatal(err) } if err := database.UpsertCountryRule(context.Background(), store.CountryRule{ CountryCode: "GB", CountryName: "United Kingdom", UpstreamProxyID: "legacy", Enabled: true, }); err != nil { t.Fatal(err) } route, err := (ProxyResolver{Store: database}).Resolve( context.Background(), vowifi.ProxyRequest{DeviceID: "ec20", HomeMCC: "234"}, ) if err != nil { t.Fatal(err) } if route.Mode != vowifi.ProxyModeDirect { t.Fatalf("route = %#v, want direct", route) } } func TestPhoneStoreRejectsUntrustedSource(t *testing.T) { database := testStore(t) phones := PhoneStore{Store: database, DeviceID: "ec20"} record := vowifi.PhoneRecord{ ICCID: "89441000400311061404", Number: "+447700900123", Source: "imsi_guess", UpdatedAt: time.Now(), } if err := phones.SaveAssociatedNumber(context.Background(), record); err == nil { t.Fatal("untrusted source was accepted") } record.Source = vowifi.PhoneSourcePAssociatedURI if err := phones.SaveAssociatedNumber(context.Background(), record); err != nil { t.Fatal(err) } got, err := database.PhoneAssociation(context.Background(), record.ICCID) if err != nil { t.Fatal(err) } if got.Number != record.Number || got.Source != record.Source { t.Fatalf("association = %#v", got) } } func TestStateProjectorRestoresVerifiedNumber(t *testing.T) { database := testStore(t) if err := database.UpsertDevice(context.Background(), store.Device{ ID: "ec20", Name: "EC20", }); err != nil { t.Fatal(err) } if err := database.UpsertPhoneAssociation(context.Background(), store.PhoneAssociation{ ICCID: "89441000400311061404", DeviceID: "ec20", Number: "+447700900123", Source: vowifi.PhoneSourcePAssociatedURI, }); err != nil { t.Fatal(err) } projector := StateProjector{ Store: database, Devices: staticDeviceReader{ iccid: "89441000400311061404", imsi: "234159598901845", }, } if err := projector.Save(context.Background(), vowifi.State{ DeviceID: "ec20", Phase: vowifi.PhaseIdle, UpdatedAt: time.Now().UTC(), }); err != nil { t.Fatal(err) } runtime, err := database.VoWiFiRuntime(context.Background(), "ec20") if err != nil { t.Fatal(err) } if runtime.LocalPhone != "+447700900123" || runtime.PhoneNumberSource != vowifi.PhoneSourcePAssociatedURI { t.Fatalf("runtime phone = %q (%q)", runtime.LocalPhone, runtime.PhoneNumberSource) } var tunnel map[string]any if err := json.Unmarshal(runtime.Tunnel, &tunnel); err != nil { t.Fatal(err) } } func TestStateProjectorPreservesConcreteDataplaneMode(t *testing.T) { database := testStore(t) if err := database.UpsertDevice(context.Background(), store.Device{ ID: "ec25", Name: "EC25", }); err != nil { t.Fatal(err) } projector := StateProjector{Store: database} if err := projector.Save(context.Background(), vowifi.State{ DeviceID: "ec25", Phase: vowifi.PhaseIMSReady, TunnelReady: true, IMSReady: true, TunnelName: "vocat-swu-ec25", DataplaneMode: "userspace", UpdatedAt: time.Now().UTC(), }); err != nil { t.Fatal(err) } runtime, err := database.VoWiFiRuntime(context.Background(), "ec25") if err != nil { t.Fatal(err) } if runtime.DataplaneMode != "userspace" { t.Fatalf("dataplane mode = %q, want userspace", runtime.DataplaneMode) } var tunnel map[string]any if err := json.Unmarshal(runtime.Tunnel, &tunnel); err != nil { t.Fatal(err) } if tunnel["dataplane_mode"] != "userspace" { t.Fatalf("tunnel dataplane mode = %#v", tunnel["dataplane_mode"]) } } type staticDeviceReader struct { iccid string imsi string } func (reader staticDeviceReader) Get(string) (device.Device, error) { return device.Device{ Snapshot: &device.Snapshot{ ICCID: reader.iccid, IMSI: reader.imsi, }, }, nil }