diff --git a/go.mod b/go.mod index d295ed6747..34218173eb 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,7 @@ require ( github.com/spf13/viper v1.10.1 github.com/stretchr/testify v1.7.0 github.com/tangzero/inflector v1.0.0 - go.mongodb.org/atlas v0.14.1-0.20220126105350-5816bca2f88c + go.mongodb.org/atlas v0.14.1-0.20220202080947-4a7d97e77246 go.mongodb.org/ops-manager v0.34.0 gopkg.in/yaml.v2 v2.4.0 ) diff --git a/go.sum b/go.sum index 12a7e90c87..2f5fcb8a65 100644 --- a/go.sum +++ b/go.sum @@ -401,8 +401,8 @@ go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQc go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs= go.mongodb.org/atlas v0.14.0/go.mod h1:lQhRHIxc6jQHEK3/q9WLu/SdBkPj2fQYhjLGUF6Z3U8= -go.mongodb.org/atlas v0.14.1-0.20220126105350-5816bca2f88c h1:K3Q2ggsg1XtJuurBbJspNRzmkPtIZl5lD1oICR6lW28= -go.mongodb.org/atlas v0.14.1-0.20220126105350-5816bca2f88c/go.mod h1:lQhRHIxc6jQHEK3/q9WLu/SdBkPj2fQYhjLGUF6Z3U8= +go.mongodb.org/atlas v0.14.1-0.20220202080947-4a7d97e77246 h1:BkSHgSH3o6KawlMdsnR+XpZ/pCDbWK5secDmcUmytL0= +go.mongodb.org/atlas v0.14.1-0.20220202080947-4a7d97e77246/go.mod h1:lQhRHIxc6jQHEK3/q9WLu/SdBkPj2fQYhjLGUF6Z3U8= go.mongodb.org/ops-manager v0.34.0 h1:d8TgpJpPFeVLr+6HrAbbbYnKkdk+2JW6E20OBdgqXg8= go.mongodb.org/ops-manager v0.34.0/go.mod h1:85LPPdME1TFJ/Eau/IgOmy37YWGw1p/S8PBSME8ukXs= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= diff --git a/internal/cli/auth/login.go b/internal/cli/auth/login.go index 67b1b1a7f7..013b3f99ed 100644 --- a/internal/cli/auth/login.go +++ b/internal/cli/auth/login.go @@ -228,6 +228,7 @@ func Builder() *cobra.Command { } cmd.AddCommand( LoginBuilder(), + LogoutBuilder(), ) return cmd diff --git a/internal/cli/auth/login_test.go b/internal/cli/auth/login_test.go index a0596df49f..2b584671fe 100644 --- a/internal/cli/auth/login_test.go +++ b/internal/cli/auth/login_test.go @@ -35,7 +35,7 @@ func TestBuilder(t *testing.T) { test.CmdValidator( t, Builder(), - 1, + 2, []string{}, ) } diff --git a/internal/cli/auth/logout.go b/internal/cli/auth/logout.go new file mode 100644 index 0000000000..743504d5c5 --- /dev/null +++ b/internal/cli/auth/logout.go @@ -0,0 +1,95 @@ +// 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" + "errors" + "io" + + "github.com/mongodb/mongocli/internal/cli" + "github.com/mongodb/mongocli/internal/cli/require" + "github.com/mongodb/mongocli/internal/config" + "github.com/mongodb/mongocli/internal/flag" + "github.com/mongodb/mongocli/internal/oauth" + "github.com/mongodb/mongocli/internal/usage" + "github.com/spf13/cobra" + atlas "go.mongodb.org/atlas/mongodbatlas" +) + +type logoutOpts struct { + *cli.DeleteOpts + OutWriter io.Writer + config ConfigDeleter + flow Revoker +} + +//go:generate mockgen -destination=../../mocks/mock_logout.go -package=mocks github.com/mongodb/mongocli/internal/cli/auth Revoker,ConfigDeleter + +type ConfigDeleter interface { + Delete() error +} + +type Revoker interface { + Revoke(context.Context, string, string) (*atlas.Response, error) +} + +func (opts *logoutOpts) initFlow() error { + var err error + opts.flow, err = oauth.FlowWithConfig(config.Default()) + return err +} + +func (opts *logoutOpts) Run(ctx context.Context) error { + // revoking a refresh token revokes the access token + if _, err := opts.flow.Revoke(ctx, config.RefreshToken(), "refresh_token"); err != nil { + return err + } + + return opts.Delete(opts.config.Delete) +} + +func LogoutBuilder() *cobra.Command { + opts := &logoutOpts{ + DeleteOpts: cli.NewDeleteOpts("Successfully logged out\n", " "), + } + + cmd := &cobra.Command{ + Use: "logout", + Short: "Log out the CLI.", + Example: ` To log out from the CLI: + $ mongocli auth logout +`, + PreRunE: func(cmd *cobra.Command, args []string) error { + opts.OutWriter = cmd.OutOrStdout() + opts.config = config.Default() + return opts.initFlow() + }, + RunE: func(cmd *cobra.Command, args []string) error { + if config.RefreshToken() == "" { + return errors.New("not logged in") + } + if err := opts.PromptWithMessage("Are you sure you want to log out?"); err != nil { + return err + } + return opts.Run(cmd.Context()) + }, + Args: require.NoArgs, + } + + cmd.Flags().BoolVar(&opts.Confirm, flag.Force, false, usage.Force) + + return cmd +} diff --git a/internal/cli/auth/logout_test.go b/internal/cli/auth/logout_test.go new file mode 100644 index 0000000000..27c10ff949 --- /dev/null +++ b/internal/cli/auth/logout_test.go @@ -0,0 +1,69 @@ +// 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. + +//go:build unit +// +build unit + +package auth + +import ( + "bytes" + "context" + "testing" + + "github.com/golang/mock/gomock" + "github.com/mongodb/mongocli/internal/cli" + "github.com/mongodb/mongocli/internal/flag" + "github.com/mongodb/mongocli/internal/mocks" + "github.com/mongodb/mongocli/internal/test" + "github.com/stretchr/testify/require" +) + +func TestLogoutBuilder(t *testing.T) { + test.CmdValidator( + t, + LogoutBuilder(), + 0, + []string{flag.Force}, + ) +} + +func Test_logoutOpts_Run(t *testing.T) { + ctrl := gomock.NewController(t) + mockFlow := mocks.NewMockRevoker(ctrl) + mockConfig := mocks.NewMockConfigDeleter(ctrl) + defer ctrl.Finish() + buf := new(bytes.Buffer) + + opts := logoutOpts{ + OutWriter: buf, + config: mockConfig, + flow: mockFlow, + DeleteOpts: &cli.DeleteOpts{ + Confirm: true, + }, + } + ctx := context.TODO() + mockFlow. + EXPECT(). + Revoke(ctx, gomock.Any(), gomock.Any()). + Return(nil, nil). + Times(1) + mockConfig. + EXPECT(). + Delete(). + Return(nil). + Times(1) + require.NoError(t, opts.Run(ctx)) +} diff --git a/internal/cli/delete_opts.go b/internal/cli/delete_opts.go index 3b0696bad1..b4ed230a91 100644 --- a/internal/cli/delete_opts.go +++ b/internal/cli/delete_opts.go @@ -54,6 +54,8 @@ func (opts *DeleteOpts) Delete(d interface{}, a ...string) error { var err error switch f := d.(type) { + case func() error: + err = f() case func(string) error: err = f(opts.Entry) case func(string, string) error: @@ -69,8 +71,11 @@ func (opts *DeleteOpts) Delete(d interface{}, a ...string) error { if err != nil { return err } - - fmt.Printf(opts.SuccessMessage(), opts.Entry) + if opts.Entry == "" { + fmt.Print(opts.SuccessMessage()) + } else { + fmt.Printf(opts.SuccessMessage(), opts.Entry) + } return nil } @@ -91,7 +96,11 @@ func (opts *DeleteOpts) PromptWithMessage(message string) error { return nil } - p := prompt.NewConfirm(fmt.Sprintf(message, opts.Entry)) + m := message + if opts.Entry != "" { + m = fmt.Sprintf(message, opts.Entry) + } + p := prompt.NewConfirm(m) return survey.AskOne(p, &opts.Confirm) } diff --git a/internal/mocks/mock_logout.go b/internal/mocks/mock_logout.go new file mode 100644 index 0000000000..241302bd49 --- /dev/null +++ b/internal/mocks/mock_logout.go @@ -0,0 +1,88 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/mongodb/mongocli/internal/cli/auth (interfaces: Revoker,ConfigDeleter) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + mongodbatlas "go.mongodb.org/atlas/mongodbatlas" +) + +// MockRevoker is a mock of Revoker interface. +type MockRevoker struct { + ctrl *gomock.Controller + recorder *MockRevokerMockRecorder +} + +// MockRevokerMockRecorder is the mock recorder for MockRevoker. +type MockRevokerMockRecorder struct { + mock *MockRevoker +} + +// NewMockRevoker creates a new mock instance. +func NewMockRevoker(ctrl *gomock.Controller) *MockRevoker { + mock := &MockRevoker{ctrl: ctrl} + mock.recorder = &MockRevokerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRevoker) EXPECT() *MockRevokerMockRecorder { + return m.recorder +} + +// Revoke mocks base method. +func (m *MockRevoker) Revoke(arg0 context.Context, arg1, arg2 string) (*mongodbatlas.Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Revoke", arg0, arg1, arg2) + ret0, _ := ret[0].(*mongodbatlas.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Revoke indicates an expected call of Revoke. +func (mr *MockRevokerMockRecorder) Revoke(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Revoke", reflect.TypeOf((*MockRevoker)(nil).Revoke), arg0, arg1, arg2) +} + +// MockConfigDeleter is a mock of ConfigDeleter interface. +type MockConfigDeleter struct { + ctrl *gomock.Controller + recorder *MockConfigDeleterMockRecorder +} + +// MockConfigDeleterMockRecorder is the mock recorder for MockConfigDeleter. +type MockConfigDeleterMockRecorder struct { + mock *MockConfigDeleter +} + +// NewMockConfigDeleter creates a new mock instance. +func NewMockConfigDeleter(ctrl *gomock.Controller) *MockConfigDeleter { + mock := &MockConfigDeleter{ctrl: ctrl} + mock.recorder = &MockConfigDeleterMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockConfigDeleter) EXPECT() *MockConfigDeleterMockRecorder { + return m.recorder +} + +// Delete mocks base method. +func (m *MockConfigDeleter) Delete() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete") + ret0, _ := ret[0].(error) + return ret0 +} + +// Delete indicates an expected call of Delete. +func (mr *MockConfigDeleterMockRecorder) Delete() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockConfigDeleter)(nil).Delete)) +}