Files
emcli/internal/store/store_test.go
T
2026-06-23 20:16:15 +01:00

168 lines
5.1 KiB
Go

package store
import (
"database/sql"
"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 != "2" {
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 != "2" {
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)
}
}
func TestOpenMigratesV1AddsFromAddress(t *testing.T) {
p := filepath.Join(t.TempDir(), "emcli.db")
// Hand-build a v1 database: accounts table WITHOUT from_address, a settings
// table pinned at schema_version=1, and one pre-existing account row.
raw, err := sql.Open("sqlite", p)
if err != nil {
t.Fatalf("sql.Open: %v", err)
}
const v1Schema = `
CREATE TABLE settings (key TEXT PRIMARY KEY, value TEXT NOT NULL);
CREATE TABLE accounts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE NOT NULL,
mode TEXT NOT NULL,
imap_host TEXT NOT NULL,
imap_port INTEGER NOT NULL,
imap_security TEXT NOT NULL,
smtp_host TEXT, smtp_port INTEGER, smtp_security TEXT,
auth_type TEXT NOT NULL,
username TEXT NOT NULL,
enc_password BLOB,
enc_oauth_client_id BLOB, enc_oauth_client_secret BLOB, enc_oauth_refresh_token BLOB,
whitelist_in_enabled INTEGER NOT NULL DEFAULT 0,
whitelist_out_enabled INTEGER NOT NULL DEFAULT 0,
subject_regex TEXT,
process_backlog INTEGER NOT NULL DEFAULT 0
);
INSERT INTO settings(key,value) VALUES ('schema_version','1');
INSERT INTO accounts(name,mode,imap_host,imap_port,imap_security,auth_type,username)
VALUES ('legacy','RO','imap.example.com',993,'tls','password','login@example.com');
`
if _, err := raw.Exec(v1Schema); err != nil {
t.Fatalf("seed v1 schema: %v", err)
}
raw.Close()
// Open via the store: the migration must add from_address and bump to v2.
s, err := Open(p)
if err != nil {
t.Fatalf("Open (migrate): %v", err)
}
defer s.Close()
if v, _ := s.GetSetting("schema_version"); v != "2" {
t.Fatalf("schema_version after migrate: %q, want 2", v)
}
// ListAccounts SELECTs from_address; it would error if the column were missing.
accs, err := s.ListAccounts()
if err != nil {
t.Fatalf("ListAccounts after migrate: %v", err)
}
if len(accs) != 1 || accs[0].FromAddress != "" {
t.Fatalf("legacy account wrong after migrate: %+v", accs)
}
if got := accs[0].SendFrom(); got != "login@example.com" {
t.Fatalf("legacy account should send from username, got %q", got)
}
}