Skip to content

Commit c64fd9b

Browse files
authored
fix(api-client): addresses bugs in API client (#2191)
1 parent 78e1e9e commit c64fd9b

File tree

6 files changed

+496
-16
lines changed

6 files changed

+496
-16
lines changed

api/client/auth.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"net/http"
77
)
88

9+
// Login sends a login request to the API server with the provided password and retrieves a session token. The token is stored in the client struct for subsequent requests.
910
func (c *client) Login(password string) error {
1011
loginReq := struct {
1112
Password string `json:"password"`
@@ -35,13 +36,13 @@ func (c *client) Login(password string) error {
3536
}
3637

3738
var loginResp struct {
38-
SessionToken string `json:"sessionToken"`
39+
Token string `json:"token"`
3940
}
4041
err = json.NewDecoder(resp.Body).Decode(&loginResp)
4142
if err != nil {
4243
return err
4344
}
4445

45-
c.token = loginResp.SessionToken
46+
c.token = loginResp.Token
4647
return nil
4748
}

api/client/client.go

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,6 @@ import (
99
"github.com/replicatedhq/embedded-cluster/api/types"
1010
)
1111

12-
type APIError struct {
13-
StatusCode int `json:"status_code"`
14-
Message string `json:"message"`
15-
}
16-
17-
func (e *APIError) Error() string {
18-
return fmt.Sprintf("status=%d, message=%q", e.StatusCode, e.Message)
19-
}
20-
2112
var defaultHTTPClient = &http.Client{
2213
Transport: &http.Transport{
2314
Proxy: nil, // This is a local client so no proxy is needed
@@ -66,12 +57,18 @@ func New(apiURL string, opts ...ClientOption) Client {
6657
return c
6758
}
6859

60+
func setAuthorizationHeader(req *http.Request, token string) {
61+
if token != "" {
62+
req.Header.Set("Authorization", "Bearer "+token)
63+
}
64+
}
65+
6966
func errorFromResponse(resp *http.Response) error {
7067
body, err := io.ReadAll(resp.Body)
7168
if err != nil {
7269
return fmt.Errorf("unexpected response: status=%d", resp.StatusCode)
7370
}
74-
var apiError APIError
71+
var apiError types.APIError
7572
err = json.Unmarshal(body, &apiError)
7673
if err != nil {
7774
return fmt.Errorf("unexpected response: status=%d, body=%q", resp.StatusCode, string(body))

api/client/client_test.go

Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
package client
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"io"
7+
"net/http"
8+
"net/http/httptest"
9+
"testing"
10+
11+
"github.com/replicatedhq/embedded-cluster/api/types"
12+
"github.com/stretchr/testify/assert"
13+
"github.com/stretchr/testify/require"
14+
)
15+
16+
func TestNew(t *testing.T) {
17+
// Test default client creation
18+
c := New("http://example.com")
19+
clientImpl, ok := c.(*client)
20+
assert.True(t, ok, "Expected c to be of type *client")
21+
assert.Equal(t, "http://example.com", clientImpl.apiURL)
22+
assert.Equal(t, defaultHTTPClient, clientImpl.httpClient)
23+
assert.Empty(t, clientImpl.token)
24+
25+
// Test with custom HTTP client
26+
customHTTPClient := &http.Client{}
27+
c = New("http://example.com", WithHTTPClient(customHTTPClient))
28+
clientImpl, ok = c.(*client)
29+
assert.True(t, ok, "Expected c to be of type *client")
30+
assert.Equal(t, customHTTPClient, clientImpl.httpClient)
31+
32+
// Test with token
33+
c = New("http://example.com", WithToken("test-token"))
34+
clientImpl, ok = c.(*client)
35+
assert.True(t, ok, "Expected c to be of type *client")
36+
assert.Equal(t, "test-token", clientImpl.token)
37+
38+
// Test with multiple options
39+
c = New("http://example.com", WithHTTPClient(customHTTPClient), WithToken("test-token"))
40+
clientImpl, ok = c.(*client)
41+
assert.True(t, ok, "Expected c to be of type *client")
42+
assert.Equal(t, customHTTPClient, clientImpl.httpClient)
43+
assert.Equal(t, "test-token", clientImpl.token)
44+
}
45+
46+
func TestLogin(t *testing.T) {
47+
// Create a test server
48+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
49+
assert.Equal(t, "POST", r.Method)
50+
assert.Equal(t, "/api/auth/login", r.URL.Path)
51+
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
52+
53+
// Decode request body
54+
var loginReq struct {
55+
Password string `json:"password"`
56+
}
57+
err := json.NewDecoder(r.Body).Decode(&loginReq)
58+
require.NoError(t, err, "Failed to decode request body")
59+
60+
// Check password
61+
if loginReq.Password == "correct-password" {
62+
// Return successful response
63+
w.WriteHeader(http.StatusOK)
64+
json.NewEncoder(w).Encode(struct {
65+
Token string `json:"token"`
66+
}{
67+
Token: "test-token",
68+
})
69+
} else {
70+
// Return error response
71+
w.WriteHeader(http.StatusUnauthorized)
72+
json.NewEncoder(w).Encode(types.APIError{
73+
StatusCode: http.StatusUnauthorized,
74+
Message: "Invalid password",
75+
})
76+
}
77+
}))
78+
defer server.Close()
79+
80+
// Test successful login
81+
c := New(server.URL)
82+
err := c.Login("correct-password")
83+
assert.NoError(t, err)
84+
85+
// Check that token was set
86+
clientImpl, ok := c.(*client)
87+
require.True(t, ok, "Expected c to be of type *client")
88+
assert.Equal(t, "test-token", clientImpl.token)
89+
90+
// Test failed login
91+
c = New(server.URL)
92+
err = c.Login("wrong-password")
93+
assert.Error(t, err)
94+
95+
// Check that error is of type APIError
96+
apiErr, ok := err.(*types.APIError)
97+
require.True(t, ok, "Expected err to be of type *types.APIError")
98+
assert.Equal(t, http.StatusUnauthorized, apiErr.StatusCode)
99+
assert.Equal(t, "Invalid password", apiErr.Message)
100+
}
101+
102+
func TestGetInstall(t *testing.T) {
103+
// Create a test server
104+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
105+
assert.Equal(t, "GET", r.Method)
106+
assert.Equal(t, "/api/install", r.URL.Path)
107+
108+
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
109+
assert.Equal(t, "Bearer test-token", r.Header.Get("Authorization"))
110+
111+
// Return successful response
112+
w.WriteHeader(http.StatusOK)
113+
json.NewEncoder(w).Encode(types.Install{
114+
Config: types.InstallationConfig{
115+
GlobalCIDR: "10.0.0.0/24",
116+
AdminConsolePort: 8080,
117+
},
118+
})
119+
}))
120+
defer server.Close()
121+
122+
// Test successful get
123+
c := New(server.URL, WithToken("test-token"))
124+
install, err := c.GetInstall()
125+
assert.NoError(t, err)
126+
assert.NotNil(t, install)
127+
assert.Equal(t, "10.0.0.0/24", install.Config.GlobalCIDR)
128+
assert.Equal(t, 8080, install.Config.AdminConsolePort)
129+
130+
// Test error response
131+
errorServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
132+
w.WriteHeader(http.StatusInternalServerError)
133+
json.NewEncoder(w).Encode(types.APIError{
134+
StatusCode: http.StatusInternalServerError,
135+
Message: "Internal Server Error",
136+
})
137+
}))
138+
defer errorServer.Close()
139+
140+
c = New(errorServer.URL, WithToken("test-token"))
141+
install, err = c.GetInstall()
142+
assert.Error(t, err)
143+
assert.Nil(t, install)
144+
145+
apiErr, ok := err.(*types.APIError)
146+
require.True(t, ok, "Expected err to be of type *types.APIError")
147+
assert.Equal(t, http.StatusInternalServerError, apiErr.StatusCode)
148+
assert.Equal(t, "Internal Server Error", apiErr.Message)
149+
}
150+
151+
func TestSetInstallConfig(t *testing.T) {
152+
// Create a test server
153+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
154+
// Check request method and path
155+
assert.Equal(t, "POST", r.Method) // Corrected from PUT to POST based on implementation
156+
assert.Equal(t, "/api/install/config", r.URL.Path)
157+
158+
// Check headers
159+
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
160+
assert.Equal(t, "Bearer test-token", r.Header.Get("Authorization"))
161+
162+
// Decode request body
163+
var config types.InstallationConfig
164+
err := json.NewDecoder(r.Body).Decode(&config)
165+
require.NoError(t, err, "Failed to decode request body")
166+
167+
// Return successful response
168+
w.WriteHeader(http.StatusOK)
169+
json.NewEncoder(w).Encode(types.Install{
170+
Config: config,
171+
})
172+
}))
173+
defer server.Close()
174+
175+
// Test successful set
176+
c := New(server.URL, WithToken("test-token"))
177+
config := types.InstallationConfig{
178+
GlobalCIDR: "20.0.0.0/24",
179+
LocalArtifactMirrorPort: 9081,
180+
}
181+
install, err := c.SetInstallConfig(config)
182+
assert.NoError(t, err)
183+
assert.NotNil(t, install)
184+
assert.Equal(t, config, install.Config)
185+
186+
// Test error response
187+
errorServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
188+
w.WriteHeader(http.StatusBadRequest)
189+
json.NewEncoder(w).Encode(types.APIError{
190+
StatusCode: http.StatusBadRequest,
191+
Message: "Bad Request",
192+
})
193+
}))
194+
defer errorServer.Close()
195+
196+
c = New(errorServer.URL, WithToken("test-token"))
197+
install, err = c.SetInstallConfig(config)
198+
assert.Error(t, err)
199+
assert.Nil(t, install)
200+
201+
apiErr, ok := err.(*types.APIError)
202+
require.True(t, ok, "Expected err to be of type *types.APIError")
203+
assert.Equal(t, http.StatusBadRequest, apiErr.StatusCode)
204+
assert.Equal(t, "Bad Request", apiErr.Message)
205+
}
206+
207+
func TestSetInstallStatus(t *testing.T) {
208+
// Create a test server
209+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
210+
assert.Equal(t, "POST", r.Method)
211+
assert.Equal(t, "/api/install/status", r.URL.Path)
212+
213+
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
214+
assert.Equal(t, "Bearer test-token", r.Header.Get("Authorization"))
215+
216+
// Decode request body
217+
var status types.InstallationStatus
218+
err := json.NewDecoder(r.Body).Decode(&status)
219+
require.NoError(t, err, "Failed to decode request body")
220+
221+
// Return successful response
222+
w.WriteHeader(http.StatusOK)
223+
json.NewEncoder(w).Encode(types.Install{
224+
Status: status,
225+
})
226+
}))
227+
defer server.Close()
228+
229+
// Test successful set
230+
c := New(server.URL, WithToken("test-token"))
231+
status := types.InstallationStatus{
232+
State: types.InstallationStateSucceeded,
233+
Description: "Installation successful",
234+
}
235+
install, err := c.SetInstallStatus(status)
236+
assert.NoError(t, err)
237+
assert.NotNil(t, install)
238+
assert.Equal(t, status, install.Status)
239+
240+
// Test error response
241+
errorServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
242+
w.WriteHeader(http.StatusBadRequest)
243+
json.NewEncoder(w).Encode(types.APIError{
244+
StatusCode: http.StatusBadRequest,
245+
Message: "Bad Request",
246+
})
247+
}))
248+
defer errorServer.Close()
249+
250+
c = New(errorServer.URL, WithToken("test-token"))
251+
install, err = c.SetInstallStatus(status)
252+
assert.Error(t, err)
253+
assert.Nil(t, install)
254+
255+
apiErr, ok := err.(*types.APIError)
256+
require.True(t, ok, "Expected err to be of type *types.APIError")
257+
assert.Equal(t, http.StatusBadRequest, apiErr.StatusCode)
258+
assert.Equal(t, "Bad Request", apiErr.Message)
259+
}
260+
261+
func TestErrorFromResponse(t *testing.T) {
262+
// Create a response with an error
263+
resp := &http.Response{
264+
StatusCode: http.StatusBadRequest,
265+
Body: io.NopCloser(bytes.NewBufferString(`{"status_code": 400, "message": "Bad Request"}`)),
266+
}
267+
268+
err := errorFromResponse(resp)
269+
assert.Error(t, err)
270+
271+
// Check that error is of type APIError
272+
apiErr, ok := err.(*types.APIError)
273+
require.True(t, ok, "Expected err to be of type *types.APIError")
274+
assert.Equal(t, http.StatusBadRequest, apiErr.StatusCode)
275+
assert.Equal(t, "Bad Request", apiErr.Message)
276+
277+
// Test with malformed JSON
278+
resp = &http.Response{
279+
StatusCode: http.StatusBadRequest,
280+
Body: io.NopCloser(bytes.NewBufferString(`not a json`)),
281+
}
282+
283+
err = errorFromResponse(resp)
284+
assert.Error(t, err)
285+
assert.Contains(t, err.Error(), "unexpected response")
286+
}
287+

