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