config_test.go

  1package config
  2
  3import (
  4	"reflect"
  5	"testing"
  6
  7	"github.com/zalando/go-keyring"
  8)
  9
 10// TestSaveAndLoadConfig verifies that the config can be saved to and loaded from a file correctly.
 11func TestSaveAndLoadConfig(t *testing.T) {
 12	// Use an in-memory mock keyring so tests do not interact with the host OS keyring
 13	keyring.MockInit()
 14
 15	// Create a temporary directory for the test to avoid interfering with actual user config.
 16	tempDir := t.TempDir()
 17
 18	// Temporarily override the user home directory to our temp directory.
 19	// This ensures that our config file is written to a predictable, temporary location.
 20	t.Setenv("HOME", tempDir)
 21
 22	// Define a sample configuration to save with multiple accounts.
 23	expectedConfig := &Config{
 24		Accounts: []Account{
 25			{
 26				ID:              "test-id-1",
 27				Name:            "Test User",
 28				Email:           "test@example.com",
 29				Password:        "supersecret",
 30				ServiceProvider: "gmail",
 31				SendAsEmail:     "alias@example.com",
 32			},
 33			{
 34				ID:              "test-id-2",
 35				Name:            "Custom User",
 36				Email:           "custom@example.com",
 37				Password:        "customsecret",
 38				ServiceProvider: "custom",
 39				IMAPServer:      "imap.custom.com",
 40				IMAPPort:        993,
 41				SMTPServer:      "smtp.custom.com",
 42				SMTPPort:        587,
 43			},
 44		},
 45	}
 46
 47	// Attempt to save the configuration.
 48	err := SaveConfig(expectedConfig)
 49	if err != nil {
 50		t.Fatalf("SaveConfig() failed: %v", err)
 51	}
 52
 53	// Attempt to load the configuration back.
 54	loadedConfig, err := LoadConfig()
 55	if err != nil {
 56		t.Fatalf("LoadConfig() failed: %v", err)
 57	}
 58
 59	// Compare the loaded configuration with the original one.
 60	// reflect.DeepEqual is used for a deep comparison of the structs.
 61	if !reflect.DeepEqual(loadedConfig, expectedConfig) {
 62		t.Errorf("Loaded config does not match expected config.\nGot:  %+v\nWant: %+v", loadedConfig, expectedConfig)
 63	}
 64}
 65
 66// TestAccountGetIMAPServer tests the logic that determines the IMAP server address.
 67func TestAccountGetIMAPServer(t *testing.T) {
 68	testCases := []struct {
 69		name    string
 70		account Account
 71		want    string
 72	}{
 73		{"Gmail", Account{ServiceProvider: "gmail"}, "imap.gmail.com"},
 74		{"iCloud", Account{ServiceProvider: "icloud"}, "imap.mail.me.com"},
 75		{"Custom", Account{ServiceProvider: "custom", IMAPServer: "imap.custom.com"}, "imap.custom.com"},
 76		{"Unsupported", Account{ServiceProvider: "yahoo"}, ""},
 77		{"Empty", Account{ServiceProvider: ""}, ""},
 78	}
 79
 80	for _, tc := range testCases {
 81		t.Run(tc.name, func(t *testing.T) {
 82			got := tc.account.GetIMAPServer()
 83			if got != tc.want {
 84				t.Errorf("GetIMAPServer() = %q, want %q", got, tc.want)
 85			}
 86		})
 87	}
 88}
 89
 90// TestAccountGetSMTPServer tests the logic that determines the SMTP server address.
 91func TestAccountGetSMTPServer(t *testing.T) {
 92	testCases := []struct {
 93		name    string
 94		account Account
 95		want    string
 96	}{
 97		{"Gmail", Account{ServiceProvider: "gmail"}, "smtp.gmail.com"},
 98		{"iCloud", Account{ServiceProvider: "icloud"}, "smtp.mail.me.com"},
 99		{"Custom", Account{ServiceProvider: "custom", SMTPServer: "smtp.custom.com"}, "smtp.custom.com"},
100		{"Unsupported", Account{ServiceProvider: "yahoo"}, ""},
101		{"Empty", Account{ServiceProvider: ""}, ""},
102	}
103
104	for _, tc := range testCases {
105		t.Run(tc.name, func(t *testing.T) {
106			got := tc.account.GetSMTPServer()
107			if got != tc.want {
108				t.Errorf("GetSMTPServer() = %q, want %q", got, tc.want)
109			}
110		})
111	}
112}
113
114// TestConfigAddRemoveAccount tests adding and removing accounts from config.
115func TestConfigAddRemoveAccount(t *testing.T) {
116	// Use an in-memory mock keyring to test the deletion step cleanly
117	keyring.MockInit()
118
119	cfg := &Config{}
120
121	// Add an account
122	account := Account{
123		Name:            "Test",
124		Email:           "test@example.com",
125		ServiceProvider: "gmail",
126	}
127	cfg.AddAccount(account)
128
129	if len(cfg.Accounts) != 1 {
130		t.Fatalf("Expected 1 account, got %d", len(cfg.Accounts))
131	}
132
133	// Check that ID was auto-generated
134	if cfg.Accounts[0].ID == "" {
135		t.Error("Expected account ID to be auto-generated")
136	}
137
138	// Remove the account
139	accountID := cfg.Accounts[0].ID
140	removed := cfg.RemoveAccount(accountID)
141	if !removed {
142		t.Error("RemoveAccount should return true when account exists")
143	}
144
145	if len(cfg.Accounts) != 0 {
146		t.Fatalf("Expected 0 accounts after removal, got %d", len(cfg.Accounts))
147	}
148
149	// Try to remove non-existent account
150	removed = cfg.RemoveAccount("non-existent")
151	if removed {
152		t.Error("RemoveAccount should return false for non-existent account")
153	}
154}
155
156// TestConfigGetAccountByID tests retrieving accounts by ID.
157func TestConfigGetAccountByID(t *testing.T) {
158	cfg := &Config{
159		Accounts: []Account{
160			{ID: "id-1", Email: "test1@example.com"},
161			{ID: "id-2", Email: "test2@example.com"},
162		},
163	}
164
165	account := cfg.GetAccountByID("id-1")
166	if account == nil {
167		t.Fatal("Expected to find account with id-1")
168	}
169	if account.Email != "test1@example.com" {
170		t.Errorf("Expected email test1@example.com, got %s", account.Email)
171	}
172
173	// Non-existent ID
174	account = cfg.GetAccountByID("non-existent")
175	if account != nil {
176		t.Error("Expected nil for non-existent account ID")
177	}
178}
179
180// TestConfigGetAccountByEmail tests retrieving accounts by email.
181func TestConfigGetAccountByEmail(t *testing.T) {
182	cfg := &Config{
183		Accounts: []Account{
184			{ID: "id-1", Email: "test1@example.com"},
185			{ID: "id-2", Email: "test2@example.com"},
186		},
187	}
188
189	account := cfg.GetAccountByEmail("test2@example.com")
190	if account == nil {
191		t.Fatal("Expected to find account with test2@example.com")
192	}
193	if account.ID != "id-2" {
194		t.Errorf("Expected ID id-2, got %s", account.ID)
195	}
196
197	// Non-existent email
198	account = cfg.GetAccountByEmail("nonexistent@example.com")
199	if account != nil {
200		t.Error("Expected nil for non-existent account email")
201	}
202}
203
204func TestAddContactNormalizesEmailAndDeduplicates(t *testing.T) {
205	t.Setenv("HOME", t.TempDir())
206
207	if err := AddContact("Alice", "Alice@Example.com"); err != nil {
208		t.Fatalf("AddContact() failed: %v", err)
209	}
210	if err := AddContact("", "alice@example.com"); err != nil {
211		t.Fatalf("AddContact() failed: %v", err)
212	}
213
214	cache, err := LoadContactsCache()
215	if err != nil {
216		t.Fatalf("LoadContactsCache() failed: %v", err)
217	}
218
219	if len(cache.Contacts) != 1 {
220		t.Fatalf("Expected 1 contact after deduplication, got %d", len(cache.Contacts))
221	}
222
223	contact := cache.Contacts[0]
224	if contact.Email != "alice@example.com" {
225		t.Errorf("Expected normalized email alice@example.com, got %s", contact.Email)
226	}
227	if contact.UseCount != 2 {
228		t.Errorf("Expected UseCount 2 after duplicate add, got %d", contact.UseCount)
229	}
230}
231
232// TestConfigHasAccounts tests the HasAccounts method.
233func TestConfigHasAccounts(t *testing.T) {
234	cfg := &Config{}
235	if cfg.HasAccounts() {
236		t.Error("Expected HasAccounts to return false for empty config")
237	}
238
239	cfg.AddAccount(Account{Email: "test@example.com"})
240	if !cfg.HasAccounts() {
241		t.Error("Expected HasAccounts to return true after adding account")
242	}
243}
244
245// TestAccountGetPorts tests the port retrieval methods.
246func TestAccountGetPorts(t *testing.T) {
247	// Gmail account should use default ports
248	gmailAccount := Account{ServiceProvider: "gmail"}
249	if gmailAccount.GetIMAPPort() != 993 {
250		t.Errorf("Expected Gmail IMAP port 993, got %d", gmailAccount.GetIMAPPort())
251	}
252	if gmailAccount.GetSMTPPort() != 587 {
253		t.Errorf("Expected Gmail SMTP port 587, got %d", gmailAccount.GetSMTPPort())
254	}
255
256	// Custom account with custom ports
257	customAccount := Account{
258		ServiceProvider: "custom",
259		IMAPPort:        1993,
260		SMTPPort:        1587,
261	}
262	if customAccount.GetIMAPPort() != 1993 {
263		t.Errorf("Expected custom IMAP port 1993, got %d", customAccount.GetIMAPPort())
264	}
265	if customAccount.GetSMTPPort() != 1587 {
266		t.Errorf("Expected custom SMTP port 1587, got %d", customAccount.GetSMTPPort())
267	}
268
269	// Custom account with default ports (0 means use default)
270	customDefaultAccount := Account{ServiceProvider: "custom"}
271	if customDefaultAccount.GetIMAPPort() != 993 {
272		t.Errorf("Expected default IMAP port 993 for custom with no port, got %d", customDefaultAccount.GetIMAPPort())
273	}
274	if customDefaultAccount.GetSMTPPort() != 587 {
275		t.Errorf("Expected default SMTP port 587 for custom with no port, got %d", customDefaultAccount.GetSMTPPort())
276	}
277}
278
279func TestAccountSendIdentityHelpers(t *testing.T) {
280	t.Run("send as takes precedence", func(t *testing.T) {
281		account := Account{
282			Name:        "Alias User",
283			Email:       "login@gmail.com",
284			FetchEmail:  "inbox@gmail.com",
285			SendAsEmail: "alias@example.com",
286		}
287
288		if got := account.GetFetchEmail(); got != "inbox@gmail.com" {
289			t.Fatalf("GetFetchEmail() = %q, want %q", got, "inbox@gmail.com")
290		}
291		if got := account.GetSendAsEmail(); got != "alias@example.com" {
292			t.Fatalf("GetSendAsEmail() = %q, want %q", got, "alias@example.com")
293		}
294		if got := account.FormatFromHeader(); got != "Alias User <alias@example.com>" {
295			t.Fatalf("FormatFromHeader() = %q, want %q", got, "Alias User <alias@example.com>")
296		}
297	})
298
299	t.Run("send as falls back to fetch then login", func(t *testing.T) {
300		account := Account{
301			Name:       "Fallback User",
302			Email:      "login@gmail.com",
303			FetchEmail: "inbox@gmail.com",
304		}
305		if got := account.GetSendAsEmail(); got != "inbox@gmail.com" {
306			t.Fatalf("GetSendAsEmail() = %q, want %q", got, "inbox@gmail.com")
307		}
308
309		account.FetchEmail = ""
310		if got := account.GetSendAsEmail(); got != "login@gmail.com" {
311			t.Fatalf("GetSendAsEmail() = %q, want %q", got, "login@gmail.com")
312		}
313	})
314}