config_test.go

  1package config
  2
  3import (
  4	"os"
  5	"path/filepath"
  6	"reflect"
  7	"testing"
  8	"time"
  9
 10	"github.com/zalando/go-keyring"
 11)
 12
 13// TestSaveAndLoadConfig verifies that the config can be saved to and loaded from a file correctly.
 14func TestSaveAndLoadConfig(t *testing.T) {
 15	// Use an in-memory mock keyring so tests do not interact with the host OS keyring
 16	keyring.MockInit()
 17
 18	// Create a temporary directory for the test to avoid interfering with actual user config.
 19	tempDir := t.TempDir()
 20
 21	// Temporarily override the user home directory to our temp directory.
 22	// This ensures that our config file is written to a predictable, temporary location.
 23	t.Setenv("HOME", tempDir)
 24
 25	// Define a sample configuration to save with multiple accounts.
 26	expectedConfig := &Config{
 27		Accounts: []Account{
 28			{
 29				ID:              "test-id-1",
 30				Name:            "Test User",
 31				Email:           "test@example.com",
 32				Password:        "supersecret",
 33				ServiceProvider: "gmail",
 34				SendAsEmail:     "alias@example.com",
 35				SC:              &SessionCache{},
 36			},
 37			{
 38				ID:              "test-id-2",
 39				Name:            "Custom User",
 40				Email:           "custom@example.com",
 41				Password:        "customsecret",
 42				ServiceProvider: "custom",
 43				IMAPServer:      "imap.custom.com",
 44				IMAPPort:        993,
 45				SMTPServer:      "smtp.custom.com",
 46				SMTPPort:        587,
 47				CatchAll:        true,
 48				SC:              &SessionCache{},
 49			},
 50		},
 51	}
 52
 53	// Attempt to save the configuration.
 54	err := SaveConfig(expectedConfig)
 55	if err != nil {
 56		t.Fatalf("SaveConfig() failed: %v", err)
 57	}
 58
 59	// Attempt to load the configuration back.
 60	loadedConfig, err := LoadConfig()
 61	if err != nil {
 62		t.Fatalf("LoadConfig() failed: %v", err)
 63	}
 64
 65	// Compare the loaded configuration with the original one.
 66	// reflect.DeepEqual is used for a deep comparison of the structs.
 67	if !reflect.DeepEqual(loadedConfig, expectedConfig) {
 68		t.Errorf("Loaded config does not match expected config.\nGot:  %+v\nWant: %+v", loadedConfig, expectedConfig)
 69	}
 70}
 71
 72// TestAccountGetIMAPServer tests the logic that determines the IMAP server address.
 73func TestAccountGetIMAPServer(t *testing.T) {
 74	testCases := []struct {
 75		name    string
 76		account Account
 77		want    string
 78	}{
 79		{"Gmail", Account{ServiceProvider: "gmail"}, "imap.gmail.com"},
 80		{"iCloud", Account{ServiceProvider: "icloud"}, "imap.mail.me.com"},
 81		{"Custom", Account{ServiceProvider: "custom", IMAPServer: "imap.custom.com"}, "imap.custom.com"},
 82		{"Unsupported", Account{ServiceProvider: "yahoo"}, ""},
 83		{"Empty", Account{ServiceProvider: ""}, ""},
 84	}
 85
 86	for _, tc := range testCases {
 87		t.Run(tc.name, func(t *testing.T) {
 88			got := tc.account.GetIMAPServer()
 89			if got != tc.want {
 90				t.Errorf("GetIMAPServer() = %q, want %q", got, tc.want)
 91			}
 92		})
 93	}
 94}
 95
 96// TestAccountGetSMTPServer tests the logic that determines the SMTP server address.
 97func TestAccountGetSMTPServer(t *testing.T) {
 98	testCases := []struct {
 99		name    string
100		account Account
101		want    string
102	}{
103		{"Gmail", Account{ServiceProvider: "gmail"}, "smtp.gmail.com"},
104		{"iCloud", Account{ServiceProvider: "icloud"}, "smtp.mail.me.com"},
105		{"Custom", Account{ServiceProvider: "custom", SMTPServer: "smtp.custom.com"}, "smtp.custom.com"},
106		{"Unsupported", Account{ServiceProvider: "yahoo"}, ""},
107		{"Empty", Account{ServiceProvider: ""}, ""},
108	}
109
110	for _, tc := range testCases {
111		t.Run(tc.name, func(t *testing.T) {
112			got := tc.account.GetSMTPServer()
113			if got != tc.want {
114				t.Errorf("GetSMTPServer() = %q, want %q", got, tc.want)
115			}
116		})
117	}
118}
119
120// TestConfigAddRemoveAccount tests adding and removing accounts from config.
121func TestConfigAddRemoveAccount(t *testing.T) {
122	// Use an in-memory mock keyring to test the deletion step cleanly
123	keyring.MockInit()
124
125	cfg := &Config{}
126
127	// Add an account
128	account := Account{
129		Name:            "Test",
130		Email:           "test@example.com",
131		ServiceProvider: "gmail",
132	}
133	cfg.AddAccount(account)
134
135	if len(cfg.Accounts) != 1 {
136		t.Fatalf("Expected 1 account, got %d", len(cfg.Accounts))
137	}
138
139	// Check that ID was auto-generated
140	if cfg.Accounts[0].ID == "" {
141		t.Error("Expected account ID to be auto-generated")
142	}
143
144	// Remove the account
145	accountID := cfg.Accounts[0].ID
146	removed := cfg.RemoveAccount(accountID)
147	if !removed {
148		t.Error("RemoveAccount should return true when account exists")
149	}
150
151	if len(cfg.Accounts) != 0 {
152		t.Fatalf("Expected 0 accounts after removal, got %d", len(cfg.Accounts))
153	}
154
155	// Try to remove non-existent account
156	removed = cfg.RemoveAccount("non-existent")
157	if removed {
158		t.Error("RemoveAccount should return false for non-existent account")
159	}
160}
161
162// TestConfigGetAccountByID tests retrieving accounts by ID.
163func TestConfigGetAccountByID(t *testing.T) {
164	cfg := &Config{
165		Accounts: []Account{
166			{ID: "id-1", Email: "test1@example.com"},
167			{ID: "id-2", Email: "test2@example.com"},
168		},
169	}
170
171	account := cfg.GetAccountByID("id-1")
172	if account == nil {
173		t.Fatal("Expected to find account with id-1")
174	}
175	if account.Email != "test1@example.com" {
176		t.Errorf("Expected email test1@example.com, got %s", account.Email)
177	}
178
179	// Non-existent ID
180	account = cfg.GetAccountByID("non-existent")
181	if account != nil {
182		t.Error("Expected nil for non-existent account ID")
183	}
184}
185
186// TestConfigGetAccountByEmail tests retrieving accounts by email.
187func TestConfigGetAccountByEmail(t *testing.T) {
188	cfg := &Config{
189		Accounts: []Account{
190			{ID: "id-1", Email: "test1@example.com"},
191			{ID: "id-2", Email: "test2@example.com"},
192		},
193	}
194
195	account := cfg.GetAccountByEmail("test2@example.com")
196	if account == nil {
197		t.Fatal("Expected to find account with test2@example.com")
198	}
199	if account.ID != "id-2" {
200		t.Errorf("Expected ID id-2, got %s", account.ID)
201	}
202
203	// Non-existent email
204	account = cfg.GetAccountByEmail("nonexistent@example.com")
205	if account != nil {
206		t.Error("Expected nil for non-existent account email")
207	}
208}
209
210func TestAddContactNormalizesEmailAndDeduplicates(t *testing.T) {
211	t.Setenv("HOME", t.TempDir())
212
213	if err := AddContactForAccount("Alice", "Alice@Example.com", "account-1"); err != nil {
214		t.Fatalf("AddContactForAccount() failed: %v", err)
215	}
216	if err := AddContactForAccount("", "alice@example.com", "account-1"); err != nil {
217		t.Fatalf("AddContactForAccount() failed: %v", err)
218	}
219
220	cache, err := LoadContactsCache()
221	if err != nil {
222		t.Fatalf("LoadContactsCache() failed: %v", err)
223	}
224
225	if len(cache.Contacts) != 1 {
226		t.Fatalf("Expected 1 contact after deduplication, got %d", len(cache.Contacts))
227	}
228
229	contact := cache.Contacts[0]
230	if contact.Email != "alice@example.com" {
231		t.Errorf("Expected normalized email alice@example.com, got %s", contact.Email)
232	}
233	usage := contact.Usage["account-1"]
234	if usage.UseCount != 2 {
235		t.Errorf("Expected UseCount 2 after duplicate add, got %d", usage.UseCount)
236	}
237}
238
239func TestMigrateContactsCacheUsageExpandsLegacyUsage(t *testing.T) {
240	t.Setenv("HOME", t.TempDir())
241
242	lastUsed := time.Date(2024, 3, 1, 12, 0, 0, 0, time.UTC)
243	path, err := GetContactsCachePath()
244	if err != nil {
245		t.Fatalf("GetContactsCachePath() failed: %v", err)
246	}
247	if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
248		t.Fatalf("MkdirAll() failed: %v", err)
249	}
250	legacyJSON := `{"contacts":[{"name":"Alice","email":"alice@example.com","last_used":"` + lastUsed.Format(time.RFC3339) + `","use_count":7}]}`
251	if err := os.WriteFile(path, []byte(legacyJSON), 0600); err != nil {
252		t.Fatalf("WriteFile() failed: %v", err)
253	}
254
255	if err := MigrateContactsCacheUsage([]string{"account-1", "account-2"}); err != nil {
256		t.Fatalf("MigrateContactsCacheUsage() failed: %v", err)
257	}
258
259	cache, err := LoadContactsCache()
260	if err != nil {
261		t.Fatalf("LoadContactsCache() failed: %v", err)
262	}
263	if len(cache.Contacts) != 1 {
264		t.Fatalf("Expected 1 contact, got %d", len(cache.Contacts))
265	}
266	for _, accountID := range []string{"account-1", "account-2"} {
267		usage, ok := cache.Contacts[0].Usage[accountID]
268		if !ok {
269			t.Fatalf("Expected usage for %s", accountID)
270		}
271		if usage.UseCount != 7 || !usage.LastUsed.Equal(lastUsed) {
272			t.Fatalf("Unexpected usage for %s: %+v", accountID, usage)
273		}
274	}
275	if _, ok := cache.Contacts[0].Usage[legacyContactUsageKey]; ok {
276		t.Fatal("Legacy usage key should be removed after migration")
277	}
278}
279
280func TestSearchContactsForAccountFiltersAndSortsByUsage(t *testing.T) {
281	t.Setenv("HOME", t.TempDir())
282
283	now := time.Now()
284	cache := &ContactsCache{Contacts: []Contact{
285		{
286			Name:  "Alice",
287			Email: "alice@example.com",
288			Usage: map[string]ContactUsage{
289				"account-1": {UseCount: 1, LastUsed: now},
290			},
291		},
292		{
293			Name:  "Alicia",
294			Email: "alicia@example.com",
295			Usage: map[string]ContactUsage{
296				"account-2": {UseCount: 9, LastUsed: now.Add(time.Hour)},
297			},
298		},
299		{
300			Name:  "Alina",
301			Email: "alina@example.com",
302			Usage: map[string]ContactUsage{
303				"account-1": {UseCount: 3, LastUsed: now.Add(-time.Hour)},
304			},
305		},
306	}}
307	if err := SaveContactsCache(cache); err != nil {
308		t.Fatalf("SaveContactsCache() failed: %v", err)
309	}
310
311	matches := SearchContactsForAccount("ali", "account-1")
312	if len(matches) != 2 {
313		t.Fatalf("Expected 2 account-1 matches, got %d", len(matches))
314	}
315	if matches[0].Email != "alina@example.com" {
316		t.Fatalf("Expected highest account-1 usage first, got %s", matches[0].Email)
317	}
318}
319
320func TestCleanupAccountCacheRemovesOnlyTargetAccountData(t *testing.T) {
321	t.Setenv("HOME", t.TempDir())
322
323	now := time.Now()
324	emailFor := func(accountID string, uid uint32) CachedEmail {
325		return CachedEmail{
326			UID:       uid,
327			From:      accountID + "@example.com",
328			Subject:   "subject",
329			Date:      now,
330			AccountID: accountID,
331		}
332	}
333
334	if err := SaveEmailCache(&EmailCache{Emails: []CachedEmail{
335		emailFor("account-1", 1),
336		emailFor("account-2", 2),
337	}}); err != nil {
338		t.Fatalf("SaveEmailCache() failed: %v", err)
339	}
340	if err := SaveFolderCache(&FolderCache{Accounts: []CachedFolders{
341		{AccountID: "account-1", Folders: []string{"INBOX"}},
342		{AccountID: "account-2", Folders: []string{"INBOX", "Sent"}},
343	}}); err != nil {
344		t.Fatalf("SaveFolderCache() failed: %v", err)
345	}
346	if err := SaveFolderEmailCache("INBOX", []CachedEmail{
347		emailFor("account-1", 1),
348		emailFor("account-2", 2),
349	}); err != nil {
350		t.Fatalf("SaveFolderEmailCache(INBOX) failed: %v", err)
351	}
352	if err := SaveFolderEmailCache("OnlyDeleted", []CachedEmail{
353		emailFor("account-1", 3),
354	}); err != nil {
355		t.Fatalf("SaveFolderEmailCache(OnlyDeleted) failed: %v", err)
356	}
357	if err := SaveDraftsCache(&DraftsCache{Drafts: []Draft{
358		{ID: "draft-1", AccountID: "account-1", Subject: "delete"},
359		{ID: "draft-2", AccountID: "account-2", Subject: "keep"},
360	}}); err != nil {
361		t.Fatalf("SaveDraftsCache() failed: %v", err)
362	}
363	if err := SaveContactsCache(&ContactsCache{Contacts: []Contact{
364		{
365			Name:  "Shared",
366			Email: "shared@example.com",
367			Usage: map[string]ContactUsage{
368				"account-1": {UseCount: 1, LastUsed: now},
369				"account-2": {UseCount: 2, LastUsed: now},
370			},
371		},
372		{
373			Name:  "Only Deleted",
374			Email: "deleted@example.com",
375			Usage: map[string]ContactUsage{
376				"account-1": {UseCount: 1, LastUsed: now},
377			},
378		},
379	}}); err != nil {
380		t.Fatalf("SaveContactsCache() failed: %v", err)
381	}
382	if err := SaveEmailBody("INBOX", CachedEmailBody{
383		UID:       1,
384		AccountID: "account-1",
385		Body:      "delete",
386	}, 1<<20); err != nil {
387		t.Fatalf("SaveEmailBody(account-1) failed: %v", err)
388	}
389	if err := SaveEmailBody("INBOX", CachedEmailBody{
390		UID:       2,
391		AccountID: "account-2",
392		Body:      "keep",
393	}, 1<<20); err != nil {
394		t.Fatalf("SaveEmailBody(account-2) failed: %v", err)
395	}
396	if err := SaveEmailBody("OnlyDeleted", CachedEmailBody{
397		UID:       3,
398		AccountID: "account-1",
399		Body:      "delete",
400	}, 1<<20); err != nil {
401		t.Fatalf("SaveEmailBody(OnlyDeleted) failed: %v", err)
402	}
403
404	if err := CleanupAccountCache("account-1"); err != nil {
405		t.Fatalf("CleanupAccountCache() failed: %v", err)
406	}
407
408	emailCache, err := LoadEmailCache()
409	if err != nil {
410		t.Fatalf("LoadEmailCache() failed: %v", err)
411	}
412	if len(emailCache.Emails) != 1 || emailCache.Emails[0].AccountID != "account-2" {
413		t.Fatalf("Unexpected email cache after cleanup: %+v", emailCache.Emails)
414	}
415
416	folderCache, err := LoadFolderCache()
417	if err != nil {
418		t.Fatalf("LoadFolderCache() failed: %v", err)
419	}
420	if len(folderCache.Accounts) != 1 || folderCache.Accounts[0].AccountID != "account-2" {
421		t.Fatalf("Unexpected folder cache after cleanup: %+v", folderCache.Accounts)
422	}
423
424	folderEmails, err := LoadFolderEmailCache("INBOX")
425	if err != nil {
426		t.Fatalf("LoadFolderEmailCache(INBOX) failed: %v", err)
427	}
428	if len(folderEmails) != 1 || folderEmails[0].AccountID != "account-2" {
429		t.Fatalf("Unexpected folder emails after cleanup: %+v", folderEmails)
430	}
431	onlyDeletedFolderPath, err := folderEmailCacheFile("OnlyDeleted")
432	if err != nil {
433		t.Fatalf("folderEmailCacheFile() failed: %v", err)
434	}
435	if _, err := os.Stat(onlyDeletedFolderPath); !os.IsNotExist(err) {
436		t.Fatalf("Expected folder email cache with only deleted account to be removed, stat err=%v", err)
437	}
438
439	draftsCache, err := LoadDraftsCache()
440	if err != nil {
441		t.Fatalf("LoadDraftsCache() failed: %v", err)
442	}
443	if len(draftsCache.Drafts) != 1 || draftsCache.Drafts[0].AccountID != "account-2" {
444		t.Fatalf("Unexpected drafts after cleanup: %+v", draftsCache.Drafts)
445	}
446
447	contactsCache, err := LoadContactsCache()
448	if err != nil {
449		t.Fatalf("LoadContactsCache() failed: %v", err)
450	}
451	if len(contactsCache.Contacts) != 1 || contactsCache.Contacts[0].Email != "shared@example.com" {
452		t.Fatalf("Unexpected contacts after cleanup: %+v", contactsCache.Contacts)
453	}
454	if _, ok := contactsCache.Contacts[0].Usage["account-1"]; ok {
455		t.Fatal("Deleted account usage should be removed from shared contact")
456	}
457	if _, ok := contactsCache.Contacts[0].Usage["account-2"]; !ok {
458		t.Fatal("Remaining account usage should stay on shared contact")
459	}
460
461	bodyCache, err := LoadEmailBodyCache("INBOX")
462	if err != nil {
463		t.Fatalf("LoadEmailBodyCache(INBOX) failed: %v", err)
464	}
465	if len(bodyCache.Bodies) != 1 || bodyCache.Bodies[0].AccountID != "account-2" {
466		t.Fatalf("Unexpected body cache after cleanup: %+v", bodyCache.Bodies)
467	}
468	onlyDeletedBodyPath, err := bodyCacheFile("OnlyDeleted")
469	if err != nil {
470		t.Fatalf("bodyCacheFile() failed: %v", err)
471	}
472	if _, err := os.Stat(onlyDeletedBodyPath); !os.IsNotExist(err) {
473		t.Fatalf("Expected body cache with only deleted account to be removed, stat err=%v", err)
474	}
475}
476
477// TestConfigHasAccounts tests the HasAccounts method.
478func TestConfigHasAccounts(t *testing.T) {
479	cfg := &Config{}
480	if cfg.HasAccounts() {
481		t.Error("Expected HasAccounts to return false for empty config")
482	}
483
484	cfg.AddAccount(Account{Email: "test@example.com"})
485	if !cfg.HasAccounts() {
486		t.Error("Expected HasAccounts to return true after adding account")
487	}
488}
489
490// TestAccountGetPorts tests the port retrieval methods.
491func TestAccountGetPorts(t *testing.T) {
492	// Gmail account should use default ports
493	gmailAccount := Account{ServiceProvider: "gmail"}
494	if gmailAccount.GetIMAPPort() != 993 {
495		t.Errorf("Expected Gmail IMAP port 993, got %d", gmailAccount.GetIMAPPort())
496	}
497	if gmailAccount.GetSMTPPort() != 587 {
498		t.Errorf("Expected Gmail SMTP port 587, got %d", gmailAccount.GetSMTPPort())
499	}
500
501	// Custom account with custom ports
502	customAccount := Account{
503		ServiceProvider: "custom",
504		IMAPPort:        1993,
505		SMTPPort:        1587,
506	}
507	if customAccount.GetIMAPPort() != 1993 {
508		t.Errorf("Expected custom IMAP port 1993, got %d", customAccount.GetIMAPPort())
509	}
510	if customAccount.GetSMTPPort() != 1587 {
511		t.Errorf("Expected custom SMTP port 1587, got %d", customAccount.GetSMTPPort())
512	}
513
514	// Custom account with default ports (0 means use default)
515	customDefaultAccount := Account{ServiceProvider: "custom"}
516	if customDefaultAccount.GetIMAPPort() != 993 {
517		t.Errorf("Expected default IMAP port 993 for custom with no port, got %d", customDefaultAccount.GetIMAPPort())
518	}
519	if customDefaultAccount.GetSMTPPort() != 587 {
520		t.Errorf("Expected default SMTP port 587 for custom with no port, got %d", customDefaultAccount.GetSMTPPort())
521	}
522}
523
524func TestAccountSendIdentityHelpers(t *testing.T) {
525	t.Run("send as takes precedence", func(t *testing.T) {
526		account := Account{
527			Name:        "Alias User",
528			Email:       "login@gmail.com",
529			FetchEmail:  "inbox@gmail.com",
530			SendAsEmail: "alias@example.com",
531		}
532
533		if got := account.GetFetchEmail(); got != "inbox@gmail.com" {
534			t.Fatalf("GetFetchEmail() = %q, want %q", got, "inbox@gmail.com")
535		}
536		if got := account.GetSendAsEmail(); got != "alias@example.com" {
537			t.Fatalf("GetSendAsEmail() = %q, want %q", got, "alias@example.com")
538		}
539		if got := account.FormatFromHeader(); got != "Alias User <alias@example.com>" {
540			t.Fatalf("FormatFromHeader() = %q, want %q", got, "Alias User <alias@example.com>")
541		}
542	})
543
544	t.Run("send as falls back to fetch then login", func(t *testing.T) {
545		account := Account{
546			Name:       "Fallback User",
547			Email:      "login@gmail.com",
548			FetchEmail: "inbox@gmail.com",
549		}
550		if got := account.GetSendAsEmail(); got != "inbox@gmail.com" {
551			t.Fatalf("GetSendAsEmail() = %q, want %q", got, "inbox@gmail.com")
552		}
553
554		account.FetchEmail = ""
555		if got := account.GetSendAsEmail(); got != "login@gmail.com" {
556			t.Fatalf("GetSendAsEmail() = %q, want %q", got, "login@gmail.com")
557		}
558	})
559
560	t.Run("format from header avoids double wrapping", func(t *testing.T) {
561		account := Account{
562			Name:        "Account Name",
563			SendAsEmail: "Custom Name <custom@example.com>",
564		}
565		if got := account.FormatFromHeader(); got != "Custom Name <custom@example.com>" {
566			t.Fatalf("FormatFromHeader() = %q, want %q", got, "Custom Name <custom@example.com>")
567		}
568	})
569}
570
571func TestTranslateDateFormat(t *testing.T) {
572	testCases := []struct {
573		name   string
574		input  string
575		want   string
576		sample string // expected output of time.Format for a fixed sample instant
577	}{
578		{"EU preset", DateFormatEU, "02/01/2006 15:04", "17/04/2026 09:05"},
579		{"US preset", DateFormatUS, "01/02/2006 03:04 PM", "04/17/2026 09:05 AM"},
580		{"ISO preset", DateFormatISO, "2006-01-02 15:04", "2026-04-17 09:05"},
581		{"seconds", "HH:MM:SS", "15:04:05", "09:05:00"},
582		{"explicit minutes", "YYYY-MM-DD HH:mm", "2006-01-02 15:04", "2026-04-17 09:05"},
583		{"2-digit year", "DD/MM/YY", "02/01/06", "17/04/26"},
584		{"literal passthrough", "some text", "some text", "some text"},
585	}
586
587	sample := time.Date(2026, 4, 17, 9, 5, 0, 0, time.UTC)
588	for _, tc := range testCases {
589		t.Run(tc.name, func(t *testing.T) {
590			got := translateDateFormat(tc.input)
591			if got != tc.want {
592				t.Fatalf("translateDateFormat(%q) = %q, want %q", tc.input, got, tc.want)
593			}
594			if rendered := sample.Format(got); rendered != tc.sample {
595				t.Fatalf("sample.Format(%q) = %q, want %q", got, rendered, tc.sample)
596			}
597		})
598	}
599}
600
601func TestConfigGetDateFormatDefault(t *testing.T) {
602	c := &Config{}
603	got := c.GetDateFormat()
604	want := translateDateFormat(DateFormatEU)
605	if got != want {
606		t.Fatalf("GetDateFormat() with empty DateFormat = %q, want %q", got, want)
607	}
608}
609
610func TestConfigGetDateFormatCustom(t *testing.T) {
611	c := &Config{DateFormat: "DD/MM/YYYY HH:MM"}
612	if got, want := c.GetDateFormat(), "02/01/2006 15:04"; got != want {
613		t.Fatalf("GetDateFormat() = %q, want %q", got, want)
614	}
615}