Line data Source code
1 : /**
2 : * @file test_transport_resilience.c
3 : * @brief TEST-82 — functional coverage for transport.c error paths.
4 : *
5 : * Drives the real `transport_*` helpers over the mock socket while
6 : * injecting the faults listed in US-31:
7 : *
8 : * 1. connect() refused → bootstrap fails clean with errno visible.
9 : * 2. Partial send() → transport_send loops to completion.
10 : * 3. Partial recv() → transport_recv reassembles the frame.
11 : * 4. EINTR mid-send → silent retry.
12 : * 5. EAGAIN mid-send → silent retry (blocking socket + signal).
13 : * 6. Mid-RPC EOF → transport_recv surfaces -1, explicit
14 : * close + reconnect brings the channel
15 : * back on the next poll.
16 : * 7. SIGPIPE → ignored at process level; write after
17 : * peer close surfaces EPIPE (-1) rather
18 : * than killing the test binary.
19 : *
20 : * All injection is mock-side (see `mock_socket_*` helpers). No changes to
21 : * production transport.c beyond the EINTR→EAGAIN widening that US-31
22 : * itself mandated.
23 : */
24 :
25 : #include "test_helpers.h"
26 :
27 : #include "mock_socket.h"
28 : #include "mock_tel_server.h"
29 :
30 : #include "api_call.h"
31 : #include "mtproto_rpc.h"
32 : #include "mtproto_session.h"
33 : #include "transport.h"
34 : #include "app/session_store.h"
35 : #include "tl_serial.h"
36 :
37 : #include <errno.h>
38 : #include <signal.h>
39 : #include <stdint.h>
40 : #include <stdio.h>
41 : #include <stdlib.h>
42 : #include <string.h>
43 : #include <unistd.h>
44 :
45 : /* ---- TL CRCs local to this suite ---- */
46 : #define CRC_ping 0x7abe77ecU
47 : #define CRC_pong 0x347773c5U
48 :
49 : /* ================================================================ */
50 : /* Boilerplate */
51 : /* ================================================================ */
52 :
53 32 : static void with_tmp_home(const char *tag) {
54 : char tmp[256];
55 32 : snprintf(tmp, sizeof(tmp), "/tmp/tg-cli-ft-resilience-%s", tag);
56 : char bin[512];
57 32 : snprintf(bin, sizeof(bin), "%s/.config/tg-cli/session.bin", tmp);
58 32 : (void)unlink(bin);
59 32 : setenv("HOME", tmp, 1);
60 32 : }
61 :
62 : /** Canned ping responder — emits pong echoing the ping_id. */
63 12 : static void on_ping(MtRpcContext *ctx) {
64 12 : if (ctx->req_body_len < 4 + 8) return;
65 12 : uint64_t ping_id = 0;
66 108 : for (int i = 0; i < 8; ++i) {
67 96 : ping_id |= ((uint64_t)ctx->req_body[4 + i]) << (i * 8);
68 : }
69 12 : TlWriter w; tl_writer_init(&w);
70 12 : tl_write_uint32(&w, CRC_pong);
71 12 : tl_write_uint64(&w, ctx->req_msg_id);
72 12 : tl_write_uint64(&w, ping_id);
73 12 : mt_server_reply_result(ctx, w.data, w.len);
74 12 : tl_writer_free(&w);
75 : }
76 :
77 12 : static void send_ping(MtProtoSession *s, Transport *t, uint64_t ping_id) {
78 12 : TlWriter req; tl_writer_init(&req);
79 12 : tl_write_uint32(&req, CRC_ping);
80 12 : tl_write_uint64(&req, ping_id);
81 12 : ASSERT(rpc_send_encrypted(s, t, req.data, req.len, 1) == 0,
82 : "send ping");
83 12 : tl_writer_free(&req);
84 : }
85 :
86 10 : static void expect_pong(MtProtoSession *s, Transport *t, uint64_t ping_id) {
87 : uint8_t reply[1024];
88 10 : size_t reply_len = 0;
89 10 : ASSERT(rpc_recv_encrypted(s, t, reply, sizeof(reply), &reply_len) == 0,
90 : "recv pong");
91 10 : TlReader r = tl_reader_init(reply, reply_len);
92 10 : ASSERT(tl_read_uint32(&r) == 0xf35c6d01U, "rpc_result outer");
93 10 : tl_read_uint64(&r); /* req_msg_id */
94 10 : ASSERT(tl_read_uint32(&r) == CRC_pong, "inner is pong");
95 10 : tl_read_uint64(&r); /* echoed msg_id */
96 10 : ASSERT(tl_read_uint64(&r) == ping_id, "ping_id round-trips");
97 : }
98 :
99 : /* ================================================================ */
100 : /* 1. connect() refused */
101 : /* ================================================================ */
102 :
103 : /* Refuse-connect is a persistent mode — every reconnect attempt in a
104 : * retry loop keeps failing. We assert the distinct errno and a clean
105 : * failure (transport.fd stays -1). */
106 2 : static void test_connect_refused_is_fatal_with_clean_exit(void) {
107 2 : with_tmp_home("conn-refused");
108 2 : mt_server_init(); mt_server_reset();
109 2 : mock_socket_refuse_connect();
110 :
111 : Transport t;
112 2 : transport_init(&t);
113 :
114 2 : errno = 0;
115 2 : int rc = transport_connect(&t, "127.0.0.1", 443);
116 2 : ASSERT(rc == -1,
117 : "transport_connect returns -1 when peer refuses");
118 2 : ASSERT(errno == ECONNREFUSED,
119 : "errno preserved as ECONNREFUSED across the failed call");
120 2 : ASSERT(t.fd == -1,
121 : "transport fd reset to -1 on failure (no leaked socket)");
122 2 : ASSERT(t.connected == 0,
123 : "transport.connected stays 0 after failed connect");
124 :
125 : /* A second attempt still fails — refuse is persistent until reset. */
126 2 : errno = 0;
127 2 : rc = transport_connect(&t, "127.0.0.1", 443);
128 2 : ASSERT(rc == -1, "second attempt still refused");
129 2 : ASSERT(errno == ECONNREFUSED, "errno still ECONNREFUSED on retry");
130 :
131 : /* Close on an already-failed transport must be a safe no-op. */
132 2 : transport_close(&t);
133 2 : ASSERT(t.fd == -1, "close on unconnected transport leaves fd at -1");
134 :
135 2 : mt_server_reset();
136 : }
137 :
138 : /* ================================================================ */
139 : /* 2. Partial send() retries to completion */
140 : /* ================================================================ */
141 :
142 : /* With a 16-byte send cap, every sys_socket_send returns at most 16 bytes.
143 : * The full encrypted ping frame is ~72 bytes, so transport_send has to
144 : * loop multiple times for the payload portion. The server-side reply is
145 : * still assembled correctly. */
146 2 : static void test_partial_send_retries_to_completion(void) {
147 2 : with_tmp_home("partial-send");
148 2 : mt_server_init(); mt_server_reset();
149 2 : mt_server_expect(CRC_ping, on_ping, NULL);
150 :
151 2 : ASSERT(mt_server_seed_session(2, NULL, NULL, NULL) == 0, "seed");
152 2 : MtProtoSession s; mtproto_session_init(&s);
153 2 : int dc = 0;
154 2 : ASSERT(session_store_load(&s, &dc) == 0, "load session");
155 :
156 2 : Transport t; transport_init(&t);
157 2 : ASSERT(transport_connect(&t, "127.0.0.1", 443) == 0, "connect");
158 :
159 : /* Now turn on fragmentation — the connect's own marker byte is
160 : * already gone so the cap applies only to real payload sends. */
161 2 : mock_socket_set_send_fragment(16);
162 :
163 2 : send_ping(&s, &t, 0xDEADBEEF12345678ULL);
164 2 : expect_pong(&s, &t, 0xDEADBEEF12345678ULL);
165 :
166 2 : ASSERT(mt_server_rpc_call_count() == 1,
167 : "exactly one logical RPC despite many partial writes");
168 :
169 2 : transport_close(&t);
170 2 : mt_server_reset();
171 : }
172 :
173 : /* ================================================================ */
174 : /* 3. Partial recv() reassembles the frame */
175 : /* ================================================================ */
176 :
177 : /* With a 16-byte recv cap the encrypted pong comes back across many
178 : * sys_socket_recv calls. transport_recv must continue reading until
179 : * the announced abridged length is fully drained. */
180 2 : static void test_partial_recv_reassembles_frame(void) {
181 2 : with_tmp_home("partial-recv");
182 2 : mt_server_init(); mt_server_reset();
183 2 : mt_server_expect(CRC_ping, on_ping, NULL);
184 :
185 2 : ASSERT(mt_server_seed_session(2, NULL, NULL, NULL) == 0, "seed");
186 2 : MtProtoSession s; mtproto_session_init(&s);
187 2 : int dc = 0;
188 2 : ASSERT(session_store_load(&s, &dc) == 0, "load");
189 :
190 2 : Transport t; transport_init(&t);
191 2 : ASSERT(transport_connect(&t, "127.0.0.1", 443) == 0, "connect");
192 :
193 2 : send_ping(&s, &t, 0xCAFEBABE00000001ULL);
194 :
195 : /* Arm fragmentation only for the reply path. */
196 2 : mock_socket_set_recv_fragment(16);
197 :
198 2 : expect_pong(&s, &t, 0xCAFEBABE00000001ULL);
199 :
200 2 : transport_close(&t);
201 2 : mt_server_reset();
202 : }
203 :
204 : /* ================================================================ */
205 : /* 4. EINTR is silently retried */
206 : /* ================================================================ */
207 :
208 2 : static void test_eintr_is_silent_retry(void) {
209 2 : with_tmp_home("eintr");
210 2 : mt_server_init(); mt_server_reset();
211 2 : mt_server_expect(CRC_ping, on_ping, NULL);
212 :
213 2 : ASSERT(mt_server_seed_session(2, NULL, NULL, NULL) == 0, "seed");
214 2 : MtProtoSession s; mtproto_session_init(&s);
215 2 : int dc = 0;
216 2 : ASSERT(session_store_load(&s, &dc) == 0, "load");
217 :
218 2 : Transport t; transport_init(&t);
219 2 : ASSERT(transport_connect(&t, "127.0.0.1", 443) == 0, "connect");
220 :
221 : /* Inject one EINTR on the next send AND on the next recv to exercise
222 : * both retry loops. */
223 2 : mock_socket_inject_eintr_next_send();
224 2 : mock_socket_inject_eintr_next_recv();
225 :
226 2 : send_ping(&s, &t, 0x0000EE1200000001ULL);
227 2 : expect_pong(&s, &t, 0x0000EE1200000001ULL);
228 :
229 2 : transport_close(&t);
230 2 : mt_server_reset();
231 : }
232 :
233 : /* ================================================================ */
234 : /* 5. EAGAIN is silently retried */
235 : /* ================================================================ */
236 :
237 2 : static void test_eagain_is_silent_retry(void) {
238 2 : with_tmp_home("eagain");
239 2 : mt_server_init(); mt_server_reset();
240 2 : mt_server_expect(CRC_ping, on_ping, NULL);
241 :
242 2 : ASSERT(mt_server_seed_session(2, NULL, NULL, NULL) == 0, "seed");
243 2 : MtProtoSession s; mtproto_session_init(&s);
244 2 : int dc = 0;
245 2 : ASSERT(session_store_load(&s, &dc) == 0, "load");
246 :
247 2 : Transport t; transport_init(&t);
248 2 : ASSERT(transport_connect(&t, "127.0.0.1", 443) == 0, "connect");
249 :
250 2 : mock_socket_inject_eagain_next_send();
251 2 : mock_socket_inject_eagain_next_recv();
252 :
253 2 : send_ping(&s, &t, 0x0A0A0A0A11111111ULL);
254 2 : expect_pong(&s, &t, 0x0A0A0A0A11111111ULL);
255 :
256 2 : transport_close(&t);
257 2 : mt_server_reset();
258 : }
259 :
260 : /* ================================================================ */
261 : /* 6. Mid-RPC EOF: next poll reconnects and resumes */
262 : /* ================================================================ */
263 :
264 : /* Send ping #1 → force EOF on the recv so the RPC fails.
265 : * Close, rearm the mt_server reconnect detector, connect again, and
266 : * verify a second ping round-trips cleanly. That mirrors what a
267 : * polling watch/upload loop does: treat a transport failure as a
268 : * transient and reopen the channel on the next iteration. */
269 2 : static void test_mid_rpc_disconnect_reconnects(void) {
270 2 : with_tmp_home("mid-rpc-eof");
271 2 : mt_server_init(); mt_server_reset();
272 2 : mt_server_expect(CRC_ping, on_ping, NULL);
273 :
274 2 : ASSERT(mt_server_seed_session(2, NULL, NULL, NULL) == 0, "seed");
275 2 : MtProtoSession s; mtproto_session_init(&s);
276 2 : int dc = 0;
277 2 : ASSERT(session_store_load(&s, &dc) == 0, "load");
278 :
279 2 : Transport t; transport_init(&t);
280 2 : ASSERT(transport_connect(&t, "127.0.0.1", 443) == 0, "connect 1");
281 :
282 : /* Queue the EOF before we even send — the reply that on_ping would
283 : * normally append gets shadowed by the kill-on-next knob. */
284 2 : mock_socket_kill_on_next_recv();
285 2 : send_ping(&s, &t, 0x1111222233334444ULL);
286 :
287 : uint8_t reply[512];
288 2 : size_t reply_len = 0;
289 2 : int rc = rpc_recv_encrypted(&s, &t, reply, sizeof(reply), &reply_len);
290 2 : ASSERT(rc == -1,
291 : "rpc_recv_encrypted surfaces -1 on mid-RPC EOF");
292 :
293 : /* Simulate the poll-loop reconnect: close, rearm the mt_server
294 : * reconnect detector (so the 0xEF on the new socket is parsed as a
295 : * fresh connection), connect again. */
296 2 : transport_close(&t);
297 2 : ASSERT(t.fd == -1, "transport fd cleared after close");
298 :
299 2 : mt_server_arm_reconnect();
300 2 : transport_init(&t);
301 2 : ASSERT(transport_connect(&t, "127.0.0.1", 443) == 0, "reconnect");
302 :
303 : /* Second ping over the fresh socket — succeeds end-to-end. */
304 2 : send_ping(&s, &t, 0x5555666677778888ULL);
305 2 : expect_pong(&s, &t, 0x5555666677778888ULL);
306 :
307 2 : transport_close(&t);
308 2 : mt_server_reset();
309 : }
310 :
311 : /* ================================================================ */
312 : /* 7. SIGPIPE is ignored */
313 : /* ================================================================ */
314 :
315 : /* The real binaries call `signal(SIGPIPE, SIG_IGN)` inside their
316 : * entry points. We replicate that same guard here and assert that a
317 : * write-after-peer-close surfaces EPIPE from sys_socket_send rather
318 : * than killing the process with signal 13.
319 : *
320 : * Because we use the mock socket there is no real kernel SIGPIPE to
321 : * receive — instead the test validates that production transport_send
322 : * treats the -1/EPIPE path as a normal -1 return and does not loop
323 : * forever. Combined with the SIG_IGN guard this covers the same
324 : * contract on a real OS.
325 : */
326 2 : static void test_sigpipe_is_ignored(void) {
327 2 : with_tmp_home("sigpipe");
328 2 : mt_server_init(); mt_server_reset();
329 :
330 : /* Install the same handler the production binaries do. */
331 : struct sigaction sa;
332 2 : memset(&sa, 0, sizeof(sa));
333 2 : sa.sa_handler = SIG_IGN;
334 2 : sigemptyset(&sa.sa_mask);
335 2 : ASSERT(sigaction(SIGPIPE, &sa, NULL) == 0,
336 : "installed SIG_IGN for SIGPIPE");
337 :
338 : /* Verify the disposition took — subsequent writes on a broken
339 : * pipe do not terminate the process. */
340 : struct sigaction current;
341 2 : ASSERT(sigaction(SIGPIPE, NULL, ¤t) == 0, "query disposition");
342 2 : ASSERT(current.sa_handler == SIG_IGN,
343 : "SIGPIPE disposition is SIG_IGN");
344 :
345 2 : ASSERT(mt_server_seed_session(2, NULL, NULL, NULL) == 0, "seed");
346 2 : MtProtoSession s; mtproto_session_init(&s);
347 2 : int dc = 0;
348 2 : ASSERT(session_store_load(&s, &dc) == 0, "load");
349 :
350 2 : Transport t; transport_init(&t);
351 2 : ASSERT(transport_connect(&t, "127.0.0.1", 443) == 0, "connect");
352 :
353 : /* Prime the NEXT payload-carrying send to fail. Connect already
354 : * consumed call #1 (the 0xEF marker), so we target call #2 — the
355 : * length-prefix of the next transport_send. Setting errno=EPIPE
356 : * emulates a real broken-pipe return without relying on the mock
357 : * to propagate it. */
358 2 : mock_socket_fail_send_at(2);
359 2 : errno = EPIPE;
360 :
361 2 : uint8_t payload[4] = {0xAA, 0xBB, 0xCC, 0xDD};
362 2 : int rc = transport_send(&t, payload, sizeof(payload));
363 2 : ASSERT(rc == -1,
364 : "transport_send surfaces broken-pipe error without aborting");
365 :
366 : /* The process survived — just close the transport and move on.
367 : * Touching stderr here would normally trigger SIGPIPE on a real
368 : * head -c 100 consumer; we simulate that by writing to /dev/null. */
369 2 : FILE *f = fopen("/dev/null", "w");
370 2 : ASSERT(f != NULL, "/dev/null open");
371 2 : fprintf(f, "tg-cli watch output line\n");
372 2 : fclose(f);
373 :
374 2 : transport_close(&t);
375 :
376 : /* Restore default so other tests run unaffected. */
377 2 : sa.sa_handler = SIG_DFL;
378 2 : sigaction(SIGPIPE, &sa, NULL);
379 2 : mt_server_reset();
380 : }
381 :
382 : /* ================================================================ */
383 : /* 8. Additional fault-path corners */
384 : /* ================================================================ */
385 :
386 : /* Unaligned payload is rejected at the transport layer — the abridged
387 : * length prefix is in 4-byte units, so a non-multiple length would
388 : * desync the stream. */
389 2 : static void test_unaligned_payload_rejected(void) {
390 2 : with_tmp_home("unaligned");
391 2 : mt_server_init(); mt_server_reset();
392 :
393 2 : Transport t; transport_init(&t);
394 2 : ASSERT(transport_connect(&t, "127.0.0.1", 443) == 0, "connect");
395 :
396 2 : uint8_t payload[3] = {0x01, 0x02, 0x03};
397 2 : int rc = transport_send(&t, payload, sizeof(payload));
398 2 : ASSERT(rc == -1,
399 : "transport_send rejects payload not multiple of 4 bytes");
400 :
401 2 : transport_close(&t);
402 2 : mt_server_reset();
403 : }
404 :
405 : /* sys_socket_create failure: every subsequent connect attempt fails
406 : * before even reaching sys_socket_connect. Exercises the socket()
407 : * error branch that the refuse-connect test doesn't. */
408 2 : static void test_socket_create_failure(void) {
409 2 : with_tmp_home("sock-create");
410 2 : mt_server_init(); mt_server_reset();
411 2 : mock_socket_fail_create();
412 :
413 2 : Transport t; transport_init(&t);
414 2 : int rc = transport_connect(&t, "127.0.0.1", 443);
415 2 : ASSERT(rc == -1,
416 : "transport_connect fails when sys_socket_create returns -1");
417 2 : ASSERT(t.fd == -1, "fd stays -1 on socket() failure");
418 :
419 2 : mt_server_reset();
420 : }
421 :
422 : /* Abridged marker send fails in transport_connect — covers the cleanup
423 : * branch that closes the socket and resets fd. */
424 2 : static void test_marker_send_failure_closes_socket(void) {
425 2 : with_tmp_home("marker-fail");
426 2 : mt_server_init(); mt_server_reset();
427 :
428 : /* Marker send is sys_socket_send call #1 — prime it to fail. */
429 2 : mock_socket_fail_send_at(1);
430 :
431 2 : Transport t; transport_init(&t);
432 2 : int rc = transport_connect(&t, "127.0.0.1", 443);
433 2 : ASSERT(rc == -1,
434 : "transport_connect fails when marker send fails");
435 2 : ASSERT(t.fd == -1,
436 : "fd reset to -1 after marker-send failure");
437 2 : ASSERT(t.connected == 0,
438 : "connected flag stays 0 after marker-send failure");
439 :
440 2 : mt_server_reset();
441 : }
442 :
443 : /* Mid-send payload failure: after a successful length prefix the
444 : * payload chunk send fails. Exercises transport_send's "sent <= 0"
445 : * branch (line ~108-109 in transport.c). */
446 2 : static void test_payload_send_failure(void) {
447 2 : with_tmp_home("payload-send-fail");
448 2 : mt_server_init(); mt_server_reset();
449 :
450 2 : Transport t; transport_init(&t);
451 2 : ASSERT(transport_connect(&t, "127.0.0.1", 443) == 0, "connect");
452 :
453 : /* Call #1 = marker (done). Call #2 = prefix (let it pass).
454 : * Call #3 = payload → force failure. */
455 2 : mock_socket_fail_send_at(3);
456 :
457 2 : uint8_t payload[8] = {1,2,3,4,5,6,7,8};
458 2 : int rc = transport_send(&t, payload, sizeof(payload));
459 2 : ASSERT(rc == -1,
460 : "transport_send surfaces -1 when payload chunk fails");
461 :
462 2 : transport_close(&t);
463 2 : mt_server_reset();
464 : }
465 :
466 : /* Large wire_len that exceeds the caller's buffer is rejected by
467 : * transport_recv — covers the payload_len > max_len branch. */
468 2 : static void test_recv_frame_too_large_rejected(void) {
469 2 : with_tmp_home("frame-too-large");
470 2 : mt_server_init(); mt_server_reset();
471 :
472 2 : Transport t; transport_init(&t);
473 2 : ASSERT(transport_connect(&t, "127.0.0.1", 443) == 0, "connect");
474 :
475 : /* Arm a response whose abridged length prefix decodes to a huge
476 : * payload (wire_len = 0x00FFFFFF → ~67 MB) so transport_recv's
477 : * max_len check triggers without us sending 67 MB of mock data. */
478 2 : uint8_t giant[4] = { 0x7F, 0xFF, 0xFF, 0xFF };
479 2 : mock_socket_set_response(giant, sizeof(giant));
480 :
481 : uint8_t buf[256];
482 2 : size_t out_len = 0;
483 2 : int rc = transport_recv(&t, buf, sizeof(buf), &out_len);
484 2 : ASSERT(rc == -1,
485 : "transport_recv rejects a frame larger than the caller buffer");
486 :
487 2 : transport_close(&t);
488 2 : mt_server_reset();
489 : }
490 :
491 : /* Recv length-prefix fails partway through the 3-byte extended prefix.
492 : * Covers the secondary prefix-recv failure branch. */
493 2 : static void test_extended_prefix_recv_failure(void) {
494 2 : with_tmp_home("ext-prefix-fail");
495 2 : mt_server_init(); mt_server_reset();
496 :
497 2 : Transport t; transport_init(&t);
498 2 : ASSERT(transport_connect(&t, "127.0.0.1", 443) == 0, "connect");
499 :
500 : /* Queue only the 0x7F marker byte; the 3-byte continuation is
501 : * missing, so sys_socket_recv returns 0 on the follow-up call. */
502 2 : uint8_t only_marker = 0x7F;
503 2 : mock_socket_set_response(&only_marker, 1);
504 :
505 : uint8_t buf[64];
506 2 : size_t out_len = 0;
507 2 : int rc = transport_recv(&t, buf, sizeof(buf), &out_len);
508 2 : ASSERT(rc == -1,
509 : "transport_recv fails when 3-byte extended prefix is truncated");
510 :
511 2 : transport_close(&t);
512 2 : mt_server_reset();
513 : }
514 :
515 : /* 4-byte extended length prefix fails to send. Payload size must be
516 : * at least 0x7F * 4 = 508 bytes for transport_send to pick the 4-byte
517 : * prefix branch. Call #1 = marker (connect), call #2 = 4-byte prefix
518 : * (primed to fail). */
519 2 : static void test_extended_prefix_send_failure(void) {
520 2 : with_tmp_home("ext-prefix-send-fail");
521 2 : mt_server_init(); mt_server_reset();
522 :
523 2 : Transport t; transport_init(&t);
524 2 : ASSERT(transport_connect(&t, "127.0.0.1", 443) == 0, "connect");
525 :
526 2 : mock_socket_fail_send_at(2);
527 :
528 : /* 508 bytes = 0x7F * 4 — the minimum that triggers the extended
529 : * prefix branch. Contents don't matter, mock fails the send. */
530 2 : uint8_t payload[508] = {0};
531 2 : int rc = transport_send(&t, payload, sizeof(payload));
532 2 : ASSERT(rc == -1,
533 : "transport_send fails when 4-byte length prefix send fails");
534 :
535 2 : transport_close(&t);
536 2 : mt_server_reset();
537 : }
538 :
539 : /* Payload recv fails partway through: arm the first-byte length prefix
540 : * to read cleanly, then fail the follow-up payload read. Covers the
541 : * "r <= 0" branch of the payload-read loop. */
542 2 : static void test_payload_recv_failure_mid_frame(void) {
543 2 : with_tmp_home("payload-recv-fail");
544 2 : mt_server_init(); mt_server_reset();
545 :
546 2 : Transport t; transport_init(&t);
547 2 : ASSERT(transport_connect(&t, "127.0.0.1", 443) == 0, "connect");
548 :
549 : /* Queue a length prefix that promises 12 bytes of payload, but
550 : * supply none. Recv call #1 reads the prefix byte (1 byte of the
551 : * buffered response). Recv call #2 runs against an empty queue
552 : * and returns 0 (EOF) → transport_recv bails. */
553 2 : uint8_t prefix_only = 0x03; /* 3 * 4 = 12 bytes announced */
554 2 : mock_socket_set_response(&prefix_only, 1);
555 :
556 : uint8_t buf[64];
557 2 : size_t out_len = 0;
558 2 : int rc = transport_recv(&t, buf, sizeof(buf), &out_len);
559 2 : ASSERT(rc == -1,
560 : "transport_recv surfaces -1 when payload read yields EOF early");
561 :
562 2 : transport_close(&t);
563 2 : mt_server_reset();
564 : }
565 :
566 : /* Zero-payload frame: wire_len prefix = 0 → transport_recv returns 0
567 : * with out_len=0. Covers the empty-frame shortcut branch. */
568 2 : static void test_zero_payload_frame(void) {
569 2 : with_tmp_home("zero-frame");
570 2 : mt_server_init(); mt_server_reset();
571 :
572 2 : Transport t; transport_init(&t);
573 2 : ASSERT(transport_connect(&t, "127.0.0.1", 443) == 0, "connect");
574 :
575 2 : uint8_t zero = 0x00;
576 2 : mock_socket_set_response(&zero, 1);
577 :
578 : uint8_t buf[32];
579 2 : size_t out_len = 99;
580 2 : int rc = transport_recv(&t, buf, sizeof(buf), &out_len);
581 2 : ASSERT(rc == 0,
582 : "transport_recv returns 0 on zero-length frame");
583 2 : ASSERT(out_len == 0,
584 : "out_len reset to 0 for zero-length frame");
585 :
586 2 : transport_close(&t);
587 2 : mt_server_reset();
588 : }
589 :
590 : /* ================================================================ */
591 : /* Suite entry point */
592 : /* ================================================================ */
593 :
594 2 : void run_transport_resilience_tests(void) {
595 2 : RUN_TEST(test_connect_refused_is_fatal_with_clean_exit);
596 2 : RUN_TEST(test_partial_send_retries_to_completion);
597 2 : RUN_TEST(test_partial_recv_reassembles_frame);
598 2 : RUN_TEST(test_eintr_is_silent_retry);
599 2 : RUN_TEST(test_eagain_is_silent_retry);
600 2 : RUN_TEST(test_mid_rpc_disconnect_reconnects);
601 2 : RUN_TEST(test_sigpipe_is_ignored);
602 :
603 : /* Extra error-path corners for coverage completeness. */
604 2 : RUN_TEST(test_unaligned_payload_rejected);
605 2 : RUN_TEST(test_socket_create_failure);
606 2 : RUN_TEST(test_marker_send_failure_closes_socket);
607 2 : RUN_TEST(test_payload_send_failure);
608 2 : RUN_TEST(test_recv_frame_too_large_rejected);
609 2 : RUN_TEST(test_extended_prefix_recv_failure);
610 2 : RUN_TEST(test_extended_prefix_send_failure);
611 2 : RUN_TEST(test_payload_recv_failure_mid_frame);
612 2 : RUN_TEST(test_zero_payload_frame);
613 2 : }
|