Line data Source code
1 : /**
2 : * @file test_api_call.c
3 : * @brief Unit tests for API call wrapper (initConnection + invokeWithLayer).
4 : */
5 :
6 : #include "test_helpers.h"
7 : #include "api_call.h"
8 : #include "tl_serial.h"
9 : #include "mock_crypto.h"
10 :
11 : #include <stdlib.h>
12 : #include <string.h>
13 :
14 1 : void test_api_config_init(void) {
15 : ApiConfig cfg;
16 1 : api_config_init(&cfg);
17 1 : ASSERT(cfg.api_id == 0, "api_id should be 0 before setting");
18 1 : ASSERT(strcmp(cfg.device_model, "tg-cli") == 0, "device_model default");
19 1 : ASSERT(strcmp(cfg.app_version, "0.1.0") == 0, "app_version default");
20 1 : ASSERT(strcmp(cfg.system_lang_code, "en") == 0, "lang_code default");
21 : }
22 :
23 1 : void test_api_wrap_query_structure(void) {
24 : ApiConfig cfg;
25 1 : api_config_init(&cfg);
26 1 : cfg.api_id = 12345;
27 :
28 : /* Simple inner query: just a constructor */
29 : TlWriter q;
30 1 : tl_writer_init(&q);
31 1 : tl_write_uint32(&q, 0xDEADBEEF); /* fake query constructor */
32 :
33 : uint8_t out[4096];
34 1 : size_t out_len = 0;
35 1 : int rc = api_wrap_query(&cfg, q.data, q.len, out, sizeof(out), &out_len);
36 1 : tl_writer_free(&q);
37 :
38 1 : ASSERT(rc == 0, "wrap should succeed");
39 1 : ASSERT(out_len > 0, "output should have data");
40 :
41 : /* Parse the wrapped output */
42 1 : TlReader r = tl_reader_init(out, out_len);
43 :
44 : /* invokeWithLayer constructor */
45 1 : uint32_t crc1 = tl_read_uint32(&r);
46 1 : ASSERT(crc1 == CRC_invokeWithLayer, "should start with invokeWithLayer");
47 :
48 : /* layer */
49 1 : int32_t layer = tl_read_int32(&r);
50 1 : ASSERT(layer == TL_LAYER, "layer should match TL_LAYER");
51 :
52 : /* initConnection constructor */
53 1 : uint32_t crc2 = tl_read_uint32(&r);
54 1 : ASSERT(crc2 == CRC_initConnection, "should have initConnection");
55 :
56 : /* flags */
57 1 : int32_t flags = tl_read_int32(&r);
58 1 : ASSERT(flags == 0, "flags should be 0");
59 :
60 : /* api_id */
61 1 : int32_t api_id = tl_read_int32(&r);
62 1 : ASSERT(api_id == 12345, "api_id should be 12345");
63 :
64 : /* device_model */
65 1 : char *dm = tl_read_string(&r);
66 1 : ASSERT(dm != NULL, "device_model should not be NULL");
67 1 : ASSERT(strcmp(dm, "tg-cli") == 0, "device_model should be tg-cli");
68 1 : free(dm);
69 :
70 : /* system_version */
71 1 : char *sv = tl_read_string(&r);
72 1 : ASSERT(sv != NULL, "system_version");
73 1 : free(sv);
74 :
75 : /* app_version */
76 1 : char *av = tl_read_string(&r);
77 1 : ASSERT(av != NULL, "app_version");
78 1 : free(av);
79 :
80 : /* system_lang_code */
81 1 : char *slc = tl_read_string(&r);
82 1 : ASSERT(slc != NULL, "system_lang_code");
83 1 : free(slc);
84 :
85 : /* lang_pack */
86 1 : char *lp = tl_read_string(&r);
87 1 : ASSERT(lp != NULL, "lang_pack");
88 1 : free(lp);
89 :
90 : /* lang_code */
91 1 : char *lc = tl_read_string(&r);
92 1 : ASSERT(lc != NULL, "lang_code");
93 1 : free(lc);
94 :
95 : /* Inner query should follow */
96 1 : uint32_t inner_crc = tl_read_uint32(&r);
97 1 : ASSERT(inner_crc == 0xDEADBEEF, "inner query constructor should be at the end");
98 : }
99 :
100 1 : void test_api_wrap_query_null_args(void) {
101 : ApiConfig cfg;
102 1 : api_config_init(&cfg);
103 1 : uint8_t query[4] = {0};
104 : uint8_t out[256];
105 : size_t out_len;
106 :
107 1 : ASSERT(api_wrap_query(NULL, query, 4, out, 256, &out_len) == -1, "NULL cfg");
108 1 : ASSERT(api_wrap_query(&cfg, NULL, 4, out, 256, &out_len) == -1, "NULL query");
109 1 : ASSERT(api_wrap_query(&cfg, query, 4, NULL, 256, &out_len) == -1, "NULL out");
110 1 : ASSERT(api_wrap_query(&cfg, query, 4, out, 256, NULL) == -1, "NULL out_len");
111 : }
112 :
113 1 : void test_api_wrap_query_buffer_too_small(void) {
114 : ApiConfig cfg;
115 1 : api_config_init(&cfg);
116 1 : cfg.api_id = 1;
117 :
118 1 : uint8_t query[4] = {0};
119 : uint8_t out[8]; /* way too small */
120 : size_t out_len;
121 :
122 1 : int rc = api_wrap_query(&cfg, query, 4, out, 8, &out_len);
123 1 : ASSERT(rc == -1, "should fail with buffer too small");
124 : }
125 :
126 : /* ---- bad_server_salt retry ---- */
127 :
128 : #include "mock_socket.h"
129 : #include "mtproto_session.h"
130 : #include "transport.h"
131 : #include "tl_registry.h"
132 :
133 8 : static void pack_encrypted(const uint8_t *payload, size_t plen,
134 : uint8_t *out, size_t *out_len) {
135 8 : TlWriter w; tl_writer_init(&w);
136 8 : uint8_t z24[24] = {0}; tl_write_raw(&w, z24, 24);
137 8 : uint8_t hdr[32] = {0};
138 8 : uint32_t pl32 = (uint32_t)plen;
139 8 : memcpy(hdr + 28, &pl32, 4);
140 8 : tl_write_raw(&w, hdr, 32);
141 8 : tl_write_raw(&w, payload, plen);
142 8 : size_t enc = w.len - 24;
143 8 : if (enc % 16 != 0) {
144 8 : uint8_t pad[16] = {0}; tl_write_raw(&w, pad, 16 - (enc % 16));
145 : }
146 8 : out[0] = (uint8_t)(w.len / 4);
147 8 : memcpy(out + 1, w.data, w.len);
148 8 : *out_len = 1 + w.len;
149 8 : tl_writer_free(&w);
150 8 : }
151 :
152 1 : static void test_bad_server_salt_retry(void) {
153 1 : mock_socket_reset();
154 1 : mock_crypto_reset();
155 :
156 : /* First response: bad_server_salt. */
157 : uint8_t bad_payload[64];
158 1 : memset(bad_payload, 0, sizeof(bad_payload));
159 1 : uint32_t crc = TL_bad_server_salt;
160 1 : memcpy(bad_payload, &crc, 4);
161 : /* bad_msg_id (8) + bad_msg_seqno (4) + error_code (4) zeros */
162 1 : uint64_t new_salt = 0x9988776655443322ULL;
163 1 : memcpy(bad_payload + 20, &new_salt, 8);
164 :
165 1 : uint8_t resp1[256]; size_t rlen1 = 0;
166 1 : pack_encrypted(bad_payload, 28, resp1, &rlen1);
167 :
168 : /* Second response (post-retry): a valid rpc_result carrying bool_true. */
169 : uint8_t ok_payload[32];
170 1 : TlWriter w2; tl_writer_init(&w2);
171 1 : tl_write_uint32(&w2, TL_boolTrue);
172 1 : memcpy(ok_payload, w2.data, w2.len);
173 1 : size_t ok_plen = w2.len;
174 1 : tl_writer_free(&w2);
175 :
176 1 : uint8_t resp2[256]; size_t rlen2 = 0;
177 1 : pack_encrypted(ok_payload, ok_plen, resp2, &rlen2);
178 :
179 1 : mock_socket_set_response(resp1, rlen1);
180 1 : mock_socket_append_response(resp2, rlen2);
181 :
182 : MtProtoSession s;
183 1 : mtproto_session_init(&s);
184 1 : s.session_id = 0; /* match the zero session_id in fake encrypted frames */
185 1 : uint8_t key[256] = {0};
186 1 : mtproto_session_set_auth_key(&s, key);
187 1 : mtproto_session_set_salt(&s, 0x1111111111111111ULL);
188 :
189 : Transport t;
190 1 : transport_init(&t); t.fd = 42; t.connected = 1; t.dc_id = 1;
191 :
192 1 : ApiConfig cfg; api_config_init(&cfg);
193 1 : cfg.api_id = 12345; cfg.api_hash = "deadbeef";
194 :
195 : uint8_t query[8];
196 1 : memset(query, 0, sizeof(query));
197 1 : crc = TL_boolTrue; /* any tiny query */
198 1 : memcpy(query, &crc, 4);
199 :
200 : uint8_t resp[128];
201 1 : size_t resp_len = 0;
202 1 : int rc = api_call(&cfg, &s, &t, query, 4, resp, sizeof(resp), &resp_len);
203 :
204 1 : ASSERT(rc == 0, "api_call succeeds after bad_salt retry");
205 1 : ASSERT(s.server_salt == 0x9988776655443322ULL,
206 : "salt updated from bad_server_salt");
207 : }
208 :
209 : /* ---- new_session_created skip ---- */
210 1 : static void test_new_session_created_skipped(void) {
211 1 : mock_socket_reset();
212 1 : mock_crypto_reset();
213 :
214 : /* First response: new_session_created — should be silently swallowed. */
215 : uint8_t ns_payload[32];
216 1 : memset(ns_payload, 0, sizeof(ns_payload));
217 1 : uint32_t ns_crc = TL_new_session_created;
218 1 : memcpy(ns_payload, &ns_crc, 4);
219 : /* first_msg_id (8) + unique_id (8) + server_salt (8) — put a
220 : * recognisable salt at offset 20. */
221 1 : uint64_t salt = 0xAABBCCDD00112233ULL;
222 1 : memcpy(ns_payload + 20, &salt, 8);
223 :
224 1 : uint8_t resp_ns[256]; size_t rlen_ns = 0;
225 1 : pack_encrypted(ns_payload, 28, resp_ns, &rlen_ns);
226 :
227 : /* Second response: real bool_true. */
228 1 : TlWriter w; tl_writer_init(&w);
229 1 : tl_write_uint32(&w, TL_boolTrue);
230 1 : uint8_t ok_pay[8]; memcpy(ok_pay, w.data, w.len);
231 1 : size_t ok_plen = w.len; tl_writer_free(&w);
232 :
233 1 : uint8_t resp_ok[256]; size_t rlen_ok = 0;
234 1 : pack_encrypted(ok_pay, ok_plen, resp_ok, &rlen_ok);
235 :
236 1 : mock_socket_set_response(resp_ns, rlen_ns);
237 1 : mock_socket_append_response(resp_ok, rlen_ok);
238 :
239 : MtProtoSession s;
240 1 : mtproto_session_init(&s);
241 1 : s.session_id = 0; /* match the zero session_id in fake encrypted frames */
242 1 : uint8_t key[256] = {0};
243 1 : mtproto_session_set_auth_key(&s, key);
244 1 : mtproto_session_set_salt(&s, 0x1);
245 :
246 : Transport t;
247 1 : transport_init(&t); t.fd = 42; t.connected = 1; t.dc_id = 1;
248 :
249 1 : ApiConfig cfg; api_config_init(&cfg);
250 1 : cfg.api_id = 12345; cfg.api_hash = "deadbeef";
251 :
252 1 : uint32_t q_crc = TL_boolTrue;
253 1 : uint8_t query[4]; memcpy(query, &q_crc, 4);
254 :
255 1 : uint8_t resp[64]; size_t resp_len = 0;
256 1 : int rc = api_call(&cfg, &s, &t, query, 4, resp, sizeof(resp), &resp_len);
257 1 : ASSERT(rc == 0, "new_session_created was skipped cleanly");
258 1 : ASSERT(s.server_salt == 0xAABBCCDD00112233ULL,
259 : "salt taken from new_session_created");
260 : }
261 :
262 : /* ---- bad_msg_notification tests ---- */
263 :
264 : /** Helper: build a minimal bad_msg_notification payload of given length. */
265 2 : static void build_bad_msg_payload(uint8_t *buf, size_t len, int32_t error_code) {
266 2 : memset(buf, 0, len);
267 2 : uint32_t crc = TL_bad_msg_notification;
268 2 : memcpy(buf, &crc, 4);
269 : /* bad_msg_id (8 bytes) at offset 4 — zeros */
270 : /* bad_msg_seqno (4 bytes) at offset 12 — zeros */
271 2 : if (len >= 20) memcpy(buf + 16, &error_code, 4);
272 2 : }
273 :
274 : /** Test 1: bad_msg_notification as first response returns -1. */
275 1 : static void test_bad_msg_notification_first_response(void) {
276 1 : mock_socket_reset();
277 1 : mock_crypto_reset();
278 :
279 : uint8_t payload[20];
280 1 : build_bad_msg_payload(payload, sizeof(payload), 16 /* msg_id too low */);
281 :
282 1 : uint8_t resp[256]; size_t rlen = 0;
283 1 : pack_encrypted(payload, sizeof(payload), resp, &rlen);
284 1 : mock_socket_set_response(resp, rlen);
285 :
286 : MtProtoSession s;
287 1 : mtproto_session_init(&s);
288 1 : s.session_id = 0;
289 1 : uint8_t key[256] = {0};
290 1 : mtproto_session_set_auth_key(&s, key);
291 :
292 : Transport t;
293 1 : transport_init(&t); t.fd = 42; t.connected = 1; t.dc_id = 1;
294 :
295 1 : ApiConfig cfg; api_config_init(&cfg);
296 1 : cfg.api_id = 12345; cfg.api_hash = "deadbeef";
297 :
298 1 : uint32_t q_crc = TL_boolTrue;
299 1 : uint8_t query[4]; memcpy(query, &q_crc, 4);
300 1 : uint8_t out[64]; size_t out_len = 0;
301 :
302 1 : int rc = api_call(&cfg, &s, &t, query, 4, out, sizeof(out), &out_len);
303 1 : ASSERT(rc == -1, "api_call returns -1 on bad_msg_notification");
304 : }
305 :
306 : /** Test 2: bad_msg_notification after a msgs_ack service frame → bails. */
307 1 : static void test_bad_msg_notification_after_msgs_ack(void) {
308 1 : mock_socket_reset();
309 1 : mock_crypto_reset();
310 :
311 : /* First response: msgs_ack (should be silently skipped). */
312 : uint8_t ack_payload[8];
313 1 : memset(ack_payload, 0, sizeof(ack_payload));
314 1 : uint32_t ack_crc = TL_msgs_ack;
315 1 : memcpy(ack_payload, &ack_crc, 4);
316 1 : uint8_t resp1[256]; size_t rlen1 = 0;
317 1 : pack_encrypted(ack_payload, sizeof(ack_payload), resp1, &rlen1);
318 :
319 : /* Second response: bad_msg_notification. */
320 : uint8_t bm_payload[20];
321 1 : build_bad_msg_payload(bm_payload, sizeof(bm_payload), 32 /* seqno too low */);
322 1 : uint8_t resp2[256]; size_t rlen2 = 0;
323 1 : pack_encrypted(bm_payload, sizeof(bm_payload), resp2, &rlen2);
324 :
325 1 : mock_socket_set_response(resp1, rlen1);
326 1 : mock_socket_append_response(resp2, rlen2);
327 :
328 : MtProtoSession s;
329 1 : mtproto_session_init(&s);
330 1 : s.session_id = 0;
331 1 : uint8_t key[256] = {0};
332 1 : mtproto_session_set_auth_key(&s, key);
333 :
334 : Transport t;
335 1 : transport_init(&t); t.fd = 42; t.connected = 1; t.dc_id = 1;
336 :
337 1 : ApiConfig cfg; api_config_init(&cfg);
338 1 : cfg.api_id = 12345; cfg.api_hash = "deadbeef";
339 :
340 1 : uint32_t q_crc = TL_boolTrue;
341 1 : uint8_t query[4]; memcpy(query, &q_crc, 4);
342 1 : uint8_t out[64]; size_t out_len = 0;
343 :
344 1 : int rc = api_call(&cfg, &s, &t, query, 4, out, sizeof(out), &out_len);
345 1 : ASSERT(rc == -1, "api_call returns -1 after msgs_ack + bad_msg_notification");
346 : }
347 :
348 : /** Test 3: bad_msg_notification with resp_len < 20 (no error_code) → SVC_ERROR. */
349 1 : static void test_bad_msg_notification_short_frame(void) {
350 1 : mock_socket_reset();
351 1 : mock_crypto_reset();
352 :
353 : /* Only 4 bytes: just the CRC, no fields — triggers the < 20 guard. */
354 : uint8_t short_payload[4];
355 1 : uint32_t crc = TL_bad_msg_notification;
356 1 : memcpy(short_payload, &crc, 4);
357 :
358 1 : uint8_t resp[256]; size_t rlen = 0;
359 1 : pack_encrypted(short_payload, sizeof(short_payload), resp, &rlen);
360 1 : mock_socket_set_response(resp, rlen);
361 :
362 : MtProtoSession s;
363 1 : mtproto_session_init(&s);
364 1 : s.session_id = 0;
365 1 : uint8_t key[256] = {0};
366 1 : mtproto_session_set_auth_key(&s, key);
367 :
368 : Transport t;
369 1 : transport_init(&t); t.fd = 42; t.connected = 1; t.dc_id = 1;
370 :
371 1 : ApiConfig cfg; api_config_init(&cfg);
372 1 : cfg.api_id = 12345; cfg.api_hash = "deadbeef";
373 :
374 1 : uint32_t q_crc = TL_boolTrue;
375 1 : uint8_t query[4]; memcpy(query, &q_crc, 4);
376 1 : uint8_t out[64]; size_t out_len = 0;
377 :
378 1 : int rc = api_call(&cfg, &s, &t, query, 4, out, sizeof(out), &out_len);
379 1 : ASSERT(rc == -1, "api_call returns -1 on truncated bad_msg_notification");
380 : }
381 :
382 1 : void test_api_call(void) {
383 1 : RUN_TEST(test_api_config_init);
384 1 : RUN_TEST(test_api_wrap_query_structure);
385 1 : RUN_TEST(test_api_wrap_query_null_args);
386 1 : RUN_TEST(test_api_wrap_query_buffer_too_small);
387 1 : RUN_TEST(test_bad_server_salt_retry);
388 1 : RUN_TEST(test_new_session_created_skipped);
389 1 : RUN_TEST(test_bad_msg_notification_first_response);
390 1 : RUN_TEST(test_bad_msg_notification_after_msgs_ack);
391 1 : RUN_TEST(test_bad_msg_notification_short_frame);
392 1 : }
|