Files
emcli/internal/tui/account_test.go
T
steve b6e68ddeae feat(cli): configurable send-as From address (flags, TUI, validation)
- tui.ValidFromAddress: exported validator; blank passes, malformed rejects
- Fields.FromAddress: new field, round-trips through ToAccount/FieldsFromAccount
- Fields.Validate: calls ValidFromAddress before returning nil
- TUI form: from_address fieldDef between username and password
- send.go: From set via acc.SendFrom() instead of acc.Username
- admin.go account add: --from flag with pre-parse validation
- admin.go account edit: --from flag; validate before Visit, apply in Visit
- USER-MANUAL.md: --from flag added to account add flags table

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 20:25:14 +01:00

193 lines
5.4 KiB
Go

package tui
import (
"testing"
tea "github.com/charmbracelet/bubbletea"
"git.dcglab.co.uk/steve/emcli/internal/store"
)
func validFields() Fields {
return Fields{
Name: "work", Mode: "RW",
IMAPHost: "imap.x.com", IMAPPort: "993", IMAPSecurity: "tls",
SMTPHost: "smtp.x.com", SMTPPort: "465", SMTPSecurity: "tls",
Username: "u@x.com", Password: "pw",
}
}
func TestFieldsValidateRequired(t *testing.T) {
f := validFields()
f.Name = ""
if err := f.Validate(); err == nil {
t.Fatal("missing name must fail validation")
}
f = validFields()
f.Username = ""
if err := f.Validate(); err == nil {
t.Fatal("missing username must fail validation")
}
f = validFields()
f.IMAPHost = ""
if err := f.Validate(); err == nil {
t.Fatal("missing imap host must fail validation")
}
}
func TestFieldsValidateEnums(t *testing.T) {
f := validFields()
f.Mode = "XX"
if err := f.Validate(); err == nil {
t.Fatal("mode must be RO or RW")
}
f = validFields()
f.IMAPSecurity = "ssl"
if err := f.Validate(); err == nil {
t.Fatal("security must be tls or starttls")
}
f = validFields()
f.IMAPPort = "notnum"
if err := f.Validate(); err == nil {
t.Fatal("port must be numeric")
}
}
func TestFieldsValidateRWNeedsSMTP(t *testing.T) {
f := validFields()
f.SMTPHost = ""
if err := f.Validate(); err == nil {
t.Fatal("RW account requires an SMTP host")
}
// RO without SMTP is fine.
f = validFields()
f.Mode = "RO"
f.SMTPHost, f.SMTPPort, f.SMTPSecurity = "", "", ""
if err := f.Validate(); err != nil {
t.Fatalf("RO without SMTP should validate: %v", err)
}
}
func TestFieldsToAccount(t *testing.T) {
f := validFields()
f.WhitelistIn = true
f.SubjectRegex = "^urgent"
acc, pwSet := f.ToAccount()
if !pwSet {
t.Fatal("password was provided, PasswordSet should be true")
}
if acc.Name != "work" || acc.Mode != "RW" || acc.IMAPPort != 993 || acc.SMTPPort != 465 {
t.Fatalf("account not assembled: %+v", acc)
}
if acc.AuthType != "password" || !acc.WhitelistInEnabled || acc.SubjectRegex != "^urgent" {
t.Fatalf("account flags wrong: %+v", acc)
}
if acc.Password != "pw" {
t.Fatalf("password not carried: %q", acc.Password)
}
}
func TestFieldsToAccountBlankPassword(t *testing.T) {
f := validFields()
f.Password = ""
_, pwSet := f.ToAccount()
if pwSet {
t.Fatal("blank password should report PasswordSet=false (edit keeps existing)")
}
}
func TestFieldsFromAccountRoundTrip(t *testing.T) {
a := store.Account{
Name: "g", Mode: "RW", IMAPHost: "i", IMAPPort: 993, IMAPSecurity: "tls",
SMTPHost: "s", SMTPPort: 587, SMTPSecurity: "starttls",
Username: "u@x.com", WhitelistOutEnabled: true, SubjectRegex: "re:",
}
f := FieldsFromAccount(a)
if f.Name != "g" || f.IMAPPort != "993" || f.SMTPPort != "587" || !f.WhitelistOut || f.SubjectRegex != "re:" {
t.Fatalf("FieldsFromAccount wrong: %+v", f)
}
// Password is never read back from an account.
if f.Password != "" {
t.Fatalf("password must not be prefilled: %q", f.Password)
}
}
func TestNewAccountFormDefaultsSMTPPort(t *testing.T) {
f := NewAccountForm(Fields{}, false).collect()
if f.SMTPPort != "465" {
t.Fatalf("SMTP port should default to 465, got %q", f.SMTPPort)
}
// The other prefilled defaults must remain intact.
if f.IMAPPort != "993" || f.Mode != "RO" || f.IMAPSecurity != "tls" || f.SMTPSecurity != "tls" {
t.Fatalf("existing defaults regressed: %+v", f)
}
}
func TestAccountFormSubmitValid(t *testing.T) {
m := NewAccountForm(validFields(), false)
// Enter submits; with valid fields the form completes.
nm, _ := m.Update(tea.KeyMsg{Type: tea.KeyEnter})
m = nm.(AccountForm)
if !m.Done() || m.Cancelled() {
t.Fatalf("valid submit should be Done, not cancelled (err=%v)", m.Err())
}
if m.Account().Name != "work" {
t.Fatalf("submitted account wrong: %+v", m.Account())
}
}
func TestAccountFormSubmitInvalidStays(t *testing.T) {
f := validFields()
f.Name = ""
m := NewAccountForm(f, false)
nm, _ := m.Update(tea.KeyMsg{Type: tea.KeyEnter})
m = nm.(AccountForm)
if m.Done() {
t.Fatal("invalid submit must not complete the form")
}
if m.Err() == nil {
t.Fatal("invalid submit should set an error to show the user")
}
}
func TestAccountFormCancel(t *testing.T) {
m := NewAccountForm(validFields(), false)
nm, _ := m.Update(tea.KeyMsg{Type: tea.KeyEsc})
m = nm.(AccountForm)
if !m.Cancelled() {
t.Fatal("esc should cancel the form")
}
}
func TestValidateRejectsBadFromAddress(t *testing.T) {
f := validFields()
f.FromAddress = "not an address"
if err := f.Validate(); err == nil {
t.Fatal("malformed from-address should fail validation")
}
f.FromAddress = "Steve Cliff <me@stevecliff.com>"
if err := f.Validate(); err != nil {
t.Fatalf("display-name from-address should validate: %v", err)
}
f.FromAddress = "me@stevecliff.com"
if err := f.Validate(); err != nil {
t.Fatalf("bare from-address should validate: %v", err)
}
f.FromAddress = "" // blank ⇒ fall back, always valid
if err := f.Validate(); err != nil {
t.Fatalf("blank from-address should validate: %v", err)
}
}
func TestFieldsFromToAccountCarriesFromAddress(t *testing.T) {
f := validFields()
f.FromAddress = "Steve Cliff <me@stevecliff.com>"
acc, _ := f.ToAccount()
if acc.FromAddress != "Steve Cliff <me@stevecliff.com>" {
t.Fatalf("ToAccount lost FromAddress: %q", acc.FromAddress)
}
back := FieldsFromAccount(acc)
if back.FromAddress != "Steve Cliff <me@stevecliff.com>" {
t.Fatalf("FieldsFromAccount lost FromAddress: %q", back.FromAddress)
}
}