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}