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 67 : for (const char *p = hdr; *p; p++)
183 66 : 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 : /* No UNREAD, no STARRED — but ATTACH_CHECKED/DMARC_CHECKED are always set */
219 1 : const char *last_tab = strrchr(hdr, '\t');
220 1 : int flags = atoi(last_tab + 1);
221 1 : ASSERT((flags & MSG_FLAG_UNSEEN) == 0, "build_hdr no labels: UNSEEN not set");
222 1 : ASSERT((flags & MSG_FLAG_FLAGGED) == 0, "build_hdr no labels: FLAGGED not set");
223 1 : ASSERT((flags & MSG_FLAG_ATTACH_CHECKED) != 0, "build_hdr no labels: ATTACH_CHECKED set");
224 :
225 1 : free(hdr);
226 : }
227 :
228 1 : static void test_build_hdr_missing_headers(void) {
229 : /* Message with no From/Subject/Date headers */
230 1 : const char *raw = "\r\nJust a body.\r\n";
231 1 : char *labels[] = {"INBOX"};
232 1 : char *hdr = gmail_sync_build_hdr(raw, labels, 1);
233 1 : ASSERT(hdr != NULL, "build_hdr missing headers: not NULL");
234 :
235 : /* Should have empty fields but not crash */
236 1 : ASSERT(hdr[0] == '\t', "build_hdr missing headers: from is empty");
237 :
238 1 : free(hdr);
239 : }
240 :
241 1 : static void test_build_hdr_combined_flags(void) {
242 1 : const char *raw = "From: X\r\nSubject: Y\r\nDate: Mon, 14 Apr 2026 08:00:00 +0000\r\n\r\n";
243 1 : char *labels[] = {"UNREAD", "STARRED", "INBOX"};
244 1 : char *hdr = gmail_sync_build_hdr(raw, labels, 3);
245 1 : ASSERT(hdr != NULL, "build_hdr combined: not NULL");
246 :
247 1 : const char *last_tab = strrchr(hdr, '\t');
248 1 : int flags = atoi(last_tab + 1);
249 1 : ASSERT((flags & 1) != 0, "build_hdr combined: UNSEEN set");
250 1 : ASSERT((flags & 2) != 0, "build_hdr combined: FLAGGED set");
251 :
252 1 : free(hdr);
253 : }
254 :
255 : /* ── incremental sync — no history ───────────────────────────────────── */
256 :
257 1 : static void test_incremental_no_history(void) {
258 : /* Without local_store_init, local_gmail_history_load returns NULL → -2 */
259 1 : int rc = gmail_sync_incremental(NULL);
260 1 : ASSERT(rc == -2, "incremental: no historyId → returns -2");
261 : }
262 :
263 : /* ── repair_archive_flags ────────────────────────────────────────────── */
264 :
265 : /* Helper: sets HOME to a temp dir and inits local store for Gmail. */
266 29 : static void setup_gmail_test_env(const char *home) {
267 29 : setenv("HOME", home, 1);
268 29 : unsetenv("XDG_DATA_HOME");
269 29 : local_store_init("gmail://csjpeterjaket@gmail.com", "csjpeterjaket@gmail.com");
270 29 : }
271 :
272 : /* Helper: wipe the test home dir and reinitialise store.
273 : * Used by network-based tests that need a clean message store to avoid
274 : * interference from messages written by earlier tests. */
275 13 : static void reset_gmail_test_env(void) {
276 13 : int _sr = system("rm -rf '/tmp/email-cli-gmail-sync-test'"); (void)_sr;
277 13 : setup_gmail_test_env("/tmp/email-cli-gmail-sync-test");
278 13 : }
279 :
280 1 : static void test_repair_archive_flags_clears_unseen(void) {
281 : /* A message synced to _nolabel while still having UNREAD label → UNSEEN
282 : * flag should be cleared by repair_archive_flags(). */
283 1 : const char home[] = "/tmp/email-cli-gmail-sync-test";
284 1 : setup_gmail_test_env(home);
285 :
286 1 : const char *uid = "0000000000aabbcc";
287 :
288 : /* Write a .hdr with UNSEEN bit set (flags = 1) */
289 1 : const char *hdr = "Sender\tArchived msg\t2026-04-20\tUNREAD\t1";
290 1 : local_hdr_save("", uid, hdr, strlen(hdr));
291 :
292 : /* Add to _nolabel index */
293 1 : label_idx_add("_nolabel", uid);
294 :
295 : /* Run repair */
296 1 : gmail_sync_repair_archive_flags();
297 :
298 : /* Load and verify UNSEEN was cleared */
299 1 : char *loaded = local_hdr_load("", uid);
300 1 : ASSERT(loaded != NULL, "repair: .hdr still exists");
301 1 : const char *last_tab = strrchr(loaded, '\t');
302 1 : ASSERT(last_tab != NULL, "repair: flags tab present");
303 1 : int flags = atoi(last_tab + 1);
304 1 : ASSERT((flags & 1) == 0, "repair: UNSEEN bit cleared for archived message");
305 1 : free(loaded);
306 : }
307 :
308 1 : static void test_repair_archive_flags_preserves_flagged(void) {
309 : /* STARRED (FLAGGED bit) must survive the repair. */
310 1 : const char home[] = "/tmp/email-cli-gmail-sync-test";
311 1 : setup_gmail_test_env(home);
312 :
313 1 : const char *uid = "0000000000aabbdd";
314 :
315 : /* flags = 3 = UNSEEN | FLAGGED */
316 1 : const char *hdr = "Sender\tStarred archived\t2026-04-20\tUNREAD,STARRED\t3";
317 1 : local_hdr_save("", uid, hdr, strlen(hdr));
318 1 : label_idx_add("_nolabel", uid);
319 :
320 1 : gmail_sync_repair_archive_flags();
321 :
322 1 : char *loaded = local_hdr_load("", uid);
323 1 : ASSERT(loaded != NULL, "repair flagged: .hdr exists");
324 1 : const char *last_tab = strrchr(loaded, '\t');
325 1 : int flags = atoi(last_tab + 1);
326 1 : ASSERT((flags & 1) == 0, "repair flagged: UNSEEN cleared");
327 1 : ASSERT((flags & 2) != 0, "repair flagged: FLAGGED preserved");
328 1 : free(loaded);
329 : }
330 :
331 1 : static void test_repair_archive_flags_noop_when_already_read(void) {
332 : /* If UNSEEN is already 0, the .hdr should not be rewritten (flags stay). */
333 1 : const char home[] = "/tmp/email-cli-gmail-sync-test";
334 1 : setup_gmail_test_env(home);
335 :
336 1 : const char *uid = "0000000000aabbee";
337 :
338 : /* flags = 0, no UNREAD */
339 1 : const char *hdr = "Sender\tAlready read\t2026-04-20\t\t0";
340 1 : local_hdr_save("", uid, hdr, strlen(hdr));
341 1 : label_idx_add("_nolabel", uid);
342 :
343 1 : gmail_sync_repair_archive_flags();
344 :
345 1 : char *loaded = local_hdr_load("", uid);
346 1 : ASSERT(loaded != NULL, "repair noop: .hdr exists");
347 1 : const char *last_tab = strrchr(loaded, '\t');
348 1 : int flags = atoi(last_tab + 1);
349 1 : ASSERT(flags == 0, "repair noop: flags remain 0");
350 1 : free(loaded);
351 : }
352 :
353 1 : static void test_build_hdr_archive_unread_flags(void) {
354 : /* build_hdr with UNREAD but no real label → flags still has UNSEEN.
355 : * The caller (sync loop) is responsible for clearing it when assigning
356 : * to _nolabel. Verify build_hdr itself does not silently drop the flag. */
357 1 : const char *raw = "From: X\r\nSubject: Archived\r\nDate: Mon, 14 Apr 2026 08:00:00 +0000\r\n\r\n";
358 1 : char *labels[] = {"UNREAD", "CATEGORY_PROMOTIONS"};
359 1 : char *hdr = gmail_sync_build_hdr(raw, labels, 2);
360 1 : ASSERT(hdr != NULL, "build_hdr archive+unread: not NULL");
361 :
362 1 : const char *last_tab = strrchr(hdr, '\t');
363 1 : int flags = atoi(last_tab + 1);
364 : /* build_hdr sets UNSEEN; the caller must clear it for _nolabel messages */
365 1 : ASSERT((flags & 1) != 0, "build_hdr archive+unread: UNSEEN set by build_hdr (caller clears it)");
366 :
367 1 : free(hdr);
368 : }
369 :
370 : /* ── pending_fetch queue (local_store) ───────────────────────────────── */
371 :
372 1 : static void test_pending_fetch_empty_initially(void) {
373 1 : const char home[] = "/tmp/email-cli-gmail-sync-test";
374 1 : setup_gmail_test_env(home);
375 :
376 1 : local_pending_fetch_clear();
377 1 : ASSERT(local_pending_fetch_count() == 0, "pending_fetch: empty initially");
378 1 : int count = -1;
379 1 : char (*uids)[17] = local_pending_fetch_load(&count);
380 1 : ASSERT(count == 0, "pending_fetch: load count is 0");
381 1 : free(uids);
382 : }
383 :
384 1 : static void test_pending_fetch_add_and_load(void) {
385 1 : const char home[] = "/tmp/email-cli-gmail-sync-test";
386 1 : setup_gmail_test_env(home);
387 1 : local_pending_fetch_clear();
388 :
389 1 : const char *uid1 = "aaaa000000000001";
390 1 : const char *uid2 = "aaaa000000000002";
391 1 : ASSERT(local_pending_fetch_add(uid1) == 0, "pending_fetch: add uid1");
392 1 : ASSERT(local_pending_fetch_add(uid2) == 0, "pending_fetch: add uid2");
393 :
394 1 : ASSERT(local_pending_fetch_count() == 2, "pending_fetch: count == 2");
395 :
396 1 : int count = 0;
397 1 : char (*uids)[17] = local_pending_fetch_load(&count);
398 1 : ASSERT(count == 2, "pending_fetch: load returns 2");
399 1 : ASSERT(uids != NULL, "pending_fetch: uids not NULL");
400 1 : ASSERT(strcmp(uids[0], uid1) == 0 || strcmp(uids[1], uid1) == 0,
401 : "pending_fetch: uid1 present");
402 1 : ASSERT(strcmp(uids[0], uid2) == 0 || strcmp(uids[1], uid2) == 0,
403 : "pending_fetch: uid2 present");
404 1 : free(uids);
405 : }
406 :
407 1 : static void test_pending_fetch_remove(void) {
408 1 : const char home[] = "/tmp/email-cli-gmail-sync-test";
409 1 : setup_gmail_test_env(home);
410 1 : local_pending_fetch_clear();
411 :
412 1 : local_pending_fetch_add("bbbb000000000001");
413 1 : local_pending_fetch_add("bbbb000000000002");
414 1 : local_pending_fetch_add("bbbb000000000003");
415 :
416 1 : local_pending_fetch_remove("bbbb000000000002");
417 :
418 1 : int count = 0;
419 1 : char (*uids)[17] = local_pending_fetch_load(&count);
420 1 : ASSERT(count == 2, "pending_fetch remove: 2 entries remain");
421 1 : int found2 = 0;
422 3 : for (int i = 0; i < count; i++)
423 2 : if (strcmp(uids[i], "bbbb000000000002") == 0) found2 = 1;
424 1 : ASSERT(!found2, "pending_fetch remove: uid2 gone");
425 1 : free(uids);
426 : }
427 :
428 1 : static void test_pending_fetch_clear(void) {
429 1 : const char home[] = "/tmp/email-cli-gmail-sync-test";
430 1 : setup_gmail_test_env(home);
431 :
432 1 : local_pending_fetch_add("cccc000000000001");
433 1 : local_pending_fetch_add("cccc000000000002");
434 1 : ASSERT(local_pending_fetch_count() >= 2, "pending_fetch clear: non-zero before clear");
435 :
436 1 : local_pending_fetch_clear();
437 1 : ASSERT(local_pending_fetch_count() == 0, "pending_fetch clear: zero after clear");
438 : }
439 :
440 1 : static void test_pending_fetch_count_matches_load(void) {
441 1 : const char home[] = "/tmp/email-cli-gmail-sync-test";
442 1 : setup_gmail_test_env(home);
443 1 : local_pending_fetch_clear();
444 :
445 6 : for (int i = 0; i < 5; i++) {
446 : char uid[17];
447 5 : snprintf(uid, sizeof(uid), "dddd%012d", i);
448 5 : local_pending_fetch_add(uid);
449 : }
450 :
451 1 : int cnt_fast = local_pending_fetch_count();
452 1 : int cnt_load = 0;
453 1 : char (*uids)[17] = local_pending_fetch_load(&cnt_load);
454 1 : ASSERT(cnt_fast == cnt_load,
455 : "pending_fetch: count() matches load() count");
456 1 : free(uids);
457 : }
458 :
459 : /* ── gmail_sync_rebuild_indexes ──────────────────────────────────────────── */
460 :
461 : /*
462 : * Exercise the full rebuild_label_indexes + gmail_sync_rebuild_indexes code
463 : * path by populating a set of .hdr files then calling the public function.
464 : *
465 : * Coverage goal: lines 192-338 (rebuild_label_indexes) + 346-357
466 : * (gmail_sync_rebuild_indexes).
467 : */
468 1 : static void test_rebuild_indexes_empty_store(void) {
469 : /* With no messages, rebuild_indexes is a no-op but must not crash. */
470 1 : const char home[] = "/tmp/email-cli-gmail-sync-test";
471 1 : setup_gmail_test_env(home);
472 :
473 1 : int rc = gmail_sync_rebuild_indexes();
474 1 : ASSERT(rc == 0, "rebuild_indexes empty: returns 0");
475 : }
476 :
477 1 : static void test_rebuild_indexes_basic(void) {
478 1 : const char home[] = "/tmp/email-cli-gmail-sync-test";
479 1 : setup_gmail_test_env(home);
480 :
481 : /* Write a few .hdr files with known labels */
482 1 : const char *uid1 = "1100000000000001";
483 1 : const char *uid2 = "1100000000000002";
484 1 : const char *uid3 = "1100000000000003";
485 :
486 : /* uid1: INBOX + UNREAD (flags=1) */
487 1 : const char *hdr1 = "Alice\tHello\t2026-04-01\tINBOX,UNREAD\t1";
488 1 : local_hdr_save("", uid1, hdr1, strlen(hdr1));
489 :
490 : /* uid2: STARRED only (flags=2) */
491 1 : const char *hdr2 = "Bob\tStarred\t2026-04-02\tSTARRED\t2";
492 1 : local_hdr_save("", uid2, hdr2, strlen(hdr2));
493 :
494 : /* uid3: SPAM (flags=0) — should be indexed as _spam */
495 1 : const char *hdr3 = "Eve\tSpam\t2026-04-03\tSPAM\t0";
496 1 : local_hdr_save("", uid3, hdr3, strlen(hdr3));
497 :
498 1 : int rc = gmail_sync_rebuild_indexes();
499 1 : ASSERT(rc == 0, "rebuild_indexes basic: returns 0");
500 :
501 : /* Verify INBOX index contains uid1 */
502 1 : char (*idx_uids)[17] = NULL;
503 1 : int idx_count = 0;
504 1 : int load_rc = label_idx_load("INBOX", &idx_uids, &idx_count);
505 1 : ASSERT(load_rc == 0, "rebuild_indexes: INBOX index loaded");
506 1 : int found1 = 0;
507 2 : for (int i = 0; i < idx_count; i++)
508 1 : if (strcmp(idx_uids[i], uid1) == 0) found1 = 1;
509 1 : free(idx_uids);
510 1 : ASSERT(found1, "rebuild_indexes: uid1 in INBOX index");
511 :
512 : /* Verify _spam index contains uid3 */
513 1 : idx_uids = NULL; idx_count = 0;
514 1 : load_rc = label_idx_load("_spam", &idx_uids, &idx_count);
515 1 : ASSERT(load_rc == 0, "rebuild_indexes: _spam index loaded");
516 1 : int found3 = 0;
517 2 : for (int i = 0; i < idx_count; i++)
518 1 : if (strcmp(idx_uids[i], uid3) == 0) found3 = 1;
519 1 : free(idx_uids);
520 1 : ASSERT(found3, "rebuild_indexes: uid3 in _spam index");
521 : }
522 :
523 1 : static void test_rebuild_indexes_nolabel(void) {
524 : /* A message with only CATEGORY_ labels and no real labels → goes to _nolabel.
525 : * Note: UNREAD is a real (non-CATEGORY_) label, so we must NOT include it
526 : * here — otherwise has_real=1 and the message won't go to _nolabel. */
527 1 : const char home[] = "/tmp/email-cli-gmail-sync-test";
528 1 : setup_gmail_test_env(home);
529 :
530 1 : const char *uid = "2200000000000001";
531 : /* Only category label, no UNREAD (flags=0) → should go to _nolabel */
532 1 : const char *hdr = "Cat\tCategory mail\t2026-04-04\tCATEGORY_PROMOTIONS\t0";
533 1 : local_hdr_save("", uid, hdr, strlen(hdr));
534 :
535 1 : int rc = gmail_sync_rebuild_indexes();
536 1 : ASSERT(rc == 0, "rebuild_indexes nolabel: returns 0");
537 :
538 : /* _nolabel index should contain this uid */
539 1 : char (*idx_uids)[17] = NULL;
540 1 : int idx_count = 0;
541 1 : int load_rc = label_idx_load("_nolabel", &idx_uids, &idx_count);
542 1 : ASSERT(load_rc == 0, "rebuild_indexes nolabel: _nolabel index loaded");
543 1 : int found = 0;
544 3 : for (int i = 0; i < idx_count; i++)
545 2 : if (strcmp(idx_uids[i], uid) == 0) found = 1;
546 1 : free(idx_uids);
547 1 : ASSERT(found, "rebuild_indexes nolabel: uid in _nolabel");
548 :
549 : /* The flags field should remain 0 (no UNSEEN to clear) */
550 1 : char *loaded = local_hdr_load("", uid);
551 1 : ASSERT(loaded != NULL, "rebuild_indexes nolabel: hdr still exists");
552 1 : const char *last_tab = strrchr(loaded, '\t');
553 1 : ASSERT(last_tab != NULL, "rebuild_indexes nolabel: flags tab present");
554 1 : int flags = atoi(last_tab + 1);
555 1 : ASSERT((flags & 1) == 0, "rebuild_indexes nolabel: UNSEEN clear for archived msg");
556 1 : free(loaded);
557 : }
558 :
559 1 : static void test_rebuild_indexes_many_labels(void) {
560 : /* Write many messages to force realloc in rebuild_label_indexes.
561 : * Each message has 6 labels → ~cap*5 pairs initial cap, then grows. */
562 1 : const char home[] = "/tmp/email-cli-gmail-sync-test";
563 1 : setup_gmail_test_env(home);
564 :
565 : /* Write 12 messages, each with multiple labels, to force the realloc path */
566 13 : for (int i = 0; i < 12; i++) {
567 : char uid[17];
568 12 : snprintf(uid, sizeof(uid), "3300%012d", i);
569 : char hdr[256];
570 12 : snprintf(hdr, sizeof(hdr),
571 : "Sender%d\tSubject%d\t2026-04-01\tINBOX,UNREAD,STARRED,SENT,DRAFT,Work\t3",
572 : i, i);
573 12 : local_hdr_save("", uid, hdr, strlen(hdr));
574 : }
575 :
576 1 : int rc = gmail_sync_rebuild_indexes();
577 1 : ASSERT(rc == 0, "rebuild_indexes many: returns 0");
578 :
579 : /* INBOX should have all 12 */
580 1 : char (*idx_uids)[17] = NULL;
581 1 : int idx_count = 0;
582 1 : label_idx_load("INBOX", &idx_uids, &idx_count);
583 1 : ASSERT(idx_count >= 12, "rebuild_indexes many: INBOX has >= 12 entries");
584 1 : free(idx_uids);
585 : }
586 :
587 1 : static void test_rebuild_indexes_trash(void) {
588 : /* TRASH label should be indexed as _trash */
589 1 : const char home[] = "/tmp/email-cli-gmail-sync-test";
590 1 : setup_gmail_test_env(home);
591 :
592 1 : const char *uid = "4400000000000001";
593 1 : const char *hdr = "Trashed\tDeleted\t2026-04-05\tTRASH\t0";
594 1 : local_hdr_save("", uid, hdr, strlen(hdr));
595 :
596 1 : gmail_sync_rebuild_indexes();
597 :
598 1 : char (*idx_uids)[17] = NULL;
599 1 : int idx_count = 0;
600 1 : label_idx_load("_trash", &idx_uids, &idx_count);
601 1 : int found = 0;
602 2 : for (int i = 0; i < idx_count; i++)
603 1 : if (strcmp(idx_uids[i], uid) == 0) found = 1;
604 1 : free(idx_uids);
605 1 : ASSERT(found, "rebuild_indexes trash: uid in _trash index");
606 : }
607 :
608 1 : static void test_rebuild_indexes_flags_sync(void) {
609 : /* A message where flags integer is inconsistent with labels:
610 : * labels say UNREAD,STARRED but flags field says 0.
611 : * rebuild_label_indexes must update the flags field. */
612 1 : const char home[] = "/tmp/email-cli-gmail-sync-test";
613 1 : setup_gmail_test_env(home);
614 :
615 1 : const char *uid = "5500000000000001";
616 : /* flags=0 but UNREAD and STARRED present → new_flags should be 3 */
617 1 : const char *hdr = "X\tFlagsSync\t2026-04-06\tINBOX,UNREAD,STARRED\t0";
618 1 : local_hdr_save("", uid, hdr, strlen(hdr));
619 :
620 1 : gmail_sync_rebuild_indexes();
621 :
622 1 : char *loaded = local_hdr_load("", uid);
623 1 : ASSERT(loaded != NULL, "flags_sync: hdr still exists");
624 1 : const char *last_tab = strrchr(loaded, '\t');
625 1 : int flags = last_tab ? atoi(last_tab + 1) : -1;
626 1 : ASSERT((flags & 1) != 0, "flags_sync: UNSEEN bit set after rebuild");
627 1 : ASSERT((flags & 2) != 0, "flags_sync: FLAGGED bit set after rebuild");
628 1 : free(loaded);
629 : }
630 :
631 1 : static void test_rebuild_indexes_hdr_no_tabs(void) {
632 : /* A malformed .hdr with no tabs should be gracefully skipped. */
633 1 : const char home[] = "/tmp/email-cli-gmail-sync-test";
634 1 : setup_gmail_test_env(home);
635 :
636 1 : const char *uid = "6600000000000001";
637 : /* No tab separators — rebuild_label_indexes should skip gracefully */
638 1 : const char *hdr = "malformed-hdr-no-tabs";
639 1 : local_hdr_save("", uid, hdr, strlen(hdr));
640 :
641 1 : int rc = gmail_sync_rebuild_indexes();
642 1 : ASSERT(rc == 0, "rebuild_indexes no-tabs: no crash, returns 0");
643 : }
644 :
645 : /* ── gmail_sync_incremental: re-verify no-history path after store init ─── */
646 :
647 1 : static void test_incremental_with_saved_history_no_server(void) {
648 : /* Verify that gmail_sync_incremental returns -2 whenever there is no
649 : * saved historyId, regardless of prior store state. This exercises the
650 : * early-return branch in gmail_sync_incremental (lines 794-798). */
651 1 : const char home[] = "/tmp/email-cli-gmail-sync-test";
652 1 : setup_gmail_test_env(home);
653 :
654 : /* Ensure no history id is saved (fresh environment) */
655 1 : char *hid = local_gmail_history_load();
656 : /* If some previous test left a history file, skip gracefully */
657 1 : if (hid) {
658 0 : free(hid);
659 0 : ASSERT(1, "incremental no-server: history present from prev test, skipped");
660 0 : return;
661 : }
662 :
663 1 : int rc = gmail_sync_incremental(NULL);
664 1 : ASSERT(rc == -2, "incremental no-server: no historyId → -2");
665 : }
666 :
667 : /* ── build_hdr: SPAM label sets MSG_FLAG_JUNK ─────────────────────────────── */
668 :
669 1 : static void test_build_hdr_spam_flag(void) {
670 1 : const char *raw = "From: Spammer\r\nSubject: Buy now\r\nDate: Mon, 14 Apr 2026 08:00:00 +0000\r\n\r\n";
671 1 : char *labels[] = {"SPAM"};
672 1 : char *hdr = gmail_sync_build_hdr(raw, labels, 1);
673 1 : ASSERT(hdr != NULL, "build_hdr spam: not NULL");
674 :
675 : /* SPAM label sets MSG_FLAG_JUNK (1<<6 = 64 per local_store.h) */
676 1 : const char *last_tab = strrchr(hdr, '\t');
677 1 : ASSERT(last_tab != NULL, "build_hdr spam: flags tab present");
678 1 : int flags = atoi(last_tab + 1);
679 1 : ASSERT((flags & 64) != 0, "build_hdr spam: JUNK flag set");
680 :
681 1 : free(hdr);
682 : }
683 :
684 : /* ── Mock servers for gmail_sync tests ──────────────────────────────── */
685 :
686 : /*
687 : * reconcile_server: responds to:
688 : * GET /messages → 2-message list with historyId
689 : * GET /labels → label list
690 : * GET /profile → profile with historyId (fallback)
691 : * GET /messages/{id} → raw message body
692 : * any other → 404
693 : */
694 5 : static void run_reconcile_server(int lfd, int count) {
695 5 : const char *raw_email =
696 : "From: alice@example.com\r\n"
697 : "To: me@gmail.com\r\n"
698 : "Subject: Reconcile Test\r\n"
699 : "Date: Mon, 01 Jan 2024 00:00:00 +0000\r\n"
700 : "\r\n"
701 : "Body here.\r\n";
702 5 : char *b64 = gs_b64encode(raw_email, strlen(raw_email));
703 :
704 5 : struct sockaddr_in cli = {0};
705 5 : socklen_t cli_len = sizeof(cli);
706 15 : for (int i = 0; i < count; i++) {
707 10 : int cfd = accept(lfd, (struct sockaddr *)&cli, &cli_len);
708 10 : if (cfd < 0) break;
709 10 : struct timeval tv = {.tv_sec = 5, .tv_usec = 0};
710 10 : setsockopt(cfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
711 :
712 : char buf[4096];
713 10 : if (gs_read_req(cfd, buf, (int)sizeof(buf)) <= 0) { close(cfd); continue; }
714 :
715 10 : char method[16] = {0}, path[2048] = {0};
716 10 : sscanf(buf, "%15s %2047s", method, path);
717 :
718 10 : if (strstr(path, "/messages") && !strstr(path, "/messages/") &&
719 3 : strcmp(method, "GET") == 0) {
720 3 : gs_send_json(cfd, 200,
721 : "{\"messages\":["
722 : "{\"id\":\"aabbcc0000000001\",\"threadId\":\"t1\"},"
723 : "{\"id\":\"aabbcc0000000002\",\"threadId\":\"t2\"}"
724 : "],\"resultSizeEstimate\":2,\"historyId\":\"99001\"}");
725 11 : } else if (strstr(path, "/messages/") && strcmp(method, "GET") == 0) {
726 : char body_buf[2048];
727 4 : snprintf(body_buf, sizeof(body_buf),
728 : "{\"id\":\"aabbcc0000000001\","
729 : "\"labelIds\":[\"INBOX\",\"UNREAD\"],"
730 : "\"raw\":\"%s\"}",
731 : b64 ? b64 : "");
732 4 : gs_send_json(cfd, 200, body_buf);
733 3 : } else if (strstr(path, "/labels") && strcmp(method, "GET") == 0) {
734 3 : gs_send_json(cfd, 200,
735 : "{\"labels\":["
736 : "{\"id\":\"INBOX\",\"name\":\"INBOX\"},"
737 : "{\"id\":\"UNREAD\",\"name\":\"UNREAD\"}"
738 : "]}");
739 0 : } else if (strstr(path, "/profile")) {
740 0 : gs_send_json(cfd, 200,
741 : "{\"historyId\":\"99001\","
742 : "\"emailAddress\":\"test@gmail.com\"}");
743 : } else {
744 0 : gs_send_json(cfd, 404, "{}");
745 : }
746 10 : close(cfd);
747 : }
748 5 : free(b64);
749 5 : close(lfd);
750 5 : GCOV_FLUSH();
751 0 : _exit(0);
752 : }
753 :
754 5 : static pid_t start_reconcile_server(int *port_out, int count) {
755 5 : int lfd = gs_make_listener(port_out);
756 5 : if (lfd < 0) return -1;
757 5 : pid_t pid = fork();
758 10 : if (pid < 0) { close(lfd); return -1; }
759 10 : if (pid == 0) run_reconcile_server(lfd, count);
760 5 : close(lfd);
761 5 : return pid;
762 : }
763 :
764 1 : static void test_reconcile_success(void) {
765 1 : reset_gmail_test_env();
766 1 : local_pending_fetch_clear();
767 :
768 1 : int port = 0;
769 : /* 2 connections: list_messages + list_labels */
770 1 : pid_t pid = start_reconcile_server(&port, 2);
771 1 : if (pid < 0) { ASSERT(0, "reconcile: could not start mock server"); return; }
772 :
773 1 : usleep(20000);
774 :
775 1 : GmailClient *gc = gs_make_client(port);
776 1 : if (!gc) { gs_wait_child(pid); ASSERT(0, "reconcile: client connected"); return; }
777 :
778 1 : int queued = gmail_sync_reconcile(gc);
779 1 : int pending_cnt = local_pending_fetch_count();
780 1 : gmail_disconnect(gc);
781 1 : gs_wait_child(pid);
782 :
783 1 : ASSERT(queued == 2, "reconcile: 2 messages queued");
784 1 : ASSERT(pending_cnt == 2, "reconcile: pending_fetch count == 2");
785 : }
786 :
787 1 : static void test_reconcile_with_cached_messages(void) {
788 : /* Pre-cache one of the two server messages so reconcile sees 1 cached
789 : * and 1 queued. Covers lines 450-456 (the "cached++" path). */
790 1 : reset_gmail_test_env();
791 1 : local_pending_fetch_clear();
792 :
793 : /* Pre-save uid1 (already cached) */
794 1 : const char *uid1 = "aabbcc0000000001";
795 1 : const char *raw = "From: alice@example.com\r\nSubject: Reconcile Test\r\n\r\nBody\r\n";
796 1 : local_msg_save("", uid1, raw, strlen(raw));
797 1 : local_hdr_save("", uid1, "Alice\tReconcile Test\t2024-01-01\tINBOX\t0",
798 : strlen("Alice\tReconcile Test\t2024-01-01\tINBOX\t0"));
799 :
800 1 : int port = 0;
801 : /* 2 connections: list_messages + list_labels */
802 1 : pid_t pid = start_reconcile_server(&port, 2);
803 1 : if (pid < 0) { ASSERT(0, "reconcile_cached: could not start mock server"); return; }
804 :
805 1 : usleep(20000);
806 :
807 1 : GmailClient *gc = gs_make_client(port);
808 1 : if (!gc) { gs_wait_child(pid); ASSERT(0, "reconcile_cached: client connected"); return; }
809 :
810 1 : int queued = gmail_sync_reconcile(gc);
811 1 : int pend_cnt = local_pending_fetch_count();
812 1 : gmail_disconnect(gc);
813 1 : gs_wait_child(pid);
814 :
815 : /* uid1 is cached, uid2 is new → 1 queued */
816 1 : ASSERT(queued == 1, "reconcile_cached: 1 new message queued");
817 1 : ASSERT(pend_cnt == 1, "reconcile_cached: pending count == 1");
818 : }
819 :
820 1 : static void test_fetch_pending_empty_queue(void) {
821 : /* fetch_pending with an empty queue should return 0 immediately. */
822 1 : reset_gmail_test_env();
823 1 : local_pending_fetch_clear();
824 :
825 : /* No network needed — empty queue exits early */
826 1 : setenv("GMAIL_TEST_TOKEN", "test_access_token", 1);
827 1 : setenv("GMAIL_API_BASE_URL", "http://127.0.0.1:1/gmail/v1/users/me", 1);
828 1 : Config cfg = {0};
829 1 : cfg.gmail_mode = 1;
830 1 : cfg.gmail_refresh_token = "fake";
831 1 : GmailClient *gc = gmail_connect(&cfg);
832 1 : if (!gc) { ASSERT(0, "fetch_empty: client created"); return; }
833 :
834 1 : int fetched = gmail_sync_fetch_pending(gc);
835 1 : gmail_disconnect(gc);
836 :
837 1 : ASSERT(fetched == 0, "fetch_empty: returns 0 on empty queue");
838 : }
839 :
840 : /* Server that returns a message with only CATEGORY_ labels (no real labels).
841 : * Used to test the _nolabel path in store_fetched_message (lines 397-404). */
842 1 : static void run_nolabel_msg_server(int lfd, int count) {
843 1 : const char *raw_email =
844 : "From: promo@example.com\r\n"
845 : "To: me@gmail.com\r\n"
846 : "Subject: Promotions\r\n"
847 : "Date: Tue, 01 Jan 2025 00:00:00 +0000\r\n"
848 : "\r\n"
849 : "Click here to buy things!\r\n";
850 1 : char *b64 = gs_b64encode(raw_email, strlen(raw_email));
851 :
852 1 : struct sockaddr_in cli = {0};
853 1 : socklen_t cli_len = sizeof(cli);
854 2 : for (int i = 0; i < count; i++) {
855 1 : int cfd = accept(lfd, (struct sockaddr *)&cli, &cli_len);
856 1 : if (cfd < 0) break;
857 1 : struct timeval tv = {.tv_sec = 5, .tv_usec = 0};
858 1 : setsockopt(cfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
859 :
860 : char buf[4096];
861 1 : if (gs_read_req(cfd, buf, (int)sizeof(buf)) <= 0) { close(cfd); continue; }
862 :
863 1 : char method[16] = {0}, path[2048] = {0};
864 1 : sscanf(buf, "%15s %2047s", method, path);
865 :
866 2 : if (strstr(path, "/messages/") && strcmp(method, "GET") == 0) {
867 : /* Message with only CATEGORY_ label and IMPORTANT (both non-real).
868 : * IMPORTANT is filtered (filtered_label→skip).
869 : * CATEGORY_PROMOTIONS passes filter but is_category_label→true.
870 : * Result: has_real_label=0 → goes to _nolabel with UNSEEN cleared. */
871 : char body_buf[2048];
872 1 : snprintf(body_buf, sizeof(body_buf),
873 : "{\"id\":\"nolabel000000001\","
874 : "\"labelIds\":[\"CATEGORY_PROMOTIONS\",\"IMPORTANT\"],"
875 : "\"raw\":\"%s\"}",
876 : b64 ? b64 : "");
877 1 : gs_send_json(cfd, 200, body_buf);
878 : } else {
879 0 : gs_send_json(cfd, 404, "{}");
880 : }
881 1 : close(cfd);
882 : }
883 1 : free(b64);
884 1 : close(lfd);
885 1 : GCOV_FLUSH();
886 0 : _exit(0);
887 : }
888 :
889 1 : static pid_t start_nolabel_msg_server(int *port_out, int count) {
890 1 : int lfd = gs_make_listener(port_out);
891 1 : if (lfd < 0) return -1;
892 1 : pid_t pid = fork();
893 2 : if (pid < 0) { close(lfd); return -1; }
894 2 : if (pid == 0) run_nolabel_msg_server(lfd, count);
895 1 : close(lfd);
896 1 : return pid;
897 : }
898 :
899 1 : static void test_fetch_pending_nolabel_message(void) {
900 : /* Message has CATEGORY_PROMOTIONS + IMPORTANT labels.
901 : * CATEGORY_PROMOTIONS: is_category_label=true → has_real_label not set.
902 : * IMPORTANT: filtered → skipped entirely.
903 : * Result: has_real_label=0 → goes to _nolabel (covers lines 397-403).
904 : * No UNREAD → cur_flags=0 → UNSEEN was never set, flags remain 0. */
905 1 : reset_gmail_test_env();
906 1 : local_pending_fetch_clear();
907 :
908 1 : const char *uid = "nolabel000000001";
909 1 : local_pending_fetch_add(uid);
910 :
911 1 : int port = 0;
912 1 : pid_t pid = start_nolabel_msg_server(&port, 1);
913 1 : if (pid < 0) { ASSERT(0, "fetch_nolabel: no server"); return; }
914 :
915 1 : usleep(20000);
916 :
917 1 : GmailClient *gc = gs_make_client(port);
918 1 : if (!gc) { gs_wait_child(pid); ASSERT(0, "fetch_nolabel: client connected"); return; }
919 :
920 1 : int fetched = gmail_sync_fetch_pending(gc);
921 1 : gmail_disconnect(gc);
922 1 : gs_wait_child(pid);
923 :
924 1 : ASSERT(fetched == 1, "fetch_nolabel: 1 message downloaded");
925 1 : ASSERT(local_msg_exists("", uid), "fetch_nolabel: .eml saved");
926 :
927 : /* Should be in _nolabel index */
928 1 : char (*idx_uids)[17] = NULL;
929 1 : int idx_count = 0;
930 1 : label_idx_load("_nolabel", &idx_uids, &idx_count);
931 1 : int found = 0;
932 2 : for (int i = 0; i < idx_count; i++)
933 1 : if (strcmp(idx_uids[i], uid) == 0) found = 1;
934 1 : free(idx_uids);
935 1 : ASSERT(found, "fetch_nolabel: uid in _nolabel index");
936 :
937 : /* UNSEEN flag should be cleared (archived messages are always read) */
938 1 : char *hdr = local_hdr_load("", uid);
939 1 : if (hdr) {
940 1 : const char *last_tab = strrchr(hdr, '\t');
941 1 : int flags = last_tab ? atoi(last_tab + 1) : -1;
942 1 : ASSERT((flags & 1) == 0, "fetch_nolabel: UNSEEN cleared for archived msg");
943 1 : free(hdr);
944 : }
945 : }
946 :
947 1 : static void test_fetch_pending_with_rules(void) {
948 : /* Write a mail rule that matches alice@example.com, then fetch_pending
949 : * so apply_rules_to_new_message is called. Covers lines 109-167. */
950 1 : reset_gmail_test_env();
951 1 : local_pending_fetch_clear();
952 :
953 : /* Write a rules.ini for the test account */
954 : char rules_dir[512];
955 1 : snprintf(rules_dir, sizeof(rules_dir),
956 : "/tmp/email-cli-gmail-sync-test/.config/email-cli/accounts/csjpeterjaket@gmail.com");
957 : /* Use system() with literal paths — no variable in rm -rf */
958 1 : int _sr2 = system("mkdir -p '/tmp/email-cli-gmail-sync-test/.config/email-cli/accounts/csjpeterjaket@gmail.com'"); (void)_sr2;
959 :
960 : char rules_path[600];
961 1 : snprintf(rules_path, sizeof(rules_path), "%s/rules.ini", rules_dir);
962 1 : FILE *f = fopen(rules_path, "w");
963 1 : if (f) {
964 1 : fputs("[rule \"Test Rule\"]\n", f);
965 1 : fputs("if-from = *@example.com\n", f);
966 1 : fputs("then-add-label = Filtered\n", f);
967 1 : fclose(f);
968 : }
969 :
970 1 : const char *uid = "aabbcc0000000001";
971 1 : local_pending_fetch_add(uid);
972 :
973 1 : int port = 0;
974 : /* 1 connection: gmail_fetch_message */
975 1 : pid_t pid = start_reconcile_server(&port, 1);
976 1 : if (pid < 0) { ASSERT(0, "fetch_rules: no server"); return; }
977 :
978 1 : usleep(20000);
979 :
980 1 : GmailClient *gc = gs_make_client(port);
981 1 : if (!gc) { gs_wait_child(pid); ASSERT(0, "fetch_rules: client connected"); return; }
982 :
983 1 : int fetched = gmail_sync_fetch_pending(gc);
984 1 : gmail_disconnect(gc);
985 1 : gs_wait_child(pid);
986 :
987 1 : ASSERT(fetched == 1, "fetch_rules: 1 message downloaded");
988 : /* Rule applied — message should be fetched (rules firing doesn't prevent storage) */
989 1 : ASSERT(local_msg_exists("", uid), "fetch_rules: .eml saved after rule application");
990 : }
991 :
992 1 : static void test_reconcile_server_error(void) {
993 : /* When the server returns 500 for messages.list, gmail_list_messages
994 : * treats it as an empty result (0 messages). Reconcile returns 0 with
995 : * nothing queued. Exercises the error-response code path in list. */
996 1 : reset_gmail_test_env();
997 :
998 1 : int port = 0;
999 1 : int lfd = gs_make_listener(&port);
1000 1 : if (lfd < 0) { ASSERT(0, "reconcile_server_err: no listener"); return; }
1001 :
1002 1 : pid_t pid = fork();
1003 2 : if (pid < 0) { close(lfd); ASSERT(0, "reconcile_server_err: fork failed"); return; }
1004 2 : if (pid == 0) {
1005 1 : struct sockaddr_in cli = {0};
1006 1 : socklen_t cli_len = sizeof(cli);
1007 4 : for (int i = 0; i < 4; i++) {
1008 4 : int cfd = accept(lfd, (struct sockaddr *)&cli, &cli_len);
1009 4 : if (cfd < 0) break;
1010 : char buf[512];
1011 3 : gs_read_req(cfd, buf, (int)sizeof(buf));
1012 3 : gs_send_json(cfd, 500, "{\"error\":\"server error\"}");
1013 3 : close(cfd);
1014 : }
1015 1 : close(lfd);
1016 1 : GCOV_FLUSH();
1017 0 : _exit(0);
1018 : }
1019 1 : close(lfd);
1020 1 : usleep(20000);
1021 :
1022 1 : GmailClient *gc = gs_make_client(port);
1023 1 : if (!gc) { gs_wait_child(pid); ASSERT(0, "reconcile_server_err: client connected"); return; }
1024 :
1025 1 : int rc = gmail_sync_reconcile(gc);
1026 1 : gmail_disconnect(gc);
1027 1 : gs_wait_child(pid);
1028 :
1029 : /* gmail_list_messages returns 0 even on 500 (treats as empty list).
1030 : * So reconcile returns 0 with 0 messages queued. */
1031 1 : ASSERT(rc == 0, "reconcile_server_err: returns 0 (empty list) on server 500");
1032 1 : ASSERT(local_pending_fetch_count() == 0,
1033 : "reconcile_server_err: nothing queued when server returns 500");
1034 : }
1035 :
1036 1 : static void test_fetch_pending_success(void) {
1037 1 : reset_gmail_test_env();
1038 1 : local_pending_fetch_clear();
1039 :
1040 1 : const char *uid = "aabbcc0000000001";
1041 1 : local_pending_fetch_add(uid);
1042 :
1043 1 : int port = 0;
1044 : /* 1 connection: gmail_fetch_message for the 1 pending UID */
1045 1 : pid_t pid = start_reconcile_server(&port, 1);
1046 1 : if (pid < 0) { ASSERT(0, "fetch_pending: no server"); return; }
1047 :
1048 1 : usleep(20000);
1049 :
1050 1 : GmailClient *gc = gs_make_client(port);
1051 1 : if (!gc) { gs_wait_child(pid); ASSERT(0, "fetch_pending: client connected"); return; }
1052 :
1053 1 : int fetched = gmail_sync_fetch_pending(gc);
1054 1 : int msg_ok = local_msg_exists("", uid);
1055 1 : int hdr_ok = local_hdr_exists("", uid);
1056 1 : int pend_cnt = local_pending_fetch_count();
1057 1 : gmail_disconnect(gc);
1058 1 : gs_wait_child(pid);
1059 :
1060 1 : ASSERT(fetched == 1, "fetch_pending: 1 message downloaded");
1061 1 : ASSERT(msg_ok, "fetch_pending: .eml saved");
1062 1 : ASSERT(hdr_ok, "fetch_pending: .hdr saved");
1063 1 : ASSERT(pend_cnt == 0, "fetch_pending: queue empty");
1064 : }
1065 :
1066 1 : static void test_fetch_pending_already_cached(void) {
1067 1 : reset_gmail_test_env();
1068 1 : local_pending_fetch_clear();
1069 :
1070 1 : const char *uid = "bbccdd0000000001";
1071 1 : const char *raw = "From: X\r\nSubject: Y\r\n\r\nBody\r\n";
1072 1 : local_msg_save("", uid, raw, strlen(raw));
1073 1 : const char *hdr = "X\tY\t2024-01-01\tINBOX\t0";
1074 1 : local_hdr_save("", uid, hdr, strlen(hdr));
1075 1 : local_pending_fetch_add(uid);
1076 :
1077 : /* Use an unreachable port — no network calls should happen */
1078 1 : setenv("GMAIL_TEST_TOKEN", "test_access_token", 1);
1079 1 : setenv("GMAIL_API_BASE_URL", "http://127.0.0.1:1/gmail/v1/users/me", 1);
1080 1 : Config cfg = {0};
1081 1 : cfg.gmail_mode = 1;
1082 1 : cfg.gmail_refresh_token = "fake";
1083 1 : GmailClient *gc = gmail_connect(&cfg);
1084 1 : if (!gc) { ASSERT(0, "fetch_cached: client created"); return; }
1085 :
1086 1 : int fetched = gmail_sync_fetch_pending(gc);
1087 1 : int pend_cnt = local_pending_fetch_count();
1088 1 : gmail_disconnect(gc);
1089 :
1090 1 : ASSERT(fetched == 0, "fetch_cached: 0 downloaded (already cached)");
1091 1 : ASSERT(pend_cnt == 0, "fetch_cached: queue cleared");
1092 : }
1093 :
1094 1 : static void test_fetch_pending_server_error(void) {
1095 1 : reset_gmail_test_env();
1096 1 : local_pending_fetch_clear();
1097 :
1098 1 : const char *uid = "ccddee0000000001";
1099 1 : local_pending_fetch_add(uid);
1100 :
1101 1 : int port = 0;
1102 1 : int lfd = gs_make_listener(&port);
1103 1 : if (lfd < 0) { ASSERT(0, "fetch_err: no listener"); return; }
1104 :
1105 1 : pid_t pid = fork();
1106 2 : if (pid < 0) { close(lfd); ASSERT(0, "fetch_err: fork failed"); return; }
1107 2 : if (pid == 0) {
1108 1 : struct sockaddr_in cli = {0};
1109 1 : socklen_t cli_len = sizeof(cli);
1110 2 : for (int i = 0; i < 3; i++) {
1111 2 : int cfd = accept(lfd, (struct sockaddr *)&cli, &cli_len);
1112 2 : if (cfd < 0) break;
1113 : char buf[512];
1114 1 : gs_read_req(cfd, buf, (int)sizeof(buf));
1115 1 : gs_send_json(cfd, 404, "{\"error\":{\"code\":404}}");
1116 1 : close(cfd);
1117 : }
1118 1 : close(lfd);
1119 1 : GCOV_FLUSH();
1120 0 : _exit(0);
1121 : }
1122 1 : close(lfd);
1123 1 : usleep(20000);
1124 :
1125 1 : GmailClient *gc = gs_make_client(port);
1126 1 : if (!gc) { gs_wait_child(pid); ASSERT(0, "fetch_err: client connected"); return; }
1127 :
1128 1 : int fetched = gmail_sync_fetch_pending(gc);
1129 1 : int pend_cnt = local_pending_fetch_count();
1130 1 : gmail_disconnect(gc);
1131 1 : gs_wait_child(pid);
1132 :
1133 1 : ASSERT(fetched == 0, "fetch_err: 0 downloaded on 404");
1134 1 : ASSERT(pend_cnt == 1, "fetch_err: uid stays in queue");
1135 : }
1136 :
1137 : /*
1138 : * incremental_server: handles the various gmail_sync_incremental paths.
1139 : * Returns history with messagesAdded, labelsAdded, labelsRemoved, messagesDeleted.
1140 : */
1141 1 : static void run_incremental_server(int lfd, int count) {
1142 1 : const char *raw_email =
1143 : "From: bob@example.com\r\n"
1144 : "To: me@gmail.com\r\n"
1145 : "Subject: Incremental Test\r\n"
1146 : "Date: Mon, 01 Jan 2024 12:00:00 +0000\r\n"
1147 : "\r\n"
1148 : "Incremental body.\r\n";
1149 1 : char *b64 = gs_b64encode(raw_email, strlen(raw_email));
1150 :
1151 1 : struct sockaddr_in cli = {0};
1152 1 : socklen_t cli_len = sizeof(cli);
1153 6 : for (int i = 0; i < count; i++) {
1154 5 : int cfd = accept(lfd, (struct sockaddr *)&cli, &cli_len);
1155 5 : if (cfd < 0) break;
1156 5 : struct timeval tv = {.tv_sec = 5, .tv_usec = 0};
1157 5 : setsockopt(cfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
1158 :
1159 : char buf[4096];
1160 5 : if (gs_read_req(cfd, buf, (int)sizeof(buf)) <= 0) { close(cfd); continue; }
1161 :
1162 5 : char method[16] = {0}, path[2048] = {0};
1163 5 : sscanf(buf, "%15s %2047s", method, path);
1164 :
1165 6 : if (strstr(path, "/history") && strcmp(method, "GET") == 0) {
1166 : /* Nested format matching the real Gmail History API:
1167 : * events are objects inside the "history" array, and each
1168 : * event item nests the message ID under {"message":{"id":"..."}}. */
1169 : char body_buf[2048];
1170 1 : snprintf(body_buf, sizeof(body_buf),
1171 : "{"
1172 : "\"historyId\":\"99999\","
1173 : "\"history\":["
1174 : " {"
1175 : " \"messagesAdded\":[{\"message\":{\"id\":\"incr000000000001\"}}],"
1176 : " \"labelsAdded\":[{\"message\":{\"id\":\"incr000000000001\"},\"labelIds\":[\"STARRED\"]}],"
1177 : " \"labelsRemoved\":[{\"message\":{\"id\":\"incr000000000001\"},\"labelIds\":[\"UNREAD\"]}],"
1178 : " \"messagesDeleted\":[{\"message\":{\"id\":\"incr000000000002\"}}]"
1179 : " }"
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 : }
|