folder_cache_test.go

  1package config
  2
  3import (
  4	"os"
  5	"path/filepath"
  6	"reflect"
  7	"sort"
  8	"testing"
  9)
 10
 11// folderCacheTestSetup redirects HOME to a per-test temp directory so
 12// cacheDir() resolves under the temp tree and the cache file does not
 13// collide with the user's real ~/.cache/matcha state. USERPROFILE is set
 14// for the same reason on Windows, where os.UserHomeDir() reads it instead
 15// of HOME.
 16func folderCacheTestSetup(t *testing.T) string {
 17	t.Helper()
 18	resetLRU()
 19	tempDir := t.TempDir()
 20	t.Setenv("HOME", tempDir)
 21	t.Setenv("USERPROFILE", tempDir)
 22	return tempDir
 23}
 24
 25func TestSaveLoadFolderCache_RoundTrip(t *testing.T) {
 26	folderCacheTestSetup(t)
 27
 28	expected := &FolderCache{
 29		Accounts: []CachedFolders{
 30			{AccountID: "acct-1", Folders: []string{"INBOX", "Sent", "Drafts"}},
 31			{AccountID: "acct-2", Folders: []string{"INBOX", "Archive"}},
 32		},
 33	}
 34
 35	if err := SaveFolderCache(expected); err != nil {
 36		t.Fatalf("SaveFolderCache: %v", err)
 37	}
 38
 39	got, err := LoadFolderCache()
 40	if err != nil {
 41		t.Fatalf("LoadFolderCache: %v", err)
 42	}
 43
 44	if len(got.Accounts) != len(expected.Accounts) {
 45		t.Fatalf("accounts: got %d, want %d", len(got.Accounts), len(expected.Accounts))
 46	}
 47	for i, acc := range got.Accounts {
 48		if acc.AccountID != expected.Accounts[i].AccountID {
 49			t.Errorf("account %d ID: got %q, want %q", i, acc.AccountID, expected.Accounts[i].AccountID)
 50		}
 51		if !reflect.DeepEqual(acc.Folders, expected.Accounts[i].Folders) {
 52			t.Errorf("account %d folders: got %v, want %v", i, acc.Folders, expected.Accounts[i].Folders)
 53		}
 54	}
 55
 56	// SaveFolderCache stamps UpdatedAt at write time; round-trip should preserve a non-zero value.
 57	if got.UpdatedAt.IsZero() {
 58		t.Error("UpdatedAt should be set after SaveFolderCache")
 59	}
 60}
 61
 62func TestSaveAccountFolders_InvalidatesExistingEntry(t *testing.T) {
 63	folderCacheTestSetup(t)
 64
 65	if err := SaveAccountFolders("acct-1", []string{"INBOX", "Sent"}, nil); err != nil {
 66		t.Fatalf("first SaveAccountFolders: %v", err)
 67	}
 68
 69	// Overwriting the same accountID must replace the folder list, not append.
 70	if err := SaveAccountFolders("acct-1", []string{"INBOX", "Trash"}, nil); err != nil {
 71		t.Fatalf("second SaveAccountFolders: %v", err)
 72	}
 73
 74	folders, _ := GetCachedFolders("acct-1")
 75	want := []string{"INBOX", "Trash"}
 76	sort.Strings(folders)
 77	sort.Strings(want)
 78	if !reflect.DeepEqual(folders, want) {
 79		t.Errorf("after overwrite: got %v, want %v", folders, want)
 80	}
 81
 82	cache, err := LoadFolderCache()
 83	if err != nil {
 84		t.Fatalf("LoadFolderCache: %v", err)
 85	}
 86	if len(cache.Accounts) != 1 {
 87		t.Errorf("accounts: got %d, want 1 (overwrite must not duplicate)", len(cache.Accounts))
 88	}
 89}
 90
 91func TestSaveAccountFolders_AddsNewAccount(t *testing.T) {
 92	folderCacheTestSetup(t)
 93
 94	if err := SaveAccountFolders("acct-1", []string{"INBOX"}, nil); err != nil {
 95		t.Fatalf("SaveAccountFolders acct-1: %v", err)
 96	}
 97	if err := SaveAccountFolders("acct-2", []string{"INBOX", "Spam"}, nil); err != nil {
 98		t.Fatalf("SaveAccountFolders acct-2: %v", err)
 99	}
100
101	cache, err := LoadFolderCache()
102	if err != nil {
103		t.Fatalf("LoadFolderCache: %v", err)
104	}
105	if len(cache.Accounts) != 2 {
106		t.Errorf("accounts: got %d, want 2", len(cache.Accounts))
107	}
108	if folders, _ := GetCachedFolders("acct-1"); !reflect.DeepEqual(folders, []string{"INBOX"}) {
109		t.Errorf("acct-1: got %v", folders)
110	}
111	if folders, _ := GetCachedFolders("acct-2"); !reflect.DeepEqual(folders, []string{"INBOX", "Spam"}) {
112		t.Errorf("acct-2: got %v", folders)
113	}
114}
115
116func TestLoadFolderCache_CorruptFileReturnsError(t *testing.T) {
117	folderCacheTestSetup(t)
118
119	path, err := folderCacheFile()
120	if err != nil {
121		t.Fatalf("folderCacheFile: %v", err)
122	}
123	if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
124		t.Fatalf("MkdirAll: %v", err)
125	}
126	if err := os.WriteFile(path, []byte("{not valid json"), 0600); err != nil {
127		t.Fatalf("WriteFile: %v", err)
128	}
129
130	cache, err := LoadFolderCache()
131	if err == nil {
132		t.Errorf("LoadFolderCache should fail on corrupt JSON; got cache=%+v", cache)
133	}
134}
135
136func TestSaveAccountFolders_RecoversFromCorruptCache(t *testing.T) {
137	folderCacheTestSetup(t)
138
139	path, err := folderCacheFile()
140	if err != nil {
141		t.Fatalf("folderCacheFile: %v", err)
142	}
143	if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
144		t.Fatalf("MkdirAll: %v", err)
145	}
146	if err := os.WriteFile(path, []byte("garbage"), 0600); err != nil {
147		t.Fatalf("WriteFile: %v", err)
148	}
149
150	// SaveAccountFolders treats a load error as "start fresh" (see the
151	// `cache = &FolderCache{}` fall-back) so a corrupt file must be
152	// silently replaced with a valid one, not fail the whole save.
153	if err := SaveAccountFolders("acct-1", []string{"INBOX"}, nil); err != nil {
154		t.Fatalf("SaveAccountFolders should recover from corrupt cache: %v", err)
155	}
156	if folders, _ := GetCachedFolders("acct-1"); !reflect.DeepEqual(folders, []string{"INBOX"}) {
157		t.Errorf("after recovery: got %v, want [INBOX]", folders)
158	}
159}
160
161func TestSaveFolderCache_EmptyAccounts(t *testing.T) {
162	folderCacheTestSetup(t)
163
164	empty := &FolderCache{Accounts: []CachedFolders{}}
165	if err := SaveFolderCache(empty); err != nil {
166		t.Fatalf("SaveFolderCache(empty): %v", err)
167	}
168	got, err := LoadFolderCache()
169	if err != nil {
170		t.Fatalf("LoadFolderCache: %v", err)
171	}
172	if len(got.Accounts) != 0 {
173		t.Errorf("accounts: got %d, want 0", len(got.Accounts))
174	}
175}
176
177func TestSaveAccountFolders_EmptyFolderList(t *testing.T) {
178	folderCacheTestSetup(t)
179
180	if err := SaveAccountFolders("acct-1", nil, nil); err != nil {
181		t.Fatalf("SaveAccountFolders nil folders: %v", err)
182	}
183	if err := SaveAccountFolders("acct-2", []string{}, nil); err != nil {
184		t.Fatalf("SaveAccountFolders empty folders: %v", err)
185	}
186
187	if folders, _ := GetCachedFolders("acct-1"); len(folders) != 0 {
188		t.Errorf("acct-1: got %v, want empty", folders)
189	}
190	if folders, _ := GetCachedFolders("acct-2"); len(folders) != 0 {
191		t.Errorf("acct-2: got %v, want empty", folders)
192	}
193
194	// Both accounts should still be tracked even though their folder
195	// lists are empty -- the write itself is meaningful (it records
196	// "we asked the server and got nothing").
197	cache, err := LoadFolderCache()
198	if err != nil {
199		t.Fatalf("LoadFolderCache: %v", err)
200	}
201	if len(cache.Accounts) != 2 {
202		t.Errorf("accounts: got %d, want 2", len(cache.Accounts))
203	}
204}
205
206func TestGetCachedFolders_MissingAccount(t *testing.T) {
207	folderCacheTestSetup(t)
208
209	if err := SaveAccountFolders("acct-1", []string{"INBOX"}, nil); err != nil {
210		t.Fatalf("SaveAccountFolders: %v", err)
211	}
212	if folders, _ := GetCachedFolders("missing"); folders != nil {
213		t.Errorf("missing account: got %v, want nil", folders)
214	}
215}
216
217func TestGetCachedFolders_NoCacheFile(t *testing.T) {
218	folderCacheTestSetup(t)
219
220	// No cache file has been written yet.
221	if folders, _ := GetCachedFolders("acct-1"); folders != nil {
222		t.Errorf("no cache file: got %v, want nil", folders)
223	}
224}