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 : #include <stdio.h>
9 :
10 : /* ── Helper to set up a clean test environment ────────────────────────── */
11 :
12 18 : static void setup_test_env(const char *home) {
13 18 : setenv("HOME", home, 1);
14 18 : unsetenv("XDG_DATA_HOME");
15 18 : local_store_init("imaps://test.example.com", "testuser");
16 18 : }
17 :
18 : /* ── Message store tests ─────────────────────────────────────────────── */
19 :
20 1 : void test_local_msg_store(void) {
21 1 : char *old_home = getenv("HOME");
22 1 : setup_test_env("/tmp/email-cli-store-test");
23 :
24 1 : const char *folder = "INBOX";
25 1 : const char *uid = "0000000000000137";
26 :
27 : /* Pre-clean */
28 1 : unlink("/tmp/email-cli-store-test/.local/share/email-cli/accounts/"
29 : "testuser/store/INBOX/7/3/0000000000000137.eml");
30 :
31 : /* 1. Not stored initially */
32 1 : ASSERT(local_msg_exists(folder, uid) == 0,
33 : "local_msg_exists: should be 0 before save");
34 :
35 : /* 2. Save and verify */
36 1 : const char *content = "From: test@example.com\r\nDate: Mon, 30 Mar 2026 12:00:00 +0000\r\n"
37 : "Subject: Test\r\n\r\nHello!";
38 1 : int rc = local_msg_save(folder, uid, content, strlen(content));
39 1 : ASSERT(rc == 0, "local_msg_save: should return 0");
40 1 : ASSERT(local_msg_exists(folder, uid) == 1,
41 : "local_msg_exists: should be 1 after save");
42 :
43 : /* 3. Load and verify content */
44 : {
45 2 : RAII_STRING char *loaded = local_msg_load(folder, uid);
46 1 : ASSERT(loaded != NULL, "local_msg_load: should not be NULL");
47 1 : ASSERT(strcmp(loaded, content) == 0, "local_msg_load: content mismatch");
48 : }
49 :
50 : /* 4. Different UIDs are independent */
51 1 : ASSERT(local_msg_exists(folder, "0000000000000099") == 0,
52 : "local_msg_exists: UID 99 should not exist");
53 :
54 : /* 5. Reverse digit bucketing: UID 42 → 2/4/ */
55 1 : const char *c2 = "Subject: UID 42\r\n\r\nBody";
56 1 : local_msg_save(folder, "0000000000000042", c2, strlen(c2));
57 1 : ASSERT(local_msg_exists(folder, "0000000000000042") == 1,
58 : "local_msg_exists: UID 42 after save (bucket 2/4)");
59 :
60 : /* 6. UID 5 → 5/0/ (single digit pads to 0) */
61 1 : const char *c3 = "Subject: UID 5\r\n\r\nBody";
62 1 : local_msg_save(folder, "0000000000000005", c3, strlen(c3));
63 1 : ASSERT(local_msg_exists(folder, "0000000000000005") == 1,
64 : "local_msg_exists: UID 5 after save (bucket 5/0)");
65 :
66 : /* Cleanup */
67 1 : unlink("/tmp/email-cli-store-test/.local/share/email-cli/accounts/"
68 : "testuser/store/INBOX/7/3/0000000000000137.eml");
69 1 : unlink("/tmp/email-cli-store-test/.local/share/email-cli/accounts/"
70 : "testuser/store/INBOX/2/4/0000000000000042.eml");
71 1 : unlink("/tmp/email-cli-store-test/.local/share/email-cli/accounts/"
72 : "testuser/store/INBOX/5/0/0000000000000005.eml");
73 :
74 1 : if (old_home) setenv("HOME", old_home, 1);
75 0 : else unsetenv("HOME");
76 : }
77 :
78 : /* ── Header eviction tests ───────────────────────────────────────────── */
79 :
80 1 : void test_local_hdr_evict(void) {
81 1 : char *old_home = getenv("HOME");
82 1 : setup_test_env("/tmp/email-cli-hdr-evict-test");
83 :
84 1 : const char *folder = "INBOX";
85 :
86 1 : local_hdr_save(folder, "0000000000000010", "header-10", 9);
87 1 : local_hdr_save(folder, "0000000000000020", "header-20", 9);
88 :
89 1 : ASSERT(local_hdr_exists(folder, "0000000000000010") == 1, "hdr_evict: UID 10 before");
90 1 : ASSERT(local_hdr_exists(folder, "0000000000000020") == 1, "hdr_evict: UID 20 before");
91 :
92 1 : char keep[][17] = {"0000000000000020"};
93 1 : local_hdr_evict_stale(folder, (const char (*)[17])keep, 1);
94 :
95 1 : ASSERT(local_hdr_exists(folder, "0000000000000010") == 0, "hdr_evict: UID 10 evicted");
96 1 : ASSERT(local_hdr_exists(folder, "0000000000000020") == 1, "hdr_evict: UID 20 kept");
97 :
98 : /* Cleanup */
99 1 : local_hdr_evict_stale(folder, NULL, 0);
100 :
101 1 : if (old_home) setenv("HOME", old_home, 1);
102 0 : else unsetenv("HOME");
103 : }
104 :
105 : /* ── Index tests ─────────────────────────────────────────────────────── */
106 :
107 1 : void test_local_index(void) {
108 1 : char *old_home = getenv("HOME");
109 1 : setup_test_env("/tmp/email-cli-index-test");
110 :
111 1 : const char *msg =
112 : "From: noreply@github.com\r\n"
113 : "Date: Tue, 15 Mar 2026 10:30:00 +0100\r\n"
114 : "Subject: Test\r\n\r\nBody";
115 :
116 1 : int rc = local_index_update("INBOX", "0000000000000042", msg);
117 1 : ASSERT(rc == 0, "local_index_update: should return 0");
118 :
119 : /* Verify from index exists */
120 1 : const char *from_path =
121 : "/tmp/email-cli-index-test/.local/share/email-cli/accounts/"
122 : "testuser/index/from/github.com/noreply";
123 : {
124 2 : RAII_FILE FILE *fp = fopen(from_path, "r");
125 1 : ASSERT(fp != NULL, "from index file should exist");
126 1 : if (fp) {
127 : char line[256];
128 1 : ASSERT(fgets(line, sizeof(line), fp) != NULL,
129 : "from index should have a line");
130 1 : ASSERT(strstr(line, "INBOX/0000000000000042") != NULL,
131 : "from index should contain INBOX/0000000000000042");
132 : }
133 : }
134 :
135 : /* Verify date index exists */
136 1 : const char *date_path =
137 : "/tmp/email-cli-index-test/.local/share/email-cli/accounts/"
138 : "testuser/index/date/2026/03/15";
139 : {
140 2 : RAII_FILE FILE *fp = fopen(date_path, "r");
141 1 : ASSERT(fp != NULL, "date index file should exist");
142 1 : if (fp) {
143 : char line[256];
144 1 : ASSERT(fgets(line, sizeof(line), fp) != NULL,
145 : "date index should have a line");
146 1 : ASSERT(strstr(line, "INBOX/0000000000000042") != NULL,
147 : "date index should contain INBOX/0000000000000042");
148 : }
149 : }
150 :
151 : /* Duplicate should not be added */
152 1 : local_index_update("INBOX", "0000000000000042", msg);
153 : {
154 2 : RAII_FILE FILE *fp = fopen(from_path, "r");
155 1 : int count = 0;
156 : char line[256];
157 2 : while (fp && fgets(line, sizeof(line), fp)) count++;
158 1 : ASSERT(count == 1, "from index should have exactly 1 entry (no dupes)");
159 : }
160 :
161 : /* Cleanup */
162 1 : if (from_path) unlink(from_path);
163 1 : if (date_path) unlink(date_path);
164 :
165 1 : if (old_home) setenv("HOME", old_home, 1);
166 0 : else unsetenv("HOME");
167 : }
168 :
169 : /* ── Manifest tests ──────────────────────────────────────────────────── */
170 :
171 1 : void test_manifest(void) {
172 1 : char *old_home = getenv("HOME");
173 1 : setup_test_env("/tmp/email-cli-manifest-test");
174 :
175 : /* Pre-clean */
176 1 : unlink("/tmp/email-cli-manifest-test/.local/share/email-cli/"
177 : "accounts/testuser/manifests/INBOX.tsv");
178 :
179 : /* 1. Load non-existent manifest returns NULL */
180 1 : Manifest *m = manifest_load("INBOX");
181 1 : ASSERT(m == NULL, "manifest_load: NULL for missing file");
182 :
183 : /* 2. Create manifest, add entries, save */
184 1 : m = calloc(1, sizeof(Manifest));
185 1 : ASSERT(m != NULL, "manifest: calloc");
186 1 : manifest_upsert(m, "0000000000000042", strdup("Alice <alice@example.com>"),
187 : strdup("Hello World"),
188 : strdup("2024-03-15 10:00"), MSG_FLAG_UNSEEN);
189 1 : manifest_upsert(m, "0000000000000137", strdup("Bob <bob@test.org>"),
190 : strdup("Re: Meeting"),
191 : strdup("2024-03-16 14:30"), 0);
192 1 : ASSERT(m->count == 2, "manifest: 2 entries after upsert");
193 :
194 1 : int rc = manifest_save("INBOX", m);
195 1 : ASSERT(rc == 0, "manifest_save: returns 0");
196 :
197 : /* 3. Load back and verify */
198 1 : manifest_free(m);
199 1 : m = manifest_load("INBOX");
200 1 : ASSERT(m != NULL, "manifest_load: not NULL after save");
201 1 : ASSERT(m->count == 2, "manifest_load: 2 entries");
202 :
203 1 : ManifestEntry *e42 = manifest_find(m, "0000000000000042");
204 1 : ASSERT(e42 != NULL, "manifest_find: UID 42 found");
205 1 : ASSERT(strcmp(e42->from, "Alice <alice@example.com>") == 0,
206 : "manifest: UID 42 from correct");
207 1 : ASSERT(strcmp(e42->subject, "Hello World") == 0,
208 : "manifest: UID 42 subject correct");
209 1 : ASSERT(strcmp(e42->date, "2024-03-15 10:00") == 0,
210 : "manifest: UID 42 date correct");
211 :
212 1 : ManifestEntry *e137 = manifest_find(m, "0000000000000137");
213 1 : ASSERT(e137 != NULL, "manifest_find: UID 137 found");
214 1 : ASSERT(strcmp(e137->subject, "Re: Meeting") == 0,
215 : "manifest: UID 137 subject correct");
216 :
217 : /* 4. Upsert updates existing entry */
218 1 : manifest_upsert(m, "0000000000000042", strdup("Alice Updated"),
219 : strdup("Updated Subject"),
220 : strdup("2024-03-15 11:00"), 0 /* no flags */);
221 1 : ASSERT(m->count == 2, "manifest: still 2 after upsert-update");
222 1 : e42 = manifest_find(m, "0000000000000042");
223 1 : ASSERT(strcmp(e42->subject, "Updated Subject") == 0,
224 : "manifest: upsert updated subject");
225 :
226 : /* 5. manifest_find returns NULL for missing UID */
227 1 : ASSERT(manifest_find(m, "0000000000000999") == NULL,
228 : "manifest_find: NULL for missing UID");
229 :
230 : /* 6. manifest_retain keeps only specified UIDs */
231 1 : char keep[][17] = {"0000000000000137"};
232 1 : manifest_retain(m, (const char (*)[17])keep, 1);
233 1 : ASSERT(m->count == 1, "manifest_retain: 1 entry after retain");
234 1 : ASSERT(manifest_find(m, "0000000000000137") != NULL, "manifest_retain: UID 137 kept");
235 1 : ASSERT(manifest_find(m, "0000000000000042") == NULL, "manifest_retain: UID 42 removed");
236 :
237 : /* 7. Nested folder manifest */
238 1 : Manifest *m2 = calloc(1, sizeof(Manifest));
239 1 : manifest_upsert(m2, "0000000000000001", strdup("Test"), strdup("Nested"), strdup("2024-01-01 00:00"), 0 /* no flags */);
240 1 : ASSERT(manifest_save("munka/ai", m2) == 0, "manifest_save: nested folder");
241 1 : manifest_free(m2);
242 1 : m2 = manifest_load("munka/ai");
243 1 : ASSERT(m2 != NULL, "manifest_load: nested folder");
244 1 : ASSERT(m2->count == 1, "manifest: nested has 1 entry");
245 1 : manifest_free(m2);
246 :
247 : /* Cleanup */
248 1 : manifest_free(m);
249 1 : if (old_home) setenv("HOME", old_home, 1);
250 0 : else unsetenv("HOME");
251 : }
252 :
253 : /* ── UI preferences tests ────────────────────────────────────────────── */
254 :
255 1 : void test_ui_prefs(void) {
256 1 : char *old_home = getenv("HOME");
257 1 : setenv("HOME", "/tmp/email-cli-ui-pref-test-home", 1);
258 1 : unsetenv("XDG_DATA_HOME");
259 1 : unlink("/tmp/email-cli-ui-pref-test-home/.local/share/email-cli/ui.ini");
260 :
261 1 : ASSERT(ui_pref_get_int("folder_view_mode", 1) == 1,
262 : "ui_pref_get_int: missing key should return default 1");
263 1 : ASSERT(ui_pref_get_int("folder_view_mode", 0) == 0,
264 : "ui_pref_get_int: missing key should return default 0");
265 :
266 1 : ASSERT(ui_pref_set_int("folder_view_mode", 0) == 0,
267 : "ui_pref_set_int: should return 0");
268 1 : ASSERT(ui_pref_get_int("folder_view_mode", 1) == 0,
269 : "ui_pref_get_int: should return stored value 0");
270 :
271 1 : ASSERT(ui_pref_set_int("folder_view_mode", 1) == 0,
272 : "ui_pref_set_int: overwrite should return 0");
273 1 : ASSERT(ui_pref_get_int("folder_view_mode", 0) == 1,
274 : "ui_pref_get_int: should return updated value 1");
275 :
276 1 : ASSERT(ui_pref_set_int("other_pref", 42) == 0,
277 : "ui_pref_set_int: second key should return 0");
278 1 : ASSERT(ui_pref_get_int("folder_view_mode", 0) == 1,
279 : "ui_pref_get_int: first key intact");
280 1 : ASSERT(ui_pref_get_int("other_pref", 0) == 42,
281 : "ui_pref_get_int: second key should return 42");
282 :
283 1 : ASSERT(ui_pref_get_int("no_such_key", 7) == 7,
284 : "ui_pref_get_int: unknown key should return default");
285 :
286 1 : unlink("/tmp/email-cli-ui-pref-test-home/.local/share/email-cli/ui.ini");
287 :
288 : /* ui_pref_get_str / ui_pref_set_str */
289 1 : ASSERT(ui_pref_get_str("str_key") == NULL,
290 : "ui_pref_get_str: missing file returns NULL");
291 :
292 1 : ASSERT(ui_pref_set_str("str_key", "hello") == 0,
293 : "ui_pref_set_str: returns 0 on first write");
294 : {
295 1 : char *v = ui_pref_get_str("str_key");
296 1 : ASSERT(v != NULL, "ui_pref_get_str: returns non-NULL after set");
297 1 : ASSERT(strcmp(v, "hello") == 0, "ui_pref_get_str: value matches");
298 1 : free(v);
299 : }
300 :
301 1 : ASSERT(ui_pref_set_str("str_key", "world") == 0,
302 : "ui_pref_set_str: overwrite returns 0");
303 : {
304 1 : char *v = ui_pref_get_str("str_key");
305 1 : ASSERT(v != NULL, "ui_pref_get_str: overwrite non-NULL");
306 1 : ASSERT(strcmp(v, "world") == 0, "ui_pref_get_str: overwrite value matches");
307 1 : free(v);
308 : }
309 :
310 1 : ASSERT(ui_pref_set_str("another_key", "value2") == 0,
311 : "ui_pref_set_str: second key returns 0");
312 : {
313 1 : char *v1 = ui_pref_get_str("str_key");
314 1 : char *v2 = ui_pref_get_str("another_key");
315 1 : ASSERT(v1 && strcmp(v1, "world") == 0, "ui_pref_get_str: first key intact");
316 1 : ASSERT(v2 && strcmp(v2, "value2") == 0, "ui_pref_get_str: second key correct");
317 1 : free(v1); free(v2);
318 : }
319 :
320 1 : ASSERT(ui_pref_get_str("no_such_str_key") == NULL,
321 : "ui_pref_get_str: unknown key returns NULL");
322 :
323 1 : unlink("/tmp/email-cli-ui-pref-test-home/.local/share/email-cli/ui.ini");
324 :
325 1 : if (old_home) setenv("HOME", old_home, 1);
326 0 : else unsetenv("HOME");
327 : }
328 :
329 : /* ── msg delete tests ────────────────────────────────────────────────── */
330 :
331 1 : void test_local_msg_delete(void) {
332 1 : char *old_home = getenv("HOME");
333 1 : setup_test_env("/tmp/email-cli-delete-test");
334 :
335 1 : const char *folder = "INBOX";
336 1 : const char *uid = "0000000000001000";
337 :
338 : /* Save then delete */
339 1 : int rc = local_msg_save(folder, uid, "test body", 9);
340 1 : ASSERT(rc == 0, "delete: save succeeded");
341 1 : ASSERT(local_msg_exists(folder, uid) == 1, "delete: exists before delete");
342 :
343 1 : int del = local_msg_delete(folder, uid);
344 1 : ASSERT(del == 0, "delete: returns 0");
345 1 : ASSERT(local_msg_exists(folder, uid) == 0, "delete: does not exist after delete");
346 :
347 : /* Deleting a non-existent message should not crash */
348 1 : int del2 = local_msg_delete(folder, "0000000000001001");
349 1 : ASSERT(del2 == 0, "delete: non-existent msg returns 0");
350 :
351 : /* Delete also removes .hdr if it exists */
352 1 : const char *uid2 = "0000000000001002";
353 1 : local_hdr_save(folder, uid2, "from\tsubject\tdate\t\t0", 20);
354 1 : ASSERT(local_hdr_exists(folder, uid2) == 1, "delete: hdr exists before delete");
355 1 : local_msg_delete(folder, uid2);
356 1 : ASSERT(local_hdr_exists(folder, uid2) == 0, "delete: hdr removed by delete");
357 :
358 1 : if (old_home) setenv("HOME", old_home, 1);
359 0 : else unsetenv("HOME");
360 : }
361 :
362 : /* ── index email extraction tests ────────────────────────────────────── */
363 :
364 1 : void test_local_index_email_extraction(void) {
365 1 : char *old_home = getenv("HOME");
366 1 : setup_test_env("/tmp/email-cli-email-extract-test");
367 :
368 : /* "Name <user@domain>" format */
369 : {
370 1 : const char *msg =
371 : "From: Alice <alice@example.com>\r\n"
372 : "Date: Mon, 01 Jan 2024 10:00:00 +0000\r\n"
373 : "Subject: Test\r\n\r\nBody";
374 1 : int rc = local_index_update("INBOX", "0000000000000201", msg);
375 1 : ASSERT(rc == 0, "email_extraction: Name<email> index_update returns 0");
376 :
377 1 : const char *from_path =
378 : "/tmp/email-cli-email-extract-test/.local/share/email-cli/accounts/"
379 : "testuser/index/from/example.com/alice";
380 2 : RAII_FILE FILE *fp = fopen(from_path, "r");
381 1 : ASSERT(fp != NULL, "email_extraction: Name<email> from index created");
382 1 : if (fp) {
383 : char line[256];
384 1 : ASSERT(fgets(line, sizeof(line), fp) != NULL,
385 : "email_extraction: from index has a line");
386 1 : ASSERT(strstr(line, "INBOX/0000000000000201") != NULL,
387 : "email_extraction: from index contains correct ref");
388 : }
389 : }
390 :
391 : /* "<user@domain>" format without display name */
392 : {
393 1 : const char *msg2 =
394 : "From: <bob@test.org>\r\n"
395 : "Date: Tue, 02 Jan 2024 11:00:00 +0000\r\n"
396 : "Subject: Test2\r\n\r\nBody";
397 1 : int rc = local_index_update("INBOX", "0000000000000202", msg2);
398 1 : ASSERT(rc == 0, "email_extraction: <email> index_update returns 0");
399 :
400 1 : const char *from_path2 =
401 : "/tmp/email-cli-email-extract-test/.local/share/email-cli/accounts/"
402 : "testuser/index/from/test.org/bob";
403 2 : RAII_FILE FILE *fp2 = fopen(from_path2, "r");
404 1 : ASSERT(fp2 != NULL, "email_extraction: <email> from index created");
405 : }
406 :
407 : /* Plain "user@domain" format */
408 : {
409 1 : const char *msg3 =
410 : "From: carol@sample.net\r\n"
411 : "Date: Wed, 03 Jan 2024 12:00:00 +0000\r\n"
412 : "Subject: Test3\r\n\r\nBody";
413 1 : int rc = local_index_update("INBOX", "0000000000000203", msg3);
414 1 : ASSERT(rc == 0, "email_extraction: plain email index_update returns 0");
415 :
416 1 : const char *from_path3 =
417 : "/tmp/email-cli-email-extract-test/.local/share/email-cli/accounts/"
418 : "testuser/index/from/sample.net/carol";
419 2 : RAII_FILE FILE *fp3 = fopen(from_path3, "r");
420 1 : ASSERT(fp3 != NULL, "email_extraction: plain email from index created");
421 : }
422 :
423 : /* From header with no @ sign: no from index entry created (graceful skip) */
424 : {
425 1 : const char *msg4 =
426 : "From: noemail\r\n"
427 : "Date: Thu, 04 Jan 2024 13:00:00 +0000\r\n"
428 : "Subject: Test4\r\n\r\nBody";
429 1 : int rc = local_index_update("INBOX", "0000000000000204", msg4);
430 1 : ASSERT(rc == 0, "email_extraction: no-@ returns 0 (graceful)");
431 : }
432 :
433 1 : if (old_home) setenv("HOME", old_home, 1);
434 0 : else unsetenv("HOME");
435 : }
436 :
437 : /* ── trash labels tests ──────────────────────────────────────────────── */
438 :
439 1 : void test_local_trash_labels(void) {
440 1 : char *old_home = getenv("HOME");
441 1 : setup_test_env("/tmp/email-cli-trash-labels-test");
442 :
443 1 : const char *uid = "0000000000002000";
444 1 : const char *labels = "INBOX,Work,Important";
445 :
446 : /* Save and load */
447 1 : int rc = local_trash_labels_save(uid, labels);
448 1 : ASSERT(rc == 0, "trash_labels: save returns 0");
449 :
450 2 : RAII_STRING char *loaded = local_trash_labels_load(uid);
451 1 : ASSERT(loaded != NULL, "trash_labels: load returns non-NULL");
452 1 : ASSERT(strcmp(loaded, labels) == 0, "trash_labels: loaded value matches saved");
453 :
454 : /* Remove */
455 1 : local_trash_labels_remove(uid);
456 2 : RAII_STRING char *after_remove = local_trash_labels_load(uid);
457 1 : ASSERT(after_remove == NULL, "trash_labels: NULL after remove");
458 :
459 : /* Load non-existent returns NULL */
460 2 : RAII_STRING char *missing = local_trash_labels_load("0000000000009999");
461 1 : ASSERT(missing == NULL, "trash_labels: missing uid returns NULL");
462 :
463 : /* Remove non-existent should not crash */
464 1 : local_trash_labels_remove("0000000000009998");
465 :
466 1 : if (old_home) setenv("HOME", old_home, 1);
467 0 : else unsetenv("HOME");
468 : }
469 :
470 : /* ── gmail history id tests ──────────────────────────────────────────── */
471 :
472 1 : void test_local_gmail_history(void) {
473 1 : char *old_home = getenv("HOME");
474 1 : setup_test_env("/tmp/email-cli-gmail-history-test");
475 :
476 : /* Pre-clean any leftover history file from previous runs */
477 1 : unlink("/tmp/email-cli-gmail-history-test/.local/share/email-cli/accounts/"
478 : "testuser/gmail_history_id");
479 :
480 : /* Load when missing returns NULL */
481 2 : RAII_STRING char *none = local_gmail_history_load();
482 1 : ASSERT(none == NULL, "gmail_history: missing returns NULL");
483 :
484 : /* Save and load */
485 1 : int rc = local_gmail_history_save("12345678");
486 1 : ASSERT(rc == 0, "gmail_history: save returns 0");
487 :
488 2 : RAII_STRING char *loaded = local_gmail_history_load();
489 1 : ASSERT(loaded != NULL, "gmail_history: load returns non-NULL");
490 1 : ASSERT(strcmp(loaded, "12345678") == 0, "gmail_history: loaded value matches");
491 :
492 : /* Overwrite with new value */
493 1 : int rc2 = local_gmail_history_save("99999999");
494 1 : ASSERT(rc2 == 0, "gmail_history: overwrite returns 0");
495 :
496 2 : RAII_STRING char *loaded2 = local_gmail_history_load();
497 1 : ASSERT(loaded2 != NULL, "gmail_history: overwrite load returns non-NULL");
498 1 : ASSERT(strcmp(loaded2, "99999999") == 0, "gmail_history: overwrite value correct");
499 :
500 : /* Null history id returns -1 */
501 1 : int rc3 = local_gmail_history_save(NULL);
502 1 : ASSERT(rc3 == -1, "gmail_history: NULL id returns -1");
503 :
504 1 : if (old_home) setenv("HOME", old_home, 1);
505 0 : else unsetenv("HOME");
506 : }
507 :
508 : /* ── local_hdr_get_labels tests ──────────────────────────────────────── */
509 :
510 1 : void test_local_hdr_get_labels(void) {
511 1 : char *old_home = getenv("HOME");
512 1 : setup_test_env("/tmp/email-cli-hdr-labels-test");
513 :
514 1 : const char *folder = ""; /* Gmail flat store uses empty folder */
515 1 : const char *uid = "0000000000003000";
516 :
517 : /* Gmail .hdr format: from\tsubject\tdate\tlabels\tflags */
518 1 : const char *hdr_content = "Alice <alice@example.com>\tHello\t2024-01-01 10:00\tINBOX,Work\t1";
519 1 : int rc = local_hdr_save(folder, uid, hdr_content, strlen(hdr_content));
520 1 : ASSERT(rc == 0, "hdr_labels: save returns 0");
521 :
522 2 : RAII_STRING char *labels = local_hdr_get_labels(folder, uid);
523 1 : ASSERT(labels != NULL, "hdr_labels: get_labels returns non-NULL");
524 1 : ASSERT(strcmp(labels, "INBOX,Work") == 0, "hdr_labels: labels value correct");
525 :
526 : /* Non-Gmail .hdr (no labels field) → NULL */
527 1 : const char *uid2 = "0000000000003001";
528 1 : const char *hdr2 = "Bob\tSubject\tDate"; /* only 3 fields, no 4th tab */
529 1 : local_hdr_save(folder, uid2, hdr2, strlen(hdr2));
530 2 : RAII_STRING char *labels2 = local_hdr_get_labels(folder, uid2);
531 1 : ASSERT(labels2 == NULL, "hdr_labels: non-Gmail hdr returns NULL");
532 :
533 : /* Non-existent uid → NULL */
534 2 : RAII_STRING char *labels3 = local_hdr_get_labels(folder, "0000000000009997");
535 1 : ASSERT(labels3 == NULL, "hdr_labels: missing uid returns NULL");
536 :
537 1 : if (old_home) setenv("HOME", old_home, 1);
538 0 : else unsetenv("HOME");
539 : }
540 :
541 : /* ── local_flag_search tests ────────────────────────────────────────────── */
542 :
543 1 : void test_local_flag_search(void) {
544 1 : char *old_home = getenv("HOME");
545 1 : setup_test_env("/tmp/email-cli-flag-search-test");
546 :
547 : /* Pre-clean */
548 1 : unlink("/tmp/email-cli-flag-search-test/.local/share/email-cli/"
549 : "accounts/testuser/manifests/INBOX.tsv");
550 1 : unlink("/tmp/email-cli-flag-search-test/.local/share/email-cli/"
551 : "accounts/testuser/manifests/Sent.tsv");
552 :
553 : /* Build two manifests across two folders */
554 1 : Manifest *inbox = calloc(1, sizeof(Manifest));
555 1 : manifest_upsert(inbox, "0000000000000001", strdup("Alice"), strdup("Unread in INBOX"),
556 : strdup("2024-03-01 10:00"), MSG_FLAG_UNSEEN);
557 1 : manifest_upsert(inbox, "0000000000000002", strdup("Bob"), strdup("Read in INBOX"),
558 : strdup("2024-03-02 11:00"), 0);
559 1 : manifest_save("INBOX", inbox);
560 1 : manifest_free(inbox);
561 :
562 1 : Manifest *sent = calloc(1, sizeof(Manifest));
563 1 : manifest_upsert(sent, "0000000000000003", strdup("Carol"), strdup("Flagged in Sent"),
564 : strdup("2024-03-03 12:00"), MSG_FLAG_FLAGGED);
565 1 : manifest_upsert(sent, "0000000000000004", strdup("Dave"), strdup("Unread in Sent"),
566 : strdup("2024-03-04 13:00"), MSG_FLAG_UNSEEN);
567 1 : manifest_save("Sent", sent);
568 1 : manifest_free(sent);
569 :
570 : /* Test 1: flag_search for UNSEEN finds UID 1 (INBOX) and UID 4 (Sent) */
571 1 : SearchResult *res = NULL;
572 1 : int cnt = 0;
573 1 : int rc = local_flag_search(MSG_FLAG_UNSEEN, &res, &cnt);
574 1 : ASSERT(rc == 0, "flag_search: returns 0");
575 1 : ASSERT(cnt == 2, "flag_search UNSEEN: 2 results");
576 1 : int found1 = 0, found4 = 0;
577 3 : for (int i = 0; i < cnt; i++) {
578 2 : if (strcmp(res[i].uid, "0000000000000001") == 0) {
579 1 : found1 = 1;
580 1 : ASSERT(strcmp(res[i].folder, "INBOX") == 0, "flag_search: UID1 folder is INBOX");
581 1 : ASSERT(res[i].flags & MSG_FLAG_UNSEEN, "flag_search: UID1 has UNSEEN");
582 : }
583 2 : if (strcmp(res[i].uid, "0000000000000004") == 0) {
584 1 : found4 = 1;
585 1 : ASSERT(strcmp(res[i].folder, "Sent") == 0, "flag_search: UID4 folder is Sent");
586 : }
587 : }
588 1 : ASSERT(found1, "flag_search UNSEEN: UID1 found");
589 1 : ASSERT(found4, "flag_search UNSEEN: UID4 found");
590 1 : local_search_free(res, cnt);
591 :
592 : /* Test 2: flag_search for FLAGGED finds only UID 3 (Sent) */
593 1 : res = NULL; cnt = 0;
594 1 : local_flag_search(MSG_FLAG_FLAGGED, &res, &cnt);
595 1 : ASSERT(cnt == 1, "flag_search FLAGGED: 1 result");
596 1 : ASSERT(strcmp(res[0].uid, "0000000000000003") == 0, "flag_search FLAGGED: UID3");
597 1 : ASSERT(strcmp(res[0].folder, "Sent") == 0, "flag_search FLAGGED: Sent folder");
598 1 : local_search_free(res, cnt);
599 :
600 : /* Test 3: flag_search for both bits returns union (UID1, UID3, UID4) */
601 1 : res = NULL; cnt = 0;
602 1 : local_flag_search(MSG_FLAG_UNSEEN | MSG_FLAG_FLAGGED, &res, &cnt);
603 1 : ASSERT(cnt == 3, "flag_search UNSEEN|FLAGGED: 3 results");
604 1 : local_search_free(res, cnt);
605 :
606 : /* Test 4: UID2 (read, no flags) is never returned */
607 1 : res = NULL; cnt = 0;
608 1 : local_flag_search(MSG_FLAG_UNSEEN, &res, &cnt);
609 1 : int found2 = 0;
610 3 : for (int i = 0; i < cnt; i++)
611 2 : if (strcmp(res[i].uid, "0000000000000002") == 0) found2 = 1;
612 1 : ASSERT(!found2, "flag_search: read UID2 not in UNSEEN results");
613 1 : local_search_free(res, cnt);
614 :
615 1 : if (old_home) setenv("HOME", old_home, 1);
616 0 : else unsetenv("HOME");
617 : }
618 :
619 1 : void test_manifest_count_after_flag_update(void) {
620 1 : char *old_home = getenv("HOME");
621 1 : setup_test_env("/tmp/email-cli-count-update-test");
622 :
623 : /* Pre-clean */
624 1 : unlink("/tmp/email-cli-count-update-test/.local/share/email-cli/"
625 : "accounts/testuser/manifests/INBOX.tsv");
626 :
627 : /* Build a manifest with 3 unread messages */
628 1 : Manifest *m = calloc(1, sizeof(Manifest));
629 1 : manifest_upsert(m, "0000000000000010", strdup("A"), strdup("Msg1"),
630 : strdup("2024-01-01 08:00"), MSG_FLAG_UNSEEN);
631 1 : manifest_upsert(m, "0000000000000020", strdup("B"), strdup("Msg2"),
632 : strdup("2024-01-02 09:00"), MSG_FLAG_UNSEEN);
633 1 : manifest_upsert(m, "0000000000000030", strdup("C"), strdup("Msg3"),
634 : strdup("2024-01-03 10:00"), 0 /* read */);
635 1 : manifest_save("INBOX", m);
636 :
637 : /* Initial count: 2 unread, 0 flagged */
638 1 : int unread = -1, flagged = -1;
639 1 : manifest_count_all_flags(&unread, &flagged, NULL, NULL, NULL, NULL);
640 1 : ASSERT(unread == 2, "count_after_update: initial unread is 2");
641 1 : ASSERT(flagged == 0, "count_after_update: initial flagged is 0");
642 :
643 : /* Simulate user pressing 'n' on UID 10 — mark as read */
644 1 : ManifestEntry *e = manifest_find(m, "0000000000000010");
645 1 : ASSERT(e != NULL, "count_after_update: UID10 found");
646 1 : e->flags &= ~MSG_FLAG_UNSEEN;
647 1 : manifest_save("INBOX", m);
648 :
649 : /* Count should now be 1 */
650 1 : manifest_count_all_flags(&unread, &flagged, NULL, NULL, NULL, NULL);
651 1 : ASSERT(unread == 1, "count_after_update: unread drops to 1 after save");
652 :
653 : /* Mark UID 20 as read too */
654 1 : e = manifest_find(m, "0000000000000020");
655 1 : e->flags &= ~MSG_FLAG_UNSEEN;
656 1 : manifest_save("INBOX", m);
657 :
658 1 : manifest_count_all_flags(&unread, &flagged, NULL, NULL, NULL, NULL);
659 1 : ASSERT(unread == 0, "count_after_update: unread drops to 0");
660 :
661 : /* Flag UID 30 */
662 1 : e = manifest_find(m, "0000000000000030");
663 1 : e->flags |= MSG_FLAG_FLAGGED;
664 1 : manifest_save("INBOX", m);
665 :
666 1 : manifest_count_all_flags(&unread, &flagged, NULL, NULL, NULL, NULL);
667 1 : ASSERT(flagged == 1, "count_after_update: flagged becomes 1");
668 :
669 1 : manifest_free(m);
670 1 : if (old_home) setenv("HOME", old_home, 1);
671 0 : else unsetenv("HOME");
672 : }
673 :
674 1 : void test_flag_search_folder_isolation(void) {
675 1 : char *old_home = getenv("HOME");
676 1 : setup_test_env("/tmp/email-cli-flag-iso-test");
677 :
678 : /* Pre-clean */
679 1 : unlink("/tmp/email-cli-flag-iso-test/.local/share/email-cli/"
680 : "accounts/testuser/manifests/INBOX.tsv");
681 1 : unlink("/tmp/email-cli-flag-iso-test/.local/share/email-cli/"
682 : "accounts/testuser/manifests/Spam.tsv");
683 :
684 : /* Same UID in two different folders (shouldn't happen in practice but verify
685 : * that flag_search reports the correct folder for each) */
686 1 : Manifest *inbox = calloc(1, sizeof(Manifest));
687 1 : manifest_upsert(inbox, "0000000000000099", strdup("X"), strdup("INBOX copy"),
688 : strdup("2024-06-01 00:00"), MSG_FLAG_UNSEEN);
689 1 : manifest_save("INBOX", inbox);
690 1 : manifest_free(inbox);
691 :
692 1 : Manifest *spam = calloc(1, sizeof(Manifest));
693 1 : manifest_upsert(spam, "0000000000000099", strdup("X"), strdup("Spam copy"),
694 : strdup("2024-06-01 00:00"), MSG_FLAG_UNSEEN);
695 1 : manifest_save("Spam", spam);
696 1 : manifest_free(spam);
697 :
698 1 : SearchResult *res = NULL; int cnt = 0;
699 1 : local_flag_search(MSG_FLAG_UNSEEN, &res, &cnt);
700 1 : ASSERT(cnt == 2, "flag_search isolation: 2 results (same UID in 2 folders)");
701 1 : int found_inbox = 0, found_spam = 0;
702 3 : for (int i = 0; i < cnt; i++) {
703 2 : if (strcmp(res[i].folder, "INBOX") == 0) found_inbox = 1;
704 2 : if (strcmp(res[i].folder, "Spam") == 0) found_spam = 1;
705 : }
706 1 : ASSERT(found_inbox, "flag_search isolation: INBOX result present");
707 1 : ASSERT(found_spam, "flag_search isolation: Spam result present");
708 1 : local_search_free(res, cnt);
709 :
710 1 : if (old_home) setenv("HOME", old_home, 1);
711 0 : else unsetenv("HOME");
712 : }
713 :
714 : /* ── local_contacts_update tests ─────────────────────────────────────── */
715 :
716 : #define CONTACTS_PATH \
717 : "/tmp/email-cli-contacts-test/.local/share/email-cli/accounts/testuser/contacts.tsv"
718 :
719 10 : static void contacts_cleanup(void) { unlink(CONTACTS_PATH); }
720 :
721 2 : static int contacts_count_lines(void) {
722 2 : FILE *f = fopen(CONTACTS_PATH, "r");
723 2 : if (!f) return 0;
724 2 : int n = 0; char line[512];
725 5 : while (fgets(line, sizeof(line), f)) n++;
726 2 : fclose(f);
727 2 : return n;
728 : }
729 :
730 7 : static int contacts_has_addr(const char *addr) {
731 7 : FILE *f = fopen(CONTACTS_PATH, "r");
732 7 : if (!f) return 0;
733 7 : char line[512]; int found = 0;
734 9 : while (fgets(line, sizeof(line), f)) {
735 9 : char *tab = strchr(line, '\t');
736 9 : if (tab) *tab = '\0';
737 9 : char *nl = strchr(line, '\n'); if (nl) *nl = '\0';
738 9 : if (strcasecmp(line, addr) == 0) { found = 1; break; }
739 : }
740 7 : fclose(f);
741 7 : return found;
742 : }
743 :
744 3 : static int contacts_get_freq(const char *addr) {
745 3 : FILE *f = fopen(CONTACTS_PATH, "r");
746 3 : if (!f) return -1;
747 3 : char line[512]; int freq = -1;
748 3 : while (fgets(line, sizeof(line), f)) {
749 3 : char copy[512]; snprintf(copy, sizeof(copy), "%s", line);
750 3 : char *t1 = strchr(copy, '\t');
751 3 : if (!t1) continue;
752 3 : *t1 = '\0';
753 3 : if (strcasecmp(copy, addr) != 0) continue;
754 3 : char *t2 = strchr(t1 + 1, '\t');
755 3 : if (t2) freq = atoi(t2 + 1);
756 3 : break;
757 : }
758 3 : fclose(f);
759 3 : return freq;
760 : }
761 :
762 2 : static char *contacts_get_name(const char *addr) {
763 2 : FILE *f = fopen(CONTACTS_PATH, "r");
764 2 : if (!f) return NULL;
765 2 : char line[512]; static char cname[256]; cname[0] = '\0';
766 2 : while (fgets(line, sizeof(line), f)) {
767 2 : char *t1 = strchr(line, '\t');
768 2 : if (!t1) continue;
769 2 : *t1 = '\0';
770 2 : if (strcasecmp(line, addr) != 0) continue;
771 2 : char *t2 = strchr(t1 + 1, '\t');
772 2 : if (t2) { *t2 = '\0'; snprintf(cname, sizeof(cname), "%s", t1 + 1); }
773 2 : break;
774 : }
775 2 : fclose(f);
776 2 : return cname[0] ? cname : NULL;
777 : }
778 :
779 1 : void test_local_contacts_update(void) {
780 1 : char *old_home = getenv("HOME");
781 1 : setup_test_env("/tmp/email-cli-contacts-test");
782 1 : contacts_cleanup();
783 :
784 : /* 1. Bare address in From creates entry with freq=1 */
785 1 : local_contacts_update("alice@example.com", NULL, NULL);
786 1 : ASSERT(contacts_has_addr("alice@example.com"),
787 : "contacts: bare From address added");
788 1 : ASSERT(contacts_get_freq("alice@example.com") == 1,
789 : "contacts: initial frequency is 1");
790 :
791 : /* 2. Display-name form: addr extracted, name stored */
792 1 : contacts_cleanup();
793 1 : local_contacts_update("Alice Smith <alice@example.com>", NULL, NULL);
794 1 : ASSERT(contacts_has_addr("alice@example.com"),
795 : "contacts: addr extracted from display-name form");
796 1 : char *cn = contacts_get_name("alice@example.com");
797 1 : ASSERT(cn && strcmp(cn, "Alice Smith") == 0,
798 : "contacts: display name stored correctly");
799 :
800 : /* 3. Multiple comma-separated addresses in To */
801 1 : contacts_cleanup();
802 1 : local_contacts_update(NULL, "alice@example.com, bob@example.com", NULL);
803 1 : ASSERT(contacts_has_addr("alice@example.com"), "contacts: To addr1 added");
804 1 : ASSERT(contacts_has_addr("bob@example.com"), "contacts: To addr2 added");
805 1 : ASSERT(contacts_count_lines() == 2, "contacts: 2 entries total");
806 :
807 : /* 4. Cc addresses are also collected */
808 1 : contacts_cleanup();
809 1 : local_contacts_update(NULL, NULL, "carol@example.com");
810 1 : ASSERT(contacts_has_addr("carol@example.com"), "contacts: Cc addr added");
811 :
812 : /* 5. Frequency increments on repeated calls */
813 1 : contacts_cleanup();
814 1 : local_contacts_update("alice@example.com", NULL, NULL);
815 1 : local_contacts_update("alice@example.com", NULL, NULL);
816 1 : local_contacts_update("alice@example.com", NULL, NULL);
817 1 : ASSERT(contacts_get_freq("alice@example.com") == 3,
818 : "contacts: frequency increments to 3 on 3 calls");
819 :
820 : /* 6. Case-insensitive deduplication */
821 1 : contacts_cleanup();
822 1 : local_contacts_update("ALICE@EXAMPLE.COM", NULL, NULL);
823 1 : local_contacts_update("alice@example.com", NULL, NULL);
824 1 : ASSERT(contacts_count_lines() == 1,
825 : "contacts: case-insensitive dedup — only 1 entry");
826 1 : ASSERT(contacts_get_freq("alice@example.com") == 2,
827 : "contacts: case-insensitive freq increment");
828 :
829 : /* 7. Name updated when initially absent */
830 1 : contacts_cleanup();
831 1 : local_contacts_update("alice@example.com", NULL, NULL);
832 1 : local_contacts_update("Alice Smith <alice@example.com>", NULL, NULL);
833 1 : char *cn2 = contacts_get_name("alice@example.com");
834 1 : ASSERT(cn2 && strcmp(cn2, "Alice Smith") == 0,
835 : "contacts: name updated when initially missing");
836 :
837 : /* 8. Most frequent addr appears first in file */
838 1 : contacts_cleanup();
839 1 : local_contacts_update("bob@example.com", NULL, NULL);
840 1 : local_contacts_update("alice@example.com", NULL, NULL);
841 1 : local_contacts_update("alice@example.com", NULL, NULL);
842 : {
843 1 : FILE *cf = fopen(CONTACTS_PATH, "r");
844 1 : ASSERT(cf != NULL, "contacts: file exists after updates");
845 1 : if (cf) {
846 1 : char fline[256]; fline[0] = '\0';
847 1 : if (fgets(fline, sizeof(fline), cf) == NULL) fline[0] = '\0';
848 1 : fclose(cf);
849 1 : char *tab = strchr(fline, '\t'); if (tab) *tab = '\0';
850 1 : ASSERT(strcasecmp(fline, "alice@example.com") == 0,
851 : "contacts: most frequent addr is first");
852 : }
853 : }
854 :
855 : /* 9. NULL headers are safe (no crash) */
856 1 : contacts_cleanup();
857 1 : local_contacts_update(NULL, NULL, NULL);
858 1 : ASSERT(1, "contacts: all-NULL headers do not crash");
859 :
860 : /* 10. Semicolon-separated addresses parsed */
861 1 : contacts_cleanup();
862 1 : local_contacts_update(NULL, "dave@example.com; eve@example.com", NULL);
863 1 : ASSERT(contacts_has_addr("dave@example.com"), "contacts: semicolon sep addr1");
864 1 : ASSERT(contacts_has_addr("eve@example.com"), "contacts: semicolon sep addr2");
865 :
866 1 : if (old_home) setenv("HOME", old_home, 1);
867 0 : else unsetenv("HOME");
868 : }
869 :
870 : /* ── local_search (text/subject/from search) ──────────────────────────── */
871 :
872 1 : void test_local_search(void) {
873 1 : char *old_home = getenv("HOME");
874 1 : setup_test_env("/tmp/email-cli-search-test");
875 :
876 : /* Build a manifest with two messages */
877 1 : Manifest *m = calloc(1, sizeof(Manifest));
878 1 : manifest_upsert(m, "0000000000000001", strdup("alice@example.com"),
879 : strdup("Hello World subject"), strdup("2024-01-01 10:00"), 0);
880 1 : manifest_upsert(m, "0000000000000002", strdup("bob@other.org"),
881 : strdup("Different topic"), strdup("2024-01-02 11:00"), MSG_FLAG_UNSEEN);
882 1 : manifest_save("INBOX", m);
883 1 : manifest_free(m);
884 :
885 1 : SearchResult *res = NULL;
886 1 : int cnt = 0;
887 :
888 : /* scope=0: subject search — "Hello" matches UID1 */
889 1 : int rc = local_search("Hello", 0, &res, &cnt);
890 1 : ASSERT(rc == 0, "local_search subject: returns 0");
891 1 : ASSERT(cnt == 1, "local_search subject: 1 result");
892 1 : if (cnt == 1) {
893 1 : ASSERT(strcmp(res[0].uid, "0000000000000001") == 0, "local_search subject: UID1");
894 1 : ASSERT(strcmp(res[0].folder, "INBOX") == 0, "local_search subject: INBOX");
895 : }
896 1 : local_search_free(res, cnt);
897 :
898 : /* scope=0: case-insensitive */
899 1 : res = NULL; cnt = 0;
900 1 : local_search("hello", 0, &res, &cnt);
901 1 : ASSERT(cnt == 1, "local_search case-insensitive: 1 result");
902 1 : local_search_free(res, cnt);
903 :
904 : /* scope=1: from search — "alice" matches UID1 */
905 1 : res = NULL; cnt = 0;
906 1 : local_search("alice", 1, &res, &cnt);
907 1 : ASSERT(cnt == 1, "local_search from: 1 result");
908 1 : if (cnt == 1)
909 1 : ASSERT(strcmp(res[0].uid, "0000000000000001") == 0, "local_search from: UID1");
910 1 : local_search_free(res, cnt);
911 :
912 : /* scope=0: no match → 0 results */
913 1 : res = NULL; cnt = 0;
914 1 : local_search("ZZZNOMATCH99", 0, &res, &cnt);
915 1 : ASSERT(cnt == 0, "local_search no match: 0 results");
916 1 : local_search_free(res, cnt);
917 :
918 : /* NULL / empty query → 0 results, no crash */
919 1 : res = NULL; cnt = 0;
920 1 : local_search(NULL, 0, &res, &cnt);
921 1 : ASSERT(cnt == 0, "local_search NULL query: safe");
922 1 : res = NULL; cnt = 0;
923 1 : local_search("", 0, &res, &cnt);
924 1 : ASSERT(cnt == 0, "local_search empty query: safe");
925 :
926 1 : if (old_home) setenv("HOME", old_home, 1);
927 0 : else unsetenv("HOME");
928 : }
929 :
930 : /* ── local_contacts_rebuild ───────────────────────────────────────────── */
931 :
932 1 : void test_local_contacts_rebuild(void) {
933 1 : char *old_home = getenv("HOME");
934 1 : setup_test_env("/tmp/email-cli-rebuild-test");
935 :
936 : /* Save a message header with From/To so rebuild can extract contacts */
937 1 : const char *hdr = "From: Carol <carol@rebuild.test>\r\nTo: dave@rebuild.test\r\n"
938 : "Subject: Test\r\nDate: Mon, 1 Jan 2024 10:00:00 +0000\r\n";
939 1 : local_hdr_save("INBOX", "0000000000000001", hdr, strlen(hdr));
940 :
941 : /* Save folder list so local_contacts_rebuild can find INBOX */
942 1 : const char *flist[] = { "INBOX", NULL };
943 1 : local_folder_list_save(flist, 1, '/');
944 :
945 : /* Run rebuild — should not crash, creates contacts file */
946 1 : local_contacts_rebuild();
947 :
948 : /* Check the contacts file at the correct path for this test (not CONTACTS_PATH
949 : * which is hardcoded to the contacts-update test directory). */
950 1 : const char *ctacts_file =
951 : "/tmp/email-cli-rebuild-test/.local/share/email-cli/accounts/testuser/contacts.tsv";
952 1 : FILE *cf = fopen(ctacts_file, "r");
953 1 : ASSERT(cf != NULL, "contacts_rebuild: contacts file created");
954 1 : int found = 0;
955 : char cline[256];
956 1 : while (fgets(cline, sizeof(cline), cf)) {
957 1 : if (strstr(cline, "carol@rebuild.test")) { found = 1; break; }
958 : }
959 1 : fclose(cf);
960 1 : ASSERT(found, "contacts_rebuild: From addr present");
961 :
962 1 : if (old_home) setenv("HOME", old_home, 1);
963 0 : else unsetenv("HOME");
964 : }
965 :
966 : /* ── local_pending_append_* ───────────────────────────────────────────── */
967 :
968 1 : void test_local_pending_append(void) {
969 1 : char *old_home = getenv("HOME");
970 1 : setup_test_env("/tmp/email-cli-pending-append-test");
971 : /* Remove leftover from previous test runs */
972 1 : unlink("/tmp/email-cli-pending-append-test/.local/share/email-cli/accounts/testuser/pending_appends.tsv");
973 :
974 1 : int rc1 = local_pending_append_add("Sent", "0000000000000042");
975 1 : int rc2 = local_pending_append_add("Drafts", "0000000000000043");
976 1 : ASSERT(rc1 == 0, "pending_append_add: first entry ok");
977 1 : ASSERT(rc2 == 0, "pending_append_add: second entry ok");
978 :
979 1 : int cnt = 0;
980 1 : PendingAppend *pa = local_pending_append_load(&cnt);
981 1 : ASSERT(cnt == 2, "pending_append_load: 2 entries");
982 1 : if (cnt >= 2) {
983 1 : ASSERT(strcmp(pa[0].folder, "Sent") == 0, "pending_append: first folder");
984 1 : ASSERT(strcmp(pa[0].uid, "0000000000000042") == 0, "pending_append: first uid");
985 1 : ASSERT(strcmp(pa[1].folder, "Drafts") == 0, "pending_append: second folder");
986 : }
987 1 : free(pa);
988 :
989 1 : local_pending_append_remove("Sent", "0000000000000042");
990 1 : cnt = 0;
991 1 : pa = local_pending_append_load(&cnt);
992 1 : ASSERT(cnt == 1, "pending_append_remove: 1 entry left");
993 1 : if (cnt == 1)
994 1 : ASSERT(strcmp(pa[0].folder, "Drafts") == 0, "pending_append_remove: Drafts left");
995 1 : free(pa);
996 :
997 : /* no crash on removing non-existent entry */
998 1 : local_pending_append_remove("INBOX", "0000000000000099");
999 :
1000 1 : if (old_home) setenv("HOME", old_home, 1);
1001 0 : else unsetenv("HOME");
1002 : }
1003 :
1004 : /* ── local_pending_fetch_* ────────────────────────────────────────────── */
1005 :
1006 1 : void test_local_pending_fetch(void) {
1007 1 : char *old_home = getenv("HOME");
1008 1 : setup_test_env("/tmp/email-cli-pending-fetch-test");
1009 :
1010 1 : ASSERT(local_pending_fetch_count() == 0, "pending_fetch: initially 0");
1011 :
1012 1 : local_pending_fetch_add("0000000000000010");
1013 1 : local_pending_fetch_add("0000000000000011");
1014 1 : ASSERT(local_pending_fetch_count() == 2, "pending_fetch_add: count 2");
1015 :
1016 1 : int cnt = 0;
1017 1 : char (*uids)[17] = local_pending_fetch_load(&cnt);
1018 1 : ASSERT(cnt == 2, "pending_fetch_load: 2 entries");
1019 1 : free(uids);
1020 :
1021 1 : local_pending_fetch_remove("0000000000000010");
1022 1 : ASSERT(local_pending_fetch_count() == 1, "pending_fetch_remove: count 1");
1023 :
1024 1 : local_pending_fetch_clear();
1025 1 : ASSERT(local_pending_fetch_count() == 0, "pending_fetch_clear: count 0");
1026 :
1027 1 : local_pending_fetch_add(NULL);
1028 1 : ASSERT(local_pending_fetch_count() == 0, "pending_fetch_add NULL: safe");
1029 :
1030 1 : if (old_home) setenv("HOME", old_home, 1);
1031 0 : else unsetenv("HOME");
1032 : }
1033 :
1034 : /* ── local_save_outgoing ──────────────────────────────────────────────── */
1035 :
1036 1 : void test_local_save_outgoing(void) {
1037 1 : char *old_home = getenv("HOME");
1038 1 : setup_test_env("/tmp/email-cli-outgoing-test");
1039 : /* Remove leftover from previous test runs */
1040 1 : unlink("/tmp/email-cli-outgoing-test/.local/share/email-cli/accounts/testuser/pending_appends.tsv");
1041 :
1042 1 : const char *msg = "From: me@test.local\r\nTo: you@test.local\r\n"
1043 : "Subject: test outgoing\r\n\r\nHello!\r\n";
1044 1 : size_t mlen = strlen(msg);
1045 :
1046 1 : int rc = local_save_outgoing("Sent", msg, mlen);
1047 1 : ASSERT(rc == 0, "local_save_outgoing: returns 0");
1048 :
1049 1 : int cnt = 0;
1050 1 : PendingAppend *pa = local_pending_append_load(&cnt);
1051 1 : ASSERT(cnt == 1, "local_save_outgoing: pending append queued");
1052 1 : if (cnt == 1)
1053 1 : ASSERT(strcmp(pa[0].folder, "Sent") == 0, "local_save_outgoing: Sent folder");
1054 1 : free(pa);
1055 :
1056 : /* NULL folder should not crash */
1057 1 : local_save_outgoing(NULL, msg, mlen);
1058 :
1059 1 : if (old_home) setenv("HOME", old_home, 1);
1060 0 : else unsetenv("HOME");
1061 : }
|