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
2 changes: 2 additions & 0 deletions pkg/cmd/application/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"github.com/spf13/cobra"

"github.com/algolia/cli/pkg/cmd/application/create"
"github.com/algolia/cli/pkg/cmd/application/current"
"github.com/algolia/cli/pkg/cmd/application/downgrade"
"github.com/algolia/cli/pkg/cmd/application/list"
"github.com/algolia/cli/pkg/cmd/application/plans"
Expand All @@ -23,6 +24,7 @@ func NewApplicationCmd(f *cmdutil.Factory) *cobra.Command {

cmd.AddCommand(create.NewCreateCmd(f))
cmd.AddCommand(list.NewListCmd(f))
cmd.AddCommand(current.NewCurrentCmd(f))
cmd.AddCommand(selectapp.NewSelectCmd(f))
cmd.AddCommand(update.NewUpdateCmd(f))
cmd.AddCommand(plans.NewPlansCmd(f))
Expand Down
149 changes: 149 additions & 0 deletions pkg/cmd/application/current/current.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package current

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/config"
"github.com/algolia/cli/pkg/iostreams"
"github.com/algolia/cli/pkg/validators"
)

type CurrentOptions struct {
IO *iostreams.IOStreams
Config config.IConfig

PrintFlags *cmdutil.PrintFlags

NewDashboardClient func(clientID string) *dashboard.Client
}

type currentApplication struct {
ID string `json:"id"`
Alias string `json:"alias"`
Name string `json:"name"`
Plan string `json:"plan"`
}

func NewCurrentCmd(f *cmdutil.Factory) *cobra.Command {
opts := &CurrentOptions{
IO: f.IOStreams,
Config: f.Config,
PrintFlags: cmdutil.NewPrintFlags(),
NewDashboardClient: func(clientID string) *dashboard.Client {
return dashboard.NewClient(clientID)
},
}

cmd := &cobra.Command{
Use: "current",
Short: "Show the currently selected application",
Long: heredoc.Doc(`
Show which Algolia application is currently selected, along with its
name and plan.

The application ID (and its alias, when set) is shown even if the name
and plan can't be fetched.
`),
Example: heredoc.Doc(`
# Show the current application
$ algolia application current

# Output as JSON
$ algolia application current --output json
`),
Args: validators.NoArgs(),
Annotations: map[string]string{
"skipAuthCheck": "true",
},
RunE: func(cmd *cobra.Command, args []string) error {
return runCurrentCmd(opts)
},
}

opts.PrintFlags.AddFlags(cmd)

return cmd
}

func runCurrentCmd(opts *CurrentOptions) error {
cs := opts.IO.ColorScheme()

appID, err := opts.Config.Profile().GetApplicationID()
if err != nil {
return fmt.Errorf(
"no current application configured; run \"algolia application select\" or \"algolia auth login\" first: %w",
err,
)
}

current := currentApplication{ID: appID}
if alias, ok := opts.Config.ApplicationAlias(appID); ok {
current.Alias = alias
}

app, signedOut := fetchApplication(opts, appID)
if app != nil {
current.Name = app.Name
current.Plan = app.PlanLabel
}

if opts.PrintFlags.OutputFlagSpecified() && opts.PrintFlags.OutputFormat != nil {
p, err := opts.PrintFlags.ToPrinter()
if err != nil {
return err
}
return p.Print(opts.IO, current)
}

fmt.Fprintf(opts.IO.Out, "%s Current application: %s\n", cs.SuccessIcon(), cs.Bold(appID))
if current.Alias != "" {
fmt.Fprintf(opts.IO.Out, " Alias: %s\n", current.Alias)
}
if current.Name != "" {
fmt.Fprintf(opts.IO.Out, " Name: %s\n", current.Name)
}
if current.Plan != "" {
fmt.Fprintf(opts.IO.Out, " Plan: %s\n", current.Plan)
}
if current.Name == "" && current.Plan == "" {
if signedOut {
fmt.Fprintf(
opts.IO.Out,
"%s Sign in with \"algolia auth login\" to see the application name and plan.\n",
cs.WarningIcon(),
)
} else {
fmt.Fprintf(
opts.IO.Out,
"%s Couldn't fetch the application name and plan; showing the selected application only.\n",
cs.WarningIcon(),
)
}
}

return nil
}

func fetchApplication(opts *CurrentOptions, appID string) (*dashboard.Application, bool) {
client := opts.NewDashboardClient(auth.OAuthClientID())

token, err := auth.GetValidToken(client)
if err != nil {
return nil, true
}

opts.IO.StartProgressIndicatorWithLabel("Fetching application")
app, err := client.GetApplication(token, appID)
opts.IO.StopProgressIndicator()
if err != nil {
return nil, false
}

return app, false
}
169 changes: 169 additions & 0 deletions pkg/cmd/application/current/current_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package current

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

"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/config"
"github.com/algolia/cli/pkg/iostreams"
"github.com/algolia/cli/test"
)

func seedToken(t *testing.T) {
t.Helper()
keyring.MockInit()
require.NoError(t, auth.SaveToken(&dashboard.OAuthTokenResponse{
AccessToken: "test-token",
ExpiresIn: 3600,
CreatedAt: time.Now().Unix(),
}))
}

