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}