Files
emcli/internal/store/store_test.go
T
steve cb0425f18d feat(store): envelope DEK with admin/agent wrap slots
Open() now opens LOCKED; InitKeys generates a DEK sealed under both KEKs;
Unlock loads it from the role's slot (admin slot has no agent fallback).
s.key becomes the DEK, so account/mail crypto is unchanged.

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

107 lines
3.0 KiB
Go

package store
import (
"path/filepath"
"testing"
)
// openTemp opens a fresh store in a temp dir and initialises keys so that
// account tests (which do crypto) work without needing their own setup.
func openTemp(t *testing.T) *Store {
t.Helper()
p := filepath.Join(t.TempDir(), "emcli.db")
s, err := Open(p)
if err != nil {
t.Fatalf("Open: %v", err)
}
if err := s.InitKeys(k(0xAA), k(0xBB)); err != nil {
t.Fatalf("InitKeys: %v", err)
}
t.Cleanup(func() { s.Close() })
return s
}
func TestOpenCreatesSchemaAndIsIdempotent(t *testing.T) {
p := filepath.Join(t.TempDir(), "emcli.db")
s, err := Open(p)
if err != nil {
t.Fatalf("first Open: %v", err)
}
v, err := s.GetSetting("schema_version")
if err != nil || v != "1" {
t.Fatalf("schema_version: %q err=%v", v, err)
}
s.Close()
// Re-open: must not error or duplicate.
s2, err := Open(p)
if err != nil {
t.Fatalf("second Open: %v", err)
}
defer s2.Close()
if v, _ := s2.GetSetting("schema_version"); v != "1" {
t.Fatalf("schema_version after reopen: %q", v)
}
}
func TestSettingsRoundTrip(t *testing.T) {
s := openTemp(t)
if err := s.SetSetting("audit_retention_days", "30"); err != nil {
t.Fatalf("SetSetting: %v", err)
}
got, err := s.GetSetting("audit_retention_days")
if err != nil || got != "30" {
t.Fatalf("got %q err=%v", got, err)
}
// Upsert overwrites.
_ = s.SetSetting("audit_retention_days", "7")
if got, _ := s.GetSetting("audit_retention_days"); got != "7" {
t.Fatalf("upsert failed: %q", got)
}
}
func TestForeignKeyCascade(t *testing.T) {
s := openTemp(t)
// Insert an account directly via raw SQL.
_, err := s.db.Exec(`
INSERT INTO accounts(name, mode, imap_host, imap_port, imap_security, auth_type, username)
VALUES('test_account', 'RO', 'imap.example.com', 993, 'tls', 'password', 'user@example.com')
`)
if err != nil {
t.Fatalf("insert account: %v", err)
}
// Get the inserted account ID.
var accountID int64
err = s.db.QueryRow("SELECT id FROM accounts WHERE name = 'test_account'").Scan(&accountID)
if err != nil {
t.Fatalf("query account id: %v", err)
}
// Insert a whitelist_in row referencing the account.
_, err = s.db.Exec("INSERT INTO whitelist_in(account_id, address) VALUES(?, 'test@example.com')", accountID)
if err != nil {
t.Fatalf("insert whitelist_in: %v", err)
}
// Verify the whitelist_in row exists.
var count int
err = s.db.QueryRow("SELECT COUNT(*) FROM whitelist_in WHERE account_id = ?", accountID).Scan(&count)
if err != nil || count != 1 {
t.Fatalf("whitelist_in row not found: count=%d err=%v", count, err)
}
// Delete the account (should cascade and delete whitelist_in row).
_, err = s.db.Exec("DELETE FROM accounts WHERE name = 'test_account'")
if err != nil {
t.Fatalf("delete account: %v", err)
}
// Verify the whitelist_in row was cascade-deleted.
err = s.db.QueryRow("SELECT COUNT(*) FROM whitelist_in WHERE account_id = ?", accountID).Scan(&count)
if err != nil || count != 0 {
t.Fatalf("whitelist_in row not cascade-deleted: count=%d err=%v", count, err)
}
}