config_test.go

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