Files
emcli/internal/cli/admin_test.go
T
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

135 lines
4.1 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)
}
}