Line data Source code
1 : /**
2 : * @file test_send_stdin.c
3 : * @brief TEST-13 — stdin pipe → domain_send_message functional test.
4 : *
5 : * Validates the stdin-reading branch used by `cmd_send` when no inline
6 : * message is provided:
7 : * 1. Redirect stdin to a pipe containing "hello from pipe\n".
8 : * 2. Read from stdin exactly as cmd_send does (fread + strip newline).
9 : * 3. Call domain_send_message with the resulting text.
10 : * 4. Assert the mock server received "hello from pipe" in the TL wire.
11 : *
12 : * The test does NOT call the static cmd_send() in tg_cli.c directly; it
13 : * replicates the exact stdin-read idiom so the coverage is equivalent.
14 : */
15 :
16 : #include "test_helpers.h"
17 :
18 : #include "mock_socket.h"
19 : #include "mock_tel_server.h"
20 :
21 : #include "api_call.h"
22 : #include "mtproto_session.h"
23 : #include "transport.h"
24 : #include "app/session_store.h"
25 : #include "tl_serial.h"
26 :
27 : #include "domain/write/send.h"
28 :
29 : #include <stdio.h>
30 : #include <stdlib.h>
31 : #include <string.h>
32 : #include <unistd.h>
33 :
34 : /* ------------------------------------------------------------------ */
35 : /* Helpers shared with test_write_path.c (duplicated to stay simple). */
36 : /* ------------------------------------------------------------------ */
37 :
38 : #define CRC_messages_sendMessage 0x0d9d75a4U
39 : #define CRC_updateShortSentMessage 0x9015e101U
40 :
41 4 : static void with_tmp_home_stdin(const char *tag) {
42 : char tmp[256];
43 4 : snprintf(tmp, sizeof(tmp), "/tmp/tg-cli-ft-stdin-%s", tag);
44 : char bin[512];
45 4 : snprintf(bin, sizeof(bin), "%s/.config/tg-cli/session.bin", tmp);
46 4 : (void)unlink(bin);
47 4 : setenv("HOME", tmp, 1);
48 4 : }
49 :
50 4 : static void connect_mock_stdin(Transport *t) {
51 4 : transport_init(t);
52 4 : ASSERT(transport_connect(t, "127.0.0.1", 443) == 0, "connect");
53 : }
54 :
55 4 : static void init_cfg_stdin(ApiConfig *cfg) {
56 4 : api_config_init(cfg);
57 4 : cfg->api_id = 12345;
58 4 : cfg->api_hash = "deadbeefcafebabef00dbaadfeedc0de";
59 4 : }
60 :
61 4 : static void load_session_stdin(MtProtoSession *s) {
62 4 : ASSERT(mt_server_seed_session(2, NULL, NULL, NULL) == 0, "seed");
63 4 : mtproto_session_init(s);
64 4 : int dc = 0;
65 4 : ASSERT(session_store_load(s, &dc) == 0, "load");
66 : }
67 :
68 : /* ------------------------------------------------------------------ */
69 : /* State captured by the responder. */
70 : /* ------------------------------------------------------------------ */
71 :
72 : static char g_captured_message[4096];
73 :
74 : /* ------------------------------------------------------------------ */
75 : /* Responder: parse messages.sendMessage, capture the 'message' field. */
76 : /* */
77 : /* Wire layout (after invokeWithLayer / initConnection stripped): */
78 : /* CRC uint32 0x0d9d75a4 */
79 : /* flags uint32 */
80 : /* peer inputPeerSelf = uint32 TL_inputPeerSelf */
81 : /* message TL string (length-prefixed) */
82 : /* random_id int64 */
83 : /* ------------------------------------------------------------------ */
84 2 : static void on_send_stdin(MtRpcContext *ctx) {
85 2 : g_captured_message[0] = '\0';
86 :
87 2 : TlReader r = tl_reader_init(ctx->req_body, ctx->req_body_len);
88 2 : tl_read_uint32(&r); /* CRC */
89 2 : tl_read_uint32(&r); /* flags */
90 2 : tl_read_uint32(&r); /* inputPeerSelf constructor */
91 2 : char *msg = tl_read_string(&r);
92 2 : if (msg) {
93 2 : snprintf(g_captured_message, sizeof(g_captured_message), "%s", msg);
94 2 : free(msg);
95 : }
96 :
97 : /* Reply with updateShortSentMessage so domain_send_message returns 0. */
98 2 : TlWriter w; tl_writer_init(&w);
99 2 : tl_write_uint32(&w, CRC_updateShortSentMessage);
100 2 : tl_write_uint32(&w, 0); /* flags */
101 2 : tl_write_int32 (&w, 777); /* id */
102 2 : tl_write_int32 (&w, 0); /* pts */
103 2 : tl_write_int32 (&w, 0); /* pts_count */
104 2 : tl_write_int32 (&w, 0); /* date */
105 2 : mt_server_reply_result(ctx, w.data, w.len);
106 2 : tl_writer_free(&w);
107 2 : }
108 :
109 : /* ------------------------------------------------------------------ */
110 : /* Tests */
111 : /* ------------------------------------------------------------------ */
112 :
113 : /**
114 : * Happy path: pipe "hello from pipe\n" into stdin, read it as cmd_send
115 : * does, pass to domain_send_message, assert the server sees the text.
116 : */
117 2 : static void test_send_from_stdin_pipe(void) {
118 2 : with_tmp_home_stdin("pipe");
119 2 : mt_server_init(); mt_server_reset();
120 2 : MtProtoSession s; load_session_stdin(&s);
121 2 : mt_server_expect(CRC_messages_sendMessage, on_send_stdin, NULL);
122 :
123 2 : ApiConfig cfg; init_cfg_stdin(&cfg);
124 2 : Transport t; connect_mock_stdin(&t);
125 :
126 : /* Set up a pipe and redirect stdin to its read end. */
127 : int pipefd[2];
128 2 : ASSERT(pipe(pipefd) == 0, "pipe() created");
129 2 : const char *pipe_content = "hello from pipe\n";
130 2 : ssize_t written = write(pipefd[1], pipe_content, strlen(pipe_content));
131 2 : ASSERT(written == (ssize_t)strlen(pipe_content), "wrote to pipe");
132 2 : close(pipefd[1]);
133 :
134 2 : int saved_stdin = dup(STDIN_FILENO);
135 2 : ASSERT(saved_stdin >= 0, "dup stdin");
136 2 : ASSERT(dup2(pipefd[0], STDIN_FILENO) == STDIN_FILENO, "dup2 stdin");
137 2 : close(pipefd[0]);
138 :
139 : /* --- Replicate cmd_send's stdin-read idiom --- */
140 : char stdin_buf[4096];
141 2 : size_t n = fread(stdin_buf, 1, sizeof(stdin_buf) - 1, stdin);
142 2 : ASSERT(n > 0, "fread from pipe got bytes");
143 2 : stdin_buf[n] = '\0';
144 : /* Strip one trailing newline for convenience (same as cmd_send). */
145 2 : if (n > 0 && stdin_buf[n - 1] == '\n') stdin_buf[n - 1] = '\0';
146 2 : const char *msg = stdin_buf;
147 :
148 : /* Restore stdin so subsequent test output is not affected. */
149 2 : ASSERT(dup2(saved_stdin, STDIN_FILENO) == STDIN_FILENO, "restore stdin");
150 2 : close(saved_stdin);
151 :
152 : /* --- Send via domain layer --- */
153 2 : int32_t mid = 0;
154 2 : RpcError err = {0};
155 2 : ASSERT(domain_send_message(&cfg, &s, &t, &(HistoryPeer){.kind = HISTORY_PEER_SELF},
156 : msg, &mid, &err) == 0,
157 : "domain_send_message succeeds");
158 2 : ASSERT(mid == 777, "message id echoed from mock server");
159 2 : ASSERT(strcmp(g_captured_message, "hello from pipe") == 0,
160 : "server received 'hello from pipe' (newline stripped)");
161 :
162 2 : transport_close(&t);
163 2 : mt_server_reset();
164 : }
165 :
166 : /**
167 : * Empty stdin should not reach the server (domain_send_message rejects
168 : * empty strings before the wire).
169 : */
170 2 : static void test_send_empty_stdin_rejected(void) {
171 2 : with_tmp_home_stdin("empty");
172 2 : mt_server_init(); mt_server_reset();
173 2 : MtProtoSession s; load_session_stdin(&s);
174 : /* No handler — wire must not be touched. */
175 :
176 2 : ApiConfig cfg; init_cfg_stdin(&cfg);
177 2 : Transport t; connect_mock_stdin(&t);
178 :
179 : /* domain_send_message rejects "" before sending. */
180 2 : int32_t mid = 0;
181 2 : RpcError err = {0};
182 2 : ASSERT(domain_send_message(&cfg, &s, &t,
183 : &(HistoryPeer){.kind = HISTORY_PEER_SELF},
184 : "", &mid, &err) == -1,
185 : "empty message rejected");
186 2 : ASSERT(mt_server_rpc_call_count() == 0, "no RPC dispatched for empty");
187 :
188 2 : transport_close(&t);
189 2 : mt_server_reset();
190 : }
191 :
192 : /* ------------------------------------------------------------------ */
193 : /* Suite entry point */
194 : /* ------------------------------------------------------------------ */
195 :
196 2 : void run_send_stdin_tests(void) {
197 2 : RUN_TEST(test_send_from_stdin_pipe);
198 2 : RUN_TEST(test_send_empty_stdin_rejected);
199 2 : }
|