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

            Line data    Source code
       1              : /**
       2              :  * @file test_read_path.c
       3              :  * @brief FT-04 — read-path functional tests through the mock server.
       4              :  *
       5              :  * Covers the minimum viable read surface: self profile, dialogs,
       6              :  * history, contacts, resolve-username, and updates.state /
       7              :  * updates.getDifference. Every test wires real production parser code
       8              :  * (domain_*) against in-process responders that emit canonical TL
       9              :  * envelopes — the bytes the client sees are byte-for-byte what Telegram
      10              :  * would put on the wire for that constructor.
      11              :  */
      12              : 
      13              : #include "test_helpers.h"
      14              : 
      15              : #include "mock_socket.h"
      16              : #include "mock_tel_server.h"
      17              : 
      18              : #include "api_call.h"
      19              : #include "mtproto_session.h"
      20              : #include "transport.h"
      21              : #include "app/session_store.h"
      22              : #include "tl_registry.h"
      23              : #include "tl_serial.h"
      24              : 
      25              : #include "domain/read/self.h"
      26              : #include "domain/read/dialogs.h"
      27              : #include "domain/read/history.h"
      28              : #include "domain/read/contacts.h"
      29              : #include "domain/read/user_info.h"
      30              : #include "domain/read/updates.h"
      31              : #include "domain/read/search.h"
      32              : #include "arg_parse.h"
      33              : 
      34              : /* for resolve cache flush */
      35              : extern void resolve_cache_flush(void);
      36              : 
      37              : #include <stdio.h>
      38              : #include <stdlib.h>
      39              : #include <string.h>
      40              : #include <unistd.h>
      41              : 
      42              : /* ---- CRCs not already surfaced by public headers ---- */
      43              : #define CRC_messages_search           0x29ee847aU
      44              : #define CRC_messages_searchGlobal     0x4bc6589aU
      45              : #define CRC_inputMessagesFilterEmpty  0x57e9a944U
      46              : #define CRC_users_getUsers            0x0d91a548U
      47              : #define CRC_inputUserSelf             0xf7c1b13fU
      48              : #define CRC_messages_getDialogs       0xa0f4cb4fU
      49              : #define CRC_dialog                    0xd58a08c6U
      50              : #define CRC_messages_getHistory       0x4423e6c5U
      51              : #define CRC_contacts_getContacts      0x5dd69e12U
      52              : #define CRC_contact                   0x145ade0bU
      53              : #define CRC_contacts_resolveUsername  0xf93ccba3U
      54              : #define CRC_users_getFullUser         0xb9f11a99U
      55              : #define CRC_users_userFull            0x3b6d152eU
      56              : /* inner userFull object — matches TL_userFull in tl_registry.h */
      57              : #define CRC_userFull_inner            0x93eadb53U
      58              : #define CRC_updates_getState          0xedd4882aU
      59              : #define CRC_updates_getDifference     0x19c2f763U
      60              : #define CRC_peerNotifySettings        0xa83b0426U
      61              : 
      62              : /* InputPeer CRCs (wire values for TEST-06 assertions). */
      63              : #define CRC_inputPeerSelf             0x7da07ec9U
      64              : #define CRC_inputPeerUser             0xdde8a54cU
      65              : #define CRC_inputPeerChannel          0x27bcbbfcU
      66              : 
      67              : /* ---- helpers ---- */
      68              : 
      69           62 : static void with_tmp_home(const char *tag) {
      70              :     char tmp[256];
      71           62 :     snprintf(tmp, sizeof(tmp), "/tmp/tg-cli-ft-read-%s", tag);
      72              :     char bin[512];
      73           62 :     snprintf(bin, sizeof(bin), "%s/.config/tg-cli/session.bin", tmp);
      74           62 :     (void)unlink(bin);
      75           62 :     setenv("HOME", tmp, 1);
      76           62 : }
      77              : 
      78           62 : static void connect_mock(Transport *t) {
      79           62 :     transport_init(t);
      80           62 :     ASSERT(transport_connect(t, "127.0.0.1", 443) == 0, "connect");
      81              : }
      82              : 
      83           62 : static void init_cfg(ApiConfig *cfg) {
      84           62 :     api_config_init(cfg);
      85           62 :     cfg->api_id = 12345;
      86           62 :     cfg->api_hash = "deadbeefcafebabef00dbaadfeedc0de";
      87           62 : }
      88              : 
      89           62 : static void load_session(MtProtoSession *s) {
      90           62 :     ASSERT(mt_server_seed_session(2, NULL, NULL, NULL) == 0, "seed");
      91           62 :     mtproto_session_init(s);
      92           62 :     int dc = 0;
      93           62 :     ASSERT(session_store_load(s, &dc) == 0, "load session");
      94              : }
      95              : 
      96              : /* ================================================================ */
      97              : /* Responders                                                       */
      98              : /* ================================================================ */
      99              : 
     100              : /* Vector<User> with one userEmpty (simplest user: id only). */
     101            2 : static void on_get_self(MtRpcContext *ctx) {
     102              :     TlWriter w;
     103            2 :     tl_writer_init(&w);
     104            2 :     tl_write_uint32(&w, TL_vector);
     105            2 :     tl_write_uint32(&w, 1);
     106            2 :     tl_write_uint32(&w, TL_userEmpty);
     107            2 :     tl_write_int64 (&w, 99001LL);
     108            2 :     mt_server_reply_result(ctx, w.data, w.len);
     109            2 :     tl_writer_free(&w);
     110            2 : }
     111              : 
     112              : /* Vector<User> with one full user that has premium flag set (flags2.3). */
     113            2 : static void on_get_self_premium(MtRpcContext *ctx) {
     114              :     TlWriter w;
     115            2 :     tl_writer_init(&w);
     116            2 :     tl_write_uint32(&w, TL_vector);
     117            2 :     tl_write_uint32(&w, 1);
     118            2 :     tl_write_uint32(&w, TL_user);
     119              :     /* flags: has_first_name (1) | has_phone (4) */
     120            2 :     uint32_t flags = (1u << 1) | (1u << 4);
     121            2 :     tl_write_uint32(&w, flags);
     122              :     /* flags2: premium bit is flags2.3 */
     123            2 :     tl_write_uint32(&w, (1u << 3));
     124            2 :     tl_write_int64 (&w, 77002LL);       /* id */
     125            2 :     tl_write_string(&w, "Premium");     /* first_name */
     126            2 :     tl_write_string(&w, "+19995550001");/* phone */
     127            2 :     mt_server_reply_result(ctx, w.data, w.len);
     128            2 :     tl_writer_free(&w);
     129            2 : }
     130              : 
     131              : /* messages.dialogs with 0 dialogs / messages / chats / users. */
     132            6 : static void on_dialogs_empty(MtRpcContext *ctx) {
     133              :     TlWriter w;
     134            6 :     tl_writer_init(&w);
     135            6 :     tl_write_uint32(&w, TL_messages_dialogs);
     136            6 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0); /* dialogs */
     137            6 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0); /* messages */
     138            6 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0); /* chats */
     139            6 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0); /* users */
     140            6 :     mt_server_reply_result(ctx, w.data, w.len);
     141            6 :     tl_writer_free(&w);
     142            6 : }
     143              : 
     144              : /* messages.dialogs with one user-peer dialog (id=555, unread=7). */
     145            2 : static void on_dialogs_one_user(MtRpcContext *ctx) {
     146              :     TlWriter w;
     147            2 :     tl_writer_init(&w);
     148            2 :     tl_write_uint32(&w, TL_messages_dialogs);
     149              : 
     150              :     /* dialogs: Vector<Dialog> with 1 entry */
     151            2 :     tl_write_uint32(&w, TL_vector);
     152            2 :     tl_write_uint32(&w, 1);
     153            2 :     tl_write_uint32(&w, CRC_dialog);
     154            2 :     tl_write_uint32(&w, 0);                  /* flags=0 — no optional fields */
     155            2 :     tl_write_uint32(&w, TL_peerUser);        /* peer */
     156            2 :     tl_write_int64 (&w, 555LL);
     157            2 :     tl_write_int32 (&w, 1200);               /* top_message */
     158            2 :     tl_write_int32 (&w, 0);                  /* read_inbox_max_id */
     159            2 :     tl_write_int32 (&w, 0);                  /* read_outbox_max_id */
     160            2 :     tl_write_int32 (&w, 7);                  /* unread_count */
     161            2 :     tl_write_int32 (&w, 0);                  /* unread_mentions_count */
     162            2 :     tl_write_int32 (&w, 0);                  /* unread_reactions_count */
     163              :     /* peerNotifySettings with flags=0 — no sub-fields. */
     164            2 :     tl_write_uint32(&w, CRC_peerNotifySettings);
     165            2 :     tl_write_uint32(&w, 0);
     166              : 
     167              :     /* messages vector: empty */
     168            2 :     tl_write_uint32(&w, TL_vector);
     169            2 :     tl_write_uint32(&w, 0);
     170              : 
     171              :     /* chats vector: empty */
     172            2 :     tl_write_uint32(&w, TL_vector);
     173            2 :     tl_write_uint32(&w, 0);
     174              : 
     175              :     /* users vector: one user with access_hash only (flags.0=1, flags2=0) */
     176            2 :     tl_write_uint32(&w, TL_vector);
     177            2 :     tl_write_uint32(&w, 1);
     178            2 :     tl_write_uint32(&w, TL_user);
     179            2 :     tl_write_uint32(&w, 1u);                 /* flags: has access_hash */
     180            2 :     tl_write_uint32(&w, 0);                  /* flags2 */
     181            2 :     tl_write_int64 (&w, 555LL);              /* id */
     182            2 :     tl_write_int64 (&w, 0xAABBCCDDEEFF0011LL);/* access_hash */
     183              : 
     184            2 :     mt_server_reply_result(ctx, w.data, w.len);
     185            2 :     tl_writer_free(&w);
     186            2 : }
     187              : 
     188              : /* messages.dialogsSlice#71e094f3 — two entries returned from a server that
     189              :  * has 50 total dialogs.  The first is a user peer (id=777, unread=3) and the
     190              :  * second is a channel peer (id=888, unread=0).  Users/chats vectors are
     191              :  * minimal (no access_hash on either) so the title join leaves titles empty —
     192              :  * we are testing the slice parse path, not the join. */
     193            2 : static void on_dialogs_slice(MtRpcContext *ctx) {
     194              :     TlWriter w;
     195            2 :     tl_writer_init(&w);
     196            2 :     tl_write_uint32(&w, TL_messages_dialogsSlice);
     197            2 :     tl_write_int32 (&w, 50);               /* count — total on server */
     198              : 
     199              :     /* dialogs: Vector<Dialog> with 2 entries */
     200            2 :     tl_write_uint32(&w, TL_vector);
     201            2 :     tl_write_uint32(&w, 2);
     202              : 
     203              :     /* dialog 0: user peer id=777 unread=3 top=42 */
     204            2 :     tl_write_uint32(&w, CRC_dialog);
     205            2 :     tl_write_uint32(&w, 0);               /* flags=0 */
     206            2 :     tl_write_uint32(&w, TL_peerUser);
     207            2 :     tl_write_int64 (&w, 777LL);
     208            2 :     tl_write_int32 (&w, 42);              /* top_message */
     209            2 :     tl_write_int32 (&w, 0);              /* read_inbox_max_id */
     210            2 :     tl_write_int32 (&w, 0);              /* read_outbox_max_id */
     211            2 :     tl_write_int32 (&w, 3);              /* unread_count */
     212            2 :     tl_write_int32 (&w, 0);              /* unread_mentions_count */
     213            2 :     tl_write_int32 (&w, 0);              /* unread_reactions_count */
     214            2 :     tl_write_uint32(&w, CRC_peerNotifySettings);
     215            2 :     tl_write_uint32(&w, 0);
     216              : 
     217              :     /* dialog 1: channel peer id=888 unread=0 top=99 */
     218            2 :     tl_write_uint32(&w, CRC_dialog);
     219            2 :     tl_write_uint32(&w, 0);               /* flags=0 */
     220            2 :     tl_write_uint32(&w, TL_peerChannel);
     221            2 :     tl_write_int64 (&w, 888LL);
     222            2 :     tl_write_int32 (&w, 99);             /* top_message */
     223            2 :     tl_write_int32 (&w, 0);
     224            2 :     tl_write_int32 (&w, 0);
     225            2 :     tl_write_int32 (&w, 0);             /* unread_count */
     226            2 :     tl_write_int32 (&w, 0);
     227            2 :     tl_write_int32 (&w, 0);
     228            2 :     tl_write_uint32(&w, CRC_peerNotifySettings);
     229            2 :     tl_write_uint32(&w, 0);
     230              : 
     231              :     /* messages vector: empty */
     232            2 :     tl_write_uint32(&w, TL_vector);
     233            2 :     tl_write_uint32(&w, 0);
     234              : 
     235              :     /* chats vector: empty */
     236            2 :     tl_write_uint32(&w, TL_vector);
     237            2 :     tl_write_uint32(&w, 0);
     238              : 
     239              :     /* users vector: empty */
     240            2 :     tl_write_uint32(&w, TL_vector);
     241            2 :     tl_write_uint32(&w, 0);
     242              : 
     243            2 :     mt_server_reply_result(ctx, w.data, w.len);
     244            2 :     tl_writer_free(&w);
     245            2 : }
     246              : 
     247              : /* messages.dialogsNotModified#f0e3e596 count:int — server says nothing changed;
     248              :  * reports 37 total dialogs in the cache. */
     249            2 : static void on_dialogs_not_modified(MtRpcContext *ctx) {
     250              :     TlWriter w;
     251            2 :     tl_writer_init(&w);
     252            2 :     tl_write_uint32(&w, TL_messages_dialogsNotModified);
     253            2 :     tl_write_int32 (&w, 37);    /* count */
     254            2 :     mt_server_reply_result(ctx, w.data, w.len);
     255            2 :     tl_writer_free(&w);
     256            2 : }
     257              : 
     258              : /* messages.messages empty. */
     259           12 : static void on_history_empty(MtRpcContext *ctx) {
     260              :     TlWriter w;
     261           12 :     tl_writer_init(&w);
     262           12 :     tl_write_uint32(&w, TL_messages_messages);
     263           12 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0); /* messages */
     264           12 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0); /* chats */
     265           12 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0); /* users */
     266           12 :     mt_server_reply_result(ctx, w.data, w.len);
     267           12 :     tl_writer_free(&w);
     268           12 : }
     269              : 
     270              : /* messages.messages with one messageEmpty (id=42, no peer). */
     271            2 : static void on_history_one_empty(MtRpcContext *ctx) {
     272              :     TlWriter w;
     273            2 :     tl_writer_init(&w);
     274            2 :     tl_write_uint32(&w, TL_messages_messages);
     275            2 :     tl_write_uint32(&w, TL_vector);
     276            2 :     tl_write_uint32(&w, 1);
     277            2 :     tl_write_uint32(&w, TL_messageEmpty);
     278            2 :     tl_write_uint32(&w, 0);                   /* flags */
     279            2 :     tl_write_int32 (&w, 42);                  /* id */
     280              :     /* No peer (flags.0 off) */
     281            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0); /* chats */
     282            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0); /* users */
     283            2 :     mt_server_reply_result(ctx, w.data, w.len);
     284            2 :     tl_writer_free(&w);
     285            2 : }
     286              : 
     287              : /* contacts.contacts with empty vector. */
     288            2 : static void on_contacts_empty(MtRpcContext *ctx) {
     289              :     TlWriter w;
     290            2 :     tl_writer_init(&w);
     291            2 :     tl_write_uint32(&w, TL_contacts_contacts);
     292            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0); /* contacts */
     293            2 :     tl_write_uint32(&w, 0);                                 /* saved_count */
     294            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0); /* users */
     295            2 :     mt_server_reply_result(ctx, w.data, w.len);
     296            2 :     tl_writer_free(&w);
     297            2 : }
     298              : 
     299              : /* contacts.contacts with two entries (mutual + non-mutual). */
     300            2 : static void on_contacts_two(MtRpcContext *ctx) {
     301              :     TlWriter w;
     302            2 :     tl_writer_init(&w);
     303            2 :     tl_write_uint32(&w, TL_contacts_contacts);
     304            2 :     tl_write_uint32(&w, TL_vector);
     305            2 :     tl_write_uint32(&w, 2);
     306              :     /* contact#145ade0b user_id:long mutual:Bool */
     307            2 :     tl_write_uint32(&w, CRC_contact);
     308            2 :     tl_write_int64 (&w, 101LL);
     309            2 :     tl_write_uint32(&w, TL_boolTrue);
     310            2 :     tl_write_uint32(&w, CRC_contact);
     311            2 :     tl_write_int64 (&w, 202LL);
     312            2 :     tl_write_uint32(&w, TL_boolFalse);
     313            2 :     tl_write_uint32(&w, 0);
     314            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0);
     315            2 :     mt_server_reply_result(ctx, w.data, w.len);
     316            2 :     tl_writer_free(&w);
     317            2 : }
     318              : 
     319              : /* contacts.resolvedPeer pointing at user id 8001 with access_hash. */
     320            8 : static void on_resolve_user(MtRpcContext *ctx) {
     321              :     TlWriter w;
     322            8 :     tl_writer_init(&w);
     323            8 :     tl_write_uint32(&w, TL_contacts_resolvedPeer);
     324            8 :     tl_write_uint32(&w, TL_peerUser);
     325            8 :     tl_write_int64 (&w, 8001LL);
     326              :     /* chats vector: empty */
     327            8 :     tl_write_uint32(&w, TL_vector);
     328            8 :     tl_write_uint32(&w, 0);
     329              :     /* users vector: one user */
     330            8 :     tl_write_uint32(&w, TL_vector);
     331            8 :     tl_write_uint32(&w, 1);
     332            8 :     tl_write_uint32(&w, TL_user);
     333            8 :     tl_write_uint32(&w, 1u);                 /* flags.0 → access_hash */
     334            8 :     tl_write_uint32(&w, 0);                  /* flags2 */
     335            8 :     tl_write_int64 (&w, 8001LL);
     336            8 :     tl_write_int64 (&w, 0xDEADBEEFCAFEBABEULL);
     337            8 :     mt_server_reply_result(ctx, w.data, w.len);
     338            8 :     tl_writer_free(&w);
     339            8 : }
     340              : 
     341            2 : static void on_resolve_not_found(MtRpcContext *ctx) {
     342            2 :     mt_server_reply_error(ctx, 400, "USERNAME_NOT_OCCUPIED");
     343            2 : }
     344              : 
     345              : /* updates.state pts=100 qts=5 date=1700000000 seq=1 unread=3 */
     346            2 : static void on_updates_state(MtRpcContext *ctx) {
     347              :     TlWriter w;
     348            2 :     tl_writer_init(&w);
     349            2 :     tl_write_uint32(&w, TL_updates_state);
     350            2 :     tl_write_int32 (&w, 100);
     351            2 :     tl_write_int32 (&w, 5);
     352            2 :     tl_write_int32 (&w, 1700000000);
     353            2 :     tl_write_int32 (&w, 1);
     354            2 :     tl_write_int32 (&w, 3);
     355            2 :     mt_server_reply_result(ctx, w.data, w.len);
     356            2 :     tl_writer_free(&w);
     357            2 : }
     358              : 
     359              : /* updates.differenceEmpty — the trivial "nothing changed" reply. */
     360            2 : static void on_updates_diff_empty(MtRpcContext *ctx) {
     361              :     TlWriter w;
     362            2 :     tl_writer_init(&w);
     363            2 :     tl_write_uint32(&w, TL_updates_differenceEmpty);
     364            2 :     tl_write_int32 (&w, 1700000500);             /* date */
     365            2 :     tl_write_int32 (&w, 2);                      /* seq */
     366            2 :     mt_server_reply_result(ctx, w.data, w.len);
     367            2 :     tl_writer_free(&w);
     368            2 : }
     369              : 
     370              : /* updates.difference#00f49d63 with one plain message (id=501, date=1700001000,
     371              :  * text="hello from diff").  After the new_messages vector we include the
     372              :  * remaining required vectors (new_encrypted_messages, other_updates, chats,
     373              :  * users) as empty so the wire is well-formed. */
     374            2 : static void on_updates_diff_with_messages(MtRpcContext *ctx) {
     375              :     TlWriter w;
     376            2 :     tl_writer_init(&w);
     377            2 :     tl_write_uint32(&w, TL_updates_difference);
     378              : 
     379              :     /* new_messages: Vector<Message> — one entry */
     380            2 :     tl_write_uint32(&w, TL_vector);
     381            2 :     tl_write_uint32(&w, 1);
     382              :     /* message#94345242 flags=0 flags2=0 id=501 peer=peerUser(1) date=1700001000
     383              :      * message="hello from diff" */
     384            2 :     tl_write_uint32(&w, TL_message);
     385            2 :     tl_write_uint32(&w, 0);              /* flags = 0 */
     386            2 :     tl_write_uint32(&w, 0);              /* flags2 = 0 */
     387            2 :     tl_write_int32 (&w, 501);            /* id */
     388            2 :     tl_write_uint32(&w, TL_peerUser);
     389            2 :     tl_write_int64 (&w, 1LL);            /* peer user id */
     390            2 :     tl_write_int32 (&w, 1700001000);     /* date */
     391            2 :     tl_write_string(&w, "hello from diff"); /* message text */
     392              : 
     393              :     /* new_encrypted_messages: empty */
     394            2 :     tl_write_uint32(&w, TL_vector);
     395            2 :     tl_write_uint32(&w, 0);
     396              :     /* other_updates: empty */
     397            2 :     tl_write_uint32(&w, TL_vector);
     398            2 :     tl_write_uint32(&w, 0);
     399              :     /* chats: empty */
     400            2 :     tl_write_uint32(&w, TL_vector);
     401            2 :     tl_write_uint32(&w, 0);
     402              :     /* users: empty */
     403            2 :     tl_write_uint32(&w, TL_vector);
     404            2 :     tl_write_uint32(&w, 0);
     405              :     /* state: updates.state pts=110 qts=5 date=1700001000 seq=2 unread=0 */
     406            2 :     tl_write_uint32(&w, TL_updates_state);
     407            2 :     tl_write_int32 (&w, 110);
     408            2 :     tl_write_int32 (&w, 5);
     409            2 :     tl_write_int32 (&w, 1700001000);
     410            2 :     tl_write_int32 (&w, 2);
     411            2 :     tl_write_int32 (&w, 0);
     412              : 
     413            2 :     mt_server_reply_result(ctx, w.data, w.len);
     414            2 :     tl_writer_free(&w);
     415            2 : }
     416              : 
     417              : /* updates.differenceSlice#a8fb1981 with two plain messages.
     418              :  * Same shape as difference but uses the Slice constructor and an
     419              :  * intermediate_state instead of state. */
     420            2 : static void on_updates_diff_slice_with_messages(MtRpcContext *ctx) {
     421              :     TlWriter w;
     422            2 :     tl_writer_init(&w);
     423            2 :     tl_write_uint32(&w, TL_updates_differenceSlice);
     424              : 
     425              :     /* new_messages: Vector<Message> — two entries */
     426            2 :     tl_write_uint32(&w, TL_vector);
     427            2 :     tl_write_uint32(&w, 2);
     428              : 
     429              :     /* message 0: id=601 date=1700002000 text="first slice msg" */
     430            2 :     tl_write_uint32(&w, TL_message);
     431            2 :     tl_write_uint32(&w, 0); tl_write_uint32(&w, 0);
     432            2 :     tl_write_int32 (&w, 601);
     433            2 :     tl_write_uint32(&w, TL_peerUser); tl_write_int64(&w, 1LL);
     434            2 :     tl_write_int32 (&w, 1700002000);
     435            2 :     tl_write_string(&w, "first slice msg");
     436              : 
     437              :     /* message 1: id=602 date=1700002001 text="second slice msg" */
     438            2 :     tl_write_uint32(&w, TL_message);
     439            2 :     tl_write_uint32(&w, 0); tl_write_uint32(&w, 0);
     440            2 :     tl_write_int32 (&w, 602);
     441            2 :     tl_write_uint32(&w, TL_peerUser); tl_write_int64(&w, 2LL);
     442            2 :     tl_write_int32 (&w, 1700002001);
     443            2 :     tl_write_string(&w, "second slice msg");
     444              : 
     445              :     /* new_encrypted_messages: empty */
     446            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0);
     447              :     /* other_updates: empty */
     448            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0);
     449              :     /* chats: empty */
     450            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0);
     451              :     /* users: empty */
     452            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0);
     453              :     /* intermediate_state (differenceSlice uses this instead of state) */
     454            2 :     tl_write_uint32(&w, TL_updates_state);
     455            2 :     tl_write_int32 (&w, 120);
     456            2 :     tl_write_int32 (&w, 5);
     457            2 :     tl_write_int32 (&w, 1700002001);
     458            2 :     tl_write_int32 (&w, 3);
     459            2 :     tl_write_int32 (&w, 0);
     460              : 
     461            2 :     mt_server_reply_result(ctx, w.data, w.len);
     462            2 :     tl_writer_free(&w);
     463            2 : }
     464              : 
     465              : /* TEST-28: capture getDialogs request fields (flags + folder_id).
     466              :  *
     467              :  * messages.getDialogs layout after inner-CRC:
     468              :  *   flags:int32  [folder_id:int32 if flags.1]  offset_date:int32
     469              :  *   offset_id:int32  offset_peer:InputPeer  limit:int32  hash:int64
     470              :  *
     471              :  * We only need to inspect the first 8 (archived) or 4 (inbox) bytes
     472              :  * after the leading CRC. */
     473              : typedef struct {
     474              :     uint32_t flags;
     475              :     int32_t  folder_id; /* 0 if not present on the wire */
     476              : } CapturedDialogsReq;
     477              : 
     478              : static CapturedDialogsReq g_dialogs_req;
     479              : 
     480            4 : static void on_dialogs_capture_and_reply(MtRpcContext *ctx) {
     481            4 :     memset(&g_dialogs_req, 0, sizeof(g_dialogs_req));
     482              :     /* req_body starts with CRC_messages_getDialogs (4 bytes). Skip it. */
     483            4 :     if (ctx->req_body_len >= 8) {
     484            4 :         const uint8_t *p = ctx->req_body + 4; /* skip CRC */
     485            4 :         g_dialogs_req.flags = (uint32_t)p[0] | ((uint32_t)p[1] << 8)
     486            4 :                             | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
     487              :         /* folder_id is present when flags bit 1 is set */
     488            4 :         if ((g_dialogs_req.flags & (1u << 1)) && ctx->req_body_len >= 12) {
     489            2 :             const uint8_t *fp = p + 4;
     490            2 :             g_dialogs_req.folder_id = (int32_t)((uint32_t)fp[0]
     491            2 :                                     | ((uint32_t)fp[1] << 8)
     492            2 :                                     | ((uint32_t)fp[2] << 16)
     493            2 :                                     | ((uint32_t)fp[3] << 24));
     494              :         }
     495              :     }
     496              :     /* Always reply with an empty dialogs so the call completes. */
     497            4 :     on_dialogs_empty(ctx);
     498            4 : }
     499              : 
     500              : /* Generic handler for asserting RPC errors propagate. */
     501            2 : static void on_generic_500(MtRpcContext *ctx) {
     502            2 :     mt_server_reply_error(ctx, 500, "INTERNAL_SERVER_ERROR");
     503            2 : }
     504              : 
     505              : /* users.userFull wrapper containing a minimal userFull with:
     506              :  *   about = "Test bio string"
     507              :  *   phone = "+15550001234"
     508              :  *   common_chats_count = 7
     509              :  *
     510              :  * userFull flags used:
     511              :  *   bit 4  → phone present
     512              :  *   bit 5  → about present
     513              :  *   bit 20 → common_chats_count present
     514              :  *
     515              :  * Layout written: flags(u32) id(i64) about(str) phone(str)
     516              :  *                 common_chats_count(i32)
     517              :  * (Matches the order parse_user_full() reads them.) */
     518            2 : static void on_get_full_user(MtRpcContext *ctx) {
     519              :     (void)ctx;
     520            2 :     uint32_t flags = (1u << 5) | (1u << 4) | (1u << 20);
     521              : 
     522              :     TlWriter w;
     523            2 :     tl_writer_init(&w);
     524              : 
     525              :     /* users.userFull wrapper */
     526            2 :     tl_write_uint32(&w, CRC_users_userFull);
     527              : 
     528              :     /* full_user:UserFull — inner userFull object */
     529            2 :     tl_write_uint32(&w, CRC_userFull_inner);
     530            2 :     tl_write_uint32(&w, flags);
     531            2 :     tl_write_int64 (&w, 8001LL);            /* id */
     532            2 :     tl_write_string(&w, "Test bio string"); /* about (flags.5) */
     533            2 :     tl_write_string(&w, "+15550001234");    /* phone (flags.4) */
     534            2 :     tl_write_int32 (&w, 7);                /* common_chats_count (flags.20) */
     535              : 
     536              :     /* chats:Vector<Chat> — empty */
     537            2 :     tl_write_uint32(&w, TL_vector);
     538            2 :     tl_write_uint32(&w, 0);
     539              : 
     540              :     /* users:Vector<User> — empty */
     541            2 :     tl_write_uint32(&w, TL_vector);
     542            2 :     tl_write_uint32(&w, 0);
     543              : 
     544            2 :     mt_server_reply_result(ctx, w.data, w.len);
     545            2 :     tl_writer_free(&w);
     546            2 : }
     547              : 
     548              : /* contacts.resolvedPeer pointing at channel id 9001 with access_hash. */
     549            4 : static void on_resolve_channel(MtRpcContext *ctx) {
     550              :     TlWriter w;
     551            4 :     tl_writer_init(&w);
     552            4 :     tl_write_uint32(&w, TL_contacts_resolvedPeer);
     553            4 :     tl_write_uint32(&w, TL_peerChannel);
     554            4 :     tl_write_int64 (&w, 9001LL);
     555              :     /* chats vector: one channel */
     556            4 :     tl_write_uint32(&w, TL_vector);
     557            4 :     tl_write_uint32(&w, 1);
     558            4 :     tl_write_uint32(&w, TL_channel);
     559              :     /* flags: bit 13 = has access_hash */
     560            4 :     tl_write_uint32(&w, (1u << 13));
     561            4 :     tl_write_uint32(&w, 0);                   /* flags2 */
     562            4 :     tl_write_int64 (&w, 9001LL);
     563            4 :     tl_write_int64 (&w, 0x0102030405060708LL);/* access_hash */
     564              :     /* users vector: empty */
     565            4 :     tl_write_uint32(&w, TL_vector);
     566            4 :     tl_write_uint32(&w, 0);
     567            4 :     mt_server_reply_result(ctx, w.data, w.len);
     568            4 :     tl_writer_free(&w);
     569            4 : }
     570              : 
     571              : /* Capture the InputPeer CRC and first 8 bytes of peer args from a
     572              :  * getHistory request body (starts at CRC_messages_getHistory).
     573              :  *
     574              :  * Layout: [crc_getHistory:4][peer_crc:4][...peer_args...][offset_id:4]...
     575              :  * We expose this via a static so the responder can write it and the test
     576              :  * can read it after the call returns. */
     577              : typedef struct {
     578              :     uint32_t peer_crc;
     579              :     int64_t  peer_id;
     580              :     int64_t  peer_hash; /* valid only when peer_crc carries hash */
     581              :     int32_t  offset_id; /* first int32 after the peer */
     582              : } CapturedHistoryReq;
     583              : 
     584              : static CapturedHistoryReq g_captured_req;
     585              : 
     586           10 : static void on_history_capture(MtRpcContext *ctx) {
     587              :     /* req_body starts with CRC_messages_getHistory (4 bytes). Skip it. */
     588           10 :     if (ctx->req_body_len < 8) { on_history_empty(ctx); return; }
     589           10 :     const uint8_t *p = ctx->req_body + 4; /* skip getHistory CRC */
     590           10 :     size_t rem = ctx->req_body_len - 4;
     591              : 
     592           10 :     uint32_t pcrc = (uint32_t)p[0] | ((uint32_t)p[1] << 8)
     593           10 :                   | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
     594           10 :     g_captured_req.peer_crc  = pcrc;
     595           10 :     g_captured_req.peer_id   = 0;
     596           10 :     g_captured_req.peer_hash = 0;
     597           10 :     g_captured_req.offset_id = 0;
     598           10 :     p += 4; rem -= 4;
     599              : 
     600           10 :     if (pcrc == CRC_inputPeerSelf) {
     601              :         /* No additional fields; offset_id follows. */
     602            4 :         if (rem >= 4) {
     603            4 :             int32_t oi = (int32_t)((uint32_t)p[0] | ((uint32_t)p[1] << 8)
     604            4 :                                   | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24));
     605            4 :             g_captured_req.offset_id = oi;
     606              :         }
     607            6 :     } else if (pcrc == CRC_inputPeerUser || pcrc == CRC_inputPeerChannel) {
     608              :         /* id:int64 + access_hash:int64 */
     609            6 :         if (rem < 16) { on_history_empty(ctx); return; }
     610            6 :         int64_t id = 0;
     611           54 :         for (int i = 0; i < 8; i++) id |= ((int64_t)p[i]) << (i * 8);
     612            6 :         p += 8; rem -= 8;
     613            6 :         int64_t hash = 0;
     614           54 :         for (int i = 0; i < 8; i++) hash |= ((int64_t)p[i]) << (i * 8);
     615            6 :         p += 8; rem -= 8;
     616            6 :         g_captured_req.peer_id   = id;
     617            6 :         g_captured_req.peer_hash = hash;
     618            6 :         if (rem >= 4) {
     619            6 :             int32_t oi = (int32_t)((uint32_t)p[0] | ((uint32_t)p[1] << 8)
     620            6 :                                   | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24));
     621            6 :             g_captured_req.offset_id = oi;
     622              :         }
     623              :     }
     624              : 
     625           10 :     on_history_empty(ctx);
     626              : }
     627              : 
     628              : /* ================================================================ */
     629              : /* Tests                                                            */
     630              : /* ================================================================ */
     631              : 
     632            2 : static void test_get_self(void) {
     633            2 :     with_tmp_home("self");
     634            2 :     mt_server_init(); mt_server_reset();
     635            2 :     MtProtoSession s; load_session(&s);
     636            2 :     mt_server_expect(CRC_users_getUsers, on_get_self, NULL);
     637              : 
     638            2 :     ApiConfig cfg; init_cfg(&cfg);
     639            2 :     Transport t; connect_mock(&t);
     640              : 
     641            2 :     SelfInfo si = {0};
     642            2 :     ASSERT(domain_get_self(&cfg, &s, &t, &si) == 0, "get_self succeeds");
     643            2 :     ASSERT(si.id == 99001LL, "id == 99001");
     644              : 
     645            2 :     transport_close(&t);
     646            2 :     mt_server_reset();
     647              : }
     648              : 
     649            2 : static void test_dialogs_empty(void) {
     650            2 :     with_tmp_home("dlg-empty");
     651            2 :     mt_server_init(); mt_server_reset();
     652            2 :     dialogs_cache_flush();
     653            2 :     MtProtoSession s; load_session(&s);
     654            2 :     mt_server_expect(CRC_messages_getDialogs, on_dialogs_empty, NULL);
     655              : 
     656            2 :     ApiConfig cfg; init_cfg(&cfg);
     657            2 :     Transport t; connect_mock(&t);
     658              : 
     659              :     DialogEntry rows[8];
     660            2 :     int n = -1;
     661            2 :     ASSERT(domain_get_dialogs(&cfg, &s, &t, 8, 0, rows, &n, NULL) == 0,
     662              :            "get_dialogs succeeds on empty");
     663            2 :     ASSERT(n == 0, "zero dialogs returned");
     664              : 
     665            2 :     transport_close(&t);
     666            2 :     mt_server_reset();
     667              : }
     668              : 
     669            2 : static void test_dialogs_one_user(void) {
     670            2 :     with_tmp_home("dlg-user");
     671            2 :     mt_server_init(); mt_server_reset();
     672            2 :     dialogs_cache_flush();
     673            2 :     MtProtoSession s; load_session(&s);
     674            2 :     mt_server_expect(CRC_messages_getDialogs, on_dialogs_one_user, NULL);
     675              : 
     676            2 :     ApiConfig cfg; init_cfg(&cfg);
     677            2 :     Transport t; connect_mock(&t);
     678              : 
     679              :     DialogEntry rows[8];
     680            2 :     int n = 0;
     681            2 :     ASSERT(domain_get_dialogs(&cfg, &s, &t, 8, 0, rows, &n, NULL) == 0,
     682              :            "get_dialogs succeeds");
     683            2 :     ASSERT(n == 1, "one dialog parsed");
     684            2 :     ASSERT(rows[0].kind == DIALOG_PEER_USER, "user peer kind");
     685            2 :     ASSERT(rows[0].peer_id == 555LL, "peer_id roundtrips");
     686            2 :     ASSERT(rows[0].top_message_id == 1200, "top_message roundtrips");
     687            2 :     ASSERT(rows[0].unread_count == 7, "unread_count roundtrips");
     688              :     /* access_hash comes from the users vector join — the user carried
     689              :      * flags.0 so have_access_hash should be set. */
     690            2 :     ASSERT(rows[0].have_access_hash == 1, "access_hash joined from users vec");
     691            2 :     ASSERT(rows[0].access_hash == (int64_t)0xAABBCCDDEEFF0011LL,
     692              :            "access_hash value");
     693              : 
     694            2 :     transport_close(&t);
     695            2 :     mt_server_reset();
     696              : }
     697              : 
     698              : /* TEST-02: messages.dialogsSlice variant — two entries in the batch, server
     699              :  * reports 50 total.  Verify that the batch entries are parsed correctly and
     700              :  * that total_count surfaces the server-side count rather than the batch
     701              :  * size. */
     702            2 : static void test_dialogs_slice_variant(void) {
     703            2 :     with_tmp_home("dlg-slice");
     704            2 :     mt_server_init(); mt_server_reset();
     705            2 :     dialogs_cache_flush();
     706            2 :     MtProtoSession s; load_session(&s);
     707            2 :     mt_server_expect(CRC_messages_getDialogs, on_dialogs_slice, NULL);
     708              : 
     709            2 :     ApiConfig cfg; init_cfg(&cfg);
     710            2 :     Transport t; connect_mock(&t);
     711              : 
     712              :     DialogEntry rows[8];
     713            2 :     int n = 0;
     714            2 :     int total = 0;
     715            2 :     ASSERT(domain_get_dialogs(&cfg, &s, &t, 8, 0, rows, &n, &total) == 0,
     716              :            "get_dialogs slice succeeds");
     717            2 :     ASSERT(n == 2, "two dialogs in batch");
     718            2 :     ASSERT(total == 50, "total_count from slice header");
     719              : 
     720              :     /* First entry: user peer */
     721            2 :     ASSERT(rows[0].kind == DIALOG_PEER_USER, "first is user peer");
     722            2 :     ASSERT(rows[0].peer_id == 777LL, "user peer_id");
     723            2 :     ASSERT(rows[0].top_message_id == 42, "user top_message");
     724            2 :     ASSERT(rows[0].unread_count == 3, "user unread_count");
     725              : 
     726              :     /* Second entry: channel peer */
     727            2 :     ASSERT(rows[1].kind == DIALOG_PEER_CHANNEL, "second is channel peer");
     728            2 :     ASSERT(rows[1].peer_id == 888LL, "channel peer_id");
     729            2 :     ASSERT(rows[1].top_message_id == 99, "channel top_message");
     730            2 :     ASSERT(rows[1].unread_count == 0, "channel unread_count");
     731              : 
     732            2 :     transport_close(&t);
     733            2 :     mt_server_reset();
     734              : }
     735              : 
     736              : /* TEST-03: messages.dialogsNotModified variant — server returns the not-modified
     737              :  * constructor with a count field.  The domain should return success with zero
     738              :  * entries and surface the server count via total_count so callers know their
     739              :  * cached list is still valid. */
     740            2 : static void test_dialogs_not_modified_variant(void) {
     741            2 :     with_tmp_home("dlg-notmod");
     742            2 :     mt_server_init(); mt_server_reset();
     743            2 :     dialogs_cache_flush();
     744            2 :     MtProtoSession s; load_session(&s);
     745            2 :     mt_server_expect(CRC_messages_getDialogs, on_dialogs_not_modified, NULL);
     746              : 
     747            2 :     ApiConfig cfg; init_cfg(&cfg);
     748            2 :     Transport t; connect_mock(&t);
     749              : 
     750              :     DialogEntry rows[8];
     751            2 :     int n = -1;
     752            2 :     int total = -1;
     753            2 :     ASSERT(domain_get_dialogs(&cfg, &s, &t, 8, 0, rows, &n, &total) == 0,
     754              :            "get_dialogs succeeds on not-modified");
     755              :     /* Zero entries — caller must consult its cache. */
     756            2 :     ASSERT(n == 0, "zero entries on not-modified");
     757              :     /* Server-reported count must propagate so the caller knows cache is valid. */
     758            2 :     ASSERT(total == 37, "total_count carries server count");
     759              : 
     760            2 :     transport_close(&t);
     761            2 :     mt_server_reset();
     762              : }
     763              : 
     764            2 : static void test_history_empty(void) {
     765            2 :     with_tmp_home("hist-empty");
     766            2 :     mt_server_init(); mt_server_reset();
     767            2 :     MtProtoSession s; load_session(&s);
     768            2 :     mt_server_expect(CRC_messages_getHistory, on_history_empty, NULL);
     769              : 
     770            2 :     ApiConfig cfg; init_cfg(&cfg);
     771            2 :     Transport t; connect_mock(&t);
     772              : 
     773              :     HistoryEntry rows[4];
     774            2 :     int n = -1;
     775            2 :     ASSERT(domain_get_history_self(&cfg, &s, &t, 0, 4, rows, &n) == 0,
     776              :            "get_history_self empty ok");
     777            2 :     ASSERT(n == 0, "zero messages");
     778              : 
     779            2 :     transport_close(&t);
     780            2 :     mt_server_reset();
     781              : }
     782              : 
     783            2 : static void test_history_one_message_empty(void) {
     784            2 :     with_tmp_home("hist-msg");
     785            2 :     mt_server_init(); mt_server_reset();
     786            2 :     MtProtoSession s; load_session(&s);
     787            2 :     mt_server_expect(CRC_messages_getHistory, on_history_one_empty, NULL);
     788              : 
     789            2 :     ApiConfig cfg; init_cfg(&cfg);
     790            2 :     Transport t; connect_mock(&t);
     791              : 
     792              :     HistoryEntry rows[4];
     793            2 :     int n = 0;
     794            2 :     ASSERT(domain_get_history_self(&cfg, &s, &t, 0, 4, rows, &n) == 0,
     795              :            "get_history_self ok");
     796              :     /* domain_get_history only records entries that have id or text; a
     797              :      * messageEmpty with id=42 has id set, so it should land. */
     798            2 :     ASSERT(n == 1, "one messageEmpty parsed");
     799            2 :     ASSERT(rows[0].id == 42, "id == 42");
     800              : 
     801            2 :     transport_close(&t);
     802            2 :     mt_server_reset();
     803              : }
     804              : 
     805            2 : static void test_contacts_empty(void) {
     806            2 :     with_tmp_home("cont-empty");
     807            2 :     mt_server_init(); mt_server_reset();
     808            2 :     MtProtoSession s; load_session(&s);
     809            2 :     mt_server_expect(CRC_contacts_getContacts, on_contacts_empty, NULL);
     810              : 
     811            2 :     ApiConfig cfg; init_cfg(&cfg);
     812            2 :     Transport t; connect_mock(&t);
     813              : 
     814              :     ContactEntry rows[8];
     815            2 :     int n = -1;
     816            2 :     ASSERT(domain_get_contacts(&cfg, &s, &t, rows, 8, &n) == 0,
     817              :            "contacts empty ok");
     818            2 :     ASSERT(n == 0, "zero contacts");
     819              : 
     820            2 :     transport_close(&t);
     821            2 :     mt_server_reset();
     822              : }
     823              : 
     824            2 : static void test_contacts_two(void) {
     825            2 :     with_tmp_home("cont-two");
     826            2 :     mt_server_init(); mt_server_reset();
     827            2 :     MtProtoSession s; load_session(&s);
     828            2 :     mt_server_expect(CRC_contacts_getContacts, on_contacts_two, NULL);
     829              : 
     830            2 :     ApiConfig cfg; init_cfg(&cfg);
     831            2 :     Transport t; connect_mock(&t);
     832              : 
     833              :     ContactEntry rows[8];
     834            2 :     int n = 0;
     835            2 :     ASSERT(domain_get_contacts(&cfg, &s, &t, rows, 8, &n) == 0,
     836              :            "contacts ok");
     837            2 :     ASSERT(n == 2, "two contacts");
     838            2 :     ASSERT(rows[0].user_id == 101 && rows[0].mutual == 1, "first is mutual");
     839            2 :     ASSERT(rows[1].user_id == 202 && rows[1].mutual == 0, "second not mutual");
     840              : 
     841            2 :     transport_close(&t);
     842            2 :     mt_server_reset();
     843              : }
     844              : 
     845            2 : static void test_resolve_username_happy(void) {
     846            2 :     with_tmp_home("resolve-ok");
     847            2 :     mt_server_init(); mt_server_reset();
     848            2 :     MtProtoSession s; load_session(&s);
     849            2 :     mt_server_expect(CRC_contacts_resolveUsername, on_resolve_user, NULL);
     850              : 
     851            2 :     ApiConfig cfg; init_cfg(&cfg);
     852            2 :     Transport t; connect_mock(&t);
     853              : 
     854            2 :     ResolvedPeer rp = {0};
     855            2 :     ASSERT(domain_resolve_username(&cfg, &s, &t, "@somebody", &rp) == 0,
     856              :            "resolve ok");
     857            2 :     ASSERT(rp.kind == RESOLVED_KIND_USER, "USER kind");
     858            2 :     ASSERT(rp.id == 8001LL, "id 8001");
     859            2 :     ASSERT(rp.have_hash == 1, "have access_hash");
     860            2 :     ASSERT((uint64_t)rp.access_hash == 0xDEADBEEFCAFEBABEULL,
     861              :            "access_hash value");
     862            2 :     ASSERT(strcmp(rp.username, "somebody") == 0, "'@' stripped");
     863              : 
     864            2 :     transport_close(&t);
     865            2 :     mt_server_reset();
     866              : }
     867              : 
     868            2 : static void test_resolve_username_not_found(void) {
     869            2 :     with_tmp_home("resolve-nf");
     870            2 :     mt_server_init(); mt_server_reset();
     871            2 :     MtProtoSession s; load_session(&s);
     872            2 :     mt_server_expect(CRC_contacts_resolveUsername, on_resolve_not_found, NULL);
     873              : 
     874            2 :     ApiConfig cfg; init_cfg(&cfg);
     875            2 :     Transport t; connect_mock(&t);
     876              : 
     877            2 :     ResolvedPeer rp = {0};
     878            2 :     ASSERT(domain_resolve_username(&cfg, &s, &t, "@nonexistent", &rp) == -1,
     879              :            "resolve returns -1 on RPC error");
     880              : 
     881            2 :     transport_close(&t);
     882            2 :     mt_server_reset();
     883              : }
     884              : 
     885              : /* TEST-22: resolveUsername returns TL_channel — verify ResolvedPeer is populated
     886              :  * with kind=CHANNEL, correct id and access_hash.  No follow-up getHistory call
     887              :  * is made; this exercises the channel branch of domain_resolve_username alone. */
     888            2 : static void test_resolve_username_channel(void) {
     889            2 :     with_tmp_home("resolve-chan");
     890            2 :     mt_server_init(); mt_server_reset();
     891            2 :     resolve_cache_flush();
     892            2 :     MtProtoSession s; load_session(&s);
     893            2 :     mt_server_expect(CRC_contacts_resolveUsername, on_resolve_channel, NULL);
     894              : 
     895            2 :     ApiConfig cfg; init_cfg(&cfg);
     896            2 :     Transport t; connect_mock(&t);
     897              : 
     898            2 :     ResolvedPeer rp = {0};
     899            2 :     ASSERT(domain_resolve_username(&cfg, &s, &t, "@mychannel", &rp) == 0,
     900              :            "resolve channel ok");
     901            2 :     ASSERT(rp.kind == RESOLVED_KIND_CHANNEL, "kind == RESOLVED_KIND_CHANNEL");
     902            2 :     ASSERT(rp.id == 9001LL, "channel id == 9001");
     903            2 :     ASSERT(rp.have_hash == 1, "have_hash set for channel");
     904            2 :     ASSERT((uint64_t)rp.access_hash == 0x0102030405060708ULL,
     905              :            "channel access_hash value matches");
     906              : 
     907            2 :     transport_close(&t);
     908            2 :     mt_server_reset();
     909              : }
     910              : 
     911            2 : static void test_updates_state(void) {
     912            2 :     with_tmp_home("upd-state");
     913            2 :     mt_server_init(); mt_server_reset();
     914            2 :     MtProtoSession s; load_session(&s);
     915            2 :     mt_server_expect(CRC_updates_getState, on_updates_state, NULL);
     916              : 
     917            2 :     ApiConfig cfg; init_cfg(&cfg);
     918            2 :     Transport t; connect_mock(&t);
     919              : 
     920            2 :     UpdatesState st = {0};
     921            2 :     ASSERT(domain_updates_state(&cfg, &s, &t, &st) == 0, "state ok");
     922            2 :     ASSERT(st.pts == 100, "pts");
     923            2 :     ASSERT(st.qts == 5, "qts");
     924            2 :     ASSERT(st.date == 1700000000, "date");
     925            2 :     ASSERT(st.seq == 1, "seq");
     926            2 :     ASSERT(st.unread_count == 3, "unread_count");
     927              : 
     928            2 :     transport_close(&t);
     929            2 :     mt_server_reset();
     930              : }
     931              : 
     932            2 : static void test_updates_difference_empty(void) {
     933            2 :     with_tmp_home("upd-diff");
     934            2 :     mt_server_init(); mt_server_reset();
     935            2 :     MtProtoSession s; load_session(&s);
     936            2 :     mt_server_expect(CRC_updates_getDifference, on_updates_diff_empty, NULL);
     937              : 
     938            2 :     ApiConfig cfg; init_cfg(&cfg);
     939            2 :     Transport t; connect_mock(&t);
     940              : 
     941            2 :     UpdatesState prev = { .pts = 100, .qts = 5, .date = 1700000000, .seq = 1 };
     942            2 :     UpdatesDifference diff = {0};
     943            2 :     ASSERT(domain_updates_difference(&cfg, &s, &t, &prev, &diff) == 0,
     944              :            "getDifference ok");
     945            2 :     ASSERT(diff.is_empty == 1, "marked empty");
     946            2 :     ASSERT(diff.next_state.date == 1700000500, "date advanced");
     947            2 :     ASSERT(diff.next_state.seq == 2, "seq advanced");
     948            2 :     ASSERT(diff.new_messages_count == 0, "no new messages");
     949              : 
     950            2 :     transport_close(&t);
     951            2 :     mt_server_reset();
     952              : }
     953              : 
     954              : /* TEST-24a: updates.getDifference returns TL_updates_difference with one
     955              :  * real message.  Assert new_messages_count == 1 and the message fields. */
     956            2 : static void test_updates_difference_with_messages(void) {
     957            2 :     with_tmp_home("upd-diff-msg");
     958            2 :     mt_server_init(); mt_server_reset();
     959            2 :     MtProtoSession s; load_session(&s);
     960            2 :     mt_server_expect(CRC_updates_getDifference,
     961              :                      on_updates_diff_with_messages, NULL);
     962              : 
     963            2 :     ApiConfig cfg; init_cfg(&cfg);
     964            2 :     Transport t; connect_mock(&t);
     965              : 
     966            2 :     UpdatesState prev = { .pts = 100, .qts = 5, .date = 1700000000, .seq = 1 };
     967            2 :     UpdatesDifference diff = {0};
     968            2 :     ASSERT(domain_updates_difference(&cfg, &s, &t, &prev, &diff) == 0,
     969              :            "getDifference with messages ok");
     970            2 :     ASSERT(diff.is_empty == 0, "not marked empty");
     971            2 :     ASSERT(diff.new_messages_count == 1, "one new message");
     972            2 :     ASSERT(diff.new_messages[0].id == 501, "message id == 501");
     973            2 :     ASSERT(diff.new_messages[0].date == 1700001000, "message date correct");
     974            2 :     ASSERT(strcmp(diff.new_messages[0].text, "hello from diff") == 0,
     975              :            "message text matches");
     976              : 
     977            2 :     transport_close(&t);
     978            2 :     mt_server_reset();
     979              : }
     980              : 
     981              : /* TEST-24b: updates.getDifference returns TL_updates_differenceSlice with two
     982              :  * messages.  Asserts both messages are parsed correctly. */
     983            2 : static void test_updates_differenceSlice_with_messages(void) {
     984            2 :     with_tmp_home("upd-diff-slice");
     985            2 :     mt_server_init(); mt_server_reset();
     986            2 :     MtProtoSession s; load_session(&s);
     987            2 :     mt_server_expect(CRC_updates_getDifference,
     988              :                      on_updates_diff_slice_with_messages, NULL);
     989              : 
     990            2 :     ApiConfig cfg; init_cfg(&cfg);
     991            2 :     Transport t; connect_mock(&t);
     992              : 
     993            2 :     UpdatesState prev = { .pts = 100, .qts = 5, .date = 1700000000, .seq = 1 };
     994            2 :     UpdatesDifference diff = {0};
     995            2 :     ASSERT(domain_updates_difference(&cfg, &s, &t, &prev, &diff) == 0,
     996              :            "getDifferenceSlice with messages ok");
     997            2 :     ASSERT(diff.is_empty == 0, "not marked empty");
     998            2 :     ASSERT(diff.new_messages_count == 2, "two new messages");
     999            2 :     ASSERT(diff.new_messages[0].id == 601, "first message id == 601");
    1000            2 :     ASSERT(diff.new_messages[0].date == 1700002000, "first message date correct");
    1001            2 :     ASSERT(strcmp(diff.new_messages[0].text, "first slice msg") == 0,
    1002              :            "first message text matches");
    1003            2 :     ASSERT(diff.new_messages[1].id == 602, "second message id == 602");
    1004            2 :     ASSERT(diff.new_messages[1].date == 1700002001, "second message date correct");
    1005            2 :     ASSERT(strcmp(diff.new_messages[1].text, "second slice msg") == 0,
    1006              :            "second message text matches");
    1007              : 
    1008            2 :     transport_close(&t);
    1009            2 :     mt_server_reset();
    1010              : }
    1011              : 
    1012            2 : static void test_rpc_error_propagation(void) {
    1013            2 :     with_tmp_home("rpc-err");
    1014            2 :     mt_server_init(); mt_server_reset();
    1015            2 :     MtProtoSession s; load_session(&s);
    1016              :     /* Any read method — use get_self as the canary. */
    1017            2 :     mt_server_expect(CRC_users_getUsers, on_generic_500, NULL);
    1018              : 
    1019            2 :     ApiConfig cfg; init_cfg(&cfg);
    1020            2 :     Transport t; connect_mock(&t);
    1021              : 
    1022            2 :     SelfInfo si = {0};
    1023            2 :     ASSERT(domain_get_self(&cfg, &s, &t, &si) == -1,
    1024              :            "domain_get_self -1 on rpc_error");
    1025              : 
    1026            2 :     transport_close(&t);
    1027            2 :     mt_server_reset();
    1028              : }
    1029              : 
    1030              : /* TEST-05a: premium bit (flags2.3) decoded correctly from a full user
    1031              :  * record returned by users.getUsers. */
    1032            2 : static void test_get_self_premium(void) {
    1033            2 :     with_tmp_home("self-prem");
    1034            2 :     mt_server_init(); mt_server_reset();
    1035            2 :     MtProtoSession s; load_session(&s);
    1036            2 :     mt_server_expect(CRC_users_getUsers, on_get_self_premium, NULL);
    1037              : 
    1038            2 :     ApiConfig cfg; init_cfg(&cfg);
    1039            2 :     Transport t; connect_mock(&t);
    1040              : 
    1041            2 :     SelfInfo si = {0};
    1042            2 :     ASSERT(domain_get_self(&cfg, &s, &t, &si) == 0, "premium get_self succeeds");
    1043            2 :     ASSERT(si.id == 77002LL, "id == 77002");
    1044            2 :     ASSERT(strcmp(si.first_name, "Premium") == 0, "first_name == Premium");
    1045            2 :     ASSERT(si.is_premium == 1, "is_premium flag set");
    1046            2 :     ASSERT(si.is_bot == 0, "is_bot not set");
    1047              : 
    1048            2 :     transport_close(&t);
    1049            2 :     mt_server_reset();
    1050              : }
    1051              : 
    1052              : /* TEST-05b: arg_parse maps the "self" alias to CMD_ME (same as "me"). */
    1053            2 : static void test_self_alias_maps_to_cmd_me(void) {
    1054            2 :     const char *argv_self[] = {"tg-cli", "self"};
    1055            2 :     ArgResult   ar_self = {0};
    1056            2 :     int rc_self = arg_parse(2, (char **)argv_self, &ar_self);
    1057            2 :     ASSERT(rc_self == 0, "arg_parse(self) succeeds");
    1058            2 :     ASSERT(ar_self.command == CMD_ME, "self alias maps to CMD_ME");
    1059              : 
    1060            2 :     const char *argv_me[] = {"tg-cli", "me"};
    1061            2 :     ArgResult   ar_me = {0};
    1062            2 :     int rc_me = arg_parse(2, (char **)argv_me, &ar_me);
    1063            2 :     ASSERT(rc_me == 0, "arg_parse(me) succeeds");
    1064            2 :     ASSERT(ar_me.command == CMD_ME, "me maps to CMD_ME");
    1065              : }
    1066              : 
    1067              : /* ================================================================ */
    1068              : /* TEST-06: history peer variants                                   */
    1069              : /* ================================================================ */
    1070              : 
    1071              : /* Case 1 — history self: getHistory must carry inputPeerSelf. */
    1072            2 : static void test_history_self(void) {
    1073            2 :     with_tmp_home("hist-self");
    1074            2 :     mt_server_init(); mt_server_reset();
    1075            2 :     resolve_cache_flush();
    1076            2 :     MtProtoSession s; load_session(&s);
    1077            2 :     memset(&g_captured_req, 0, sizeof(g_captured_req));
    1078            2 :     mt_server_expect(CRC_messages_getHistory, on_history_capture, NULL);
    1079              : 
    1080            2 :     ApiConfig cfg; init_cfg(&cfg);
    1081            2 :     Transport t; connect_mock(&t);
    1082              : 
    1083            2 :     HistoryPeer peer = { .kind = HISTORY_PEER_SELF };
    1084            2 :     HistoryEntry rows[4]; int n = -1;
    1085            2 :     ASSERT(domain_get_history(&cfg, &s, &t, &peer, 0, 4, rows, &n) == 0,
    1086              :            "history_self ok");
    1087            2 :     ASSERT(g_captured_req.peer_crc == CRC_inputPeerSelf,
    1088              :            "wire carries inputPeerSelf");
    1089              : 
    1090            2 :     transport_close(&t);
    1091            2 :     mt_server_reset();
    1092              : }
    1093              : 
    1094              : /* Case 2 — history numeric user id: getHistory must carry inputPeerUser
    1095              :  * with id=123 and access_hash=0. */
    1096            2 : static void test_history_user_numeric_id(void) {
    1097            2 :     with_tmp_home("hist-uid");
    1098            2 :     mt_server_init(); mt_server_reset();
    1099            2 :     resolve_cache_flush();
    1100            2 :     MtProtoSession s; load_session(&s);
    1101            2 :     memset(&g_captured_req, 0, sizeof(g_captured_req));
    1102            2 :     mt_server_expect(CRC_messages_getHistory, on_history_capture, NULL);
    1103              : 
    1104            2 :     ApiConfig cfg; init_cfg(&cfg);
    1105            2 :     Transport t; connect_mock(&t);
    1106              : 
    1107            2 :     HistoryPeer peer = { .kind = HISTORY_PEER_USER, .peer_id = 123, .access_hash = 0 };
    1108            2 :     HistoryEntry rows[4]; int n = -1;
    1109            2 :     ASSERT(domain_get_history(&cfg, &s, &t, &peer, 0, 4, rows, &n) == 0,
    1110              :            "history numeric id ok");
    1111            2 :     ASSERT(g_captured_req.peer_crc == CRC_inputPeerUser,
    1112              :            "wire carries inputPeerUser");
    1113            2 :     ASSERT(g_captured_req.peer_id == 123LL, "peer_id == 123");
    1114            2 :     ASSERT(g_captured_req.peer_hash == 0LL, "access_hash == 0");
    1115              : 
    1116            2 :     transport_close(&t);
    1117            2 :     mt_server_reset();
    1118              : }
    1119              : 
    1120              : /* Case 3 — history @foo: resolveUsername fires, then getHistory carries
    1121              :  * inputPeerUser with id=8001 and the resolved access_hash. */
    1122            2 : static void test_history_username_resolve(void) {
    1123            2 :     with_tmp_home("hist-uname");
    1124            2 :     mt_server_init(); mt_server_reset();
    1125            2 :     resolve_cache_flush();
    1126            2 :     MtProtoSession s; load_session(&s);
    1127            2 :     memset(&g_captured_req, 0, sizeof(g_captured_req));
    1128            2 :     mt_server_expect(CRC_contacts_resolveUsername, on_resolve_user, NULL);
    1129            2 :     mt_server_expect(CRC_messages_getHistory, on_history_capture, NULL);
    1130              : 
    1131            2 :     ApiConfig cfg; init_cfg(&cfg);
    1132            2 :     Transport t; connect_mock(&t);
    1133              : 
    1134              :     /* Resolve @foo first, then pass the result into history. */
    1135            2 :     ResolvedPeer rp = {0};
    1136            2 :     ASSERT(domain_resolve_username(&cfg, &s, &t, "@foo", &rp) == 0,
    1137              :            "resolve @foo ok");
    1138            2 :     ASSERT(rp.kind == RESOLVED_KIND_USER, "resolved as USER");
    1139            2 :     ASSERT(rp.id == 8001LL, "resolved id == 8001");
    1140              : 
    1141            2 :     HistoryPeer peer = {
    1142              :         .kind        = HISTORY_PEER_USER,
    1143            2 :         .peer_id     = rp.id,
    1144            2 :         .access_hash = rp.access_hash,
    1145              :     };
    1146            2 :     HistoryEntry rows[4]; int n = -1;
    1147            2 :     ASSERT(domain_get_history(&cfg, &s, &t, &peer, 0, 4, rows, &n) == 0,
    1148              :            "getHistory after resolve ok");
    1149            2 :     ASSERT(g_captured_req.peer_crc == CRC_inputPeerUser,
    1150              :            "wire carries inputPeerUser");
    1151            2 :     ASSERT(g_captured_req.peer_id == 8001LL, "peer_id == 8001");
    1152            2 :     ASSERT((uint64_t)g_captured_req.peer_hash == 0xDEADBEEFCAFEBABEULL,
    1153              :            "access_hash threaded through");
    1154              : 
    1155            2 :     transport_close(&t);
    1156            2 :     mt_server_reset();
    1157              : }
    1158              : 
    1159              : /* Case 4 — history @channel: resolved as channel, access_hash threads to
    1160              :  * getHistory via inputPeerChannel. */
    1161            2 : static void test_history_channel_access_hash(void) {
    1162            2 :     with_tmp_home("hist-chan");
    1163            2 :     mt_server_init(); mt_server_reset();
    1164            2 :     resolve_cache_flush();
    1165            2 :     MtProtoSession s; load_session(&s);
    1166            2 :     memset(&g_captured_req, 0, sizeof(g_captured_req));
    1167            2 :     mt_server_expect(CRC_contacts_resolveUsername, on_resolve_channel, NULL);
    1168            2 :     mt_server_expect(CRC_messages_getHistory, on_history_capture, NULL);
    1169              : 
    1170            2 :     ApiConfig cfg; init_cfg(&cfg);
    1171            2 :     Transport t; connect_mock(&t);
    1172              : 
    1173            2 :     ResolvedPeer rp = {0};
    1174            2 :     ASSERT(domain_resolve_username(&cfg, &s, &t, "@mychannel", &rp) == 0,
    1175              :            "resolve @mychannel ok");
    1176            2 :     ASSERT(rp.kind == RESOLVED_KIND_CHANNEL, "resolved as CHANNEL");
    1177            2 :     ASSERT(rp.id == 9001LL, "channel id == 9001");
    1178            2 :     ASSERT((uint64_t)rp.access_hash == 0x0102030405060708ULL,
    1179              :            "channel access_hash");
    1180              : 
    1181            2 :     HistoryPeer peer = {
    1182              :         .kind        = HISTORY_PEER_CHANNEL,
    1183            2 :         .peer_id     = rp.id,
    1184            2 :         .access_hash = rp.access_hash,
    1185              :     };
    1186            2 :     HistoryEntry rows[4]; int n = -1;
    1187            2 :     ASSERT(domain_get_history(&cfg, &s, &t, &peer, 0, 4, rows, &n) == 0,
    1188              :            "getHistory channel ok");
    1189            2 :     ASSERT(g_captured_req.peer_crc == CRC_inputPeerChannel,
    1190              :            "wire carries inputPeerChannel");
    1191            2 :     ASSERT(g_captured_req.peer_id == 9001LL, "channel peer_id == 9001");
    1192            2 :     ASSERT((uint64_t)g_captured_req.peer_hash == 0x0102030405060708ULL,
    1193              :            "channel access_hash on wire");
    1194              : 
    1195            2 :     transport_close(&t);
    1196            2 :     mt_server_reset();
    1197              : }
    1198              : 
    1199              : /* Case 5 — --offset flag: offset_id=50 lands on the wire. */
    1200            2 : static void test_history_offset_flag(void) {
    1201            2 :     with_tmp_home("hist-off");
    1202            2 :     mt_server_init(); mt_server_reset();
    1203            2 :     resolve_cache_flush();
    1204            2 :     MtProtoSession s; load_session(&s);
    1205            2 :     memset(&g_captured_req, 0, sizeof(g_captured_req));
    1206            2 :     mt_server_expect(CRC_messages_getHistory, on_history_capture, NULL);
    1207              : 
    1208            2 :     ApiConfig cfg; init_cfg(&cfg);
    1209            2 :     Transport t; connect_mock(&t);
    1210              : 
    1211            2 :     HistoryPeer peer = { .kind = HISTORY_PEER_SELF };
    1212            2 :     HistoryEntry rows[4]; int n = -1;
    1213            2 :     ASSERT(domain_get_history(&cfg, &s, &t, &peer, 50, 4, rows, &n) == 0,
    1214              :            "history offset ok");
    1215            2 :     ASSERT(g_captured_req.peer_crc == CRC_inputPeerSelf,
    1216              :            "peer is inputPeerSelf");
    1217            2 :     ASSERT(g_captured_req.offset_id == 50, "offset_id == 50 on wire");
    1218              : 
    1219            2 :     transport_close(&t);
    1220            2 :     mt_server_reset();
    1221              : }
    1222              : 
    1223              : /* Case 6 — resolve cache hit: two consecutive calls fire one RPC.
    1224              :  * The second call must return the same data from cache. */
    1225            2 : static void test_history_cache_hit(void) {
    1226            2 :     with_tmp_home("hist-cache");
    1227            2 :     mt_server_init(); mt_server_reset();
    1228            2 :     resolve_cache_flush();
    1229            2 :     MtProtoSession s; load_session(&s);
    1230            2 :     mt_server_expect(CRC_contacts_resolveUsername, on_resolve_user, NULL);
    1231              : 
    1232            2 :     ApiConfig cfg; init_cfg(&cfg);
    1233            2 :     Transport t; connect_mock(&t);
    1234              : 
    1235              :     /* First call: goes to the wire. */
    1236            2 :     ResolvedPeer rp1 = {0};
    1237            2 :     ASSERT(domain_resolve_username(&cfg, &s, &t, "@cached_user", &rp1) == 0,
    1238              :            "first resolve ok");
    1239            2 :     int calls_after_first = mt_server_rpc_call_count();
    1240              : 
    1241              :     /* Second call: must be served from cache — no new RPC. */
    1242            2 :     ResolvedPeer rp2 = {0};
    1243            2 :     ASSERT(domain_resolve_username(&cfg, &s, &t, "@cached_user", &rp2) == 0,
    1244              :            "second resolve ok (from cache)");
    1245            2 :     ASSERT(mt_server_rpc_call_count() == calls_after_first,
    1246              :            "no additional RPC for cache hit");
    1247            2 :     ASSERT(rp2.id == rp1.id, "cached id matches");
    1248            2 :     ASSERT(rp2.access_hash == rp1.access_hash, "cached hash matches");
    1249              : 
    1250            2 :     transport_close(&t);
    1251            2 :     mt_server_reset();
    1252              : }
    1253              : 
    1254              : /* TEST-09: users.getFullUser happy path.
    1255              :  * Fires contacts.resolveUsername (→ user id 8001) followed by
    1256              :  * users.getFullUser (→ minimal userFull with about/phone/common_chats).
    1257              :  * Asserts that domain_get_user_info surfaces all three fields. */
    1258            2 : static void test_get_full_user_happy(void) {
    1259            2 :     with_tmp_home("full-user");
    1260            2 :     mt_server_init(); mt_server_reset();
    1261            2 :     resolve_cache_flush();
    1262            2 :     MtProtoSession s; load_session(&s);
    1263            2 :     mt_server_expect(CRC_contacts_resolveUsername, on_resolve_user, NULL);
    1264            2 :     mt_server_expect(CRC_users_getFullUser,        on_get_full_user, NULL);
    1265              : 
    1266            2 :     ApiConfig cfg; init_cfg(&cfg);
    1267            2 :     Transport t; connect_mock(&t);
    1268              : 
    1269            2 :     UserFullInfo fi = {0};
    1270            2 :     ASSERT(domain_get_user_info(&cfg, &s, &t, "@testuser", &fi) == 0,
    1271              :            "get_user_info ok");
    1272            2 :     ASSERT(fi.id == 8001LL, "id == 8001");
    1273            2 :     ASSERT(strcmp(fi.bio, "Test bio string") == 0, "bio decoded");
    1274            2 :     ASSERT(strcmp(fi.phone, "+15550001234") == 0, "phone decoded");
    1275            2 :     ASSERT(fi.common_chats_count == 7, "common_chats_count == 7");
    1276              : 
    1277            2 :     transport_close(&t);
    1278            2 :     mt_server_reset();
    1279              : }
    1280              : 
    1281              : /* ================================================================ */
    1282              : /* TEST-10: search functional tests                                 */
    1283              : /* ================================================================ */
    1284              : 
    1285              : /* Helper: write a minimal messages.messages with N plain text messages.
    1286              :  * Each message uses TL_message constructor with:
    1287              :  *   flags=0, flags2=0 (no optional fields), out=0
    1288              :  *   id = base_id + i, peer = inputPeerSelf (skipped by parser as from_id)
    1289              :  *   date = 1700000000 + i, message = text[i]
    1290              :  *
    1291              :  * Actual wire layout for a message with flags=0, flags2=0:
    1292              :  *   crc(4) flags(4) flags2(4) id(4)
    1293              :  *   [no from_id — flags.8 off]
    1294              :  *   peer_id: peerUser id(4+8)   (flags.28 off → no saved_peer)
    1295              :  *   [no fwd_header]
    1296              :  *   date(4)  message:string
    1297              :  */
    1298            6 : static void write_messages_messages(TlWriter *w, int count, int base_id,
    1299              :                                     int base_date, const char **texts) {
    1300            6 :     tl_write_uint32(w, TL_messages_messages);
    1301              :     /* messages vector */
    1302            6 :     tl_write_uint32(w, TL_vector);
    1303            6 :     tl_write_uint32(w, (uint32_t)count);
    1304           22 :     for (int i = 0; i < count; i++) {
    1305           16 :         tl_write_uint32(w, TL_message);
    1306           16 :         tl_write_uint32(w, 0);              /* flags = 0 */
    1307           16 :         tl_write_uint32(w, 0);              /* flags2 = 0 */
    1308           16 :         tl_write_int32 (w, base_id + i);    /* id */
    1309              :         /* peer_id: peerUser with id=1 (flags.28 off, flags.8 off) */
    1310           16 :         tl_write_uint32(w, TL_peerUser);
    1311           16 :         tl_write_int64 (w, 1LL);
    1312           16 :         tl_write_int32 (w, base_date + i);  /* date */
    1313           16 :         tl_write_string(w, texts[i]);       /* message */
    1314              :     }
    1315              :     /* chats vector: empty */
    1316            6 :     tl_write_uint32(w, TL_vector);
    1317            6 :     tl_write_uint32(w, 0);
    1318              :     /* users vector: empty */
    1319            6 :     tl_write_uint32(w, TL_vector);
    1320            6 :     tl_write_uint32(w, 0);
    1321            6 : }
    1322              : 
    1323              : /* Responder for messages.searchGlobal — returns 3 messages. */
    1324            4 : static void on_search_global_three(MtRpcContext *ctx) {
    1325              :     static const char *texts[3] = { "hello world", "second hit", "third one" };
    1326              :     TlWriter w;
    1327            4 :     tl_writer_init(&w);
    1328            4 :     write_messages_messages(&w, 3, 1001, 1700100000, texts);
    1329            4 :     mt_server_reply_result(ctx, w.data, w.len);
    1330            4 :     tl_writer_free(&w);
    1331            4 : }
    1332              : 
    1333              : /* Responder for messages.search (per-peer) — returns 2 messages. */
    1334            2 : static void on_search_peer_two(MtRpcContext *ctx) {
    1335              :     static const char *texts[2] = { "peer match one", "peer match two" };
    1336              :     TlWriter w;
    1337            2 :     tl_writer_init(&w);
    1338            2 :     write_messages_messages(&w, 2, 2001, 1700200000, texts);
    1339            2 :     mt_server_reply_result(ctx, w.data, w.len);
    1340            2 :     tl_writer_free(&w);
    1341            2 : }
    1342              : 
    1343              : /* Capture state for search request bytes. */
    1344              : typedef struct {
    1345              :     uint32_t crc;            /* first CRC in request body */
    1346              :     int32_t  limit;          /* limit field */
    1347              :     char     query[128];     /* query string (UTF-8) */
    1348              :     uint32_t peer_crc;       /* inputPeer CRC (per-peer only, 0 for global) */
    1349              : } CapturedSearchReq;
    1350              : 
    1351              : static CapturedSearchReq g_search_req;
    1352              : 
    1353              : /* Read a TL string from a byte buffer (little-endian, Pascal-style).
    1354              :  * Returns number of bytes consumed (including length byte(s) + padding),
    1355              :  * or 0 on error. Writes up to dst_max-1 bytes into dst. */
    1356            6 : static size_t read_tl_string_raw(const uint8_t *p, size_t rem,
    1357              :                                   char *dst, size_t dst_max) {
    1358            6 :     if (rem < 1) return 0;
    1359              :     size_t slen, hdr;
    1360            6 :     if (p[0] < 254) {
    1361            6 :         slen = p[0]; hdr = 1;
    1362            0 :     } else if (p[0] == 254) {
    1363            0 :         if (rem < 4) return 0;
    1364            0 :         slen = (size_t)p[1] | ((size_t)p[2] << 8) | ((size_t)p[3] << 16);
    1365            0 :         hdr = 4;
    1366              :     } else {
    1367            0 :         return 0;
    1368              :     }
    1369            6 :     if (rem < hdr + slen) return 0;
    1370            6 :     size_t copy = slen < dst_max - 1 ? slen : dst_max - 1;
    1371            6 :     memcpy(dst, p + hdr, copy);
    1372            6 :     dst[copy] = '\0';
    1373            6 :     size_t total = hdr + slen;
    1374              :     /* round up to 4-byte boundary */
    1375            6 :     if (total % 4) total += 4 - (total % 4);
    1376            6 :     return total;
    1377              : }
    1378              : 
    1379              : /* Responder that captures global-search request fields. */
    1380            4 : static void on_search_global_capture(MtRpcContext *ctx) {
    1381            4 :     memset(&g_search_req, 0, sizeof(g_search_req));
    1382            4 :     if (ctx->req_body_len < 4) { on_search_global_three(ctx); return; }
    1383              : 
    1384            4 :     const uint8_t *p = ctx->req_body;
    1385            4 :     size_t rem = ctx->req_body_len;
    1386              : 
    1387              :     /* CRC (4 bytes) */
    1388            4 :     g_search_req.crc = (uint32_t)p[0] | ((uint32_t)p[1] << 8)
    1389            4 :                      | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
    1390            4 :     p += 4; rem -= 4;
    1391              : 
    1392              :     /* flags (4 bytes) */
    1393            4 :     if (rem < 4) { on_search_global_three(ctx); return; }
    1394            4 :     p += 4; rem -= 4;
    1395              : 
    1396              :     /* query string */
    1397            4 :     size_t adv = read_tl_string_raw(p, rem, g_search_req.query,
    1398              :                                     sizeof(g_search_req.query));
    1399            4 :     if (adv == 0) { on_search_global_three(ctx); return; }
    1400            4 :     p += adv; rem -= adv;
    1401              : 
    1402              :     /* filter CRC (4) + min_date (4) + max_date (4) + offset_rate (4) */
    1403            4 :     if (rem < 16) { on_search_global_three(ctx); return; }
    1404            4 :     p += 16; rem -= 16;
    1405              : 
    1406              :     /* offset_peer CRC (4) + skip TL_inputPeerEmpty (no extra fields) */
    1407            4 :     if (rem < 4) { on_search_global_three(ctx); return; }
    1408            4 :     p += 4; rem -= 4;
    1409              : 
    1410              :     /* offset_id (4) */
    1411            4 :     if (rem < 4) { on_search_global_three(ctx); return; }
    1412            4 :     p += 4; rem -= 4;
    1413              : 
    1414              :     /* limit (4) */
    1415            4 :     if (rem >= 4) {
    1416            4 :         g_search_req.limit = (int32_t)((uint32_t)p[0] | ((uint32_t)p[1] << 8)
    1417            4 :                            | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24));
    1418              :     }
    1419              : 
    1420            4 :     on_search_global_three(ctx);
    1421              : }
    1422              : 
    1423              : /* Responder that captures per-peer search request fields. */
    1424            2 : static void on_search_peer_capture(MtRpcContext *ctx) {
    1425            2 :     memset(&g_search_req, 0, sizeof(g_search_req));
    1426            2 :     if (ctx->req_body_len < 4) { on_search_peer_two(ctx); return; }
    1427              : 
    1428            2 :     const uint8_t *p = ctx->req_body;
    1429            2 :     size_t rem = ctx->req_body_len;
    1430              : 
    1431              :     /* CRC (4) */
    1432            2 :     g_search_req.crc = (uint32_t)p[0] | ((uint32_t)p[1] << 8)
    1433            2 :                      | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
    1434            2 :     p += 4; rem -= 4;
    1435              : 
    1436              :     /* flags (4) */
    1437            2 :     if (rem < 4) { on_search_peer_two(ctx); return; }
    1438            2 :     p += 4; rem -= 4;
    1439              : 
    1440              :     /* peer CRC (4) */
    1441            2 :     if (rem < 4) { on_search_peer_two(ctx); return; }
    1442            2 :     g_search_req.peer_crc = (uint32_t)p[0] | ((uint32_t)p[1] << 8)
    1443            2 :                           | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
    1444            2 :     p += 4; rem -= 4;
    1445              : 
    1446              :     /* skip peer args: inputPeerUser → id(8) + access_hash(8) */
    1447            2 :     if (g_search_req.peer_crc == TL_inputPeerUser ||
    1448            0 :         g_search_req.peer_crc == TL_inputPeerChannel) {
    1449            2 :         if (rem < 16) { on_search_peer_two(ctx); return; }
    1450            2 :         p += 16; rem -= 16;
    1451            0 :     } else if (g_search_req.peer_crc == TL_inputPeerChat) {
    1452            0 :         if (rem < 8) { on_search_peer_two(ctx); return; }
    1453            0 :         p += 8; rem -= 8;
    1454              :     }
    1455              :     /* inputPeerSelf: no extra bytes */
    1456              : 
    1457              :     /* query string */
    1458            2 :     size_t adv = read_tl_string_raw(p, rem, g_search_req.query,
    1459              :                                     sizeof(g_search_req.query));
    1460            2 :     if (adv == 0) { on_search_peer_two(ctx); return; }
    1461            2 :     p += adv; rem -= adv;
    1462              : 
    1463              :     /* filter CRC (4) + min_date (4) + max_date (4) + offset_id (4) +
    1464              :        add_offset (4) */
    1465            2 :     if (rem < 20) { on_search_peer_two(ctx); return; }
    1466            2 :     p += 20; rem -= 20;
    1467              : 
    1468              :     /* limit (4) */
    1469            2 :     if (rem >= 4) {
    1470            2 :         g_search_req.limit = (int32_t)((uint32_t)p[0] | ((uint32_t)p[1] << 8)
    1471            2 :                            | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24));
    1472              :     }
    1473              : 
    1474            2 :     on_search_peer_two(ctx);
    1475              : }
    1476              : 
    1477              : /* TEST-10a: messages.searchGlobal — three results come back, request CRC
    1478              :  * is correct, and the query string lands on the wire. */
    1479            2 : static void test_search_global_happy(void) {
    1480            2 :     with_tmp_home("srch-global");
    1481            2 :     mt_server_init(); mt_server_reset();
    1482            2 :     MtProtoSession s; load_session(&s);
    1483            2 :     mt_server_expect(CRC_messages_searchGlobal, on_search_global_capture, NULL);
    1484              : 
    1485            2 :     ApiConfig cfg; init_cfg(&cfg);
    1486            2 :     Transport t; connect_mock(&t);
    1487              : 
    1488              :     HistoryEntry hits[8];
    1489            2 :     int n = -1;
    1490            2 :     ASSERT(domain_search_global(&cfg, &s, &t, "hello", 10, hits, &n) == 0,
    1491              :            "search_global succeeds");
    1492            2 :     ASSERT(n == 3, "three hits returned");
    1493            2 :     ASSERT(hits[0].id == 1001, "first hit id == 1001");
    1494            2 :     ASSERT(hits[1].id == 1002, "second hit id == 1002");
    1495            2 :     ASSERT(hits[2].id == 1003, "third hit id == 1003");
    1496            2 :     ASSERT(strcmp(hits[0].text, "hello world") == 0, "first hit text");
    1497            2 :     ASSERT(hits[0].date == 1700100000, "first hit date");
    1498            2 :     ASSERT(g_search_req.crc == CRC_messages_searchGlobal,
    1499              :            "request CRC is searchGlobal");
    1500            2 :     ASSERT(strcmp(g_search_req.query, "hello") == 0,
    1501              :            "query string threaded to wire");
    1502              : 
    1503            2 :     transport_close(&t);
    1504            2 :     mt_server_reset();
    1505              : }
    1506              : 
    1507              : /* TEST-10b: messages.search per-peer — two results, inputPeerUser on wire. */
    1508            2 : static void test_search_per_peer_happy(void) {
    1509            2 :     with_tmp_home("srch-peer");
    1510            2 :     mt_server_init(); mt_server_reset();
    1511            2 :     MtProtoSession s; load_session(&s);
    1512            2 :     mt_server_expect(CRC_messages_search, on_search_peer_capture, NULL);
    1513              : 
    1514            2 :     ApiConfig cfg; init_cfg(&cfg);
    1515            2 :     Transport t; connect_mock(&t);
    1516              : 
    1517            2 :     HistoryPeer peer = {
    1518              :         .kind        = HISTORY_PEER_USER,
    1519              :         .peer_id     = 5555LL,
    1520              :         .access_hash = 0xABCDEF1234567890LL,
    1521              :     };
    1522              :     HistoryEntry hits[8];
    1523            2 :     int n = -1;
    1524            2 :     ASSERT(domain_search_peer(&cfg, &s, &t, &peer, "find me", 5, hits, &n) == 0,
    1525              :            "search_peer succeeds");
    1526            2 :     ASSERT(n == 2, "two hits returned");
    1527            2 :     ASSERT(hits[0].id == 2001, "first hit id == 2001");
    1528            2 :     ASSERT(hits[1].id == 2002, "second hit id == 2002");
    1529            2 :     ASSERT(strcmp(hits[0].text, "peer match one") == 0, "first hit text");
    1530            2 :     ASSERT(hits[0].date == 1700200000, "first hit date");
    1531            2 :     ASSERT(g_search_req.crc == CRC_messages_search,
    1532              :            "request CRC is messages.search");
    1533            2 :     ASSERT(g_search_req.peer_crc == TL_inputPeerUser,
    1534              :            "peer field carries inputPeerUser");
    1535            2 :     ASSERT(strcmp(g_search_req.query, "find me") == 0,
    1536              :            "query string threaded to wire");
    1537              : 
    1538            2 :     transport_close(&t);
    1539            2 :     mt_server_reset();
    1540              : }
    1541              : 
    1542              : /* TEST-10c: limit field equals what was passed (FEAT-08). */
    1543            2 : static void test_search_limit_respected(void) {
    1544            2 :     with_tmp_home("srch-limit");
    1545            2 :     mt_server_init(); mt_server_reset();
    1546            2 :     MtProtoSession s; load_session(&s);
    1547            2 :     mt_server_expect(CRC_messages_searchGlobal, on_search_global_capture, NULL);
    1548              : 
    1549            2 :     ApiConfig cfg; init_cfg(&cfg);
    1550            2 :     Transport t; connect_mock(&t);
    1551              : 
    1552              :     HistoryEntry hits[8];
    1553            2 :     int n = -1;
    1554            2 :     ASSERT(domain_search_global(&cfg, &s, &t, "test", 7, hits, &n) == 0,
    1555              :            "search_global with limit=7 succeeds");
    1556            2 :     ASSERT(g_search_req.limit == 7, "limit == 7 on wire");
    1557              : 
    1558            2 :     transport_close(&t);
    1559            2 :     mt_server_reset();
    1560              : }
    1561              : 
    1562              : /* TEST-25: updates.getDifference error-then-success path.
    1563              :  *
    1564              :  * First call: mock returns rpc_error(500, "INTERNAL").
    1565              :  * Second call: mock returns updates.differenceEmpty.
    1566              :  * Assert first call returns -1, second returns 0.
    1567              :  * Verify two getDifference frames hit the server. */
    1568              : 
    1569              : static int g_diff_call_seq = 0;
    1570              : 
    1571            4 : static void on_diff_error_then_empty(MtRpcContext *ctx) {
    1572            4 :     g_diff_call_seq++;
    1573            4 :     if (g_diff_call_seq == 1) {
    1574              :         /* First call: simulate a transient server error. */
    1575            2 :         mt_server_reply_error(ctx, 500, "INTERNAL_SERVER_ERROR");
    1576              :     } else {
    1577              :         /* Subsequent calls: return differenceEmpty. */
    1578              :         TlWriter w;
    1579            2 :         tl_writer_init(&w);
    1580            2 :         tl_write_uint32(&w, TL_updates_differenceEmpty);
    1581            2 :         tl_write_int32 (&w, 1700000500);   /* date */
    1582            2 :         tl_write_int32 (&w, 2);            /* seq */
    1583            2 :         mt_server_reply_result(ctx, w.data, w.len);
    1584            2 :         tl_writer_free(&w);
    1585              :     }
    1586            4 : }
    1587              : 
    1588              : /* TEST-28: dialogs --archived sends folder_id=1 on the wire; inbox sends 0. */
    1589            2 : static void test_dialogs_archived_folder_id(void) {
    1590              :     /* ---- Part A: archived=1 → flags.1 set, folder_id == 1 ---- */
    1591            2 :     with_tmp_home("dlg-arch");
    1592            2 :     mt_server_init(); mt_server_reset();
    1593            2 :     dialogs_cache_flush();
    1594            2 :     MtProtoSession s; load_session(&s);
    1595            2 :     mt_server_expect(CRC_messages_getDialogs, on_dialogs_capture_and_reply, NULL);
    1596              : 
    1597            2 :     ApiConfig cfg; init_cfg(&cfg);
    1598            2 :     Transport t; connect_mock(&t);
    1599              : 
    1600            2 :     memset(&g_dialogs_req, 0, sizeof(g_dialogs_req));
    1601              :     DialogEntry rows[8];
    1602            2 :     int n = -1;
    1603            2 :     ASSERT(domain_get_dialogs(&cfg, &s, &t, 8, /*archived=*/1, rows, &n, NULL) == 0,
    1604              :            "dialogs archived=1 succeeds");
    1605            2 :     ASSERT((g_dialogs_req.flags & (1u << 1)) != 0,
    1606              :            "flags bit 1 set for archived request");
    1607            2 :     ASSERT(g_dialogs_req.folder_id == 1,
    1608              :            "folder_id == 1 on wire for archived request");
    1609              : 
    1610            2 :     transport_close(&t);
    1611            2 :     mt_server_reset();
    1612              : 
    1613              :     /* ---- Part B: archived=0 → flags.1 clear, folder_id field absent ---- */
    1614            2 :     with_tmp_home("dlg-inbox");
    1615            2 :     mt_server_init(); mt_server_reset();
    1616            2 :     dialogs_cache_flush();
    1617            2 :     load_session(&s);
    1618            2 :     mt_server_expect(CRC_messages_getDialogs, on_dialogs_capture_and_reply, NULL);
    1619              : 
    1620            2 :     init_cfg(&cfg);
    1621            2 :     connect_mock(&t);
    1622              : 
    1623            2 :     memset(&g_dialogs_req, 0, sizeof(g_dialogs_req));
    1624            2 :     n = -1;
    1625            2 :     ASSERT(domain_get_dialogs(&cfg, &s, &t, 8, /*archived=*/0, rows, &n, NULL) == 0,
    1626              :            "dialogs archived=0 succeeds");
    1627            2 :     ASSERT((g_dialogs_req.flags & (1u << 1)) == 0,
    1628              :            "flags bit 1 clear for inbox request");
    1629            2 :     ASSERT(g_dialogs_req.folder_id == 0,
    1630              :            "folder_id not present on wire for inbox request");
    1631              : 
    1632            2 :     transport_close(&t);
    1633            2 :     mt_server_reset();
    1634              : }
    1635              : 
    1636            2 : static void test_watch_backoff_then_succeed(void) {
    1637            2 :     with_tmp_home("upd-backoff");
    1638            2 :     mt_server_init(); mt_server_reset();
    1639            2 :     g_diff_call_seq = 0;
    1640            2 :     MtProtoSession s; load_session(&s);
    1641            2 :     mt_server_expect(CRC_updates_getDifference, on_diff_error_then_empty, NULL);
    1642              : 
    1643            2 :     ApiConfig cfg; init_cfg(&cfg);
    1644            2 :     Transport t; connect_mock(&t);
    1645              : 
    1646            2 :     UpdatesState prev = { .pts = 100, .qts = 5, .date = 1700000000, .seq = 1 };
    1647              : 
    1648              :     /* First call: server returns 500 — domain must return -1. */
    1649            2 :     UpdatesDifference diff1 = {0};
    1650            2 :     ASSERT(domain_updates_difference(&cfg, &s, &t, &prev, &diff1) == -1,
    1651              :            "first getDifference returns -1 on RPC error");
    1652              : 
    1653              :     /* Second call: server returns differenceEmpty — domain must return 0. */
    1654            2 :     UpdatesDifference diff2 = {0};
    1655            2 :     ASSERT(domain_updates_difference(&cfg, &s, &t, &prev, &diff2) == 0,
    1656              :            "second getDifference succeeds after error");
    1657            2 :     ASSERT(diff2.is_empty == 1, "second call marked empty");
    1658              : 
    1659              :     /* Verify the server received exactly two getDifference frames. */
    1660            2 :     ASSERT(mt_server_request_crc_count(CRC_updates_getDifference) == 2,
    1661              :            "two getDifference frames sent to server");
    1662              : 
    1663            2 :     transport_close(&t);
    1664            2 :     mt_server_reset();
    1665              : }
    1666              : 
    1667            2 : void run_read_path_tests(void) {
    1668            2 :     RUN_TEST(test_get_self);
    1669            2 :     RUN_TEST(test_get_self_premium);
    1670            2 :     RUN_TEST(test_self_alias_maps_to_cmd_me);
    1671            2 :     RUN_TEST(test_dialogs_empty);
    1672            2 :     RUN_TEST(test_dialogs_one_user);
    1673            2 :     RUN_TEST(test_dialogs_slice_variant);
    1674            2 :     RUN_TEST(test_dialogs_not_modified_variant);
    1675            2 :     RUN_TEST(test_dialogs_archived_folder_id);
    1676            2 :     RUN_TEST(test_history_empty);
    1677            2 :     RUN_TEST(test_history_one_message_empty);
    1678            2 :     RUN_TEST(test_history_self);
    1679            2 :     RUN_TEST(test_history_user_numeric_id);
    1680            2 :     RUN_TEST(test_history_username_resolve);
    1681            2 :     RUN_TEST(test_history_channel_access_hash);
    1682            2 :     RUN_TEST(test_history_offset_flag);
    1683            2 :     RUN_TEST(test_history_cache_hit);
    1684            2 :     RUN_TEST(test_contacts_empty);
    1685            2 :     RUN_TEST(test_contacts_two);
    1686            2 :     RUN_TEST(test_resolve_username_happy);
    1687            2 :     RUN_TEST(test_resolve_username_not_found);
    1688            2 :     RUN_TEST(test_resolve_username_channel);
    1689            2 :     RUN_TEST(test_get_full_user_happy);
    1690            2 :     RUN_TEST(test_updates_state);
    1691            2 :     RUN_TEST(test_updates_difference_empty);
    1692            2 :     RUN_TEST(test_updates_difference_with_messages);
    1693            2 :     RUN_TEST(test_updates_differenceSlice_with_messages);
    1694            2 :     RUN_TEST(test_rpc_error_propagation);
    1695            2 :     RUN_TEST(test_search_global_happy);
    1696            2 :     RUN_TEST(test_search_per_peer_happy);
    1697            2 :     RUN_TEST(test_search_limit_respected);
    1698            2 :     RUN_TEST(test_watch_backoff_then_succeed);
    1699            2 : }
        

Generated by: LCOV version 2.0-1