Skip to content

fix(api-client): addresses bugs in API client #2191

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions api/client/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"net/http"
)

// 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.
func (c *client) Login(password string) error {
loginReq := struct {
Password string `json:"password"`
Expand Down Expand Up @@ -35,13 +36,13 @@ func (c *client) Login(password string) error {
}

var loginResp struct {
SessionToken string `json:"sessionToken"`
Token string `json:"token"`
}
err = json.NewDecoder(resp.Body).Decode(&loginResp)
if err != nil {
return err
}

c.token = loginResp.SessionToken
c.token = loginResp.Token
Comment on lines +39 to +46
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Matching the API response body.

return nil
}
17 changes: 7 additions & 10 deletions api/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,6 @@ import (
"github.com/replicatedhq/embedded-cluster/api/types"
)

type APIError struct {
StatusCode int `json:"status_code"`
Message string `json:"message"`
}

func (e *APIError) Error() string {
return fmt.Sprintf("status=%d, message=%q", e.StatusCode, e.Message)
}
Comment on lines -12 to -19
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can leverage types.APIError instead.


var defaultHTTPClient = &http.Client{
Transport: &http.Transport{
Proxy: nil, // This is a local client so no proxy is needed
Expand Down Expand Up @@ -66,12 +57,18 @@ func New(apiURL string, opts ...ClientOption) Client {
return c
}

func setAuthorizationHeader(req *http.Request, token string) {
if token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
}

func errorFromResponse(resp *http.Response) error {
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("unexpected response: status=%d", resp.StatusCode)
}
var apiError APIError
var apiError types.APIError
err = json.Unmarshal(body, &apiError)
if err != nil {
return fmt.Errorf("unexpected response: status=%d, body=%q", resp.StatusCode, string(body))
Expand Down
287 changes: 287 additions & 0 deletions api/client/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
package client

import (
"bytes"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"testing"

"github.com/replicatedhq/embedded-cluster/api/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestNew(t *testing.T) {
// Test default client creation
c := New("http://example.com")
clientImpl, ok := c.(*client)
assert.True(t, ok, "Expected c to be of type *client")
assert.Equal(t, "http://example.com", clientImpl.apiURL)
assert.Equal(t, defaultHTTPClient, clientImpl.httpClient)
assert.Empty(t, clientImpl.token)

// Test with custom HTTP client
customHTTPClient := &http.Client{}
c = New("http://example.com", WithHTTPClient(customHTTPClient))
clientImpl, ok = c.(*client)
assert.True(t, ok, "Expected c to be of type *client")
assert.Equal(t, customHTTPClient, clientImpl.httpClient)

// Test with token
c = New("http://example.com", WithToken("test-token"))
clientImpl, ok = c.(*client)
assert.True(t, ok, "Expected c to be of type *client")
assert.Equal(t, "test-token", clientImpl.token)

// Test with multiple options
c = New("http://example.com", WithHTTPClient(customHTTPClient), WithToken("test-token"))
clientImpl, ok = c.(*client)
assert.True(t, ok, "Expected c to be of type *client")
assert.Equal(t, customHTTPClient, clientImpl.httpClient)
assert.Equal(t, "test-token", clientImpl.token)
}

func TestLogin(t *testing.T) {
// Create a test server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "POST", r.Method)
assert.Equal(t, "/api/auth/login", r.URL.Path)
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))

// Decode request body
var loginReq struct {
Password string `json:"password"`
}
err := json.NewDecoder(r.Body).Decode(&loginReq)
require.NoError(t, err, "Failed to decode request body")

// Check password
if loginReq.Password == "correct-password" {
// Return successful response
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(struct {
Token string `json:"token"`
}{
Token: "test-token",
})
} else {
// Return error response
w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(types.APIError{
StatusCode: http.StatusUnauthorized,
Message: "Invalid password",
})
}
}))
defer server.Close()

// Test successful login
c := New(server.URL)
err := c.Login("correct-password")
assert.NoError(t, err)

// Check that token was set
clientImpl, ok := c.(*client)
require.True(t, ok, "Expected c to be of type *client")
assert.Equal(t, "test-token", clientImpl.token)

// Test failed login
c = New(server.URL)
err = c.Login("wrong-password")
assert.Error(t, err)

// Check that error is of type APIError
apiErr, ok := err.(*types.APIError)
require.True(t, ok, "Expected err to be of type *types.APIError")
assert.Equal(t, http.StatusUnauthorized, apiErr.StatusCode)
assert.Equal(t, "Invalid password", apiErr.Message)
}

func TestGetInstall(t *testing.T) {
// Create a test server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "GET", r.Method)
assert.Equal(t, "/api/install", r.URL.Path)

assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
assert.Equal(t, "Bearer test-token", r.Header.Get("Authorization"))

// Return successful response
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(types.Install{
Config: types.InstallationConfig{
GlobalCIDR: "10.0.0.0/24",
AdminConsolePort: 8080,
},
})
}))
defer server.Close()