api/client/install.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ import (
99
)
1010

1111
func (c *client) GetInstall() (*types.Install, error) {
12-
req, err := http.NewRequest("GET", c.apiURL, nil)
12+
req, err := http.NewRequest("GET", c.apiURL+"/api/install", nil)
1313
if err != nil {
1414
return nil, err
1515
}
1616
req.Header.Set("Content-Type", "application/json")
17-
req.Header.Set("Authorization", c.token)
17+
setAuthorizationHeader(req, c.token)
1818

1919
resp, err := c.httpClient.Do(req)
2020
if err != nil {
@@ -46,7 +46,7 @@ func (c *client) SetInstallConfig(config types.InstallationConfig) (*types.Insta
4646
return nil, err
4747
}
4848
req.Header.Set("Content-Type", "application/json")
49-
req.Header.Set("Authorization", c.token)
49+
setAuthorizationHeader(req, c.token)
5050

5151
resp, err := c.httpClient.Do(req)
5252
if err != nil {
@@ -78,7 +78,7 @@ func (c *client) SetInstallStatus(status types.InstallationStatus) (*types.Insta
7878
return nil, err
7979
}
8080
req.Header.Set("Content-Type", "application/json")
81-
req.Header.Set("Authorization", c.token)
81+
setAuthorizationHeader(req, c.token)
8282

8383
resp, err := c.httpClient.Do(req)
8484
if err != nil {

0 commit comments

Comments
 (0)