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
20 changes: 5 additions & 15 deletions pkg/cmd/application/current/current.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/algolia/cli/api/dashboard"
"github.com/algolia/cli/pkg/auth"
"github.com/algolia/cli/pkg/cmd/shared/apputil"
"github.com/algolia/cli/pkg/cmdutil"
"github.com/algolia/cli/pkg/config"
"github.com/algolia/cli/pkg/iostreams"
Expand All @@ -23,13 +24,6 @@ type CurrentOptions struct {
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,
Expand Down Expand Up @@ -82,16 +76,12 @@ func runCurrentCmd(opts *CurrentOptions) error {
)
}

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

// The ID and alias are shown even when the name and plan can't be fetched.
app, signedOut := fetchApplication(opts, appID)
if app != nil {
current.Name = app.Name
current.Plan = app.PlanLabel
if app == nil {
app = &dashboard.Application{ID: appID}
}
current := apputil.NewApplicationOutput(opts.Config, app)

if opts.PrintFlags.OutputFlagSpecified() && opts.PrintFlags.OutputFormat != nil {
p, err := opts.PrintFlags.ToPrinter()
Expand Down
56 changes: 52 additions & 4 deletions pkg/cmd/application/selectapp/select.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,20 @@ type SelectOptions struct {
AppID string
AppName string

// NonInteractive disables every prompt and defaults the output to JSON, so
// the command is usable from scripts.
NonInteractive bool

PrintFlags *cmdutil.PrintFlags

NewDashboardClient func(clientID string) *dashboard.Client
}

func NewSelectCmd(f *cmdutil.Factory) *cobra.Command {
opts := &SelectOptions{
IO: f.IOStreams,
Config: f.Config,
IO: f.IOStreams,
Config: f.Config,
PrintFlags: cmdutil.NewPrintFlags(),
NewDashboardClient: func(clientID string) *dashboard.Client {
return dashboard.NewClient(clientID)
},
Expand All @@ -53,15 +60,31 @@ func NewSelectCmd(f *cmdutil.Factory) *cobra.Command {

# Select by application ID (non-interactive)
$ algolia application select --app-id "ABCDEF1234"

# Select from a script: no prompts, JSON on stdout
$ algolia application select --non-interactive --app-id "ABCDEF1234"
`),
Aliases: []string{"use"},
Args: validators.NoArgs(),
Annotations: map[string]string{
"skipAuthCheck": "true",
},
RunE: func(cmd *cobra.Command, args []string) error {
_, err := runSelectCmd(opts)
return err
if opts.NonInteractive {
cmdutil.ApplyNonInteractive(opts.IO, opts.PrintFlags)
}

// Fail before authenticating: with no selector there is nothing to pick.
if opts.NonInteractive && opts.AppID == "" && opts.AppName == "" {
return fmt.Errorf("--app-id or --app-name is required in non-interactive mode")
}

app, err := runSelectCmd(opts)
if err != nil {
return err
}

return printSelection(opts, app)
},
}

Expand All @@ -70,10 +93,29 @@ func NewSelectCmd(f *cmdutil.Factory) *cobra.Command {
cmd.Flags().
StringVar(&opts.AppName, "app-name", "", "Select application by name (non-interactive)")
cmd.MarkFlagsMutuallyExclusive("app-id", "app-name")
cmd.Flags().
BoolVar(&opts.NonInteractive, "non-interactive", false, "Never prompt; output JSON unless --output is set (requires --app-id or --app-name)")
opts.PrintFlags.AddFlags(cmd)

return cmd
}

// printSelection emits the structured document once the flow is done. The
// human-readable flow output has already been written to stderr by then.
func printSelection(opts *SelectOptions, app *dashboard.Application) error {
if !opts.PrintFlags.HasStructuredOutput() {
return nil
}

if app == nil {
return fmt.Errorf(
"no applications found; create one with \"algolia application create\"",
)
}

return opts.PrintFlags.Print(opts.IO, apputil.NewApplicationOutput(opts.Config, app))
}

// Run executes the interactive application-selection flow and returns the
// chosen application. Other commands (e.g. open) use it to ensure an
// application is selected before proceeding. A nil application is returned
Expand All @@ -91,6 +133,12 @@ func Run(f *cmdutil.Factory) (*dashboard.Application, error) {
}

func runSelectCmd(opts *SelectOptions) (*dashboard.Application, error) {
// Move the progress narration to stderr so stdout carries the JSON document
// only.
if opts.PrintFlags.HasStructuredOutput() {
defer cmdutil.RedirectHumanOutput(opts.IO)()
}

cs := opts.IO.ColorScheme()
client := opts.NewDashboardClient(auth.OAuthClientID())

Expand Down
62 changes: 62 additions & 0 deletions pkg/cmd/application/selectapp/select_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package selectapp

import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
Expand All @@ -13,6 +14,8 @@ import (

"github.com/algolia/cli/api/dashboard"
"github.com/algolia/cli/pkg/auth"
"github.com/algolia/cli/pkg/cmd/shared/apputil"
"github.com/algolia/cli/pkg/cmdutil"
"github.com/algolia/cli/pkg/iostreams"
"github.com/algolia/cli/pkg/keychain"
"github.com/algolia/cli/test"
Expand Down Expand Up @@ -93,6 +96,65 @@ func newSelectOptsWithSelector(
return opts
}

func Test_runSelectCmd_NonInteractiveWritesJSONOnlyToStdout(t *testing.T) {
createHit := false
srv := selectServer(t, &createHit)
defer srv.Close()

cfg := &test.ConfigStub{}
opts := newSelectOpts(t, srv, cfg)
opts.NonInteractive = true
opts.PrintFlags = cmdutil.NewPrintFlags()
// Mirrors what the command does before running.
cmdutil.ApplyNonInteractive(opts.IO, opts.PrintFlags)

stdout, stderr := captureOutput(t, opts.IO)

app, err := runSelectCmd(opts)
require.NoError(t, err)
require.NoError(t, printSelection(opts, app))

var got apputil.ApplicationOutput
require.NoError(t, json.Unmarshal(stdout.Bytes(), &got), "stdout: %q", stdout.String())
assert.Equal(t, apputil.ApplicationOutput{
ID: "APP1",
Alias: "my app",
Name: "My App",
}, got)
assert.NotContains(t, stdout.String(), "API key")
assert.NotContains(t, stdout.String(), "new-key")

assert.Contains(t, stderr.String(), "API key generated for application APP1")
assert.NotContains(t, stderr.String(), "new-key")
}

func TestNewSelectCmd_NonInteractiveRequiresSelector(t *testing.T) {
f, inOut := test.NewFactory(true, nil, &test.ConfigStub{}, "")

_, err := test.Execute(NewSelectCmd(f), "--non-interactive", inOut)
assert.ErrorContains(t, err, "--app-id or --app-name is required in non-interactive mode")
assert.Empty(t, inOut.OutBuf.String())
}

func Test_printSelection_NoApplication(t *testing.T) {
io, _, _, _ := iostreams.Test()
opts := &SelectOptions{IO: io, PrintFlags: cmdutil.NewPrintFlags(), NonInteractive: true}
cmdutil.ApplyNonInteractive(opts.IO, opts.PrintFlags)

assert.ErrorContains(t, printSelection(opts, nil), "no applications found")
}

// captureOutput swaps the test streams for buffers we can assert on separately.
func captureOutput(t *testing.T, io *iostreams.IOStreams) (*bytes.Buffer, *bytes.Buffer) {
t.Helper()
stdout := &bytes.Buffer{}
stderr := &bytes.Buffer{}
io.Out = stdout
io.ErrOut = stderr

return stdout, stderr
}

func Test_runSelectCmd_RegeneratesKeyWhenNoUUID(t *testing.T) {
createHit := false
srv := selectServer(t, &createHit)
Expand Down
Loading
Loading