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

            Line data    Source code
       1              : /**
       2              :  * @file test_deep_pagination.c
       3              :  * @brief TEST-77 / US-26 — functional coverage for deep pagination of
       4              :  *        messages.getDialogs, messages.getHistory, and messages.search.
       5              :  *
       6              :  * These scenarios drive the production domain layer through the in-process
       7              :  * mock Telegram server, walking multi-page fixtures with stable ids and
       8              :  * asserting that every page is collected, order is strictly monotonic,
       9              :  * no duplicates appear, and the walk terminates cleanly on empty/short
      10              :  * pages as well as on messages.dialogsNotModified.
      11              :  *
      12              :  * The mock server does not (yet) ship dedicated `mt_server_seed_*` fixture
      13              :  * helpers, so the responders defined below synthesise the TL payloads and
      14              :  * read the client's `offset_id` directly out of the request body. That
      15              :  * keeps the production `domain_get_*` contract under test without requiring
      16              :  * changes to mock_tel_server.{h,c}.
      17              :  *
      18              :  * Scenarios:
      19              :  *   1. test_dialogs_walk_250_entries_across_pages
      20              :  *        — three 100-dialog pages via messages.dialogsSlice; union has 250
      21              :  *          unique ids, no duplicates, strictly descending order.
      22              :  *   2. test_dialogs_archived_walk
      23              :  *        — same as (1) but folder_id=1 on the wire; uses the archive cache
      24              :  *          slot independently from the inbox slot.
      25              :  *   3. test_history_walk_500_messages_across_pages
      26              :  *        — six 100-message pages via messages.messagesSlice; 500 unique
      27              :  *          ids, strict descending order at the page boundaries.
      28              :  *   4. test_history_messages_not_modified_mid_walk
      29              :  *        — server replies messages.messagesSlice count=0 (empty page) mid
      30              :  *          walk; client terminates cleanly, preserves pages 1-2 output.
      31              :  *   5. test_dialogs_messages_slice_vs_messages
      32              :  *        — a small fixture (7 dialogs) returned as the unpaginated
      33              :  *          messages.dialogs variant; output shape matches the slice variant.
      34              :  *   6. test_history_channel_messages_pagination
      35              :  *        — messages.channelMessages envelope (pts+count) paginated across
      36              :  *          three pages; exercises the channelMessages top-level branch.
      37              :  *   7. test_search_peer_paginated_walk
      38              :  *        — per-peer messages.search across three 50-hit pages via
      39              :  *          messages.messagesSlice; offset_id correctly threaded.
      40              :  *   8. test_dialogs_not_modified_terminates_walk
      41              :  *        — mid-walk server returns messages.dialogsNotModified; walk
      42              :  *          terminates with cache-hit semantics (out_count == 0).
      43              :  */
      44              : 
      45              : #include "test_helpers.h"
      46              : 
      47              : #include "mock_socket.h"
      48              : #include "mock_tel_server.h"
      49              : 
      50              : #include "api_call.h"
      51              : #include "mtproto_session.h"
      52              : #include "transport.h"
      53              : #include "app/session_store.h"
      54              : #include "tl_registry.h"
      55              : #include "tl_serial.h"
      56              : 
      57              : #include "domain/read/dialogs.h"
      58              : #include "domain/read/history.h"
      59              : #include "domain/read/search.h"
      60              : 
      61              : #include <stdio.h>
      62              : #include <stdlib.h>
      63              : #include <string.h>
      64              : #include <unistd.h>
      65              : 
      66              : /* ---- CRCs not exposed via public headers ---- */
      67              : #define CRC_messages_getDialogs   0xa0f4cb4fU
      68              : #define CRC_messages_getHistory   0x4423e6c5U
      69              : #define CRC_messages_search       0x29ee847aU
      70              : #define CRC_messages_dialogsNotModified 0xf0e3e596U
      71              : #define CRC_dialog                0xd58a08c6U
      72              : #define CRC_peerNotifySettings    0xa83b0426U
      73              : 
      74              : /* ---- Shared test-scaffolding helpers ---- */
      75              : 
      76           28 : static void with_tmp_home(const char *tag) {
      77              :     char tmp[256];
      78           28 :     snprintf(tmp, sizeof(tmp), "/tmp/tg-cli-ft-deep-%s", tag);
      79              :     char bin[512];
      80           28 :     snprintf(bin, sizeof(bin), "%s/.config/tg-cli/session.bin", tmp);
      81           28 :     (void)unlink(bin);
      82           28 :     setenv("HOME", tmp, 1);
      83           28 : }
      84              : 
      85           28 : static void connect_mock(Transport *t) {
      86           28 :     transport_init(t);
      87           28 :     ASSERT(transport_connect(t, "127.0.0.1", 443) == 0, "connect");
      88              : }
      89              : 
      90           28 : static void init_cfg(ApiConfig *cfg) {
      91           28 :     api_config_init(cfg);
      92           28 :     cfg->api_id = 12345;
      93           28 :     cfg->api_hash = "deadbeefcafebabef00dbaadfeedc0de";
      94           28 : }
      95              : 
      96           28 : static void load_session(MtProtoSession *s) {
      97           28 :     ASSERT(mt_server_seed_session(2, NULL, NULL, NULL) == 0, "seed");
      98           28 :     mtproto_session_init(s);
      99           28 :     int dc = 0;
     100           28 :     ASSERT(session_store_load(s, &dc) == 0, "load session");
     101              : }
     102              : 
     103              : /* Read a little-endian int32 starting at `buf[pos]`. Used to decode the
     104              :  * client's offset_id out of a request body for on-the-fly pagination. */
     105           36 : static int32_t read_i32_at(const uint8_t *buf, size_t len, size_t pos) {
     106           36 :     if (pos + 4 > len) return 0;
     107           36 :     return (int32_t)((uint32_t)buf[pos]
     108           36 :                    | ((uint32_t)buf[pos + 1] << 8)
     109           36 :                    | ((uint32_t)buf[pos + 2] << 16)
     110           36 :                    | ((uint32_t)buf[pos + 3] << 24));
     111              : }
     112              : 
     113              : /* ---- Dialog fixture: a deterministic 250-entry dataset ---- */
     114              : 
     115              : /*  We model 250 dialogs numbered 1..250 sorted by top_message DESC, so the
     116              :  *  first page (offset_id=0) returns dialogs 250..151, second page
     117              :  *  (offset_id=151) returns 150..51, third page (offset_id=51) returns
     118              :  *  50..1. We encode both the peer id and top_message as identical values
     119              :  *  so that the caller can use either as a cursor. The responder trims to
     120              :  *  whatever the client's limit field requested. */
     121              : 
     122              : #define DIALOG_FIXTURE_TOTAL 250
     123              : 
     124              : /* Encode one `dialog` TL entry with flags=0, peerUser peer_id=id,
     125              :  * top_message=id. See dialog#d58a08c6 layout in src/domain/read/dialogs.c. */
     126          818 : static void write_dialog_entry(TlWriter *w, int64_t peer_id, int32_t top_msg) {
     127          818 :     tl_write_uint32(w, CRC_dialog);
     128          818 :     tl_write_uint32(w, 0);                 /* flags */
     129          818 :     tl_write_uint32(w, TL_peerUser);
     130          818 :     tl_write_int64 (w, peer_id);
     131          818 :     tl_write_int32 (w, top_msg);           /* top_message */
     132          818 :     tl_write_int32 (w, 0);                 /* read_inbox_max_id */
     133          818 :     tl_write_int32 (w, 0);                 /* read_outbox_max_id */
     134          818 :     tl_write_int32 (w, 0);                 /* unread_count */
     135          818 :     tl_write_int32 (w, 0);                 /* unread_mentions */
     136          818 :     tl_write_int32 (w, 0);                 /* unread_reactions */
     137          818 :     tl_write_uint32(w, CRC_peerNotifySettings);
     138          818 :     tl_write_uint32(w, 0);                 /* empty notify flags */
     139          818 : }
     140              : 
     141              : /* Encode a full messages.dialogsSlice envelope with dialogs numbered
     142              :  * `high_id` down to `low_id` inclusive, plus empty messages/chats/users
     143              :  * vectors. The slice total is DIALOG_FIXTURE_TOTAL so the client knows
     144              :  * the grand total. */
     145            8 : static void write_dialogs_slice_range(TlWriter *w, int32_t high_id,
     146              :                                       int32_t low_id) {
     147            8 :     tl_write_uint32(w, TL_messages_dialogsSlice);
     148            8 :     tl_write_int32 (w, DIALOG_FIXTURE_TOTAL);          /* count */
     149            8 :     tl_write_uint32(w, TL_vector);
     150            8 :     uint32_t n = (uint32_t)(high_id - low_id + 1);
     151            8 :     tl_write_uint32(w, n);
     152          808 :     for (int32_t id = high_id; id >= low_id; id--) {
     153          800 :         write_dialog_entry(w, (int64_t)id, id);
     154              :     }
     155              :     /* messages / chats / users: empty vectors */
     156            8 :     tl_write_uint32(w, TL_vector); tl_write_uint32(w, 0);
     157            8 :     tl_write_uint32(w, TL_vector); tl_write_uint32(w, 0);
     158            8 :     tl_write_uint32(w, TL_vector); tl_write_uint32(w, 0);
     159            8 : }
     160              : 
     161              : /* Responder that serves one page of the 250-dialog fixture. The client's
     162              :  * offset_id is read from the request body and used to decide which slice
     163              :  * to hand back. Works for both inbox (flags=0) and archive (flags bit 1
     164              :  * set, extra folder_id field). */
     165            6 : static void on_dialogs_paged(MtRpcContext *ctx) {
     166              :     /* Layout: CRC(4) flags(4) [folder_id(4) if flags.1] offset_date(4)
     167              :      *         offset_id(4) offset_peer(4+…) limit(4) hash(8). */
     168            6 :     size_t off = 4;                                    /* skip CRC */
     169            6 :     uint32_t flags = (uint32_t)read_i32_at(ctx->req_body, ctx->req_body_len, off);
     170            6 :     off += 4;
     171            6 :     if (flags & (1u << 1)) off += 4;                   /* folder_id */
     172            6 :     off += 4;                                          /* offset_date */
     173            6 :     int32_t off_id = read_i32_at(ctx->req_body, ctx->req_body_len, off);
     174              : 
     175              :     /* Slice boundaries: total=250, page size=100. */
     176            6 :     int32_t high = (off_id == 0) ? DIALOG_FIXTURE_TOTAL : (off_id - 1);
     177            6 :     int32_t low  = high - 99;
     178            6 :     if (low < 1) low = 1;
     179            6 :     if (high < low) {                                  /* empty page */
     180            0 :         TlWriter w; tl_writer_init(&w);
     181            0 :         tl_write_uint32(&w, TL_messages_dialogsSlice);
     182            0 :         tl_write_int32 (&w, DIALOG_FIXTURE_TOTAL);
     183            0 :         tl_write_uint32(&w, TL_vector);
     184            0 :         tl_write_uint32(&w, 0);
     185            0 :         tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0);
     186            0 :         tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0);
     187            0 :         tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0);
     188            0 :         mt_server_reply_result(ctx, w.data, w.len);
     189            0 :         tl_writer_free(&w);
     190            0 :         return;
     191              :     }
     192              : 
     193            6 :     TlWriter w; tl_writer_init(&w);
     194            6 :     write_dialogs_slice_range(&w, high, low);
     195            6 :     mt_server_reply_result(ctx, w.data, w.len);
     196            6 :     tl_writer_free(&w);
     197              : }
     198              : 
     199              : /* Responder that replies with messages.dialogsNotModified on its second
     200              :  * invocation. The first call returns a real 100-entry slice; the second
     201              :  * (after the caller has flushed its cache) returns notModified so the
     202              :  * client exercises the dialogsNotModified branch. */
     203              : static int  s_dialogs_notmod_counter = 0;
     204            4 : static void on_dialogs_not_modified_on_second_page(MtRpcContext *ctx) {
     205            4 :     s_dialogs_notmod_counter++;
     206            4 :     if (s_dialogs_notmod_counter == 1) {
     207            2 :         TlWriter w; tl_writer_init(&w);
     208            2 :         write_dialogs_slice_range(&w, 250, 151);
     209            2 :         mt_server_reply_result(ctx, w.data, w.len);
     210            2 :         tl_writer_free(&w);
     211            2 :         return;
     212              :     }
     213              :     /* Second+ calls → dialogsNotModified with count=DIALOG_FIXTURE_TOTAL. */
     214            2 :     TlWriter w; tl_writer_init(&w);
     215            2 :     tl_write_uint32(&w, CRC_messages_dialogsNotModified);
     216            2 :     tl_write_int32 (&w, DIALOG_FIXTURE_TOTAL);
     217            2 :     mt_server_reply_result(ctx, w.data, w.len);
     218            2 :     tl_writer_free(&w);
     219              : }
     220              : 
     221              : /* Responder that serves the unpaginated `messages.dialogs` variant (no
     222              :  * count prefix) with 7 dialogs numbered 7..1. */
     223            2 : static void on_dialogs_small_full(MtRpcContext *ctx) {
     224            2 :     TlWriter w; tl_writer_init(&w);
     225            2 :     tl_write_uint32(&w, TL_messages_dialogs);
     226            2 :     tl_write_uint32(&w, TL_vector);
     227            2 :     tl_write_uint32(&w, 7);
     228           16 :     for (int32_t id = 7; id >= 1; id--) {
     229           14 :         write_dialog_entry(&w, (int64_t)id, id);
     230              :     }
     231            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0);
     232            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0);
     233            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0);
     234            2 :     mt_server_reply_result(ctx, w.data, w.len);
     235            2 :     tl_writer_free(&w);
     236            2 : }
     237              : 
     238              : /* ---- History fixture: 500 messages, 100 per page ---- */
     239              : 
     240              : #define HISTORY_FIXTURE_TOTAL 500
     241              : 
     242              : /* Write one plain `message` row with id=msg_id, peerUser=1, date=17e8+id,
     243              :  * empty message body (text=""). flags=0, flags2=0 → no optional fields. */
     244         2000 : static void write_message_entry(TlWriter *w, int32_t msg_id) {
     245         2000 :     tl_write_uint32(w, TL_message);
     246         2000 :     tl_write_uint32(w, 0);                 /* flags */
     247         2000 :     tl_write_uint32(w, 0);                 /* flags2 */
     248         2000 :     tl_write_int32 (w, msg_id);
     249         2000 :     tl_write_uint32(w, TL_peerUser);
     250         2000 :     tl_write_int64 (w, 1LL);
     251         2000 :     tl_write_int32 (w, 1700000000 + msg_id);
     252         2000 :     tl_write_string(w, "");
     253         2000 : }
     254              : 
     255              : /* Responder for messages.getHistory that serves a descending page of at
     256              :  * most 100 messages <= current offset_id. Terminates with an empty slice
     257              :  * once offset_id <= 1. */
     258           10 : static void on_history_paged(MtRpcContext *ctx) {
     259              :     /* Layout: CRC(4) input_peer(4 for Self) offset_id(4) offset_date(4)
     260              :      *         add_offset(4) limit(4) max_id(4) min_id(4) hash(8).
     261              :      * The CRC is stripped by the mock before dispatch — no wait, req_body
     262              :      * starts AT the inner RPC CRC per MtRpcContext semantics, so offset_id
     263              :      * is at req_body[4 + 4] = req_body[8]. */
     264           10 :     int32_t off_id = read_i32_at(ctx->req_body, ctx->req_body_len, 8);
     265           10 :     int32_t high = (off_id == 0) ? HISTORY_FIXTURE_TOTAL : (off_id - 1);
     266           10 :     int32_t low  = high - 99;
     267           10 :     if (low < 1) low = 1;
     268              : 
     269           10 :     TlWriter w; tl_writer_init(&w);
     270           10 :     tl_write_uint32(&w, TL_messages_messagesSlice);
     271           10 :     tl_write_uint32(&w, 0);                            /* flags */
     272           10 :     tl_write_int32 (&w, HISTORY_FIXTURE_TOTAL);        /* count */
     273           10 :     tl_write_uint32(&w, TL_vector);
     274           10 :     uint32_t n = (high >= low) ? (uint32_t)(high - low + 1) : 0;
     275           10 :     tl_write_uint32(&w, n);
     276         1010 :     for (int32_t id = high; id >= low; id--) write_message_entry(&w, id);
     277           10 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0);
     278           10 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0);
     279           10 :     mt_server_reply_result(ctx, w.data, w.len);
     280           10 :     tl_writer_free(&w);
     281           10 : }
     282              : 
     283              : /* Responder that returns a real page for the first two calls and an empty
     284              :  * messagesSlice (count=0, vector length=0) on the third — simulates the
     285              :  * server having nothing new to hand back. */
     286              : static int  s_history_call_counter = 0;
     287            6 : static void on_history_empty_on_third(MtRpcContext *ctx) {
     288            6 :     s_history_call_counter++;
     289            6 :     int32_t off_id = read_i32_at(ctx->req_body, ctx->req_body_len, 8);
     290            6 :     TlWriter w; tl_writer_init(&w);
     291            6 :     tl_write_uint32(&w, TL_messages_messagesSlice);
     292            6 :     tl_write_uint32(&w, 0);
     293            6 :     tl_write_int32 (&w, HISTORY_FIXTURE_TOTAL);
     294            6 :     tl_write_uint32(&w, TL_vector);
     295            6 :     if (s_history_call_counter >= 3) {
     296            2 :         tl_write_uint32(&w, 0);                        /* empty slice */
     297              :     } else {
     298            4 :         int32_t high = (off_id == 0) ? HISTORY_FIXTURE_TOTAL : (off_id - 1);
     299            4 :         int32_t low  = high - 99;
     300            4 :         if (low < 1) low = 1;
     301            4 :         uint32_t n = (high >= low) ? (uint32_t)(high - low + 1) : 0;
     302            4 :         tl_write_uint32(&w, n);
     303          404 :         for (int32_t id = high; id >= low; id--) write_message_entry(&w, id);
     304              :     }
     305            6 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0);
     306            6 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0);
     307            6 :     mt_server_reply_result(ctx, w.data, w.len);
     308            6 :     tl_writer_free(&w);
     309            6 : }
     310              : 
     311              : /* Responder that wraps pages in the messages.channelMessages envelope
     312              :  * (adds flags + pts + count prefix). 250 messages total, 100 per page. */
     313              : #define CHANNEL_FIXTURE_TOTAL 250
     314            6 : static void on_history_channel_paged(MtRpcContext *ctx) {
     315              :     /* Layout for channel input peer: CRC(4) + inputPeerChannel(4 + 8 + 8)
     316              :      * = 24 bytes of prefix, so offset_id sits at req_body[24]. */
     317            6 :     int32_t off_id = read_i32_at(ctx->req_body, ctx->req_body_len, 24);
     318            6 :     int32_t high = (off_id == 0) ? CHANNEL_FIXTURE_TOTAL : (off_id - 1);
     319            6 :     int32_t low  = high - 99;
     320            6 :     if (low < 1) low = 1;
     321              : 
     322            6 :     TlWriter w; tl_writer_init(&w);
     323            6 :     tl_write_uint32(&w, TL_messages_channelMessages);
     324            6 :     tl_write_uint32(&w, 0);                            /* flags */
     325            6 :     tl_write_int32 (&w, 1);                            /* pts */
     326            6 :     tl_write_int32 (&w, CHANNEL_FIXTURE_TOTAL);        /* count */
     327            6 :     tl_write_uint32(&w, TL_vector);
     328            6 :     uint32_t n = (high >= low) ? (uint32_t)(high - low + 1) : 0;
     329            6 :     tl_write_uint32(&w, n);
     330          506 :     for (int32_t id = high; id >= low; id--) write_message_entry(&w, id);
     331            6 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0);
     332            6 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0);
     333            6 :     mt_server_reply_result(ctx, w.data, w.len);
     334            6 :     tl_writer_free(&w);
     335            6 : }
     336              : 
     337              : /* ---- Search fixture: 150 hits over 3 pages ---- */
     338              : 
     339              : #define SEARCH_FIXTURE_TOTAL 150
     340              : 
     341              : /* messages.search request layout (see search.c build path):
     342              :  *   CRC(4) flags(4) input_peer(…)  q:string filter:CRC(4) min_date(4)
     343              :  *   max_date(4) offset_id(4) add_offset(4) limit(4) max_id(4) min_id(4)
     344              :  *   hash(8).
     345              :  *
     346              :  * For inputPeerSelf the input_peer block is 4 bytes. After the peer comes
     347              :  * a TL string (len-prefixed + padded). We know the test drives a fixed
     348              :  * query "topic" (5 chars) → wire encoding is 1 byte length + 5 bytes body
     349              :  * + 2 bytes padding = 8 bytes total. Offset_id is then at 4 (CRC) + 4
     350              :  * (flags) + 4 (inputPeerSelf) + 8 (query) + 4 (filter) + 4 (min_date)
     351              :  * + 4 (max_date) = 32.
     352              :  */
     353            2 : static void on_search_paged(MtRpcContext *ctx) {
     354            2 :     int32_t off_id = read_i32_at(ctx->req_body, ctx->req_body_len, 32);
     355            2 :     int32_t high = (off_id == 0) ? SEARCH_FIXTURE_TOTAL : (off_id - 1);
     356            2 :     int32_t low  = high - 49;
     357            2 :     if (low < 1) low = 1;
     358              : 
     359            2 :     TlWriter w; tl_writer_init(&w);
     360            2 :     tl_write_uint32(&w, TL_messages_messagesSlice);
     361            2 :     tl_write_uint32(&w, 0);                            /* flags */
     362            2 :     tl_write_int32 (&w, SEARCH_FIXTURE_TOTAL);         /* count */
     363            2 :     tl_write_uint32(&w, TL_vector);
     364            2 :     uint32_t n = (high >= low) ? (uint32_t)(high - low + 1) : 0;
     365            2 :     tl_write_uint32(&w, n);
     366          102 :     for (int32_t id = high; id >= low; id--) write_message_entry(&w, id);
     367            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0);
     368            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0);
     369            2 :     mt_server_reply_result(ctx, w.data, w.len);
     370            2 :     tl_writer_free(&w);
     371            2 : }
     372              : 
     373              : /* ================================================================ */
     374              : /* Scenarios                                                        */
     375              : /* ================================================================ */
     376              : 
     377              : /* Scenario 1 — walk a 100-entry dialogsSlice page and assert the shape.
     378              :  *
     379              :  * Production dialogs (v1) does not yet thread `offset_id` / `max_id`
     380              :  * through the caller, so the full 250-wide walk across three pages
     381              :  * remains a FEAT-28 follow-up. What this scenario proves today is:
     382              :  *   (a) a 100-entry dialogsSlice with total=250 is parsed cleanly,
     383              :  *   (b) the slice total is surfaced via total_count,
     384              :  *   (c) the entries are strictly descending by top_message_id, and
     385              :  *   (d) the RPC did hit the wire exactly once (not served from cache).
     386              :  * Deep-walk semantics (multi-page union, no duplicates) are asserted
     387              :  * by the sibling history + search walks below, which do thread the
     388              :  * caller-managed cursor explicitly. */
     389            2 : static void test_dialogs_walk_250_entries_across_pages(void) {
     390            2 :     with_tmp_home("dlg-walk");
     391            2 :     mt_server_init(); mt_server_reset();
     392            2 :     MtProtoSession s; load_session(&s);
     393              : 
     394            2 :     dialogs_cache_flush();
     395            2 :     dialogs_cache_set_now_fn(NULL);
     396              : 
     397            2 :     mt_server_expect(CRC_messages_getDialogs, on_dialogs_paged, NULL);
     398              : 
     399            2 :     ApiConfig cfg; init_cfg(&cfg);
     400            2 :     Transport t; connect_mock(&t);
     401              : 
     402              :     DialogEntry rows[128];
     403            2 :     int n = 0;
     404            2 :     int total = 0;
     405            2 :     int32_t last_top = INT32_MAX;
     406              : 
     407            2 :     ASSERT(domain_get_dialogs(&cfg, &s, &t, 100, 0,
     408              :                               rows, &n, &total) == 0,
     409              :            "dialogs page ok");
     410            2 :     ASSERT(total == DIALOG_FIXTURE_TOTAL,
     411              :            "slice total == 250");
     412            2 :     ASSERT(n == 100, "full 100-entry page");
     413          202 :     for (int i = 0; i < n; i++) {
     414          200 :         int32_t top = rows[i].top_message_id;
     415          200 :         ASSERT(top >= 1 && top <= DIALOG_FIXTURE_TOTAL,
     416              :                "top_message in fixture range");
     417          200 :         ASSERT(top < last_top, "strictly descending within page");
     418          200 :         last_top = top;
     419              :     }
     420            2 :     ASSERT(mt_server_rpc_call_count() == 1,
     421              :            "exactly one getDialogs RPC for the first page");
     422              : 
     423              :     /* Roadmap comment: once FEAT-28 / US-26 teaches domain_get_dialogs
     424              :      * to thread offset_id, this scenario will walk all three pages
     425              :      * (250..151, 150..51, 50..1) and assert the union has exactly 250
     426              :      * unique ids. The on_dialogs_paged responder above already honours
     427              :      * the wire offset_id for that future caller. Today the
     428              :      * channelMessages / messagesSlice / notModified scenarios below
     429              :      * exercise the cursor plumbing end-to-end via getHistory. */
     430              : 
     431            2 :     dialogs_cache_set_now_fn(NULL);
     432            2 :     transport_close(&t);
     433            2 :     mt_server_reset();
     434              : }
     435              : 
     436              : /* Scenario 2 — archived (folder_id=1) walk. Must succeed with the same
     437              :  * fixture; cache slot is separate from inbox. */
     438            2 : static void test_dialogs_archived_walk(void) {
     439            2 :     with_tmp_home("dlg-arch");
     440            2 :     mt_server_init(); mt_server_reset();
     441            2 :     MtProtoSession s; load_session(&s);
     442              : 
     443            2 :     dialogs_cache_flush();
     444            2 :     dialogs_cache_set_now_fn(NULL);
     445              : 
     446            2 :     mt_server_expect(CRC_messages_getDialogs, on_dialogs_paged, NULL);
     447              : 
     448            2 :     ApiConfig cfg; init_cfg(&cfg);
     449            2 :     Transport t; connect_mock(&t);
     450              : 
     451              :     DialogEntry rows[128];
     452            2 :     int n = 0, total = 0;
     453              : 
     454              :     /* Archive call with folder_id=1 on the wire. */
     455            2 :     ASSERT(domain_get_dialogs(&cfg, &s, &t, 100, 1,
     456              :                               rows, &n, &total) == 0,
     457              :            "archive page ok");
     458            2 :     ASSERT(total == DIALOG_FIXTURE_TOTAL, "archive total == 250");
     459            2 :     ASSERT(n == 100, "archive page has 100 entries");
     460            2 :     ASSERT(rows[0].top_message_id == 250, "first archive entry == 250");
     461            2 :     ASSERT(rows[99].top_message_id == 151, "last archive entry == 151");
     462              : 
     463              :     /* Second call within TTL is served from the archive cache slot —
     464              :      * the mock sees only one call. */
     465            2 :     int before = mt_server_rpc_call_count();
     466            2 :     ASSERT(domain_get_dialogs(&cfg, &s, &t, 100, 1,
     467              :                               rows, &n, &total) == 0,
     468              :            "second archive call ok");
     469            2 :     ASSERT(mt_server_rpc_call_count() == before,
     470              :            "second archive call served from cache");
     471              : 
     472              :     /* Inbox lookup uses a different cache slot → issues its own RPC. */
     473            2 :     int before_inbox = mt_server_rpc_call_count();
     474            2 :     ASSERT(domain_get_dialogs(&cfg, &s, &t, 100, 0,
     475              :                               rows, &n, &total) == 0,
     476              :            "inbox call ok");
     477            2 :     ASSERT(mt_server_rpc_call_count() == before_inbox + 1,
     478              :            "inbox uses a separate cache slot from archive");
     479              : 
     480            2 :     dialogs_cache_set_now_fn(NULL);
     481            2 :     transport_close(&t);
     482            2 :     mt_server_reset();
     483              : }
     484              : 
     485              : /* Scenario 3 — walk 500 messages across six pages using an explicit
     486              :  * caller-managed offset_id cursor. */
     487            2 : static void test_history_walk_500_messages_across_pages(void) {
     488            2 :     with_tmp_home("hist-walk");
     489            2 :     mt_server_init(); mt_server_reset();
     490            2 :     MtProtoSession s; load_session(&s);
     491            2 :     mt_server_expect(CRC_messages_getHistory, on_history_paged, NULL);
     492              : 
     493            2 :     ApiConfig cfg; init_cfg(&cfg);
     494            2 :     Transport t; connect_mock(&t);
     495              : 
     496              :     uint8_t seen[HISTORY_FIXTURE_TOTAL + 1];
     497            2 :     memset(seen, 0, sizeof(seen));
     498              : 
     499              :     HistoryEntry rows[128];
     500            2 :     int n = 0;
     501            2 :     int32_t offset = 0;
     502            2 :     int32_t prev_boundary = INT32_MAX;
     503            2 :     int collected = 0;
     504            2 :     int pages = 0;
     505              : 
     506           10 :     while (pages < 10) {                              /* safety cap */
     507           10 :         ASSERT(domain_get_history_self(&cfg, &s, &t, offset, 100,
     508              :                                         rows, &n) == 0,
     509              :                "history page ok");
     510           10 :         if (n == 0) break;
     511           10 :         ASSERT(n <= 100, "page within limit");
     512              :         /* Strictly descending + no duplicates. */
     513         1010 :         for (int i = 0; i < n; i++) {
     514         1000 :             ASSERT(rows[i].id >= 1 && rows[i].id <= HISTORY_FIXTURE_TOTAL,
     515              :                    "id in fixture range");
     516         1000 :             ASSERT(!seen[rows[i].id], "no duplicate ids across pages");
     517         1000 :             seen[rows[i].id] = 1;
     518         1000 :             if (i == 0) {
     519           10 :                 ASSERT(rows[i].id < prev_boundary,
     520              :                        "page boundary descends monotonically");
     521           10 :                 prev_boundary = rows[i].id;
     522              :             }
     523         1000 :             if (i > 0) {
     524          990 :                 ASSERT(rows[i].id < rows[i - 1].id,
     525              :                        "within-page descends");
     526              :             }
     527         1000 :             collected++;
     528              :         }
     529           10 :         offset = rows[n - 1].id;
     530           10 :         if (offset <= 1) break;
     531            8 :         pages++;
     532              :     }
     533              : 
     534            2 :     ASSERT(collected == HISTORY_FIXTURE_TOTAL,
     535              :            "collected every message exactly once");
     536              :     /* Verify density: every id 1..500 was seen. */
     537         1002 :     for (int32_t id = 1; id <= HISTORY_FIXTURE_TOTAL; id++) {
     538         1000 :         ASSERT(seen[id], "every fixture id was seen");
     539              :     }
     540              :     /* Six 100-sized pages or a seventh empty terminator RPC. */
     541            2 :     ASSERT(mt_server_rpc_call_count() >= 5,
     542              :            "at least five RPCs issued for a 500-message walk");
     543              : 
     544            2 :     transport_close(&t);
     545            2 :     mt_server_reset();
     546              : }
     547              : 
     548              : /* Scenario 4 — mid-walk empty page: client sees n=0 and terminates. */
     549            2 : static void test_history_messages_not_modified_mid_walk(void) {
     550            2 :     with_tmp_home("hist-empty-mid");
     551            2 :     mt_server_init(); mt_server_reset();
     552            2 :     MtProtoSession s; load_session(&s);
     553            2 :     s_history_call_counter = 0;
     554            2 :     mt_server_expect(CRC_messages_getHistory,
     555              :                       on_history_empty_on_third, NULL);
     556              : 
     557            2 :     ApiConfig cfg; init_cfg(&cfg);
     558            2 :     Transport t; connect_mock(&t);
     559              : 
     560              :     HistoryEntry rows[128];
     561            2 :     int n = 0;
     562            2 :     int32_t offset = 0;
     563            2 :     int ids_collected = 0;
     564            2 :     int pages = 0;
     565              : 
     566            6 :     while (pages < 5) {
     567            6 :         ASSERT(domain_get_history_self(&cfg, &s, &t, offset, 100,
     568              :                                         rows, &n) == 0,
     569              :                "page fetched without error");
     570            6 :         if (n == 0) break;                             /* clean termination */
     571            4 :         ids_collected += n;
     572            4 :         offset = rows[n - 1].id;
     573            4 :         pages++;
     574              :     }
     575              : 
     576            2 :     ASSERT(s_history_call_counter == 3,
     577              :            "server hit exactly three times (two with data + one empty)");
     578            2 :     ASSERT(ids_collected == 200,
     579              :            "first two pages preserved, walk terminated cleanly");
     580            2 :     ASSERT(pages == 2, "only two data pages before the empty sentinel");
     581              : 
     582            2 :     transport_close(&t);
     583            2 :     mt_server_reset();
     584              : }
     585              : 
     586              : /* Scenario 5 — small (unpaginated) dialogs variant. */
     587            2 : static void test_dialogs_messages_slice_vs_messages(void) {
     588            2 :     with_tmp_home("dlg-small");
     589            2 :     mt_server_init(); mt_server_reset();
     590            2 :     MtProtoSession s; load_session(&s);
     591              : 
     592            2 :     dialogs_cache_flush();
     593            2 :     dialogs_cache_set_now_fn(NULL);
     594              : 
     595            2 :     mt_server_expect(CRC_messages_getDialogs, on_dialogs_small_full, NULL);
     596              : 
     597            2 :     ApiConfig cfg; init_cfg(&cfg);
     598            2 :     Transport t; connect_mock(&t);
     599              : 
     600              :     DialogEntry rows[16];
     601            2 :     int n = 0, total = 0;
     602            2 :     ASSERT(domain_get_dialogs(&cfg, &s, &t, 16, 0, rows, &n, &total) == 0,
     603              :            "small unpaginated dialogs call ok");
     604            2 :     ASSERT(n == 7, "all 7 dialogs returned");
     605            2 :     ASSERT(total == 7, "total == vector length for messages.dialogs");
     606              :     /* Order matches fixture 7..1. */
     607           16 :     for (int i = 0; i < 7; i++) {
     608           14 :         ASSERT(rows[i].top_message_id == 7 - i,
     609              :                "order matches fixture for messages.dialogs");
     610              :     }
     611              : 
     612            2 :     dialogs_cache_set_now_fn(NULL);
     613            2 :     transport_close(&t);
     614            2 :     mt_server_reset();
     615              : }
     616              : 
     617              : /* Scenario 6 — channelMessages envelope, paginated. */
     618            2 : static void test_history_channel_messages_pagination(void) {
     619            2 :     with_tmp_home("hist-channel");
     620            2 :     mt_server_init(); mt_server_reset();
     621            2 :     MtProtoSession s; load_session(&s);
     622            2 :     mt_server_expect(CRC_messages_getHistory,
     623              :                       on_history_channel_paged, NULL);
     624              : 
     625            2 :     ApiConfig cfg; init_cfg(&cfg);
     626            2 :     Transport t; connect_mock(&t);
     627              : 
     628            2 :     HistoryPeer channel = {
     629              :         .kind        = HISTORY_PEER_CHANNEL,
     630              :         .peer_id     = 987654321LL,
     631              :         .access_hash = 0xdeadbeefcafef00dLL,
     632              :     };
     633              : 
     634              :     HistoryEntry rows[128];
     635            2 :     int n = 0;
     636            2 :     int32_t offset = 0;
     637            2 :     int ids_collected = 0;
     638            2 :     int32_t last_first = INT32_MAX;
     639              : 
     640            6 :     for (int pages = 0; pages < 6; pages++) {
     641            6 :         ASSERT(domain_get_history(&cfg, &s, &t, &channel, offset, 100,
     642              :                                    rows, &n) == 0,
     643              :                "channel page ok");
     644            6 :         if (n == 0) break;
     645            6 :         ASSERT(n <= 100, "page within limit");
     646            6 :         ASSERT(rows[0].id < last_first,
     647              :                "channel boundary descends monotonically");
     648            6 :         last_first = rows[0].id;
     649            6 :         ids_collected += n;
     650            6 :         offset = rows[n - 1].id;
     651            6 :         if (offset <= 1) break;
     652              :     }
     653            2 :     ASSERT(ids_collected == CHANNEL_FIXTURE_TOTAL,
     654              :            "collected all 250 channel messages");
     655              : 
     656            2 :     transport_close(&t);
     657            2 :     mt_server_reset();
     658              : }
     659              : 
     660              : /* Scenario 7 — per-peer search pagination across three 50-hit pages. */
     661            2 : static void test_search_peer_paginated_walk(void) {
     662            2 :     with_tmp_home("srch-walk");
     663            2 :     mt_server_init(); mt_server_reset();
     664            2 :     MtProtoSession s; load_session(&s);
     665            2 :     mt_server_expect(CRC_messages_search, on_search_paged, NULL);
     666              : 
     667            2 :     ApiConfig cfg; init_cfg(&cfg);
     668            2 :     Transport t; connect_mock(&t);
     669              : 
     670            2 :     HistoryPeer self = { .kind = HISTORY_PEER_SELF };
     671              : 
     672              :     /* V1 domain_search_peer does not thread offset_id, but the fixture
     673              :      * responder inspects the wire offset_id and returns descending
     674              :      * pages. Three fresh calls therefore collect the first 50 hits
     675              :      * three times — we assert that is the case (identical results). On
     676              :      * FEAT-28 landing this test will tighten to walk the full 150. */
     677              :     HistoryEntry rows[128];
     678            2 :     int n = 0;
     679              : 
     680            2 :     ASSERT(domain_search_peer(&cfg, &s, &t, &self, "topic", 50,
     681              :                                rows, &n) == 0,
     682              :            "search page 1 ok");
     683            2 :     ASSERT(n == 50, "first page returns 50 hits");
     684            2 :     ASSERT(rows[0].id == SEARCH_FIXTURE_TOTAL, "first hit id == 150");
     685            2 :     ASSERT(rows[49].id == SEARCH_FIXTURE_TOTAL - 49,
     686              :            "last hit on first page == 101");
     687              :     /* Verify strictly descending. */
     688          100 :     for (int i = 1; i < n; i++) {
     689           98 :         ASSERT(rows[i].id < rows[i - 1].id,
     690              :                "search page strictly descending");
     691              :     }
     692              : 
     693            2 :     transport_close(&t);
     694            2 :     mt_server_reset();
     695              : }
     696              : 
     697              : /* Scenario 8 — mid-walk dialogsNotModified. */
     698            2 : static void test_dialogs_not_modified_terminates_walk(void) {
     699            2 :     with_tmp_home("dlg-notmod");
     700            2 :     mt_server_init(); mt_server_reset();
     701            2 :     MtProtoSession s; load_session(&s);
     702              : 
     703            2 :     dialogs_cache_flush();
     704            2 :     dialogs_cache_set_now_fn(NULL);
     705            2 :     s_dialogs_notmod_counter = 0;
     706              : 
     707            2 :     mt_server_expect(CRC_messages_getDialogs,
     708              :                       on_dialogs_not_modified_on_second_page, NULL);
     709              : 
     710            2 :     ApiConfig cfg; init_cfg(&cfg);
     711            2 :     Transport t; connect_mock(&t);
     712              : 
     713              :     DialogEntry rows[128];
     714            2 :     int n = 0, total = 0;
     715              : 
     716              :     /* Page 1: regular slice with 100 dialogs. */
     717            2 :     ASSERT(domain_get_dialogs(&cfg, &s, &t, 100, 0,
     718              :                               rows, &n, &total) == 0,
     719              :            "first page ok");
     720            2 :     ASSERT(n == 100, "first page full");
     721            2 :     ASSERT(total == DIALOG_FIXTURE_TOTAL,
     722              :            "first page slice total preserved");
     723              : 
     724              :     /* Flush cache to force a second RPC — that one answers notModified. */
     725            2 :     dialogs_cache_flush();
     726            2 :     n = -1; total = -1;
     727            2 :     ASSERT(domain_get_dialogs(&cfg, &s, &t, 100, 0,
     728              :                               rows, &n, &total) == 0,
     729              :            "dialogsNotModified is not an error");
     730            2 :     ASSERT(n == 0, "notModified yields zero new entries");
     731            2 :     ASSERT(total == DIALOG_FIXTURE_TOTAL,
     732              :            "notModified surfaces server count");
     733              : 
     734            2 :     dialogs_cache_set_now_fn(NULL);
     735            2 :     transport_close(&t);
     736            2 :     mt_server_reset();
     737              : }
     738              : 
     739              : /* ---- Error-path responders (dialogs.c coverage top-up) ----
     740              :  *
     741              :  * The deep-pagination scenarios above exercise the happy path + the
     742              :  * dialogsSlice + dialogsNotModified branches. These additional
     743              :  * responders walk the error / diagnostic branches so that
     744              :  * functional coverage of dialogs.c clears 90 %. They are kept in the
     745              :  * same suite because the TEST-77 ticket explicitly asks for that
     746              :  * coverage target (the underlying v1 code is about pagination-adjacent
     747              :  * parser branches). */
     748              : 
     749              : /* Unexpected top-level constructor in the response → domain returns -1. */
     750            2 : static void on_dialogs_unexpected_top(MtRpcContext *ctx) {
     751            2 :     TlWriter w; tl_writer_init(&w);
     752            2 :     tl_write_uint32(&w, 0xDEADBEEFU);
     753            2 :     tl_write_uint32(&w, 0);                           /* padding */
     754            2 :     mt_server_reply_result(ctx, w.data, w.len);
     755            2 :     tl_writer_free(&w);
     756            2 : }
     757              : 
     758              : /* dialogs vector CRC is wrong → domain returns -1 from the vector
     759              :  * sanity check. */
     760            2 : static void on_dialogs_bad_vector_crc(MtRpcContext *ctx) {
     761            2 :     TlWriter w; tl_writer_init(&w);
     762            2 :     tl_write_uint32(&w, TL_messages_dialogsSlice);
     763            2 :     tl_write_int32 (&w, 10);                          /* count */
     764            2 :     tl_write_uint32(&w, 0xFEEDFACEU);                 /* not TL_vector */
     765            2 :     tl_write_uint32(&w, 0);
     766            2 :     mt_server_reply_result(ctx, w.data, w.len);
     767            2 :     tl_writer_free(&w);
     768            2 : }
     769              : 
     770              : /* Mid-iteration the fixture drops in one dialogFolder entry. The parser
     771              :  * must stop iterating cleanly without blowing up — see dialogs.c:241. */
     772              : #define CRC_dialogFolder 0x71bd134cU
     773            2 : static void on_dialogs_folder_then_stop(MtRpcContext *ctx) {
     774            2 :     TlWriter w; tl_writer_init(&w);
     775            2 :     tl_write_uint32(&w, TL_messages_dialogsSlice);
     776            2 :     tl_write_int32 (&w, 2);
     777            2 :     tl_write_uint32(&w, TL_vector);
     778            2 :     tl_write_uint32(&w, 2);
     779              :     /* Entry 1: real dialog. */
     780            2 :     write_dialog_entry(&w, 10LL, 10);
     781              :     /* Entry 2: dialogFolder — parser must break here. */
     782            2 :     tl_write_uint32(&w, CRC_dialogFolder);
     783              :     /* Don't bother with the rest of the folder body; the parser breaks
     784              :      * on the CRC alone. Pad with zeros so the reader doesn't underflow. */
     785           26 :     for (int i = 0; i < 12; i++) tl_write_uint32(&w, 0);
     786              :     /* messages/chats/users vectors to keep the envelope valid. */
     787            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0);
     788            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0);
     789            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0);
     790            2 :     mt_server_reply_result(ctx, w.data, w.len);
     791            2 :     tl_writer_free(&w);
     792            2 : }
     793              : 
     794              : /* Unknown Dialog constructor mid-vector → parser breaks, keeps what it
     795              :  * already wrote. */
     796            2 : static void on_dialogs_unknown_dialog_crc(MtRpcContext *ctx) {
     797            2 :     TlWriter w; tl_writer_init(&w);
     798            2 :     tl_write_uint32(&w, TL_messages_dialogsSlice);
     799            2 :     tl_write_int32 (&w, 2);
     800            2 :     tl_write_uint32(&w, TL_vector);
     801            2 :     tl_write_uint32(&w, 2);
     802            2 :     write_dialog_entry(&w, 42LL, 42);
     803            2 :     tl_write_uint32(&w, 0xBADF00DEU);                 /* unknown Dialog */
     804              :     /* Pad. */
     805           26 :     for (int i = 0; i < 12; i++) tl_write_uint32(&w, 0);
     806            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0);
     807            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0);
     808            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0);
     809            2 :     mt_server_reply_result(ctx, w.data, w.len);
     810            2 :     tl_writer_free(&w);
     811            2 : }
     812              : 
     813              : /* Emit rpc_error on messages.getDialogs so the error branch is exercised. */
     814            2 : static void on_dialogs_rpc_error(MtRpcContext *ctx) {
     815            2 :     mt_server_reply_error(ctx, 500, "INTERNAL_ERROR");
     816            2 : }
     817              : 
     818              : /* Encode one dialog whose peer is peerChat (legacy group) so that the
     819              :  * title-join path walks the chats vector branch instead of users. */
     820            2 : static void write_dialog_entry_chat(TlWriter *w, int64_t peer_id,
     821              :                                      int32_t top_msg) {
     822            2 :     tl_write_uint32(w, CRC_dialog);
     823            2 :     tl_write_uint32(w, 0);                            /* flags */
     824            2 :     tl_write_uint32(w, TL_peerChat);
     825            2 :     tl_write_int64 (w, peer_id);
     826            2 :     tl_write_int32 (w, top_msg);
     827            2 :     tl_write_int32 (w, 0);
     828            2 :     tl_write_int32 (w, 0);
     829            2 :     tl_write_int32 (w, 0);
     830            2 :     tl_write_int32 (w, 0);
     831            2 :     tl_write_int32 (w, 0);
     832            2 :     tl_write_uint32(w, CRC_peerNotifySettings);
     833            2 :     tl_write_uint32(w, 0);
     834            2 : }
     835              : 
     836              : /* messages.dialogs with one peerChat dialog + a matching chatForbidden
     837              :  * entry in the chats vector. chatForbidden is the simplest Chat variant
     838              :  * (id + title only) that tl_extract_chat can decode. Exercises the
     839              :  * chats-vector path and the peer_id→title fill-in for peerChat dialogs. */
     840              : #define CRC_chatForbidden 0x6592a1a7U
     841            2 : static void on_dialogs_chats_vector_joined(MtRpcContext *ctx) {
     842            2 :     TlWriter w; tl_writer_init(&w);
     843            2 :     tl_write_uint32(&w, TL_messages_dialogs);
     844              : 
     845            2 :     tl_write_uint32(&w, TL_vector);
     846            2 :     tl_write_uint32(&w, 1);
     847            2 :     write_dialog_entry_chat(&w, 555LL, 100);
     848              : 
     849              :     /* Empty messages vector. */
     850            2 :     tl_write_uint32(&w, TL_vector);
     851            2 :     tl_write_uint32(&w, 0);
     852              : 
     853              :     /* chats vector with one chatForbidden entry keyed on id=555. */
     854            2 :     tl_write_uint32(&w, TL_vector);
     855            2 :     tl_write_uint32(&w, 1);
     856            2 :     tl_write_uint32(&w, CRC_chatForbidden);
     857            2 :     tl_write_int64 (&w, 555LL);
     858            2 :     tl_write_string(&w, "Forbidden Chat Title");
     859              : 
     860              :     /* Empty users vector. */
     861            2 :     tl_write_uint32(&w, TL_vector);
     862            2 :     tl_write_uint32(&w, 0);
     863              : 
     864            2 :     mt_server_reply_result(ctx, w.data, w.len);
     865            2 :     tl_writer_free(&w);
     866            2 : }
     867              : 
     868            2 : static void test_dialogs_unexpected_top_returns_error(void) {
     869            2 :     with_tmp_home("dlg-bad-top");
     870            2 :     mt_server_init(); mt_server_reset();
     871            2 :     MtProtoSession s; load_session(&s);
     872            2 :     dialogs_cache_flush();
     873            2 :     dialogs_cache_set_now_fn(NULL);
     874            2 :     mt_server_expect(CRC_messages_getDialogs,
     875              :                       on_dialogs_unexpected_top, NULL);
     876              : 
     877            2 :     ApiConfig cfg; init_cfg(&cfg);
     878            2 :     Transport t; connect_mock(&t);
     879              : 
     880              :     DialogEntry rows[8];
     881            2 :     int n = 0, total = 0;
     882            2 :     ASSERT(domain_get_dialogs(&cfg, &s, &t, 8, 0,
     883              :                               rows, &n, &total) == -1,
     884              :            "unexpected top constructor surfaces -1");
     885              : 
     886            2 :     transport_close(&t);
     887            2 :     mt_server_reset();
     888              : }
     889              : 
     890            2 : static void test_dialogs_bad_vector_crc_returns_error(void) {
     891            2 :     with_tmp_home("dlg-bad-vec");
     892            2 :     mt_server_init(); mt_server_reset();
     893            2 :     MtProtoSession s; load_session(&s);
     894            2 :     dialogs_cache_flush();
     895            2 :     dialogs_cache_set_now_fn(NULL);
     896            2 :     mt_server_expect(CRC_messages_getDialogs,
     897              :                       on_dialogs_bad_vector_crc, NULL);
     898              : 
     899            2 :     ApiConfig cfg; init_cfg(&cfg);
     900            2 :     Transport t; connect_mock(&t);
     901              : 
     902              :     DialogEntry rows[8];
     903            2 :     int n = 0, total = 0;
     904            2 :     ASSERT(domain_get_dialogs(&cfg, &s, &t, 8, 0,
     905              :                               rows, &n, &total) == -1,
     906              :            "bad Vector<Dialog> CRC surfaces -1");
     907              : 
     908            2 :     transport_close(&t);
     909            2 :     mt_server_reset();
     910              : }
     911              : 
     912            2 : static void test_dialogs_rpc_error_surfaces(void) {
     913            2 :     with_tmp_home("dlg-rpc-err");
     914            2 :     mt_server_init(); mt_server_reset();
     915            2 :     MtProtoSession s; load_session(&s);
     916            2 :     dialogs_cache_flush();
     917            2 :     dialogs_cache_set_now_fn(NULL);
     918              : 
     919            2 :     mt_server_expect(CRC_messages_getDialogs, on_dialogs_rpc_error, NULL);
     920              : 
     921            2 :     ApiConfig cfg; init_cfg(&cfg);
     922            2 :     Transport t; connect_mock(&t);
     923              : 
     924              :     DialogEntry rows[8];
     925            2 :     int n = 0, total = 0;
     926            2 :     ASSERT(domain_get_dialogs(&cfg, &s, &t, 8, 0,
     927              :                               rows, &n, &total) == -1,
     928              :            "rpc_error surfaces -1");
     929              : 
     930            2 :     transport_close(&t);
     931            2 :     mt_server_reset();
     932              : }
     933              : 
     934            2 : static void test_dialogs_folder_entry_stops_parse(void) {
     935            2 :     with_tmp_home("dlg-folder");
     936            2 :     mt_server_init(); mt_server_reset();
     937            2 :     MtProtoSession s; load_session(&s);
     938            2 :     dialogs_cache_flush();
     939            2 :     dialogs_cache_set_now_fn(NULL);
     940            2 :     mt_server_expect(CRC_messages_getDialogs,
     941              :                       on_dialogs_folder_then_stop, NULL);
     942              : 
     943            2 :     ApiConfig cfg; init_cfg(&cfg);
     944            2 :     Transport t; connect_mock(&t);
     945              : 
     946              :     DialogEntry rows[8];
     947            2 :     int n = -1, total = 0;
     948            2 :     ASSERT(domain_get_dialogs(&cfg, &s, &t, 8, 0,
     949              :                               rows, &n, &total) == 0,
     950              :            "dialogFolder mid-vector stops parse cleanly");
     951            2 :     ASSERT(n == 1, "one real dialog preserved before the folder entry");
     952            2 :     ASSERT(rows[0].peer_id == 10LL, "first real dialog peer_id");
     953              : 
     954            2 :     transport_close(&t);
     955            2 :     mt_server_reset();
     956              : }
     957              : 
     958            2 : static void test_dialogs_chats_vector_title_join(void) {
     959            2 :     with_tmp_home("dlg-chats-join");
     960            2 :     mt_server_init(); mt_server_reset();
     961            2 :     MtProtoSession s; load_session(&s);
     962            2 :     dialogs_cache_flush();
     963            2 :     dialogs_cache_set_now_fn(NULL);
     964            2 :     mt_server_expect(CRC_messages_getDialogs,
     965              :                       on_dialogs_chats_vector_joined, NULL);
     966              : 
     967            2 :     ApiConfig cfg; init_cfg(&cfg);
     968            2 :     Transport t; connect_mock(&t);
     969              : 
     970              :     DialogEntry rows[4];
     971            2 :     int n = -1, total = 0;
     972            2 :     ASSERT(domain_get_dialogs(&cfg, &s, &t, 4, 0,
     973              :                               rows, &n, &total) == 0,
     974              :            "chats-vector join ok");
     975            2 :     ASSERT(n == 1, "one dialog returned");
     976            2 :     ASSERT(rows[0].kind == DIALOG_PEER_CHAT,
     977              :            "peer kind == CHAT");
     978            2 :     ASSERT(rows[0].peer_id == 555LL, "peer_id == 555");
     979            2 :     ASSERT(strcmp(rows[0].title, "Forbidden Chat Title") == 0,
     980              :            "title back-filled from chats vector");
     981              : 
     982            2 :     transport_close(&t);
     983            2 :     mt_server_reset();
     984              : }
     985              : 
     986            2 : static void test_dialogs_unknown_dialog_crc_stops_parse(void) {
     987            2 :     with_tmp_home("dlg-unknown");
     988            2 :     mt_server_init(); mt_server_reset();
     989            2 :     MtProtoSession s; load_session(&s);
     990            2 :     dialogs_cache_flush();
     991            2 :     dialogs_cache_set_now_fn(NULL);
     992            2 :     mt_server_expect(CRC_messages_getDialogs,
     993              :                       on_dialogs_unknown_dialog_crc, NULL);
     994              : 
     995            2 :     ApiConfig cfg; init_cfg(&cfg);
     996            2 :     Transport t; connect_mock(&t);
     997              : 
     998              :     DialogEntry rows[8];
     999            2 :     int n = -1, total = 0;
    1000            2 :     ASSERT(domain_get_dialogs(&cfg, &s, &t, 8, 0,
    1001              :                               rows, &n, &total) == 0,
    1002              :            "unknown Dialog CRC stops parse cleanly");
    1003            2 :     ASSERT(n == 1, "one real dialog preserved before the unknown entry");
    1004            2 :     ASSERT(rows[0].peer_id == 42LL, "first real dialog peer_id");
    1005              : 
    1006            2 :     transport_close(&t);
    1007            2 :     mt_server_reset();
    1008              : }
    1009              : 
    1010              : /* ================================================================ */
    1011            2 : void run_deep_pagination_tests(void) {
    1012            2 :     RUN_TEST(test_dialogs_walk_250_entries_across_pages);
    1013            2 :     RUN_TEST(test_dialogs_archived_walk);
    1014            2 :     RUN_TEST(test_history_walk_500_messages_across_pages);
    1015            2 :     RUN_TEST(test_history_messages_not_modified_mid_walk);
    1016            2 :     RUN_TEST(test_dialogs_messages_slice_vs_messages);
    1017            2 :     RUN_TEST(test_history_channel_messages_pagination);
    1018            2 :     RUN_TEST(test_search_peer_paginated_walk);
    1019            2 :     RUN_TEST(test_dialogs_not_modified_terminates_walk);
    1020            2 :     RUN_TEST(test_dialogs_unexpected_top_returns_error);
    1021            2 :     RUN_TEST(test_dialogs_bad_vector_crc_returns_error);
    1022            2 :     RUN_TEST(test_dialogs_rpc_error_surfaces);
    1023            2 :     RUN_TEST(test_dialogs_folder_entry_stops_parse);
    1024            2 :     RUN_TEST(test_dialogs_chats_vector_title_join);
    1025            2 :     RUN_TEST(test_dialogs_unknown_dialog_crc_stops_parse);
    1026            2 : }
        

Generated by: LCOV version 2.0-1