Line data Source code
1 : #include "test_helpers.h"
2 : #include "local_store.h"
3 : #include "fs_util.h"
4 : #include "raii.h"
5 : #include <string.h>
6 : #include <stdlib.h>
7 : #include <unistd.h>
8 :
9 : /* ── Helper to set up a clean test environment ────────────────────────── */
10 :
11 4 : static void setup_test_env(const char *home) {
12 4 : setenv("HOME", home, 1);
13 4 : unsetenv("XDG_DATA_HOME");
14 4 : local_store_init("imaps://test.example.com", "testuser");
15 4 : }
16 :
17 : /* ── Message store tests ─────────────────────────────────────────────── */
18 :
19 1 : void test_local_msg_store(void) {
20 1 : char *old_home = getenv("HOME");
21 1 : setup_test_env("/tmp/email-cli-store-test");
22 :
23 1 : const char *folder = "INBOX";
24 1 : const int uid = 137;
25 :
26 : /* Pre-clean */
27 1 : unlink("/tmp/email-cli-store-test/.local/share/email-cli/accounts/"
28 : "testuser/store/INBOX/7/3/137.eml");
29 :
30 : /* 1. Not stored initially */
31 1 : ASSERT(local_msg_exists(folder, uid) == 0,
32 : "local_msg_exists: should be 0 before save");
33 :
34 : /* 2. Save and verify */
35 1 : const char *content = "From: test@example.com\r\nDate: Mon, 30 Mar 2026 12:00:00 +0000\r\n"
36 : "Subject: Test\r\n\r\nHello!";
37 1 : int rc = local_msg_save(folder, uid, content, strlen(content));
38 1 : ASSERT(rc == 0, "local_msg_save: should return 0");
39 1 : ASSERT(local_msg_exists(folder, uid) == 1,
40 : "local_msg_exists: should be 1 after save");
41 :
42 : /* 3. Load and verify content */
43 : {
44 2 : RAII_STRING char *loaded = local_msg_load(folder, uid);
45 1 : ASSERT(loaded != NULL, "local_msg_load: should not be NULL");
46 1 : ASSERT(strcmp(loaded, content) == 0, "local_msg_load: content mismatch");
47 : }
48 :
49 : /* 4. Different UIDs are independent */
50 1 : ASSERT(local_msg_exists(folder, 99) == 0,
51 : "local_msg_exists: UID 99 should not exist");
52 :
53 : /* 5. Reverse digit bucketing: UID 42 → 2/4/ */
54 1 : const char *c2 = "Subject: UID 42\r\n\r\nBody";
55 1 : local_msg_save(folder, 42, c2, strlen(c2));
56 1 : ASSERT(local_msg_exists(folder, 42) == 1,
57 : "local_msg_exists: UID 42 after save (bucket 2/4)");
58 :
59 : /* 6. UID 5 → 5/0/ (single digit pads to 0) */
60 1 : const char *c3 = "Subject: UID 5\r\n\r\nBody";
61 1 : local_msg_save(folder, 5, c3, strlen(c3));
62 1 : ASSERT(local_msg_exists(folder, 5) == 1,
63 : "local_msg_exists: UID 5 after save (bucket 5/0)");
64 :
65 : /* Cleanup */
66 1 : unlink("/tmp/email-cli-store-test/.local/share/email-cli/accounts/"
67 : "testuser/store/INBOX/7/3/137.eml");
68 1 : unlink("/tmp/email-cli-store-test/.local/share/email-cli/accounts/"
69 : "testuser/store/INBOX/2/4/42.eml");
70 1 : unlink("/tmp/email-cli-store-test/.local/share/email-cli/accounts/"
71 : "testuser/store/INBOX/5/0/5.eml");
72 :
73 1 : if (old_home) setenv("HOME", old_home, 1);
74 0 : else unsetenv("HOME");
75 : }
76 :
77 : /* ── Header eviction tests ───────────────────────────────────────────── */
78 :
79 1 : void test_local_hdr_evict(void) {
80 1 : char *old_home = getenv("HOME");
81 1 : setup_test_env("/tmp/email-cli-hdr-evict-test");
82 :
83 1 : const char *folder = "INBOX";
84 :
85 1 : local_hdr_save(folder, 10, "header-10", 9);
86 1 : local_hdr_save(folder, 20, "header-20", 9);
87 :
88 1 : ASSERT(local_hdr_exists(folder, 10) == 1, "hdr_evict: UID 10 before");
89 1 : ASSERT(local_hdr_exists(folder, 20) == 1, "hdr_evict: UID 20 before");
90 :
91 1 : int keep[] = {20};
92 1 : local_hdr_evict_stale(folder, keep, 1);
93 :
94 1 : ASSERT(local_hdr_exists(folder, 10) == 0, "hdr_evict: UID 10 evicted");
95 1 : ASSERT(local_hdr_exists(folder, 20) == 1, "hdr_evict: UID 20 kept");
96 :
97 : /* Cleanup */
98 1 : local_hdr_evict_stale(folder, NULL, 0);
99 :
100 1 : if (old_home) setenv("HOME", old_home, 1);
101 0 : else unsetenv("HOME");
102 : }
103 :
104 : /* ── Index tests ─────────────────────────────────────────────────────── */
105 :
106 1 : void test_local_index(void) {
107 1 : char *old_home = getenv("HOME");
108 1 : setup_test_env("/tmp/email-cli-index-test");
109 :
110 1 : const char *msg =
111 : "From: noreply@github.com\r\n"
112 : "Date: Tue, 15 Mar 2026 10:30:00 +0100\r\n"
113 : "Subject: Test\r\n\r\nBody";
114 :
115 1 : int rc = local_index_update("INBOX", 42, msg);
116 1 : ASSERT(rc == 0, "local_index_update: should return 0");
117 :
118 : /* Verify from index exists */
119 1 : const char *from_path =
120 : "/tmp/email-cli-index-test/.local/share/email-cli/accounts/"
121 : "testuser/index/from/github.com/noreply";
122 : {
123 2 : RAII_FILE FILE *fp = fopen(from_path, "r");
124 1 : ASSERT(fp != NULL, "from index file should exist");
125 1 : if (fp) {
126 1 : char line[256];
127 1 : ASSERT(fgets(line, sizeof(line), fp) != NULL,
128 : "from index should have a line");
129 1 : ASSERT(strstr(line, "INBOX/42") != NULL,
130 : "from index should contain INBOX/42");
131 : }
132 : }
133 :
134 : /* Verify date index exists */
135 1 : const char *date_path =
136 : "/tmp/email-cli-index-test/.local/share/email-cli/accounts/"
137 : "testuser/index/date/2026/03/15";
138 : {
139 2 : RAII_FILE FILE *fp = fopen(date_path, "r");
140 1 : ASSERT(fp != NULL, "date index file should exist");
141 1 : if (fp) {
142 1 : char line[256];
143 1 : ASSERT(fgets(line, sizeof(line), fp) != NULL,
144 : "date index should have a line");
145 1 : ASSERT(strstr(line, "INBOX/42") != NULL,
146 : "date index should contain INBOX/42");
147 : }
148 : }
149 :
150 : /* Duplicate should not be added */
151 1 : local_index_update("INBOX", 42, msg);
152 : {
153 2 : RAII_FILE FILE *fp = fopen(from_path, "r");
154 1 : int count = 0;
155 1 : char line[256];
156 2 : while (fp && fgets(line, sizeof(line), fp)) count++;
157 1 : ASSERT(count == 1, "from index should have exactly 1 entry (no dupes)");
158 : }
159 :
160 : /* Cleanup */
161 1 : if (from_path) unlink(from_path);
162 1 : if (date_path) unlink(date_path);
163 :
164 1 : if (old_home) setenv("HOME", old_home, 1);
165 0 : else unsetenv("HOME");
166 : }
167 :
168 : /* ── Manifest tests ──────────────────────────────────────────────────── */
169 :
170 1 : void test_manifest(void) {
171 1 : char *old_home = getenv("HOME");
172 1 : setup_test_env("/tmp/email-cli-manifest-test");
173 :
174 : /* Pre-clean */
175 1 : unlink("/tmp/email-cli-manifest-test/.local/share/email-cli/"
176 : "accounts/testuser/manifests/INBOX.tsv");
177 :
178 : /* 1. Load non-existent manifest returns NULL */
179 1 : Manifest *m = manifest_load("INBOX");
180 1 : ASSERT(m == NULL, "manifest_load: NULL for missing file");
181 :
182 : /* 2. Create manifest, add entries, save */
183 1 : m = calloc(1, sizeof(Manifest));
184 1 : ASSERT(m != NULL, "manifest: calloc");
185 1 : manifest_upsert(m, 42, strdup("Alice <alice@example.com>"),
186 : strdup("Hello World"),
187 : strdup("2024-03-15 10:00"), MSG_FLAG_UNSEEN);
188 1 : manifest_upsert(m, 137, strdup("Bob <bob@test.org>"),
189 : strdup("Re: Meeting"),
190 : strdup("2024-03-16 14:30"), 0);
191 1 : ASSERT(m->count == 2, "manifest: 2 entries after upsert");
192 :
193 1 : int rc = manifest_save("INBOX", m);
194 1 : ASSERT(rc == 0, "manifest_save: returns 0");
195 :
196 : /* 3. Load back and verify */
197 1 : manifest_free(m);
198 1 : m = manifest_load("INBOX");
199 1 : ASSERT(m != NULL, "manifest_load: not NULL after save");
200 1 : ASSERT(m->count == 2, "manifest_load: 2 entries");
201 :
202 1 : ManifestEntry *e42 = manifest_find(m, 42);
203 1 : ASSERT(e42 != NULL, "manifest_find: UID 42 found");
204 1 : ASSERT(strcmp(e42->from, "Alice <alice@example.com>") == 0,
205 : "manifest: UID 42 from correct");
206 1 : ASSERT(strcmp(e42->subject, "Hello World") == 0,
207 : "manifest: UID 42 subject correct");
208 1 : ASSERT(strcmp(e42->date, "2024-03-15 10:00") == 0,
209 : "manifest: UID 42 date correct");
210 :
211 1 : ManifestEntry *e137 = manifest_find(m, 137);
212 1 : ASSERT(e137 != NULL, "manifest_find: UID 137 found");
213 1 : ASSERT(strcmp(e137->subject, "Re: Meeting") == 0,
214 : "manifest: UID 137 subject correct");
215 :
216 : /* 4. Upsert updates existing entry */
217 1 : manifest_upsert(m, 42, strdup("Alice Updated"),
218 : strdup("Updated Subject"),
219 : strdup("2024-03-15 11:00"), 0 /* no flags */);
220 1 : ASSERT(m->count == 2, "manifest: still 2 after upsert-update");
221 1 : e42 = manifest_find(m, 42);
222 1 : ASSERT(strcmp(e42->subject, "Updated Subject") == 0,
223 : "manifest: upsert updated subject");
224 :
225 : /* 5. manifest_find returns NULL for missing UID */
226 1 : ASSERT(manifest_find(m, 999) == NULL,
227 : "manifest_find: NULL for missing UID");
228 :
229 : /* 6. manifest_retain keeps only specified UIDs */
230 1 : int keep[] = {137};
231 1 : manifest_retain(m, keep, 1);
232 1 : ASSERT(m->count == 1, "manifest_retain: 1 entry after retain");
233 1 : ASSERT(manifest_find(m, 137) != NULL, "manifest_retain: UID 137 kept");
234 1 : ASSERT(manifest_find(m, 42) == NULL, "manifest_retain: UID 42 removed");
235 :
236 : /* 7. Nested folder manifest */
237 1 : Manifest *m2 = calloc(1, sizeof(Manifest));
238 1 : manifest_upsert(m2, 1, strdup("Test"), strdup("Nested"), strdup("2024-01-01 00:00"), 0 /* no flags */);
239 1 : ASSERT(manifest_save("munka/ai", m2) == 0, "manifest_save: nested folder");
240 1 : manifest_free(m2);
241 1 : m2 = manifest_load("munka/ai");
242 1 : ASSERT(m2 != NULL, "manifest_load: nested folder");
243 1 : ASSERT(m2->count == 1, "manifest: nested has 1 entry");
244 1 : manifest_free(m2);
245 :
246 : /* Cleanup */
247 1 : manifest_free(m);
248 1 : if (old_home) setenv("HOME", old_home, 1);
249 0 : else unsetenv("HOME");
250 : }
251 :
252 : /* ── UI preferences tests ────────────────────────────────────────────── */
253 :
254 1 : void test_ui_prefs(void) {
255 1 : char *old_home = getenv("HOME");
256 1 : setenv("HOME", "/tmp/email-cli-ui-pref-test-home", 1);
257 1 : unsetenv("XDG_DATA_HOME");
258 1 : unlink("/tmp/email-cli-ui-pref-test-home/.local/share/email-cli/ui.ini");
259 :
260 1 : ASSERT(ui_pref_get_int("folder_view_mode", 1) == 1,
261 : "ui_pref_get_int: missing key should return default 1");
262 1 : ASSERT(ui_pref_get_int("folder_view_mode", 0) == 0,
263 : "ui_pref_get_int: missing key should return default 0");
264 :
265 1 : ASSERT(ui_pref_set_int("folder_view_mode", 0) == 0,
266 : "ui_pref_set_int: should return 0");
267 1 : ASSERT(ui_pref_get_int("folder_view_mode", 1) == 0,
268 : "ui_pref_get_int: should return stored value 0");
269 :
270 1 : ASSERT(ui_pref_set_int("folder_view_mode", 1) == 0,
271 : "ui_pref_set_int: overwrite should return 0");
272 1 : ASSERT(ui_pref_get_int("folder_view_mode", 0) == 1,
273 : "ui_pref_get_int: should return updated value 1");
274 :
275 1 : ASSERT(ui_pref_set_int("other_pref", 42) == 0,
276 : "ui_pref_set_int: second key should return 0");
277 1 : ASSERT(ui_pref_get_int("folder_view_mode", 0) == 1,
278 : "ui_pref_get_int: first key intact");
279 1 : ASSERT(ui_pref_get_int("other_pref", 0) == 42,
280 : "ui_pref_get_int: second key should return 42");
281 :
282 1 : ASSERT(ui_pref_get_int("no_such_key", 7) == 7,
283 : "ui_pref_get_int: unknown key should return default");
284 :
285 1 : unlink("/tmp/email-cli-ui-pref-test-home/.local/share/email-cli/ui.ini");
286 :
287 1 : if (old_home) setenv("HOME", old_home, 1);
288 0 : else unsetenv("HOME");
289 : }
|