diff --git a/README.md b/README.md index 60a8bf5..fb4f122 100644 --- a/README.md +++ b/README.md @@ -259,7 +259,10 @@ pass, and an existing configuration is carried over the first time dwshell saves anything. ```sh -$ dwshell login --user info@example.com # the email is the account's identity +$ dwshell login # the email is the account's identity +User (email) [ale@example.net]: info@example.com +Password: … + $ dwshell account list ale@example.net (default) info@example.com @@ -269,6 +272,11 @@ $ dwshell list --account info@example.com # the other one $ DWSHELL_ACCOUNT=info@example.com dwshell list # for a whole session ``` +`login` asks which email to use, offering the account you already have: press +Enter to refresh it, or type another to register a second. `--user` answers the +question up front, and without a terminal — a script — the default account is +used and nothing is asked. + The first account you log in with becomes the default; change it with `dwshell account default `. Logging in again with an email already registered just refreshes that account. diff --git a/cmd/dwshell/login_test.go b/cmd/dwshell/login_test.go new file mode 100644 index 0000000..bb98f1c --- /dev/null +++ b/cmd/dwshell/login_test.go @@ -0,0 +1,70 @@ +package main + +import "testing" + +// asking records what the prompt was offered as a default, and answers with a +// canned reply, standing in for a person at a terminal. +func asking(answer string, seen *string) func(string) string { + return func(suggested string) string { + *seen = suggested + return answer + } +} + +func TestLoginUserPrefersTheFlag(t *testing.T) { + var asked string + got := loginUser("typed@x", "default@x", true, asking("ignored@x", &asked)) + if got != "typed@x" { + t.Fatalf("user = %q, want the --user value", got) + } + if asked != "" { + t.Error("with --user given there is nothing to ask") + } +} + +// The whole point: at a terminal the email is asked, so a bare `dwshell login` +// can register an account that is not the default one. +func TestLoginUserAsksSoAnAccountCanBeAdded(t *testing.T) { + var asked string + got := loginUser("", "default@x", true, asking("second@x", &asked)) + if got != "second@x" { + t.Fatalf("user = %q: a new email typed at the prompt must be honoured", got) + } + if asked != "default@x" { + t.Errorf("the prompt should offer the default account, offered %q", asked) + } +} + +// Answering nothing accepts the default, so refreshing the account you already +// have stays a single keystroke. +func TestLoginUserEmptyAnswerKeepsTheDefault(t *testing.T) { + var asked string + got := loginUser("", "default@x", true, asking("", &asked)) + if got != "default@x" { + t.Fatalf("user = %q, want the default", got) + } +} + +// With no terminal there is nobody to ask, so a script refreshing the default +// account keeps working exactly as before. +func TestLoginUserFallsBackToTheDefaultWithoutATerminal(t *testing.T) { + var asked string + got := loginUser("", "default@x", false, asking("unused@x", &asked)) + if got != "default@x" { + t.Fatalf("user = %q, want the default", got) + } + if asked != "" { + t.Error("nothing may be asked when there is no terminal") + } +} + +func TestLoginUserAsksWithNothingConfigured(t *testing.T) { + var asked string + got := loginUser("", "", true, asking("first@x", &asked)) + if got != "first@x" { + t.Fatalf("user = %q", got) + } + if asked != "" { + t.Errorf("with no account there is no default to offer, offered %q", asked) + } +} diff --git a/cmd/dwshell/main.go b/cmd/dwshell/main.go index 240b67d..6c99383 100644 --- a/cmd/dwshell/main.go +++ b/cmd/dwshell/main.go @@ -196,14 +196,7 @@ func cmdLogin(ctx context.Context, args []string) int { if err != nil { return fail("%v", err) } - if user == "" { - // Re-logging in with no --user refreshes the default account. - user = c.Config().Default - } - if user == "" { - fmt.Fprint(os.Stderr, "User (email): ") - fmt.Scanln(&user) - } + user = loginUser(user, c.Config().Default, term.IsTTY(), promptUser) if user == "" { return fail("a user is required") } @@ -223,6 +216,41 @@ func cmdLogin(ctx context.Context, args []string) int { return 0 } +// loginUser decides which account `login` authenticates. +// +// The email is asked for whenever there is someone to ask, because it is the +// account's identity: skipping the question when an account already exists +// would make a bare `dwshell login` able only ever to refresh that one, never +// to register a second. The account already configured is offered as the +// answer, so refreshing it stays one keystroke. +// +// With no terminal — a script — there is nobody to ask, so the default account +// is used, which is what such a script has always done. +func loginUser(flagUser, defaultAccount string, interactive bool, prompt func(suggested string) string) string { + if flagUser != "" { + return flagUser + } + if !interactive { + return defaultAccount + } + if answer := strings.TrimSpace(prompt(defaultAccount)); answer != "" { + return answer + } + return defaultAccount +} + +// promptUser asks for the account email, offering the one already configured. +func promptUser(suggested string) string { + if suggested != "" { + fmt.Fprintf(os.Stderr, "User (email) [%s]: ", suggested) + } else { + fmt.Fprint(os.Stderr, "User (email): ") + } + var answer string + fmt.Scanln(&answer) + return answer +} + func readPassword() (string, error) { if p := os.Getenv("DWSHELL_PASSWORD"); p != "" { return p, nil