Files
steve 9d946b1b03 feat(cli): two-key role routing + init bootstrap
openStore(role) selects the DEK wrap slot; admin commands require
EMCLI_ADMIN_KEY (admin slot only, no agent fallback); init writes both
slots from both keys. Test helpers seed the wrap slots.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 22:59:16 +01:00

111 lines
3.4 KiB
Go

package cli
import (
"errors"
"path/filepath"
"strings"
"testing"
"git.dcglab.co.uk/steve/emcli/internal/store"
)
func doctorDeps(t *testing.T, accounts []store.Account, imap, smtp func(store.Account) error) (Deps, *[]byte) {
t.Helper()
st, err := store.Open(filepath.Join(t.TempDir(), "e.db"))
if err != nil {
t.Fatalf("store: %v", err)
}
if err := st.InitKeys(testKey(), testKey()); err != nil {
t.Fatalf("InitKeys: %v", err)
}
t.Cleanup(func() { st.Close() })
for _, a := range accounts {
if _, err := st.AddAccount(a); err != nil {
t.Fatalf("AddAccount %s: %v", a.Name, err)
}
}
buf := &[]byte{}
d := Deps{Store: st, CheckIMAP: imap, CheckSMTP: smtp, Out: bufWriter{buf}}
return d, buf
}
func roAcc(name string) store.Account {
return store.Account{Name: name, Mode: "RO", IMAPHost: "h", IMAPPort: 993, IMAPSecurity: "tls",
AuthType: "password", Username: "u@x.com", Password: "p"}
}
func rwAcc(name string) store.Account {
a := roAcc(name)
a.Mode = "RW"
a.SMTPHost, a.SMTPPort, a.SMTPSecurity = "h", 465, "tls"
return a
}
func TestDoctorAllOK(t *testing.T) {
ok := func(store.Account) error { return nil }
d, buf := doctorDeps(t, []store.Account{rwAcc("work")}, ok, ok)
if err := DoctorCmd(d, ""); err != nil {
t.Fatalf("DoctorCmd returned error when all checks pass: %v", err)
}
out := string(*buf)
if !strings.Contains(out, "work") || strings.Contains(strings.ToLower(out), "fail") {
t.Fatalf("unexpected report:\n%s", out)
}
}
func TestDoctorReportsSMTPFailure(t *testing.T) {
ok := func(store.Account) error { return nil }
bad := func(store.Account) error { return errors.New("auth rejected") }
d, buf := doctorDeps(t, []store.Account{rwAcc("work")}, ok, bad)
err := DoctorCmd(d, "")
if err == nil {
t.Fatal("DoctorCmd must return error when a check fails (non-zero exit)")
}
out := string(*buf)
if !strings.Contains(strings.ToLower(out), "fail") || !strings.Contains(out, "auth rejected") {
t.Fatalf("failure not reported:\n%s", out)
}
}
func TestDoctorSkipsSMTPForRO(t *testing.T) {
ok := func(store.Account) error { return nil }
smtpCalled := false
smtp := func(store.Account) error { smtpCalled = true; return nil }
d, _ := doctorDeps(t, []store.Account{roAcc("ro")}, ok, smtp)
if err := DoctorCmd(d, ""); err != nil {
t.Fatalf("DoctorCmd: %v", err)
}
if smtpCalled {
t.Fatal("SMTP check must be skipped for RO accounts")
}
}
func TestDoctorUsesDecryptedCredentials(t *testing.T) {
// roAcc has Password "p". ListAccounts strips secrets, so doctor must
// re-fetch the decrypted account before checking — otherwise the live
// check runs with an empty password.
var gotPassword string
imap := func(a store.Account) error { gotPassword = a.Password; return nil }
ok := func(store.Account) error { return nil }
d, _ := doctorDeps(t, []store.Account{roAcc("work")}, imap, ok)
if err := DoctorCmd(d, ""); err != nil {
t.Fatalf("DoctorCmd: %v", err)
}
if gotPassword != "p" {
t.Fatalf("check received password %q, want decrypted \"p\"", gotPassword)
}
}
func TestDoctorFiltersByAccount(t *testing.T) {
ok := func(store.Account) error { return nil }
checked := map[string]bool{}
imap := func(a store.Account) error { checked[a.Name] = true; return nil }
d, _ := doctorDeps(t, []store.Account{roAcc("a"), roAcc("b")}, imap, ok)
if err := DoctorCmd(d, "b"); err != nil {
t.Fatalf("DoctorCmd: %v", err)
}
if checked["a"] || !checked["b"] {
t.Fatalf("account filter wrong: %v", checked)
}
}