diff --git a/cli/logout.go b/cli/logout.go index 0271454e9c374..15d57b37c0f5b 100644 --- a/cli/logout.go +++ b/cli/logout.go @@ -6,21 +6,64 @@ import ( "github.com/spf13/cobra" "golang.org/x/xerrors" + + "github.com/coder/coder/cli/cliui" ) func logout() *cobra.Command { - return &cobra.Command{ + cmd := &cobra.Command{ Use: "logout", - Short: "Remove local autheticated session", + Short: "Remove the local authenticated session", RunE: func(cmd *cobra.Command, args []string) error { + var isLoggedOut bool + config := createConfig(cmd) - err := os.RemoveAll(string(config)) + + _, err := cliui.Prompt(cmd, cliui.PromptOptions{ + Text: "Are you sure you want to logout?", + IsConfirm: true, + Default: "yes", + }) if err != nil { - return xerrors.Errorf("remove files at %s: %w", config, err) + return err } - _, _ = fmt.Fprintf(cmd.OutOrStdout(), caret+"Successfully logged out.\n") + err = config.URL().Delete() + if err != nil { + // Only throw error if the URL configuration file is present, + // otherwise the user is already logged out, and we proceed + if !os.IsNotExist(err) { + return xerrors.Errorf("remove URL file: %w", err) + } + isLoggedOut = true + } + + err = config.Session().Delete() + if err != nil { + // Only throw error if the session configuration file is present, + // otherwise the user is already logged out, and we proceed + if !os.IsNotExist(err) { + return xerrors.Errorf("remove session file: %w", err) + } + isLoggedOut = true + } + + err = config.Organization().Delete() + // If the organization configuration file is absent, we still proceed + if err != nil && !os.IsNotExist(err) { + return xerrors.Errorf("remove organization file: %w", err) + } + + // If the user was already logged out, we show them a different message + if isLoggedOut { + _, _ = fmt.Fprintf(cmd.OutOrStdout(), notLoggedInMessage+"\n") + } else { + _, _ = fmt.Fprintf(cmd.OutOrStdout(), caret+"Successfully logged out.\n") + } return nil }, } + + cliui.AllowSkipPrompt(cmd) + return cmd } diff --git a/cli/logout_test.go b/cli/logout_test.go index adca7c802e601..bae15ef204ca4 100644 --- a/cli/logout_test.go +++ b/cli/logout_test.go @@ -1,26 +1,148 @@ package cli_test import ( + "os" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/coder/coder/cli/clitest" + "github.com/coder/coder/cli/config" "github.com/coder/coder/coderd/coderdtest" "github.com/coder/coder/pty/ptytest" ) func TestLogout(t *testing.T) { t.Parallel() + t.Run("Logout", func(t *testing.T) { + t.Parallel() + + pty := ptytest.New(t) + config := login(t, pty) + + // ensure session files exist + require.FileExists(t, string(config.URL())) + require.FileExists(t, string(config.Session())) + + logoutChan := make(chan struct{}) + logout, _ := clitest.New(t, "logout", "--global-config", string(config)) + logout.SetIn(pty.Input()) + logout.SetOut(pty.Output()) + + go func() { + defer close(logoutChan) + err := logout.Execute() + assert.NoError(t, err) + assert.NoFileExists(t, string(config.URL())) + assert.NoFileExists(t, string(config.Session())) + }() + + pty.ExpectMatch("Are you sure you want to logout?") + pty.WriteLine("yes") + pty.ExpectMatch("Successfully logged out") + <-logoutChan + }) + t.Run("SkipPrompt", func(t *testing.T) { + t.Parallel() + + pty := ptytest.New(t) + config := login(t, pty) + + // ensure session files exist + require.FileExists(t, string(config.URL())) + require.FileExists(t, string(config.Session())) + + logoutChan := make(chan struct{}) + logout, _ := clitest.New(t, "logout", "--global-config", string(config), "-y") + logout.SetIn(pty.Input()) + logout.SetOut(pty.Output()) + + go func() { + defer close(logoutChan) + err := logout.Execute() + assert.NoError(t, err) + assert.NoFileExists(t, string(config.URL())) + assert.NoFileExists(t, string(config.Session())) + }() + + pty.ExpectMatch("Successfully logged out") + <-logoutChan + }) + t.Run("NoURLFile", func(t *testing.T) { + t.Parallel() + + pty := ptytest.New(t) + config := login(t, pty) + + // ensure session files exist + require.FileExists(t, string(config.URL())) + require.FileExists(t, string(config.Session())) + + err := os.Remove(string(config.URL())) + require.NoError(t, err) + + logoutChan := make(chan struct{}) + logout, _ := clitest.New(t, "logout", "--global-config", string(config)) + + logout.SetIn(pty.Input()) + logout.SetOut(pty.Output()) + + go func() { + defer close(logoutChan) + err := logout.Execute() + assert.NoError(t, err) + assert.NoFileExists(t, string(config.URL())) + assert.NoFileExists(t, string(config.Session())) + }() + + pty.ExpectMatch("Are you sure you want to logout?") + pty.WriteLine("yes") + pty.ExpectMatch("You are not logged in. Try logging in using 'coder login '.") + <-logoutChan + }) + t.Run("NoSessionFile", func(t *testing.T) { + t.Parallel() + + pty := ptytest.New(t) + config := login(t, pty) + + // ensure session files exist + require.FileExists(t, string(config.URL())) + require.FileExists(t, string(config.Session())) + + err := os.Remove(string(config.Session())) + require.NoError(t, err) + + logoutChan := make(chan struct{}) + logout, _ := clitest.New(t, "logout", "--global-config", string(config)) + + logout.SetIn(pty.Input()) + logout.SetOut(pty.Output()) + + go func() { + defer close(logoutChan) + err = logout.Execute() + assert.NoError(t, err) + assert.NoFileExists(t, string(config.URL())) + assert.NoFileExists(t, string(config.Session())) + }() + + pty.ExpectMatch("Are you sure you want to logout?") + pty.WriteLine("yes") + pty.ExpectMatch("You are not logged in. Try logging in using 'coder login '.") + <-logoutChan + }) +} + +func login(t *testing.T, pty *ptytest.PTY) config.Root { + t.Helper() - // login client := coderdtest.New(t, nil) coderdtest.CreateFirstUser(t, client) doneChan := make(chan struct{}) - root, config := clitest.New(t, "login", "--force-tty", client.URL.String(), "--no-open") - pty := ptytest.New(t) + root, cfg := clitest.New(t, "login", "--force-tty", client.URL.String(), "--no-open") root.SetIn(pty.Input()) root.SetOut(pty.Output()) go func() { @@ -34,13 +156,5 @@ func TestLogout(t *testing.T) { pty.ExpectMatch("Welcome to Coder") <-doneChan - // ensure session files exist - require.FileExists(t, string(config.URL())) - require.FileExists(t, string(config.Session())) - - logout, _ := clitest.New(t, "logout", "--global-config", string(config)) - err := logout.Execute() - require.NoError(t, err) - require.NoFileExists(t, string(config.URL())) - require.NoFileExists(t, string(config.Session())) + return cfg } diff --git a/cli/root.go b/cli/root.go index 7398986608b79..1314c71ea280d 100644 --- a/cli/root.go +++ b/cli/root.go @@ -31,13 +31,14 @@ var ( ) const ( - varURL = "url" - varToken = "token" - varAgentToken = "agent-token" - varAgentURL = "agent-url" - varGlobalConfig = "global-config" - varNoOpen = "no-open" - varForceTty = "force-tty" + varURL = "url" + varToken = "token" + varAgentToken = "agent-token" + varAgentURL = "agent-url" + varGlobalConfig = "global-config" + varNoOpen = "no-open" + varForceTty = "force-tty" + notLoggedInMessage = "You are not logged in. Try logging in using 'coder login '." ) func init() { @@ -117,7 +118,7 @@ func createClient(cmd *cobra.Command) (*codersdk.Client, error) { if err != nil { // If the configuration files are absent, the user is logged out if os.IsNotExist(err) { - return nil, xerrors.New("You are not logged in. Try logging in using 'coder login '.") + return nil, xerrors.New(notLoggedInMessage) } return nil, err } @@ -132,7 +133,7 @@ func createClient(cmd *cobra.Command) (*codersdk.Client, error) { if err != nil { // If the configuration files are absent, the user is logged out if os.IsNotExist(err) { - return nil, xerrors.New("You are not logged in. Try logging in using 'coder login '.") + return nil, xerrors.New(notLoggedInMessage) } return nil, err } diff --git a/cli/userlist_test.go b/cli/userlist_test.go index 8529f6980fb9c..7c3d596e220b0 100644 --- a/cli/userlist_test.go +++ b/cli/userlist_test.go @@ -12,6 +12,7 @@ import ( ) func TestUserList(t *testing.T) { + t.Parallel() t.Run("List", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil)