Skip to content

Improve CLI logout flow #1692

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
May 24, 2022
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
53 changes: 48 additions & 5 deletions cli/logout.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
138 changes: 126 additions & 12 deletions cli/logout_test.go
Original file line number Diff line number Diff line change
@@ -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 <url>'.")
<-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 <url>'.")
<-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() {
Expand All @@ -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
}
19 changes: 10 additions & 9 deletions cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 <url>'."
)

func init() {
Expand Down Expand Up @@ -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 <url>'.")
return nil, xerrors.New(notLoggedInMessage)
}
return nil, err
}
Expand All @@ -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 <url>'.")
return nil, xerrors.New(notLoggedInMessage)
}
return nil, err
}
Expand Down
1 change: 1 addition & 0 deletions cli/userlist_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down