Line data Source code
1 : #include "test_helpers.h"
2 : #include "gmail_client.h"
3 : #include "config.h"
4 : #include <stdlib.h>
5 : #include <string.h>
6 : #include <stdio.h>
7 : #include <unistd.h>
8 : #include <time.h>
9 : #include <signal.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 : /* ── gmail_connect — error paths ──────────────────────────────────── */
22 :
23 1 : static void test_connect_not_gmail(void) {
24 1 : Config cfg = {0};
25 : /* gmail_mode is 0 → should fail */
26 1 : GmailClient *c = gmail_connect(&cfg);
27 1 : ASSERT(c == NULL, "connect: fails for non-Gmail account");
28 : }
29 :
30 1 : static void test_connect_no_token(void) {
31 1 : Config cfg = {0};
32 1 : cfg.gmail_mode = 1;
33 : /* No refresh_token → auth_refresh fails → connect fails */
34 1 : GmailClient *c = gmail_connect(&cfg);
35 1 : ASSERT(c == NULL, "connect: fails with no refresh_token");
36 : }
37 :
38 1 : static void test_disconnect_null(void) {
39 : /* Should not crash */
40 1 : gmail_disconnect(NULL);
41 1 : ASSERT(1, "disconnect NULL: no crash");
42 : }
43 :
44 : /* ── base64url encode/decode ───────────────────────────────────────── */
45 :
46 1 : static void test_b64_roundtrip(void) {
47 1 : const char *orig = "Hello, Gmail API!";
48 1 : char *enc = gmail_base64url_encode((const unsigned char *)orig, strlen(orig));
49 1 : ASSERT(enc != NULL, "b64 encode: not NULL");
50 :
51 1 : size_t dec_len = 0;
52 1 : char *dec = gmail_base64url_decode(enc, strlen(enc), &dec_len);
53 1 : ASSERT(dec != NULL, "b64 decode: not NULL");
54 1 : ASSERT(dec_len == strlen(orig), "b64 roundtrip: length matches");
55 1 : ASSERT(memcmp(dec, orig, dec_len) == 0, "b64 roundtrip: content matches");
56 1 : free(enc);
57 1 : free(dec);
58 : }
59 :
60 1 : static void test_b64_empty(void) {
61 1 : char *enc = gmail_base64url_encode((const unsigned char *)"", 0);
62 1 : ASSERT(enc != NULL, "b64 encode empty: not NULL");
63 1 : ASSERT(enc[0] == '\0', "b64 encode empty: empty string");
64 :
65 1 : size_t dec_len = 0;
66 1 : char *dec = gmail_base64url_decode("", 0, &dec_len);
67 1 : ASSERT(dec != NULL, "b64 decode empty: not NULL");
68 1 : ASSERT(dec_len == 0, "b64 decode empty: zero length");
69 1 : free(enc);
70 1 : free(dec);
71 : }
72 :
73 1 : static void test_b64_known_vector(void) {
74 : /* "Man" → TWFu in standard base64, same in base64url */
75 1 : size_t len = 0;
76 1 : char *dec = gmail_base64url_decode("TWFu", 4, &len);
77 1 : ASSERT(dec != NULL && len == 3, "b64 known: length=3");
78 1 : ASSERT(memcmp(dec, "Man", 3) == 0, "b64 known: Man");
79 1 : free(dec);
80 : }
81 :
82 1 : static void test_b64_url_chars(void) {
83 : /* Verify - and _ (base64url) instead of + and / */
84 1 : unsigned char data[] = {0xfb, 0xff, 0xfe};
85 1 : char *enc = gmail_base64url_encode(data, 3);
86 1 : ASSERT(enc != NULL, "b64url chars: not NULL");
87 1 : ASSERT(strchr(enc, '+') == NULL, "b64url: no +");
88 1 : ASSERT(strchr(enc, '/') == NULL, "b64url: no /");
89 1 : ASSERT(strchr(enc, '=') == NULL, "b64url: no padding");
90 1 : free(enc);
91 : }
92 :
93 1 : static void test_b64_decode_null_len_out(void) {
94 : /* NULL out_len should not crash */
95 1 : char *dec = gmail_base64url_decode("TWFu", 4, NULL);
96 1 : ASSERT(dec != NULL, "b64 decode null len_out: not NULL");
97 1 : free(dec);
98 : }
99 :
100 1 : static void test_b64_large_roundtrip(void) {
101 : /* 256 bytes of binary data */
102 : unsigned char data[256];
103 257 : for (int i = 0; i < 256; i++) data[i] = (unsigned char)i;
104 1 : char *enc = gmail_base64url_encode(data, 256);
105 1 : ASSERT(enc != NULL, "b64 large encode: not NULL");
106 1 : size_t out_len = 0;
107 1 : char *dec = gmail_base64url_decode(enc, strlen(enc), &out_len);
108 1 : ASSERT(dec != NULL, "b64 large decode: not NULL");
109 1 : ASSERT(out_len == 256, "b64 large: length matches");
110 1 : free(enc);
111 1 : free(dec);
112 : }
113 :
114 : /* ── Mock HTTP server helpers ─────────────────────────────────────── */
115 :
116 : /*
117 : * Create a listening TCP socket bound to a random loopback port.
118 : * Returns the fd and fills *port_out with the actual port number.
119 : */
120 30 : static int make_mock_listener(int *port_out) {
121 30 : int fd = socket(AF_INET, SOCK_STREAM, 0);
122 30 : if (fd < 0) return -1;
123 30 : int one = 1;
124 30 : setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
125 : /* 3-second accept() timeout so server children don't hang when the test
126 : * returns early due to an ASSERT failure before connecting. */
127 30 : struct timeval acc_tv = {.tv_sec = 3, .tv_usec = 0};
128 30 : setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &acc_tv, sizeof(acc_tv));
129 30 : struct sockaddr_in addr = {0};
130 30 : addr.sin_family = AF_INET;
131 30 : addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
132 30 : addr.sin_port = 0;
133 60 : if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0 ||
134 30 : listen(fd, 8) < 0) {
135 0 : close(fd);
136 0 : return -1;
137 : }
138 30 : socklen_t len = sizeof(addr);
139 30 : getsockname(fd, (struct sockaddr *)&addr, &len);
140 30 : *port_out = ntohs(addr.sin_port);
141 30 : return fd;
142 : }
143 :
144 : /* Base64url encode helper for mock server (same logic as production) */
145 : static const char mock_b64url_chars[] =
146 : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
147 :
148 2 : static char *mock_b64url_encode(const char *data, size_t len) {
149 2 : size_t alloc = ((len + 2) / 3) * 4 + 1;
150 2 : char *out = malloc(alloc);
151 2 : if (!out) return NULL;
152 2 : size_t o = 0;
153 98 : for (size_t i = 0; i < len; i += 3) {
154 96 : unsigned int n = ((unsigned int)(unsigned char)data[i]) << 16;
155 96 : if (i + 1 < len) n |= ((unsigned int)(unsigned char)data[i+1]) << 8;
156 96 : if (i + 2 < len) n |= ((unsigned int)(unsigned char)data[i+2]);
157 96 : out[o++] = mock_b64url_chars[(n >> 18) & 0x3F];
158 96 : out[o++] = mock_b64url_chars[(n >> 12) & 0x3F];
159 96 : if (i + 1 < len) out[o++] = mock_b64url_chars[(n >> 6) & 0x3F];
160 96 : if (i + 2 < len) out[o++] = mock_b64url_chars[n & 0x3F];
161 : }
162 2 : out[o] = '\0';
163 2 : return out;
164 : }
165 :
166 29 : static void mock_send_json(int fd, int status_code, const char *body) {
167 29 : const char *reason = (status_code == 200) ? "OK" :
168 : (status_code == 204) ? "No Content" :
169 : (status_code == 401) ? "Unauthorized" :
170 : (status_code == 404) ? "Not Found" : "Error";
171 : char header[512];
172 29 : size_t body_len = body ? strlen(body) : 0;
173 29 : snprintf(header, sizeof(header),
174 : "HTTP/1.1 %d %s\r\n"
175 : "Content-Type: application/json\r\n"
176 : "Content-Length: %zu\r\n"
177 : "Connection: close\r\n"
178 : "\r\n",
179 : status_code, reason, body_len);
180 : ssize_t r;
181 29 : r = write(fd, header, strlen(header)); (void)r;
182 29 : if (body && body_len > 0) {
183 28 : r = write(fd, body, body_len); (void)r;
184 : }
185 29 : }
186 :
187 : /* Reads HTTP request headers into buf, returns bytes read */
188 30 : static int mock_read_request(int fd, char *buf, int bufsz) {
189 30 : int total = 0;
190 30 : while (total < bufsz - 1) {
191 30 : ssize_t n = read(fd, buf + total, (size_t)(bufsz - total - 1));
192 30 : if (n <= 0) break;
193 30 : total += (int)n;
194 30 : buf[total] = '\0';
195 30 : if (strstr(buf, "\r\n\r\n")) break;
196 : }
197 30 : buf[total] = '\0';
198 30 : return total;
199 : }
200 :
201 : /*
202 : * Dispatch one HTTP request and send a response based on the path.
203 : * This mock handles all Gmail API paths used by gmail_client.c.
204 : */
205 17 : static void mock_handle_one(int fd) {
206 : char buf[8192];
207 34 : if (mock_read_request(fd, buf, (int)sizeof(buf)) <= 0) return;
208 :
209 17 : char method[16] = {0};
210 17 : char path[2048] = {0};
211 17 : if (sscanf(buf, "%15s %2047s", method, path) != 2) return;
212 :
213 : /* POST /token (auth refresh — used by 401 retry tests) */
214 17 : if (strstr(path, "/token")) {
215 0 : mock_send_json(fd, 200, "{\"access_token\":\"new_token_after_401\",\"expires_in\":3600}");
216 0 : return;
217 : }
218 :
219 : /* DELETE /labels/{id} */
220 17 : if (strstr(path, "/labels/") && strcmp(method, "DELETE") == 0) {
221 1 : mock_send_json(fd, 204, NULL);
222 1 : return;
223 : }
224 :
225 : /* POST /labels — create label */
226 16 : if (strstr(path, "/labels") && strcmp(method, "POST") == 0) {
227 2 : mock_send_json(fd, 200,
228 : "{\"id\":\"Label_Test001\","
229 : "\"name\":\"TestLabel\","
230 : "\"type\":\"user\"}");
231 2 : return;
232 : }
233 :
234 : /* GET /labels */
235 14 : if (strstr(path, "/labels") && strcmp(method, "GET") == 0) {
236 1 : mock_send_json(fd, 200,
237 : "{\"labels\":["
238 : "{\"id\":\"INBOX\",\"name\":\"INBOX\"},"
239 : "{\"id\":\"UNREAD\",\"name\":\"UNREAD\"},"
240 : "{\"id\":\"Work\",\"name\":\"Work\"}"
241 : "]}");
242 1 : return;
243 : }
244 :
245 : /* GET /profile */
246 13 : if (strstr(path, "/profile")) {
247 1 : mock_send_json(fd, 200,
248 : "{\"historyId\":\"12345\","
249 : "\"emailAddress\":\"test@gmail.com\"}");
250 1 : return;
251 : }
252 :
253 : /* GET /history */
254 12 : if (strstr(path, "/history")) {
255 1 : mock_send_json(fd, 200,
256 : "{\"historyId\":\"12346\","
257 : "\"history\":[]}");
258 1 : return;
259 : }
260 :
261 : /* POST /messages/{id}/modify */
262 11 : if (strstr(path, "/modify") && strcmp(method, "POST") == 0) {
263 3 : mock_send_json(fd, 200,
264 : "{\"id\":\"msg001\",\"labelIds\":[\"INBOX\"]}");
265 3 : return;
266 : }
267 :
268 : /* POST /messages/{id}/trash */
269 8 : if (strstr(path, "/trash") && strcmp(method, "POST") == 0) {
270 1 : mock_send_json(fd, 200,
271 : "{\"id\":\"msg001\",\"labelIds\":[\"TRASH\"]}");
272 1 : return;
273 : }
274 :
275 : /* POST /messages/{id}/untrash */
276 7 : if (strstr(path, "/untrash") && strcmp(method, "POST") == 0) {
277 1 : mock_send_json(fd, 200,
278 : "{\"id\":\"msg001\",\"labelIds\":[\"INBOX\"]}");
279 1 : return;
280 : }
281 :
282 : /* POST /messages/send */
283 6 : if (strstr(path, "/messages/send") && strcmp(method, "POST") == 0) {
284 1 : mock_send_json(fd, 200,
285 : "{\"id\":\"sent001\",\"labelIds\":[\"SENT\"]}");
286 1 : return;
287 : }
288 :
289 : /* GET /messages/{id}?format=raw — single message fetch */
290 5 : if (strstr(path, "/messages/") && strcmp(method, "GET") == 0) {
291 2 : const char *raw_email =
292 : "From: test@example.com\r\n"
293 : "To: me@gmail.com\r\n"
294 : "Subject: Test Message\r\n"
295 : "Date: Mon, 01 Jan 2024 00:00:00 +0000\r\n"
296 : "\r\n"
297 : "Hello, this is a test message body.\r\n";
298 2 : char *raw_b64 = mock_b64url_encode(raw_email, strlen(raw_email));
299 2 : if (!raw_b64) { mock_send_json(fd, 500, "{}"); return; }
300 : char body_buf[4096];
301 2 : snprintf(body_buf, sizeof(body_buf),
302 : "{\"id\":\"msg001\","
303 : "\"threadId\":\"thread001\","
304 : "\"labelIds\":[\"INBOX\",\"UNREAD\",\"STARRED\"],"
305 : "\"raw\":\"%s\"}",
306 : raw_b64);
307 2 : free(raw_b64);
308 2 : mock_send_json(fd, 200, body_buf);
309 2 : return;
310 : }
311 :
312 : /* GET /messages?... — list messages */
313 3 : if (strstr(path, "/messages") && strcmp(method, "GET") == 0) {
314 3 : mock_send_json(fd, 200,
315 : "{\"messages\":["
316 : "{\"id\":\"msg001\",\"threadId\":\"thread001\"},"
317 : "{\"id\":\"msg002\",\"threadId\":\"thread002\"}"
318 : "],\"resultSizeEstimate\":2,\"historyId\":\"12345\"}");
319 3 : return;
320 : }
321 :
322 0 : mock_send_json(fd, 404, "{}");
323 : }
324 :
325 : /*
326 : * Run the mock HTTP server child process.
327 : * Handles exactly `count` connections then exits.
328 : */
329 17 : static void run_mock_http_server(int listen_fd, int count) {
330 17 : struct sockaddr_in cli = {0};
331 17 : socklen_t cli_len = sizeof(cli);
332 34 : for (int i = 0; i < count; i++) {
333 17 : int cfd = accept(listen_fd, (struct sockaddr *)&cli, &cli_len);
334 17 : if (cfd < 0) break;
335 17 : struct timeval tv = {.tv_sec = 5, .tv_usec = 0};
336 17 : setsockopt(cfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
337 17 : mock_handle_one(cfd);
338 17 : close(cfd);
339 : }
340 17 : close(listen_fd);
341 17 : GCOV_FLUSH();
342 0 : _exit(0);
343 : }
344 :
345 : /* Start mock server in a forked child. Returns child PID or -1. */
346 17 : static pid_t start_mock_server(int *port_out, int connection_count) {
347 17 : int listen_fd = make_mock_listener(port_out);
348 17 : if (listen_fd < 0) return -1;
349 :
350 17 : pid_t pid = fork();
351 34 : if (pid < 0) { close(listen_fd); return -1; }
352 34 : if (pid == 0) {
353 17 : run_mock_http_server(listen_fd, connection_count);
354 : /* unreachable */
355 : }
356 17 : close(listen_fd);
357 17 : return pid;
358 : }
359 :
360 : /* Build a connected GmailClient pointing at a local mock HTTP server. */
361 30 : static GmailClient *make_test_client(int port) {
362 : /* Set test token and API base URL */
363 : char api_base[128];
364 30 : snprintf(api_base, sizeof(api_base), "http://127.0.0.1:%d/gmail/v1/users/me", port);
365 30 : setenv("GMAIL_TEST_TOKEN", "test_access_token_12345", 1);
366 30 : setenv("GMAIL_API_BASE_URL", api_base, 1);
367 :
368 30 : Config cfg = {0};
369 30 : cfg.gmail_mode = 1;
370 30 : cfg.gmail_refresh_token = "fake_refresh_token";
371 :
372 30 : GmailClient *c = gmail_connect(&cfg);
373 30 : return c;
374 : }
375 :
376 30 : static void wait_child(pid_t pid) {
377 60 : if (pid <= 0) return;
378 : /* Poll with timeout: kill child if it doesn't exit within 5s. */
379 60 : for (int i = 0; i < 50; i++) {
380 : int st;
381 60 : pid_t r = waitpid(pid, &st, WNOHANG);
382 60 : if (r != 0) return;
383 30 : struct timespec ts = {0, 100000000L}; /* 100ms */
384 30 : nanosleep(&ts, NULL);
385 : }
386 0 : kill(pid, SIGKILL);
387 0 : int st; waitpid(pid, &st, 0);
388 : }
389 :
390 : /* ── Tests using the mock HTTP server ─────────────────────────────── */
391 :
392 1 : static void test_connect_with_test_token(void) {
393 : /* GMAIL_TEST_TOKEN allows connect without a real refresh */
394 1 : setenv("GMAIL_TEST_TOKEN", "test_token_abc", 1);
395 1 : setenv("GMAIL_API_BASE_URL", "http://127.0.0.1:1/gmail/v1/users/me", 1);
396 :
397 1 : Config cfg = {0};
398 1 : cfg.gmail_mode = 1;
399 1 : cfg.gmail_refresh_token = "any";
400 :
401 1 : GmailClient *c = gmail_connect(&cfg);
402 1 : ASSERT(c != NULL, "connect with GMAIL_TEST_TOKEN: succeeds");
403 1 : gmail_disconnect(c);
404 :
405 1 : unsetenv("GMAIL_TEST_TOKEN");
406 1 : unsetenv("GMAIL_API_BASE_URL");
407 : }
408 :
409 1 : static void test_set_progress(void) {
410 1 : setenv("GMAIL_TEST_TOKEN", "tok", 1);
411 1 : setenv("GMAIL_API_BASE_URL", "http://127.0.0.1:1/gmail/v1/users/me", 1);
412 :
413 1 : Config cfg = {0};
414 1 : cfg.gmail_mode = 1;
415 1 : cfg.gmail_refresh_token = "any";
416 1 : GmailClient *c = gmail_connect(&cfg);
417 1 : ASSERT(c != NULL, "set_progress: client created");
418 :
419 : /* NULL client should not crash */
420 1 : gmail_set_progress(NULL, NULL, NULL);
421 :
422 : /* set_progress with valid client */
423 1 : gmail_set_progress(c, NULL, NULL);
424 :
425 1 : gmail_disconnect(c);
426 1 : unsetenv("GMAIL_TEST_TOKEN");
427 1 : unsetenv("GMAIL_API_BASE_URL");
428 : }
429 :
430 1 : static void test_list_labels(void) {
431 1 : int port = 0;
432 1 : pid_t pid = start_mock_server(&port, 1);
433 1 : if (pid < 0) { ASSERT(0, "list_labels: could not start mock server"); return; }
434 :
435 1 : usleep(20000); /* let child bind */
436 :
437 1 : GmailClient *c = make_test_client(port);
438 1 : ASSERT(c != NULL, "list_labels: client connected");
439 :
440 1 : char **names = NULL, **ids = NULL;
441 1 : int count = 0;
442 1 : int rc = gmail_list_labels(c, &names, &ids, &count);
443 1 : ASSERT(rc == 0, "list_labels: returns 0");
444 1 : ASSERT(count >= 1, "list_labels: at least one label");
445 :
446 4 : for (int i = 0; i < count; i++) { free(names[i]); free(ids[i]); }
447 1 : free(names);
448 1 : free(ids);
449 1 : gmail_disconnect(c);
450 1 : wait_child(pid);
451 : }
452 :
453 1 : static void test_list_messages(void) {
454 1 : int port = 0;
455 1 : pid_t pid = start_mock_server(&port, 1);
456 1 : if (pid < 0) { ASSERT(0, "list_messages: could not start mock server"); return; }
457 :
458 1 : usleep(20000);
459 :
460 1 : GmailClient *c = make_test_client(port);
461 1 : ASSERT(c != NULL, "list_messages: client connected");
462 :
463 1 : char (*uids)[17] = NULL;
464 1 : int count = 0;
465 1 : int rc = gmail_list_messages(c, "INBOX", NULL, &uids, &count, NULL);
466 1 : ASSERT(rc == 0, "list_messages: returns 0");
467 1 : ASSERT(count >= 1, "list_messages: at least one message");
468 :
469 1 : free(uids);
470 1 : gmail_disconnect(c);
471 1 : wait_child(pid);
472 : }
473 :
474 1 : static void test_list_messages_with_query(void) {
475 1 : int port = 0;
476 1 : pid_t pid = start_mock_server(&port, 1);
477 1 : if (pid < 0) { ASSERT(0, "list_messages_query: could not start mock server"); return; }
478 :
479 1 : usleep(20000);
480 :
481 1 : GmailClient *c = make_test_client(port);
482 1 : ASSERT(c != NULL, "list_messages_query: client connected");
483 :
484 1 : char (*uids)[17] = NULL;
485 1 : int count = 0;
486 1 : char *history_id = NULL;
487 1 : int rc = gmail_list_messages(c, NULL, "is:unread", &uids, &count, &history_id);
488 1 : ASSERT(rc == 0, "list_messages_query: returns 0");
489 :
490 1 : free(uids);
491 1 : free(history_id);
492 1 : gmail_disconnect(c);
493 1 : wait_child(pid);
494 : }
495 :
496 1 : static void test_fetch_message(void) {
497 1 : int port = 0;
498 1 : pid_t pid = start_mock_server(&port, 1);
499 1 : if (pid < 0) { ASSERT(0, "fetch_message: could not start mock server"); return; }
500 :
501 1 : usleep(20000);
502 :
503 1 : GmailClient *c = make_test_client(port);
504 1 : ASSERT(c != NULL, "fetch_message: client connected");
505 :
506 1 : char **labels = NULL;
507 1 : int label_count = 0;
508 1 : char *body = gmail_fetch_message(c, "msg001", &labels, &label_count);
509 1 : ASSERT(body != NULL, "fetch_message: body not NULL");
510 1 : ASSERT(label_count >= 1, "fetch_message: has labels");
511 1 : ASSERT(strstr(body, "Test Message") != NULL, "fetch_message: contains subject");
512 :
513 4 : for (int i = 0; i < label_count; i++) free(labels[i]);
514 1 : free(labels);
515 1 : free(body);
516 1 : gmail_disconnect(c);
517 1 : wait_child(pid);
518 : }
519 :
520 1 : static void test_fetch_message_no_labels(void) {
521 1 : int port = 0;
522 1 : pid_t pid = start_mock_server(&port, 1);
523 1 : if (pid < 0) { ASSERT(0, "fetch_msg_nolabels: could not start mock server"); return; }
524 :
525 1 : usleep(20000);
526 :
527 1 : GmailClient *c = make_test_client(port);
528 1 : ASSERT(c != NULL, "fetch_msg_nolabels: client connected");
529 :
530 : /* Pass NULL for labels_out, NULL for label_count_out */
531 1 : char *body = gmail_fetch_message(c, "msg001", NULL, NULL);
532 1 : ASSERT(body != NULL, "fetch_msg_nolabels: body not NULL");
533 :
534 1 : free(body);
535 1 : gmail_disconnect(c);
536 1 : wait_child(pid);
537 : }
538 :
539 1 : static void test_modify_labels(void) {
540 1 : int port = 0;
541 1 : pid_t pid = start_mock_server(&port, 1);
542 1 : if (pid < 0) { ASSERT(0, "modify_labels: could not start mock server"); return; }
543 :
544 1 : usleep(20000);
545 :
546 1 : GmailClient *c = make_test_client(port);
547 1 : ASSERT(c != NULL, "modify_labels: client connected");
548 :
549 1 : const char *add[] = { "STARRED" };
550 1 : const char *remove[] = { "UNREAD" };
551 1 : int rc = gmail_modify_labels(c, "msg001", add, 1, remove, 1);
552 1 : ASSERT(rc == 0, "modify_labels: returns 0");
553 :
554 1 : gmail_disconnect(c);
555 1 : wait_child(pid);
556 : }
557 :
558 1 : static void test_modify_labels_add_only(void) {
559 1 : int port = 0;
560 1 : pid_t pid = start_mock_server(&port, 1);
561 1 : if (pid < 0) { ASSERT(0, "modify_labels_add: could not start mock server"); return; }
562 :
563 1 : usleep(20000);
564 :
565 1 : GmailClient *c = make_test_client(port);
566 1 : ASSERT(c != NULL, "modify_labels_add: client connected");
567 :
568 1 : const char *add[] = { "INBOX" };
569 1 : int rc = gmail_modify_labels(c, "msg001", add, 1, NULL, 0);
570 1 : ASSERT(rc == 0, "modify_labels_add: returns 0");
571 :
572 1 : gmail_disconnect(c);
573 1 : wait_child(pid);
574 : }
575 :
576 1 : static void test_modify_labels_remove_only(void) {
577 1 : int port = 0;
578 1 : pid_t pid = start_mock_server(&port, 1);
579 1 : if (pid < 0) { ASSERT(0, "modify_labels_rm: could not start mock server"); return; }
580 :
581 1 : usleep(20000);
582 :
583 1 : GmailClient *c = make_test_client(port);
584 1 : ASSERT(c != NULL, "modify_labels_rm: client connected");
585 :
586 1 : const char *rm[] = { "UNREAD" };
587 1 : int rc = gmail_modify_labels(c, "msg001", NULL, 0, rm, 1);
588 1 : ASSERT(rc == 0, "modify_labels_rm: returns 0");
589 :
590 1 : gmail_disconnect(c);
591 1 : wait_child(pid);
592 : }
593 :
594 1 : static void test_trash(void) {
595 1 : int port = 0;
596 1 : pid_t pid = start_mock_server(&port, 1);
597 1 : if (pid < 0) { ASSERT(0, "trash: could not start mock server"); return; }
598 :
599 1 : usleep(20000);
600 :
601 1 : GmailClient *c = make_test_client(port);
602 1 : ASSERT(c != NULL, "trash: client connected");
603 :
604 1 : int rc = gmail_trash(c, "msg001");
605 1 : ASSERT(rc == 0, "trash: returns 0");
606 :
607 1 : gmail_disconnect(c);
608 1 : wait_child(pid);
609 : }
610 :
611 1 : static void test_untrash(void) {
612 1 : int port = 0;
613 1 : pid_t pid = start_mock_server(&port, 1);
614 1 : if (pid < 0) { ASSERT(0, "untrash: could not start mock server"); return; }
615 :
616 1 : usleep(20000);
617 :
618 1 : GmailClient *c = make_test_client(port);
619 1 : ASSERT(c != NULL, "untrash: client connected");
620 :
621 1 : int rc = gmail_untrash(c, "msg001");
622 1 : ASSERT(rc == 0, "untrash: returns 0");
623 :
624 1 : gmail_disconnect(c);
625 1 : wait_child(pid);
626 : }
627 :
628 1 : static void test_send(void) {
629 1 : int port = 0;
630 1 : pid_t pid = start_mock_server(&port, 1);
631 1 : if (pid < 0) { ASSERT(0, "send: could not start mock server"); return; }
632 :
633 1 : usleep(20000);
634 :
635 1 : GmailClient *c = make_test_client(port);
636 1 : ASSERT(c != NULL, "send: client connected");
637 :
638 1 : const char *raw_msg =
639 : "From: me@gmail.com\r\n"
640 : "To: you@example.com\r\n"
641 : "Subject: Test\r\n"
642 : "\r\n"
643 : "Hello!\r\n";
644 1 : int rc = gmail_send(c, raw_msg, strlen(raw_msg));
645 1 : ASSERT(rc == 0, "send: returns 0");
646 :
647 1 : gmail_disconnect(c);
648 1 : wait_child(pid);
649 : }
650 :
651 1 : static void test_get_history_id(void) {
652 1 : int port = 0;
653 1 : pid_t pid = start_mock_server(&port, 1);
654 1 : if (pid < 0) { ASSERT(0, "get_history_id: could not start mock server"); return; }
655 :
656 1 : usleep(20000);
657 :
658 1 : GmailClient *c = make_test_client(port);
659 1 : ASSERT(c != NULL, "get_history_id: client connected");
660 :
661 1 : char *hid = gmail_get_history_id(c);
662 1 : ASSERT(hid != NULL, "get_history_id: not NULL");
663 1 : ASSERT(strlen(hid) > 0, "get_history_id: non-empty");
664 1 : free(hid);
665 :
666 1 : gmail_disconnect(c);
667 1 : wait_child(pid);
668 : }
669 :
670 1 : static void test_get_history(void) {
671 1 : int port = 0;
672 1 : pid_t pid = start_mock_server(&port, 1);
673 1 : if (pid < 0) { ASSERT(0, "get_history: could not start mock server"); return; }
674 :
675 1 : usleep(20000);
676 :
677 1 : GmailClient *c = make_test_client(port);
678 1 : ASSERT(c != NULL, "get_history: client connected");
679 :
680 1 : char *resp = gmail_get_history(c, "12345");
681 1 : ASSERT(resp != NULL, "get_history: not NULL");
682 1 : free(resp);
683 :
684 1 : gmail_disconnect(c);
685 1 : wait_child(pid);
686 : }
687 :
688 1 : static void test_create_label(void) {
689 1 : int port = 0;
690 1 : pid_t pid = start_mock_server(&port, 1);
691 1 : if (pid < 0) { ASSERT(0, "create_label: could not start mock server"); return; }
692 :
693 1 : usleep(20000);
694 :
695 1 : GmailClient *c = make_test_client(port);
696 1 : ASSERT(c != NULL, "create_label: client connected");
697 :
698 1 : char *id_out = NULL;
699 1 : int rc = gmail_create_label(c, "MyNewLabel", &id_out);
700 1 : ASSERT(rc == 0, "create_label: returns 0");
701 1 : ASSERT(id_out != NULL, "create_label: id_out not NULL");
702 1 : free(id_out);
703 :
704 1 : gmail_disconnect(c);
705 1 : wait_child(pid);
706 : }
707 :
708 1 : static void test_create_label_no_id_out(void) {
709 1 : int port = 0;
710 1 : pid_t pid = start_mock_server(&port, 1);
711 1 : if (pid < 0) { ASSERT(0, "create_label_noid: could not start mock server"); return; }
712 :
713 1 : usleep(20000);
714 :
715 1 : GmailClient *c = make_test_client(port);
716 1 : ASSERT(c != NULL, "create_label_noid: client connected");
717 :
718 1 : int rc = gmail_create_label(c, "AnotherLabel", NULL);
719 1 : ASSERT(rc == 0, "create_label_noid: returns 0");
720 :
721 1 : gmail_disconnect(c);
722 1 : wait_child(pid);
723 : }
724 :
725 1 : static void test_delete_label(void) {
726 1 : int port = 0;
727 1 : pid_t pid = start_mock_server(&port, 1);
728 1 : if (pid < 0) { ASSERT(0, "delete_label: could not start mock server"); return; }
729 :
730 1 : usleep(20000);
731 :
732 1 : GmailClient *c = make_test_client(port);
733 1 : ASSERT(c != NULL, "delete_label: client connected");
734 :
735 1 : int rc = gmail_delete_label(c, "Label_Test001");
736 1 : ASSERT(rc == 0, "delete_label: returns 0");
737 :
738 1 : gmail_disconnect(c);
739 1 : wait_child(pid);
740 : }
741 :
742 1 : static void test_list_messages_with_history_id(void) {
743 1 : int port = 0;
744 1 : pid_t pid = start_mock_server(&port, 1);
745 1 : if (pid < 0) { ASSERT(0, "list_msg_histid: could not start mock server"); return; }
746 :
747 1 : usleep(20000);
748 :
749 1 : GmailClient *c = make_test_client(port);
750 1 : ASSERT(c != NULL, "list_msg_histid: client connected");
751 :
752 1 : char (*uids)[17] = NULL;
753 1 : int count = 0;
754 1 : char *history_id = NULL;
755 1 : int rc = gmail_list_messages(c, "INBOX", NULL, &uids, &count, &history_id);
756 1 : ASSERT(rc == 0, "list_msg_histid: returns 0");
757 1 : ASSERT(history_id != NULL, "list_msg_histid: history_id not NULL");
758 1 : free(uids);
759 1 : free(history_id);
760 :
761 1 : gmail_disconnect(c);
762 1 : wait_child(pid);
763 : }
764 :
765 : /* ── Error path: HTTP 404 for message fetch ───────────────────────── */
766 :
767 : /*
768 : * Mock server that returns 404 for message requests.
769 : */
770 8 : static void run_404_server(int listen_fd, int count) {
771 8 : struct sockaddr_in cli = {0};
772 8 : socklen_t cli_len = sizeof(cli);
773 16 : for (int i = 0; i < count; i++) {
774 8 : int cfd = accept(listen_fd, (struct sockaddr *)&cli, &cli_len);
775 8 : if (cfd < 0) break;
776 8 : struct timeval tv = {.tv_sec = 5, .tv_usec = 0};
777 8 : setsockopt(cfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
778 : char buf[2048];
779 8 : mock_read_request(cfd, buf, (int)sizeof(buf));
780 8 : mock_send_json(cfd, 404, "{\"error\":{\"code\":404}}");
781 8 : close(cfd);
782 : }
783 8 : close(listen_fd);
784 8 : GCOV_FLUSH();
785 0 : _exit(0);
786 : }
787 :
788 8 : static pid_t start_404_server(int *port_out, int count) {
789 8 : int listen_fd = make_mock_listener(port_out);
790 8 : if (listen_fd < 0) return -1;
791 8 : pid_t pid = fork();
792 16 : if (pid < 0) { close(listen_fd); return -1; }
793 16 : if (pid == 0) { run_404_server(listen_fd, count); }
794 8 : close(listen_fd);
795 8 : return pid;
796 : }
797 :
798 1 : static void test_fetch_message_404(void) {
799 1 : int port = 0;
800 1 : pid_t pid = start_404_server(&port, 1);
801 1 : if (pid < 0) { ASSERT(0, "fetch_404: could not start mock server"); return; }
802 :
803 1 : usleep(20000);
804 :
805 1 : GmailClient *c = make_test_client(port);
806 1 : ASSERT(c != NULL, "fetch_404: client connected");
807 :
808 1 : char *body = gmail_fetch_message(c, "nonexistent", NULL, NULL);
809 1 : ASSERT(body == NULL, "fetch_404: returns NULL on 404");
810 :
811 1 : gmail_disconnect(c);
812 1 : wait_child(pid);
813 : }
814 :
815 1 : static void test_list_labels_error(void) {
816 1 : int port = 0;
817 1 : pid_t pid = start_404_server(&port, 1);
818 1 : if (pid < 0) { ASSERT(0, "list_labels_err: could not start mock server"); return; }
819 :
820 1 : usleep(20000);
821 :
822 1 : GmailClient *c = make_test_client(port);
823 1 : ASSERT(c != NULL, "list_labels_err: client connected");
824 :
825 1 : char **names = NULL, **ids = NULL;
826 1 : int count = 0;
827 1 : int rc = gmail_list_labels(c, &names, &ids, &count);
828 1 : ASSERT(rc != 0, "list_labels_err: returns error on 404");
829 :
830 1 : gmail_disconnect(c);
831 1 : wait_child(pid);
832 : }
833 :
834 1 : static void test_create_label_error(void) {
835 1 : int port = 0;
836 1 : pid_t pid = start_404_server(&port, 1);
837 1 : if (pid < 0) { ASSERT(0, "create_label_err: could not start mock server"); return; }
838 :
839 1 : usleep(20000);
840 :
841 1 : GmailClient *c = make_test_client(port);
842 1 : ASSERT(c != NULL, "create_label_err: client connected");
843 :
844 1 : char *id = NULL;
845 1 : int rc = gmail_create_label(c, "Bad", &id);
846 1 : ASSERT(rc != 0, "create_label_err: returns error");
847 1 : ASSERT(id == NULL, "create_label_err: id is NULL on error");
848 :
849 1 : gmail_disconnect(c);
850 1 : wait_child(pid);
851 : }
852 :
853 1 : static void test_trash_error(void) {
854 1 : int port = 0;
855 1 : pid_t pid = start_404_server(&port, 1);
856 1 : if (pid < 0) { ASSERT(0, "trash_err: could not start mock server"); return; }
857 :
858 1 : usleep(20000);
859 :
860 1 : GmailClient *c = make_test_client(port);
861 1 : ASSERT(c != NULL, "trash_err: client connected");
862 :
863 1 : int rc = gmail_trash(c, "msg_gone");
864 1 : ASSERT(rc != 0, "trash_err: returns error on 404");
865 :
866 1 : gmail_disconnect(c);
867 1 : wait_child(pid);
868 : }
869 :
870 1 : static void test_modify_labels_error(void) {
871 1 : int port = 0;
872 1 : pid_t pid = start_404_server(&port, 1);
873 1 : if (pid < 0) { ASSERT(0, "modify_err: could not start mock server"); return; }
874 :
875 1 : usleep(20000);
876 :
877 1 : GmailClient *c = make_test_client(port);
878 1 : ASSERT(c != NULL, "modify_err: client connected");
879 :
880 1 : const char *add[] = { "INBOX" };
881 1 : int rc = gmail_modify_labels(c, "msg_gone", add, 1, NULL, 0);
882 1 : ASSERT(rc != 0, "modify_err: returns error on 404");
883 :
884 1 : gmail_disconnect(c);
885 1 : wait_child(pid);
886 : }
887 :
888 1 : static void test_get_history_id_error(void) {
889 1 : int port = 0;
890 1 : pid_t pid = start_404_server(&port, 1);
891 1 : if (pid < 0) { ASSERT(0, "histid_err: could not start mock server"); return; }
892 :
893 1 : usleep(20000);
894 :
895 1 : GmailClient *c = make_test_client(port);
896 1 : ASSERT(c != NULL, "histid_err: client connected");
897 :
898 1 : char *hid = gmail_get_history_id(c);
899 1 : ASSERT(hid == NULL, "histid_err: returns NULL on error");
900 :
901 1 : gmail_disconnect(c);
902 1 : wait_child(pid);
903 : }
904 :
905 : /* ── Mock server: returns HTTP 500 for list_messages (covers break) ─ */
906 :
907 1 : static void run_500_server(int listen_fd, int count) {
908 1 : struct sockaddr_in cli = {0};
909 1 : socklen_t cli_len = sizeof(cli);
910 2 : for (int i = 0; i < count; i++) {
911 1 : int cfd = accept(listen_fd, (struct sockaddr *)&cli, &cli_len);
912 1 : if (cfd < 0) break;
913 1 : struct timeval tv = {.tv_sec = 5, .tv_usec = 0};
914 1 : setsockopt(cfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
915 : char buf[2048];
916 1 : mock_read_request(cfd, buf, (int)sizeof(buf));
917 1 : mock_send_json(cfd, 500, "{\"error\":\"server error\"}");
918 1 : close(cfd);
919 : }
920 1 : close(listen_fd);
921 1 : GCOV_FLUSH();
922 0 : _exit(0);
923 : }
924 :
925 1 : static pid_t start_500_server(int *port_out, int count) {
926 1 : int listen_fd = make_mock_listener(port_out);
927 1 : if (listen_fd < 0) return -1;
928 1 : pid_t pid = fork();
929 2 : if (pid < 0) { close(listen_fd); return -1; }
930 2 : if (pid == 0) { run_500_server(listen_fd, count); }
931 1 : close(listen_fd);
932 1 : return pid;
933 : }
934 :
935 1 : static void test_list_messages_error(void) {
936 1 : int port = 0;
937 1 : pid_t pid = start_500_server(&port, 1);
938 1 : if (pid < 0) { ASSERT(0, "list_msg_err: could not start mock server"); return; }
939 :
940 1 : usleep(20000);
941 :
942 1 : GmailClient *c = make_test_client(port);
943 1 : ASSERT(c != NULL, "list_msg_err: client connected");
944 :
945 1 : char (*uids)[17] = NULL;
946 1 : int count = 0;
947 : /* 500 response — loop should break, count=0, uids=NULL → rc=0 */
948 1 : gmail_list_messages(c, "INBOX", NULL, &uids, &count, NULL);
949 1 : ASSERT(count == 0, "list_msg_err: count is 0 on error");
950 1 : free(uids);
951 :
952 1 : gmail_disconnect(c);
953 1 : wait_child(pid);
954 : }
955 :
956 : /* ── Mock server: message with no 'raw' field ─────────────────────── */
957 :
958 1 : static void run_noraw_server(int listen_fd, int count) {
959 1 : struct sockaddr_in cli = {0};
960 1 : socklen_t cli_len = sizeof(cli);
961 2 : for (int i = 0; i < count; i++) {
962 1 : int cfd = accept(listen_fd, (struct sockaddr *)&cli, &cli_len);
963 1 : if (cfd < 0) break;
964 1 : struct timeval tv = {.tv_sec = 5, .tv_usec = 0};
965 1 : setsockopt(cfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
966 : char buf[2048];
967 1 : mock_read_request(cfd, buf, (int)sizeof(buf));
968 : /* Message response without 'raw' field */
969 1 : mock_send_json(cfd, 200, "{\"id\":\"msg001\",\"threadId\":\"t001\"}");
970 1 : close(cfd);
971 : }
972 1 : close(listen_fd);
973 1 : GCOV_FLUSH();
974 0 : _exit(0);
975 : }
976 :
977 1 : static pid_t start_noraw_server(int *port_out, int count) {
978 1 : int listen_fd = make_mock_listener(port_out);
979 1 : if (listen_fd < 0) return -1;
980 1 : pid_t pid = fork();
981 2 : if (pid < 0) { close(listen_fd); return -1; }
982 2 : if (pid == 0) { run_noraw_server(listen_fd, count); }
983 1 : close(listen_fd);
984 1 : return pid;
985 : }
986 :
987 1 : static void test_fetch_message_no_raw_field(void) {
988 1 : int port = 0;
989 1 : pid_t pid = start_noraw_server(&port, 1);
990 1 : if (pid < 0) { ASSERT(0, "fetch_noraw: could not start mock server"); return; }
991 :
992 1 : usleep(20000);
993 :
994 1 : GmailClient *c = make_test_client(port);
995 1 : ASSERT(c != NULL, "fetch_noraw: client connected");
996 :
997 1 : char *body = gmail_fetch_message(c, "msg001", NULL, NULL);
998 1 : ASSERT(body == NULL, "fetch_noraw: returns NULL when no raw field");
999 :
1000 1 : gmail_disconnect(c);
1001 1 : wait_child(pid);
1002 : }
1003 :
1004 : /* ── Mock server: history 404 (expired) ──────────────────────────── */
1005 :
1006 1 : static void run_history_expired_server(int listen_fd, int count) {
1007 1 : struct sockaddr_in cli = {0};
1008 1 : socklen_t cli_len = sizeof(cli);
1009 2 : for (int i = 0; i < count; i++) {
1010 1 : int cfd = accept(listen_fd, (struct sockaddr *)&cli, &cli_len);
1011 1 : if (cfd < 0) break;
1012 1 : struct timeval tv = {.tv_sec = 5, .tv_usec = 0};
1013 1 : setsockopt(cfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
1014 : char buf[2048];
1015 1 : mock_read_request(cfd, buf, (int)sizeof(buf));
1016 1 : mock_send_json(cfd, 404, "{\"error\":{\"code\":404,\"message\":\"historyId expired\"}}");
1017 1 : close(cfd);
1018 : }
1019 1 : close(listen_fd);
1020 1 : GCOV_FLUSH();
1021 0 : _exit(0);
1022 : }
1023 :
1024 1 : static pid_t start_history_expired_server(int *port_out, int count) {
1025 1 : int listen_fd = make_mock_listener(port_out);
1026 1 : if (listen_fd < 0) return -1;
1027 1 : pid_t pid = fork();
1028 2 : if (pid < 0) { close(listen_fd); return -1; }
1029 2 : if (pid == 0) { run_history_expired_server(listen_fd, count); }
1030 1 : close(listen_fd);
1031 1 : return pid;
1032 : }
1033 :
1034 1 : static void test_get_history_expired(void) {
1035 1 : int port = 0;
1036 1 : pid_t pid = start_history_expired_server(&port, 1);
1037 1 : if (pid < 0) { ASSERT(0, "history_expired: could not start mock server"); return; }
1038 :
1039 1 : usleep(20000);
1040 :
1041 1 : GmailClient *c = make_test_client(port);
1042 1 : ASSERT(c != NULL, "history_expired: client connected");
1043 :
1044 1 : char *resp = gmail_get_history(c, "99999");
1045 1 : ASSERT(resp == NULL, "history_expired: returns NULL on 404");
1046 :
1047 1 : gmail_disconnect(c);
1048 1 : wait_child(pid);
1049 : }
1050 :
1051 : /* ── Mock server: history non-200/404 response ────────────────────── */
1052 :
1053 1 : static void run_history_503_server(int listen_fd, int count) {
1054 1 : struct sockaddr_in cli = {0};
1055 1 : socklen_t cli_len = sizeof(cli);
1056 2 : for (int i = 0; i < count; i++) {
1057 1 : int cfd = accept(listen_fd, (struct sockaddr *)&cli, &cli_len);
1058 1 : if (cfd < 0) break;
1059 1 : struct timeval tv = {.tv_sec = 5, .tv_usec = 0};
1060 1 : setsockopt(cfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
1061 : char buf[2048];
1062 1 : mock_read_request(cfd, buf, (int)sizeof(buf));
1063 1 : const char *resp =
1064 : "HTTP/1.1 503 Service Unavailable\r\n"
1065 : "Content-Type: application/json\r\n"
1066 : "Content-Length: 2\r\n"
1067 : "Connection: close\r\n"
1068 : "\r\n"
1069 : "{}";
1070 1 : ssize_t r = write(cfd, resp, strlen(resp)); (void)r;
1071 1 : close(cfd);
1072 : }
1073 1 : close(listen_fd);
1074 1 : GCOV_FLUSH();
1075 0 : _exit(0);
1076 : }
1077 :
1078 1 : static pid_t start_history_503_server(int *port_out, int count) {
1079 1 : int listen_fd = make_mock_listener(port_out);
1080 1 : if (listen_fd < 0) return -1;
1081 1 : pid_t pid = fork();
1082 2 : if (pid < 0) { close(listen_fd); return -1; }
1083 2 : if (pid == 0) { run_history_503_server(listen_fd, count); }
1084 1 : close(listen_fd);
1085 1 : return pid;
1086 : }
1087 :
1088 1 : static void test_get_history_503(void) {
1089 1 : int port = 0;
1090 1 : pid_t pid = start_history_503_server(&port, 1);
1091 1 : if (pid < 0) { ASSERT(0, "history_503: could not start mock server"); return; }
1092 :
1093 1 : usleep(20000);
1094 :
1095 1 : GmailClient *c = make_test_client(port);
1096 1 : ASSERT(c != NULL, "history_503: client connected");
1097 :
1098 1 : char *resp = gmail_get_history(c, "12345");
1099 1 : ASSERT(resp == NULL, "history_503: returns NULL on 503");
1100 :
1101 1 : gmail_disconnect(c);
1102 1 : wait_child(pid);
1103 : }
1104 :
1105 : /* ── Mock server: untrash error ───────────────────────────────────── */
1106 :
1107 1 : static void test_untrash_error(void) {
1108 1 : int port = 0;
1109 1 : pid_t pid = start_404_server(&port, 1);
1110 1 : if (pid < 0) { ASSERT(0, "untrash_err: could not start mock server"); return; }
1111 :
1112 1 : usleep(20000);
1113 :
1114 1 : GmailClient *c = make_test_client(port);
1115 1 : ASSERT(c != NULL, "untrash_err: client connected");
1116 :
1117 1 : int rc = gmail_untrash(c, "msg_gone");
1118 1 : ASSERT(rc != 0, "untrash_err: returns error on 404");
1119 :
1120 1 : gmail_disconnect(c);
1121 1 : wait_child(pid);
1122 : }
1123 :
1124 : /* ── Mock server: send error ──────────────────────────────────────── */
1125 :
1126 1 : static void test_send_error(void) {
1127 1 : int port = 0;
1128 1 : pid_t pid = start_404_server(&port, 1);
1129 1 : if (pid < 0) { ASSERT(0, "send_err: could not start mock server"); return; }
1130 :
1131 1 : usleep(20000);
1132 :
1133 1 : GmailClient *c = make_test_client(port);
1134 1 : ASSERT(c != NULL, "send_err: client connected");
1135 :
1136 1 : const char *msg = "From: a@b.com\r\n\r\nHi\r\n";
1137 1 : int rc = gmail_send(c, msg, strlen(msg));
1138 1 : ASSERT(rc != 0, "send_err: returns error on 404");
1139 :
1140 1 : gmail_disconnect(c);
1141 1 : wait_child(pid);
1142 : }
1143 :
1144 : /* ── Mock server: delete_label error ─────────────────────────────── */
1145 :
1146 1 : static void run_delete_500_server(int listen_fd, int count) {
1147 1 : struct sockaddr_in cli = {0};
1148 1 : socklen_t cli_len = sizeof(cli);
1149 2 : for (int i = 0; i < count; i++) {
1150 1 : int cfd = accept(listen_fd, (struct sockaddr *)&cli, &cli_len);
1151 1 : if (cfd < 0) break;
1152 1 : struct timeval tv = {.tv_sec = 5, .tv_usec = 0};
1153 1 : setsockopt(cfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
1154 : char buf[2048];
1155 1 : mock_read_request(cfd, buf, (int)sizeof(buf));
1156 1 : mock_send_json(cfd, 500, "{\"error\":\"internal\"}");
1157 1 : close(cfd);
1158 : }
1159 1 : close(listen_fd);
1160 1 : GCOV_FLUSH();
1161 0 : _exit(0);
1162 : }
1163 :
1164 1 : static pid_t start_delete_500_server(int *port_out, int count) {
1165 1 : int listen_fd = make_mock_listener(port_out);
1166 1 : if (listen_fd < 0) return -1;
1167 1 : pid_t pid = fork();
1168 2 : if (pid < 0) { close(listen_fd); return -1; }
1169 2 : if (pid == 0) { run_delete_500_server(listen_fd, count); }
1170 1 : close(listen_fd);
1171 1 : return pid;
1172 : }
1173 :
1174 1 : static void test_delete_label_error(void) {
1175 1 : int port = 0;
1176 1 : pid_t pid = start_delete_500_server(&port, 1);
1177 1 : if (pid < 0) { ASSERT(0, "delete_label_err: could not start mock server"); return; }
1178 :
1179 1 : usleep(20000);
1180 :
1181 1 : GmailClient *c = make_test_client(port);
1182 1 : ASSERT(c != NULL, "delete_label_err: client connected");
1183 :
1184 1 : int rc = gmail_delete_label(c, "Label_Test001");
1185 1 : ASSERT(rc != 0, "delete_label_err: returns error on 500");
1186 :
1187 1 : gmail_disconnect(c);
1188 1 : wait_child(pid);
1189 : }
1190 :
1191 : /* ── Registration ─────────────────────────────────────────────────── */
1192 :
1193 1 : void test_gmail_client(void) {
1194 1 : RUN_TEST(test_connect_not_gmail);
1195 1 : RUN_TEST(test_connect_no_token);
1196 1 : RUN_TEST(test_disconnect_null);
1197 1 : RUN_TEST(test_b64_roundtrip);
1198 1 : RUN_TEST(test_b64_empty);
1199 1 : RUN_TEST(test_b64_known_vector);
1200 1 : RUN_TEST(test_b64_url_chars);
1201 1 : RUN_TEST(test_b64_decode_null_len_out);
1202 1 : RUN_TEST(test_b64_large_roundtrip);
1203 1 : RUN_TEST(test_connect_with_test_token);
1204 1 : RUN_TEST(test_set_progress);
1205 1 : RUN_TEST(test_list_labels);
1206 1 : RUN_TEST(test_list_messages);
1207 1 : RUN_TEST(test_list_messages_with_query);
1208 1 : RUN_TEST(test_list_messages_with_history_id);
1209 1 : RUN_TEST(test_fetch_message);
1210 1 : RUN_TEST(test_fetch_message_no_labels);
1211 1 : RUN_TEST(test_fetch_message_404);
1212 1 : RUN_TEST(test_fetch_message_no_raw_field);
1213 1 : RUN_TEST(test_modify_labels);
1214 1 : RUN_TEST(test_modify_labels_add_only);
1215 1 : RUN_TEST(test_modify_labels_remove_only);
1216 1 : RUN_TEST(test_trash);
1217 1 : RUN_TEST(test_untrash);
1218 1 : RUN_TEST(test_send);
1219 1 : RUN_TEST(test_get_history_id);
1220 1 : RUN_TEST(test_get_history);
1221 1 : RUN_TEST(test_get_history_expired);
1222 1 : RUN_TEST(test_get_history_503);
1223 1 : RUN_TEST(test_create_label);
1224 1 : RUN_TEST(test_create_label_no_id_out);
1225 1 : RUN_TEST(test_delete_label);
1226 1 : RUN_TEST(test_list_labels_error);
1227 1 : RUN_TEST(test_list_messages_error);
1228 1 : RUN_TEST(test_create_label_error);
1229 1 : RUN_TEST(test_trash_error);
1230 1 : RUN_TEST(test_untrash_error);
1231 1 : RUN_TEST(test_send_error);
1232 1 : RUN_TEST(test_modify_labels_error);
1233 1 : RUN_TEST(test_get_history_id_error);
1234 1 : RUN_TEST(test_delete_label_error);
1235 1 : }
|