Skip to content
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
31 changes: 14 additions & 17 deletions pkg/cmd/auth/get/get.go
Original file line number Diff line number Diff line change
@@ -1,39 +1,39 @@
package get

import (
"fmt"

"github.com/MakeNowJust/heredoc"
"github.com/spf13/cobra"

"github.com/algolia/cli/api/dashboard"
"github.com/algolia/cli/pkg/auth"
"github.com/algolia/cli/pkg/cmdutil"
"github.com/algolia/cli/pkg/iostreams"
"github.com/algolia/cli/pkg/validators"
)

// GetOptions represents the options for the get command.
type GetOptions struct {
IO *iostreams.IOStreams

LoadToken func() *auth.StoredToken

PrintFlags *cmdutil.PrintFlags
IO *iostreams.IOStreams
LoadToken func() *auth.StoredToken
PrintFlags *cmdutil.PrintFlags
NewDashboardClient func(clientID string) *dashboard.Client
EnsureAuthenticated func(io *iostreams.IOStreams, client *dashboard.Client) (string, error)
}

// Identity is the authenticated user, without any token information.
type Identity struct {
UserID string `json:"user_id,omitempty"`
Email string `json:"email,omitempty"`
Name string `json:"name,omitempty"`
}

// NewGetCmd returns a new instance of the get command.
func NewGetCmd(f *cmdutil.Factory) *cobra.Command {
opts := &GetOptions{
IO: f.IOStreams,
LoadToken: auth.LoadToken,
PrintFlags: cmdutil.NewPrintFlags().WithDefaultOutput("json"),
NewDashboardClient: func(clientID string) *dashboard.Client {
return dashboard.NewClient(clientID)
},
EnsureAuthenticated: auth.EnsureAuthenticated,
}

cmd := &cobra.Command{
Expand All @@ -58,16 +58,13 @@ func NewGetCmd(f *cmdutil.Factory) *cobra.Command {
return cmd
}

// runGetCmd runs the get command.
func runGetCmd(opts *GetOptions) error {
stored := opts.LoadToken()
if stored == nil {
return fmt.Errorf("you are not logged in — run `algolia auth login` first")
client := opts.NewDashboardClient(auth.OAuthClientID())
if _, err := opts.EnsureAuthenticated(opts.IO, client); err != nil {
return err
}

if stored.IsExpired() {
return fmt.Errorf("your session has expired — run `algolia auth login` again")
}
stored := opts.LoadToken()

identity := Identity{
UserID: stored.UserID,
Expand Down
124 changes: 112 additions & 12 deletions pkg/cmd/auth/get/get_test.go
Original file line number Diff line number Diff line change
@@ -1,48 +1,148 @@
package get

import (
"bytes"
"fmt"
"io"
"net/http"
"testing"
"time"

"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zalando/go-keyring"

"github.com/algolia/cli/api/dashboard"
"github.com/algolia/cli/pkg/auth"
"github.com/algolia/cli/pkg/cmdutil"
"github.com/algolia/cli/pkg/iostreams"
"github.com/algolia/cli/test"
)

func TestGet_NotLoggedIn(t *testing.T) {
// roundTripFunc lets a test stub the HTTP transport of the dashboard client.
type roundTripFunc func(req *http.Request) (*http.Response, error)

func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
return f(req)
}

// cmdWithOpts wires runGetCmd to a custom GetOptions so tests can stub the
// auth seam (EnsureAuthenticated) or the dashboard client.
func cmdWithOpts(opts *GetOptions) *cobra.Command {
cmd := &cobra.Command{
RunE: func(cmd *cobra.Command, args []string) error {
return runGetCmd(opts)
},
}
opts.PrintFlags.AddFlags(cmd)
return cmd
}

// When there's no usable session, auth get launches the login flow (here
// stubbed) and proceeds with the resulting identity.
func TestGet_PromptsLoginWhenNoSession(t *testing.T) {
keyring.MockInit()
auth.ClearToken()
t.Cleanup(auth.ClearToken)

f, out := test.NewFactory(false, nil, nil, "")
cmd := NewGetCmd(f)
_, err := test.Execute(cmd, "", out)
called := false
opts := &GetOptions{
IO: f.IOStreams,
LoadToken: auth.LoadToken,
PrintFlags: cmdutil.NewPrintFlags().WithDefaultOutput("json"),
NewDashboardClient: func(clientID string) *dashboard.Client { return nil },
EnsureAuthenticated: func(_ *iostreams.IOStreams, _ *dashboard.Client) (string, error) {
called = true
// Simulate a successful browser login persisting a session.
require.NoError(t, auth.SaveToken(&dashboard.OAuthTokenResponse{
AccessToken: "fresh-access",
CreatedAt: time.Now().Unix(),
ExpiresIn: 3600,
User: &dashboard.User{ID: 7, Email: "new@example.com", Name: "New User"},
}))
return "fresh-access", nil
},
}

out, err := test.Execute(cmdWithOpts(opts), "--output ndjson", out)
require.NoError(t, err)
assert.True(t, called, "expected login flow to be triggered")
assert.Contains(t, out.String(), `"user_id":"7"`)
assert.Contains(t, out.String(), `"email":"new@example.com"`)
}

// If the login flow fails (e.g. the user aborts), the error is propagated.
func TestGet_ReturnsErrorWhenLoginFails(t *testing.T) {
keyring.MockInit()
auth.ClearToken()

f, out := test.NewFactory(false, nil, nil, "")
opts := &GetOptions{
IO: f.IOStreams,
LoadToken: auth.LoadToken,
PrintFlags: cmdutil.NewPrintFlags().WithDefaultOutput("json"),
NewDashboardClient: func(clientID string) *dashboard.Client { return nil },
EnsureAuthenticated: func(_ *iostreams.IOStreams, _ *dashboard.Client) (string, error) {
return "", fmt.Errorf("authorization failed: access_denied")
},
}

_, err := test.Execute(cmdWithOpts(opts), "", out)
require.Error(t, err)
assert.Equal(t, "you are not logged in — run `algolia auth login` first", err.Error())
assert.Equal(t, "authorization failed: access_denied", err.Error())
}

func TestGet_Expired(t *testing.T) {
func TestGet_RefreshesExpiredToken(t *testing.T) {
keyring.MockInit()
t.Cleanup(auth.ClearToken)
require.NoError(t, auth.SaveToken(&dashboard.OAuthTokenResponse{
AccessToken: "secret-access",
CreatedAt: time.Now().Unix() - 7200,
ExpiresIn: 3600,
AccessToken: "old-access",
RefreshToken: "valid-refresh",
CreatedAt: time.Now().Unix() - 7200,
ExpiresIn: 3600,
User: &dashboard.User{
ID: 42,
Email: "user@example.com",
Name: "Test User",
},
}))

body := `{"access_token":"new-access","refresh_token":"new-refresh","expires_in":3600}`
httpClient := &http.Client{
Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewBufferString(body)),
Header: http.Header{"Content-Type": []string{"application/json"}},
}, nil
}),
}

f, out := test.NewFactory(false, nil, nil, "")
cmd := NewGetCmd(f)
_, err := test.Execute(cmd, "", out)
require.Error(t, err)
assert.Equal(t, "your session has expired — run `algolia auth login` again", err.Error())
opts := &GetOptions{
IO: f.IOStreams,
LoadToken: auth.LoadToken,
PrintFlags: cmdutil.NewPrintFlags().WithDefaultOutput("json"),
NewDashboardClient: func(clientID string) *dashboard.Client {
return dashboard.NewClientWithHTTPClient(clientID, httpClient)
},
// Real auth seam: GetValidToken refreshes via the stubbed client and
// succeeds, so the browser flow is never reached.
EnsureAuthenticated: auth.EnsureAuthenticated,
}

out, err := test.Execute(cmdWithOpts(opts), "--output ndjson", out)
require.NoError(t, err)

// Identity preserved from the pre-refresh token (refresh response has no user).
assert.Contains(t, out.String(), `"user_id":"42"`)
assert.Contains(t, out.String(), `"email":"user@example.com"`)
assert.NotContains(t, out.String(), "new-access")

// Refreshed token was persisted.
assert.Equal(t, "new-access", auth.LoadToken().AccessToken)
}

func TestGet_PrintsIdentityWithoutTokens(t *testing.T) {
Expand Down
Loading