Skip to content

Commit

Permalink
WIP: mock device updates in tests
Browse files Browse the repository at this point in the history
Signed-off-by: Marques Johansson <[email protected]>
  • Loading branch information
displague committed Aug 27, 2024
1 parent 08407f8 commit 221430d
Show file tree
Hide file tree
Showing 2 changed files with 122 additions and 14 deletions.
43 changes: 29 additions & 14 deletions internal/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ const (

type Client struct {
// metalApiClient client
metalApiClient *metal.APIClient
metalApiClient *metal.APIClient
metalApiConnect func(c *Client, httpClient *http.Client) *metal.APIClient

includes *[]string // nolint:unused
excludes *[]string // nolint:unused
Expand Down Expand Up @@ -56,11 +57,30 @@ func (t *headerTransport) RoundTrip(r *http.Request) (*http.Response, error) {
return http.DefaultTransport.RoundTrip(r)
}

func NewClient(consumerToken, apiURL, Version string) *Client {
return &Client{
consumerToken: consumerToken,
apiURL: apiURL,
Version: Version,
type ClientOpt func(*Client)

func NewClient(consumerToken, apiURL, Version string, opts ...ClientOpt) *Client {
client := &Client{
consumerToken: consumerToken,
apiURL: apiURL,
Version: Version,
metalApiConnect: DefaultMetalApiConnect,
}
for _, opt := range opts {
opt(client)
}

return client
}

func (c *Client) SetMetalAPIConnect(f func(c *Client, httpClient *http.Client) *metal.APIClient) {
c.metalApiConnect = f
}

// WithMetalApiConnect sets the function to connect to the Equinix Metal API
func WithMetalApiConnect(metalApiConnect func(c *Client, httpClient *http.Client) *metal.APIClient) ClientOpt {
return func(c *Client) {
c.metalApiConnect = metalApiConnect
}
}

Expand All @@ -71,7 +91,7 @@ func checkEnvForDebug() bool {
return os.Getenv(debugVar) != ""
}

func (c *Client) metalApiConnect(httpClient *http.Client) error {
func DefaultMetalApiConnect(c *Client, httpClient *http.Client) *metal.APIClient {
configuration := metal.NewConfiguration()
configuration.Debug = checkEnvForDebug()
configuration.AddDefaultHeader("X-Auth-Token", c.Token())
Expand All @@ -81,9 +101,7 @@ func (c *Client) metalApiConnect(httpClient *http.Client) error {
URL: c.apiURL,
},
}
metalgoClient := metal.NewAPIClient(configuration)
c.metalApiClient = metalgoClient
return nil
return metal.NewAPIClient(configuration)
}

func (c *Client) Config(cmd *cobra.Command) *viper.Viper {
Expand Down Expand Up @@ -159,10 +177,7 @@ func (c *Client) MetalAPI(cmd *cobra.Command) *metal.APIClient {
},
}

err := c.metalApiConnect(httpClient)
if err != nil {
log.Fatal(err)
}
c.metalApiClient = c.metalApiConnect(c, httpClient)
}
return c.metalApiClient
}
Expand Down
93 changes: 93 additions & 0 deletions test/mock/devices/deviceupdatetest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package deviceupdatetest

import (
"fmt"
"io"
"net/http"
"strings"
"testing"

metal "github.com/equinix/equinix-sdk-go/services/metalv1"
root "github.com/equinix/metal-cli/internal/cli"
"github.com/equinix/metal-cli/internal/devices"
outputPkg "github.com/equinix/metal-cli/internal/outputs"
"github.com/equinix/metal-cli/test/helper"
"github.com/spf13/cobra"
)

type mockRoundTripper struct {
handler func(req *http.Request) (*http.Response, error)
}

func (m *mockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
return m.handler(req)
}

func TestCli_Devices_Update(t *testing.T) {
var setupMockClientOpt root.ClientOpt
setupMockClientFn := func(c *root.Client, httpClient *http.Client) *metal.APIClient {

This comment has been minimized.

Copy link
@ctreatma

ctreatma Aug 27, 2024

Contributor

The 2-factor auth tests use a mock API: https://github.com/equinix/metal-cli/blob/81ea9578c2f7dbaf2e526df70024dc9fd6ea75cc/test/e2e/twofa_test.go

IMO mocking the API is easier to read through. Are there benefits to mocking in the client's HTTP transport as opposed to creating a client that talks to a mock API?

cfg := metal.NewConfiguration()
httpClient.Transport = &mockRoundTripper{handler: func(req *http.Request) (*http.Response, error) {
if strings.Contains(req.URL.Path, "/devices/") && req.Method == http.MethodPut {
body, _ := io.ReadAll(req.Body)
if !strings.Contains(string(body), `"locked": true`) {
t.FailNow()
}

return &http.Response{
Body: io.NopCloser(strings.NewReader(`{}`)),
Header: http.Header{"Content-Type": []string{"application/json"}},
StatusCode: http.StatusOK,
}, nil
}
return nil, fmt.Errorf("unknown request: %s %s", req.Method, req.URL.Path)
},
}
cfg.HTTPClient = httpClient
return metal.NewAPIClient(cfg)
}
setupMockClientOpt = func(c *root.Client) {
c.SetMetalAPIConnect(setupMockClientFn)
}

subCommand := "device"
rootClient := root.NewClient(helper.ConsumerToken, helper.URL, helper.Version, setupMockClientOpt)
type fields struct {
MainCmd *cobra.Command
Outputer outputPkg.Outputer
}
tests := []struct {
name string
fields fields
want *cobra.Command
cmdFunc func(*testing.T, *cobra.Command)
}{
{
name: "update_device",
fields: fields{
MainCmd: devices.NewClient(rootClient, outputPkg.Outputer(&outputPkg.Standard{})).NewCommand(),
Outputer: outputPkg.Outputer(&outputPkg.Standard{}),
},
want: &cobra.Command{},
cmdFunc: func(t *testing.T, c *cobra.Command) {
root := c.Root()

root.SetArgs([]string{subCommand, "update", "-i", "1234", "-H", "metal-cli-update-dev-test", "--locked", "true"})

out := helper.ExecuteAndCaptureOutput(t, root)

if !strings.Contains(string(out[:]), "metal-cli-update-dev-test") {
t.Error("expected output should include metal-cli-update-dev-test in the out string ")
}
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
rootCmd := rootClient.NewCommand()
rootCmd.AddCommand(tt.fields.MainCmd)
tt.cmdFunc(t, tt.fields.MainCmd)
})
}
}

0 comments on commit 221430d

Please sign in to comment.