LCOV - code coverage report
Current view: top level - tests/unit - test_gmail_client.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 98.2 % 779 765
Test Date: 2026-05-07 15:53:07 Functions: 100.0 % 63 63

            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 : }
        

Generated by: LCOV version 2.0-1