This is an automated email from the ASF dual-hosted git repository.

sjanc pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mynewt-newtmgr.git

commit e17ed97035cfb43581afd8c0e6462cbf2470cbdf
Author: Szymon Janc <[email protected]>
AuthorDate: Thu Oct 22 00:20:32 2026 +0200

    newtmgr/config: Add unit tests for connstring and profile handling
    
    Cover the serial, BLE (blehostd), bll and Multitech LoRa connstring
    parsers: defaults, every supported key, values containing '=', bad
    values with the exact error text, unrecognized keys and malformed
    pairs. Exercise BuildBllSesnCfg() including the advertisement filter
    closures with a stub ble.Advertisement, and FillMtechLoraSesnCfg().
    
    For connection profiles, test ConnType string and JSON conversions
    (documenting that an unknown type name silently maps to
    CONN_TYPE_NONE), and ConnProfileMgr loading and saving in a temporary
    home directory: missing file, add/replace/delete round trip, sorted
    listing, missing profile errors, corrupt and unreadable files, and a
    golden test of the on-disk JSON format.
    
    Assisted-by: Claude:claude-opus-5-5
---
 newtmgr/config/ble_config_test.go        | 132 ++++++++++++
 newtmgr/config/bll_config_test.go        | 209 ++++++++++++++++++
 newtmgr/config/connprofile_test.go       | 355 +++++++++++++++++++++++++++++++
 newtmgr/config/mtech_lora_config_test.go | 140 ++++++++++++
 newtmgr/config/serial_config_test.go     | 100 +++++++++
 newtmgr/config/util_test.go              |  55 +++++
 6 files changed, 991 insertions(+)

