Skip to content

Commit 9097dd3

Browse files
Marta CarboneMarta Carbone
authored andcommitted
Add the server version to arduino-app-cli version.
1 parent a6c084e commit 9097dd3

File tree

2 files changed

+207
-9
lines changed

2 files changed

+207
-9
lines changed

cmd/arduino-app-cli/version/version.go

Lines changed: 107 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,34 +16,132 @@
1616
package version
1717

1818
import (
19+
"encoding/json"
1920
"fmt"
21+
"io"
22+
"net"
23+
"net/http"
24+
"net/url"
25+
"time"
2026

2127
"github.com/spf13/cobra"
2228

2329
"github.com/arduino/arduino-app-cli/cmd/feedback"
30+
"github.com/arduino/arduino-app-cli/cmd/i18n"
2431
)
2532

26-
func NewVersionCmd(version string) *cobra.Command {
33+
// The actual listening address for the daemon
34+
// is defined in the installation package
35+
const (
36+
DefaultHostname = "localhost"
37+
DefaultPort = "8800"
38+
)
39+
40+
func NewVersionCmd(clientVersion string) *cobra.Command {
2741
cmd := &cobra.Command{
2842
Use: "version",
29-
Short: "Print the version number of Arduino App CLI",
43+
Short: "Print the client and server version numbers for the Arduino App CLI.",
3044
Run: func(cmd *cobra.Command, args []string) {
31-
feedback.PrintResult(versionResult{
32-
AppName: "Arduino App CLI",
33-
Version: version,
34-
})
45+
host, _ := cmd.Flags().GetString("host")
46+
47+
versionHandler(clientVersion, host)
3548
},
3649
}
50+
cmd.Flags().String("host", fmt.Sprintf("%s:%s", DefaultHostname, DefaultPort),
51+
"The daemon network address [host]:[port]")
3752
return cmd
3853
}
3954

40-
type versionResult struct {
41-
AppName string `json:"appName"`
55+
func versionHandler(clientVersion string, host string) {
56+
httpClient := http.Client{
57+
Timeout: time.Second,
58+
}
59+
result := doVersionHandler(httpClient, clientVersion, host)
60+
feedback.PrintResult(result)
61+
}
62+
63+
func doVersionHandler(httpClient http.Client, clientVersion string, host string) versionResult {
64+
url, err := getValidOrDefaultUrl(host)
65+
if err != nil {
66+
feedback.Fatal(i18n.Tr("Error: invalid host:port format"), feedback.ErrBadArgument)
67+
}
68+
69+
serverVersion, err := getServerVersion(httpClient, url)
70+
if err != nil {
71+
serverVersion = fmt.Sprintf("n/a (cannot connect to the server %s://%s)", url.Scheme, url.Host)
72+
}
73+
74+
return versionResult{
75+
ClientVersion: clientVersion,
76+
ServerVersion: serverVersion,
77+
}
78+
}
79+
80+
func getValidOrDefaultUrl(hostPort string) (url.URL, error) {
81+
host := DefaultHostname
82+
port := DefaultPort
83+
84+
if hostPort != "" {
85+
h, p, err := net.SplitHostPort(hostPort)
86+
if err != nil {
87+
return url.URL{}, err
88+
}
89+
if h != "" {
90+
host = h
91+
}
92+
if p != "" {
93+
port = p
94+
}
95+
96+
}
97+
98+
hostAndPort := net.JoinHostPort(host, port)
99+
100+
u := url.URL{
101+
Scheme: "http",
102+
Host: hostAndPort,
103+
Path: "/v1/version",
104+
}
105+
106+
return u, nil
107+
}
108+
109+
func getServerVersion(httpClient http.Client, url url.URL) (string, error) {
110+
resp, err := httpClient.Get(url.String())
111+
if err != nil {
112+
return "", err
113+
}
114+
defer resp.Body.Close()
115+
116+
if resp.StatusCode != http.StatusOK {
117+
return "", fmt.Errorf("request failed with status %d", resp.StatusCode)
118+
}
119+
120+
body, err := io.ReadAll(resp.Body)
121+
if err != nil {
122+
return "", err
123+
}
124+
125+
var serverResponse serverVersionResponse
126+
if err := json.Unmarshal(body, &serverResponse); err != nil {
127+
return "", err
128+
}
129+
130+
return serverResponse.Version, nil
131+
}
132+
133+
type serverVersionResponse struct {
42134
Version string `json:"version"`
43135
}
44136

137+
type versionResult struct {
138+
ClientVersion string `json:"version"`
139+
ServerVersion string `json:"serverVersion"`
140+
}
141+
45142
func (r versionResult) String() string {
46-
return fmt.Sprintf("%s v%s", r.AppName, r.Version)
143+
return fmt.Sprintf("client: %s\nserver: %s",
144+
r.ClientVersion, r.ServerVersion)
47145
}
48146

49147
func (r versionResult) Data() interface{} {
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package version
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"io"
7+
"net/http"
8+
"strings"
9+
"testing"
10+
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
func TestServerVersion(t *testing.T) {
15+
clientVersion := "5.1-dev"
16+
17+
testCases := []struct {
18+
name string
19+
serverStub Tripper
20+
expectedResult versionResult
21+
host string
22+
}{
23+
{
24+
name: "return the server version when the server is up",
25+
serverStub: successServer,
26+
expectedResult: versionResult{
27+
ClientVersion: "5.1-dev",
28+
ServerVersion: "3.0",
29+
},
30+
host: "",
31+
},
32+
{
33+
name: "return error if default server is not listening",
34+
serverStub: failureServer,
35+
expectedResult: versionResult{
36+
ClientVersion: "5.1-dev",
37+
ServerVersion: fmt.Sprintf("n/a (cannot connect to the server http://%s:%s)", DefaultHostname, DefaultPort),
38+
},
39+
host: "",
40+
},
41+
{
42+
name: "return error if provided server is not listening",
43+
serverStub: failureServer,
44+
expectedResult: versionResult{
45+
ClientVersion: "5.1-dev",
46+
ServerVersion: "n/a (cannot connect to the server http://unreacheable:123)",
47+
},
48+
host: "unreacheable:123",
49+
},
50+
{
51+
name: "return error for server resopnse 500 Internal Server Error",
52+
serverStub: failureInternalServerError,
53+
expectedResult: versionResult{
54+
ClientVersion: "5.1-dev",
55+
ServerVersion: "n/a (cannot connect to the server http://unreacheable:123)",
56+
},
57+
host: "unreacheable:123",
58+
},
59+
}
60+
for _, tc := range testCases {
61+
t.Run(tc.name, func(t *testing.T) {
62+
// arrange
63+
httpClient := http.Client{}
64+
httpClient.Transport = tc.serverStub
65+
66+
// act
67+
result := doVersionHandler(httpClient, clientVersion, tc.host)
68+
69+
// assert
70+
require.Equal(t, tc.expectedResult, result)
71+
})
72+
}
73+
}
74+
75+
// Leverage the http.Client's RoundTripper
76+
// to return a canned response and bypass network calls.
77+
type Tripper func(*http.Request) (*http.Response, error)
78+
79+
func (t Tripper) RoundTrip(request *http.Request) (*http.Response, error) {
80+
return t(request)
81+
}
82+
83+
var successServer = Tripper(func(*http.Request) (*http.Response, error) {
84+
body := io.NopCloser(strings.NewReader(`{"version":"3.0"}`))
85+
return &http.Response{
86+
StatusCode: http.StatusOK,
87+
Body: body,
88+
}, nil
89+
})
90+
91+
var failureServer = Tripper(func(*http.Request) (*http.Response, error) {
92+
return nil, errors.New("connetion refused")
93+
})
94+
95+
var failureInternalServerError = Tripper(func(*http.Request) (*http.Response, error) {
96+
return &http.Response{
97+
StatusCode: http.StatusInternalServerError,
98+
Body: io.NopCloser(strings.NewReader("")),
99+
}, nil
100+
})

0 commit comments

Comments
 (0)