func newServer(t *testing.T, status int) *httptest.Server {
t.Helper()
mux := http.NewServeMux()
mux.HandleFunc("/1/application/APP1", func(w http.ResponseWriter, _ *http.Request) {
if status != http.StatusOK {
w.WriteHeader(status)
return
}
require.NoError(t, json.NewEncoder(w).Encode(dashboard.SingleApplicationResponse{
Data: dashboard.ApplicationResource{
ID: "APP1",
Type: "application",
Attributes: dashboard.ApplicationAttributes{
ApplicationID: "APP1",
Name: "My App",
Plan: dashboard.ApplicationPlan{Label: "Grow Plus"},
},
},
}))
})
return httptest.NewServer(mux)
}

func newOpts(
t *testing.T,
srv *httptest.Server,
cfg *test.ConfigStub,
output string,
signedIn bool,
) (*CurrentOptions, *bytes.Buffer) {
t.Helper()
if signedIn {
seedToken(t)
} else {
keyring.MockInit()
auth.ClearToken()
}

io, _, stdout, _ := iostreams.Test()
pf := cmdutil.NewPrintFlags()
*pf.OutputFormat = output
pf.OutputFlagSpecified = func() bool { return output != "" }

opts := &CurrentOptions{
IO: io,
Config: cfg,
PrintFlags: pf,
NewDashboardClient: func(string) *dashboard.Client {
c := dashboard.NewClientWithHTTPClient("test", srv.Client())
c.APIURL = srv.URL
return c
},
}
return opts, stdout
}

func configWithApp(appID, alias string) *test.ConfigStub {
cfg := &test.ConfigStub{
CurrentProfile: config.Profile{ApplicationID: appID},
}
if alias != "" {
cfg.SavedApps = map[string]test.SavedApplication{
appID: {Alias: alias},
}
}
return cfg
}

func Test_runCurrentCmd(t *testing.T) {
srv := newServer(t, http.StatusOK)
defer srv.Close()

opts, out := newOpts(t, srv, configWithApp("APP1", "my-alias"), "", true)
require.NoError(t, runCurrentCmd(opts))

got := out.String()
assert.Contains(t, got, "APP1")
assert.Contains(t, got, "my-alias")
assert.Contains(t, got, "My App")
assert.Contains(t, got, "Grow Plus")
}

func Test_runCurrentCmd_notConfigured(t *testing.T) {
srv := newServer(t, http.StatusOK)
defer srv.Close()

opts, _ := newOpts(t, srv, configWithApp("", ""), "", true)
err := runCurrentCmd(opts)
require.Error(t, err)
assert.Contains(t, err.Error(), "no current application configured")
}

func Test_runCurrentCmd_apiFailure(t *testing.T) {
srv := newServer(t, http.StatusInternalServerError)
defer srv.Close()

opts, out := newOpts(t, srv, configWithApp("APP1", "my-alias"), "", true)
require.NoError(t, runCurrentCmd(opts))

got := out.String()
assert.Contains(t, got, "APP1")
assert.Contains(t, got, "my-alias")
assert.NotContains(t, got, "My App")
assert.Contains(t, got, "Couldn't fetch the application name and plan")
}

func Test_runCurrentCmd_signedOut(t *testing.T) {
hit := false
srv := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {
hit = true
}))
defer srv.Close()

opts, out := newOpts(t, srv, configWithApp("APP1", "my-alias"), "", false)
require.NoError(t, runCurrentCmd(opts))

got := out.String()
assert.Contains(t, got, "APP1")
assert.Contains(t, got, "my-alias")
assert.NotContains(t, got, "My App")
assert.Contains(t, got, `Sign in with "algolia auth login"`)
assert.False(t, hit, "expected no API/login call when signed out")
}

func Test_runCurrentCmd_outputJSON(t *testing.T) {
srv := newServer(t, http.StatusOK)
defer srv.Close()

opts, out := newOpts(t, srv, configWithApp("APP1", "my-alias"), "json", true)
require.NoError(t, runCurrentCmd(opts))

got := out.String()
assert.Contains(t, got, `"id":"APP1"`)
assert.Contains(t, got, `"alias":"my-alias"`)
assert.Contains(t, got, `"name":"My App"`)
assert.Contains(t, got, `"plan":"Grow Plus"`)
}
1 change: 1 addition & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ type IConfig interface {
// New model (state.toml + OS keychain).
ActiveApplicationID() string
APIKeyUUID(appID string) (string, bool)
ApplicationAlias(appID string) (string, bool)
ApplicationInState(appID string) bool
ApplicationIDByAlias(alias string) (string, bool)
SaveApplication(appID, alias, apiKeyUUID, apiKey string, setCurrent bool) error
Expand Down
8 changes: 8 additions & 0 deletions pkg/config/write.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ func (c *Config) ApplicationIDByAlias(alias string) (string, bool) {
return c.loadState().ApplicationByAlias(alias)
}

func (c *Config) ApplicationAlias(appID string) (string, bool) {
app, ok := c.loadState().Applications[appID]
if !ok || app.Alias == "" {
return "", false
}
return app.Alias, true
}

// SaveApplication persists an application's credentials in the new model.
// The keychain is written first so a failure never leaves state.toml pointing
// at a key that was not stored. Empty alias/apiKeyUUID preserve the values
Expand Down
8 changes: 8 additions & 0 deletions test/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,14 @@ func (c *ConfigStub) APIKeyUUID(appID string) (string, bool) {
return app.APIKeyUUID, true
}

func (c *ConfigStub) ApplicationAlias(appID string) (string, bool) {
app, ok := c.SavedApps[appID]
if !ok || app.Alias == "" {
return "", false
}
return app.Alias, true
}

func (c *ConfigStub) ApplicationIDByAlias(alias string) (string, bool) {
for appID, app := range c.SavedApps {
if app.Alias == alias {
Expand Down
Loading