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