32f5a8d933
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
149 lines
4.7 KiB
Go
149 lines
4.7 KiB
Go
package cli
|
|
|
|
import (
|
|
"bytes"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.dcglab.co.uk/steve/emcli/internal/crypto"
|
|
"git.dcglab.co.uk/steve/emcli/internal/store"
|
|
)
|
|
|
|
// adminEnv points both keys + EMCLI_DB at a fresh, initialized temp DB.
|
|
func adminEnv(t *testing.T) string {
|
|
t.Helper()
|
|
db := filepath.Join(t.TempDir(), "emcli.db")
|
|
t.Setenv("EMCLI_ADMIN_KEY", b64Key())
|
|
t.Setenv("EMCLI_KEY", b64AgentKey())
|
|
t.Setenv("EMCLI_DB", db)
|
|
|
|
st, err := store.Open(db)
|
|
if err != nil {
|
|
t.Fatalf("Open: %v", err)
|
|
}
|
|
adminKey, _ := crypto.AdminKeyFromEnv()
|
|
agentKey, _ := crypto.AgentKeyFromEnv()
|
|
if err := st.InitKeys(adminKey, agentKey); err != nil {
|
|
t.Fatalf("InitKeys: %v", err)
|
|
}
|
|
st.Close()
|
|
return db
|
|
}
|
|
|
|
func run(t *testing.T, args ...string) (int, string, string) {
|
|
t.Helper()
|
|
var out, errOut bytes.Buffer
|
|
code := Run(args, &out, &errOut)
|
|
return code, out.String(), errOut.String()
|
|
}
|
|
|
|
func TestConfigSetGet(t *testing.T) {
|
|
adminEnv(t)
|
|
if code, _, e := run(t, "config", "set", "audit_retention_days", "30"); code != 0 {
|
|
t.Fatalf("config set failed: %s", e)
|
|
}
|
|
code, out, _ := run(t, "config", "get", "audit_retention_days")
|
|
if code != 0 || !strings.Contains(out, "30") {
|
|
t.Fatalf("config get: code=%d out=%q", code, out)
|
|
}
|
|
}
|
|
|
|
func TestConfigSetRejectsBadRetention(t *testing.T) {
|
|
adminEnv(t)
|
|
if code, _, _ := run(t, "config", "set", "audit_retention_days", "-5"); code == 0 {
|
|
t.Fatal("negative retention must be rejected")
|
|
}
|
|
if code, _, _ := run(t, "config", "set", "audit_retention_days", "abc"); code == 0 {
|
|
t.Fatal("non-integer retention must be rejected")
|
|
}
|
|
}
|
|
|
|
func TestAccountRemove(t *testing.T) {
|
|
adminEnv(t)
|
|
run(t, "account", "add", "--name", "gone", "--imap-host", "h", "--username", "u@x.com")
|
|
if code, _, e := run(t, "account", "remove", "--name", "gone", "--yes"); code != 0 {
|
|
t.Fatalf("remove failed: %s", e)
|
|
}
|
|
_, out, _ := run(t, "account", "list")
|
|
if strings.Contains(out, "gone") {
|
|
t.Fatalf("account still listed after remove:\n%s", out)
|
|
}
|
|
}
|
|
|
|
func TestAccountRemoveMissing(t *testing.T) {
|
|
adminEnv(t)
|
|
if code, _, _ := run(t, "account", "remove", "--name", "nope", "--yes"); code == 0 {
|
|
t.Fatal("removing a missing account must be non-zero")
|
|
}
|
|
}
|
|
|
|
func TestAccountEditPartialPreservesOtherFields(t *testing.T) {
|
|
db := adminEnv(t)
|
|
run(t, "account", "add", "--name", "ed", "--mode", "RO",
|
|
"--imap-host", "imap.x.com", "--username", "u@x.com", "--password", "orig")
|
|
// Edit only mode + add SMTP; imap-host, username, password must be preserved.
|
|
if code, _, e := run(t, "account", "edit", "--name", "ed", "--mode", "RW",
|
|
"--smtp-host", "smtp.x.com", "--smtp-port", "587", "--smtp-security", "starttls"); code != 0 {
|
|
t.Fatalf("edit failed: %s", e)
|
|
}
|
|
st, err := store.Open(db)
|
|
if err != nil {
|
|
t.Fatalf("open: %v", err)
|
|
}
|
|
defer st.Close()
|
|
adminKey, _ := crypto.AdminKeyFromEnv()
|
|
agentKey, _ := crypto.AgentKeyFromEnv()
|
|
if err := st.Unlock(store.RoleAdmin, adminKey, agentKey); err != nil {
|
|
t.Fatalf("Unlock: %v", err)
|
|
}
|
|
got, err := st.GetAccount("ed")
|
|
if err != nil {
|
|
t.Fatalf("GetAccount: %v", err)
|
|
}
|
|
if got.Mode != "RW" || got.SMTPHost != "smtp.x.com" || got.SMTPPort != 587 {
|
|
t.Fatalf("edit didn't apply: %+v", got)
|
|
}
|
|
if got.IMAPHost != "imap.x.com" || got.Username != "u@x.com" || got.Password != "orig" {
|
|
t.Fatalf("edit clobbered preserved fields: %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestAuditListCoreRenders(t *testing.T) {
|
|
st, err := store.Open(filepath.Join(t.TempDir(), "e.db"))
|
|
if err != nil {
|
|
t.Fatalf("open: %v", err)
|
|
}
|
|
defer st.Close()
|
|
if err := st.InitKeys(testKey(), testKey()); err != nil {
|
|
t.Fatalf("InitKeys: %v", err)
|
|
}
|
|
now := time.Date(2026, 6, 22, 0, 0, 0, 0, time.UTC)
|
|
_ = st.Audit(now, store.AuditEntry{Account: "a", Action: "list", Target: "INBOX", Result: "allowed"})
|
|
_ = st.Audit(now, store.AuditEntry{Account: "a", Action: "send", Target: "x@y.com", Result: "blocked", Reason: "whitelist_out"})
|
|
var buf bytes.Buffer
|
|
if err := auditList(st, "", 50, &buf); err != nil {
|
|
t.Fatalf("auditList: %v", err)
|
|
}
|
|
out := buf.String()
|
|
if !strings.Contains(out, "list") || !strings.Contains(out, "whitelist_out") {
|
|
t.Fatalf("audit rows not rendered:\n%s", out)
|
|
}
|
|
}
|
|
|
|
func TestAccountEditFromValidationRejectsMailformed(t *testing.T) {
|
|
adminEnv(t)
|
|
// Seed an account so the failure is from --from validation, not a missing account.
|
|
run(t, "account", "add", "--name", "valacc", "--imap-host", "imap.x.com", "--username", "u@x.com")
|
|
// A malformed --from value must be rejected with exit code 2 before touching the account.
|
|
code, _, errStr := run(t, "account", "edit", "--name", "valacc", "--from", "not an address")
|
|
if code != 2 {
|
|
t.Fatalf("expected exit code 2 for malformed --from, got %d (stderr: %q)", code, errStr)
|
|
}
|
|
if errStr == "" {
|
|
t.Fatal("expected an error message on stderr for malformed --from, got none")
|
|
}
|
|
}
|
|
|