Line data Source code
1 : /**
2 : * @file test_rich_media_types.c
3 : * @brief TEST-73 — functional coverage for rich media types.
4 : *
5 : * US-22 lists nine MessageMedia variants whose parsing is silently
6 : * dropped or mislabelled today: video, audio, voice, sticker,
7 : * animation (GIF), round-video, geo, contact, poll, webpage. Unit
8 : * tests in test_tl_skip_message_functional.c exercise the skippers
9 : * per-variant; this suite drives the production `domain_get_history`
10 : * end-to-end through the in-process mock server so the full
11 : * TL_messages_messages → Vector<Message> → MessageMedia chain is
12 : * walked with real OpenSSL on both sides (same pattern as the
13 : * TEST-79 sibling test_history_rich_metadata.c).
14 : *
15 : * For each variant we assert:
16 : * - domain_get_history does NOT bail (rows[0].text stays intact)
17 : * - HistoryEntry.media is set to the correct MediaKind
18 : * - MEDIA_PHOTO / MEDIA_DOCUMENT also expose media_id + media_dc +
19 : * media_info metadata (document_mime, document_filename, size)
20 : *
21 : * US-22 "printed label" assertions (e.g. "[video WxH Ds BYTES]") are
22 : * intentionally NOT made here because the domain layer currently
23 : * stores only the MediaKind enum, not a rendered label string —
24 : * closing that gap is the US-22 prod change, out of scope for a
25 : * test-only ticket.
26 : *
27 : * The suite also exercises the download-path error branches of
28 : * media.c that test_upload_download.c does not yet cover (invalid
29 : * MediaInfo and `download_any` dispatch through
30 : * domain_download_media_cross_dc for a non-photo/document kind) to
31 : * push functional coverage of media.c past the 63 % baseline.
32 : */
33 :
34 : #include "test_helpers.h"
35 :
36 : #include "mock_socket.h"
37 : #include "mock_tel_server.h"
38 :
39 : #include "api_call.h"
40 : #include "mtproto_session.h"
41 : #include "transport.h"
42 : #include "app/session_store.h"
43 : #include "tl_registry.h"
44 : #include "tl_serial.h"
45 : #include "tl_skip.h"
46 :
47 : #include "domain/read/history.h"
48 : #include "domain/read/media.h"
49 :
50 : #include <stdio.h>
51 : #include <stdlib.h>
52 : #include <string.h>
53 : #include <sys/stat.h>
54 : #include <unistd.h>
55 :
56 : /* ---- CRCs not re-exposed from public headers ---- */
57 : #define CRC_messages_getHistory 0x4423e6c5U
58 : #define CRC_upload_getFile 0xbe5335beU
59 : #define CRC_upload_file 0x096a18d5U
60 : #define CRC_storage_filePartial 0x40bc6f52U
61 :
62 : #define CRC_messageMediaEmpty 0x3ded6320U
63 : #define CRC_messageMediaPhoto 0x695150d7U
64 : #define CRC_messageMediaDocument 0x4cf4d72dU
65 : #define CRC_messageMediaGeo 0x56e0d474U
66 : #define CRC_messageMediaContact 0x70322949U
67 : #define CRC_messageMediaWebPage 0xddf8c26eU
68 : #define CRC_messageMediaPoll 0x4bd6e798U
69 :
70 : #define CRC_geoPoint 0xb2a2f663U
71 :
72 : #define CRC_document 0x8fd4c4d8U
73 : #define CRC_photo 0xfb197a65U
74 : #define CRC_photoSize 0x75c78e60U
75 :
76 : #define CRC_documentAttributeAnimated 0x11b58939U
77 : #define CRC_documentAttributeFilename 0x15590068U
78 : #define CRC_documentAttributeVideo 0x43c57c48U
79 : #define CRC_documentAttributeAudio 0x9852f9c6U
80 : #define CRC_documentAttributeSticker 0x6319d612U
81 : #define CRC_inputStickerSetEmpty 0xffb62b95U
82 :
83 : #define CRC_webPage 0xe89c45b2U
84 : #define CRC_poll 0x58747131U
85 : #define CRC_pollAnswer 0x6ca9c2e9U
86 : #define CRC_pollResults 0x7adc669dU
87 : #define CRC_textWithEntities_poll 0x751f3146U
88 :
89 : /* Message.flags bits we use. */
90 : #define MSG_FLAG_MEDIA (1u << 9)
91 :
92 : /* ---- Boilerplate ---- */
93 :
94 54 : static void with_tmp_home(const char *tag) {
95 : char tmp[256];
96 54 : snprintf(tmp, sizeof(tmp), "/tmp/tg-cli-ft-rich-media-%s", tag);
97 : char bin[512];
98 54 : snprintf(bin, sizeof(bin), "%s/.config/tg-cli/session.bin", tmp);
99 54 : (void)unlink(bin);
100 54 : setenv("HOME", tmp, 1);
101 54 : }
102 :
103 54 : static void connect_mock(Transport *t) {
104 54 : transport_init(t);
105 54 : ASSERT(transport_connect(t, "127.0.0.1", 443) == 0, "connect");
106 : }
107 :
108 54 : static void init_cfg(ApiConfig *cfg) {
109 54 : api_config_init(cfg);
110 54 : cfg->api_id = 12345;
111 54 : cfg->api_hash = "deadbeefcafebabef00dbaadfeedc0de";
112 54 : }
113 :
114 54 : static void load_session(MtProtoSession *s) {
115 54 : ASSERT(mt_server_seed_session(2, NULL, NULL, NULL) == 0, "seed");
116 54 : mtproto_session_init(s);
117 54 : int dc = 0;
118 54 : ASSERT(session_store_load(s, &dc) == 0, "load session");
119 : }
120 :
121 : /* Envelope: messages.messages { messages: Vector<Message>{1}, chats, users }
122 : * with the caller providing the inner message bytes (starting at TL_message). */
123 24 : static void wrap_messages_messages(TlWriter *w, const uint8_t *msg_bytes,
124 : size_t msg_len) {
125 24 : tl_write_uint32(w, TL_messages_messages);
126 24 : tl_write_uint32(w, TL_vector);
127 24 : tl_write_uint32(w, 1);
128 24 : tl_write_raw(w, msg_bytes, msg_len);
129 24 : tl_write_uint32(w, TL_vector); tl_write_uint32(w, 0); /* chats */
130 24 : tl_write_uint32(w, TL_vector); tl_write_uint32(w, 0); /* users */
131 24 : }
132 :
133 : /* Write the common Message prefix up to and including the `message:string`
134 : * field, leaving the writer positioned at the media payload. Every variant
135 : * in this suite uses the same envelope (no flags2 bits, peer=self, minimal
136 : * date/text) so only the nested MessageMedia differs between tests. */
137 24 : static void write_message_prefix(TlWriter *w, int32_t msg_id,
138 : const char *caption) {
139 24 : tl_write_uint32(w, TL_message);
140 24 : tl_write_uint32(w, MSG_FLAG_MEDIA); /* flags */
141 24 : tl_write_uint32(w, 0); /* flags2 */
142 24 : tl_write_int32 (w, msg_id);
143 24 : tl_write_uint32(w, TL_peerUser); /* peer_id */
144 24 : tl_write_int64 (w, 1LL);
145 24 : tl_write_int32 (w, 1700000500); /* date */
146 24 : tl_write_string(w, caption); /* message */
147 24 : }
148 :
149 : /* Shared helper: arm getHistory → return an envelope built by @p build
150 : * into @p ctx. The responder allocates its own TlWriters — the inner
151 : * bytes are copied into the outer frame before either is freed. */
152 : typedef void (*MediaBuilder)(TlWriter *w);
153 :
154 24 : static void reply_history_with_media(MtRpcContext *ctx,
155 : MediaBuilder build_media,
156 : int32_t msg_id,
157 : const char *caption) {
158 24 : TlWriter inner; tl_writer_init(&inner);
159 24 : write_message_prefix(&inner, msg_id, caption);
160 24 : build_media(&inner);
161 :
162 24 : TlWriter w; tl_writer_init(&w);
163 24 : wrap_messages_messages(&w, inner.data, inner.len);
164 24 : mt_server_reply_result(ctx, w.data, w.len);
165 24 : tl_writer_free(&w);
166 24 : tl_writer_free(&inner);
167 24 : }
168 :
169 : /* Each responder below builds a single MessageMedia variant and hands
170 : * off to reply_history_with_media. */
171 : /* ---- Media builders ---------------------------------------------- */
172 :
173 : /* messageMediaGeo { geoPoint flags=0 long:double lat:double access_hash:long } */
174 2 : static void build_media_geo(TlWriter *w) {
175 2 : tl_write_uint32(w, CRC_messageMediaGeo);
176 2 : tl_write_uint32(w, CRC_geoPoint);
177 2 : tl_write_uint32(w, 0); /* geoPoint flags */
178 2 : tl_write_double(w, 19.0402); /* long */
179 2 : tl_write_double(w, 47.4979); /* lat */
180 2 : tl_write_int64 (w, 0LL); /* access_hash */
181 2 : }
182 :
183 : /* messageMediaContact#70322949
184 : * phone:string first_name:string last_name:string vcard:string user_id:long */
185 2 : static void build_media_contact(TlWriter *w) {
186 2 : tl_write_uint32(w, CRC_messageMediaContact);
187 2 : tl_write_string(w, "+36301234567");
188 2 : tl_write_string(w, "Janos");
189 2 : tl_write_string(w, "Example");
190 2 : tl_write_string(w, "");
191 2 : tl_write_int64 (w, 555001LL);
192 2 : }
193 :
194 : /* messageMediaWebPage#ddf8c26e flags:# webpage:WebPage
195 : * Minimal webPage variant with url+display_url+hash, no optional fields. */
196 2 : static void build_media_webpage(TlWriter *w) {
197 2 : tl_write_uint32(w, CRC_messageMediaWebPage);
198 2 : tl_write_uint32(w, 0); /* outer flags */
199 2 : tl_write_uint32(w, CRC_webPage);
200 2 : tl_write_uint32(w, 0); /* webPage flags */
201 2 : tl_write_int64 (w, 77001LL); /* id */
202 2 : tl_write_string(w, "https://example.com/");
203 2 : tl_write_string(w, "example.com");
204 2 : tl_write_int32 (w, 0); /* hash */
205 2 : }
206 :
207 : /* messageMediaPoll#4bd6e798 poll:Poll results:PollResults
208 : * Poll = flags:# id:long question:textWithEntities answers:Vector<PollAnswer>
209 : * close_period:flags.4?int close_date:flags.5?int
210 : * PollResults = flags:# (all optional vectors/fields skipped).
211 : * A single poll answer with one option. */
212 2 : static void build_media_poll(TlWriter *w) {
213 2 : tl_write_uint32(w, CRC_messageMediaPoll);
214 : /* poll */
215 2 : tl_write_uint32(w, CRC_poll);
216 2 : tl_write_uint32(w, 0); /* poll flags */
217 2 : tl_write_int64 (w, 42LL); /* poll id */
218 : /* question */
219 2 : tl_write_uint32(w, CRC_textWithEntities_poll);
220 2 : tl_write_string(w, "Sunny today?");
221 2 : tl_write_uint32(w, TL_vector); tl_write_uint32(w, 0); /* entities */
222 : /* answers vector with 1 entry */
223 2 : tl_write_uint32(w, TL_vector);
224 2 : tl_write_uint32(w, 1);
225 2 : tl_write_uint32(w, CRC_pollAnswer);
226 2 : tl_write_uint32(w, CRC_textWithEntities_poll);
227 2 : tl_write_string(w, "Yes");
228 2 : tl_write_uint32(w, TL_vector); tl_write_uint32(w, 0); /* entities */
229 2 : tl_write_string(w, "y"); /* option:bytes */
230 : /* pollResults — empty flags */
231 2 : tl_write_uint32(w, CRC_pollResults);
232 2 : tl_write_uint32(w, 0);
233 2 : }
234 :
235 : /* Build a messageMediaPhoto with a fully-populated photo#fb197a65
236 : * (layer 170+): flags + id + access_hash + file_reference:bytes + date
237 : * + sizes:Vector<PhotoSize> + dc_id. One photoSize entry (type="y"). */
238 2 : static void build_media_photo(TlWriter *w) {
239 2 : tl_write_uint32(w, CRC_messageMediaPhoto);
240 2 : tl_write_uint32(w, 1u); /* outer flags — has photo */
241 2 : tl_write_uint32(w, CRC_photo);
242 2 : tl_write_uint32(w, 0); /* photo flags */
243 2 : tl_write_int64 (w, 0x1234567890ABLL); /* id */
244 2 : tl_write_int64 (w, 0xCAFEBABEDEADBEEFLL); /* access_hash */
245 : {
246 : /* file_reference bytes */
247 : static const unsigned char fr[] = { 0xDE, 0xAD, 0xBE, 0xEF };
248 2 : tl_write_bytes(w, fr, sizeof(fr));
249 : }
250 2 : tl_write_int32 (w, 1700000500); /* date */
251 : /* sizes: Vector<PhotoSize> with 1 photoSize#75c78e60 type+w+h+size */
252 2 : tl_write_uint32(w, TL_vector);
253 2 : tl_write_uint32(w, 1);
254 2 : tl_write_uint32(w, CRC_photoSize);
255 2 : tl_write_string(w, "y");
256 2 : tl_write_int32 (w, 1280);
257 2 : tl_write_int32 (w, 720);
258 2 : tl_write_int32 (w, 123456);
259 2 : tl_write_int32 (w, 2); /* dc_id */
260 2 : }
261 :
262 : /* Build a messageMediaDocument carrying a Document with a single
263 : * DocumentAttribute supplied by the caller. The Document layout we
264 : * emit:
265 : * document#8fd4c4d8 flags=0 id access_hash file_reference:bytes date
266 : * mime_type:string size:long dc_id:int
267 : * attributes:Vector<DocumentAttribute>
268 : * No thumbs / video_thumbs (flags.0 / flags.1 both 0) so the skipper
269 : * takes the happy path. */
270 : typedef void (*AttrBuilder)(TlWriter *w);
271 :
272 14 : static void emit_document(TlWriter *w, const char *mime, int64_t size,
273 : AttrBuilder build_attr) {
274 14 : tl_write_uint32(w, CRC_messageMediaDocument);
275 14 : tl_write_uint32(w, 1u); /* outer flags — has document */
276 14 : tl_write_uint32(w, CRC_document);
277 14 : tl_write_uint32(w, 0); /* document flags */
278 14 : tl_write_int64 (w, 0x1111222233334444LL); /* id */
279 14 : tl_write_int64 (w, 0x5555666677778888LL); /* access_hash */
280 : {
281 : static const unsigned char fr[] = { 0xAA, 0xBB, 0xCC, 0xDD };
282 14 : tl_write_bytes(w, fr, sizeof(fr));
283 : }
284 14 : tl_write_int32 (w, 1700000500); /* date */
285 14 : tl_write_string(w, mime);
286 14 : tl_write_int64 (w, size);
287 14 : tl_write_int32 (w, 2); /* dc_id */
288 : /* attributes — always exactly one in these fixtures. */
289 14 : tl_write_uint32(w, TL_vector);
290 14 : tl_write_uint32(w, 1);
291 14 : build_attr(w);
292 14 : }
293 :
294 : /* documentAttributeVideo#43c57c48 flags:# duration:double w:int h:int ... */
295 2 : static void attr_video(TlWriter *w) {
296 2 : tl_write_uint32(w, CRC_documentAttributeVideo);
297 2 : tl_write_uint32(w, 0); /* flags */
298 2 : tl_write_double(w, 42.0); /* duration */
299 2 : tl_write_int32 (w, 1280); /* w */
300 2 : tl_write_int32 (w, 720); /* h */
301 2 : }
302 :
303 : /* documentAttributeVideo with round_message (flags.0). */
304 2 : static void attr_round_video(TlWriter *w) {
305 2 : tl_write_uint32(w, CRC_documentAttributeVideo);
306 2 : tl_write_uint32(w, 1u); /* flags: round_message */
307 2 : tl_write_double(w, 4.0);
308 2 : tl_write_int32 (w, 320);
309 2 : tl_write_int32 (w, 320);
310 2 : }
311 :
312 : /* documentAttributeAudio#9852f9c6 flags:# duration:int (ints, NOT double)
313 : * Voice flag lives at flags.10 — we set it to differentiate voice from
314 : * ordinary audio downloads. */
315 2 : static void attr_audio_voice(TlWriter *w) {
316 2 : tl_write_uint32(w, CRC_documentAttributeAudio);
317 2 : tl_write_uint32(w, 1u << 10); /* flags: voice */
318 2 : tl_write_int32 (w, 8); /* duration */
319 2 : }
320 :
321 2 : static void attr_audio_music(TlWriter *w) {
322 2 : tl_write_uint32(w, CRC_documentAttributeAudio);
323 2 : tl_write_uint32(w, 0u); /* flags: plain audio */
324 2 : tl_write_int32 (w, 197); /* duration (m:s) */
325 2 : }
326 :
327 : /* documentAttributeSticker#6319d612 flags:# alt:string
328 : * stickerset:InputStickerSet mask_coords:flags.0?MaskCoords
329 : * inputStickerSetEmpty#ffb62b95 — no body. */
330 2 : static void attr_sticker(TlWriter *w) {
331 2 : tl_write_uint32(w, CRC_documentAttributeSticker);
332 2 : tl_write_uint32(w, 0); /* flags */
333 2 : tl_write_string(w, ":heart_eyes:"); /* alt */
334 2 : tl_write_uint32(w, CRC_inputStickerSetEmpty);
335 2 : }
336 :
337 : /* documentAttributeAnimated#11b58939 (GIF). Followed by a filename attr
338 : * would be redundant; keep it single-attr. */
339 2 : static void attr_animated(TlWriter *w) {
340 2 : tl_write_uint32(w, CRC_documentAttributeAnimated);
341 2 : }
342 :
343 : /* documentAttributeFilename#15590068 file_name:string */
344 2 : static void attr_filename_hello_ogg(TlWriter *w) {
345 2 : tl_write_uint32(w, CRC_documentAttributeFilename);
346 2 : tl_write_string(w, "voice.ogg");
347 2 : }
348 :
349 : /* ---- getHistory responders ---- */
350 :
351 2 : static void on_history_geo(MtRpcContext *ctx) {
352 2 : reply_history_with_media(ctx, build_media_geo, 1001, "pin drop");
353 2 : }
354 2 : static void on_history_contact(MtRpcContext *ctx) {
355 2 : reply_history_with_media(ctx, build_media_contact, 1002, "contact");
356 2 : }
357 2 : static void on_history_webpage(MtRpcContext *ctx) {
358 2 : reply_history_with_media(ctx, build_media_webpage, 1003, "link");
359 2 : }
360 2 : static void on_history_poll(MtRpcContext *ctx) {
361 2 : reply_history_with_media(ctx, build_media_poll, 1004, "poll");
362 2 : }
363 2 : static void on_history_photo(MtRpcContext *ctx) {
364 2 : reply_history_with_media(ctx, build_media_photo, 1005, "pic");
365 2 : }
366 :
367 : /* Per-document-variant responder factory — each builds a Document with
368 : * the given attribute as its single DocumentAttribute and a controlled
369 : * mime/size pair so tests can assert the parser captured them. */
370 : static const char *g_doc_mime = NULL;
371 : static int64_t g_doc_size = 0;
372 : static AttrBuilder g_doc_attr_build = NULL;
373 : static int32_t g_doc_msg_id = 0;
374 : static const char *g_doc_caption = NULL;
375 :
376 14 : static void build_media_document_dispatch(TlWriter *w) {
377 14 : emit_document(w, g_doc_mime, g_doc_size, g_doc_attr_build);
378 14 : }
379 :
380 14 : static void on_history_document(MtRpcContext *ctx) {
381 14 : reply_history_with_media(ctx, build_media_document_dispatch,
382 : g_doc_msg_id, g_doc_caption);
383 14 : }
384 :
385 : /* ================================================================ */
386 : /* Parse tests */
387 : /* ================================================================ */
388 :
389 2 : static void test_rich_media_parse_geo(void) {
390 2 : with_tmp_home("parse-geo");
391 2 : mt_server_init(); mt_server_reset();
392 2 : MtProtoSession s; load_session(&s);
393 2 : mt_server_expect(CRC_messages_getHistory, on_history_geo, NULL);
394 :
395 2 : ApiConfig cfg; init_cfg(&cfg);
396 2 : Transport t; connect_mock(&t);
397 :
398 2 : HistoryEntry rows[4]; int n = 0;
399 2 : ASSERT(domain_get_history_self(&cfg, &s, &t, 0, 4, rows, &n) == 0,
400 : "getHistory with messageMediaGeo succeeds");
401 2 : ASSERT(n == 1, "one row parsed");
402 2 : ASSERT(rows[0].id == 1001, "id preserved");
403 2 : ASSERT(strcmp(rows[0].text, "pin drop") == 0,
404 : "caption preserved before geo media");
405 2 : ASSERT(rows[0].media == MEDIA_GEO, "media classified as MEDIA_GEO");
406 2 : ASSERT(rows[0].complex == 0, "geo does not mark complex");
407 :
408 2 : transport_close(&t);
409 2 : mt_server_reset();
410 : }
411 :
412 2 : static void test_rich_media_parse_contact(void) {
413 2 : with_tmp_home("parse-contact");
414 2 : mt_server_init(); mt_server_reset();
415 2 : MtProtoSession s; load_session(&s);
416 2 : mt_server_expect(CRC_messages_getHistory, on_history_contact, NULL);
417 :
418 2 : ApiConfig cfg; init_cfg(&cfg);
419 2 : Transport t; connect_mock(&t);
420 :
421 2 : HistoryEntry rows[4]; int n = 0;
422 2 : ASSERT(domain_get_history_self(&cfg, &s, &t, 0, 4, rows, &n) == 0,
423 : "getHistory with messageMediaContact succeeds");
424 2 : ASSERT(n == 1, "one row parsed");
425 2 : ASSERT(rows[0].id == 1002, "id preserved");
426 2 : ASSERT(rows[0].media == MEDIA_CONTACT, "classified as MEDIA_CONTACT");
427 2 : ASSERT(rows[0].complex == 0, "contact does not mark complex");
428 :
429 2 : transport_close(&t);
430 2 : mt_server_reset();
431 : }
432 :
433 2 : static void test_rich_media_parse_webpage(void) {
434 2 : with_tmp_home("parse-webpage");
435 2 : mt_server_init(); mt_server_reset();
436 2 : MtProtoSession s; load_session(&s);
437 2 : mt_server_expect(CRC_messages_getHistory, on_history_webpage, NULL);
438 :
439 2 : ApiConfig cfg; init_cfg(&cfg);
440 2 : Transport t; connect_mock(&t);
441 :
442 2 : HistoryEntry rows[4]; int n = 0;
443 2 : ASSERT(domain_get_history_self(&cfg, &s, &t, 0, 4, rows, &n) == 0,
444 : "getHistory with messageMediaWebPage succeeds");
445 2 : ASSERT(n == 1, "one row parsed");
446 2 : ASSERT(rows[0].id == 1003, "id preserved");
447 2 : ASSERT(strcmp(rows[0].text, "link") == 0, "caption preserved");
448 2 : ASSERT(rows[0].media == MEDIA_WEBPAGE, "classified as MEDIA_WEBPAGE");
449 2 : ASSERT(rows[0].complex == 0, "webpage does not mark complex");
450 :
451 2 : transport_close(&t);
452 2 : mt_server_reset();
453 : }
454 :
455 2 : static void test_rich_media_parse_poll(void) {
456 2 : with_tmp_home("parse-poll");
457 2 : mt_server_init(); mt_server_reset();
458 2 : MtProtoSession s; load_session(&s);
459 2 : mt_server_expect(CRC_messages_getHistory, on_history_poll, NULL);
460 :
461 2 : ApiConfig cfg; init_cfg(&cfg);
462 2 : Transport t; connect_mock(&t);
463 :
464 2 : HistoryEntry rows[4]; int n = 0;
465 2 : ASSERT(domain_get_history_self(&cfg, &s, &t, 0, 4, rows, &n) == 0,
466 : "getHistory with messageMediaPoll succeeds");
467 2 : ASSERT(n == 1, "one row parsed");
468 2 : ASSERT(rows[0].id == 1004, "id preserved");
469 2 : ASSERT(rows[0].media == MEDIA_POLL, "classified as MEDIA_POLL");
470 2 : ASSERT(rows[0].complex == 0, "poll does not mark complex");
471 :
472 2 : transport_close(&t);
473 2 : mt_server_reset();
474 : }
475 :
476 2 : static void test_rich_media_parse_photo_metadata(void) {
477 2 : with_tmp_home("parse-photo");
478 2 : mt_server_init(); mt_server_reset();
479 2 : MtProtoSession s; load_session(&s);
480 2 : mt_server_expect(CRC_messages_getHistory, on_history_photo, NULL);
481 :
482 2 : ApiConfig cfg; init_cfg(&cfg);
483 2 : Transport t; connect_mock(&t);
484 :
485 2 : HistoryEntry rows[4]; int n = 0;
486 2 : ASSERT(domain_get_history_self(&cfg, &s, &t, 0, 4, rows, &n) == 0,
487 : "getHistory with messageMediaPhoto succeeds");
488 2 : ASSERT(n == 1, "one row parsed");
489 2 : ASSERT(rows[0].media == MEDIA_PHOTO, "classified as MEDIA_PHOTO");
490 2 : ASSERT(rows[0].media_id == 0x1234567890ABLL,
491 : "photo_id propagates into HistoryEntry");
492 2 : ASSERT(rows[0].media_dc == 2, "dc_id propagates into HistoryEntry");
493 2 : ASSERT(rows[0].media_info.access_hash == (int64_t)0xCAFEBABEDEADBEEFLL,
494 : "access_hash captured");
495 2 : ASSERT(rows[0].media_info.file_reference_len == 4,
496 : "file_reference length captured");
497 2 : ASSERT(strcmp(rows[0].media_info.thumb_type, "y") == 0,
498 : "largest photoSize.type captured");
499 2 : ASSERT(rows[0].complex == 0, "photo happy path does not mark complex");
500 :
501 2 : transport_close(&t);
502 2 : mt_server_reset();
503 : }
504 :
505 : /* Helper: run a single-document parse test with the given attribute
506 : * builder and mime/size pair. Collapses the per-variant boilerplate. */
507 14 : static void run_document_parse(const char *tag, AttrBuilder attr,
508 : const char *mime, int64_t size,
509 : int32_t msg_id, const char *caption,
510 : const char *want_filename) {
511 14 : with_tmp_home(tag);
512 14 : mt_server_init(); mt_server_reset();
513 14 : MtProtoSession s; load_session(&s);
514 14 : g_doc_mime = mime;
515 14 : g_doc_size = size;
516 14 : g_doc_attr_build = attr;
517 14 : g_doc_msg_id = msg_id;
518 14 : g_doc_caption = caption;
519 14 : mt_server_expect(CRC_messages_getHistory, on_history_document, NULL);
520 :
521 14 : ApiConfig cfg; init_cfg(&cfg);
522 14 : Transport t; connect_mock(&t);
523 :
524 14 : HistoryEntry rows[4]; int n = 0;
525 14 : ASSERT(domain_get_history_self(&cfg, &s, &t, 0, 4, rows, &n) == 0,
526 : "getHistory with messageMediaDocument succeeds");
527 14 : ASSERT(n == 1, "one row parsed");
528 14 : ASSERT(rows[0].id == msg_id, "id preserved past Document trailer");
529 14 : ASSERT(rows[0].media == MEDIA_DOCUMENT,
530 : "classified as MEDIA_DOCUMENT");
531 14 : ASSERT(rows[0].media_id == 0x1111222233334444LL,
532 : "document_id propagates into HistoryEntry");
533 14 : ASSERT(rows[0].media_info.document_size == size,
534 : "document size captured");
535 14 : ASSERT(strcmp(rows[0].media_info.document_mime, mime) == 0,
536 : "document mime captured verbatim");
537 14 : if (want_filename) {
538 2 : ASSERT(strcmp(rows[0].media_info.document_filename,
539 : want_filename) == 0,
540 : "document filename captured");
541 : }
542 14 : ASSERT(rows[0].complex == 0, "document attr does not mark complex");
543 :
544 14 : transport_close(&t);
545 14 : mt_server_reset();
546 : }
547 :
548 2 : static void test_rich_media_parse_document_video(void) {
549 2 : run_document_parse("parse-video", attr_video,
550 : "video/mp4", 1234567LL, 2001, "vid", NULL);
551 2 : }
552 :
553 2 : static void test_rich_media_parse_document_round_video(void) {
554 2 : run_document_parse("parse-round", attr_round_video,
555 : "video/mp4", 655360LL, 2002, "round", NULL);
556 2 : }
557 :
558 2 : static void test_rich_media_parse_document_audio_music(void) {
559 2 : run_document_parse("parse-audio", attr_audio_music,
560 : "audio/mpeg", 4915200LL, 2003, "track", NULL);
561 2 : }
562 :
563 2 : static void test_rich_media_parse_document_voice_note(void) {
564 2 : run_document_parse("parse-voice", attr_audio_voice,
565 : "audio/ogg", 42000LL, 2004, "vm", NULL);
566 2 : }
567 :
568 2 : static void test_rich_media_parse_document_sticker(void) {
569 2 : run_document_parse("parse-sticker", attr_sticker,
570 : "image/webp", 51200LL, 2005, "", NULL);
571 2 : }
572 :
573 2 : static void test_rich_media_parse_document_animation(void) {
574 2 : run_document_parse("parse-gif", attr_animated,
575 : "video/mp4", 131072LL, 2006, "gif", NULL);
576 2 : }
577 :
578 2 : static void test_rich_media_parse_document_filename_captured(void) {
579 : /* Single-attribute vector carrying documentAttributeFilename lets us
580 : * assert that the filename propagates out of the skipper into
581 : * HistoryEntry.media_info.document_filename — a guarantee the
582 : * download-name inference in future US-22 work depends on. */
583 2 : run_document_parse("parse-filename", attr_filename_hello_ogg,
584 : "audio/ogg", 42000LL, 2007, "", "voice.ogg");
585 2 : }
586 :
587 : /* ================================================================ */
588 : /* Download-path coverage tests */
589 : /* ================================================================ */
590 :
591 : /* upload.file helper reused by the download tests. */
592 12 : static void reply_upload_file_short(MtRpcContext *ctx) {
593 : uint8_t payload[64];
594 780 : for (size_t i = 0; i < sizeof(payload); ++i)
595 768 : payload[i] = (uint8_t)(i ^ 0xC3u);
596 :
597 12 : TlWriter w; tl_writer_init(&w);
598 12 : tl_write_uint32(&w, CRC_upload_file);
599 12 : tl_write_uint32(&w, CRC_storage_filePartial);
600 12 : tl_write_int32 (&w, 0);
601 12 : tl_write_bytes (&w, payload, sizeof(payload));
602 12 : mt_server_reply_result(ctx, w.data, w.len);
603 12 : tl_writer_free(&w);
604 12 : }
605 :
606 12 : static void on_get_file_short_doc(MtRpcContext *ctx) {
607 12 : reply_upload_file_short(ctx);
608 12 : }
609 :
610 24 : static void make_document_mi(MediaInfo *mi, const char *fname,
611 : const char *mime) {
612 24 : memset(mi, 0, sizeof(*mi));
613 24 : mi->kind = MEDIA_DOCUMENT;
614 24 : mi->document_id = 0x1111222233334444LL;
615 24 : mi->access_hash = 0x5555666677778888LL;
616 24 : mi->dc_id = 2;
617 24 : mi->file_reference_len = 4;
618 24 : mi->file_reference[0] = 0xAA; mi->file_reference[1] = 0xBB;
619 24 : mi->file_reference[2] = 0xCC; mi->file_reference[3] = 0xDD;
620 24 : if (fname) {
621 24 : snprintf(mi->document_filename,
622 : sizeof(mi->document_filename), "%s", fname);
623 : }
624 24 : if (mime) {
625 24 : snprintf(mi->document_mime,
626 : sizeof(mi->document_mime), "%s", mime);
627 : }
628 24 : }
629 :
630 : /* Exercises the happy path for a voice document download — same chunk
631 : * flow as plain documents (no extension inference today), which keeps
632 : * the test tied to behaviour actually present in media.c. */
633 2 : static void test_rich_media_download_voice_note_chunked(void) {
634 2 : with_tmp_home("dl-voice");
635 2 : mt_server_init(); mt_server_reset();
636 2 : MtProtoSession s; load_session(&s);
637 2 : mt_server_expect(CRC_upload_getFile, on_get_file_short_doc, NULL);
638 :
639 2 : ApiConfig cfg; init_cfg(&cfg);
640 2 : Transport t; connect_mock(&t);
641 :
642 2 : MediaInfo mi; make_document_mi(&mi, "voice.ogg", "audio/ogg");
643 2 : const char *out = "/tmp/tg-cli-ft-rich-media-voice.ogg";
644 2 : int wrong = -1;
645 2 : ASSERT(domain_download_document(&cfg, &s, &t, &mi, out, &wrong) == 0,
646 : "voice download returns 0");
647 2 : ASSERT(wrong == 0, "no wrong_dc surfaced");
648 :
649 : struct stat st;
650 2 : ASSERT(stat(out, &st) == 0, "voice file written");
651 2 : ASSERT(st.st_size == 64, "64 bytes written for voice");
652 2 : unlink(out);
653 :
654 2 : transport_close(&t);
655 2 : mt_server_reset();
656 : }
657 :
658 2 : static void test_rich_media_download_sticker_chunked(void) {
659 2 : with_tmp_home("dl-sticker");
660 2 : mt_server_init(); mt_server_reset();
661 2 : MtProtoSession s; load_session(&s);
662 2 : mt_server_expect(CRC_upload_getFile, on_get_file_short_doc, NULL);
663 :
664 2 : ApiConfig cfg; init_cfg(&cfg);
665 2 : Transport t; connect_mock(&t);
666 :
667 2 : MediaInfo mi; make_document_mi(&mi, "heart.webp", "image/webp");
668 2 : const char *out = "/tmp/tg-cli-ft-rich-media-heart.webp";
669 2 : int wrong = -1;
670 2 : ASSERT(domain_download_document(&cfg, &s, &t, &mi, out, &wrong) == 0,
671 : "sticker download returns 0");
672 :
673 : struct stat st;
674 2 : ASSERT(stat(out, &st) == 0, "sticker file written");
675 2 : ASSERT(st.st_size == 64, "64 bytes written for sticker");
676 2 : unlink(out);
677 :
678 2 : transport_close(&t);
679 2 : mt_server_reset();
680 : }
681 :
682 2 : static void test_rich_media_download_video_chunked(void) {
683 2 : with_tmp_home("dl-video");
684 2 : mt_server_init(); mt_server_reset();
685 2 : MtProtoSession s; load_session(&s);
686 2 : mt_server_expect(CRC_upload_getFile, on_get_file_short_doc, NULL);
687 :
688 2 : ApiConfig cfg; init_cfg(&cfg);
689 2 : Transport t; connect_mock(&t);
690 :
691 2 : MediaInfo mi; make_document_mi(&mi, "clip.mp4", "video/mp4");
692 2 : const char *out = "/tmp/tg-cli-ft-rich-media-clip.mp4";
693 2 : int wrong = -1;
694 2 : ASSERT(domain_download_document(&cfg, &s, &t, &mi, out, &wrong) == 0,
695 : "video download returns 0");
696 :
697 : struct stat st;
698 2 : ASSERT(stat(out, &st) == 0, "video file written");
699 2 : ASSERT(st.st_size == 64, "64 bytes written for video");
700 2 : unlink(out);
701 :
702 2 : transport_close(&t);
703 2 : mt_server_reset();
704 : }
705 :
706 : /* Guard: download_photo must refuse a MEDIA_DOCUMENT MediaInfo
707 : * (exercises the kind-guard branch in media.c). */
708 2 : static void test_rich_media_download_photo_rejects_document_kind(void) {
709 2 : with_tmp_home("dl-guard-doc");
710 2 : mt_server_init(); mt_server_reset();
711 2 : MtProtoSession s; load_session(&s);
712 :
713 2 : ApiConfig cfg; init_cfg(&cfg);
714 2 : Transport t; connect_mock(&t);
715 :
716 2 : MediaInfo mi; make_document_mi(&mi, "x.bin", "application/octet-stream");
717 2 : int wrong = 7;
718 2 : ASSERT(domain_download_photo(&cfg, &s, &t, &mi,
719 : "/tmp/tg-cli-ft-rich-media-guard.bin",
720 : &wrong) == -1,
721 : "download_photo rejects MEDIA_DOCUMENT MediaInfo");
722 2 : ASSERT(wrong == 0, "wrong_dc cleared on kind mismatch");
723 :
724 2 : transport_close(&t);
725 2 : mt_server_reset();
726 : }
727 :
728 : /* Guard: download_document must refuse a MEDIA_PHOTO MediaInfo and a
729 : * MediaInfo with a missing document_id (both hit validation branches
730 : * that the existing suite never triggers). */
731 2 : static void test_rich_media_download_document_rejects_photo_kind(void) {
732 2 : with_tmp_home("dl-guard-photo");
733 2 : mt_server_init(); mt_server_reset();
734 2 : MtProtoSession s; load_session(&s);
735 :
736 2 : ApiConfig cfg; init_cfg(&cfg);
737 2 : Transport t; connect_mock(&t);
738 :
739 : MediaInfo mi;
740 2 : memset(&mi, 0, sizeof(mi));
741 2 : mi.kind = MEDIA_PHOTO;
742 2 : mi.photo_id = 0xAABBLL;
743 2 : mi.access_hash = 0xCCDDLL;
744 2 : mi.file_reference_len = 4;
745 2 : int wrong = 9;
746 2 : ASSERT(domain_download_document(&cfg, &s, &t, &mi,
747 : "/tmp/tg-cli-ft-rich-media-guard2.bin",
748 : &wrong) == -1,
749 : "download_document rejects MEDIA_PHOTO MediaInfo");
750 2 : ASSERT(wrong == 0, "wrong_dc cleared on kind mismatch");
751 :
752 : /* Second sub-case: MEDIA_DOCUMENT but with zero document_id — the
753 : * required-field guard. */
754 : MediaInfo mi2;
755 2 : memset(&mi2, 0, sizeof(mi2));
756 2 : mi2.kind = MEDIA_DOCUMENT; /* id=0, access_hash=0 */
757 2 : wrong = 9;
758 2 : ASSERT(domain_download_document(&cfg, &s, &t, &mi2,
759 : "/tmp/tg-cli-ft-rich-media-guard3.bin",
760 : &wrong) == -1,
761 : "download_document rejects zero document_id");
762 2 : ASSERT(wrong == 0, "wrong_dc cleared on empty MediaInfo");
763 :
764 2 : transport_close(&t);
765 2 : mt_server_reset();
766 : }
767 :
768 : /* cross_dc wrapper: happy path (home DC succeeds on first shot) — hits
769 : * the early-return branch we otherwise never exercise. */
770 2 : static void test_rich_media_cross_dc_home_succeeds(void) {
771 2 : with_tmp_home("dl-xdc-home");
772 2 : mt_server_init(); mt_server_reset();
773 2 : MtProtoSession s; load_session(&s);
774 2 : mt_server_expect(CRC_upload_getFile, on_get_file_short_doc, NULL);
775 :
776 2 : ApiConfig cfg; init_cfg(&cfg);
777 2 : Transport t; connect_mock(&t);
778 :
779 2 : MediaInfo mi; make_document_mi(&mi, "home.bin", "application/octet-stream");
780 2 : const char *out = "/tmp/tg-cli-ft-rich-media-home.bin";
781 2 : ASSERT(domain_download_media_cross_dc(&cfg, &s, &t, &mi, out) == 0,
782 : "cross_dc happy path returns 0 without migration");
783 :
784 : struct stat st;
785 2 : ASSERT(stat(out, &st) == 0, "output written by cross_dc wrapper");
786 2 : ASSERT(st.st_size == 64, "64 bytes written via cross_dc happy path");
787 2 : unlink(out);
788 :
789 2 : transport_close(&t);
790 2 : mt_server_reset();
791 : }
792 :
793 : /* cross_dc wrapper: unsupported kind (MEDIA_GEO) routes through
794 : * download_any's dispatch and returns -1 immediately. Covers the
795 : * "unsupported kind" log branch without touching any network path. */
796 2 : static void test_rich_media_cross_dc_rejects_unsupported_kind(void) {
797 2 : with_tmp_home("dl-xdc-unsup");
798 2 : mt_server_init(); mt_server_reset();
799 2 : MtProtoSession s; load_session(&s);
800 :
801 2 : ApiConfig cfg; init_cfg(&cfg);
802 2 : Transport t; connect_mock(&t);
803 :
804 : MediaInfo mi;
805 2 : memset(&mi, 0, sizeof(mi));
806 2 : mi.kind = MEDIA_GEO; /* not photo, not document */
807 2 : ASSERT(domain_download_media_cross_dc(&cfg, &s, &t, &mi,
808 : "/tmp/tg-cli-ft-rich-media-unsup.bin")
809 : == -1,
810 : "cross_dc refuses MEDIA_GEO kind");
811 :
812 2 : transport_close(&t);
813 2 : mt_server_reset();
814 : }
815 :
816 : /* ================================================================ */
817 : /* download_loop error-branch and cache-copy coverage */
818 : /* ================================================================ */
819 :
820 : /* Reply with a raw TL body whose first word is CRC_upload_fileCdnRedirect.
821 : * download_loop sees this as CDN redirect and returns -1. */
822 : #define CRC_upload_fileCdnRedirect_T 0xf18cda44u
823 :
824 4 : static void on_get_file_cdn_redirect(MtRpcContext *ctx) {
825 4 : TlWriter w; tl_writer_init(&w);
826 4 : tl_write_uint32(&w, CRC_upload_fileCdnRedirect_T);
827 : /* Minimal trailing bytes so resp_len >= 4 */
828 4 : tl_write_uint32(&w, 0);
829 4 : tl_write_uint32(&w, 0);
830 4 : tl_write_uint32(&w, 0);
831 4 : mt_server_reply_result(ctx, w.data, w.len);
832 4 : tl_writer_free(&w);
833 4 : }
834 :
835 : /* Reply with an unknown CRC — triggers the "unexpected top" branch. */
836 2 : static void on_get_file_unknown_top(MtRpcContext *ctx) {
837 2 : TlWriter w; tl_writer_init(&w);
838 2 : tl_write_uint32(&w, 0xDEAD1234u);
839 2 : tl_write_uint32(&w, 0);
840 2 : tl_write_uint32(&w, 0);
841 2 : tl_write_uint32(&w, 0);
842 2 : mt_server_reply_result(ctx, w.data, w.len);
843 2 : tl_writer_free(&w);
844 2 : }
845 :
846 : /* download_loop: CDN redirect reply → download returns -1. */
847 2 : static void test_media_download_loop_cdn_redirect(void) {
848 2 : with_tmp_home("dl-cdn");
849 2 : mt_server_init(); mt_server_reset();
850 2 : MtProtoSession s; load_session(&s);
851 2 : mt_server_expect(CRC_upload_getFile, on_get_file_cdn_redirect, NULL);
852 :
853 2 : ApiConfig cfg; init_cfg(&cfg);
854 2 : Transport t; connect_mock(&t);
855 :
856 2 : MediaInfo mi; make_document_mi(&mi, "x.bin", "application/octet-stream");
857 2 : int wrong = 0;
858 2 : ASSERT(domain_download_document(&cfg, &s, &t, &mi,
859 : "/tmp/tg-cli-ft-rm-cdn.bin",
860 : &wrong) == -1,
861 : "CDN redirect causes download to return -1");
862 :
863 2 : transport_close(&t);
864 2 : mt_server_reset();
865 : }
866 :
867 : /* download_loop: unknown top CRC → download returns -1. */
868 2 : static void test_media_download_loop_unexpected_top(void) {
869 2 : with_tmp_home("dl-badtop");
870 2 : mt_server_init(); mt_server_reset();
871 2 : MtProtoSession s; load_session(&s);
872 2 : mt_server_expect(CRC_upload_getFile, on_get_file_unknown_top, NULL);
873 :
874 2 : ApiConfig cfg; init_cfg(&cfg);
875 2 : Transport t; connect_mock(&t);
876 :
877 2 : MediaInfo mi; make_document_mi(&mi, "x.bin", "application/octet-stream");
878 2 : int wrong = 0;
879 2 : ASSERT(domain_download_document(&cfg, &s, &t, &mi,
880 : "/tmp/tg-cli-ft-rm-badtop.bin",
881 : &wrong) == -1,
882 : "Unexpected top CRC causes download to return -1");
883 :
884 2 : transport_close(&t);
885 2 : mt_server_reset();
886 : }
887 :
888 : /* download_loop: api_call fails (bad_msg_notification) → download returns -1.
889 : * Covers the "api_call failed at offset" log branch. */
890 2 : static void test_media_download_loop_api_call_fail(void) {
891 2 : with_tmp_home("dl-apifail");
892 2 : mt_server_init(); mt_server_reset();
893 2 : MtProtoSession s; load_session(&s);
894 : /* Arm bad_msg_notification: api_call returns -1 on the first RPC. */
895 2 : mt_server_reply_bad_msg_notification(0, 16);
896 2 : mt_server_expect(CRC_upload_getFile, on_get_file_cdn_redirect, NULL);
897 :
898 2 : ApiConfig cfg; init_cfg(&cfg);
899 2 : Transport t; connect_mock(&t);
900 :
901 2 : MediaInfo mi; make_document_mi(&mi, "x.bin", "application/octet-stream");
902 2 : int wrong = 0;
903 2 : ASSERT(domain_download_document(&cfg, &s, &t, &mi,
904 : "/tmp/tg-cli-ft-rm-apifail.bin",
905 : &wrong) == -1,
906 : "api_call fail causes download to return -1");
907 :
908 2 : transport_close(&t);
909 2 : mt_server_reset();
910 : }
911 :
912 : /* download_loop: fopen fails because output directory does not exist. */
913 2 : static void test_media_download_loop_fopen_fail(void) {
914 2 : with_tmp_home("dl-fopen");
915 2 : mt_server_init(); mt_server_reset();
916 2 : MtProtoSession s; load_session(&s);
917 : /* No server expectation needed — fopen fails before the first RPC. */
918 :
919 2 : ApiConfig cfg; init_cfg(&cfg);
920 2 : Transport t; connect_mock(&t);
921 :
922 2 : MediaInfo mi; make_document_mi(&mi, "x.bin", "application/octet-stream");
923 2 : int wrong = 0;
924 : /* Path under a directory that does not exist. */
925 2 : ASSERT(domain_download_document(&cfg, &s, &t, &mi,
926 : "/tmp/tg-cli-no-such-dir/out.bin",
927 : &wrong) == -1,
928 : "fopen fail causes download to return -1");
929 :
930 2 : transport_close(&t);
931 2 : mt_server_reset();
932 : }
933 :
934 : /* Cache-hit copy path for photos: first download to path A, second to
935 : * path B (different path, same photo_id). The copy branch in
936 : * domain_download_photo runs and B must exist with identical content. */
937 2 : static void make_photo_mi(MediaInfo *mi) {
938 2 : memset(mi, 0, sizeof(*mi));
939 2 : mi->kind = MEDIA_PHOTO;
940 2 : mi->photo_id = 0xABCD1234EF56LL;
941 2 : mi->access_hash = (int64_t)0xCAFEBABEDEADBEEFLL;
942 2 : mi->dc_id = 2;
943 2 : mi->file_reference_len = 4;
944 2 : mi->file_reference[0] = 0x01; mi->file_reference[1] = 0x02;
945 2 : mi->file_reference[2] = 0x03; mi->file_reference[3] = 0x04;
946 2 : mi->thumb_type[0] = 'y'; mi->thumb_type[1] = '\0';
947 2 : }
948 :
949 2 : static void test_media_photo_cache_hit_copy(void) {
950 2 : with_tmp_home("dl-cache-copy-photo");
951 2 : mt_server_init(); mt_server_reset();
952 2 : MtProtoSession s; load_session(&s);
953 2 : mt_server_expect(CRC_upload_getFile, on_get_file_short_doc, NULL);
954 :
955 2 : ApiConfig cfg; init_cfg(&cfg);
956 2 : Transport t; connect_mock(&t);
957 :
958 2 : MediaInfo mi; make_photo_mi(&mi);
959 2 : const char *path_a = "/tmp/tg-cli-ft-rm-cache-photo-a.bin";
960 2 : const char *path_b = "/tmp/tg-cli-ft-rm-cache-photo-b.bin";
961 2 : unlink(path_a); unlink(path_b);
962 :
963 : /* First download → server is called, result cached at path_a. */
964 2 : int wrong = 0;
965 2 : ASSERT(domain_download_photo(&cfg, &s, &t, &mi, path_a, &wrong) == 0,
966 : "first photo download ok");
967 :
968 : /* Second download to a DIFFERENT path → cache-hit copy branch fires. */
969 2 : ASSERT(domain_download_photo(&cfg, &s, &t, &mi, path_b, &wrong) == 0,
970 : "second photo download (different path) ok via cache copy");
971 :
972 : struct stat st_a, st_b;
973 2 : ASSERT(stat(path_a, &st_a) == 0, "path_a exists");
974 2 : ASSERT(stat(path_b, &st_b) == 0, "path_b exists after copy");
975 2 : ASSERT(st_a.st_size == st_b.st_size, "copy has same size as original");
976 :
977 2 : unlink(path_a); unlink(path_b);
978 2 : transport_close(&t);
979 2 : mt_server_reset();
980 : }
981 :
982 : /* Cache-hit copy path for documents: first download to path A, second to
983 : * path B — exercises the copy branch in domain_download_document. */
984 2 : static void test_media_document_cache_hit_copy(void) {
985 2 : with_tmp_home("dl-cache-copy-doc");
986 2 : mt_server_init(); mt_server_reset();
987 2 : MtProtoSession s; load_session(&s);
988 2 : mt_server_expect(CRC_upload_getFile, on_get_file_short_doc, NULL);
989 :
990 2 : ApiConfig cfg; init_cfg(&cfg);
991 2 : Transport t; connect_mock(&t);
992 :
993 2 : MediaInfo mi; make_document_mi(&mi, "doc.bin", "application/octet-stream");
994 2 : const char *path_a = "/tmp/tg-cli-ft-rm-cache-doc-a.bin";
995 2 : const char *path_b = "/tmp/tg-cli-ft-rm-cache-doc-b.bin";
996 2 : unlink(path_a); unlink(path_b);
997 :
998 2 : int wrong = 0;
999 2 : ASSERT(domain_download_document(&cfg, &s, &t, &mi, path_a, &wrong) == 0,
1000 : "first document download ok");
1001 :
1002 2 : ASSERT(domain_download_document(&cfg, &s, &t, &mi, path_b, &wrong) == 0,
1003 : "second document download (different path) ok via cache copy");
1004 :
1005 : struct stat st_a, st_b;
1006 2 : ASSERT(stat(path_a, &st_a) == 0, "path_a exists");
1007 2 : ASSERT(stat(path_b, &st_b) == 0, "path_b exists after copy");
1008 2 : ASSERT(st_a.st_size == st_b.st_size, "copy has same size as original");
1009 :
1010 2 : unlink(path_a); unlink(path_b);
1011 2 : transport_close(&t);
1012 2 : mt_server_reset();
1013 : }
1014 :
1015 : /* cross-DC FILE_MIGRATE: home DC returns FILE_MIGRATE_4 on upload.getFile;
1016 : * domain_download_media_cross_dc enters the retry path (lines 273+).
1017 : * We test two sub-cases:
1018 : * A) dc_session_open fails → domain_download_media_cross_dc returns -1
1019 : * (covers the LOG_INFO "FILE_MIGRATE" line and the dc_session_open
1020 : * failure branch at lines 277-279).
1021 : * B) dc_session_open succeeds (pre-seeded DC4) but the DC4 download
1022 : * also returns FILE_MIGRATE → dummy wrong_dc ignored, returns -1
1023 : * (covers dc_session_ensure_authorized fast path + download_any
1024 : * + dc_session_close at lines 285-300).
1025 : */
1026 6 : static void on_get_file_always_file_migrate(MtRpcContext *ctx) {
1027 : /* Always returns FILE_MIGRATE_4 — used in sub-case B where we need
1028 : * the home-DC call to set wrong_dc=4 and the DC4 call to also fail. */
1029 6 : mt_server_arm_reconnect();
1030 6 : mt_server_reply_error(ctx, 303, "FILE_MIGRATE_4");
1031 6 : }
1032 :
1033 2 : static void test_media_cross_dc_session_open_fails(void) {
1034 : /* Sub-case A: the home DC returns FILE_MIGRATE_4, then dc_session_open
1035 : * for DC4 fails (mock connect failure) → cross_dc returns -1.
1036 : * Covers lines 273, 277-279. */
1037 2 : with_tmp_home("dl-xdc-openfail");
1038 2 : mt_server_init(); mt_server_reset();
1039 :
1040 2 : MtProtoSession s; load_session(&s);
1041 2 : mt_server_expect(CRC_upload_getFile, on_get_file_always_file_migrate, NULL);
1042 :
1043 2 : ApiConfig cfg; init_cfg(&cfg);
1044 2 : Transport t; connect_mock(&t);
1045 :
1046 : /* Do NOT seed DC4 session → session_store_load_dc(4) fails → dc_session
1047 : * falls through to the DH handshake path → auth_flow_connect_dc runs
1048 : * transport_connect which succeeds (mock), but the handshake itself fails
1049 : * because there is no proper DH responder set up. This causes
1050 : * dc_session_open to return -1. */
1051 2 : mock_socket_fail_connect(); /* make the DC4 transport_connect fail */
1052 :
1053 2 : MediaInfo mi; make_document_mi(&mi, "xdc.bin", "application/octet-stream");
1054 2 : const char *out = "/tmp/tg-cli-ft-rm-xdc-openfail.bin";
1055 2 : unlink(out);
1056 :
1057 2 : ASSERT(domain_download_media_cross_dc(&cfg, &s, &t, &mi, out) == -1,
1058 : "cross_dc returns -1 when dc_session_open fails after FILE_MIGRATE_4");
1059 :
1060 2 : unlink(out);
1061 2 : transport_close(&t);
1062 2 : mt_server_reset();
1063 : }
1064 :
1065 2 : static void test_media_cross_dc_download_any_on_foreign_dc(void) {
1066 : /* Sub-case B: home DC returns FILE_MIGRATE_4, DC4 session opens OK
1067 : * (pre-seeded), but DC4's download also returns an rpc_error (not a
1068 : * migration this time — 400 MEDIA_INVALID) so download_any fails on the
1069 : * foreign DC, covering lines 293-300. */
1070 2 : with_tmp_home("dl-xdc-foreign");
1071 2 : mt_server_init(); mt_server_reset();
1072 :
1073 2 : MtProtoSession s; load_session(&s);
1074 2 : ASSERT(mt_server_seed_extra_dc(4) == 0, "seed DC4 session");
1075 : /* Same handler for both calls: first call sets wrong_dc=4 (home DC),
1076 : * reconnect is armed; second call (DC4) also returns an rpc_error
1077 : * (400, not a migration) so download_any on DC4 fails. */
1078 2 : mt_server_expect(CRC_upload_getFile, on_get_file_always_file_migrate, NULL);
1079 :
1080 2 : ApiConfig cfg; init_cfg(&cfg);
1081 2 : Transport t; connect_mock(&t);
1082 :
1083 2 : MediaInfo mi; make_document_mi(&mi, "xdc.bin", "application/octet-stream");
1084 2 : const char *out = "/tmp/tg-cli-ft-rm-xdc-foreign.bin";
1085 2 : unlink(out);
1086 :
1087 : /* Both home DC and DC4 return FILE_MIGRATE_4. The DC4 call receives the
1088 : * migrate error but since wrong_dc is the dummy parameter,
1089 : * domain_download_media_cross_dc returns -1 (download_any rc != 0). */
1090 2 : ASSERT(domain_download_media_cross_dc(&cfg, &s, &t, &mi, out) == -1,
1091 : "cross_dc returns -1 when DC4 download also fails");
1092 :
1093 2 : unlink(out);
1094 2 : transport_close(&t);
1095 2 : mt_server_reset();
1096 : }
1097 :
1098 2 : void run_rich_media_types_tests(void) {
1099 : /* Parse-path coverage — one test per media variant. */
1100 2 : RUN_TEST(test_rich_media_parse_geo);
1101 2 : RUN_TEST(test_rich_media_parse_contact);
1102 2 : RUN_TEST(test_rich_media_parse_webpage);
1103 2 : RUN_TEST(test_rich_media_parse_poll);
1104 2 : RUN_TEST(test_rich_media_parse_photo_metadata);
1105 2 : RUN_TEST(test_rich_media_parse_document_video);
1106 2 : RUN_TEST(test_rich_media_parse_document_round_video);
1107 2 : RUN_TEST(test_rich_media_parse_document_audio_music);
1108 2 : RUN_TEST(test_rich_media_parse_document_voice_note);
1109 2 : RUN_TEST(test_rich_media_parse_document_sticker);
1110 2 : RUN_TEST(test_rich_media_parse_document_animation);
1111 2 : RUN_TEST(test_rich_media_parse_document_filename_captured);
1112 :
1113 : /* Download-path coverage for media.c guards + cross-DC wrapper. */
1114 2 : RUN_TEST(test_rich_media_download_voice_note_chunked);
1115 2 : RUN_TEST(test_rich_media_download_sticker_chunked);
1116 2 : RUN_TEST(test_rich_media_download_video_chunked);
1117 2 : RUN_TEST(test_rich_media_download_photo_rejects_document_kind);
1118 2 : RUN_TEST(test_rich_media_download_document_rejects_photo_kind);
1119 2 : RUN_TEST(test_rich_media_cross_dc_home_succeeds);
1120 2 : RUN_TEST(test_rich_media_cross_dc_rejects_unsupported_kind);
1121 :
1122 : /* download_loop error branches and cache-copy paths (media.c ≥90%). */
1123 2 : RUN_TEST(test_media_download_loop_cdn_redirect);
1124 2 : RUN_TEST(test_media_download_loop_unexpected_top);
1125 2 : RUN_TEST(test_media_download_loop_api_call_fail);
1126 2 : RUN_TEST(test_media_download_loop_fopen_fail);
1127 2 : RUN_TEST(test_media_photo_cache_hit_copy);
1128 2 : RUN_TEST(test_media_document_cache_hit_copy);
1129 2 : RUN_TEST(test_media_cross_dc_session_open_fails);
1130 2 : RUN_TEST(test_media_cross_dc_download_any_on_foreign_dc);
1131 2 : }
|