2026-04-10 08:08:21 +00:00
|
|
|
package cli
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
|
|
|
|
"context"
|
2026-04-10 08:17:38 +00:00
|
|
|
"errors"
|
|
|
|
|
"io"
|
|
|
|
|
"os"
|
2026-04-10 08:08:21 +00:00
|
|
|
"strings"
|
|
|
|
|
"testing"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func TestInteractiveSetupPrompterPromptSetupCollectsCredential(t *testing.T) {
|
|
|
|
|
input := strings.NewReader(" imap.example.com \n alice \n secret \n")
|
|
|
|
|
output := &bytes.Buffer{}
|
|
|
|
|
prompter := NewInteractiveSetupPrompter(input, output)
|
|
|
|
|
|
|
|
|
|
cred, err := prompter.PromptSetup(context.Background())
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("PromptSetup returned error: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if cred.Host != "imap.example.com" || cred.Username != "alice" || cred.Password != "secret" {
|
|
|
|
|
t.Fatalf("unexpected credential: %#v", cred)
|
|
|
|
|
}
|
|
|
|
|
if got := output.String(); got != "IMAP host: Username: Password: " {
|
|
|
|
|
t.Fatalf("unexpected prompts: %q", got)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 08:17:38 +00:00
|
|
|
func TestInteractiveSetupPrompterPromptSetupUsesPasswordReader(t *testing.T) {
|
|
|
|
|
output := &bytes.Buffer{}
|
|
|
|
|
passwordReader := &passwordReaderStub{password: "secret"}
|
|
|
|
|
prompter := NewInteractiveSetupPrompterWithPasswordReader(
|
|
|
|
|
strings.NewReader("imap.example.com\nalice\n"),
|
|
|
|
|
output,
|
|
|
|
|
passwordReader,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
cred, err := prompter.PromptSetup(context.Background())
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("PromptSetup returned error: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if cred.Password != "secret" {
|
|
|
|
|
t.Fatalf("expected password from password reader, got %#v", cred)
|
|
|
|
|
}
|
|
|
|
|
if passwordReader.prompt != "Password: " {
|
|
|
|
|
t.Fatalf("unexpected password prompt %q", passwordReader.prompt)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 08:08:21 +00:00
|
|
|
func TestInteractiveSetupPrompterPromptSetupRejectsMissingFields(t *testing.T) {
|
|
|
|
|
input := strings.NewReader("imap.example.com\nalice\n \n")
|
|
|
|
|
prompter := NewInteractiveSetupPrompter(input, &bytes.Buffer{})
|
|
|
|
|
|
|
|
|
|
_, err := prompter.PromptSetup(context.Background())
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Fatal("expected validation error")
|
|
|
|
|
}
|
|
|
|
|
if !strings.Contains(err.Error(), "password is required") {
|
|
|
|
|
t.Fatalf("expected password validation error, got %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-10 08:17:38 +00:00
|
|
|
|
|
|
|
|
func TestTerminalPasswordReaderReadPasswordUsesHiddenInputForTTY(t *testing.T) {
|
|
|
|
|
hiddenCalls := 0
|
|
|
|
|
fallbackCalls := 0
|
|
|
|
|
reader := terminalPasswordReader{
|
|
|
|
|
input: os.Stdin,
|
|
|
|
|
output: &bytes.Buffer{},
|
|
|
|
|
isTerminal: func(io.Reader) bool {
|
|
|
|
|
return true
|
|
|
|
|
},
|
|
|
|
|
readHiddenPassword: func(file *os.File) ([]byte, error) {
|
|
|
|
|
hiddenCalls++
|
|
|
|
|
if file != os.Stdin {
|
|
|
|
|
t.Fatalf("expected os.Stdin, got %v", file)
|
|
|
|
|
}
|
|
|
|
|
return []byte("secret"), nil
|
|
|
|
|
},
|
|
|
|
|
readFallbackPassword: func(string) (string, error) {
|
|
|
|
|
fallbackCalls++
|
|
|
|
|
return "", nil
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
password, err := reader.ReadPassword("Password: ")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("ReadPassword returned error: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if password != "secret" {
|
|
|
|
|
t.Fatalf("expected hidden password, got %q", password)
|
|
|
|
|
}
|
|
|
|
|
if hiddenCalls != 1 {
|
|
|
|
|
t.Fatalf("expected hidden reader to be called once, got %d", hiddenCalls)
|
|
|
|
|
}
|
|
|
|
|
if fallbackCalls != 0 {
|
|
|
|
|
t.Fatalf("expected fallback reader not to be called, got %d", fallbackCalls)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestTerminalPasswordReaderReadPasswordFallsBackWhenInputIsNotTTY(t *testing.T) {
|
|
|
|
|
hiddenCalls := 0
|
|
|
|
|
reader := terminalPasswordReader{
|
|
|
|
|
input: strings.NewReader("ignored\n"),
|
|
|
|
|
output: &bytes.Buffer{},
|
|
|
|
|
isTerminal: func(io.Reader) bool {
|
|
|
|
|
return false
|
|
|
|
|
},
|
|
|
|
|
readHiddenPassword: func(*os.File) ([]byte, error) {
|
|
|
|
|
hiddenCalls++
|
|
|
|
|
return nil, errors.New("should not be called")
|
|
|
|
|
},
|
|
|
|
|
readFallbackPassword: func(prompt string) (string, error) {
|
|
|
|
|
if prompt != "Password: " {
|
|
|
|
|
t.Fatalf("unexpected prompt %q", prompt)
|
|
|
|
|
}
|
|
|
|
|
return "secret", nil
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
password, err := reader.ReadPassword("Password: ")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("ReadPassword returned error: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if password != "secret" {
|
|
|
|
|
t.Fatalf("expected fallback password, got %q", password)
|
|
|
|
|
}
|
|
|
|
|
if hiddenCalls != 0 {
|
|
|
|
|
t.Fatalf("expected hidden reader not to be called, got %d", hiddenCalls)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type passwordReaderStub struct {
|
|
|
|
|
password string
|
|
|
|
|
err error
|
|
|
|
|
prompt string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *passwordReaderStub) ReadPassword(prompt string) (string, error) {
|
|
|
|
|
p.prompt = prompt
|
|
|
|
|
if p.err != nil {
|
|
|
|
|
return "", p.err
|
|
|
|
|
}
|
|
|
|
|
return p.password, nil
|
|
|
|
|
}
|