Line data Source code
1 : /**
2 : * @file test_domain_media.c
3 : * @brief Unit tests for domain_download_photo (P6-01).
4 : */
5 :
6 : #include "test_helpers.h"
7 : #include "domain/read/media.h"
8 : #include "tl_serial.h"
9 : #include "tl_registry.h"
10 : #include "tl_skip.h"
11 : #include "mock_socket.h"
12 : #include "mock_crypto.h"
13 : #include "mtproto_session.h"
14 : #include "transport.h"
15 : #include "api_call.h"
16 :
17 : #include <stdio.h>
18 : #include <stdlib.h>
19 : #include <string.h>
20 : #include <unistd.h>
21 :
22 : /* Build an encrypted frame compatible with mtproto_rpc.c's decrypt path
23 : * (same recipe used by test_domain_history.c). */
24 7 : static void build_fake_encrypted_response(const uint8_t *payload, size_t plen,
25 : uint8_t *out, size_t *out_len) {
26 7 : TlWriter w; tl_writer_init(&w);
27 7 : uint8_t zeros24[24] = {0}; tl_write_raw(&w, zeros24, 24);
28 7 : uint8_t header[32] = {0};
29 7 : uint32_t plen32 = (uint32_t)plen;
30 7 : memcpy(header + 28, &plen32, 4);
31 7 : tl_write_raw(&w, header, 32);
32 7 : tl_write_raw(&w, payload, plen);
33 7 : size_t enc = w.len - 24;
34 7 : if (enc % 16 != 0) {
35 3 : uint8_t pad[16] = {0}; tl_write_raw(&w, pad, 16 - (enc % 16));
36 : }
37 7 : out[0] = (uint8_t)(w.len / 4);
38 7 : memcpy(out + 1, w.data, w.len);
39 7 : *out_len = 1 + w.len;
40 7 : tl_writer_free(&w);
41 7 : }
42 :
43 11 : static void fix_session(MtProtoSession *s) {
44 11 : mtproto_session_init(s);
45 11 : s->session_id = 0; /* match the zero session_id in fake encrypted frames */
46 11 : uint8_t k[256] = {0}; mtproto_session_set_auth_key(s, k);
47 11 : mtproto_session_set_salt(s, 0xBADCAFEDEADBEEFULL);
48 11 : }
49 11 : static void fix_transport(Transport *t) {
50 11 : transport_init(t); t->fd = 42; t->connected = 1; t->dc_id = 1;
51 11 : }
52 11 : static void fix_cfg(ApiConfig *cfg) {
53 11 : api_config_init(cfg); cfg->api_id = 12345; cfg->api_hash = "deadbeef";
54 11 : }
55 :
56 : #define CRC_upload_file 0x096a18d5u
57 : #define CRC_storage_fileJpeg 0x007efe0eu
58 :
59 : /* Build one upload.file response carrying @p body_len bytes. */
60 5 : static size_t make_upload_file(uint8_t *buf, size_t max,
61 : const uint8_t *body, size_t body_len) {
62 5 : TlWriter w; tl_writer_init(&w);
63 5 : tl_write_uint32(&w, CRC_upload_file);
64 5 : tl_write_uint32(&w, CRC_storage_fileJpeg);
65 5 : tl_write_int32 (&w, 1700000000); /* mtime */
66 5 : tl_write_bytes (&w, body, body_len);
67 5 : size_t n = w.len < max ? w.len : max;
68 5 : memcpy(buf, w.data, n);
69 5 : tl_writer_free(&w);
70 5 : return n;
71 : }
72 :
73 7 : static MediaInfo fake_photo_info(void) {
74 7 : MediaInfo m = {0};
75 7 : m.kind = MEDIA_PHOTO;
76 7 : m.photo_id = 123456789LL;
77 7 : m.access_hash = 0xDEADBEEFCAFEBABEULL;
78 7 : m.dc_id = 1;
79 7 : m.file_reference_len = 4;
80 7 : m.file_reference[0] = 0xAA;
81 7 : m.file_reference[1] = 0xBB;
82 7 : m.file_reference[2] = 0xCC;
83 7 : m.file_reference[3] = 0xDD;
84 7 : strncpy(m.thumb_type, "y", sizeof(m.thumb_type) - 1);
85 7 : return m;
86 : }
87 :
88 : /* Single-chunk download: body smaller than CHUNK_SIZE, loop exits after
89 : * one iteration. */
90 1 : static void test_download_photo_single_chunk(void) {
91 1 : mock_socket_reset(); mock_crypto_reset();
92 :
93 1 : const char *path = "/tmp/tg-cli-media-test.jpg";
94 1 : unlink(path);
95 :
96 : uint8_t body[128];
97 129 : for (size_t i = 0; i < sizeof(body); i++) body[i] = (uint8_t)(i ^ 0x5A);
98 :
99 : uint8_t payload[512];
100 1 : size_t plen = make_upload_file(payload, sizeof(payload),
101 : body, sizeof(body));
102 1 : uint8_t resp[1024]; size_t rlen = 0;
103 1 : build_fake_encrypted_response(payload, plen, resp, &rlen);
104 1 : mock_socket_set_response(resp, rlen);
105 :
106 : MtProtoSession s; Transport t; ApiConfig cfg;
107 1 : fix_session(&s); fix_transport(&t); fix_cfg(&cfg);
108 :
109 1 : MediaInfo info = fake_photo_info();
110 1 : int wrong_dc = -1;
111 1 : int rc = domain_download_photo(&cfg, &s, &t, &info, path, &wrong_dc);
112 1 : ASSERT(rc == 0, "single chunk download succeeds");
113 1 : ASSERT(wrong_dc == 0, "wrong_dc stays 0 on success");
114 :
115 1 : FILE *fp = fopen(path, "rb");
116 1 : ASSERT(fp != NULL, "file created on disk");
117 1 : if (fp) {
118 1 : uint8_t got[256] = {0};
119 1 : size_t n = fread(got, 1, sizeof(got), fp);
120 1 : fclose(fp);
121 1 : ASSERT(n == sizeof(body), "file size matches body");
122 1 : ASSERT(memcmp(got, body, sizeof(body)) == 0, "body round-trips");
123 : }
124 1 : unlink(path);
125 : }
126 :
127 1 : static void test_download_photo_rpc_error_migrate(void) {
128 1 : mock_socket_reset(); mock_crypto_reset();
129 :
130 1 : const char *path = "/tmp/tg-cli-media-migrate.jpg";
131 1 : unlink(path);
132 :
133 : /* Craft an RPC error with FILE_MIGRATE_2. */
134 : uint8_t payload[128];
135 1 : TlWriter w; tl_writer_init(&w);
136 1 : tl_write_uint32(&w, TL_rpc_error);
137 1 : tl_write_int32 (&w, 303);
138 1 : tl_write_string(&w, "FILE_MIGRATE_2");
139 1 : memcpy(payload, w.data, w.len);
140 1 : size_t plen = w.len;
141 1 : tl_writer_free(&w);
142 :
143 1 : uint8_t resp[512]; size_t rlen = 0;
144 1 : build_fake_encrypted_response(payload, plen, resp, &rlen);
145 1 : mock_socket_set_response(resp, rlen);
146 :
147 : MtProtoSession s; Transport t; ApiConfig cfg;
148 1 : fix_session(&s); fix_transport(&t); fix_cfg(&cfg);
149 :
150 1 : MediaInfo info = fake_photo_info();
151 1 : int wrong_dc = 0;
152 1 : int rc = domain_download_photo(&cfg, &s, &t, &info, path, &wrong_dc);
153 1 : ASSERT(rc != 0, "RPC error propagates");
154 1 : ASSERT(wrong_dc == 2, "migrate_dc extracted from FILE_MIGRATE_2");
155 1 : unlink(path);
156 : }
157 :
158 1 : static void test_download_photo_rejects_non_photo(void) {
159 : ApiConfig cfg; MtProtoSession s; Transport t;
160 1 : fix_session(&s); fix_transport(&t); fix_cfg(&cfg);
161 :
162 1 : MediaInfo info = fake_photo_info();
163 1 : info.kind = MEDIA_DOCUMENT;
164 1 : int wrong_dc = 0;
165 1 : int rc = domain_download_photo(&cfg, &s, &t, &info,
166 : "/tmp/tg-cli-media-x.jpg", &wrong_dc);
167 1 : ASSERT(rc != 0, "non-photo kind rejected");
168 : }
169 :
170 1 : static void test_download_photo_null_args(void) {
171 1 : int wrong_dc = 0;
172 1 : ASSERT(domain_download_photo(NULL, NULL, NULL, NULL, NULL, &wrong_dc) == -1,
173 : "null args rejected");
174 : }
175 :
176 1 : static void test_download_photo_requires_credentials(void) {
177 : ApiConfig cfg; MtProtoSession s; Transport t;
178 1 : fix_session(&s); fix_transport(&t); fix_cfg(&cfg);
179 :
180 1 : MediaInfo info = {0};
181 1 : info.kind = MEDIA_PHOTO;
182 : /* photo_id=0 means info is incomplete — must fail before any RPC. */
183 1 : int wrong_dc = 0;
184 1 : int rc = domain_download_photo(&cfg, &s, &t, &info,
185 : "/tmp/tg-cli-media-x.jpg", &wrong_dc);
186 1 : ASSERT(rc != 0, "empty MediaInfo rejected");
187 : }
188 :
189 : /* ---- Document download ---- */
190 :
191 3 : static MediaInfo fake_document_info(void) {
192 3 : MediaInfo m = {0};
193 3 : m.kind = MEDIA_DOCUMENT;
194 3 : m.document_id = 5551212LL;
195 3 : m.access_hash = 0xFEEDFACE01234567LL;
196 3 : m.dc_id = 2;
197 3 : m.file_reference_len = 5;
198 3 : uint8_t fr[5] = {0xDE,0xAD,0xBE,0xEF,0x01};
199 3 : memcpy(m.file_reference, fr, 5);
200 3 : m.document_size = 64;
201 3 : snprintf(m.document_mime, sizeof(m.document_mime), "%s", "application/pdf");
202 3 : snprintf(m.document_filename, sizeof(m.document_filename), "%s",
203 : "report.pdf");
204 3 : return m;
205 : }
206 :
207 1 : static void test_download_document_single_chunk(void) {
208 1 : mock_socket_reset(); mock_crypto_reset();
209 1 : const char *path = "/tmp/tg-cli-media-doc-test.bin";
210 1 : unlink(path);
211 :
212 : uint8_t body[64];
213 65 : for (size_t i = 0; i < sizeof(body); i++) body[i] = (uint8_t)(i ^ 0x3C);
214 :
215 : uint8_t payload[256];
216 1 : size_t plen = make_upload_file(payload, sizeof(payload),
217 : body, sizeof(body));
218 1 : uint8_t resp[512]; size_t rlen = 0;
219 1 : build_fake_encrypted_response(payload, plen, resp, &rlen);
220 1 : mock_socket_set_response(resp, rlen);
221 :
222 : MtProtoSession s; Transport t; ApiConfig cfg;
223 1 : fix_session(&s); fix_transport(&t); fix_cfg(&cfg);
224 1 : MediaInfo info = fake_document_info();
225 1 : int wrong_dc = -1;
226 1 : int rc = domain_download_document(&cfg, &s, &t, &info, path, &wrong_dc);
227 1 : ASSERT(rc == 0, "document download ok");
228 1 : ASSERT(wrong_dc == 0, "wrong_dc stays 0");
229 :
230 1 : FILE *fp = fopen(path, "rb");
231 1 : ASSERT(fp != NULL, "doc file created");
232 1 : if (fp) {
233 1 : uint8_t got[128] = {0};
234 1 : size_t n = fread(got, 1, sizeof(got), fp);
235 1 : fclose(fp);
236 1 : ASSERT(n == sizeof(body), "doc size matches");
237 1 : ASSERT(memcmp(got, body, sizeof(body)) == 0, "doc round-trips");
238 : }
239 1 : unlink(path);
240 : }
241 :
242 1 : static void test_download_document_wire_has_doc_location_crc(void) {
243 1 : mock_socket_reset(); mock_crypto_reset();
244 1 : const char *path = "/tmp/tg-cli-media-doc-wire.bin";
245 1 : unlink(path);
246 :
247 : uint8_t body[16];
248 17 : for (size_t i = 0; i < sizeof(body); i++) body[i] = (uint8_t)(i + 1);
249 : uint8_t payload[128];
250 1 : size_t plen = make_upload_file(payload, sizeof(payload),
251 : body, sizeof(body));
252 1 : uint8_t resp[256]; size_t rlen = 0;
253 1 : build_fake_encrypted_response(payload, plen, resp, &rlen);
254 1 : mock_socket_set_response(resp, rlen);
255 :
256 : MtProtoSession s; Transport t; ApiConfig cfg;
257 1 : fix_session(&s); fix_transport(&t); fix_cfg(&cfg);
258 1 : MediaInfo info = fake_document_info();
259 1 : int rc = domain_download_document(&cfg, &s, &t, &info, path, NULL);
260 1 : ASSERT(rc == 0, "download ok");
261 :
262 1 : size_t sent_len = 0;
263 1 : const uint8_t *sent = mock_socket_get_sent(&sent_len);
264 1 : uint32_t want = 0xbad07584u; /* inputDocumentFileLocation */
265 1 : int found = 0;
266 122 : for (size_t i = 0; i + 4 <= sent_len; i++)
267 122 : if (memcmp(sent + i, &want, 4) == 0) { found = 1; break; }
268 1 : ASSERT(found, "inputDocumentFileLocation CRC on wire");
269 1 : unlink(path);
270 : }
271 :
272 1 : static void test_download_document_rejects_non_document(void) {
273 : ApiConfig cfg; MtProtoSession s; Transport t;
274 1 : fix_session(&s); fix_transport(&t); fix_cfg(&cfg);
275 1 : MediaInfo info = fake_photo_info(); /* PHOTO, not DOC */
276 1 : int rc = domain_download_document(&cfg, &s, &t, &info,
277 : "/tmp/tg-cli-media-x.bin", NULL);
278 1 : ASSERT(rc != 0, "photo rejected for document download");
279 : }
280 :
281 : /* Verify that the wire payload for inputDocumentFileLocation contains the
282 : * document id and access_hash bytes after the CRC.
283 : * Layout: CRC(4) id:long(8) access_hash:long(8) file_reference:bytes thumb_size:string */
284 1 : static void test_download_document_wire_id_and_hash(void) {
285 1 : mock_socket_reset(); mock_crypto_reset();
286 1 : const char *path = "/tmp/tg-cli-media-doc-idcheck.bin";
287 1 : unlink(path);
288 :
289 1 : uint8_t body[8] = {0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08};
290 : uint8_t payload[128];
291 1 : size_t plen = make_upload_file(payload, sizeof(payload), body, sizeof(body));
292 1 : uint8_t resp[256]; size_t rlen = 0;
293 1 : build_fake_encrypted_response(payload, plen, resp, &rlen);
294 1 : mock_socket_set_response(resp, rlen);
295 :
296 : MtProtoSession s; Transport t; ApiConfig cfg;
297 1 : fix_session(&s); fix_transport(&t); fix_cfg(&cfg);
298 1 : MediaInfo info = fake_document_info(); /* document_id=5551212, access_hash=0xFEEDFACE01234567 */
299 1 : int rc = domain_download_document(&cfg, &s, &t, &info, path, NULL);
300 1 : ASSERT(rc == 0, "download ok for id/hash check");
301 :
302 1 : size_t sent_len = 0;
303 1 : const uint8_t *sent = mock_socket_get_sent(&sent_len);
304 :
305 : /* Find the inputDocumentFileLocation CRC in the sent bytes. */
306 1 : uint32_t crc = 0xbad07584u;
307 1 : size_t crc_pos = sent_len;
308 122 : for (size_t i = 0; i + 4 <= sent_len; i++) {
309 122 : if (memcmp(sent + i, &crc, 4) == 0) { crc_pos = i; break; }
310 : }
311 1 : ASSERT(crc_pos + 4 + 8 + 8 <= sent_len, "id+hash bytes present after CRC");
312 :
313 : /* id follows immediately after the CRC (little-endian int64). */
314 1 : int64_t wire_id = 0;
315 1 : memcpy(&wire_id, sent + crc_pos + 4, 8);
316 1 : ASSERT(wire_id == info.document_id, "document id serialized correctly");
317 :
318 : /* access_hash follows id. */
319 1 : int64_t wire_hash = 0;
320 1 : memcpy(&wire_hash, sent + crc_pos + 12, 8);
321 1 : ASSERT(wire_hash == (int64_t)info.access_hash, "access_hash serialized correctly");
322 :
323 1 : unlink(path);
324 : }
325 :
326 : /* ---- Cross-DC wrapper ---- */
327 :
328 1 : static void test_cross_dc_null_args(void) {
329 : ApiConfig cfg; MtProtoSession s; Transport t;
330 1 : fix_session(&s); fix_transport(&t); fix_cfg(&cfg);
331 1 : MediaInfo info = fake_photo_info();
332 1 : ASSERT(domain_download_media_cross_dc(NULL, &s, &t, &info, "/tmp/x") == -1,
333 : "null cfg rejected");
334 1 : ASSERT(domain_download_media_cross_dc(&cfg, NULL, &t, &info, "/tmp/x") == -1,
335 : "null session rejected");
336 1 : ASSERT(domain_download_media_cross_dc(&cfg, &s, NULL, &info, "/tmp/x") == -1,
337 : "null transport rejected");
338 1 : ASSERT(domain_download_media_cross_dc(&cfg, &s, &t, NULL, "/tmp/x") == -1,
339 : "null info rejected");
340 1 : ASSERT(domain_download_media_cross_dc(&cfg, &s, &t, &info, NULL) == -1,
341 : "null path rejected");
342 : }
343 :
344 : /* Happy path: home DC succeeds on the first attempt, no DcSession ever
345 : * gets opened. */
346 1 : static void test_cross_dc_happy_path_no_migration(void) {
347 1 : mock_socket_reset(); mock_crypto_reset();
348 :
349 1 : const char *path = "/tmp/tg-cli-media-xdc-happy.jpg";
350 1 : unlink(path);
351 :
352 : uint8_t body[64];
353 65 : for (size_t i = 0; i < sizeof(body); i++) body[i] = (uint8_t)(0xA0 + i);
354 :
355 : uint8_t payload[256];
356 1 : size_t plen = make_upload_file(payload, sizeof(payload),
357 : body, sizeof(body));
358 1 : uint8_t resp[512]; size_t rlen = 0;
359 1 : build_fake_encrypted_response(payload, plen, resp, &rlen);
360 1 : mock_socket_set_response(resp, rlen);
361 :
362 : MtProtoSession s; Transport t; ApiConfig cfg;
363 1 : fix_session(&s); fix_transport(&t); fix_cfg(&cfg);
364 :
365 1 : MediaInfo info = fake_photo_info();
366 1 : int rc = domain_download_media_cross_dc(&cfg, &s, &t, &info, path);
367 1 : ASSERT(rc == 0, "cross-dc wrapper succeeds on home DC");
368 :
369 1 : FILE *fp = fopen(path, "rb");
370 1 : ASSERT(fp != NULL, "file created on disk");
371 1 : if (fp) fclose(fp);
372 1 : unlink(path);
373 : }
374 :
375 : /* Non-migrate failure (e.g. FLOOD_WAIT) must NOT trigger a secondary DC
376 : * open — the wrapper just returns -1. */
377 1 : static void test_cross_dc_non_migrate_failure_bails(void) {
378 1 : mock_socket_reset(); mock_crypto_reset();
379 :
380 1 : const char *path = "/tmp/tg-cli-media-xdc-bail.jpg";
381 1 : unlink(path);
382 :
383 : uint8_t payload[128];
384 1 : TlWriter w; tl_writer_init(&w);
385 1 : tl_write_uint32(&w, TL_rpc_error);
386 1 : tl_write_int32 (&w, 420);
387 1 : tl_write_string(&w, "FLOOD_WAIT_10");
388 1 : memcpy(payload, w.data, w.len);
389 1 : size_t plen = w.len;
390 1 : tl_writer_free(&w);
391 :
392 1 : uint8_t resp[512]; size_t rlen = 0;
393 1 : build_fake_encrypted_response(payload, plen, resp, &rlen);
394 1 : mock_socket_set_response(resp, rlen);
395 :
396 : MtProtoSession s; Transport t; ApiConfig cfg;
397 1 : fix_session(&s); fix_transport(&t); fix_cfg(&cfg);
398 1 : int creates_before = mock_socket_was_created();
399 :
400 1 : MediaInfo info = fake_photo_info();
401 1 : int rc = domain_download_media_cross_dc(&cfg, &s, &t, &info, path);
402 1 : ASSERT(rc == -1, "non-migrate failure returns -1");
403 :
404 : /* dc_session_open would create a new socket; for FLOOD_WAIT we must
405 : * not. The mock exposes a cumulative creation counter. */
406 1 : ASSERT(mock_socket_was_created() == creates_before,
407 : "no new socket created for non-migrate error");
408 1 : unlink(path);
409 : }
410 :
411 1 : void run_domain_media_tests(void) {
412 1 : RUN_TEST(test_download_photo_single_chunk);
413 1 : RUN_TEST(test_download_photo_rpc_error_migrate);
414 1 : RUN_TEST(test_download_photo_rejects_non_photo);
415 1 : RUN_TEST(test_download_photo_null_args);
416 1 : RUN_TEST(test_download_photo_requires_credentials);
417 1 : RUN_TEST(test_download_document_single_chunk);
418 1 : RUN_TEST(test_download_document_wire_has_doc_location_crc);
419 1 : RUN_TEST(test_download_document_rejects_non_document);
420 1 : RUN_TEST(test_download_document_wire_id_and_hash);
421 1 : RUN_TEST(test_cross_dc_null_args);
422 1 : RUN_TEST(test_cross_dc_happy_path_no_migration);
423 1 : RUN_TEST(test_cross_dc_non_migrate_failure_bails);
424 1 : }
|