Skip to content

Commit 036063d

Browse files
LorrisSaintGenezLorris Saint-Genez
andauthored
feat(application): add current command (#271)
Co-authored-by: Lorris Saint-Genez <lorris.saint-genez@algolia.com>
1 parent c9337e6 commit 036063d

6 files changed

Lines changed: 337 additions & 0 deletions

File tree

‎pkg/cmd/application/application.go‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"github.com/spf13/cobra"
55

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

2425
cmd.AddCommand(create.NewCreateCmd(f))
2526
cmd.AddCommand(list.NewListCmd(f))
27+
cmd.AddCommand(current.NewCurrentCmd(f))
2628
cmd.AddCommand(selectapp.NewSelectCmd(f))
2729
cmd.AddCommand(update.NewUpdateCmd(f))
2830
cmd.AddCommand(plans.NewPlansCmd(f))
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
package current
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/MakeNowJust/heredoc"
7+
"github.com/spf13/cobra"
8+
9+
"github.com/algolia/cli/api/dashboard"
10+
"github.com/algolia/cli/pkg/auth"
11+
"github.com/algolia/cli/pkg/cmdutil"
12+
"github.com/algolia/cli/pkg/config"
13+
"github.com/algolia/cli/pkg/iostreams"
14+
"github.com/algolia/cli/pkg/validators"
15+
)
16+
17+
type CurrentOptions struct {
18+
IO *iostreams.IOStreams
19+
Config config.IConfig
20+
21+
PrintFlags *cmdutil.PrintFlags
22+
23+
NewDashboardClient func(clientID string) *dashboard.Client
24+
}
25+
26+
type currentApplication struct {
27+
ID string `json:"id"`
28+
Alias string `json:"alias"`
29+
Name string `json:"name"`
30+
Plan string `json:"plan"`
31+
}
32+
33+
func NewCurrentCmd(f *cmdutil.Factory) *cobra.Command {
34+
opts := &CurrentOptions{
35+
IO: f.IOStreams,
36+
Config: f.Config,
37+
PrintFlags: cmdutil.NewPrintFlags(),
38+
NewDashboardClient: func(clientID string) *dashboard.Client {
39+
return dashboard.NewClient(clientID)
40+
},
41+
}
42+
43+
cmd := &cobra.Command{
44+
Use: "current",
45+
Short: "Show the currently selected application",
46+
Long: heredoc.Doc(`
47+
Show which Algolia application is currently selected, along with its
48+
name and plan.
49+
50+
The application ID (and its alias, when set) is shown even if the name
51+
and plan can't be fetched.
52+
`),
53+
Example: heredoc.Doc(`
54+
# Show the current application
55+
$ algolia application current
56+
57+
# Output as JSON
58+
$ algolia application current --output json
59+
`),
60+
Args: validators.NoArgs(),
61+
Annotations: map[string]string{
62+
"skipAuthCheck": "true",
63+
},
64+
RunE: func(cmd *cobra.Command, args []string) error {
65+
return runCurrentCmd(opts)
66+
},
67+
}
68+
69+
opts.PrintFlags.AddFlags(cmd)
70+
71+
return cmd
72+
}
73+
74+
func runCurrentCmd(opts *CurrentOptions) error {
75+
cs := opts.IO.ColorScheme()
76+
77+
appID, err := opts.Config.Profile().GetApplicationID()
78+
if err != nil {
79+
return fmt.Errorf(
80+
"no current application configured; run \"algolia application select\" or \"algolia auth login\" first: %w",
81+
err,
82+
)
83+
}
84+
85+
current := currentApplication{ID: appID}
86+
if alias, ok := opts.Config.ApplicationAlias(appID); ok {
87+
current.Alias = alias
88+
}
89+
90+
app, signedOut := fetchApplication(opts, appID)
91+
if app != nil {
92+
current.Name = app.Name
93+
current.Plan = app.PlanLabel
94+
}
95+
96+
if opts.PrintFlags.OutputFlagSpecified() && opts.PrintFlags.OutputFormat != nil {
97+
p, err := opts.PrintFlags.ToPrinter()
98+
if err != nil {
99+
return err
100+
}
101+
return p.Print(opts.IO, current)
102+
}
103+
104+
fmt.Fprintf(opts.IO.Out, "%s Current application: %s\n", cs.SuccessIcon(), cs.Bold(appID))
105+
if current.Alias != "" {
106+
fmt.Fprintf(opts.IO.Out, " Alias: %s\n", current.Alias)
107+
}
108+
if current.Name != "" {
109+
fmt.Fprintf(opts.IO.Out, " Name: %s\n", current.Name)
110+
}
111+
if current.Plan != "" {
112+
fmt.Fprintf(opts.IO.Out, " Plan: %s\n", current.Plan)
113+
}
114+
if current.Name == "" && current.Plan == "" {
115+
if signedOut {
116+
fmt.Fprintf(
117+
opts.IO.Out,
118+
"%s Sign in with \"algolia auth login\" to see the application name and plan.\n",
119+
cs.WarningIcon(),
120+
)
121+
} else {
122+
fmt.Fprintf(
123+
opts.IO.Out,
124+
"%s Couldn't fetch the application name and plan; showing the selected application only.\n",
125+
cs.WarningIcon(),
126+
)
127+
}
128+
}
129+
130+
return nil
131+
}
132+
133+
func fetchApplication(opts *CurrentOptions, appID string) (*dashboard.Application, bool) {
134+
client := opts.NewDashboardClient(auth.OAuthClientID())
135+
136+
token, err := auth.GetValidToken(client)
137+
if err != nil {
138+
return nil, true
139+
}
140+
141+
opts.IO.StartProgressIndicatorWithLabel("Fetching application")
142+
app, err := client.GetApplication(token, appID)
143+
opts.IO.StopProgressIndicator()
144+
if err != nil {
145+
return nil, false
146+
}
147+
148+
return app, false
149+
}
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
package current
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"net/http"
7+
"net/http/httptest"
8+
"testing"
9+
"time"
10+
11+
"github.com/stretchr/testify/assert"
12+
"github.com/stretchr/testify/require"
13+
"github.com/zalando/go-keyring"
14+
15+
"github.com/algolia/cli/api/dashboard"
16+
"github.com/algolia/cli/pkg/auth"
17+
"github.com/algolia/cli/pkg/cmdutil"
18+
"github.com/algolia/cli/pkg/config"
19+
"github.com/algolia/cli/pkg/iostreams"
20+
"github.com/algolia/cli/test"
21+
)
22+
23+
func seedToken(t *testing.T) {
24+
t.Helper()
25+
keyring.MockInit()
26+
require.NoError(t, auth.SaveToken(&dashboard.OAuthTokenResponse{
27+
AccessToken: "test-token",
28+
ExpiresIn: 3600,
29+
CreatedAt: time.Now().Unix(),
30+
}))
31+
}
32+
33+
func newServer(t *testing.T, status int) *httptest.Server {
34+
t.Helper()
35+
mux := http.NewServeMux()
36+
mux.HandleFunc("/1/application/APP1", func(w http.ResponseWriter, _ *http.Request) {
37+
if status != http.StatusOK {
38+
w.WriteHeader(status)
39+
return
40+
}
41+
require.NoError(t, json.NewEncoder(w).Encode(dashboard.SingleApplicationResponse{
42+
Data: dashboard.ApplicationResource{
43+
ID: "APP1",
44+
Type: "application",
45+
Attributes: dashboard.ApplicationAttributes{
46+
ApplicationID: "APP1",
47+
Name: "My App",
48+
Plan: dashboard.ApplicationPlan{Label: "Grow Plus"},
49+
},
50+
},
51+
}))
52+
})
53+
return httptest.NewServer(mux)
54+
}
55+
56+
func newOpts(
57+
t *testing.T,
58+
srv *httptest.Server,
59+
cfg *test.ConfigStub,
60+
output string,
61+
signedIn bool,
62+
) (*CurrentOptions, *bytes.Buffer) {
63+
t.Helper()
64+
if signedIn {
65+
seedToken(t)
66+
} else {
67+
keyring.MockInit()
68+
auth.ClearToken()
69+
}
70+
71+
io, _, stdout, _ := iostreams.Test()
72+
pf := cmdutil.NewPrintFlags()
73+
*pf.OutputFormat = output
74+
pf.OutputFlagSpecified = func() bool { return output != "" }
75+
76+
opts := &CurrentOptions{
77+
IO: io,
78+
Config: cfg,
79+
PrintFlags: pf,
80+
NewDashboardClient: func(string) *dashboard.Client {
81+
c := dashboard.NewClientWithHTTPClient("test", srv.Client())
82+
c.APIURL = srv.URL
83+
return c
84+
},
85+
}
86+
return opts, stdout
87+
}
88+
89+
func configWithApp(appID, alias string) *test.ConfigStub {
90+
cfg := &test.ConfigStub{
91+
CurrentProfile: config.Profile{ApplicationID: appID},
92+
}
93+
if alias != "" {
94+
cfg.SavedApps = map[string]test.SavedApplication{
95+
appID: {Alias: alias},
96+
}
97+
}
98+
return cfg
99+
}
100+
101+
func Test_runCurrentCmd(t *testing.T) {
102+
srv := newServer(t, http.StatusOK)
103+
defer srv.Close()
104+
105+
opts, out := newOpts(t, srv, configWithApp("APP1", "my-alias"), "", true)
106+
require.NoError(t, runCurrentCmd(opts))
107+
108+
got := out.String()
109+
assert.Contains(t, got, "APP1")
110+
assert.Contains(t, got, "my-alias")
111+
assert.Contains(t, got, "My App")
112+
assert.Contains(t, got, "Grow Plus")
113+
}
114+
115+
func Test_runCurrentCmd_notConfigured(t *testing.T) {
116+
srv := newServer(t, http.StatusOK)
117+
defer srv.Close()
118+
119+
opts, _ := newOpts(t, srv, configWithApp("", ""), "", true)
120+
err := runCurrentCmd(opts)
121+
require.Error(t, err)
122+
assert.Contains(t, err.Error(), "no current application configured")
123+
}
124+
125+
func Test_runCurrentCmd_apiFailure(t *testing.T) {
126+
srv := newServer(t, http.StatusInternalServerError)
127+
defer srv.Close()
128+
129+
opts, out := newOpts(t, srv, configWithApp("APP1", "my-alias"), "", true)
130+
require.NoError(t, runCurrentCmd(opts))
131+
132+
got := out.String()
133+
assert.Contains(t, got, "APP1")
134+
assert.Contains(t, got, "my-alias")
135+
assert.NotContains(t, got, "My App")
136+
assert.Contains(t, got, "Couldn't fetch the application name and plan")
137+
}
138+
139+
func Test_runCurrentCmd_signedOut(t *testing.T) {
140+
hit := false
141+
srv := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {
142+
hit = true
143+
}))
144+
defer srv.Close()
145+
146+
opts, out := newOpts(t, srv, configWithApp("APP1", "my-alias"), "", false)
147+
require.NoError(t, runCurrentCmd(opts))
148+
149+
got := out.String()
150+
assert.Contains(t, got, "APP1")
151+
assert.Contains(t, got, "my-alias")
152+
assert.NotContains(t, got, "My App")
153+
assert.Contains(t, got, `Sign in with "algolia auth login"`)
154+
assert.False(t, hit, "expected no API/login call when signed out")
155+
}
156+
157+
func Test_runCurrentCmd_outputJSON(t *testing.T) {
158+
srv := newServer(t, http.StatusOK)
159+
defer srv.Close()
160+
161+
opts, out := newOpts(t, srv, configWithApp("APP1", "my-alias"), "json", true)
162+
require.NoError(t, runCurrentCmd(opts))
163+
164+
got := out.String()
165+
assert.Contains(t, got, `"id":"APP1"`)
166+
assert.Contains(t, got, `"alias":"my-alias"`)
167+
assert.Contains(t, got, `"name":"My App"`)
168+
assert.Contains(t, got, `"plan":"Grow Plus"`)
169+
}

