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}