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

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

Generated by: LCOV version 2.0-1