diff --git a/internal/cli/atlas/quickstart/quick_start.go b/internal/cli/atlas/quickstart/quick_start.go index bd956b72fe..675a884161 100644 --- a/internal/cli/atlas/quickstart/quick_start.go +++ b/internal/cli/atlas/quickstart/quick_start.go @@ -28,6 +28,7 @@ import ( "github.com/AlecAivazis/survey/v2" "github.com/mongodb/mongodb-atlas-cli/internal/cli" + "github.com/mongodb/mongodb-atlas-cli/internal/cli/auth" "github.com/mongodb/mongodb-atlas-cli/internal/config" "github.com/mongodb/mongodb-atlas-cli/internal/flag" "github.com/mongodb/mongodb-atlas-cli/internal/mongosh" @@ -85,6 +86,8 @@ const ( type Opts struct { cli.GlobalOpts cli.WatchOpts + login auth.LoginFlow + loginOpts *auth.LoginOpts defaultName string ClusterName string Tier string @@ -103,6 +106,7 @@ type Opts struct { Confirm bool CurrentIP bool store store.AtlasClusterQuickStarter + shouldRunLogin bool } type quickstart struct { @@ -122,6 +126,17 @@ type Flow interface { Run() error } +func NewQuickstartFlow(qsOpts *Opts) Flow { + return qsOpts +} + +func NewQuickstartOpts(loginOpts *auth.LoginOpts) *Opts { + return &Opts{ + loginOpts: loginOpts, + login: auth.NewLoginFlow(loginOpts), + } +} + func (opts *Opts) initStore(ctx context.Context) func() error { return func() error { var err error @@ -130,13 +145,42 @@ func (opts *Opts) initStore(ctx context.Context) func() error { } } -func (opts *Opts) quickstartPreRun() error { - return opts.PreRunE( - opts.ValidateProjectID, - ) +func (opts *Opts) quickstartPreRun(ctx context.Context, outWriter io.Writer) error { + opts.shouldRunLogin = false + opts.OutWriter = outWriter + + // Get authentication status to define whether login should be run + status, _ := auth.GetStatus(ctx) + if status == auth.LoggedInWithValidToken || status == auth.LoggedInWithAPIKeys { + return opts.PreRunE( + opts.ValidateProjectID, + ) + } + + // If customer used --force and is not authenticated, check credentials and proceed. Likely to + // throw an error here. + if opts.Confirm { + if err := validate.Credentials(); err != nil { + return err + } + return opts.PreRunE( + opts.ValidateProjectID, + ) + } + + opts.loginOpts.OutWriter = opts.OutWriter + if err := opts.login.PreRun(); err != nil { + return err + } + + opts.shouldRunLogin = true + _, _ = fmt.Fprintf(opts.OutWriter, `This action requires authentication. +`) + return opts.login.Run(ctx) } func (opts *Opts) PreRun(ctx context.Context, outWriter io.Writer) error { + opts.shouldRunLogin = false opts.setTier() if opts.CurrentIP && len(opts.IPAddresses) > 0 { @@ -443,7 +487,7 @@ func (opts *Opts) interactiveSetup() error { // [--skipMongosh skipMongosh] // [--default] func Builder() *cobra.Command { - opts := &Opts{} + opts := NewQuickstartOpts(auth.NewLoginOpts()) cmd := &cobra.Command{ Use: "quickstart", Short: "Create and access an Atlas Cluster.", @@ -451,10 +495,10 @@ func Builder() *cobra.Command { Example: fmt.Sprintf(` Skip setting cluster name, provider or database username by using the command options: $ %s quickstart --clusterName Test --provider GCP --username dbuserTest`, cli.ExampleAtlasEntryPoint()), PreRunE: func(cmd *cobra.Command, args []string) error { - if err := opts.quickstartPreRun(); err != nil { + if err := opts.PreRun(cmd.Context(), cmd.OutOrStdout()); err != nil { return err } - return opts.PreRun(cmd.Context(), cmd.OutOrStdout()) + return opts.quickstartPreRun(cmd.Context(), cmd.OutOrStdout()) }, RunE: func(cmd *cobra.Command, args []string) error { return opts.Run() diff --git a/internal/cli/atlas/quickstart/quick_start_test.go b/internal/cli/atlas/quickstart/quick_start_test.go index 633e80ba56..c311802572 100644 --- a/internal/cli/atlas/quickstart/quick_start_test.go +++ b/internal/cli/atlas/quickstart/quick_start_test.go @@ -18,16 +18,34 @@ package quickstart import ( + "bytes" + "context" + "fmt" "testing" "github.com/golang/mock/gomock" + "github.com/mongodb/mongodb-atlas-cli/internal/cli/auth" + "github.com/mongodb/mongodb-atlas-cli/internal/config" "github.com/mongodb/mongodb-atlas-cli/internal/flag" "github.com/mongodb/mongodb-atlas-cli/internal/mocks" "github.com/mongodb/mongodb-atlas-cli/internal/test" + "github.com/mongodb/mongodb-atlas-cli/internal/validate" + "github.com/stretchr/testify/require" "go.mongodb.org/atlas/mongodbatlas" ) +func TestBuilder(t *testing.T) { + t.Cleanup(cleanUpConfig) + test.CmdValidator( + t, + Builder(), + 0, + []string{flag.ProjectID, flag.Region, flag.ClusterName, flag.Provider, flag.AccessListIP, flag.Username, flag.Password, flag.SkipMongosh, flag.SkipSampleData}, + ) +} + func TestQuickstartOpts_Run(t *testing.T) { + t.Cleanup(cleanUpConfig) ctrl := gomock.NewController(t) mockStore := mocks.NewMockAtlasClusterQuickStarter(ctrl) defer ctrl.Finish() @@ -85,11 +103,125 @@ func TestQuickstartOpts_Run(t *testing.T) { } } -func TestBuilder(t *testing.T) { - test.CmdValidator( - t, - Builder(), - 0, - []string{flag.ProjectID, flag.Region, flag.ClusterName, flag.Provider, flag.AccessListIP, flag.Username, flag.Password, flag.SkipMongosh, flag.SkipSampleData}, - ) +func TestQuickstartOpts_Run_NotLoggedIn(t *testing.T) { + t.Cleanup(cleanUpConfig) + ctrl := gomock.NewController(t) + mockStore := mocks.NewMockAtlasClusterQuickStarter(ctrl) + defer ctrl.Finish() + buf := new(bytes.Buffer) + ctx := context.TODO() + opts := &Opts{ + ClusterName: "ProjectBar", + Region: "US", + store: mockStore, + IPAddresses: []string{"0.0.0.0"}, + DBUsername: "user", + DBUserPassword: "test", + Provider: "AWS", + SkipMongosh: true, + SkipSampleData: true, + Confirm: true, + } + + opts.runMongoShell = true + require.Error(t, validate.ErrMissingCredentials, opts.quickstartPreRun(ctx, buf)) +} + +func TestQuickstartOpts_Run_NeedLogin_ForceAfterLogin(t *testing.T) { + t.Cleanup(cleanUpConfig) + ctrl := gomock.NewController(t) + mockStore := mocks.NewMockAtlasClusterQuickStarter(ctrl) + mockLoginFlow := mocks.NewMockLoginFlow(ctrl) + defer ctrl.Finish() + + ctx := context.TODO() + buf := new(bytes.Buffer) + + expectedCluster := &mongodbatlas.AdvancedCluster{ + StateName: "IDLE", + ConnectionStrings: &mongodbatlas.ConnectionStrings{ + StandardSrv: "", + }, + } + + expectedDBUser := &mongodbatlas.DatabaseUser{} + + var expectedProjectAccessLists *mongodbatlas.ProjectIPAccessLists + + opts := &Opts{ + ClusterName: "ProjectBar", + Region: "US", + store: mockStore, + IPAddresses: []string{"0.0.0.0"}, + DBUsername: "user", + DBUserPassword: "test", + Provider: "AWS", + SkipMongosh: true, + SkipSampleData: true, + Confirm: false, + login: mockLoginFlow, + loginOpts: auth.NewLoginOpts(), + } + + opts.runMongoShell = true + setConfig() + projectIPAccessList := opts.newProjectIPAccessList() + + mockLoginFlow. + EXPECT(). + PreRun(). + Return(nil). + Times(1) + + mockLoginFlow. + EXPECT(). + Run(ctx). + Return(nil). + Times(1) + + mockStore. + EXPECT(). + CreateCluster(opts.newCluster()).Return(expectedCluster, nil). + Times(1) + + mockStore. + EXPECT(). + CreateProjectIPAccessList(projectIPAccessList).Return(expectedProjectAccessLists, nil). + Times(1) + + mockStore. + EXPECT(). + AtlasCluster(opts.ConfigProjectID(), opts.ClusterName).Return(expectedCluster, nil). + Times(2) + + mockStore. + EXPECT(). + CreateDatabaseUser(opts.newDatabaseUser()).Return(expectedDBUser, nil). + Times(1) + + fmt.Println(config.Service()) + + if err := opts.quickstartPreRun(ctx, buf); err != nil { + t.Fatalf("Run() unexpected error: %v", err) + } + opts.Confirm = true + + if err := opts.Run(); err != nil { + t.Fatalf("Run() unexpected error: %v", err) + } +} + +func setConfig() func(ctx context.Context) error { + return func(ctx context.Context) error { + config.SetOrgID("a") + config.SetProjectID("b") + config.SetService("cloud") + return nil + } +} + +func cleanUpConfig() { + config.SetAccessToken("") + config.SetPublicAPIKey("") + config.SetPrivateAPIKey("") } diff --git a/internal/cli/atlas/setup/setup_cmd.go b/internal/cli/atlas/setup/setup_cmd.go index feceef19db..4dc15d51cb 100644 --- a/internal/cli/atlas/setup/setup_cmd.go +++ b/internal/cli/atlas/setup/setup_cmd.go @@ -22,10 +22,10 @@ import ( "github.com/mongodb/mongodb-atlas-cli/internal/cli" "github.com/mongodb/mongodb-atlas-cli/internal/cli/atlas/quickstart" "github.com/mongodb/mongodb-atlas-cli/internal/cli/auth" + "github.com/mongodb/mongodb-atlas-cli/internal/cli/require" "github.com/mongodb/mongodb-atlas-cli/internal/config" "github.com/mongodb/mongodb-atlas-cli/internal/flag" "github.com/mongodb/mongodb-atlas-cli/internal/usage" - "github.com/mongodb/mongodb-atlas-cli/internal/validate" "github.com/spf13/cobra" ) @@ -82,32 +82,30 @@ This command will help you } func (opts *Opts) PreRun(ctx context.Context) error { - opts.skipRegister = false + opts.skipRegister = true opts.skipLogin = true - if config.PublicAPIKey() != "" && config.PrivateAPIKey() != "" { - opts.skipRegister = true + status, _ := auth.GetStatus(ctx) + switch status { + case auth.LoggedInWithAPIKeys: msg := fmt.Sprintf(auth.AlreadyAuthenticatedMsg, config.PublicAPIKey()) _, _ = fmt.Fprintf(opts.OutWriter, ` %s %s `, msg, withProfileMsg) - } - - if account, err := auth.AccountWithAccessToken(); err == nil { - opts.skipRegister = true + case auth.LoggedInWithValidToken: + account, _ := auth.AccountWithAccessToken() msg := fmt.Sprintf(auth.AlreadyAuthenticatedEmailMsg, account) - // token exists but it is not refreshed - if err := cli.RefreshToken(ctx); err != nil || validate.Token() != nil { - opts.skipLogin = false - return nil - } - _, _ = fmt.Fprintf(opts.OutWriter, `%s %s `, msg, withProfileMsg) + case auth.LoggedInWithInvalidToken: + opts.skipLogin = false + case auth.NotLoggedIn: + opts.skipRegister = false + default: } return nil @@ -123,11 +121,11 @@ func (opts *Opts) PreRun(ctx context.Context) error { // [--skipMongosh skipMongosh] func Builder() *cobra.Command { loginOpts := auth.NewLoginOpts() - qsOpts := &quickstart.Opts{} + qsOpts := quickstart.NewQuickstartOpts(loginOpts) opts := &Opts{ register: auth.NewRegisterFlow(loginOpts), login: auth.NewLoginFlow(loginOpts), - quickstart: qsOpts, + quickstart: quickstart.NewQuickstartFlow(qsOpts), } cmd := &cobra.Command{ @@ -137,6 +135,7 @@ func Builder() *cobra.Command { Example: ` Override default cluster settings like name, provider, or database username by using the command options $ atlas setup --clusterName Test --provider GCP --username dbuserTest`, Hidden: false, + Args: require.NoArgs, PreRunE: func(cmd *cobra.Command, args []string) error { opts.OutWriter = cmd.OutOrStdout() // setup pre run diff --git a/internal/cli/atlas/setup/setup_cmd_test.go b/internal/cli/atlas/setup/setup_cmd_test.go index ccd8cafaf5..ee096bc44f 100644 --- a/internal/cli/atlas/setup/setup_cmd_test.go +++ b/internal/cli/atlas/setup/setup_cmd_test.go @@ -33,6 +33,7 @@ import ( ) func TestBuilder(t *testing.T) { + t.Cleanup(cleanUpConfig) test.CmdValidator( t, Builder(), @@ -42,6 +43,7 @@ func TestBuilder(t *testing.T) { } func Test_setupOpts_Run(t *testing.T) { + t.Cleanup(cleanUpConfig) ctrl := gomock.NewController(t) mockRegFlow := mocks.NewMockRegisterFlow(ctrl) mockQuickstartFlow := mocks.NewMockFlow(ctrl) @@ -80,6 +82,7 @@ func Test_setupOpts_Run(t *testing.T) { } func Test_setupOpts_RunWithAPIKeys(t *testing.T) { + t.Cleanup(cleanUpConfig) ctrl := gomock.NewController(t) mockRegFlow := mocks.NewMockRegisterFlow(ctrl) mockQuickstartFlow := mocks.NewMockFlow(ctrl) @@ -95,18 +98,18 @@ func Test_setupOpts_RunWithAPIKeys(t *testing.T) { config.SetPublicAPIKey("publicKey") config.SetPrivateAPIKey("privateKey") + _ = setConfig()(ctx) opts.OutWriter = buf - mockQuickstartFlow. EXPECT(). - Run(). + PreRun(ctx, buf). Return(nil). Times(1) mockQuickstartFlow. EXPECT(). - PreRun(ctx, buf). + Run(). Return(nil). Times(1) @@ -120,6 +123,7 @@ Run "atlas auth setup --profile " to create a new Atlas account on } func Test_setupOpts_RunSkipRegister(t *testing.T) { + t.Cleanup(cleanUpConfig) ctrl := gomock.NewController(t) mockRegFlow := mocks.NewMockRegisterFlow(ctrl) mockQuickstartFlow := mocks.NewMockFlow(ctrl) @@ -170,3 +174,9 @@ func setConfig() func(ctx context.Context) error { return nil } } + +func cleanUpConfig() { + config.SetAccessToken("") + config.SetPublicAPIKey("") + config.SetPrivateAPIKey("") +} diff --git a/internal/cli/auth/register_test.go b/internal/cli/auth/register_test.go index c842e50988..5162d346dd 100644 --- a/internal/cli/auth/register_test.go +++ b/internal/cli/auth/register_test.go @@ -34,6 +34,7 @@ import ( ) func TestRegisterBuilder(t *testing.T) { + t.Cleanup(cleanUpConfig) test.CmdValidator( t, RegisterBuilder(), @@ -43,6 +44,7 @@ func TestRegisterBuilder(t *testing.T) { } func Test_registerOpts_Run(t *testing.T) { + t.Cleanup(cleanUpConfig) ctrl := gomock.NewController(t) mockFlow := mocks.NewMockAuthenticator(ctrl) mockRegisterFlow := ®isterAuthenticator{ @@ -129,6 +131,7 @@ Successfully logged in as test@10gen.com. } func TestRegisterPreRun(t *testing.T) { + t.Cleanup(cleanUpConfig) config.SetPublicAPIKey("public") config.SetPrivateAPIKey("private") require.ErrorContains(t, registerPreRun(), fmt.Sprintf(AlreadyAuthenticatedError, "public"), WithProfileMsg) diff --git a/internal/cli/auth/status.go b/internal/cli/auth/status.go new file mode 100644 index 0000000000..62592fbf66 --- /dev/null +++ b/internal/cli/auth/status.go @@ -0,0 +1,35 @@ +package auth + +import ( + "context" + + "github.com/mongodb/mongodb-atlas-cli/internal/cli" + "github.com/mongodb/mongodb-atlas-cli/internal/config" + "github.com/mongodb/mongodb-atlas-cli/internal/validate" +) + +const ( + _ = iota // ignore first value by assigning to blank identifier + LoggedInWithValidToken + LoggedInWithInvalidToken + LoggedInWithAPIKeys + NotLoggedIn +) + +// GetStatus get user authentication status. +func GetStatus(ctx context.Context) (int, error) { + var err error + + if config.PublicAPIKey() != "" && config.PrivateAPIKey() != "" { + return LoggedInWithAPIKeys, nil + } + if _, err = AccountWithAccessToken(); err == nil { + // token exists but it is not refreshed + if err = cli.RefreshToken(ctx); err != nil || validate.Token() != nil { + return LoggedInWithInvalidToken, nil + } + return LoggedInWithValidToken, nil + } + + return NotLoggedIn, err +} diff --git a/internal/cli/auth/status_test.go b/internal/cli/auth/status_test.go new file mode 100644 index 0000000000..65283ebfc8 --- /dev/null +++ b/internal/cli/auth/status_test.go @@ -0,0 +1,43 @@ +package auth + +import ( + "context" + "testing" + + "github.com/mongodb/mongodb-atlas-cli/internal/config" + "github.com/stretchr/testify/assert" +) + +func Test_GetStatus_InvalidToken(t *testing.T) { + t.Cleanup(cleanUpConfig) + ctx := context.TODO() + config.SetAccessToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ") + + status, _ := GetStatus(ctx) + assert.Equal(t, LoggedInWithInvalidToken, status) +} + +func Test_GetStatus_APIKeys(t *testing.T) { + t.Cleanup(cleanUpConfig) + ctx := context.TODO() + + config.SetPublicAPIKey("publicKey") + config.SetPrivateAPIKey("privateKey") + + status, _ := GetStatus(ctx) + assert.Equal(t, LoggedInWithAPIKeys, status) +} + +func Test_GetStatus_NotLoggedIn(t *testing.T) { + t.Cleanup(cleanUpConfig) + ctx := context.TODO() + + status, _ := GetStatus(ctx) + assert.Equal(t, NotLoggedIn, status) +} + +func cleanUpConfig() { + config.SetAccessToken("") + config.SetPublicAPIKey("") + config.SetPrivateAPIKey("") +} diff --git a/internal/cli/root/atlas/builder.go b/internal/cli/root/atlas/builder.go index f00e042208..e5e2c9f9d1 100644 --- a/internal/cli/root/atlas/builder.go +++ b/internal/cli/root/atlas/builder.go @@ -227,6 +227,7 @@ func shouldCheckCredentials(cmd *cobra.Command) bool { fmt.Sprintf("%s %s", atlas, "login"), // user wants to set credentials fmt.Sprintf("%s %s", atlas, "setup"), // user wants to set credentials fmt.Sprintf("%s %s", atlas, "register"), // user wants to set credentials + fmt.Sprintf("%s %s", atlas, "quickstart"), // command supports login } for _, p := range searchByPath { if strings.HasPrefix(cmd.CommandPath(), p) {