From fd939c58446fab70a91d226bfa6dbb28f149f114 Mon Sep 17 00:00:00 2001 From: Marta Carbone Date: Thu, 6 Nov 2025 09:31:52 +0100 Subject: [PATCH 1/9] Add the server version to arduino-app-cli version. --- cmd/arduino-app-cli/version/version.go | 116 ++++++++++++++++++-- cmd/arduino-app-cli/version/version_test.go | 100 +++++++++++++++++ 2 files changed, 207 insertions(+), 9 deletions(-) create mode 100644 cmd/arduino-app-cli/version/version_test.go diff --git a/cmd/arduino-app-cli/version/version.go b/cmd/arduino-app-cli/version/version.go index 86ed7b3c..384c2e29 100644 --- a/cmd/arduino-app-cli/version/version.go +++ b/cmd/arduino-app-cli/version/version.go @@ -16,34 +16,132 @@ package version import ( + "encoding/json" "fmt" + "io" + "net" + "net/http" + "net/url" + "time" "github.com/spf13/cobra" "github.com/arduino/arduino-app-cli/cmd/feedback" + "github.com/arduino/arduino-app-cli/cmd/i18n" ) -func NewVersionCmd(version string) *cobra.Command { +// The actual listening address for the daemon +// is defined in the installation package +const ( + DefaultHostname = "localhost" + DefaultPort = "8800" +) + +func NewVersionCmd(clientVersion string) *cobra.Command { cmd := &cobra.Command{ Use: "version", - Short: "Print the version number of Arduino App CLI", + Short: "Print the client and server version numbers for the Arduino App CLI.", Run: func(cmd *cobra.Command, args []string) { - feedback.PrintResult(versionResult{ - AppName: "Arduino App CLI", - Version: version, - }) + host, _ := cmd.Flags().GetString("host") + + versionHandler(clientVersion, host) }, } + cmd.Flags().String("host", fmt.Sprintf("%s:%s", DefaultHostname, DefaultPort), + "The daemon network address [host]:[port]") return cmd } -type versionResult struct { - AppName string `json:"appName"` +func versionHandler(clientVersion string, host string) { + httpClient := http.Client{ + Timeout: time.Second, + } + result := doVersionHandler(httpClient, clientVersion, host) + feedback.PrintResult(result) +} + +func doVersionHandler(httpClient http.Client, clientVersion string, host string) versionResult { + url, err := getValidOrDefaultUrl(host) + if err != nil { + feedback.Fatal(i18n.Tr("Error: invalid host:port format"), feedback.ErrBadArgument) + } + + serverVersion, err := getServerVersion(httpClient, url) + if err != nil { + serverVersion = fmt.Sprintf("n/a (cannot connect to the server %s://%s)", url.Scheme, url.Host) + } + + return versionResult{ + ClientVersion: clientVersion, + ServerVersion: serverVersion, + } +} + +func getValidOrDefaultUrl(hostPort string) (url.URL, error) { + host := DefaultHostname + port := DefaultPort + + if hostPort != "" { + h, p, err := net.SplitHostPort(hostPort) + if err != nil { + return url.URL{}, err + } + if h != "" { + host = h + } + if p != "" { + port = p + } + + } + + hostAndPort := net.JoinHostPort(host, port) + + u := url.URL{ + Scheme: "http", + Host: hostAndPort, + Path: "/v1/version", + } + + return u, nil +} + +func getServerVersion(httpClient http.Client, url url.URL) (string, error) { + resp, err := httpClient.Get(url.String()) + if err != nil { + return "", err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("request failed with status %d", resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + + var serverResponse serverVersionResponse + if err := json.Unmarshal(body, &serverResponse); err != nil { + return "", err + } + + return serverResponse.Version, nil +} + +type serverVersionResponse struct { Version string `json:"version"` } +type versionResult struct { + ClientVersion string `json:"version"` + ServerVersion string `json:"serverVersion"` +} + func (r versionResult) String() string { - return fmt.Sprintf("%s v%s", r.AppName, r.Version) + return fmt.Sprintf("client: %s\nserver: %s", + r.ClientVersion, r.ServerVersion) } func (r versionResult) Data() interface{} { diff --git a/cmd/arduino-app-cli/version/version_test.go b/cmd/arduino-app-cli/version/version_test.go new file mode 100644 index 00000000..d0db5bef --- /dev/null +++ b/cmd/arduino-app-cli/version/version_test.go @@ -0,0 +1,100 @@ +package version + +import ( + "errors" + "fmt" + "io" + "net/http" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestServerVersion(t *testing.T) { + clientVersion := "5.1-dev" + + testCases := []struct { + name string + serverStub Tripper + expectedResult versionResult + host string + }{ + { + name: "return the server version when the server is up", + serverStub: successServer, + expectedResult: versionResult{ + ClientVersion: "5.1-dev", + ServerVersion: "3.0", + }, + host: "", + }, + { + name: "return error if default server is not listening", + serverStub: failureServer, + expectedResult: versionResult{ + ClientVersion: "5.1-dev", + ServerVersion: fmt.Sprintf("n/a (cannot connect to the server http://%s:%s)", DefaultHostname, DefaultPort), + }, + host: "", + }, + { + name: "return error if provided server is not listening", + serverStub: failureServer, + expectedResult: versionResult{ + ClientVersion: "5.1-dev", + ServerVersion: "n/a (cannot connect to the server http://unreacheable:123)", + }, + host: "unreacheable:123", + }, + { + name: "return error for server resopnse 500 Internal Server Error", + serverStub: failureInternalServerError, + expectedResult: versionResult{ + ClientVersion: "5.1-dev", + ServerVersion: "n/a (cannot connect to the server http://unreacheable:123)", + }, + host: "unreacheable:123", + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // arrange + httpClient := http.Client{} + httpClient.Transport = tc.serverStub + + // act + result := doVersionHandler(httpClient, clientVersion, tc.host) + + // assert + require.Equal(t, tc.expectedResult, result) + }) + } +} + +// Leverage the http.Client's RoundTripper +// to return a canned response and bypass network calls. +type Tripper func(*http.Request) (*http.Response, error) + +func (t Tripper) RoundTrip(request *http.Request) (*http.Response, error) { + return t(request) +} + +var successServer = Tripper(func(*http.Request) (*http.Response, error) { + body := io.NopCloser(strings.NewReader(`{"version":"3.0"}`)) + return &http.Response{ + StatusCode: http.StatusOK, + Body: body, + }, nil +}) + +var failureServer = Tripper(func(*http.Request) (*http.Response, error) { + return nil, errors.New("connetion refused") +}) + +var failureInternalServerError = Tripper(func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusInternalServerError, + Body: io.NopCloser(strings.NewReader("")), + }, nil +}) From 466245c5f1b94f541675e25f2d89b4b3c872fb13 Mon Sep 17 00:00:00 2001 From: Marta Carbone Date: Thu, 6 Nov 2025 08:40:49 +0100 Subject: [PATCH 2/9] Add copyright header. --- cmd/arduino-app-cli/version/version_test.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/cmd/arduino-app-cli/version/version_test.go b/cmd/arduino-app-cli/version/version_test.go index d0db5bef..0db21fdf 100644 --- a/cmd/arduino-app-cli/version/version_test.go +++ b/cmd/arduino-app-cli/version/version_test.go @@ -1,3 +1,18 @@ +// This file is part of arduino-app-cli. +// +// Copyright 2025 ARDUINO SA (http://www.arduino.cc/) +// +// This software is released under the GNU General Public License version 3, +// which covers the main part of arduino-app-cli. +// The terms of this license can be found at: +// https://www.gnu.org/licenses/gpl-3.0.en.html +// +// You can be released from the requirements of the above licenses by purchasing +// a commercial license. Buying such a license is mandatory if you want to +// modify or otherwise use the software for commercial activities involving the +// Arduino software without disclosing the source code of your own applications. +// To purchase a commercial license, send an email to license@arduino.cc. + package version import ( From bf435c6e3d8450ac05343f520b465e5c2ae7f20e Mon Sep 17 00:00:00 2001 From: Marta Carbone Date: Fri, 7 Nov 2025 18:19:16 +0100 Subject: [PATCH 3/9] Address review comments. --- cmd/arduino-app-cli/version/version.go | 125 ++++++++++---------- cmd/arduino-app-cli/version/version_test.go | 81 ++++++++++--- 2 files changed, 128 insertions(+), 78 deletions(-) diff --git a/cmd/arduino-app-cli/version/version.go b/cmd/arduino-app-cli/version/version.go index 384c2e29..5619ebca 100644 --- a/cmd/arduino-app-cli/version/version.go +++ b/cmd/arduino-app-cli/version/version.go @@ -18,16 +18,15 @@ package version import ( "encoding/json" "fmt" - "io" "net" "net/http" "net/url" + "strings" "time" "github.com/spf13/cobra" "github.com/arduino/arduino-app-cli/cmd/feedback" - "github.com/arduino/arduino-app-cli/cmd/i18n" ) // The actual listening address for the daemon @@ -35,6 +34,7 @@ import ( const ( DefaultHostname = "localhost" DefaultPort = "8800" + ProgramName = "Arduino App CLI" ) func NewVersionCmd(clientVersion string) *cobra.Command { @@ -44,7 +44,20 @@ func NewVersionCmd(clientVersion string) *cobra.Command { Run: func(cmd *cobra.Command, args []string) { host, _ := cmd.Flags().GetString("host") - versionHandler(clientVersion, host) + validatedHostAndPort, err := validateHost(host) + if err != nil { + feedback.Fatal("Error: invalid host:port format", feedback.ErrBadArgument) + } + + httpClient := http.Client{ + Timeout: time.Second, + } + + result, err := versionHandler(httpClient, clientVersion, validatedHostAndPort) + if err != nil { + feedback.Warnf("Waning: " + err.Error() + "\n") + } + feedback.PrintResult(result) }, } cmd.Flags().String("host", fmt.Sprintf("%s:%s", DefaultHostname, DefaultPort), @@ -52,96 +65,82 @@ func NewVersionCmd(clientVersion string) *cobra.Command { return cmd } -func versionHandler(clientVersion string, host string) { - httpClient := http.Client{ - Timeout: time.Second, +func versionHandler(httpClient http.Client, clientVersion string, hostAndPort string) (versionResult, error) { + url := url.URL{ + Scheme: "http", + Host: hostAndPort, + Path: "/v1/version", } - result := doVersionHandler(httpClient, clientVersion, host) - feedback.PrintResult(result) -} -func doVersionHandler(httpClient http.Client, clientVersion string, host string) versionResult { - url, err := getValidOrDefaultUrl(host) - if err != nil { - feedback.Fatal(i18n.Tr("Error: invalid host:port format"), feedback.ErrBadArgument) - } + daemonVersion := getServerVersion(httpClient, url.String()) - serverVersion, err := getServerVersion(httpClient, url) - if err != nil { - serverVersion = fmt.Sprintf("n/a (cannot connect to the server %s://%s)", url.Scheme, url.Host) + result := versionResult{ + Name: ProgramName, + ClientVersion: clientVersion, + DaemonVersion: daemonVersion, } - return versionResult{ - ClientVersion: clientVersion, - ServerVersion: serverVersion, + if daemonVersion == "" { + return result, fmt.Errorf("cannot connect to %s", hostAndPort) } + return result, nil } -func getValidOrDefaultUrl(hostPort string) (url.URL, error) { - host := DefaultHostname - port := DefaultPort - - if hostPort != "" { - h, p, err := net.SplitHostPort(hostPort) - if err != nil { - return url.URL{}, err - } - if h != "" { - host = h - } - if p != "" { - port = p - } - +func validateHost(hostPort string) (string, error) { + if !strings.Contains(hostPort, ":") { + hostPort = hostPort + ":" } - hostAndPort := net.JoinHostPort(host, port) - - u := url.URL{ - Scheme: "http", - Host: hostAndPort, - Path: "/v1/version", + h, p, err := net.SplitHostPort(hostPort) + if err != nil { + return "", err + } + if h == "" { + h = DefaultHostname + } + if p == "" { + p = DefaultPort } - return u, nil + return net.JoinHostPort(h, p), nil } -func getServerVersion(httpClient http.Client, url url.URL) (string, error) { - resp, err := httpClient.Get(url.String()) +func getServerVersion(httpClient http.Client, url string) string { + resp, err := httpClient.Get(url) if err != nil { - return "", err + return "" } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("request failed with status %d", resp.StatusCode) + return "" } - body, err := io.ReadAll(resp.Body) - if err != nil { - return "", err + var serverResponse struct { + Version string `json:"version"` } - - var serverResponse serverVersionResponse - if err := json.Unmarshal(body, &serverResponse); err != nil { - return "", err + if err := json.NewDecoder(resp.Body).Decode(&serverResponse); err != nil { + return "" } - return serverResponse.Version, nil -} - -type serverVersionResponse struct { - Version string `json:"version"` + return serverResponse.Version } type versionResult struct { - ClientVersion string `json:"version"` - ServerVersion string `json:"serverVersion"` + Name string `json:"name"` + ClientVersion string `json:"client_version"` + DaemonVersion string `json:"daemon_version,omitempty"` } func (r versionResult) String() string { - return fmt.Sprintf("client: %s\nserver: %s", - r.ClientVersion, r.ServerVersion) + serverMessage := fmt.Sprintf("%s client version %s", + ProgramName, r.ClientVersion) + + if r.DaemonVersion != "" { + serverMessage = fmt.Sprintf("%s\ndaemon version: %s", + serverMessage, r.DaemonVersion) + } + return serverMessage } func (r versionResult) Data() interface{} { diff --git a/cmd/arduino-app-cli/version/version_test.go b/cmd/arduino-app-cli/version/version_test.go index 0db21fdf..cd796e54 100644 --- a/cmd/arduino-app-cli/version/version_test.go +++ b/cmd/arduino-app-cli/version/version_test.go @@ -17,7 +17,6 @@ package version import ( "errors" - "fmt" "io" "net/http" "strings" @@ -26,50 +25,102 @@ import ( "github.com/stretchr/testify/require" ) +func TestGetValidUrl(t *testing.T) { + testCases := []struct { + name string + hostPort string + expectedResult string + }{ + { + name: "Valid host and port should return default.", + hostPort: "localhost:8800", + expectedResult: "localhost:8800", + }, + { + name: "Missing host should return default host.", + hostPort: ":8800", + expectedResult: "localhost:8800", + }, + { + name: "Missing port should return default port.", + hostPort: "localhost:", + expectedResult: "localhost:8800", + }, + { + name: "Custom host and port should return the default.", + hostPort: "192.168.100.1:1234", + expectedResult: "192.168.100.1:1234", + }, + { + name: "Host only should return provided input and default port.", + hostPort: "192.168.1.1", + expectedResult: "192.168.1.1:8800", + }, + { + name: "Missing host and port should return default.", + hostPort: "", + expectedResult: "localhost:8800", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + url, _ := validateHost(tc.hostPort) + require.Equal(t, tc.expectedResult, url) + }) + } +} + func TestServerVersion(t *testing.T) { clientVersion := "5.1-dev" + unreacheableUrl := "unreacheable:123" + daemonVersion := "" testCases := []struct { name string serverStub Tripper expectedResult versionResult - host string + hostAndPort string }{ { name: "return the server version when the server is up", serverStub: successServer, expectedResult: versionResult{ - ClientVersion: "5.1-dev", - ServerVersion: "3.0", + Name: ProgramName, + ClientVersion: clientVersion, + DaemonVersion: "3.0", }, - host: "", + hostAndPort: "localhost:8800", }, { name: "return error if default server is not listening", serverStub: failureServer, expectedResult: versionResult{ - ClientVersion: "5.1-dev", - ServerVersion: fmt.Sprintf("n/a (cannot connect to the server http://%s:%s)", DefaultHostname, DefaultPort), + Name: ProgramName, + ClientVersion: clientVersion, + DaemonVersion: daemonVersion, }, - host: "", + hostAndPort: unreacheableUrl, }, { name: "return error if provided server is not listening", serverStub: failureServer, expectedResult: versionResult{ - ClientVersion: "5.1-dev", - ServerVersion: "n/a (cannot connect to the server http://unreacheable:123)", + Name: ProgramName, + ClientVersion: clientVersion, + DaemonVersion: daemonVersion, }, - host: "unreacheable:123", + hostAndPort: unreacheableUrl, }, { name: "return error for server resopnse 500 Internal Server Error", serverStub: failureInternalServerError, expectedResult: versionResult{ - ClientVersion: "5.1-dev", - ServerVersion: "n/a (cannot connect to the server http://unreacheable:123)", + Name: ProgramName, + ClientVersion: clientVersion, + DaemonVersion: daemonVersion, }, - host: "unreacheable:123", + hostAndPort: unreacheableUrl, }, } for _, tc := range testCases { @@ -79,7 +130,7 @@ func TestServerVersion(t *testing.T) { httpClient.Transport = tc.serverStub // act - result := doVersionHandler(httpClient, clientVersion, tc.host) + result, _ := versionHandler(httpClient, clientVersion, tc.hostAndPort) // assert require.Equal(t, tc.expectedResult, result) From a5d95eaf634b7d2356b858c36de1f4ca19d0388c Mon Sep 17 00:00:00 2001 From: Marta Carbone Date: Mon, 10 Nov 2025 08:15:49 +0100 Subject: [PATCH 4/9] Fix lint. --- cmd/arduino-app-cli/version/version.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/arduino-app-cli/version/version.go b/cmd/arduino-app-cli/version/version.go index 5619ebca..290db605 100644 --- a/cmd/arduino-app-cli/version/version.go +++ b/cmd/arduino-app-cli/version/version.go @@ -88,7 +88,7 @@ func versionHandler(httpClient http.Client, clientVersion string, hostAndPort st func validateHost(hostPort string) (string, error) { if !strings.Contains(hostPort, ":") { - hostPort = hostPort + ":" + hostPort += ":" } h, p, err := net.SplitHostPort(hostPort) From 776bccab2392c7141a0ed4026ea4a68b3a788ac6 Mon Sep 17 00:00:00 2001 From: Marta Carbone Date: Mon, 10 Nov 2025 15:22:15 +0100 Subject: [PATCH 5/9] Address review --- cmd/arduino-app-cli/version/version.go | 36 ++++++++++----------- cmd/arduino-app-cli/version/version_test.go | 10 +++--- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/cmd/arduino-app-cli/version/version.go b/cmd/arduino-app-cli/version/version.go index 290db605..a656b851 100644 --- a/cmd/arduino-app-cli/version/version.go +++ b/cmd/arduino-app-cli/version/version.go @@ -40,7 +40,7 @@ const ( func NewVersionCmd(clientVersion string) *cobra.Command { cmd := &cobra.Command{ Use: "version", - Short: "Print the client and server version numbers for the Arduino App CLI.", + Short: "Print the client and server versions for the Arduino App CLI.", Run: func(cmd *cobra.Command, args []string) { host, _ := cmd.Flags().GetString("host") @@ -72,16 +72,16 @@ func versionHandler(httpClient http.Client, clientVersion string, hostAndPort st Path: "/v1/version", } - daemonVersion := getServerVersion(httpClient, url.String()) + daemonVersion, err := getDaemonVersion(httpClient, url.String()) result := versionResult{ Name: ProgramName, - ClientVersion: clientVersion, + Version: clientVersion, DaemonVersion: daemonVersion, } - if daemonVersion == "" { - return result, fmt.Errorf("cannot connect to %s", hostAndPort) + if err != nil { + return result, fmt.Errorf("error getting daemon version %s", hostAndPort) } return result, nil } @@ -105,42 +105,42 @@ func validateHost(hostPort string) (string, error) { return net.JoinHostPort(h, p), nil } -func getServerVersion(httpClient http.Client, url string) string { +func getDaemonVersion(httpClient http.Client, url string) (string, error) { resp, err := httpClient.Get(url) if err != nil { - return "" + return "", err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return "" + return "", fmt.Errorf("unexpected status code received") } - var serverResponse struct { + var daemonResponse struct { Version string `json:"version"` } - if err := json.NewDecoder(resp.Body).Decode(&serverResponse); err != nil { - return "" + if err := json.NewDecoder(resp.Body).Decode(&daemonResponse); err != nil { + return "", err } - return serverResponse.Version + return daemonResponse.Version, nil } type versionResult struct { Name string `json:"name"` - ClientVersion string `json:"client_version"` + Version string `json:"version"` DaemonVersion string `json:"daemon_version,omitempty"` } func (r versionResult) String() string { - serverMessage := fmt.Sprintf("%s client version %s", - ProgramName, r.ClientVersion) + resultMessage := fmt.Sprintf("%s client version %s", + ProgramName, r.Version) if r.DaemonVersion != "" { - serverMessage = fmt.Sprintf("%s\ndaemon version: %s", - serverMessage, r.DaemonVersion) + resultMessage = fmt.Sprintf("%s\ndaemon version: %s", + resultMessage, r.DaemonVersion) } - return serverMessage + return resultMessage } func (r versionResult) Data() interface{} { diff --git a/cmd/arduino-app-cli/version/version_test.go b/cmd/arduino-app-cli/version/version_test.go index cd796e54..f0495e62 100644 --- a/cmd/arduino-app-cli/version/version_test.go +++ b/cmd/arduino-app-cli/version/version_test.go @@ -47,7 +47,7 @@ func TestGetValidUrl(t *testing.T) { expectedResult: "localhost:8800", }, { - name: "Custom host and port should return the default.", + name: "Custom host and port should return the provided host:port.", hostPort: "192.168.100.1:1234", expectedResult: "192.168.100.1:1234", }, @@ -87,7 +87,7 @@ func TestServerVersion(t *testing.T) { serverStub: successServer, expectedResult: versionResult{ Name: ProgramName, - ClientVersion: clientVersion, + Version: clientVersion, DaemonVersion: "3.0", }, hostAndPort: "localhost:8800", @@ -97,7 +97,7 @@ func TestServerVersion(t *testing.T) { serverStub: failureServer, expectedResult: versionResult{ Name: ProgramName, - ClientVersion: clientVersion, + Version: clientVersion, DaemonVersion: daemonVersion, }, hostAndPort: unreacheableUrl, @@ -107,7 +107,7 @@ func TestServerVersion(t *testing.T) { serverStub: failureServer, expectedResult: versionResult{ Name: ProgramName, - ClientVersion: clientVersion, + Version: clientVersion, DaemonVersion: daemonVersion, }, hostAndPort: unreacheableUrl, @@ -117,7 +117,7 @@ func TestServerVersion(t *testing.T) { serverStub: failureInternalServerError, expectedResult: versionResult{ Name: ProgramName, - ClientVersion: clientVersion, + Version: clientVersion, DaemonVersion: daemonVersion, }, hostAndPort: unreacheableUrl, From 7aba0e15c65baa2def705f0ce30c92b5dfd23b6c Mon Sep 17 00:00:00 2001 From: Marta Carbone Date: Tue, 11 Nov 2025 09:10:04 +0100 Subject: [PATCH 6/9] Define the port only as custom input. --- cmd/arduino-app-cli/version/version.go | 68 +++------- cmd/arduino-app-cli/version/version_test.go | 133 +++++++------------- 2 files changed, 63 insertions(+), 138 deletions(-) diff --git a/cmd/arduino-app-cli/version/version.go b/cmd/arduino-app-cli/version/version.go index a656b851..cfc54b01 100644 --- a/cmd/arduino-app-cli/version/version.go +++ b/cmd/arduino-app-cli/version/version.go @@ -18,10 +18,10 @@ package version import ( "encoding/json" "fmt" + "net" "net/http" "net/url" - "strings" "time" "github.com/spf13/cobra" @@ -40,73 +40,39 @@ const ( func NewVersionCmd(clientVersion string) *cobra.Command { cmd := &cobra.Command{ Use: "version", - Short: "Print the client and server versions for the Arduino App CLI.", + Short: "Print the client and server versions for the Arduino App CLI", Run: func(cmd *cobra.Command, args []string) { - host, _ := cmd.Flags().GetString("host") + port, _ := cmd.Flags().GetString("port") - validatedHostAndPort, err := validateHost(host) + daemonVersion, err := getDaemonVersion(http.Client{}, port) if err != nil { - feedback.Fatal("Error: invalid host:port format", feedback.ErrBadArgument) + feedback.Warnf("Warning: cannot get the running daemon version on %s:%s\n", DefaultHostname, port) } - httpClient := http.Client{ - Timeout: time.Second, + result := versionResult{ + Name: ProgramName, + Version: clientVersion, + DaemonVersion: daemonVersion, } - result, err := versionHandler(httpClient, clientVersion, validatedHostAndPort) - if err != nil { - feedback.Warnf("Waning: " + err.Error() + "\n") - } feedback.PrintResult(result) }, } - cmd.Flags().String("host", fmt.Sprintf("%s:%s", DefaultHostname, DefaultPort), - "The daemon network address [host]:[port]") + cmd.Flags().String("port", DefaultPort, "The daemon network port") return cmd } -func versionHandler(httpClient http.Client, clientVersion string, hostAndPort string) (versionResult, error) { +func getDaemonVersion(httpClient http.Client, port string) (string, error) { + + httpClient.Timeout = time.Second + url := url.URL{ Scheme: "http", - Host: hostAndPort, + Host: net.JoinHostPort(DefaultHostname, port), Path: "/v1/version", } - daemonVersion, err := getDaemonVersion(httpClient, url.String()) - - result := versionResult{ - Name: ProgramName, - Version: clientVersion, - DaemonVersion: daemonVersion, - } - - if err != nil { - return result, fmt.Errorf("error getting daemon version %s", hostAndPort) - } - return result, nil -} - -func validateHost(hostPort string) (string, error) { - if !strings.Contains(hostPort, ":") { - hostPort += ":" - } - - h, p, err := net.SplitHostPort(hostPort) - if err != nil { - return "", err - } - if h == "" { - h = DefaultHostname - } - if p == "" { - p = DefaultPort - } - - return net.JoinHostPort(h, p), nil -} - -func getDaemonVersion(httpClient http.Client, url string) (string, error) { - resp, err := httpClient.Get(url) + resp, err := httpClient.Get(url.String()) if err != nil { return "", err } @@ -133,7 +99,7 @@ type versionResult struct { } func (r versionResult) String() string { - resultMessage := fmt.Sprintf("%s client version %s", + resultMessage := fmt.Sprintf("%s version %s", ProgramName, r.Version) if r.DaemonVersion != "" { diff --git a/cmd/arduino-app-cli/version/version_test.go b/cmd/arduino-app-cli/version/version_test.go index f0495e62..39617968 100644 --- a/cmd/arduino-app-cli/version/version_test.go +++ b/cmd/arduino-app-cli/version/version_test.go @@ -25,104 +25,52 @@ import ( "github.com/stretchr/testify/require" ) -func TestGetValidUrl(t *testing.T) { +func TestDaemonVersion(t *testing.T) { testCases := []struct { - name string - hostPort string - expectedResult string + name string + serverStub Tripper + port string + expectedResult string + expectedErrorMessage string }{ { - name: "Valid host and port should return default.", - hostPort: "localhost:8800", - expectedResult: "localhost:8800", + name: "return the server version when the server is up", + serverStub: successServer, + port: "8800", + expectedResult: "3.0-server", + expectedErrorMessage: "", }, { - name: "Missing host should return default host.", - hostPort: ":8800", - expectedResult: "localhost:8800", + name: "return error if default server is not listening on default port", + serverStub: failureServer, + port: "8800", + expectedResult: "", + expectedErrorMessage: `Get "http://localhost:8800/v1/version": connection refused`, }, { - name: "Missing port should return default port.", - hostPort: "localhost:", - expectedResult: "localhost:8800", + name: "return error if provided server is not listening on provided port", + serverStub: failureServer, + port: "1234", + expectedResult: "", + expectedErrorMessage: `Get "http://localhost:1234/v1/version": connection refused`, }, { - name: "Custom host and port should return the provided host:port.", - hostPort: "192.168.100.1:1234", - expectedResult: "192.168.100.1:1234", + name: "return error for server response 500 Internal Server Error", + serverStub: failureInternalServerError, + port: "0", + expectedResult: "", + expectedErrorMessage: "unexpected status code received", }, - { - name: "Host only should return provided input and default port.", - hostPort: "192.168.1.1", - expectedResult: "192.168.1.1:8800", - }, - { - name: "Missing host and port should return default.", - hostPort: "", - expectedResult: "localhost:8800", - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - url, _ := validateHost(tc.hostPort) - require.Equal(t, tc.expectedResult, url) - }) - } -} -func TestServerVersion(t *testing.T) { - clientVersion := "5.1-dev" - unreacheableUrl := "unreacheable:123" - daemonVersion := "" - - testCases := []struct { - name string - serverStub Tripper - expectedResult versionResult - hostAndPort string - }{ { - name: "return the server version when the server is up", - serverStub: successServer, - expectedResult: versionResult{ - Name: ProgramName, - Version: clientVersion, - DaemonVersion: "3.0", - }, - hostAndPort: "localhost:8800", - }, - { - name: "return error if default server is not listening", - serverStub: failureServer, - expectedResult: versionResult{ - Name: ProgramName, - Version: clientVersion, - DaemonVersion: daemonVersion, - }, - hostAndPort: unreacheableUrl, - }, - { - name: "return error if provided server is not listening", - serverStub: failureServer, - expectedResult: versionResult{ - Name: ProgramName, - Version: clientVersion, - DaemonVersion: daemonVersion, - }, - hostAndPort: unreacheableUrl, - }, - { - name: "return error for server resopnse 500 Internal Server Error", - serverStub: failureInternalServerError, - expectedResult: versionResult{ - Name: ProgramName, - Version: clientVersion, - DaemonVersion: daemonVersion, - }, - hostAndPort: unreacheableUrl, + name: "return error for server up and wrong json response", + serverStub: successServerWrongJson, + port: "8800", + expectedResult: "", + expectedErrorMessage: "invalid character '<' looking for beginning of value", }, } + for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { // arrange @@ -130,10 +78,13 @@ func TestServerVersion(t *testing.T) { httpClient.Transport = tc.serverStub // act - result, _ := versionHandler(httpClient, clientVersion, tc.hostAndPort) + result, err := getDaemonVersion(httpClient, tc.port) // assert require.Equal(t, tc.expectedResult, result) + if err != nil { + require.Equal(t, tc.expectedErrorMessage, err.Error()) + } }) } } @@ -147,7 +98,15 @@ func (t Tripper) RoundTrip(request *http.Request) (*http.Response, error) { } var successServer = Tripper(func(*http.Request) (*http.Response, error) { - body := io.NopCloser(strings.NewReader(`{"version":"3.0"}`)) + body := io.NopCloser(strings.NewReader(`{"version":"3.0-server"}`)) + return &http.Response{ + StatusCode: http.StatusOK, + Body: body, + }, nil +}) + +var successServerWrongJson = Tripper(func(*http.Request) (*http.Response, error) { + body := io.NopCloser(strings.NewReader(` Date: Tue, 11 Nov 2025 10:59:46 +0100 Subject: [PATCH 7/9] Update cmd/arduino-app-cli/version/version.go Co-authored-by: Luca Rinaldi --- cmd/arduino-app-cli/version/version.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cmd/arduino-app-cli/version/version.go b/cmd/arduino-app-cli/version/version.go index cfc54b01..a0e7f478 100644 --- a/cmd/arduino-app-cli/version/version.go +++ b/cmd/arduino-app-cli/version/version.go @@ -99,8 +99,7 @@ type versionResult struct { } func (r versionResult) String() string { - resultMessage := fmt.Sprintf("%s version %s", - ProgramName, r.Version) + resultMessage := fmt.Sprintf("%s version %s", ProgramName, r.Version) if r.DaemonVersion != "" { resultMessage = fmt.Sprintf("%s\ndaemon version: %s", From ef78764e1a3997463104ede7b6e58709e86e00a2 Mon Sep 17 00:00:00 2001 From: martacarbone Date: Tue, 11 Nov 2025 11:00:14 +0100 Subject: [PATCH 8/9] Update cmd/arduino-app-cli/version/version.go Co-authored-by: Luca Rinaldi --- cmd/arduino-app-cli/version/version.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/arduino-app-cli/version/version.go b/cmd/arduino-app-cli/version/version.go index a0e7f478..f50b226c 100644 --- a/cmd/arduino-app-cli/version/version.go +++ b/cmd/arduino-app-cli/version/version.go @@ -40,7 +40,7 @@ const ( func NewVersionCmd(clientVersion string) *cobra.Command { cmd := &cobra.Command{ Use: "version", - Short: "Print the client and server versions for the Arduino App CLI", + Short: "Print the version number of Arduino App CLI", Run: func(cmd *cobra.Command, args []string) { port, _ := cmd.Flags().GetString("port") From 0f6e2124429cd66d93ca8b7ec47942750f992488 Mon Sep 17 00:00:00 2001 From: martacarbone Date: Tue, 11 Nov 2025 11:01:17 +0100 Subject: [PATCH 9/9] Update cmd/arduino-app-cli/version/version.go Co-authored-by: Luca Rinaldi --- cmd/arduino-app-cli/version/version.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cmd/arduino-app-cli/version/version.go b/cmd/arduino-app-cli/version/version.go index f50b226c..1cb06d05 100644 --- a/cmd/arduino-app-cli/version/version.go +++ b/cmd/arduino-app-cli/version/version.go @@ -18,7 +18,6 @@ package version import ( "encoding/json" "fmt" - "net" "net/http" "net/url"