package cli import ( "bytes" "context" "errors" "io" "os" "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) } } 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) } } 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) } } 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 }