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