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