// Test successful get
c := New(server.URL, WithToken("test-token"))
install, err := c.GetInstall()
assert.NoError(t, err)
assert.NotNil(t, install)
assert.Equal(t, "10.0.0.0/24", install.Config.GlobalCIDR)
assert.Equal(t, 8080, install.Config.AdminConsolePort)

// Test error response
errorServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(types.APIError{
StatusCode: http.StatusInternalServerError,
Message: "Internal Server Error",
})
}))
defer errorServer.Close()

c = New(errorServer.URL, WithToken("test-token"))
install, err = c.GetInstall()
assert.Error(t, err)
assert.Nil(t, install)

apiErr, ok := err.(*types.APIError)
require.True(t, ok, "Expected err to be of type *types.APIError")
assert.Equal(t, http.StatusInternalServerError, apiErr.StatusCode)
assert.Equal(t, "Internal Server Error", apiErr.Message)
}

func TestSetInstallConfig(t *testing.T) {
// Create a test server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Check request method and path
assert.Equal(t, "POST", r.Method) // Corrected from PUT to POST based on implementation
assert.Equal(t, "/api/install/config", r.URL.Path)

// Check headers
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
assert.Equal(t, "Bearer test-token", r.Header.Get("Authorization"))

// Decode request body
var config types.InstallationConfig
err := json.NewDecoder(r.Body).Decode(&config)
require.NoError(t, err, "Failed to decode request body")

// Return successful response
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(types.Install{
Config: config,
})
}))
defer server.Close()

// Test successful set
c := New(server.URL, WithToken("test-token"))
config := types.InstallationConfig{
GlobalCIDR: "20.0.0.0/24",
LocalArtifactMirrorPort: 9081,
}
install, err := c.SetInstallConfig(config)
assert.NoError(t, err)
assert.NotNil(t, install)
assert.Equal(t, config, install.Config)

// Test error response
errorServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(types.APIError{
StatusCode: http.StatusBadRequest,
Message: "Bad Request",
})
}))
defer errorServer.Close()

c = New(errorServer.URL, WithToken("test-token"))
install, err = c.SetInstallConfig(config)
assert.Error(t, err)
assert.Nil(t, install)

apiErr, ok := err.(*types.APIError)
require.True(t, ok, "Expected err to be of type *types.APIError")
assert.Equal(t, http.StatusBadRequest, apiErr.StatusCode)
assert.Equal(t, "Bad Request", apiErr.Message)
}

func TestSetInstallStatus(t *testing.T) {
// Create a test server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "POST", r.Method)
assert.Equal(t, "/api/install/status", r.URL.Path)

assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
assert.Equal(t, "Bearer test-token", r.Header.Get("Authorization"))

// Decode request body
var status types.InstallationStatus
err := json.NewDecoder(r.Body).Decode(&status)
require.NoError(t, err, "Failed to decode request body")

// Return successful response
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(types.Install{
Status: status,
})
}))
defer server.Close()

// Test successful set
c := New(server.URL, WithToken("test-token"))
status := types.InstallationStatus{
State: types.InstallationStateSucceeded,
Description: "Installation successful",
}
install, err := c.SetInstallStatus(status)
assert.NoError(t, err)
assert.NotNil(t, install)
assert.Equal(t, status, install.Status)

// Test error response
errorServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(types.APIError{
StatusCode: http.StatusBadRequest,
Message: "Bad Request",
})
}))
defer errorServer.Close()

c = New(errorServer.URL, WithToken("test-token"))
install, err = c.SetInstallStatus(status)
assert.Error(t, err)
assert.Nil(t, install)

apiErr, ok := err.(*types.APIError)
require.True(t, ok, "Expected err to be of type *types.APIError")
assert.Equal(t, http.StatusBadRequest, apiErr.StatusCode)
assert.Equal(t, "Bad Request", apiErr.Message)
}

func TestErrorFromResponse(t *testing.T) {
// Create a response with an error
resp := &http.Response{
StatusCode: http.StatusBadRequest,
Body: io.NopCloser(bytes.NewBufferString(`{"status_code": 400, "message": "Bad Request"}`)),
}

err := errorFromResponse(resp)
assert.Error(t, err)

// Check that error is of type APIError
apiErr, ok := err.(*types.APIError)
require.True(t, ok, "Expected err to be of type *types.APIError")
assert.Equal(t, http.StatusBadRequest, apiErr.StatusCode)
assert.Equal(t, "Bad Request", apiErr.Message)

// Test with malformed JSON
resp = &http.Response{
StatusCode: http.StatusBadRequest,
Body: io.NopCloser(bytes.NewBufferString(`not a json`)),
}

err = errorFromResponse(resp)
assert.Error(t, err)
assert.Contains(t, err.Error(), "unexpected response")
}

Check failure on line 287 in api/client/client_test.go

View workflow job for this annotation

GitHub Actions / Sanitize

File is not properly formatted (gofmt)
8 changes: 4 additions & 4 deletions api/client/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ import (
)

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

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

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

resp, err := c.httpClient.Do(req)
if err != nil {
Expand Down
Loading
Loading