LCOV - code coverage report
Current view: top level - tests/functional - test_rich_media_types.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 100.0 % 626 626
Test Date: 2026-04-20 19:54:22 Functions: 100.0 % 63 63

            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 : }
        

Generated by: LCOV version 2.0-1