LCOV - code coverage report
Current view: top level - tests/unit - test_gmail_auth.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 71.7 % 360 258
Test Date: 2026-05-07 15:53:07 Functions: 94.4 % 18 17

            Line data    Source code
       1              : #include "test_helpers.h"
       2              : #include "gmail_auth.h"
       3              : #include "config.h"
       4              : #include <fcntl.h>
       5              : #include <signal.h>
       6              : #include <stdlib.h>
       7              : #include <string.h>
       8              : #include <time.h>
       9              : #include <unistd.h>
      10              : #include <sys/socket.h>
      11              : #include <sys/time.h>
      12              : #include <sys/wait.h>
      13              : #include <netinet/in.h>
      14              : #include <arpa/inet.h>
      15              : 
      16              : /* Wait for child with 5-second timeout, then SIGKILL. */
      17            9 : static void wait_child(pid_t pid) {
      18           18 :     if (pid <= 0) return;
      19           16 :     for (int i = 0; i < 50; i++) {
      20              :         int st;
      21           16 :         if (waitpid(pid, &st, WNOHANG) != 0) return;
      22            7 :         struct timespec ts = {0, 100000000L};
      23            7 :         nanosleep(&ts, NULL);
      24              :     }
      25            0 :     kill(pid, SIGKILL);
      26            0 :     int st; waitpid(pid, &st, 0);
      27              : }
      28              : 
      29              : /* ── gmail_auth_refresh — error paths (no network needed) ─────────── */
      30              : 
      31            1 : static void test_refresh_no_token(void) {
      32            1 :     Config cfg = {0};
      33              :     /* No refresh_token → should return NULL */
      34            1 :     char *tok = gmail_auth_refresh(&cfg);
      35            1 :     ASSERT(tok == NULL, "refresh: returns NULL with no refresh_token");
      36              : }
      37              : 
      38            1 : static void test_refresh_empty_token(void) {
      39            1 :     Config cfg = {0};
      40            1 :     cfg.gmail_refresh_token = strdup("");
      41            1 :     char *tok = gmail_auth_refresh(&cfg);
      42            1 :     ASSERT(tok == NULL, "refresh: returns NULL with empty refresh_token");
      43            1 :     free(cfg.gmail_refresh_token);
      44              : }
      45              : 
      46              : /* ── Auth code extraction from browser redirect ──────────────────────── */
      47              : 
      48            1 : static void test_auth_code_extraction(void) {
      49              :     /* Simulate the localhost listener + browser redirect that
      50              :      * gmail_auth_device_flow uses internally. */
      51            1 :     int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
      52            1 :     ASSERT(listen_fd >= 0, "auth_code: socket created");
      53            1 :     int opt = 1;
      54            1 :     setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
      55            1 :     struct timeval tv = {5, 0};
      56            1 :     setsockopt(listen_fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
      57              : 
      58            1 :     struct sockaddr_in addr = {0};
      59            1 :     addr.sin_family = AF_INET;
      60            1 :     addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
      61            1 :     addr.sin_port = htons(18765);
      62            1 :     int bound = bind(listen_fd, (struct sockaddr *)&addr, sizeof(addr));
      63            1 :     ASSERT(bound == 0, "auth_code: bind succeeded");
      64            1 :     listen(listen_fd, 1);
      65              : 
      66              :     /* Fork child to simulate browser redirect */
      67            1 :     pid_t pid = fork();
      68            1 :     if (pid == 0) {
      69            0 :         usleep(50000);
      70            0 :         int fd = socket(AF_INET, SOCK_STREAM, 0);
      71            0 :         struct sockaddr_in ca = {0};
      72            0 :         ca.sin_family = AF_INET;
      73            0 :         ca.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
      74            0 :         ca.sin_port = htons(18765);
      75            0 :         connect(fd, (struct sockaddr *)&ca, sizeof(ca));
      76            0 :         const char *req =
      77              :             "GET /callback?code=test_code_abc&scope=x HTTP/1.1\r\n"
      78              :             "Host: localhost\r\n\r\n";
      79            0 :         ssize_t w = write(fd, req, strlen(req)); (void)w;
      80              :         char buf[256];
      81            0 :         ssize_t r = read(fd, buf, sizeof(buf)); (void)r;
      82            0 :         close(fd);
      83            0 :         _exit(0);
      84              :     }
      85              : 
      86              :     /* Parent: accept connection and extract code */
      87              :     struct sockaddr_in cli;
      88            1 :     socklen_t cli_len = sizeof(cli);
      89            1 :     int conn = accept(listen_fd, (struct sockaddr *)&cli, &cli_len);
      90            1 :     ASSERT(conn >= 0, "auth_code: accept succeeded");
      91              : 
      92            1 :     char req[4096] = {0};
      93            1 :     ssize_t n = read(conn, req, sizeof(req) - 1);
      94            1 :     ASSERT(n > 0, "auth_code: read request");
      95              : 
      96              :     /* Extract code= parameter */
      97            1 :     char *cs = strstr(req, "code=");
      98            1 :     ASSERT(cs != NULL, "auth_code: found code= in request");
      99            1 :     cs += 5;
     100            1 :     char *ce = cs;
     101           14 :     while (*ce && *ce != '&' && *ce != ' ') ce++;
     102            1 :     char *code = strndup(cs, (size_t)(ce - cs));
     103            1 :     ASSERT(strcmp(code, "test_code_abc") == 0,
     104              :            "auth_code: extracted correct code");
     105            1 :     free(code);
     106              : 
     107            1 :     const char *html = "HTTP/1.1 200 OK\r\n\r\nOK";
     108            1 :     ssize_t wr = write(conn, html, strlen(html)); (void)wr;
     109            1 :     close(conn);
     110            1 :     close(listen_fd);
     111            1 :     wait_child(pid);
     112              : }
     113              : 
     114            1 : static void test_auth_code_denied(void) {
     115            1 :     int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
     116            1 :     ASSERT(listen_fd >= 0, "denied: socket created");
     117            1 :     int opt = 1;
     118            1 :     setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
     119            1 :     struct timeval tv = {5, 0};
     120            1 :     setsockopt(listen_fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
     121              : 
     122            1 :     struct sockaddr_in addr = {0};
     123            1 :     addr.sin_family = AF_INET;
     124            1 :     addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
     125            1 :     addr.sin_port = htons(18766);
     126            1 :     bind(listen_fd, (struct sockaddr *)&addr, sizeof(addr));
     127            1 :     listen(listen_fd, 1);
     128              : 
     129            1 :     pid_t pid = fork();
     130            1 :     if (pid == 0) {
     131            0 :         usleep(50000);
     132            0 :         int fd = socket(AF_INET, SOCK_STREAM, 0);
     133            0 :         struct sockaddr_in ca = {0};
     134            0 :         ca.sin_family = AF_INET;
     135            0 :         ca.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
     136            0 :         ca.sin_port = htons(18766);
     137            0 :         connect(fd, (struct sockaddr *)&ca, sizeof(ca));
     138            0 :         const char *req =
     139              :             "GET /callback?error=access_denied HTTP/1.1\r\n"
     140              :             "Host: localhost\r\n\r\n";
     141            0 :         ssize_t w = write(fd, req, strlen(req)); (void)w;
     142              :         char buf[256];
     143            0 :         ssize_t r = read(fd, buf, sizeof(buf)); (void)r;
     144            0 :         close(fd);
     145            0 :         _exit(0);
     146              :     }
     147              : 
     148              :     struct sockaddr_in cli;
     149            1 :     socklen_t cli_len = sizeof(cli);
     150            1 :     int conn = accept(listen_fd, (struct sockaddr *)&cli, &cli_len);
     151            1 :     ASSERT(conn >= 0, "denied: accept succeeded");
     152              : 
     153            1 :     char req[4096] = {0};
     154            1 :     ssize_t n2 = read(conn, req, sizeof(req) - 1); (void)n2;
     155              : 
     156            1 :     ASSERT(strstr(req, "code=") == NULL,
     157              :            "denied: no code in request");
     158            1 :     ASSERT(strstr(req, "error=access_denied") != NULL,
     159              :            "denied: access_denied present");
     160              : 
     161            1 :     const char *html = "HTTP/1.1 200 OK\r\n\r\nDenied";
     162            1 :     ssize_t wr = write(conn, html, strlen(html)); (void)wr;
     163            1 :     close(conn);
     164            1 :     close(listen_fd);
     165            1 :     wait_child(pid);
     166              : }
     167              : 
     168              : /* ── Gmail IMAP rejection in wizard ──────────────────────────────────── */
     169              : 
     170            1 : static void test_wizard_rejects_gmail_imap(void) {
     171              :     /* The wizard should reject gmail.com as IMAP host */
     172              :     /* This is tested via test_wizard.c but we verify the concept here:
     173              :      * a Config with host containing gmail.com and gmail_mode=0 is invalid */
     174            1 :     Config cfg = {0};
     175            1 :     cfg.host = strdup("imaps://imap.gmail.com");
     176            1 :     cfg.gmail_mode = 0;
     177            1 :     ASSERT(strstr(cfg.host, "gmail.com") != NULL,
     178              :            "gmail_imap: gmail.com detected in host");
     179            1 :     ASSERT(cfg.gmail_mode == 0,
     180              :            "gmail_imap: incorrectly configured as IMAP");
     181            1 :     free(cfg.host);
     182              : }
     183              : 
     184              : /* ── Additional coverage tests ──────────────────────────────────────── */
     185              : 
     186            1 : static void test_refresh_with_client_credentials(void) {
     187              :     /* Covers get_client_id (non-NULL cfg field), get_client_secret, the post
     188              :      * building, and http_post call in gmail_auth_refresh.
     189              :      * http_post will fail (no valid OAuth credentials) → tok is NULL. */
     190            1 :     Config cfg = {0};
     191            1 :     cfg.gmail_refresh_token = strdup("dummy_refresh_token");
     192            1 :     cfg.gmail_client_id     = strdup("test-client-id.apps.googleusercontent.com");
     193            1 :     cfg.gmail_client_secret = strdup("test-client-secret");
     194            1 :     unsetenv("GMAIL_TEST_TOKEN");
     195              : 
     196            1 :     char *tok = gmail_auth_refresh(&cfg);
     197              :     /* Returns NULL (network failure or 400 from Google) — just no crash */
     198            1 :     free(tok);
     199              : 
     200            1 :     free(cfg.gmail_refresh_token);
     201            1 :     free(cfg.gmail_client_id);
     202            1 :     free(cfg.gmail_client_secret);
     203            1 : }
     204              : 
     205            1 : static void test_device_flow_no_credentials(void) {
     206              :     /* Empty client_id → returns -1 immediately, covering lines 184-203. */
     207            1 :     Config cfg = {0};
     208            1 :     int saved = dup(2);
     209            1 :     int dn = open("/dev/null", O_WRONLY);
     210            1 :     if (dn >= 0) dup2(dn, 2);
     211            1 :     int rc = gmail_auth_device_flow(&cfg);
     212            1 :     if (dn >= 0) { dup2(saved, 2); close(dn); }
     213            1 :     close(saved);
     214            1 :     ASSERT(rc == -1, "device_flow: empty client_id returns -1");
     215              : }
     216              : 
     217            1 : static void test_device_flow_access_denied(void) {
     218              :     /* Child sends error=access_denied → wait_for_auth_code returns NULL
     219              :      * (covers open_listener, wait_for_auth_code error path, device_flow setup).
     220              :      * No network call needed since auth_code is NULL → function returns -1. */
     221            1 :     Config cfg = {0};
     222            1 :     cfg.gmail_client_id     = strdup("test-client-id.apps.googleusercontent.com");
     223            1 :     cfg.gmail_client_secret = strdup("test-client-secret");
     224              : 
     225            1 :     pid_t pid = fork();
     226            1 :     if (pid == 0) {
     227            0 :         usleep(200000);
     228            0 :         for (int port = 8089; port <= 8099; port++) {
     229            0 :             int fd = socket(AF_INET, SOCK_STREAM, 0);
     230            0 :             if (fd < 0) continue;
     231            0 :             struct sockaddr_in ca = {0};
     232            0 :             ca.sin_family      = AF_INET;
     233            0 :             ca.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
     234            0 :             ca.sin_port        = htons((uint16_t)port);
     235            0 :             if (connect(fd, (struct sockaddr *)&ca, sizeof(ca)) == 0) {
     236            0 :                 const char *req =
     237              :                     "GET /callback?error=access_denied HTTP/1.1\r\n"
     238              :                     "Host: localhost\r\n\r\n";
     239            0 :                 ssize_t w = write(fd, req, strlen(req)); (void)w;
     240              :                 char buf[512];
     241            0 :                 ssize_t r = read(fd, buf, sizeof(buf)); (void)r;
     242            0 :                 close(fd);
     243            0 :                 _exit(0);
     244              :             }
     245            0 :             close(fd);
     246              :         }
     247            0 :         _exit(1);
     248              :     }
     249              : 
     250            1 :     int saved = dup(2);
     251            1 :     int dn = open("/dev/null", O_WRONLY);
     252            1 :     if (dn >= 0) dup2(dn, 2);
     253            1 :     int rc = gmail_auth_device_flow(&cfg);
     254            1 :     if (dn >= 0) { dup2(saved, 2); close(dn); }
     255            1 :     close(saved);
     256              : 
     257            1 :     ASSERT(rc == -1, "device_flow: access_denied returns -1");
     258              : 
     259            1 :     wait_child(pid);
     260              : 
     261            1 :     free(cfg.gmail_client_id);
     262            1 :     free(cfg.gmail_client_secret);
     263              : }
     264              : 
     265            1 : static void test_refresh_test_token_hook(void) {
     266              :     /* Covers the GMAIL_TEST_TOKEN early-return path in gmail_auth_refresh */
     267            1 :     setenv("GMAIL_TEST_TOKEN", "test_access_token_xyz", 1);
     268            1 :     Config cfg = {0};
     269            1 :     cfg.gmail_refresh_token = strdup("some_refresh_token");
     270            1 :     char *tok = gmail_auth_refresh(&cfg);
     271            1 :     ASSERT(tok != NULL, "refresh: GMAIL_TEST_TOKEN path returns non-NULL");
     272            1 :     if (tok)
     273            1 :         ASSERT(strcmp(tok, "test_access_token_xyz") == 0, "refresh: test token value");
     274            1 :     free(tok);
     275            1 :     free(cfg.gmail_refresh_token);
     276            1 :     unsetenv("GMAIL_TEST_TOKEN");
     277              : }
     278              : 
     279              : /* Minimal HTTP server child that sends a fixed response */
     280            0 : static void run_mock_token_server(int port, const char *response) {
     281            0 :     int srv = socket(AF_INET, SOCK_STREAM, 0);
     282            0 :     if (srv < 0) _exit(1);
     283            0 :     int opt = 1;
     284            0 :     setsockopt(srv, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
     285            0 :     struct timeval tv = {5, 0};
     286            0 :     setsockopt(srv, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
     287            0 :     struct sockaddr_in addr = {0};
     288            0 :     addr.sin_family      = AF_INET;
     289            0 :     addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
     290            0 :     addr.sin_port        = htons((uint16_t)port);
     291            0 :     if (bind(srv, (struct sockaddr *)&addr, sizeof(addr)) != 0 ||
     292            0 :         listen(srv, 1) != 0) {
     293            0 :         close(srv);
     294            0 :         _exit(1);
     295              :     }
     296            0 :     struct sockaddr_in cli = {0};
     297            0 :     socklen_t cli_len = sizeof(cli);
     298            0 :     int conn = accept(srv, (struct sockaddr *)&cli, &cli_len);
     299            0 :     if (conn < 0) { close(srv); _exit(1); }
     300              :     char buf[4096];
     301            0 :     ssize_t n = read(conn, buf, sizeof(buf) - 1); (void)n;
     302            0 :     ssize_t w = write(conn, response, strlen(response)); (void)w;
     303            0 :     close(conn);
     304            0 :     close(srv);
     305            0 :     _exit(0);
     306              : }
     307              : 
     308            1 : static void test_refresh_via_mock_server_200(void) {
     309              :     /* Covers lines 336-339: code==200 path in gmail_auth_refresh */
     310            1 :     const int mock_port = 18770;
     311            1 :     const char *http_resp =
     312              :         "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\n"
     313              :         "{\"access_token\":\"mock_access_tok\",\"token_type\":\"Bearer\"}";
     314              : 
     315            1 :     pid_t pid = fork();
     316            1 :     if (pid == 0) {
     317            0 :         run_mock_token_server(mock_port, http_resp);
     318              :     }
     319            1 :     usleep(50000); /* let server bind */
     320              : 
     321              :     char url[64];
     322            1 :     snprintf(url, sizeof(url), "http://127.0.0.1:%d/token", mock_port);
     323            1 :     setenv("GMAIL_TEST_TOKEN_URL", url, 1);
     324            1 :     unsetenv("GMAIL_TEST_TOKEN");
     325              : 
     326            1 :     Config cfg = {0};
     327            1 :     cfg.gmail_refresh_token = strdup("valid_looking_refresh_token");
     328            1 :     char *tok = gmail_auth_refresh(&cfg);
     329            1 :     ASSERT(tok != NULL, "mock 200: access_token returned");
     330            1 :     if (tok)
     331            1 :         ASSERT(strcmp(tok, "mock_access_tok") == 0, "mock 200: access_token value");
     332            1 :     free(tok);
     333            1 :     free(cfg.gmail_refresh_token);
     334              : 
     335            1 :     unsetenv("GMAIL_TEST_TOKEN_URL");
     336            1 :     wait_child(pid);
     337              : }
     338              : 
     339            1 : static void test_refresh_via_mock_server_200_no_token(void) {
     340              :     /* Covers lines 290-293: code==200 but no access_token in response */
     341            1 :     const int mock_port = 18771;
     342            1 :     const char *http_resp =
     343              :         "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\n"
     344              :         "{\"token_type\":\"Bearer\"}"; /* no access_token field */
     345              : 
     346            1 :     pid_t pid = fork();
     347            1 :     if (pid == 0) {
     348            0 :         run_mock_token_server(mock_port, http_resp);
     349              :     }
     350            1 :     usleep(50000);
     351              : 
     352              :     char url[64];
     353            1 :     snprintf(url, sizeof(url), "http://127.0.0.1:%d/token", mock_port);
     354            1 :     setenv("GMAIL_TEST_TOKEN_URL", url, 1);
     355              : 
     356            1 :     Config cfg = {0};
     357            1 :     cfg.gmail_refresh_token = strdup("valid_looking_refresh_token");
     358            1 :     char *tok = gmail_auth_refresh(&cfg);
     359            1 :     ASSERT(tok == NULL, "mock 200 no token: returns NULL");
     360            1 :     free(cfg.gmail_refresh_token);
     361              : 
     362            1 :     unsetenv("GMAIL_TEST_TOKEN_URL");
     363            1 :     wait_child(pid);
     364              : }
     365              : 
     366            1 : static void test_refresh_via_mock_server_400_unknown_error(void) {
     367              :     /* Covers lines 351-352: non-200 with error that is neither invalid_grant
     368              :      * nor invalid_client */
     369            1 :     const int mock_port = 18772;
     370            1 :     const char *http_resp =
     371              :         "HTTP/1.1 400 Bad Request\r\nContent-Type: application/json\r\n\r\n"
     372              :         "{\"error\":\"unsupported_grant_type\"}";
     373              : 
     374            1 :     pid_t pid = fork();
     375            1 :     if (pid == 0) {
     376            0 :         run_mock_token_server(mock_port, http_resp);
     377              :     }
     378            1 :     usleep(50000);
     379              : 
     380              :     char url[64];
     381            1 :     snprintf(url, sizeof(url), "http://127.0.0.1:%d/token", mock_port);
     382            1 :     setenv("GMAIL_TEST_TOKEN_URL", url, 1);
     383              : 
     384            1 :     Config cfg = {0};
     385            1 :     cfg.gmail_refresh_token = strdup("some_refresh_token");
     386            1 :     int saved = dup(2);
     387            1 :     int dn = open("/dev/null", O_WRONLY);
     388            1 :     if (dn >= 0) dup2(dn, 2);
     389            1 :     char *tok = gmail_auth_refresh(&cfg);
     390            1 :     if (dn >= 0) { dup2(saved, 2); close(dn); }
     391            1 :     close(saved);
     392            1 :     ASSERT(tok == NULL, "mock 400 unknown error: returns NULL");
     393            1 :     free(cfg.gmail_refresh_token);
     394              : 
     395            1 :     unsetenv("GMAIL_TEST_TOKEN_URL");
     396            1 :     wait_child(pid);
     397              : }
     398              : 
     399            1 : static void test_refresh_via_mock_server_curl_error(void) {
     400              :     /* Covers lines 76-80: curl error path (port not listening → connect failure) */
     401              :     /* Use a port that is definitely not listening */
     402            1 :     setenv("GMAIL_TEST_TOKEN_URL", "http://127.0.0.1:19999/token", 1);
     403            1 :     unsetenv("GMAIL_TEST_TOKEN");
     404              : 
     405            1 :     Config cfg = {0};
     406            1 :     cfg.gmail_refresh_token = strdup("some_token");
     407            1 :     char *tok = gmail_auth_refresh(&cfg);
     408            1 :     ASSERT(tok == NULL, "mock curl error: returns NULL");
     409            1 :     free(cfg.gmail_refresh_token);
     410              : 
     411            1 :     unsetenv("GMAIL_TEST_TOKEN_URL");
     412              : }
     413              : 
     414            1 : static void test_device_flow_full_mock(void) {
     415              :     /* Covers lines 287-305: successful token exchange in gmail_auth_device_flow.
     416              :      * Child 1 simulates the browser redirect (code=xxx).
     417              :      * Child 2 is the mock token server that returns a valid access_token.
     418              :      * The function completes successfully → returns 0. */
     419            1 :     const int mock_port = 18773;
     420            1 :     const char *token_resp =
     421              :         "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\n"
     422              :         "{\"access_token\":\"dev_flow_tok\",\"refresh_token\":\"dev_flow_refresh\","
     423              :         "\"token_type\":\"Bearer\"}";
     424              : 
     425              :     /* Start mock token server */
     426            1 :     pid_t srv_pid = fork();
     427            1 :     if (srv_pid == 0) {
     428            0 :         run_mock_token_server(mock_port, token_resp);
     429              :     }
     430            1 :     usleep(60000); /* let server bind */
     431              : 
     432              :     char url[64];
     433            1 :     snprintf(url, sizeof(url), "http://127.0.0.1:%d/token", mock_port);
     434            1 :     setenv("GMAIL_TEST_TOKEN_URL", url, 1);
     435              : 
     436            1 :     Config cfg = {0};
     437            1 :     cfg.gmail_client_id     = strdup("test-client-id.apps.googleusercontent.com");
     438            1 :     cfg.gmail_client_secret = strdup("test-client-secret");
     439              : 
     440              :     /* Browser redirect child */
     441            1 :     pid_t br_pid = fork();
     442            1 :     if (br_pid == 0) {
     443            0 :         usleep(250000); /* wait for listener to open */
     444            0 :         for (int port = 8089; port <= 8099; port++) {
     445            0 :             int fd = socket(AF_INET, SOCK_STREAM, 0);
     446            0 :             if (fd < 0) continue;
     447            0 :             struct sockaddr_in ca = {0};
     448            0 :             ca.sin_family      = AF_INET;
     449            0 :             ca.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
     450            0 :             ca.sin_port        = htons((uint16_t)port);
     451            0 :             if (connect(fd, (struct sockaddr *)&ca, sizeof(ca)) == 0) {
     452            0 :                 const char *req =
     453              :                     "GET /callback?code=full_test_code&scope=x HTTP/1.1\r\n"
     454              :                     "Host: localhost\r\n\r\n";
     455            0 :                 ssize_t w = write(fd, req, strlen(req)); (void)w;
     456              :                 char buf[512];
     457            0 :                 ssize_t r = read(fd, buf, sizeof(buf)); (void)r;
     458            0 :                 close(fd);
     459            0 :                 _exit(0);
     460              :             }
     461            0 :             close(fd);
     462              :         }
     463            0 :         _exit(1);
     464              :     }
     465              : 
     466            1 :     int saved = dup(2);
     467            1 :     int dn = open("/dev/null", O_WRONLY);
     468            1 :     if (dn >= 0) dup2(dn, 2);
     469            1 :     int rc = gmail_auth_device_flow(&cfg);
     470            1 :     if (dn >= 0) { dup2(saved, 2); close(dn); }
     471            1 :     close(saved);
     472              : 
     473            1 :     ASSERT(rc == 0, "device_flow full mock: returns 0 on success");
     474            1 :     if (cfg.gmail_refresh_token)
     475            1 :         ASSERT(strcmp(cfg.gmail_refresh_token, "dev_flow_refresh") == 0,
     476              :                "device_flow full mock: refresh_token set");
     477              : 
     478            1 :     free(cfg.gmail_client_id);
     479            1 :     free(cfg.gmail_client_secret);
     480            1 :     free(cfg.gmail_refresh_token);
     481              : 
     482            1 :     wait_child(br_pid);
     483            1 :     wait_child(srv_pid);
     484              : 
     485            1 :     unsetenv("GMAIL_TEST_TOKEN_URL");
     486              : }
     487              : 
     488            1 : static void test_device_flow_with_code(void) {
     489              :     /* Child sends a real code= → wait_for_auth_code returns the code.
     490              :      * Covers wait_for_auth_code code-extraction path and the post-building
     491              :      * step in gmail_auth_device_flow.  http_post to TOKEN_URL fails
     492              :      * (invalid credentials / no network) → returns -1. */
     493            1 :     Config cfg = {0};
     494            1 :     cfg.gmail_client_id     = strdup("test-client-id.apps.googleusercontent.com");
     495            1 :     cfg.gmail_client_secret = strdup("test-client-secret");
     496              : 
     497            1 :     pid_t pid = fork();
     498            1 :     if (pid == 0) {
     499            0 :         usleep(200000);
     500            0 :         for (int port = 8089; port <= 8099; port++) {
     501            0 :             int fd = socket(AF_INET, SOCK_STREAM, 0);
     502            0 :             if (fd < 0) continue;
     503            0 :             struct sockaddr_in ca = {0};
     504            0 :             ca.sin_family      = AF_INET;
     505            0 :             ca.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
     506            0 :             ca.sin_port        = htons((uint16_t)port);
     507            0 :             if (connect(fd, (struct sockaddr *)&ca, sizeof(ca)) == 0) {
     508            0 :                 const char *req =
     509              :                     "GET /callback?code=test_code_exchange&scope=x HTTP/1.1\r\n"
     510              :                     "Host: localhost\r\n\r\n";
     511            0 :                 ssize_t w = write(fd, req, strlen(req)); (void)w;
     512              :                 char buf[512];
     513            0 :                 ssize_t r = read(fd, buf, sizeof(buf)); (void)r;
     514            0 :                 close(fd);
     515            0 :                 _exit(0);
     516              :             }
     517            0 :             close(fd);
     518              :         }
     519            0 :         _exit(1);
     520              :     }
     521              : 
     522            1 :     int saved = dup(2);
     523            1 :     int dn = open("/dev/null", O_WRONLY);
     524            1 :     if (dn >= 0) dup2(dn, 2);
     525            1 :     int rc = gmail_auth_device_flow(&cfg);
     526            1 :     if (dn >= 0) { dup2(saved, 2); close(dn); }
     527            1 :     close(saved);
     528              : 
     529              :     /* Token exchange fails with invalid test credentials → returns -1 */
     530            1 :     ASSERT(rc == -1, "device_flow: invalid credentials returns -1");
     531              : 
     532            1 :     wait_child(pid);
     533              : 
     534            1 :     free(cfg.gmail_client_id);
     535            1 :     free(cfg.gmail_client_secret);
     536            1 :     free(cfg.gmail_refresh_token);
     537              : }
     538              : 
     539              : /* ── Registration ─────────────────────────────────────────────────── */
     540              : 
     541            1 : void test_gmail_auth(void) {
     542            1 :     RUN_TEST(test_refresh_no_token);
     543            1 :     RUN_TEST(test_refresh_empty_token);
     544            1 :     RUN_TEST(test_auth_code_extraction);
     545            1 :     RUN_TEST(test_auth_code_denied);
     546            1 :     RUN_TEST(test_wizard_rejects_gmail_imap);
     547            1 :     RUN_TEST(test_refresh_with_client_credentials);
     548            1 :     RUN_TEST(test_device_flow_no_credentials);
     549            1 :     RUN_TEST(test_device_flow_access_denied);
     550            1 :     RUN_TEST(test_device_flow_with_code);
     551            1 :     RUN_TEST(test_refresh_test_token_hook);
     552            1 :     RUN_TEST(test_refresh_via_mock_server_200);
     553            1 :     RUN_TEST(test_refresh_via_mock_server_200_no_token);
     554            1 :     RUN_TEST(test_refresh_via_mock_server_400_unknown_error);
     555            1 :     RUN_TEST(test_refresh_via_mock_server_curl_error);
     556            1 :     RUN_TEST(test_device_flow_full_mock);
     557            1 : }
        

Generated by: LCOV version 2.0-1