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