diff --git a/newtmgr/config/ble_config_test.go 
b/newtmgr/config/ble_config_test.go
new file mode 100644
index 0000000..8b06523
--- /dev/null
+++ b/newtmgr/config/ble_config_test.go
@@ -0,0 +1,132 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package config
+
+import (
+       "testing"
+
+       "mynewt.apache.org/newtmgr/nmxact/bledefs"
+)
+
+func mustParseBleAddr(t *testing.T, s string) bledefs.BleAddr {
+       t.Helper()
+       a, err := bledefs.ParseBleAddr(s)
+       if err != nil {
+               t.Fatalf("ParseBleAddr(%q): %v", s, err)
+       }
+       return a
+}
+
+func TestParseBleConnStringDefaults(t *testing.T) {
+       setTimeout(t, 7.25)
+       setHciIdx(t, 2)
+
+       bc, err := ParseBleConnString("peer_name=dev")
+       if err != nil {
+               t.Fatalf("unexpected error: %v", err)
+       }
+
+       want := BleConfig{
+               PeerAddrType: bledefs.BLE_ADDR_TYPE_PUBLIC,
+               PeerName:     "dev",
+               OwnAddrType:  bledefs.BLE_ADDR_TYPE_RANDOM,
+               ConnTimeout:  7.25,
+               BlehostdPath: "blehostd",
+               HciIdx:       2,
+       }
+       if *bc != want {
+               t.Errorf("got %+v, want %+v", *bc, want)
+       }
+}
+
+func TestParseBleConnStringKeys(t *testing.T) {
+       setHciIdx(t, 0)
+
+       cs := "peer_addr_type=random,peer_addr=01:23:45:67:89:AB," +
+               
"peer_name=a=b,own_addr_type=rpa_pub,own_addr=ff:ee:dd:cc:bb:aa," +
+               "bhd_path=/opt/blehostd,ctlr_path=/dev/ttyUSB1"
+       bc, err := ParseBleConnString(cs)
+       if err != nil {
+               t.Fatalf("unexpected error: %v", err)
+       }
+
+       if bc.PeerAddrType != bledefs.BLE_ADDR_TYPE_RANDOM {
+               t.Errorf("PeerAddrType = %v", bc.PeerAddrType)
+       }
+       if bc.PeerAddr != mustParseBleAddr(t, "01:23:45:67:89:ab") {
+               t.Errorf("PeerAddr = %v", bc.PeerAddr)
+       }
+       if bc.PeerName != "a=b" {
+               t.Errorf("PeerName = %q, want \"a=b\"", bc.PeerName)
+       }
+       if bc.OwnAddrType != bledefs.BLE_ADDR_TYPE_RPA_PUB {
+               t.Errorf("OwnAddrType = %v", bc.OwnAddrType)
+       }
+       if bc.OwnAddr != mustParseBleAddr(t, "ff:ee:dd:cc:bb:aa") {
+               t.Errorf("OwnAddr = %v", bc.OwnAddr)
+       }
+       if bc.BlehostdPath != "/opt/blehostd" {
+               t.Errorf("BlehostdPath = %q", bc.BlehostdPath)
+       }
+       if bc.ControllerPath != "/dev/ttyUSB1" {
+               t.Errorf("ControllerPath = %q", bc.ControllerPath)
+       }
+}
+
+func TestParseBleConnStringErrors(t *testing.T) {
+       tests := []struct {
+               cs  string
+               err string
+       }{
+               {"",
+                       "Invalid BLE connstring; expected comma-separated " +
+                               "key=value pairs; no '=' in: "},
+               {"peer_name=x,bogus",
+                       "Invalid BLE connstring; expected comma-separated " +
+                               "key=value pairs; no '=' in: bogus"},
+               {"peer_addr_type=foo",
+                       "Invalid BLE connstring; Invalid peer_addr_type: foo"},
+               {"own_addr_type=",
+                       "Invalid BLE connstring; Invalid own_addr_type: "},
+               {"peer_addr=01:02",
+                       "Invalid BLE connstring; Invalid peer_addr; " +
+                               "invalid BLE addr string: 01:02"},
+               {"own_addr=1:2:3:4:5",
+                       "Invalid BLE connstring; Invalid own_addr; " +
+                               "invalid BLE addr string: 1:2:3:4:5"},
+               {"foo=bar",
+                       "Invalid BLE connstring; Unrecognized key: foo"},
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.cs, func(t *testing.T) {
+                       bc, err := ParseBleConnString(tt.cs)
+                       if err == nil {
+                               t.Fatalf("expected error, got %+v", bc)
+                       }
+                       if bc != nil {
+                               t.Errorf("expected nil config on error")
+                       }
+                       if err.Error() != tt.err {
+                               t.Errorf("error = %q, want %q", err.Error(), 
tt.err)
+                       }
+               })
+       }
+}
diff --git a/newtmgr/config/bll_config_test.go 
b/newtmgr/config/bll_config_test.go
new file mode 100644
index 0000000..cc6474a
--- /dev/null
+++ b/newtmgr/config/bll_config_test.go
@@ -0,0 +1,209 @@
+//go:build !windows
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package config
+
+import (
+       "testing"
+       "time"
+
+       "github.com/JuulLabs-OSS/ble"
+
+       "mynewt.apache.org/newtmgr/nmxact/bledefs"
+)
+
+type fakeAddr string
+
+func (a fakeAddr) String() string { return string(a) }
+
+// fakeAdv is a stub ble.Advertisement; only the methods used by the
+// bll advertisement filters are implemented.
+type fakeAdv struct {
+       ble.Advertisement
+       name string
+       addr string
+}
+
+func (a *fakeAdv) LocalName() string { return a.name }
+func (a *fakeAdv) Addr() ble.Addr    { return fakeAddr(a.addr) }
+
+func TestParseBllConnString(t *testing.T) {
+       setTimeout(t, 4)
+       setHciIdx(t, 1)
+
+       tests := []struct {
+               name string
+               cs   string
+               want BllConfig
+       }{
+               {"peer_name", "peer_name=mydev",
+                       BllConfig{PeerName: "mydev", ConnTimeout: 4, HciIdx: 
1}},
+               {"all keys",
+                       "ctlr_name=hci0,own_addr_type=random,peer_id=aa:bb," +
+                               "peer_name=x=y,conn_timeout=23.54",
+                       BllConfig{
+                               CtlrName:    "hci0",
+                               OwnAddrType: bledefs.BLE_ADDR_TYPE_RANDOM,
+                               PeerId:      "aa:bb",
+                               PeerName:    "x=y",
+                               ConnTimeout: 23.54,
+                               HciIdx:      1,
+                       }},
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       bc, err := ParseBllConnString(tt.cs)
+                       if err != nil {
+                               t.Fatalf("unexpected error: %v", err)
+                       }
+                       if *bc != tt.want {
+                               t.Errorf("got %+v, want %+v", *bc, tt.want)
+                       }
+               })
+       }
+}
+
+func TestParseBllConnStringErrors(t *testing.T) {
+       tests := []struct {
+               cs  string
+               err string
+       }{
+               {"peer_name",
+                       "Invalid BLE connstring; expected comma-separated " +
+                               "key=value pairs; no '=' in: peer_name"},
+               {"peer_name=a,",
+                       "Invalid BLE connstring; expected comma-separated " +
+                               "key=value pairs; no '=' in: "},
+               {"own_addr_type=bad",
+                       "Invalid BLE connstring; Invalid own_addr_type: bad"},
+               {"conn_timeout=soon",
+                       "Invalid BLE connstring; Invalid conn_timeout: soon"},
+               {"peer_addr=01:02:03:04:05:06",
+                       "Invalid BLE connstring; Unrecognized key: peer_addr"},
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.cs, func(t *testing.T) {
+                       bc, err := ParseBllConnString(tt.cs)
+                       if err == nil {
+                               t.Fatalf("expected error, got %+v", bc)
+                       }
+                       if bc != nil {
+                               t.Errorf("expected nil config on error")
+                       }
+                       if err.Error() != tt.err {
+                               t.Errorf("error = %q, want %q", err.Error(), 
tt.err)
+                       }
+               })
+       }
+}
+
+func TestBuildBllSesnCfg(t *testing.T) {
+       type advCase struct {
+               adv  fakeAdv
+               want bool
+       }
+
+       tests := []struct {
+               name       string
+               devName    string
+               bc         BllConfig
+               advs       []advCase
+               wantPeerNm string
+       }{
+               {
+                       name: "peer_name",
+                       bc:   BllConfig{PeerName: "dev1", PeerId: "11:22", 
ConnTimeout: 1.5},
+                       advs: []advCase{
+                               {fakeAdv{name: "dev1", addr: "00:00"}, true},
+                               {fakeAdv{name: "dev2", addr: "11:22"}, false},
+                               {fakeAdv{name: "", addr: ""}, false},
+                       },
+                       wantPeerNm: "dev1",
+               },
+               {
+                       name: "peer_id",
+                       bc:   BllConfig{PeerId: "11:22", ConnTimeout: 1.5},
+                       advs: []advCase{
+                               {fakeAdv{name: "dev1", addr: "11:22"}, true},
+                               {fakeAdv{name: "11:22", addr: "33:44"}, false},
+                       },
+               },
+               {
+                       name:    "device name overrides",
+                       devName: "global",
+                       bc:      BllConfig{PeerName: "dev1", PeerId: "11:22", 
ConnTimeout: 1.5},
+                       advs: []advCase{
+                               {fakeAdv{name: "global", addr: "00:00"}, true},
+                               {fakeAdv{name: "dev1", addr: "11:22"}, false},
+                       },
+                       wantPeerNm: "global",
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       setDeviceName(t, tt.devName)
+                       setBleWriteRsp(t, true)
+
+                       bc := tt.bc
+                       sc, err := BuildBllSesnCfg(&bc)
+                       if err != nil {
+                               t.Fatalf("unexpected error: %v", err)
+                       }
+                       if bc.PeerName != tt.wantPeerNm {
+                               t.Errorf("PeerName = %q, want %q", bc.PeerName, 
tt.wantPeerNm)
+                       }
+                       if sc.ConnTimeout != 1500*time.Millisecond {
+                               t.Errorf("ConnTimeout = %v, want 1.5s", 
sc.ConnTimeout)
+                       }
+                       if !sc.WriteRsp {
+                               t.Errorf("WriteRsp = false, want true")
+                       }
+                       if sc.PreferredMtu != 512 || sc.ConnTries != 3 {
+                               t.Errorf("defaults not kept: %+v", sc)
+                       }
+                       if sc.AdvFilter == nil {
+                               t.Fatalf("AdvFilter is nil")
+                       }
+                       for i, ac := range tt.advs {
+                               adv := ac.adv
+                               if got := sc.AdvFilter(&adv); got != ac.want {
+                                       t.Errorf("adv %d (%+v): filter = %v, 
want %v",
+                                               i, ac.adv, got, ac.want)
+                               }
+                       }
+               })
+       }
+}
+
+func TestBuildBllSesnCfgNoPeer(t *testing.T) {
+       setDeviceName(t, "")
+
+       _, err := BuildBllSesnCfg(&BllConfig{CtlrName: "hci0"})
+       if err == nil {
+               t.Fatalf("expected error")
+       }
+       if err.Error() != "bll session lacks a peer specifier" {
+               t.Errorf("error = %q", err.Error())
+       }
+}
diff --git a/newtmgr/config/connprofile_test.go 
b/newtmgr/config/connprofile_test.go
new file mode 100644
index 0000000..df76d67
--- /dev/null
+++ b/newtmgr/config/connprofile_test.go
@@ -0,0 +1,355 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package config
+
+import (
+       "encoding/json"
+       "os"
+       "path/filepath"
+       "strings"
+       "testing"
+
+       "github.com/mitchellh/go-homedir"
+
+       "mynewt.apache.org/newtmgr/newtmgr/nmutil"
+)
+
+func TestConnTypeStrings(t *testing.T) {
+       tests := []struct {
+               ct ConnType
+               s  string
+       }{
+               {CONN_TYPE_SERIAL_PLAIN, "serial"},
+               {CONN_TYPE_SERIAL_OIC, "oic_serial"},
+               {CONN_TYPE_BLL_PLAIN, "ble"},
+               {CONN_TYPE_BLL_OIC, "oic_ble"},
+               {CONN_TYPE_BLE_PLAIN, "bhd"},
+               {CONN_TYPE_BLE_OIC, "oic_bhd"},
+               {CONN_TYPE_UDP_PLAIN, "udp"},
+               {CONN_TYPE_UDP_OIC, "oic_udp"},
+               {CONN_TYPE_MTECH_LORA_OIC, "oic_mtech"},
+               {CONN_TYPE_NONE, "???"},
+       }
+
+       for _, tt := range tests {
+               if got := ConnTypeToString(tt.ct); got != tt.s {
+                       t.Errorf("ConnTypeToString(%d) = %q, want %q", tt.ct, 
got, tt.s)
+               }
+               got, err := ConnTypeFromString(tt.s)
+               if err != nil {
+                       t.Errorf("ConnTypeFromString(%q): %v", tt.s, err)
+               } else if got != tt.ct {
+                       t.Errorf("ConnTypeFromString(%q) = %d, want %d", tt.s, 
got, tt.ct)
+               }
+       }
+
+       if len(connTypeNameMap) != len(tests) {
+               t.Errorf("connTypeNameMap has %d entries, test covers %d",
+                       len(connTypeNameMap), len(tests))
+       }
+
+       if s := ConnTypeToString(ConnType(100)); s != "" {
+               t.Errorf("ConnTypeToString(100) = %q, want \"\"", s)
+       }
+}
+
+func TestConnTypeFromStringInvalid(t *testing.T) {
+       for _, s := range []string{"", "SERIAL", "foo"} {
+               ct, err := ConnTypeFromString(s)
+               if err == nil {
+                       t.Errorf("ConnTypeFromString(%q): expected error", s)
+                       continue
+               }
+               if ct != CONN_TYPE_NONE {
+                       t.Errorf("ConnTypeFromString(%q) = %d, want NONE", s, 
ct)
+               }
+               want := "Invalid connection type: " + s
+               if err.Error() != want {
+                       t.Errorf("error = %q, want %q", err.Error(), want)
+               }
+       }
+}
+
+func TestConnTypeJSON(t *testing.T) {
+       ct := CONN_TYPE_SERIAL_OIC
+       b, err := json.Marshal(&ct)
+       if err != nil {
+               t.Fatalf("Marshal: %v", err)
+       }
+       if string(b) != `"oic_serial"` {
+               t.Errorf("Marshal = %s, want \"oic_serial\"", b)
+       }
+
+       var back ConnType
+       if err := json.Unmarshal(b, &back); err != nil {
+               t.Fatalf("Unmarshal: %v", err)
+       }
+       if back != ct {
+               t.Errorf("Unmarshal = %d, want %d", back, ct)
+       }
+
+       // Non-string JSON is rejected.
+       if err := json.Unmarshal([]byte("3"), &back); err == nil {
+               t.Errorf("Unmarshal(3): expected error")
+       }
+}
+
+// An unknown connection type name in the profile file is not an error; it
+// is silently mapped to CONN_TYPE_NONE.  This documents current behaviour.
+func TestConnTypeUnmarshalUnknown(t *testing.T) {
+       ct := CONN_TYPE_UDP_PLAIN
+       if err := json.Unmarshal([]byte(`"carrier_pigeon"`), &ct); err != nil {
+               t.Fatalf("Unmarshal: %v", err)
+       }
+       if ct != CONN_TYPE_NONE {
+               t.Errorf("Unmarshal = %d, want CONN_TYPE_NONE", ct)
+       }
+}
+
+func TestConnProfileString(t *testing.T) {
+       p := &ConnProfile{
+               Name:       "p1",
+               Type:       CONN_TYPE_SERIAL_PLAIN,
+               ConnString: "dev=/dev/ttyUSB0",
+       }
+       want := "name=p1 type=serial connstring=dev=/dev/ttyUSB0"
+       if p.String() != want {
+               t.Errorf("String() = %q, want %q", p.String(), want)
+       }
+}
+
+// fakeHome points the home directory at a fresh temporary directory and
+// returns the path of the connection profile file within it.
+func fakeHome(t *testing.T) string {
+       t.Helper()
+
+       dir := t.TempDir()
+       t.Setenv("HOME", dir)
+       t.Setenv("USERPROFILE", dir)
+       homedir.Reset()
+       t.Cleanup(homedir.Reset)
+
+       old := nmutil.ToolInfo
+       nmutil.ToolInfo.CfgFilename = ".newtmgr.cp.json"
+       t.Cleanup(func() { nmutil.ToolInfo = old })
+
+       return filepath.Join(dir, ".newtmgr.cp.json")
+}
+
+func TestConnProfileMgrMissingFile(t *testing.T) {
+       path := fakeHome(t)
+
+       cpm, err := NewConnProfileMgr()
+       if err != nil {
+               t.Fatalf("NewConnProfileMgr: %v", err)
+       }
+       list, err := cpm.GetConnProfileList()
+       if err != nil {
+               t.Fatalf("GetConnProfileList: %v", err)
+       }
+       if len(list) != 0 {
+               t.Errorf("expected no profiles, got %d", len(list))
+       }
+       if _, err := os.Stat(path); !os.IsNotExist(err) {
+               t.Errorf("profile file should not be created on load: %v", err)
+       }
+}
+
+func TestConnProfileMgrRoundTrip(t *testing.T) {
+       fakeHome(t)
+
+       cpm, err := NewConnProfileMgr()
+       if err != nil {
+               t.Fatalf("NewConnProfileMgr: %v", err)
+       }
+
+       profs := []*ConnProfile{
+               {Name: "zeta", Type: CONN_TYPE_UDP_PLAIN, ConnString: 
"127.0.0.1:5683"},
+               {Name: "alpha", Type: CONN_TYPE_SERIAL_PLAIN,
+                       ConnString: "dev=/dev/ttyUSB0,baud=115200"},
+               {Name: "mid", Type: CONN_TYPE_BLL_OIC, ConnString: 
"peer_name=a=b"},
+       }
+       for _, p := range profs {
+               if err := cpm.AddConnProfile(p); err != nil {
+                       t.Fatalf("AddConnProfile(%s): %v", p.Name, err)
+               }
+       }
+
+       cpm2, err := NewConnProfileMgr()
+       if err != nil {
+               t.Fatalf("reload: %v", err)
+       }
+       list, _ := cpm2.GetConnProfileList()
+       names := []string{}
+       for _, p := range list {
+               names = append(names, p.Name)
+       }
+       if strings.Join(names, ",") != "alpha,mid,zeta" {
+               t.Errorf("profile list = %v, want sorted alpha,mid,zeta", names)
+       }
+
+       for _, want := range profs {
+               got, err := cpm2.GetConnProfile(want.Name)
+               if err != nil {
+                       t.Fatalf("GetConnProfile(%s): %v", want.Name, err)
+               }
+               if *got != *want {
+                       t.Errorf("profile %s = %+v, want %+v", want.Name, *got, 
*want)
+               }
+       }
+
+       // Replace an existing profile.
+       repl := &ConnProfile{Name: "mid", Type: CONN_TYPE_SERIAL_OIC,
+               ConnString: "COM1"}
+       if err := cpm2.AddConnProfile(repl); err != nil {
+               t.Fatalf("AddConnProfile(replace): %v", err)
+       }
+
+       // Delete a profile and reload.
+       if err := cpm2.DeleteConnProfile("zeta"); err != nil {
+               t.Fatalf("DeleteConnProfile: %v", err)
+       }
+
+       cpm3, err := NewConnProfileMgr()
+       if err != nil {
+               t.Fatalf("reload: %v", err)
+       }
+       list, _ = cpm3.GetConnProfileList()
+       if len(list) != 2 || list[0].Name != "alpha" || list[1].Name != "mid" {
+               t.Fatalf("unexpected profiles after delete: %v", list)
+       }
+       if *list[1] != *repl {
+               t.Errorf("replaced profile = %+v, want %+v", *list[1], *repl)
+       }
+       if _, err := cpm3.GetConnProfile("zeta"); err == nil {
+               t.Errorf("deleted profile still present")
+       }
+}
+
+func TestConnProfileMgrMissingProfile(t *testing.T) {
+       fakeHome(t)
+
+       cpm, err := NewConnProfileMgr()
+       if err != nil {
+               t.Fatalf("NewConnProfileMgr: %v", err)
+       }
+
+       _, err = cpm.GetConnProfile("nope")
+       if err == nil || err.Error() != `connection profile "nope" doesn't 
exist` {
+               t.Errorf("GetConnProfile error = %v", err)
+       }
+
+       err = cpm.DeleteConnProfile("nope")
+       if err == nil || err.Error() != `connection profile "nope" doesn't 
exist` {
+               t.Errorf("DeleteConnProfile error = %v", err)
+       }
+}
+
+func TestConnProfileMgrCorruptFile(t *testing.T) {
+       path := fakeHome(t)
+
+       if err := os.WriteFile(path, []byte("{not json"), 0644); err != nil {
+               t.Fatal(err)
+       }
+
+       cpm, err := NewConnProfileMgr()
+       if err == nil {
+               t.Fatalf("expected error, got %+v", cpm)
+       }
+       prefix := "error reading connection profile config (" + path + "): "
+       if !strings.HasPrefix(err.Error(), prefix) {
+               t.Errorf("error = %q, want prefix %q", err.Error(), prefix)
+       }
+}
+
+func TestConnProfileMgrUnreadableFile(t *testing.T) {
+       path := fakeHome(t)
+
+       // A directory in place of the file cannot be read.
+       if err := os.Mkdir(path, 0755); err != nil {
+               t.Fatal(err)
+       }
+
+       if _, err := NewConnProfileMgr(); err == nil {
+               t.Fatalf("expected error")
+       }
+}
+
+const goldenProfiles = `[
+    {
+        "MyName": "a",
+        "MyType": "oic_serial",
+        "MyConnString": "dev=/dev/ttyACM0"
+    },
+    {
+        "MyName": "b",
+        "MyType": "oic_mtech",
+        "MyConnString": ""
+    }
+]`
+
+func TestConnProfileMgrFileFormat(t *testing.T) {
+       path := fakeHome(t)
+
+       cpm, err := NewConnProfileMgr()
+       if err != nil {
+               t.Fatalf("NewConnProfileMgr: %v", err)
+       }
+       if err := cpm.AddConnProfile(&ConnProfile{Name: "b",
+               Type: CONN_TYPE_MTECH_LORA_OIC}); err != nil {
+               t.Fatalf("AddConnProfile: %v", err)
+       }
+       if err := cpm.AddConnProfile(&ConnProfile{Name: "a",
+               Type: CONN_TYPE_SERIAL_OIC, ConnString: "dev=/dev/ttyACM0"}); 
err != nil {
+               t.Fatalf("AddConnProfile: %v", err)
+       }
+
+       b, err := os.ReadFile(path)
+       if err != nil {
+               t.Fatal(err)
+       }
+       if string(b) != goldenProfiles {
+               t.Errorf("file contents:\n%s\nwant:\n%s", b, goldenProfiles)
+       }
+
+       // The golden file must also load back.
+       if err := os.WriteFile(path, []byte(goldenProfiles), 0644); err != nil {
+               t.Fatal(err)
+       }
+       cpm2, err := NewConnProfileMgr()
+       if err != nil {
+               t.Fatalf("reload: %v", err)
+       }
+       p, err := cpm2.GetConnProfile("b")
+       if err != nil || p.Type != CONN_TYPE_MTECH_LORA_OIC {
+               t.Errorf("GetConnProfile(b) = %+v, %v", p, err)
+       }
+}
+
+func TestSortConnProfs(t *testing.T) {
+       in := []*ConnProfile{{Name: "c"}, {Name: "a"}, {Name: "b"}}
+       out := SortConnProfs(in)
+       if out[0].Name != "a" || out[1].Name != "b" || out[2].Name != "c" {
+               t.Errorf("unexpected order: %v", out)
+       }
+       if in[0].Name != "c" {
+               t.Errorf("input slice was modified")
+       }
+}
diff --git a/newtmgr/config/mtech_lora_config_test.go 
b/newtmgr/config/mtech_lora_config_test.go
new file mode 100644
index 0000000..a003860
--- /dev/null
+++ b/newtmgr/config/mtech_lora_config_test.go
@@ -0,0 +1,140 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package config
+
+import (
+       "testing"
+
+       "mynewt.apache.org/newtmgr/nmxact/lora"
+       "mynewt.apache.org/newtmgr/nmxact/mtech_lora"
+       "mynewt.apache.org/newtmgr/nmxact/sesn"
+)
+
+func TestParseMtechLoraConnString(t *testing.T) {
+       def := mtech_lora.LoraConfig{Port: lora.COAP_LORA_PORT}
+
+       tests := []struct {
+               name string
+               cs   string
+               want mtech_lora.LoraConfig
+       }{
+               {"empty", "", def},
+               {"addr", "addr=01:02:03:04:05:06:07:08",
+                       mtech_lora.LoraConfig{
+                               Addr: "01:02:03:04:05:06:07:08",
+                               Port: lora.COAP_LORA_PORT,
+                       }},
+               {"all keys", "addr=a=b,segsz=64,confirmedtx=true,port=10",
+                       mtech_lora.LoraConfig{
+                               Addr:        "a=b",
+                               SegSz:       64,
+                               ConfirmedTx: true,
+                               Port:        10,
+                       }},
+               {"confirmedtx numeric", "confirmedtx=1",
+                       mtech_lora.LoraConfig{
+                               ConfirmedTx: true,
+                               Port:        lora.COAP_LORA_PORT,
+                       }},
+               {"port bounds", "port=255",
+                       mtech_lora.LoraConfig{Port: 255}},
+               {"port zero", "port=0",
+                       mtech_lora.LoraConfig{Port: 0}},
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       mc, err := ParseMtechLoraConnString(tt.cs)
+                       if err != nil {
+                               t.Fatalf("unexpected error: %v", err)
+                       }
+                       if *mc != tt.want {
+                               t.Errorf("got %+v, want %+v", *mc, tt.want)
+                       }
+               })
+       }
+}
+
+func TestParseMtechLoraConnStringErrors(t *testing.T) {
+       tests := []struct {
+               cs  string
+               err string
+       }{
+               {"addr",
+                       "expected comma-separated key=value pairs; no '=' in: 
addr"},
+               {"addr=x,",
+                       "expected comma-separated key=value pairs; no '=' in: 
"},
+               {"segsz=big", "Invalid SegSz: big"},
+               {"confirmedtx=maybe", "Invalid confirmedtx: maybe"},
+               {"port=256", "Invalid port number: 256"},
+               {"port=-1", "Invalid port number: -1"},
+               {"port=x", "Invalid port number: x"},
+               {"foo=1", "Unrecognized key: foo"},
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.cs, func(t *testing.T) {
+                       _, err := ParseMtechLoraConnString(tt.cs)
+                       if err == nil {
+                               t.Fatalf("expected error")
+                       }
+                       if err.Error() != tt.err {
+                               t.Errorf("error = %q, want %q", err.Error(), 
tt.err)
+                       }
+               })
+       }
+}
+
+func TestFillMtechLoraSesnCfg(t *testing.T) {
+       mc := &mtech_lora.LoraConfig{
+               Addr:        "aa",
+               SegSz:       32,
+               ConfirmedTx: true,
+               Port:        7,
+       }
+
+       t.Run("from config", func(t *testing.T) {
+               setDeviceName(t, "")
+               var sc sesn.SesnCfg
+               if err := FillMtechLoraSesnCfg(mc, &sc); err != nil {
+                       t.Fatalf("unexpected error: %v", err)
+               }
+               want := sesn.SesnCfgLora{
+                       Addr: "aa", SegSz: 32, ConfirmedTx: true, Port: 7,
+               }
+               if sc.Lora != want {
+                       t.Errorf("got %+v, want %+v", sc.Lora, want)
+               }
+       })
+
+       t.Run("device name overrides addr", func(t *testing.T) {
+               setDeviceName(t, "bb")
+               var sc sesn.SesnCfg
+               if err := FillMtechLoraSesnCfg(mc, &sc); err != nil {
+                       t.Fatalf("unexpected error: %v", err)
+               }
+               if sc.Lora.Addr != "bb" {
+                       t.Errorf("Addr = %q, want \"bb\"", sc.Lora.Addr)
+               }
+               if mc.Addr != "aa" {
+                       t.Errorf("config modified: Addr = %q", mc.Addr)
+               }
+       })
+}
diff --git a/newtmgr/config/serial_config_test.go 
b/newtmgr/config/serial_config_test.go
new file mode 100644
index 0000000..ff713be
--- /dev/null
+++ b/newtmgr/config/serial_config_test.go
@@ -0,0 +1,100 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package config
+
+import (
+       "testing"
+       "time"
+)
+
+func TestParseSerialConnString(t *testing.T) {
+       setTimeout(t, 3.5)
+
+       tests := []struct {
+               name string
+               cs   string
+               dev  string
+               baud int
+               mtu  int
+       }{
+               // Defaults: baud 115200 (set by the parser), mtu 128 (from
+               // nmserial.NewXportCfg).
+               {"empty", "", "", 115200, 128},
+               {"old style dev only", "/dev/ttyUSB0", "/dev/ttyUSB0", 115200, 
128},
+               {"dev key", "dev=/dev/ttyACM0", "/dev/ttyACM0", 115200, 128},
+               {"windows dev", "COM3", "COM3", 115200, 128},
+               {"all keys", "dev=/dev/x,baud=9600,mtu=256", "/dev/x", 9600, 
256},
+               {"order independent", "mtu=128,baud=1000000,dev=/dev/y",
+                       "/dev/y", 1000000, 128},
+               {"value with equals", "dev=/dev/a=b", "/dev/a=b", 115200, 128},
+               {"last key wins", "baud=1,baud=2", "", 2, 128},
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       sc, err := ParseSerialConnString(tt.cs)
+                       if err != nil {
+                               t.Fatalf("unexpected error: %v", err)
+                       }
+                       if sc.DevPath != tt.dev {
+                               t.Errorf("DevPath = %q, want %q", sc.DevPath, 
tt.dev)
+                       }
+                       if sc.Baud != tt.baud {
+                               t.Errorf("Baud = %d, want %d", sc.Baud, tt.baud)
+                       }
+                       if sc.Mtu != tt.mtu {
+                               t.Errorf("Mtu = %d, want %d", sc.Mtu, tt.mtu)
+                       }
+                       if sc.ReadTimeout != 3500*time.Millisecond {
+                               t.Errorf("ReadTimeout = %v, want 3.5s", 
sc.ReadTimeout)
+                       }
+               })
+       }
+}
+
+func TestParseSerialConnStringErrors(t *testing.T) {
+       tests := []struct {
+               cs  string
+               err string
+       }{
+               {"baud=fast",
+                       "Invalid serial connstring; Invalid baud: fast"},
+               {"baud=",
+                       "Invalid serial connstring; Invalid baud: "},
+               {"mtu=big",
+                       "Invalid serial connstring; Invalid mtu: big"},
+               {"dev=/dev/x,speed=9600",
+                       "Invalid serial connstring; Unrecognized key: speed"},
+               {"=foo",
+                       "Invalid serial connstring; Unrecognized key: "},
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.cs, func(t *testing.T) {
+                       _, err := ParseSerialConnString(tt.cs)
+                       if err == nil {
+                               t.Fatalf("expected error")
+                       }
+                       if err.Error() != tt.err {
+                               t.Errorf("error = %q, want %q", err.Error(), 
tt.err)
+                       }
+               })
+       }
+}
diff --git a/newtmgr/config/util_test.go b/newtmgr/config/util_test.go
new file mode 100644
index 0000000..4a0fe94
--- /dev/null
+++ b/newtmgr/config/util_test.go
@@ -0,0 +1,55 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package config
+
+import (
+       "testing"
+
+       "mynewt.apache.org/newtmgr/newtmgr/nmutil"
+)
+
+// setTimeout overrides nmutil.Timeout for the duration of a test.
+func setTimeout(t *testing.T, secs float64) {
+       t.Helper()
+       old := nmutil.Timeout
+       nmutil.Timeout = secs
+       t.Cleanup(func() { nmutil.Timeout = old })
+}
+
+func setHciIdx(t *testing.T, idx int) {
+       t.Helper()
+       old := nmutil.HciIdx
+       nmutil.HciIdx = idx
+       t.Cleanup(func() { nmutil.HciIdx = old })
+}
+
+func setDeviceName(t *testing.T, name string) {
+       t.Helper()
+       old := nmutil.DeviceName
+       nmutil.DeviceName = name
+       t.Cleanup(func() { nmutil.DeviceName = old })
+}
+
+func setBleWriteRsp(t *testing.T, v bool) {
+       t.Helper()
+       old := nmutil.BleWriteRsp
+       nmutil.BleWriteRsp = v
+       t.Cleanup(func() { nmutil.BleWriteRsp = old })
+}

Reply via email to