Line data Source code
1 : #include "test_helpers.h"
2 : #include "gmail_sync.h"
3 : #include "gmail_client.h"
4 : #include "local_store.h"
5 : #include "config.h"
6 : #include <stdlib.h>
7 : #include <string.h>
8 : #include <stdio.h>
9 : #include <unistd.h>
10 : #include <sys/socket.h>
11 : #include <sys/wait.h>
12 : #include <netinet/in.h>
13 : #include <arpa/inet.h>
14 : #ifdef ENABLE_GCOV
15 : extern void __gcov_dump(void);
16 : # define GCOV_FLUSH() __gcov_dump()
17 : #else
18 : # define GCOV_FLUSH() ((void)0)
19 : #endif
20 :
21 : /* ── Mock HTTP server (reused from test_gmail_client.c pattern) ─────── */
22 :
23 11 : static int gs_make_listener(int *port_out) {
24 11 : int fd = socket(AF_INET, SOCK_STREAM, 0);
25 11 : if (fd < 0) return -1;
26 11 : int one = 1;
27 11 : setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
28 : /* 2-second accept() timeout so server children exit cleanly when the
29 : * test exhausts its expected connections — prevents gs_wait_child() hang */
30 11 : struct timeval acc_tv = {.tv_sec = 2, .tv_usec = 0};
31 11 : setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &acc_tv, sizeof(acc_tv));
32 11 : struct sockaddr_in addr = {0};
33 11 : addr.sin_family = AF_INET;
34 11 : addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
35 11 : addr.sin_port = 0;
36 22 : if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0 ||
37 11 : listen(fd, 8) < 0) {
38 0 : close(fd);
39 0 : return -1;
40 : }
41 11 : socklen_t len = sizeof(addr);
42 11 : getsockname(fd, (struct sockaddr *)&addr, &len);
43 11 : *port_out = ntohs(addr.sin_port);
44 11 : return fd;
45 : }
46 :
47 24 : static void gs_send_json(int fd, int code, const char *body) {
48 24 : const char *reason = (code == 200) ? "OK" :
49 : (code == 204) ? "No Content" :
50 : (code == 404) ? "Not Found" : "Error";
51 : char hdr[512];
52 24 : size_t blen = body ? strlen(body) : 0;
53 24 : snprintf(hdr, sizeof(hdr),
54 : "HTTP/1.1 %d %s\r\n"
55 : "Content-Type: application/json\r\n"
56 : "Content-Length: %zu\r\n"
57 : "Connection: close\r\n\r\n",
58 : code, reason, blen);
59 : ssize_t r;
60 24 : r = write(fd, hdr, strlen(hdr)); (void)r;
61 24 : if (body && blen > 0) { r = write(fd, body, blen); (void)r; }
62 24 : }
63 :
64 24 : static int gs_read_req(int fd, char *buf, int bufsz) {
65 24 : int total = 0;
66 24 : while (total < bufsz - 1) {
67 24 : ssize_t n = read(fd, buf + total, (size_t)(bufsz - total - 1));
68 24 : if (n <= 0) break;
69 24 : total += (int)n;
70 24 : buf[total] = '\0';
71 24 : if (strstr(buf, "\r\n\r\n")) break;
72 : }
73 24 : buf[total] = '\0';
74 24 : return total;
75 : }
76 :
77 : /* base64url encode for mock raw message */
78 : static const char gs_b64_chars[] =
79 : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
80 :
81 7 : static char *gs_b64encode(const char *data, size_t len) {
82 7 : size_t alloc = ((len + 2) / 3) * 4 + 1;
83 7 : char *out = malloc(alloc);
84 7 : if (!out) return NULL;
85 7 : size_t o = 0;
86 299 : for (size_t i = 0; i < len; i += 3) {
87 292 : unsigned int n = ((unsigned int)(unsigned char)data[i]) << 16;
88 292 : if (i + 1 < len) n |= ((unsigned int)(unsigned char)data[i+1]) << 8;
89 292 : if (i + 2 < len) n |= ((unsigned int)(unsigned char)data[i+2]);
90 292 : out[o++] = gs_b64_chars[(n >> 18) & 0x3F];
91 292 : out[o++] = gs_b64_chars[(n >> 12) & 0x3F];
92 292 : if (i + 1 < len) out[o++] = gs_b64_chars[(n >> 6) & 0x3F];
93 292 : if (i + 2 < len) out[o++] = gs_b64_chars[n & 0x3F];
94 : }
95 7 : out[o] = '\0';
96 7 : return out;
97 : }
98 :
99 : /* Build a GmailClient pointing at a mock server on loopback */
100 11 : static GmailClient *gs_make_client(int port) {
101 : char api_base[128];
102 11 : snprintf(api_base, sizeof(api_base),
103 : "http://127.0.0.1:%d/gmail/v1/users/me", port);
104 11 : setenv("GMAIL_TEST_TOKEN", "test_access_token", 1);
105 11 : setenv("GMAIL_API_BASE_URL", api_base, 1);
106 :
107 11 : Config cfg = {0};
108 11 : cfg.gmail_mode = 1;
109 11 : cfg.gmail_refresh_token = "fake";
110 11 : return gmail_connect(&cfg);
111 : }
112 :
113 11 : static void gs_wait_child(pid_t pid) {
114 11 : if (pid > 0) { int st; waitpid(pid, &st, 0); }
115 11 : }
116 :
117 : /* ── is_filtered_label ───────────────────────────────────────────────── */
118 :
119 1 : static void test_filtered_null(void) {
120 1 : ASSERT(gmail_sync_is_filtered_label(NULL) == 1, "filtered: NULL → filtered");
121 : }
122 :
123 1 : static void test_filtered_category(void) {
124 : /* CATEGORY_* labels are now indexed (not filtered) — they appear in a
125 : * dedicated section in the TUI label list. */
126 1 : ASSERT(gmail_sync_is_filtered_label("CATEGORY_PERSONAL") == 0,
127 : "not filtered: CATEGORY_PERSONAL (indexed as category)");
128 1 : ASSERT(gmail_sync_is_filtered_label("CATEGORY_SOCIAL") == 0,
129 : "not filtered: CATEGORY_SOCIAL (indexed as category)");
130 1 : ASSERT(gmail_sync_is_filtered_label("CATEGORY_PROMOTIONS") == 0,
131 : "not filtered: CATEGORY_PROMOTIONS (indexed as category)");
132 1 : ASSERT(gmail_sync_is_filtered_label("CATEGORY_UPDATES") == 0,
133 : "not filtered: CATEGORY_UPDATES (indexed as category)");
134 1 : ASSERT(gmail_sync_is_filtered_label("CATEGORY_FORUMS") == 0,
135 : "not filtered: CATEGORY_FORUMS (indexed as category)");
136 : }
137 :
138 1 : static void test_filtered_important(void) {
139 1 : ASSERT(gmail_sync_is_filtered_label("IMPORTANT") == 1,
140 : "filtered: IMPORTANT");
141 : }
142 :
143 1 : static void test_not_filtered_system(void) {
144 1 : ASSERT(gmail_sync_is_filtered_label("INBOX") == 0, "not filtered: INBOX");
145 1 : ASSERT(gmail_sync_is_filtered_label("SENT") == 0, "not filtered: SENT");
146 1 : ASSERT(gmail_sync_is_filtered_label("TRASH") == 0, "not filtered: TRASH");
147 1 : ASSERT(gmail_sync_is_filtered_label("SPAM") == 0, "not filtered: SPAM");
148 1 : ASSERT(gmail_sync_is_filtered_label("STARRED") == 0, "not filtered: STARRED");
149 1 : ASSERT(gmail_sync_is_filtered_label("UNREAD") == 0, "not filtered: UNREAD");
150 1 : ASSERT(gmail_sync_is_filtered_label("DRAFT") == 0, "not filtered: DRAFT");
151 : }
152 :
153 1 : static void test_not_filtered_user(void) {
154 1 : ASSERT(gmail_sync_is_filtered_label("Work") == 0, "not filtered: Work");
155 1 : ASSERT(gmail_sync_is_filtered_label("Personal") == 0, "not filtered: Personal");
156 1 : ASSERT(gmail_sync_is_filtered_label("Projects/Alpha") == 0,
157 : "not filtered: nested label");
158 : }
159 :
160 1 : static void test_filtered_edge_cases(void) {
161 1 : ASSERT(gmail_sync_is_filtered_label("") == 0, "not filtered: empty string");
162 1 : ASSERT(gmail_sync_is_filtered_label("CATEGORY_") == 0,
163 : "not filtered: bare CATEGORY_ prefix (indexed as category)");
164 1 : ASSERT(gmail_sync_is_filtered_label("CATEGORY_X") == 0,
165 : "not filtered: unknown CATEGORY_ suffix (indexed as category)");
166 : }
167 :
168 : /* ── build_hdr ───────────────────────────────────────────────────────── */
169 :
170 1 : static void test_build_hdr_basic(void) {
171 1 : const char *raw = "From: Alice <alice@example.com>\r\n"
172 : "Subject: Hello\r\n"
173 : "Date: Wed, 16 Apr 2026 09:30:00 +0000\r\n"
174 : "\r\n"
175 : "Body text\r\n";
176 1 : char *labels[] = {"INBOX", "UNREAD"};
177 1 : char *hdr = gmail_sync_build_hdr(raw, labels, 2);
178 1 : ASSERT(hdr != NULL, "build_hdr basic: not NULL");
179 :
180 : /* Verify tab-separated format: from\tsubject\tdate\tlabels\tflags */
181 1 : int tabs = 0;
182 64 : for (const char *p = hdr; *p; p++)
183 63 : if (*p == '\t') tabs++;
184 1 : ASSERT(tabs == 4, "build_hdr basic: 4 tab separators");
185 :
186 : /* Verify label string contains both labels */
187 1 : ASSERT(strstr(hdr, "INBOX") != NULL, "build_hdr basic: has INBOX");
188 1 : ASSERT(strstr(hdr, "UNREAD") != NULL, "build_hdr basic: has UNREAD");
189 :
190 : /* Verify UNREAD sets MSG_FLAG_UNSEEN bit (value 1) */
191 1 : const char *last_tab = strrchr(hdr, '\t');
192 1 : ASSERT(last_tab != NULL, "build_hdr basic: has last tab");
193 1 : int flags = atoi(last_tab + 1);
194 1 : ASSERT((flags & 1) != 0, "build_hdr basic: UNSEEN flag set");
195 :
196 1 : free(hdr);
197 : }
198 :
199 1 : static void test_build_hdr_starred(void) {
200 1 : const char *raw = "From: Bob\r\nSubject: Star me\r\nDate: Thu, 17 Apr 2026 10:00:00 +0000\r\n\r\n";
201 1 : char *labels[] = {"STARRED"};
202 1 : char *hdr = gmail_sync_build_hdr(raw, labels, 1);
203 1 : ASSERT(hdr != NULL, "build_hdr starred: not NULL");
204 :
205 : /* STARRED sets MSG_FLAG_FLAGGED (value 2) */
206 1 : const char *last_tab = strrchr(hdr, '\t');
207 1 : int flags = atoi(last_tab + 1);
208 1 : ASSERT((flags & 2) != 0, "build_hdr starred: FLAGGED flag set");
209 :
210 1 : free(hdr);
211 : }
212 :
213 1 : static void test_build_hdr_no_labels(void) {
214 1 : const char *raw = "From: Nobody\r\nSubject: Archived\r\nDate: Mon, 14 Apr 2026 08:00:00 +0000\r\n\r\n";
215 1 : char *hdr = gmail_sync_build_hdr(raw, NULL, 0);
216 1 : ASSERT(hdr != NULL, "build_hdr no labels: not NULL");
217 :
218 : /* Flags should be 0 (no UNREAD, no STARRED) */
219 1 : const char *last_tab = strrchr(hdr, '\t');
220 1 : int flags = atoi(last_tab + 1);
221 1 : ASSERT(flags == 0, "build_hdr no labels: flags=0");
222 :
223 1 : free(hdr);
224 : }
225 :
226 1 : static void test_build_hdr_missing_headers(void) {
227 : /* Message with no From/Subject/Date headers */
228 1 : const char *raw = "\r\nJust a body.\r\n";
229 1 : char *labels[] = {"INBOX"};
230 1 : char *hdr = gmail_sync_build_hdr(raw, labels, 1);
231 1 : ASSERT(hdr != NULL, "build_hdr missing headers: not NULL");
232 :
233 : /* Should have empty fields but not crash */
234 1 : ASSERT(hdr[0] == '\t', "build_hdr missing headers: from is empty");
235 :
236 1 : free(hdr);
237 : }
238 :
239 1 : static void test_build_hdr_combined_flags(void) {
240 1 : const char *raw = "From: X\r\nSubject: Y\r\nDate: Mon, 14 Apr 2026 08:00:00 +0000\r\n\r\n";
241 1 : char *labels[] = {"UNREAD", "STARRED", "INBOX"};
242 1 : char *hdr = gmail_sync_build_hdr(raw, labels, 3);
243 1 : ASSERT(hdr != NULL, "build_hdr combined: not NULL");
244 :
245 1 : const char *last_tab = strrchr(hdr, '\t');
246 1 : int flags = atoi(last_tab + 1);
247 1 : ASSERT((flags & 1) != 0, "build_hdr combined: UNSEEN set");
248 1 : ASSERT((flags & 2) != 0, "build_hdr combined: FLAGGED set");
249 :
250 1 : free(hdr);
251 : }
252 :
253 : /* ── incremental sync — no history ───────────────────────────────────── */
254 :
255 1 : static void test_incremental_no_history(void) {
256 : /* Without local_store_init, local_gmail_history_load returns NULL → -2 */
257 1 : int rc = gmail_sync_incremental(NULL);
258 1 : ASSERT(rc == -2, "incremental: no historyId → returns -2");
259 : }
260 :
261 : /* ── repair_archive_flags ────────────────────────────────────────────── */
262 :
263 : /* Helper: sets HOME to a temp dir and inits local store for Gmail. */
264 29 : static void setup_gmail_test_env(const char *home) {
265 29 : setenv("HOME", home, 1);
266 29 : unsetenv("XDG_DATA_HOME");
267 29 : local_store_init("gmail://csjpeterjaket@gmail.com", "csjpeterjaket@gmail.com");
268 29 : }
269 :
270 : /* Helper: wipe the test home dir and reinitialise store.
271 : * Used by network-based tests that need a clean message store to avoid
272 : * interference from messages written by earlier tests. */
273 13 : static void reset_gmail_test_env(void) {
274 13 : int _sr = system("rm -rf '/tmp/email-cli-gmail-sync-test'"); (void)_sr;
275 13 : setup_gmail_test_env("/tmp/email-cli-gmail-sync-test");
276 13 : }
277 :
278 1 : static void test_repair_archive_flags_clears_unseen(void) {
279 : /* A message synced to _nolabel while still having UNREAD label → UNSEEN
280 : * flag should be cleared by repair_archive_flags(). */
281 1 : const char home[] = "/tmp/email-cli-gmail-sync-test";
282 1 : setup_gmail_test_env(home);
283 :
284 1 : const char *uid = "0000000000aabbcc";
285 :
286 : /* Write a .hdr with UNSEEN bit set (flags = 1) */
287 1 : const char *hdr = "Sender\tArchived msg\t2026-04-20\tUNREAD\t1";
288 1 : local_hdr_save("", uid, hdr, strlen(hdr));
289 :
290 : /* Add to _nolabel index */
291 1 : label_idx_add("_nolabel", uid);
292 :
293 : /* Run repair */
294 1 : gmail_sync_repair_archive_flags();
295 :
296 : /* Load and verify UNSEEN was cleared */
297 1 : char *loaded = local_hdr_load("", uid);
298 1 : ASSERT(loaded != NULL, "repair: .hdr still exists");
299 1 : const char *last_tab = strrchr(loaded, '\t');
300 1 : ASSERT(last_tab != NULL, "repair: flags tab present");
301 1 : int flags = atoi(last_tab + 1);
302 1 : ASSERT((flags & 1) == 0, "repair: UNSEEN bit cleared for archived message");
303 1 : free(loaded);
304 : }
305 :
306 1 : static void test_repair_archive_flags_preserves_flagged(void) {
307 : /* STARRED (FLAGGED bit) must survive the repair. */
308 1 : const char home[] = "/tmp/email-cli-gmail-sync-test";
309 1 : setup_gmail_test_env(home);
310 :
311 1 : const char *uid = "0000000000aabbdd";
312 :
313 : /* flags = 3 = UNSEEN | FLAGGED */
314 1 : const char *hdr = "Sender\tStarred archived\t2026-04-20\tUNREAD,STARRED\t3";
315 1 : local_hdr_save("", uid, hdr, strlen(hdr));
316 1 : label_idx_add("_nolabel", uid);
317 :
318 1 : gmail_sync_repair_archive_flags();
319 :
320 1 : char *loaded = local_hdr_load("", uid);
321 1 : ASSERT(loaded != NULL, "repair flagged: .hdr exists");
322 1 : const char *last_tab = strrchr(loaded, '\t');
323 1 : int flags = atoi(last_tab + 1);
324 1 : ASSERT((flags & 1) == 0, "repair flagged: UNSEEN cleared");
325 1 : ASSERT((flags & 2) != 0, "repair flagged: FLAGGED preserved");
326 1 : free(loaded);
327 : }
328 :
329 1 : static void test_repair_archive_flags_noop_when_already_read(void) {
330 : /* If UNSEEN is already 0, the .hdr should not be rewritten (flags stay). */
331 1 : const char home[] = "/tmp/email-cli-gmail-sync-test";
332 1 : setup_gmail_test_env(home);
333 :
334 1 : const char *uid = "0000000000aabbee";
335 :
336 : /* flags = 0, no UNREAD */
337 1 : const char *hdr = "Sender\tAlready read\t2026-04-20\t\t0";
338 1 : local_hdr_save("", uid, hdr, strlen(hdr));
339 1 : label_idx_add("_nolabel", uid);
340 :
341 1 : gmail_sync_repair_archive_flags();
342 :
343 1 : char *loaded = local_hdr_load("", uid);
344 1 : ASSERT(loaded != NULL, "repair noop: .hdr exists");
345 1 : const char *last_tab = strrchr(loaded, '\t');
346 1 : int flags = atoi(last_tab + 1);
347 1 : ASSERT(flags == 0, "repair noop: flags remain 0");
348 1 : free(loaded);
349 : }
350 :
351 1 : static void test_build_hdr_archive_unread_flags(void) {
352 : /* build_hdr with UNREAD but no real label → flags still has UNSEEN.
353 : * The caller (sync loop) is responsible for clearing it when assigning
354 : * to _nolabel. Verify build_hdr itself does not silently drop the flag. */
355 1 : const char *raw = "From: X\r\nSubject: Archived\r\nDate: Mon, 14 Apr 2026 08:00:00 +0000\r\n\r\n";
356 1 : char *labels[] = {"UNREAD", "CATEGORY_PROMOTIONS"};
357 1 : char *hdr = gmail_sync_build_hdr(raw, labels, 2);
358 1 : ASSERT(hdr != NULL, "build_hdr archive+unread: not NULL");
359 :
360 1 : const char *last_tab = strrchr(hdr, '\t');
361 1 : int flags = atoi(last_tab + 1);
362 : /* build_hdr sets UNSEEN; the caller must clear it for _nolabel messages */
363 1 : ASSERT((flags & 1) != 0, "build_hdr archive+unread: UNSEEN set by build_hdr (caller clears it)");
364 :
365 1 : free(hdr);
366 : }
367 :
368 : /* ── pending_fetch queue (local_store) ───────────────────────────────── */
369 :
370 1 : static void test_pending_fetch_empty_initially(void) {
371 1 : const char home[] = "/tmp/email-cli-gmail-sync-test";
372 1 : setup_gmail_test_env(home);
373 :
374 1 : local_pending_fetch_clear();
375 1 : ASSERT(local_pending_fetch_count() == 0, "pending_fetch: empty initially");
376 1 : int count = -1;
377 1 : char (*uids)[17] = local_pending_fetch_load(&count);
378 1 : ASSERT(count == 0, "pending_fetch: load count is 0");
379 1 : free(uids);
380 : }
381 :
382 1 : static void test_pending_fetch_add_and_load(void) {
383 1 : const char home[] = "/tmp/email-cli-gmail-sync-test";
384 1 : setup_gmail_test_env(home);
385 1 : local_pending_fetch_clear();
386 :
387 1 : const char *uid1 = "aaaa000000000001";
388 1 : const char *uid2 = "aaaa000000000002";
389 1 : ASSERT(local_pending_fetch_add(uid1) == 0, "pending_fetch: add uid1");
390 1 : ASSERT(local_pending_fetch_add(uid2) == 0, "pending_fetch: add uid2");
391 :
392 1 : ASSERT(local_pending_fetch_count() == 2, "pending_fetch: count == 2");
393 :
394 1 : int count = 0;
395 1 : char (*uids)[17] = local_pending_fetch_load(&count);
396 1 : ASSERT(count == 2, "pending_fetch: load returns 2");
397 1 : ASSERT(uids != NULL, "pending_fetch: uids not NULL");
398 1 : ASSERT(strcmp(uids[0], uid1) == 0 || strcmp(uids[1], uid1) == 0,
399 : "pending_fetch: uid1 present");
400 1 : ASSERT(strcmp(uids[0], uid2) == 0 || strcmp(uids[1], uid2) == 0,
401 : "pending_fetch: uid2 present");
402 1 : free(uids);
403 : }
404 :
405 1 : static void test_pending_fetch_remove(void) {
406 1 : const char home[] = "/tmp/email-cli-gmail-sync-test";
407 1 : setup_gmail_test_env(home);
408 1 : local_pending_fetch_clear();
409 :
410 1 : local_pending_fetch_add("bbbb000000000001");
411 1 : local_pending_fetch_add("bbbb000000000002");
412 1 : local_pending_fetch_add("bbbb000000000003");
413 :
414 1 : local_pending_fetch_remove("bbbb000000000002");
415 :
416 1 : int count = 0;
417 1 : char (*uids)[17] = local_pending_fetch_load(&count);
418 1 : ASSERT(count == 2, "pending_fetch remove: 2 entries remain");
419 1 : int found2 = 0;
420 3 : for (int i = 0; i < count; i++)
421 2 : if (strcmp(uids[i], "bbbb000000000002") == 0) found2 = 1;
422 1 : ASSERT(!found2, "pending_fetch remove: uid2 gone");
423 1 : free(uids);
424 : }
425 :
426 1 : static void test_pending_fetch_clear(void) {
427 1 : const char home[] = "/tmp/email-cli-gmail-sync-test";
428 1 : setup_gmail_test_env(home);
429 :
430 1 : local_pending_fetch_add("cccc000000000001");
431 1 : local_pending_fetch_add("cccc000000000002");
432 1 : ASSERT(local_pending_fetch_count() >= 2, "pending_fetch clear: non-zero before clear");
433 :
434 1 : local_pending_fetch_clear();
435 1 : ASSERT(local_pending_fetch_count() == 0, "pending_fetch clear: zero after clear");
436 : }
437 :
438 1 : static void test_pending_fetch_count_matches_load(void) {
439 1 : const char home[] = "/tmp/email-cli-gmail-sync-test";
440 1 : setup_gmail_test_env(home);
441 1 : local_pending_fetch_clear();
442 :
443 6 : for (int i = 0; i < 5; i++) {
444 : char uid[17];
445 5 : snprintf(uid, sizeof(uid), "dddd%012d", i);
446 5 : local_pending_fetch_add(uid);
447 : }
448 :
449 1 : int cnt_fast = local_pending_fetch_count();
450 1 : int cnt_load = 0;
451 1 : char (*uids)[17] = local_pending_fetch_load(&cnt_load);
452 1 : ASSERT(cnt_fast == cnt_load,
453 : "pending_fetch: count() matches load() count");
454 1 : free(uids);
455 : }
456 :
457 : /* ── gmail_sync_rebuild_indexes ──────────────────────────────────────────── */
458 :
459 : /*
460 : * Exercise the full rebuild_label_indexes + gmail_sync_rebuild_indexes code
461 : * path by populating a set of .hdr files then calling the public function.
462 : *
463 : * Coverage goal: lines 192-338 (rebuild_label_indexes) + 346-357
464 : * (gmail_sync_rebuild_indexes).
465 : */
466 1 : static void test_rebuild_indexes_empty_store(void) {
467 : /* With no messages, rebuild_indexes is a no-op but must not crash. */
468 1 : const char home[] = "/tmp/email-cli-gmail-sync-test";
469 1 : setup_gmail_test_env(home);
470 :
471 1 : int rc = gmail_sync_rebuild_indexes();
472 1 : ASSERT(rc == 0, "rebuild_indexes empty: returns 0");
473 : }
474 :
475 1 : static void test_rebuild_indexes_basic(void) {
476 1 : const char home[] = "/tmp/email-cli-gmail-sync-test";
477 1 : setup_gmail_test_env(home);
478 :
479 : /* Write a few .hdr files with known labels */
480 1 : const char *uid1 = "1100000000000001";
481 1 : const char *uid2 = "1100000000000002";
482 1 : const char *uid3 = "1100000000000003";
483 :
484 : /* uid1: INBOX + UNREAD (flags=1) */
485 1 : const char *hdr1 = "Alice\tHello\t2026-04-01\tINBOX,UNREAD\t1";
486 1 : local_hdr_save("", uid1, hdr1, strlen(hdr1));
487 :
488 : /* uid2: STARRED only (flags=2) */
489 1 : const char *hdr2 = "Bob\tStarred\t2026-04-02\tSTARRED\t2";
490 1 : local_hdr_save("", uid2, hdr2, strlen(hdr2));
491 :
492 : /* uid3: SPAM (flags=0) — should be indexed as _spam */
493 1 : const char *hdr3 = "Eve\tSpam\t2026-04-03\tSPAM\t0";
494 1 : local_hdr_save("", uid3, hdr3, strlen(hdr3));
495 :
496 1 : int rc = gmail_sync_rebuild_indexes();
497 1 : ASSERT(rc == 0, "rebuild_indexes basic: returns 0");
498 :
499 : /* Verify INBOX index contains uid1 */
500 1 : char (*idx_uids)[17] = NULL;
501 1 : int idx_count = 0;
502 1 : int load_rc = label_idx_load("INBOX", &idx_uids, &idx_count);
503 1 : ASSERT(load_rc == 0, "rebuild_indexes: INBOX index loaded");
504 1 : int found1 = 0;
505 2 : for (int i = 0; i < idx_count; i++)
506 1 : if (strcmp(idx_uids[i], uid1) == 0) found1 = 1;
507 1 : free(idx_uids);
508 1 : ASSERT(found1, "rebuild_indexes: uid1 in INBOX index");
509 :
510 : /* Verify _spam index contains uid3 */
511 1 : idx_uids = NULL; idx_count = 0;
512 1 : load_rc = label_idx_load("_spam", &idx_uids, &idx_count);
513 1 : ASSERT(load_rc == 0, "rebuild_indexes: _spam index loaded");
514 1 : int found3 = 0;
515 2 : for (int i = 0; i < idx_count; i++)
516 1 : if (strcmp(idx_uids[i], uid3) == 0) found3 = 1;
517 1 : free(idx_uids);
518 1 : ASSERT(found3, "rebuild_indexes: uid3 in _spam index");
519 : }
520 :
521 1 : static void test_rebuild_indexes_nolabel(void) {
522 : /* A message with only CATEGORY_ labels and no real labels → goes to _nolabel.
523 : * Note: UNREAD is a real (non-CATEGORY_) label, so we must NOT include it
524 : * here — otherwise has_real=1 and the message won't go to _nolabel. */
525 1 : const char home[] = "/tmp/email-cli-gmail-sync-test";
526 1 : setup_gmail_test_env(home);
527 :
528 1 : const char *uid = "2200000000000001";
529 : /* Only category label, no UNREAD (flags=0) → should go to _nolabel */
530 1 : const char *hdr = "Cat\tCategory mail\t2026-04-04\tCATEGORY_PROMOTIONS\t0";
531 1 : local_hdr_save("", uid, hdr, strlen(hdr));
532 :
533 1 : int rc = gmail_sync_rebuild_indexes();
534 1 : ASSERT(rc == 0, "rebuild_indexes nolabel: returns 0");
535 :
536 : /* _nolabel index should contain this uid */
537 1 : char (*idx_uids)[17] = NULL;
538 1 : int idx_count = 0;
539 1 : int load_rc = label_idx_load("_nolabel", &idx_uids, &idx_count);
540 1 : ASSERT(load_rc == 0, "rebuild_indexes nolabel: _nolabel index loaded");
541 1 : int found = 0;
542 3 : for (int i = 0; i < idx_count; i++)
543 2 : if (strcmp(idx_uids[i], uid) == 0) found = 1;
544 1 : free(idx_uids);
545 1 : ASSERT(found, "rebuild_indexes nolabel: uid in _nolabel");
546 :
547 : /* The flags field should remain 0 (no UNSEEN to clear) */
548 1 : char *loaded = local_hdr_load("", uid);
549 1 : ASSERT(loaded != NULL, "rebuild_indexes nolabel: hdr still exists");
550 1 : const char *last_tab = strrchr(loaded, '\t');
551 1 : ASSERT(last_tab != NULL, "rebuild_indexes nolabel: flags tab present");
552 1 : int flags = atoi(last_tab + 1);
553 1 : ASSERT((flags & 1) == 0, "rebuild_indexes nolabel: UNSEEN clear for archived msg");
554 1 : free(loaded);
555 : }
556 :
557 1 : static void test_rebuild_indexes_many_labels(void) {
558 : /* Write many messages to force realloc in rebuild_label_indexes.
559 : * Each message has 6 labels → ~cap*5 pairs initial cap, then grows. */
560 1 : const char home[] = "/tmp/email-cli-gmail-sync-test";
561 1 : setup_gmail_test_env(home);
562 :
563 : /* Write 12 messages, each with multiple labels, to force the realloc path */
564 13 : for (int i = 0; i < 12; i++) {
565 : char uid[17];
566 12 : snprintf(uid, sizeof(uid), "3300%012d", i);
567 : char hdr[256];
568 12 : snprintf(hdr, sizeof(hdr),
569 : "Sender%d\tSubject%d\t2026-04-01\tINBOX,UNREAD,STARRED,SENT,DRAFT,Work\t3",
570 : i, i);
571 12 : local_hdr_save("", uid, hdr, strlen(hdr));
572 : }
573 :
574 1 : int rc = gmail_sync_rebuild_indexes();
575 1 : ASSERT(rc == 0, "rebuild_indexes many: returns 0");
576 :
577 : /* INBOX should have all 12 */
578 1 : char (*idx_uids)[17] = NULL;
579 1 : int idx_count = 0;
580 1 : label_idx_load("INBOX", &idx_uids, &idx_count);
581 1 : ASSERT(idx_count >= 12, "rebuild_indexes many: INBOX has >= 12 entries");
582 1 : free(idx_uids);
583 : }
584 :
585 1 : static void test_rebuild_indexes_trash(void) {
586 : /* TRASH label should be indexed as _trash */
587 1 : const char home[] = "/tmp/email-cli-gmail-sync-test";
588 1 : setup_gmail_test_env(home);
589 :
590 1 : const char *uid = "4400000000000001";
591 1 : const char *hdr = "Trashed\tDeleted\t2026-04-05\tTRASH\t0";
592 1 : local_hdr_save("", uid, hdr, strlen(hdr));
593 :
594 1 : gmail_sync_rebuild_indexes();
595 :
596 1 : char (*idx_uids)[17] = NULL;
597 1 : int idx_count = 0;
598 1 : label_idx_load("_trash", &idx_uids, &idx_count);
599 1 : int found = 0;
600 2 : for (int i = 0; i < idx_count; i++)
601 1 : if (strcmp(idx_uids[i], uid) == 0) found = 1;
602 1 : free(idx_uids);
603 1 : ASSERT(found, "rebuild_indexes trash: uid in _trash index");
604 : }
605 :
606 1 : static void test_rebuild_indexes_flags_sync(void) {
607 : /* A message where flags integer is inconsistent with labels:
608 : * labels say UNREAD,STARRED but flags field says 0.
609 : * rebuild_label_indexes must update the flags field. */
610 1 : const char home[] = "/tmp/email-cli-gmail-sync-test";
611 1 : setup_gmail_test_env(home);
612 :
613 1 : const char *uid = "5500000000000001";
614 : /* flags=0 but UNREAD and STARRED present → new_flags should be 3 */
615 1 : const char *hdr = "X\tFlagsSync\t2026-04-06\tINBOX,UNREAD,STARRED\t0";
616 1 : local_hdr_save("", uid, hdr, strlen(hdr));
617 :
618 1 : gmail_sync_rebuild_indexes();
619 :
620 1 : char *loaded = local_hdr_load("", uid);
621 1 : ASSERT(loaded != NULL, "flags_sync: hdr still exists");
622 1 : const char *last_tab = strrchr(loaded, '\t');
623 1 : int flags = last_tab ? atoi(last_tab + 1) : -1;
624 1 : ASSERT((flags & 1) != 0, "flags_sync: UNSEEN bit set after rebuild");
625 1 : ASSERT((flags & 2) != 0, "flags_sync: FLAGGED bit set after rebuild");
626 1 : free(loaded);
627 : }
628 :
629 1 : static void test_rebuild_indexes_hdr_no_tabs(void) {
630 : /* A malformed .hdr with no tabs should be gracefully skipped. */
631 1 : const char home[] = "/tmp/email-cli-gmail-sync-test";
632 1 : setup_gmail_test_env(home);
633 :
634 1 : const char *uid = "6600000000000001";
635 : /* No tab separators — rebuild_label_indexes should skip gracefully */
636 1 : const char *hdr = "malformed-hdr-no-tabs";
637 1 : local_hdr_save("", uid, hdr, strlen(hdr));
638 :
639 1 : int rc = gmail_sync_rebuild_indexes();
640 1 : ASSERT(rc == 0, "rebuild_indexes no-tabs: no crash, returns 0");
641 : }
642 :
643 : /* ── gmail_sync_incremental: re-verify no-history path after store init ─── */
644 :
645 1 : static void test_incremental_with_saved_history_no_server(void) {
646 : /* Verify that gmail_sync_incremental returns -2 whenever there is no
647 : * saved historyId, regardless of prior store state. This exercises the
648 : * early-return branch in gmail_sync_incremental (lines 794-798). */
649 1 : const char home[] = "/tmp/email-cli-gmail-sync-test";
650 1 : setup_gmail_test_env(home);
651 :
652 : /* Ensure no history id is saved (fresh environment) */
653 1 : char *hid = local_gmail_history_load();
654 : /* If some previous test left a history file, skip gracefully */
655 1 : if (hid) {
656 0 : free(hid);
657 0 : ASSERT(1, "incremental no-server: history present from prev test, skipped");
658 0 : return;
659 : }
660 :
661 1 : int rc = gmail_sync_incremental(NULL);
662 1 : ASSERT(rc == -2, "incremental no-server: no historyId → -2");
663 : }
664 :
665 : /* ── build_hdr: SPAM label sets MSG_FLAG_JUNK ─────────────────────────────── */
666 :
667 1 : static void test_build_hdr_spam_flag(void) {
668 1 : const char *raw = "From: Spammer\r\nSubject: Buy now\r\nDate: Mon, 14 Apr 2026 08:00:00 +0000\r\n\r\n";
669 1 : char *labels[] = {"SPAM"};
670 1 : char *hdr = gmail_sync_build_hdr(raw, labels, 1);
671 1 : ASSERT(hdr != NULL, "build_hdr spam: not NULL");
672 :
673 : /* SPAM label sets MSG_FLAG_JUNK (1<<6 = 64 per local_store.h) */
674 1 : const char *last_tab = strrchr(hdr, '\t');
675 1 : ASSERT(last_tab != NULL, "build_hdr spam: flags tab present");
676 1 : int flags = atoi(last_tab + 1);
677 1 : ASSERT((flags & 64) != 0, "build_hdr spam: JUNK flag set");
678 :
679 1 : free(hdr);
680 : }
681 :
682 : /* ── Mock servers for gmail_sync tests ──────────────────────────────── */
683 :
684 : /*
685 : * reconcile_server: responds to:
686 : * GET /messages → 2-message list with historyId
687 : * GET /labels → label list
688 : * GET /profile → profile with historyId (fallback)
689 : * GET /messages/{id} → raw message body
690 : * any other → 404
691 : */
692 5 : static void run_reconcile_server(int lfd, int count) {
693 5 : const char *raw_email =
694 : "From: alice@example.com\r\n"
695 : "To: me@gmail.com\r\n"
696 : "Subject: Reconcile Test\r\n"
697 : "Date: Mon, 01 Jan 2024 00:00:00 +0000\r\n"
698 : "\r\n"
699 : "Body here.\r\n";
700 5 : char *b64 = gs_b64encode(raw_email, strlen(raw_email));
701 :
702 5 : struct sockaddr_in cli = {0};
703 5 : socklen_t cli_len = sizeof(cli);
704 15 : for (int i = 0; i < count; i++) {
705 10 : int cfd = accept(lfd, (struct sockaddr *)&cli, &cli_len);
706 10 : if (cfd < 0) break;
707 10 : struct timeval tv = {.tv_sec = 5, .tv_usec = 0};
708 10 : setsockopt(cfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
709 :
710 : char buf[4096];
711 10 : if (gs_read_req(cfd, buf, (int)sizeof(buf)) <= 0) { close(cfd); continue; }
712 :
713 10 : char method[16] = {0}, path[2048] = {0};
714 10 : sscanf(buf, "%15s %2047s", method, path);
715 :
716 10 : if (strstr(path, "/messages") && !strstr(path, "/messages/") &&
717 3 : strcmp(method, "GET") == 0) {
718 3 : gs_send_json(cfd, 200,
719 : "{\"messages\":["
720 : "{\"id\":\"aabbcc0000000001\",\"threadId\":\"t1\"},"
721 : "{\"id\":\"aabbcc0000000002\",\"threadId\":\"t2\"}"
722 : "],\"resultSizeEstimate\":2,\"historyId\":\"99001\"}");
723 11 : } else if (strstr(path, "/messages/") && strcmp(method, "GET") == 0) {
724 : char body_buf[2048];
725 4 : snprintf(body_buf, sizeof(body_buf),
726 : "{\"id\":\"aabbcc0000000001\","
727 : "\"labelIds\":[\"INBOX\",\"UNREAD\"],"
728 : "\"raw\":\"%s\"}",
729 : b64 ? b64 : "");
730 4 : gs_send_json(cfd, 200, body_buf);
731 3 : } else if (strstr(path, "/labels") && strcmp(method, "GET") == 0) {
732 3 : gs_send_json(cfd, 200,
733 : "{\"labels\":["
734 : "{\"id\":\"INBOX\",\"name\":\"INBOX\"},"
735 : "{\"id\":\"UNREAD\",\"name\":\"UNREAD\"}"
736 : "]}");
737 0 : } else if (strstr(path, "/profile")) {
738 0 : gs_send_json(cfd, 200,
739 : "{\"historyId\":\"99001\","
740 : "\"emailAddress\":\"test@gmail.com\"}");
741 : } else {
742 0 : gs_send_json(cfd, 404, "{}");
743 : }
744 10 : close(cfd);
745 : }
746 5 : free(b64);
747 5 : close(lfd);
748 5 : GCOV_FLUSH();
749 0 : _exit(0);
750 : }
751 :
752 5 : static pid_t start_reconcile_server(int *port_out, int count) {
753 5 : int lfd = gs_make_listener(port_out);
754 5 : if (lfd < 0) return -1;
755 5 : pid_t pid = fork();
756 10 : if (pid < 0) { close(lfd); return -1; }
757 10 : if (pid == 0) run_reconcile_server(lfd, count);
758 5 : close(lfd);
759 5 : return pid;
760 : }
761 :
762 1 : static void test_reconcile_success(void) {
763 1 : reset_gmail_test_env();
764 1 : local_pending_fetch_clear();
765 :
766 1 : int port = 0;
767 : /* 2 connections: list_messages + list_labels */
768 1 : pid_t pid = start_reconcile_server(&port, 2);
769 1 : if (pid < 0) { ASSERT(0, "reconcile: could not start mock server"); return; }
770 :
771 1 : usleep(20000);
772 :
773 1 : GmailClient *gc = gs_make_client(port);
774 1 : if (!gc) { gs_wait_child(pid); ASSERT(0, "reconcile: client connected"); return; }
775 :
776 1 : int queued = gmail_sync_reconcile(gc);
777 1 : int pending_cnt = local_pending_fetch_count();
778 1 : gmail_disconnect(gc);
779 1 : gs_wait_child(pid);
780 :
781 1 : ASSERT(queued == 2, "reconcile: 2 messages queued");
782 1 : ASSERT(pending_cnt == 2, "reconcile: pending_fetch count == 2");
783 : }
784 :
785 1 : static void test_reconcile_with_cached_messages(void) {
786 : /* Pre-cache one of the two server messages so reconcile sees 1 cached
787 : * and 1 queued. Covers lines 450-456 (the "cached++" path). */
788 1 : reset_gmail_test_env();
789 1 : local_pending_fetch_clear();
790 :
791 : /* Pre-save uid1 (already cached) */
792 1 : const char *uid1 = "aabbcc0000000001";
793 1 : const char *raw = "From: alice@example.com\r\nSubject: Reconcile Test\r\n\r\nBody\r\n";
794 1 : local_msg_save("", uid1, raw, strlen(raw));
795 1 : local_hdr_save("", uid1, "Alice\tReconcile Test\t2024-01-01\tINBOX\t0",
796 : strlen("Alice\tReconcile Test\t2024-01-01\tINBOX\t0"));
797 :
798 1 : int port = 0;
799 : /* 2 connections: list_messages + list_labels */
800 1 : pid_t pid = start_reconcile_server(&port, 2);
801 1 : if (pid < 0) { ASSERT(0, "reconcile_cached: could not start mock server"); return; }
802 :
803 1 : usleep(20000);
804 :
805 1 : GmailClient *gc = gs_make_client(port);
806 1 : if (!gc) { gs_wait_child(pid); ASSERT(0, "reconcile_cached: client connected"); return; }
807 :
808 1 : int queued = gmail_sync_reconcile(gc);
809 1 : int pend_cnt = local_pending_fetch_count();
810 1 : gmail_disconnect(gc);
811 1 : gs_wait_child(pid);
812 :
813 : /* uid1 is cached, uid2 is new → 1 queued */
814 1 : ASSERT(queued == 1, "reconcile_cached: 1 new message queued");
815 1 : ASSERT(pend_cnt == 1, "reconcile_cached: pending count == 1");
816 : }
817 :
818 1 : static void test_fetch_pending_empty_queue(void) {
819 : /* fetch_pending with an empty queue should return 0 immediately. */
820 1 : reset_gmail_test_env();
821 1 : local_pending_fetch_clear();
822 :
823 : /* No network needed — empty queue exits early */
824 1 : setenv("GMAIL_TEST_TOKEN", "test_access_token", 1);
825 1 : setenv("GMAIL_API_BASE_URL", "http://127.0.0.1:1/gmail/v1/users/me", 1);
826 1 : Config cfg = {0};
827 1 : cfg.gmail_mode = 1;
828 1 : cfg.gmail_refresh_token = "fake";
829 1 : GmailClient *gc = gmail_connect(&cfg);
830 1 : if (!gc) { ASSERT(0, "fetch_empty: client created"); return; }
831 :
832 1 : int fetched = gmail_sync_fetch_pending(gc);
833 1 : gmail_disconnect(gc);
834 :
835 1 : ASSERT(fetched == 0, "fetch_empty: returns 0 on empty queue");
836 : }
837 :
838 : /* Server that returns a message with only CATEGORY_ labels (no real labels).
839 : * Used to test the _nolabel path in store_fetched_message (lines 397-404). */
840 1 : static void run_nolabel_msg_server(int lfd, int count) {
841 1 : const char *raw_email =
842 : "From: promo@example.com\r\n"
843 : "To: me@gmail.com\r\n"
844 : "Subject: Promotions\r\n"
845 : "Date: Tue, 01 Jan 2025 00:00:00 +0000\r\n"
846 : "\r\n"
847 : "Click here to buy things!\r\n";
848 1 : char *b64 = gs_b64encode(raw_email, strlen(raw_email));
849 :
850 1 : struct sockaddr_in cli = {0};
851 1 : socklen_t cli_len = sizeof(cli);
852 2 : for (int i = 0; i < count; i++) {
853 1 : int cfd = accept(lfd, (struct sockaddr *)&cli, &cli_len);
854 1 : if (cfd < 0) break;
855 1 : struct timeval tv = {.tv_sec = 5, .tv_usec = 0};
856 1 : setsockopt(cfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
857 :
858 : char buf[4096];
859 1 : if (gs_read_req(cfd, buf, (int)sizeof(buf)) <= 0) { close(cfd); continue; }
860 :
861 1 : char method[16] = {0}, path[2048] = {0};
862 1 : sscanf(buf, "%15s %2047s", method, path);
863 :
864 2 : if (strstr(path, "/messages/") && strcmp(method, "GET") == 0) {
865 : /* Message with only CATEGORY_ label and IMPORTANT (both non-real).
866 : * IMPORTANT is filtered (filtered_label→skip).
867 : * CATEGORY_PROMOTIONS passes filter but is_category_label→true.
868 : * Result: has_real_label=0 → goes to _nolabel with UNSEEN cleared. */
869 : char body_buf[2048];
870 1 : snprintf(body_buf, sizeof(body_buf),
871 : "{\"id\":\"nolabel000000001\","
872 : "\"labelIds\":[\"CATEGORY_PROMOTIONS\",\"IMPORTANT\"],"
873 : "\"raw\":\"%s\"}",
874 : b64 ? b64 : "");
875 1 : gs_send_json(cfd, 200, body_buf);
876 : } else {
877 0 : gs_send_json(cfd, 404, "{}");
878 : }
879 1 : close(cfd);
880 : }
881 1 : free(b64);
882 1 : close(lfd);
883 1 : GCOV_FLUSH();
884 0 : _exit(0);
885 : }
886 :
887 1 : static pid_t start_nolabel_msg_server(int *port_out, int count) {
888 1 : int lfd = gs_make_listener(port_out);
889 1 : if (lfd < 0) return -1;
890 1 : pid_t pid = fork();
891 2 : if (pid < 0) { close(lfd); return -1; }
892 2 : if (pid == 0) run_nolabel_msg_server(lfd, count);
893 1 : close(lfd);
894 1 : return pid;
895 : }
896 :
897 1 : static void test_fetch_pending_nolabel_message(void) {
898 : /* Message has CATEGORY_PROMOTIONS + IMPORTANT labels.
899 : * CATEGORY_PROMOTIONS: is_category_label=true → has_real_label not set.
900 : * IMPORTANT: filtered → skipped entirely.
901 : * Result: has_real_label=0 → goes to _nolabel (covers lines 397-403).
902 : * No UNREAD → cur_flags=0 → UNSEEN was never set, flags remain 0. */
903 1 : reset_gmail_test_env();
904 1 : local_pending_fetch_clear();
905 :
906 1 : const char *uid = "nolabel000000001";
907 1 : local_pending_fetch_add(uid);
908 :
909 1 : int port = 0;
910 1 : pid_t pid = start_nolabel_msg_server(&port, 1);
911 1 : if (pid < 0) { ASSERT(0, "fetch_nolabel: no server"); return; }
912 :
913 1 : usleep(20000);
914 :
915 1 : GmailClient *gc = gs_make_client(port);
916 1 : if (!gc) { gs_wait_child(pid); ASSERT(0, "fetch_nolabel: client connected"); return; }
917 :
918 1 : int fetched = gmail_sync_fetch_pending(gc);
919 1 : gmail_disconnect(gc);
920 1 : gs_wait_child(pid);
921 :
922 1 : ASSERT(fetched == 1, "fetch_nolabel: 1 message downloaded");
923 1 : ASSERT(local_msg_exists("", uid), "fetch_nolabel: .eml saved");
924 :
925 : /* Should be in _nolabel index */
926 1 : char (*idx_uids)[17] = NULL;
927 1 : int idx_count = 0;
928 1 : label_idx_load("_nolabel", &idx_uids, &idx_count);
929 1 : int found = 0;
930 2 : for (int i = 0; i < idx_count; i++)
931 1 : if (strcmp(idx_uids[i], uid) == 0) found = 1;
932 1 : free(idx_uids);
933 1 : ASSERT(found, "fetch_nolabel: uid in _nolabel index");
934 :
935 : /* UNSEEN flag should be cleared (archived messages are always read) */
936 1 : char *hdr = local_hdr_load("", uid);
937 1 : if (hdr) {
938 1 : const char *last_tab = strrchr(hdr, '\t');
939 1 : int flags = last_tab ? atoi(last_tab + 1) : -1;
940 1 : ASSERT((flags & 1) == 0, "fetch_nolabel: UNSEEN cleared for archived msg");
941 1 : free(hdr);
942 : }
943 : }
944 :
945 1 : static void test_fetch_pending_with_rules(void) {
946 : /* Write a mail rule that matches alice@example.com, then fetch_pending
947 : * so apply_rules_to_new_message is called. Covers lines 109-167. */
948 1 : reset_gmail_test_env();
949 1 : local_pending_fetch_clear();
950 :
951 : /* Write a rules.ini for the test account */
952 : char rules_dir[512];
953 1 : snprintf(rules_dir, sizeof(rules_dir),
954 : "/tmp/email-cli-gmail-sync-test/.config/email-cli/accounts/csjpeterjaket@gmail.com");
955 : /* Use system() with literal paths — no variable in rm -rf */
956 1 : int _sr2 = system("mkdir -p '/tmp/email-cli-gmail-sync-test/.config/email-cli/accounts/csjpeterjaket@gmail.com'"); (void)_sr2;
957 :
958 : char rules_path[600];
959 1 : snprintf(rules_path, sizeof(rules_path), "%s/rules.ini", rules_dir);
960 1 : FILE *f = fopen(rules_path, "w");
961 1 : if (f) {
962 1 : fputs("[rule \"Test Rule\"]\n", f);
963 1 : fputs("if-from = *@example.com\n", f);
964 1 : fputs("then-add-label = Filtered\n", f);
965 1 : fclose(f);
966 : }
967 :
968 1 : const char *uid = "aabbcc0000000001";
969 1 : local_pending_fetch_add(uid);
970 :
971 1 : int port = 0;
972 : /* 1 connection: gmail_fetch_message */
973 1 : pid_t pid = start_reconcile_server(&port, 1);
974 1 : if (pid < 0) { ASSERT(0, "fetch_rules: no server"); return; }
975 :
976 1 : usleep(20000);
977 :
978 1 : GmailClient *gc = gs_make_client(port);
979 1 : if (!gc) { gs_wait_child(pid); ASSERT(0, "fetch_rules: client connected"); return; }
980 :
981 1 : int fetched = gmail_sync_fetch_pending(gc);
982 1 : gmail_disconnect(gc);
983 1 : gs_wait_child(pid);
984 :
985 1 : ASSERT(fetched == 1, "fetch_rules: 1 message downloaded");
986 : /* Rule applied — message should be fetched (rules firing doesn't prevent storage) */
987 1 : ASSERT(local_msg_exists("", uid), "fetch_rules: .eml saved after rule application");
988 : }
989 :
990 1 : static void test_reconcile_server_error(void) {
991 : /* When the server returns 500 for messages.list, gmail_list_messages
992 : * treats it as an empty result (0 messages). Reconcile returns 0 with
993 : * nothing queued. Exercises the error-response code path in list. */
994 1 : reset_gmail_test_env();
995 :
996 1 : int port = 0;
997 1 : int lfd = gs_make_listener(&port);
998 1 : if (lfd < 0) { ASSERT(0, "reconcile_server_err: no listener"); return; }
999 :
1000 1 : pid_t pid = fork();
1001 2 : if (pid < 0) { close(lfd); ASSERT(0, "reconcile_server_err: fork failed"); return; }
1002 2 : if (pid == 0) {
1003 1 : struct sockaddr_in cli = {0};
1004 1 : socklen_t cli_len = sizeof(cli);
1005 4 : for (int i = 0; i < 4; i++) {
1006 4 : int cfd = accept(lfd, (struct sockaddr *)&cli, &cli_len);
1007 4 : if (cfd < 0) break;
1008 : char buf[512];
1009 3 : gs_read_req(cfd, buf, (int)sizeof(buf));
1010 3 : gs_send_json(cfd, 500, "{\"error\":\"server error\"}");
1011 3 : close(cfd);
1012 : }
1013 1 : close(lfd);
1014 1 : GCOV_FLUSH();
1015 0 : _exit(0);
1016 : }
1017 1 : close(lfd);
1018 1 : usleep(20000);
1019 :
1020 1 : GmailClient *gc = gs_make_client(port);
1021 1 : if (!gc) { gs_wait_child(pid); ASSERT(0, "reconcile_server_err: client connected"); return; }
1022 :
1023 1 : int rc = gmail_sync_reconcile(gc);
1024 1 : gmail_disconnect(gc);
1025 1 : gs_wait_child(pid);
1026 :
1027 : /* gmail_list_messages returns 0 even on 500 (treats as empty list).
1028 : * So reconcile returns 0 with 0 messages queued. */
1029 1 : ASSERT(rc == 0, "reconcile_server_err: returns 0 (empty list) on server 500");
1030 1 : ASSERT(local_pending_fetch_count() == 0,
1031 : "reconcile_server_err: nothing queued when server returns 500");
1032 : }
1033 :
1034 1 : static void test_fetch_pending_success(void) {
1035 1 : reset_gmail_test_env();
1036 1 : local_pending_fetch_clear();
1037 :
1038 1 : const char *uid = "aabbcc0000000001";
1039 1 : local_pending_fetch_add(uid);
1040 :
1041 1 : int port = 0;
1042 : /* 1 connection: gmail_fetch_message for the 1 pending UID */
1043 1 : pid_t pid = start_reconcile_server(&port, 1);
1044 1 : if (pid < 0) { ASSERT(0, "fetch_pending: no server"); return; }
1045 :
1046 1 : usleep(20000);
1047 :
1048 1 : GmailClient *gc = gs_make_client(port);
1049 1 : if (!gc) { gs_wait_child(pid); ASSERT(0, "fetch_pending: client connected"); return; }
1050 :
1051 1 : int fetched = gmail_sync_fetch_pending(gc);
1052 1 : int msg_ok = local_msg_exists("", uid);
1053 1 : int hdr_ok = local_hdr_exists("", uid);
1054 1 : int pend_cnt = local_pending_fetch_count();
1055 1 : gmail_disconnect(gc);
1056 1 : gs_wait_child(pid);
1057 :
1058 1 : ASSERT(fetched == 1, "fetch_pending: 1 message downloaded");
1059 1 : ASSERT(msg_ok, "fetch_pending: .eml saved");
1060 1 : ASSERT(hdr_ok, "fetch_pending: .hdr saved");
1061 1 : ASSERT(pend_cnt == 0, "fetch_pending: queue empty");
1062 : }
1063 :
1064 1 : static void test_fetch_pending_already_cached(void) {
1065 1 : reset_gmail_test_env();
1066 1 : local_pending_fetch_clear();
1067 :
1068 1 : const char *uid = "bbccdd0000000001";
1069 1 : const char *raw = "From: X\r\nSubject: Y\r\n\r\nBody\r\n";
1070 1 : local_msg_save("", uid, raw, strlen(raw));
1071 1 : const char *hdr = "X\tY\t2024-01-01\tINBOX\t0";
1072 1 : local_hdr_save("", uid, hdr, strlen(hdr));
1073 1 : local_pending_fetch_add(uid);
1074 :
1075 : /* Use an unreachable port — no network calls should happen */
1076 1 : setenv("GMAIL_TEST_TOKEN", "test_access_token", 1);
1077 1 : setenv("GMAIL_API_BASE_URL", "http://127.0.0.1:1/gmail/v1/users/me", 1);
1078 1 : Config cfg = {0};
1079 1 : cfg.gmail_mode = 1;
1080 1 : cfg.gmail_refresh_token = "fake";
1081 1 : GmailClient *gc = gmail_connect(&cfg);
1082 1 : if (!gc) { ASSERT(0, "fetch_cached: client created"); return; }
1083 :
1084 1 : int fetched = gmail_sync_fetch_pending(gc);
1085 1 : int pend_cnt = local_pending_fetch_count();
1086 1 : gmail_disconnect(gc);
1087 :
1088 1 : ASSERT(fetched == 0, "fetch_cached: 0 downloaded (already cached)");
1089 1 : ASSERT(pend_cnt == 0, "fetch_cached: queue cleared");
1090 : }
1091 :
1092 1 : static void test_fetch_pending_server_error(void) {
1093 1 : reset_gmail_test_env();
1094 1 : local_pending_fetch_clear();
1095 :
1096 1 : const char *uid = "ccddee0000000001";
1097 1 : local_pending_fetch_add(uid);
1098 :
1099 1 : int port = 0;
1100 1 : int lfd = gs_make_listener(&port);
1101 1 : if (lfd < 0) { ASSERT(0, "fetch_err: no listener"); return; }
1102 :
1103 1 : pid_t pid = fork();
1104 2 : if (pid < 0) { close(lfd); ASSERT(0, "fetch_err: fork failed"); return; }
1105 2 : if (pid == 0) {
1106 1 : struct sockaddr_in cli = {0};
1107 1 : socklen_t cli_len = sizeof(cli);
1108 2 : for (int i = 0; i < 3; i++) {
1109 2 : int cfd = accept(lfd, (struct sockaddr *)&cli, &cli_len);
1110 2 : if (cfd < 0) break;
1111 : char buf[512];
1112 1 : gs_read_req(cfd, buf, (int)sizeof(buf));
1113 1 : gs_send_json(cfd, 404, "{\"error\":{\"code\":404}}");
1114 1 : close(cfd);
1115 : }
1116 1 : close(lfd);
1117 1 : GCOV_FLUSH();
1118 0 : _exit(0);
1119 : }
1120 1 : close(lfd);
1121 1 : usleep(20000);
1122 :
1123 1 : GmailClient *gc = gs_make_client(port);
1124 1 : if (!gc) { gs_wait_child(pid); ASSERT(0, "fetch_err: client connected"); return; }
1125 :
1126 1 : int fetched = gmail_sync_fetch_pending(gc);
1127 1 : int pend_cnt = local_pending_fetch_count();
1128 1 : gmail_disconnect(gc);
1129 1 : gs_wait_child(pid);
1130 :
1131 1 : ASSERT(fetched == 0, "fetch_err: 0 downloaded on 404");
1132 1 : ASSERT(pend_cnt == 1, "fetch_err: uid stays in queue");
1133 : }
1134 :
1135 : /*
1136 : * incremental_server: handles the various gmail_sync_incremental paths.
1137 : * Returns history with messagesAdded, labelsAdded, labelsRemoved, messagesDeleted.
1138 : */
1139 1 : static void run_incremental_server(int lfd, int count) {
1140 1 : const char *raw_email =
1141 : "From: bob@example.com\r\n"
1142 : "To: me@gmail.com\r\n"
1143 : "Subject: Incremental Test\r\n"
1144 : "Date: Mon, 01 Jan 2024 12:00:00 +0000\r\n"
1145 : "\r\n"
1146 : "Incremental body.\r\n";
1147 1 : char *b64 = gs_b64encode(raw_email, strlen(raw_email));
1148 :
1149 1 : struct sockaddr_in cli = {0};
1150 1 : socklen_t cli_len = sizeof(cli);
1151 6 : for (int i = 0; i < count; i++) {
1152 5 : int cfd = accept(lfd, (struct sockaddr *)&cli, &cli_len);
1153 5 : if (cfd < 0) break;
1154 5 : struct timeval tv = {.tv_sec = 5, .tv_usec = 0};
1155 5 : setsockopt(cfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
1156 :
1157 : char buf[4096];
1158 5 : if (gs_read_req(cfd, buf, (int)sizeof(buf)) <= 0) { close(cfd); continue; }
1159 :
1160 5 : char method[16] = {0}, path[2048] = {0};
1161 5 : sscanf(buf, "%15s %2047s", method, path);
1162 :
1163 6 : if (strstr(path, "/history") && strcmp(method, "GET") == 0) {
1164 : /* Flat top-level JSON: json_foreach_object searches at top level */
1165 : char body_buf[2048];
1166 1 : snprintf(body_buf, sizeof(body_buf),
1167 : "{"
1168 : "\"historyId\":\"99999\","
1169 : "\"messagesAdded\":["
1170 : " {\"id\":\"incr000000000001\"}"
1171 : "],"
1172 : "\"labelsAdded\":["
1173 : " {\"id\":\"incr000000000001\",\"labelIds\":[\"STARRED\"]}"
1174 : "],"
1175 : "\"labelsRemoved\":["
1176 : " {\"id\":\"incr000000000001\",\"labelIds\":[\"UNREAD\"]}"
1177 : "],"
1178 : "\"messagesDeleted\":["
1179 : " {\"id\":\"incr000000000002\"}"
1180 : "]"
1181 : "}");
1182 1 : gs_send_json(cfd, 200, body_buf);
1183 6 : } else if (strstr(path, "/messages/") && strcmp(method, "GET") == 0) {
1184 : char body_buf[2048];
1185 2 : snprintf(body_buf, sizeof(body_buf),
1186 : "{\"id\":\"incr000000000001\","
1187 : "\"labelIds\":[\"INBOX\",\"STARRED\"],"
1188 : "\"raw\":\"%s\"}",
1189 : b64 ? b64 : "");
1190 2 : gs_send_json(cfd, 200, body_buf);
1191 2 : } else if (strstr(path, "/labels") && strcmp(method, "GET") == 0) {
1192 2 : gs_send_json(cfd, 200,
1193 : "{\"labels\":["
1194 : "{\"id\":\"INBOX\",\"name\":\"INBOX\"},"
1195 : "{\"id\":\"STARRED\",\"name\":\"STARRED\"},"
1196 : "{\"id\":\"UNREAD\",\"name\":\"UNREAD\"}"
1197 : "]}");
1198 0 : } else if (strstr(path, "/profile")) {
1199 0 : gs_send_json(cfd, 200,
1200 : "{\"historyId\":\"99999\","
1201 : "\"emailAddress\":\"test@gmail.com\"}");
1202 : } else {
1203 0 : gs_send_json(cfd, 404, "{}");
1204 : }
1205 5 : close(cfd);
1206 : }
1207 1 : free(b64);
1208 1 : close(lfd);
1209 1 : GCOV_FLUSH();
1210 0 : _exit(0);
1211 : }
1212 :
1213 1 : static pid_t start_incremental_server(int *port_out, int count) {
1214 1 : int lfd = gs_make_listener(port_out);
1215 1 : if (lfd < 0) return -1;
1216 1 : pid_t pid = fork();
1217 2 : if (pid < 0) { close(lfd); return -1; }
1218 2 : if (pid == 0) run_incremental_server(lfd, count);
1219 1 : close(lfd);
1220 1 : return pid;
1221 : }
1222 :
1223 1 : static void test_incremental_with_history(void) {
1224 : /* Tests gmail_sync_incremental with a live mock server.
1225 : * Covers: process_message_added, process_labels_added,
1226 : * process_labels_removed, process_message_deleted,
1227 : * the label-refresh branch (label_changes > 0). */
1228 1 : reset_gmail_test_env();
1229 1 : local_pending_fetch_clear();
1230 :
1231 1 : local_gmail_history_save("12345");
1232 :
1233 : /* Pre-save the "deleted" message so remove operations have something to do */
1234 1 : const char *del_uid = "incr000000000002";
1235 1 : local_msg_save("", del_uid, "From: X\r\n\r\nbody\r\n", 18);
1236 1 : local_hdr_save("", del_uid, "X\tDel\t2024-01-01\tINBOX\t0", 25);
1237 1 : label_idx_add("INBOX", del_uid);
1238 :
1239 1 : int port = 0;
1240 : /* Connections: history(1) + msg_added fetch(1) + labels_removed fetch(1)
1241 : * + labels for msg_deleted(1) + labels refresh(1) = 5 */
1242 1 : pid_t pid = start_incremental_server(&port, 5);
1243 1 : if (pid < 0) { ASSERT(0, "incremental: could not start mock server"); return; }
1244 :
1245 1 : usleep(20000);
1246 :
1247 1 : GmailClient *gc = gs_make_client(port);
1248 1 : if (!gc) { gs_wait_child(pid); ASSERT(0, "incremental: client connected"); return; }
1249 :
1250 1 : int rc = gmail_sync_incremental(gc);
1251 1 : char *hid = local_gmail_history_load();
1252 1 : int msg_ok = local_msg_exists("", "incr000000000001");
1253 1 : gmail_disconnect(gc);
1254 1 : gs_wait_child(pid);
1255 :
1256 1 : ASSERT(rc == 0, "incremental: returns 0 on success");
1257 1 : ASSERT(hid != NULL, "incremental: historyId saved");
1258 1 : if (hid) {
1259 1 : ASSERT(strcmp(hid, "99999") == 0, "incremental: new historyId == 99999");
1260 1 : free(hid);
1261 : }
1262 1 : ASSERT(msg_ok, "incremental: added message saved");
1263 : }
1264 :
1265 1 : static void test_incremental_history_expired(void) {
1266 : /* Server returns 404 for history → must return -2. */
1267 1 : reset_gmail_test_env();
1268 :
1269 1 : local_gmail_history_save("old_history_id");
1270 :
1271 1 : int port = 0;
1272 1 : int lfd = gs_make_listener(&port);
1273 1 : if (lfd < 0) { ASSERT(0, "incr_expired: no listener"); return; }
1274 :
1275 1 : pid_t pid = fork();
1276 2 : if (pid < 0) { close(lfd); ASSERT(0, "incr_expired: fork failed"); return; }
1277 2 : if (pid == 0) {
1278 1 : struct sockaddr_in cli = {0};
1279 1 : socklen_t cli_len = sizeof(cli);
1280 2 : for (int i = 0; i < 3; i++) {
1281 2 : int cfd = accept(lfd, (struct sockaddr *)&cli, &cli_len);
1282 2 : if (cfd < 0) break;
1283 : char buf[512];
1284 1 : gs_read_req(cfd, buf, (int)sizeof(buf));
1285 1 : gs_send_json(cfd, 404,
1286 : "{\"error\":{\"code\":404,\"message\":\"History ID is too old\"}}");
1287 1 : close(cfd);
1288 : }
1289 1 : close(lfd);
1290 1 : GCOV_FLUSH();
1291 0 : _exit(0);
1292 : }
1293 1 : close(lfd);
1294 1 : usleep(20000);
1295 :
1296 1 : GmailClient *gc = gs_make_client(port);
1297 1 : if (!gc) { gs_wait_child(pid); ASSERT(0, "incr_expired: client connected"); return; }
1298 :
1299 1 : int rc = gmail_sync_incremental(gc);
1300 1 : gmail_disconnect(gc);
1301 1 : gs_wait_child(pid);
1302 :
1303 1 : ASSERT(rc == -2, "incr_expired: returns -2 on expired historyId");
1304 : }
1305 :
1306 1 : static void test_sync_full_success(void) {
1307 : /* gmail_sync_full = reconcile + fetch_pending + rebuild_indexes. */
1308 1 : reset_gmail_test_env();
1309 1 : local_pending_fetch_clear();
1310 :
1311 1 : int port = 0;
1312 : /* reconcile: list(1) + labels(1) = 2
1313 : * fetch_pending: 2 new messages × 1 each = 2
1314 : * Total = 4 */
1315 1 : pid_t pid = start_reconcile_server(&port, 4);
1316 1 : if (pid < 0) { ASSERT(0, "sync_full: could not start mock server"); return; }
1317 :
1318 1 : usleep(20000);
1319 :
1320 1 : GmailClient *gc = gs_make_client(port);
1321 1 : if (!gc) { gs_wait_child(pid); ASSERT(0, "sync_full: client connected"); return; }
1322 :
1323 1 : int rc = gmail_sync_full(gc);
1324 1 : gmail_disconnect(gc);
1325 1 : gs_wait_child(pid);
1326 :
1327 1 : ASSERT(rc == 0, "sync_full: returns 0");
1328 : }
1329 :
1330 1 : static void test_reconcile_no_history_id_in_list(void) {
1331 : /* Messages list response has no historyId → falls back to GET /profile.
1332 : * Covers the else-branch in gmail_sync_reconcile. */
1333 1 : reset_gmail_test_env();
1334 1 : local_pending_fetch_clear();
1335 :
1336 1 : int port = 0;
1337 1 : int lfd = gs_make_listener(&port);
1338 1 : if (lfd < 0) { ASSERT(0, "reconcile_nohid: no listener"); return; }
1339 :
1340 1 : pid_t pid = fork();
1341 2 : if (pid < 0) { close(lfd); ASSERT(0, "reconcile_nohid: fork failed"); return; }
1342 2 : if (pid == 0) {
1343 1 : struct sockaddr_in cli = {0};
1344 1 : socklen_t cli_len = sizeof(cli);
1345 4 : for (int i = 0; i < 6; i++) {
1346 4 : int cfd = accept(lfd, (struct sockaddr *)&cli, &cli_len);
1347 4 : if (cfd < 0) break;
1348 3 : struct timeval tv = {.tv_sec = 5, .tv_usec = 0};
1349 3 : setsockopt(cfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
1350 : char buf[4096];
1351 3 : if (gs_read_req(cfd, buf, (int)sizeof(buf)) <= 0) { close(cfd); continue; }
1352 3 : char method[16] = {0}, path[2048] = {0};
1353 3 : sscanf(buf, "%15s %2047s", method, path);
1354 :
1355 3 : if (strstr(path, "/messages") && !strstr(path, "/messages/") &&
1356 1 : strcmp(method, "GET") == 0) {
1357 : /* No historyId in list response — forces /profile fallback */
1358 1 : gs_send_json(cfd, 200,
1359 : "{\"messages\":[],"
1360 : "\"resultSizeEstimate\":0}");
1361 2 : } else if (strstr(path, "/profile")) {
1362 1 : gs_send_json(cfd, 200,
1363 : "{\"historyId\":\"77777\","
1364 : "\"emailAddress\":\"test@gmail.com\"}");
1365 1 : } else if (strstr(path, "/labels") && strcmp(method, "GET") == 0) {
1366 1 : gs_send_json(cfd, 200,
1367 : "{\"labels\":["
1368 : "{\"id\":\"INBOX\",\"name\":\"INBOX\"}"
1369 : "]}");
1370 : } else {
1371 0 : gs_send_json(cfd, 404, "{}");
1372 : }
1373 3 : close(cfd);
1374 : }
1375 1 : close(lfd);
1376 1 : GCOV_FLUSH();
1377 0 : _exit(0);
1378 : }
1379 1 : close(lfd);
1380 1 : usleep(20000);
1381 :
1382 1 : GmailClient *gc = gs_make_client(port);
1383 1 : if (!gc) { gs_wait_child(pid); ASSERT(0, "reconcile_nohid: client connected"); return; }
1384 :
1385 1 : int queued = gmail_sync_reconcile(gc);
1386 1 : char *hid = local_gmail_history_load();
1387 1 : gmail_disconnect(gc);
1388 1 : gs_wait_child(pid);
1389 :
1390 1 : ASSERT(queued == 0, "reconcile_nohid: 0 messages queued (empty server)");
1391 1 : ASSERT(hid != NULL, "reconcile_nohid: historyId saved from /profile");
1392 1 : free(hid);
1393 : }
1394 :
1395 : /* ── Registration ────────────────────────────────────────────────────── */
1396 :
1397 1 : void test_gmail_sync(void) {
1398 1 : RUN_TEST(test_filtered_null);
1399 1 : RUN_TEST(test_filtered_category);
1400 1 : RUN_TEST(test_filtered_important);
1401 1 : RUN_TEST(test_not_filtered_system);
1402 1 : RUN_TEST(test_not_filtered_user);
1403 1 : RUN_TEST(test_filtered_edge_cases);
1404 1 : RUN_TEST(test_build_hdr_basic);
1405 1 : RUN_TEST(test_build_hdr_starred);
1406 1 : RUN_TEST(test_build_hdr_no_labels);
1407 1 : RUN_TEST(test_build_hdr_missing_headers);
1408 1 : RUN_TEST(test_build_hdr_combined_flags);
1409 1 : RUN_TEST(test_build_hdr_archive_unread_flags);
1410 1 : RUN_TEST(test_build_hdr_spam_flag);
1411 1 : RUN_TEST(test_incremental_no_history);
1412 1 : RUN_TEST(test_incremental_with_saved_history_no_server);
1413 1 : RUN_TEST(test_repair_archive_flags_clears_unseen);
1414 1 : RUN_TEST(test_repair_archive_flags_preserves_flagged);
1415 1 : RUN_TEST(test_repair_archive_flags_noop_when_already_read);
1416 1 : RUN_TEST(test_pending_fetch_empty_initially);
1417 1 : RUN_TEST(test_pending_fetch_add_and_load);
1418 1 : RUN_TEST(test_pending_fetch_remove);
1419 1 : RUN_TEST(test_pending_fetch_clear);
1420 1 : RUN_TEST(test_pending_fetch_count_matches_load);
1421 1 : RUN_TEST(test_rebuild_indexes_empty_store);
1422 1 : RUN_TEST(test_rebuild_indexes_basic);
1423 1 : RUN_TEST(test_rebuild_indexes_nolabel);
1424 1 : RUN_TEST(test_rebuild_indexes_many_labels);
1425 1 : RUN_TEST(test_rebuild_indexes_trash);
1426 1 : RUN_TEST(test_rebuild_indexes_flags_sync);
1427 1 : RUN_TEST(test_rebuild_indexes_hdr_no_tabs);
1428 1 : RUN_TEST(test_reconcile_success);
1429 1 : RUN_TEST(test_reconcile_with_cached_messages);
1430 1 : RUN_TEST(test_reconcile_server_error);
1431 1 : RUN_TEST(test_reconcile_no_history_id_in_list);
1432 1 : RUN_TEST(test_fetch_pending_success);
1433 1 : RUN_TEST(test_fetch_pending_empty_queue);
1434 1 : RUN_TEST(test_fetch_pending_already_cached);
1435 1 : RUN_TEST(test_fetch_pending_server_error);
1436 1 : RUN_TEST(test_fetch_pending_nolabel_message);
1437 1 : RUN_TEST(test_fetch_pending_with_rules);
1438 1 : RUN_TEST(test_incremental_with_history);
1439 1 : RUN_TEST(test_incremental_history_expired);
1440 1 : RUN_TEST(test_sync_full_success);
1441 1 : }
|