diff --git a/internal/cli/atlas/quickstart/access_list_setup.go b/internal/cli/atlas/quickstart/access_list_setup.go index ae9828a2d1..4e78015eef 100644 --- a/internal/cli/atlas/quickstart/access_list_setup.go +++ b/internal/cli/atlas/quickstart/access_list_setup.go @@ -20,6 +20,7 @@ import ( "github.com/AlecAivazis/survey/v2" "github.com/mongodb/mongodb-atlas-cli/internal/cli" + "github.com/mongodb/mongodb-atlas-cli/internal/flag" "github.com/mongodb/mongodb-atlas-cli/internal/store" "github.com/mongodb/mongodb-atlas-cli/internal/telemetry" atlas "go.mongodb.org/atlas/mongodbatlas" @@ -35,27 +36,30 @@ func (opts *Opts) createAccessList() error { } func (opts *Opts) askAccessListOptions() error { - if len(opts.IPAddresses) > 0 { + if !opts.shouldAskForValue(flag.AccessListIP) { return nil } + message := "" + if len(opts.IPAddresses) == 0 { + publicIP := store.IPAddress() + if publicIP != "" { + message = fmt.Sprintf(" [Press Enter to use your public IP address '%s']", publicIP) + } + opts.IPAddresses = append(opts.IPAddresses, publicIP) + } fmt.Print(` [Set up your database network access details] `) - message := "" - publicIP := store.IPAddress() - if publicIP != "" { - message = fmt.Sprintf(" [Press Enter to use your public IP address '%s']", publicIP) - } err := telemetry.TrackAskOne( - newAccessListQuestion(publicIP, message), + newAccessListQuestion(strings.Join(opts.IPAddresses, ", "), message), &opts.IPAddressesResponse, survey.WithValidator(survey.Required), ) if err == nil && opts.IPAddressesResponse != "" { ips := strings.Split(opts.IPAddressesResponse, ",") - opts.IPAddresses = append(opts.IPAddresses, ips...) + opts.IPAddresses = ips } return err } diff --git a/internal/cli/atlas/quickstart/cluster_setup.go b/internal/cli/atlas/quickstart/cluster_setup.go index 0ea46c26f6..875e6382c2 100644 --- a/internal/cli/atlas/quickstart/cluster_setup.go +++ b/internal/cli/atlas/quickstart/cluster_setup.go @@ -21,6 +21,7 @@ import ( "github.com/AlecAivazis/survey/v2" "github.com/mongodb/mongodb-atlas-cli/internal/cli" + "github.com/mongodb/mongodb-atlas-cli/internal/flag" "github.com/mongodb/mongodb-atlas-cli/internal/search" "github.com/mongodb/mongodb-atlas-cli/internal/telemetry" "github.com/mongodb/mongodb-atlas-cli/internal/usage" @@ -40,16 +41,18 @@ func (opts *Opts) createCluster() error { func (opts *Opts) askClusterOptions() error { var qs []*survey.Question - if opts.ClusterName == "" { - opts.ClusterName = opts.defaultName + if opts.shouldAskForValue(flag.ClusterName) { + if opts.ClusterName == "" { + opts.ClusterName = opts.defaultName + } qs = append(qs, newClusterNameQuestion(opts.ClusterName)) } - if opts.Provider == "" { + if opts.shouldAskForValue(flag.Provider) { qs = append(qs, newClusterProviderQuestion()) } - if opts.Provider == "" || opts.ClusterName == "" || opts.Region == "" { + if opts.shouldAskForValue(flag.ClusterName) || opts.shouldAskForValue(flag.Provider) || opts.shouldAskForValue(flag.Region) { fmt.Print(` [Set up your Atlas cluster] `) @@ -60,7 +63,7 @@ func (opts *Opts) askClusterOptions() error { } // We need the provider to ask for the region - if opts.Region == "" { + if opts.shouldAskForValue(flag.Region) { return opts.askClusterRegion() } return nil diff --git a/internal/cli/atlas/quickstart/confirm_cluster_setup.go b/internal/cli/atlas/quickstart/confirm_cluster_setup.go index addb131527..7308a06f30 100644 --- a/internal/cli/atlas/quickstart/confirm_cluster_setup.go +++ b/internal/cli/atlas/quickstart/confirm_cluster_setup.go @@ -25,6 +25,8 @@ import ( const loadSampleDataMsg = ` Load sample data: Yes` +var ErrUserAborted = errors.New("user-aborted. Not creating cluster") + func (opts *Opts) askConfirmConfigQuestion() error { if opts.Confirm { return nil @@ -68,7 +70,7 @@ Allow connections from (IP Address): %s } if !opts.Confirm { - return errors.New("user-aborted. Not creating cluster") + return ErrUserAborted } return nil } diff --git a/internal/cli/atlas/quickstart/dbuser_setup.go b/internal/cli/atlas/quickstart/dbuser_setup.go index 80f7f9c0ea..d35fb204a0 100644 --- a/internal/cli/atlas/quickstart/dbuser_setup.go +++ b/internal/cli/atlas/quickstart/dbuser_setup.go @@ -18,12 +18,12 @@ import ( "errors" "fmt" - "github.com/mongodb/mongodb-atlas-cli/internal/config" - "github.com/mongodb/mongodb-atlas-cli/internal/telemetry" - "github.com/AlecAivazis/survey/v2" + "github.com/mongodb/mongodb-atlas-cli/internal/config" "github.com/mongodb/mongodb-atlas-cli/internal/convert" + "github.com/mongodb/mongodb-atlas-cli/internal/flag" "github.com/mongodb/mongodb-atlas-cli/internal/randgen" + "github.com/mongodb/mongodb-atlas-cli/internal/telemetry" atlas "go.mongodb.org/atlas/mongodbatlas" ) @@ -40,23 +40,28 @@ func (opts *Opts) askDBUserOptions() error { if opts.DBUsername == "" { opts.DBUsername = opts.defaultName + } + if opts.shouldAskForValue(flag.Username) { qs = append(qs, newDBUsernameQuestion(opts.DBUsername, opts.validateUniqueUsername)) } - if opts.DBUserPassword == "" { - pwd, err := generatePassword() - if err != nil { - return err + if opts.shouldAskForValue(flag.Password) { + if opts.DBUserPassword == "" { + pwd, err := generatePassword() + if err != nil { + return err + } + opts.DBUserPassword = pwd } - opts.DBUserPassword = pwd + minLength := 10 if config.Service() == config.CloudGovService { minLength = 12 } message := fmt.Sprintf(" [Must be >%d characters. Press Enter to use an auto-generated password]", minLength) - qs = append(qs, newDBUserPasswordQuestion(pwd, message)) + qs = append(qs, newDBUserPasswordQuestion(opts.DBUserPassword, message)) } if len(qs) == 0 { diff --git a/internal/cli/atlas/quickstart/quick_start.go b/internal/cli/atlas/quickstart/quick_start.go index f89b489f21..4843ec4e91 100644 --- a/internal/cli/atlas/quickstart/quick_start.go +++ b/internal/cli/atlas/quickstart/quick_start.go @@ -27,6 +27,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/log" @@ -37,6 +38,7 @@ import ( "github.com/mongodb/mongodb-atlas-cli/internal/usage" "github.com/mongodb/mongodb-atlas-cli/internal/validate" "github.com/spf13/cobra" + "github.com/spf13/pflag" atlas "go.mongodb.org/atlas/mongodbatlas" ) @@ -86,6 +88,8 @@ const ( type Opts struct { cli.GlobalOpts cli.WatchOpts + login auth.LoginFlow + loginOpts *auth.LoginOpts defaultName string ClusterName string Tier string @@ -104,6 +108,9 @@ type Opts struct { Confirm bool CurrentIP bool store store.AtlasClusterQuickStarter + shouldRunLogin bool + flags *pflag.FlagSet + flagSet map[string]struct{} } type quickstart struct { @@ -123,6 +130,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 @@ -131,13 +149,59 @@ 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) shouldAskForValue(f string) bool { + _, isFlagSet := opts.flagSet[f] + return !isFlagSet +} + +func (opts *Opts) trackFlags() { + if opts.flags == nil { + opts.flagSet = make(map[string]struct{}) + return + } + + opts.flagSet = make(map[string]struct{}, opts.flags.NFlag()) + opts.flags.Visit(func(f *pflag.Flag) { + opts.flagSet[f.Name] = struct{}{} + }) } func (opts *Opts) PreRun(ctx context.Context, outWriter io.Writer) error { + opts.shouldRunLogin = false opts.setTier() if opts.CurrentIP && len(opts.IPAddresses) > 0 { @@ -154,6 +218,7 @@ func (opts *Opts) Run() error { const base10 = 10 opts.defaultName = "Cluster" + strconv.FormatInt(time.Now().Unix(), base10)[5:] opts.providerAndRegionToConstant() + opts.trackFlags() if opts.CurrentIP { if publicIP := store.IPAddress(); publicIP != "" { @@ -414,23 +479,31 @@ func (opts *Opts) replaceWithDefaultSettings(values *quickstart) { } func (opts *Opts) interactiveSetup() error { - if err := opts.askClusterOptions(); err != nil { - return err - } + for { + if err := opts.askClusterOptions(); err != nil { + return err + } - if err := opts.askSampleDataQuestion(); err != nil { - return err - } + if err := opts.askSampleDataQuestion(); err != nil { + return err + } - if err := opts.askDBUserOptions(); err != nil { - return err - } + if err := opts.askDBUserOptions(); err != nil { + return err + } - if err := opts.askAccessListOptions(); err != nil { - return err - } + if err := opts.askAccessListOptions(); err != nil { + return err + } - return opts.askConfirmConfigQuestion() + if err := opts.askConfirmConfigQuestion(); err != nil && !errors.Is(err, ErrUserAborted) { + return err + } + + if opts.Confirm { + return nil + } + } } // Builder @@ -444,7 +517,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.", @@ -453,12 +526,13 @@ func Builder() *cobra.Command { $ %[1]s quickstart --force $ %[1]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 { + opts.flags = cmd.Flags() 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..6f5c6efc77 100644 --- a/internal/cli/atlas/quickstart/quick_start_test.go +++ b/internal/cli/atlas/quickstart/quick_start_test.go @@ -18,16 +18,35 @@ package quickstart import ( + "bytes" + "context" "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/spf13/pflag" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "go.mongodb.org/atlas/mongodbatlas" ) +func TestBuilder(t *testing.T) { + t.Cleanup(test.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(test.CleanupConfig) ctrl := gomock.NewController(t) mockStore := mocks.NewMockAtlasClusterQuickStarter(ctrl) defer ctrl.Finish() @@ -85,11 +104,191 @@ 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(test.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(test.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) + + 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 TestQuickstartOpts_Run_CheckFlagsSet(t *testing.T) { + t.Cleanup(test.CleanupConfig) + ctrl := gomock.NewController(t) + mockStore := mocks.NewMockAtlasClusterQuickStarter(ctrl) + defer ctrl.Finish() + + 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: true, + } + + opts.runMongoShell = true + + projectIPAccessList := opts.newProjectIPAccessList() + + 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) + + cmd := Builder() + cmd.Flags().VisitAll(func(f *pflag.Flag) { + f.Changed = false + _ = cmd.Flags().Set(f.Name, f.DefValue) + }) + + opts.flags = cmd.Flags() + + if err := opts.Run(); err != nil { + t.Fatalf("Run() unexpected error: %v", err) + } + + assert.False(t, opts.shouldAskForValue(flag.ClusterName)) + assert.False(t, opts.shouldAskForValue(flag.Region)) + assert.False(t, opts.shouldAskForValue(flag.AccessListIP)) + assert.False(t, opts.shouldAskForValue(flag.Region)) + assert.False(t, opts.shouldAskForValue(flag.Username)) + assert.False(t, opts.shouldAskForValue(flag.Password)) +} + +func setConfig() func(ctx context.Context) error { + return func(ctx context.Context) error { + config.SetOrgID("a") + config.SetProjectID("b") + config.SetService("cloud") + return nil + } } 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..4442b39de3 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(test.CleanupConfig) test.CmdValidator( t, Builder(), @@ -42,6 +43,7 @@ func TestBuilder(t *testing.T) { } func Test_setupOpts_Run(t *testing.T) { + t.Cleanup(test.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(test.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(test.CleanupConfig) ctrl := gomock.NewController(t) mockRegFlow := mocks.NewMockRegisterFlow(ctrl) mockQuickstartFlow := mocks.NewMockFlow(ctrl) diff --git a/internal/cli/auth/login.go b/internal/cli/auth/login.go index a19fd0a725..2cb56c848c 100644 --- a/internal/cli/auth/login.go +++ b/internal/cli/auth/login.go @@ -16,6 +16,7 @@ package auth import ( "context" + "errors" "fmt" "time" @@ -55,6 +56,11 @@ const ( LogoutToLoginAccountMsg = `run "atlas auth logout" first if you want to login with another Atlas account on the same Atlas CLI profile` ) +var ( + ErrProjectIDNotFound = errors.New("you don't have access to this or it doesn't exist") + ErrOrgIDNotFound = errors.New("you don't have access to this organization ID or it doesn't exist") +) + type LoginOpts struct { cli.DefaultSetterOpts AccessToken string @@ -188,6 +194,14 @@ func (opts *LoginOpts) setUpProfile(ctx context.Context) error { // Only make references to profile if user was asked about org or projects if opts.AskedOrgsOrProjects && opts.ProjectID != "" && opts.OrgID != "" { + if !opts.ProjectExists(config.ProjectID()) { + return ErrProjectIDNotFound + } + + if !opts.OrgExists(config.OrgID()) { + return ErrOrgIDNotFound + } + _, _ = fmt.Fprint(opts.OutWriter, "\nYour profile is now configured.\n") _, _ = fmt.Fprintf(opts.OutWriter, "You can use [%s config set] to change these settings at a later time.\n", config.BinName()) } diff --git a/internal/cli/auth/login_test.go b/internal/cli/auth/login_test.go index f2038beae9..59698ba806 100644 --- a/internal/cli/auth/login_test.go +++ b/internal/cli/auth/login_test.go @@ -157,6 +157,7 @@ Successfully logged in as test@10gen.com. } func TestLoginPreRun(t *testing.T) { + t.Cleanup(test.CleanupConfig) ctx := context.TODO() config.SetPublicAPIKey("public") config.SetPrivateAPIKey("private") @@ -164,6 +165,7 @@ func TestLoginPreRun(t *testing.T) { } func Test_loginOpts_oauthFlow(t *testing.T) { + t.Cleanup(test.CleanupConfig) t.Run("updates accessToken and refreshToken after code is verified", func(t *testing.T) { ctrl := gomock.NewController(t) mockFlow := mocks.NewMockAuthenticator(ctrl) diff --git a/internal/cli/auth/register_test.go b/internal/cli/auth/register_test.go index c842e50988..10fe823c4d 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(test.CleanupConfig) test.CmdValidator( t, RegisterBuilder(), @@ -43,6 +44,7 @@ func TestRegisterBuilder(t *testing.T) { } func Test_registerOpts_Run(t *testing.T) { + t.Cleanup(test.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(test.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..4bbf969ea5 --- /dev/null +++ b/internal/cli/auth/status.go @@ -0,0 +1,49 @@ +// Copyright 2022 MongoDB Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License.package auth + +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..285f319bc6 --- /dev/null +++ b/internal/cli/auth/status_test.go @@ -0,0 +1,52 @@ +// Copyright 2022 MongoDB Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package auth + +import ( + "context" + "testing" + + "github.com/mongodb/mongodb-atlas-cli/internal/config" + "github.com/mongodb/mongodb-atlas-cli/internal/test" + "github.com/stretchr/testify/assert" +) + +func Test_GetStatus_InvalidToken(t *testing.T) { + t.Cleanup(test.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(test.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(test.CleanupConfig) + ctx := context.TODO() + + status, _ := GetStatus(ctx) + assert.Equal(t, NotLoggedIn, status) +} diff --git a/internal/cli/root/atlas/builder.go b/internal/cli/root/atlas/builder.go index 01396ab0a9..4762ebf0ba 100644 --- a/internal/cli/root/atlas/builder.go +++ b/internal/cli/root/atlas/builder.go @@ -266,6 +266,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) { diff --git a/internal/telemetry/event_test.go b/internal/telemetry/event_test.go index 85145eb278..6afcece65f 100644 --- a/internal/telemetry/event_test.go +++ b/internal/telemetry/event_test.go @@ -22,12 +22,14 @@ import ( "github.com/mongodb/mongodb-atlas-cli/internal/config" "github.com/mongodb/mongodb-atlas-cli/internal/flag" + "github.com/mongodb/mongodb-atlas-cli/internal/test" "github.com/mongodb/mongodb-atlas-cli/internal/version" "github.com/spf13/cobra" "github.com/stretchr/testify/assert" ) func TestWithCommandPath(t *testing.T) { + t.Cleanup(test.CleanupConfig) config.ToolName = config.AtlasCLI testCmd := &cobra.Command{ Use: "test", @@ -44,6 +46,7 @@ func TestWithCommandPath(t *testing.T) { } func TestWithCommandPathAndAlias(t *testing.T) { + t.Cleanup(test.CleanupConfig) config.ToolName = config.AtlasCLI rootCmd := &cobra.Command{ Use: "root", @@ -63,6 +66,7 @@ func TestWithCommandPathAndAlias(t *testing.T) { } func TestWithProfileDefault(t *testing.T) { + t.Cleanup(test.CleanupConfig) config.ToolName = config.AtlasCLI e := newEvent(withProfile()) @@ -72,6 +76,7 @@ func TestWithProfileDefault(t *testing.T) { } func TestWithProfileCustom(t *testing.T) { + t.Cleanup(test.CleanupConfig) config.ToolName = config.AtlasCLI const profile = "test" @@ -85,6 +90,7 @@ func TestWithProfileCustom(t *testing.T) { } func TestWithDuration(t *testing.T) { + t.Cleanup(test.CleanupConfig) config.ToolName = config.AtlasCLI cmd := &cobra.Command{ @@ -102,6 +108,7 @@ func TestWithDuration(t *testing.T) { } func TestWithFlags(t *testing.T) { + t.Cleanup(test.CleanupConfig) config.ToolName = config.AtlasCLI cmd := &cobra.Command{ @@ -122,6 +129,7 @@ func TestWithFlags(t *testing.T) { } func TestWithVersion(t *testing.T) { + t.Cleanup(test.CleanupConfig) config.ToolName = config.AtlasCLI version.Version = "vTest" @@ -135,6 +143,7 @@ func TestWithVersion(t *testing.T) { } func TestWithOS(t *testing.T) { + t.Cleanup(test.CleanupConfig) config.ToolName = config.AtlasCLI e := newEvent(withOS()) @@ -145,6 +154,7 @@ func TestWithOS(t *testing.T) { } func TestWithAuthMethod_apiKey(t *testing.T) { + t.Cleanup(test.CleanupConfig) config.ToolName = config.AtlasCLI config.SetPublicAPIKey("test-public") @@ -157,6 +167,7 @@ func TestWithAuthMethod_apiKey(t *testing.T) { } func TestWithAuthMethod_oauth(t *testing.T) { + t.Cleanup(test.CleanupConfig) config.ToolName = config.AtlasCLI config.SetPublicAPIKey("") @@ -169,6 +180,7 @@ func TestWithAuthMethod_oauth(t *testing.T) { } func TestWithService(t *testing.T) { + t.Cleanup(test.CleanupConfig) config.ToolName = config.AtlasCLI const url = "http://host.test" @@ -183,6 +195,7 @@ func TestWithService(t *testing.T) { } func TestWithProjectID_Flag(t *testing.T) { + t.Cleanup(test.CleanupConfig) config.ToolName = config.AtlasCLI cmd := &cobra.Command{ @@ -205,6 +218,7 @@ func TestWithProjectID_Flag(t *testing.T) { } func TestWithProjectID_Config(t *testing.T) { + t.Cleanup(test.CleanupConfig) config.ToolName = config.AtlasCLI cmd := &cobra.Command{ @@ -227,6 +241,7 @@ func TestWithProjectID_Config(t *testing.T) { } func TestWithProjectID_NoFlagOrConfig(t *testing.T) { + t.Cleanup(test.CleanupConfig) config.ToolName = config.AtlasCLI cmd := &cobra.Command{ @@ -247,6 +262,7 @@ func TestWithProjectID_NoFlagOrConfig(t *testing.T) { } func TestWithOrgID_Flag(t *testing.T) { + t.Cleanup(test.CleanupConfig) config.ToolName = config.AtlasCLI cmd := &cobra.Command{ @@ -269,6 +285,7 @@ func TestWithOrgID_Flag(t *testing.T) { } func TestWithOrgID_Config(t *testing.T) { + t.Cleanup(test.CleanupConfig) config.ToolName = config.AtlasCLI cmd := &cobra.Command{ @@ -291,6 +308,7 @@ func TestWithOrgID_Config(t *testing.T) { } func TestWithOrgID_NoFlagOrConfig(t *testing.T) { + t.Cleanup(test.CleanupConfig) config.ToolName = config.AtlasCLI cmd := &cobra.Command{ @@ -311,6 +329,7 @@ func TestWithOrgID_NoFlagOrConfig(t *testing.T) { } func TestWithError(t *testing.T) { + t.Cleanup(test.CleanupConfig) config.ToolName = config.AtlasCLI e := newEvent(withError(errors.New("test"))) @@ -371,6 +390,7 @@ func TestSanitizeSelectOption(t *testing.T) { } func TestWithPrompt(t *testing.T) { + t.Cleanup(test.CleanupConfig) config.ToolName = config.AtlasCLI q := "random question" @@ -384,6 +404,7 @@ func TestWithPrompt(t *testing.T) { } func TestWithChoice(t *testing.T) { + t.Cleanup(test.CleanupConfig) config.ToolName = config.AtlasCLI c := "test choice" @@ -395,6 +416,7 @@ func TestWithChoice(t *testing.T) { } func TestWithDefault(t *testing.T) { + t.Cleanup(test.CleanupConfig) config.ToolName = config.AtlasCLI q := true @@ -406,6 +428,7 @@ func TestWithDefault(t *testing.T) { } func TestWithEmpty(t *testing.T) { + t.Cleanup(test.CleanupConfig) config.ToolName = config.AtlasCLI q := true diff --git a/internal/test/test.go b/internal/test/test.go index 26b4a068c0..06b3fafec6 100644 --- a/internal/test/test.go +++ b/internal/test/test.go @@ -17,6 +17,7 @@ package test import ( "testing" + "github.com/mongodb/mongodb-atlas-cli/internal/config" "github.com/spf13/cobra" "github.com/stretchr/testify/assert" ) @@ -38,3 +39,11 @@ func CmdValidator(t *testing.T, subject *cobra.Command, nSubCommands int, flags a.NotNilf(subject.Flags().Lookup(f), "command has no flag: %s", f) } } + +func CleanupConfig() { + config.SetAccessToken("") + config.SetPublicAPIKey("") + config.SetPrivateAPIKey("") + config.SetOrgID("") + config.SetProjectID("") +}