‎pkg/config/config.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ type IConfig interface {
3333
// New model (state.toml + OS keychain).
3434
ActiveApplicationID() string
3535
APIKeyUUID(appID string) (string, bool)
36+
ApplicationAlias(appID string) (string, bool)
3637
ApplicationInState(appID string) bool
3738
ApplicationIDByAlias(alias string) (string, bool)
3839
SaveApplication(appID, alias, apiKeyUUID, apiKey string, setCurrent bool) error

‎pkg/config/write.go‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,14 @@ func (c *Config) ApplicationIDByAlias(alias string) (string, bool) {
2828
return c.loadState().ApplicationByAlias(alias)
2929
}
3030

31+
func (c *Config) ApplicationAlias(appID string) (string, bool) {
32+
app, ok := c.loadState().Applications[appID]
33+
if !ok || app.Alias == "" {
34+
return "", false
35+
}
36+
return app.Alias, true
37+
}
38+
3139
// SaveApplication persists an application's credentials in the new model.
3240
// The keychain is written first so a failure never leaves state.toml pointing
3341
// at a key that was not stored. Empty alias/apiKeyUUID preserve the values

‎test/config.go‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,14 @@ func (c *ConfigStub) APIKeyUUID(appID string) (string, bool) {
160160
return app.APIKeyUUID, true
161161
}
162162

163+
func (c *ConfigStub) ApplicationAlias(appID string) (string, bool) {
164+
app, ok := c.SavedApps[appID]
165+
if !ok || app.Alias == "" {
166+
return "", false
167+
}
168+
return app.Alias, true
169+
}
170+
163171
func (c *ConfigStub) ApplicationIDByAlias(alias string) (string, bool) {
164172
for appID, app := range c.SavedApps {
165173
if app.Alias == alias {

0 commit comments

Comments
 (0)