Line data Source code
1 : /**
2 : * @file test_history_rich_metadata.c
3 : * @brief TEST-79 — functional coverage for rich message metadata.
4 : *
5 : * US-28 identifies five Message flag branches in
6 : * `src/domain/read/history.c` that the existing read-path suite never
7 : * feeds: fwd_from (flags.2), via_bot_id (flags.11), reply_to (flags.3),
8 : * via_business_bot_id (flags2.0), and saved_peer_id (flags.28). Each
9 : * branch has a skipper in tl_skip.c that must execute for the parser
10 : * to surface the message body rather than drop the row.
11 : *
12 : * These scenarios craft real messages.Messages payloads carrying each
13 : * flag combination, drive the production domain_get_history() through
14 : * the in-process mock server, and assert that id / date / text still
15 : * arrive intact. Full "[fwd from @channel]" rendering is US-28 scope;
16 : * this suite validates the parse-through contract that unlocks it.
17 : */
18 :
19 : #include "test_helpers.h"
20 :
21 : #include "mock_socket.h"
22 : #include "mock_tel_server.h"
23 :
24 : #include "api_call.h"
25 : #include "mtproto_session.h"
26 : #include "transport.h"
27 : #include "app/session_store.h"
28 : #include "tl_registry.h"
29 : #include "tl_serial.h"
30 :
31 : #include "domain/read/history.h"
32 :
33 : #include <stdio.h>
34 : #include <stdlib.h>
35 : #include <string.h>
36 : #include <unistd.h>
37 :
38 : /* ---- CRCs not re-exposed from public headers ---- */
39 : #define CRC_messages_getHistory 0x4423e6c5U
40 : #define CRC_messageFwdHeader 0x4e4df4bbU
41 : #define CRC_messageReplyHeader 0xafbc09dbU
42 :
43 : /* Message flag bits tested here. */
44 : #define MSG_FLAG_FWD_FROM (1u << 2)
45 : #define MSG_FLAG_REPLY_TO (1u << 3)
46 : #define MSG_FLAG_FROM_ID (1u << 8)
47 : #define MSG_FLAG_VIA_BOT (1u << 11)
48 : #define MSG_FLAG_SAVED_PEER (1u << 28)
49 : #define MSG2_FLAG_VIA_BIZ_BOT (1u << 0)
50 :
51 : /* Fwd-header flag bits used below. */
52 : #define FWD_HAS_FROM_ID (1u << 0)
53 : #define FWD_HAS_FROM_NAME (1u << 5)
54 :
55 : /* Reply-header flag bits used below. */
56 : #define REPLY_HAS_MSG_ID (1u << 4)
57 :
58 : /* ---- boilerplate ---- */
59 :
60 16 : static void with_tmp_home(const char *tag) {
61 : char tmp[256];
62 16 : snprintf(tmp, sizeof(tmp), "/tmp/tg-cli-ft-hist-rich-%s", tag);
63 : char bin[512];
64 16 : snprintf(bin, sizeof(bin), "%s/.config/tg-cli/session.bin", tmp);
65 16 : (void)unlink(bin);
66 16 : setenv("HOME", tmp, 1);
67 16 : }
68 :
69 16 : static void connect_mock(Transport *t) {
70 16 : transport_init(t);
71 16 : ASSERT(transport_connect(t, "127.0.0.1", 443) == 0, "connect");
72 : }
73 :
74 16 : static void init_cfg(ApiConfig *cfg) {
75 16 : api_config_init(cfg);
76 16 : cfg->api_id = 12345;
77 16 : cfg->api_hash = "deadbeefcafebabef00dbaadfeedc0de";
78 16 : }
79 :
80 16 : static void load_session(MtProtoSession *s) {
81 16 : ASSERT(mt_server_seed_session(2, NULL, NULL, NULL) == 0, "seed");
82 16 : mtproto_session_init(s);
83 16 : int dc = 0;
84 16 : ASSERT(session_store_load(s, &dc) == 0, "load session");
85 : }
86 :
87 : /* Envelope: messages.messages { messages: Vector<Message>{1}, chats, users }
88 : * with the caller providing the inner message bytes (starting at TL_message). */
89 16 : static void wrap_messages_messages(TlWriter *w, const uint8_t *msg_bytes,
90 : size_t msg_len) {
91 16 : tl_write_uint32(w, TL_messages_messages);
92 16 : tl_write_uint32(w, TL_vector);
93 16 : tl_write_uint32(w, 1);
94 16 : tl_write_raw(w, msg_bytes, msg_len);
95 16 : tl_write_uint32(w, TL_vector); tl_write_uint32(w, 0); /* chats */
96 16 : tl_write_uint32(w, TL_vector); tl_write_uint32(w, 0); /* users */
97 16 : }
98 :
99 : /* ================================================================ */
100 : /* Responders */
101 : /* ================================================================ */
102 :
103 : /* Shared pre-text layout (no from_id unless FROM_ID flag set):
104 : * TL_message | flags | flags2 | id(i32)
105 : * [from_id:Peer if flags.8]
106 : * peer_id:Peer (peerUser 1)
107 : * [saved_peer_id:Peer if flags.28]
108 : * [fwd_from:MessageFwdHeader if flags.2]
109 : * [via_bot_id:i64 if flags.11]
110 : * [via_business_bot_id:i64 if flags2.0]
111 : * [reply_to:MessageReplyHeader if flags.3]
112 : * date(i32) message:string
113 : */
114 :
115 : /* Test 1 — fwd_from with a channel peer (from_id variant). */
116 2 : static void on_history_fwd_from_channel(MtRpcContext *ctx) {
117 2 : TlWriter inner; tl_writer_init(&inner);
118 2 : tl_write_uint32(&inner, TL_message);
119 2 : tl_write_uint32(&inner, MSG_FLAG_FWD_FROM);
120 2 : tl_write_uint32(&inner, 0); /* flags2 */
121 2 : tl_write_int32 (&inner, 10347); /* id */
122 2 : tl_write_uint32(&inner, TL_peerUser); /* peer_id */
123 2 : tl_write_int64 (&inner, 1LL);
124 : /* messageFwdHeader flags: from_id=peerChannel */
125 2 : tl_write_uint32(&inner, CRC_messageFwdHeader);
126 2 : tl_write_uint32(&inner, FWD_HAS_FROM_ID);
127 2 : tl_write_uint32(&inner, TL_peerChannel); /* from_id */
128 2 : tl_write_int64 (&inner, 12345678LL);
129 2 : tl_write_int32 (&inner, 1700000123); /* fwd date */
130 2 : tl_write_int32 (&inner, 1700000200); /* outer date */
131 2 : tl_write_string(&inner, "fresh news"); /* message */
132 :
133 2 : TlWriter w; tl_writer_init(&w);
134 2 : wrap_messages_messages(&w, inner.data, inner.len);
135 2 : mt_server_reply_result(ctx, w.data, w.len);
136 2 : tl_writer_free(&w);
137 2 : tl_writer_free(&inner);
138 2 : }
139 :
140 : /* Test 2 — fwd_from with a hidden-user from_name (string, not peer). */
141 2 : static void on_history_fwd_from_hidden(MtRpcContext *ctx) {
142 2 : TlWriter inner; tl_writer_init(&inner);
143 2 : tl_write_uint32(&inner, TL_message);
144 2 : tl_write_uint32(&inner, MSG_FLAG_FWD_FROM);
145 2 : tl_write_uint32(&inner, 0);
146 2 : tl_write_int32 (&inner, 10348);
147 2 : tl_write_uint32(&inner, TL_peerUser);
148 2 : tl_write_int64 (&inner, 1LL);
149 : /* fwd header with only from_name (flag 5). */
150 2 : tl_write_uint32(&inner, CRC_messageFwdHeader);
151 2 : tl_write_uint32(&inner, FWD_HAS_FROM_NAME);
152 2 : tl_write_string(&inner, "HiddenSender"); /* from_name */
153 2 : tl_write_int32 (&inner, 1700000500); /* fwd date */
154 2 : tl_write_int32 (&inner, 1700000600); /* outer date */
155 2 : tl_write_string(&inner, "hidden fwd body");
156 :
157 2 : TlWriter w; tl_writer_init(&w);
158 2 : wrap_messages_messages(&w, inner.data, inner.len);
159 2 : mt_server_reply_result(ctx, w.data, w.len);
160 2 : tl_writer_free(&w);
161 2 : tl_writer_free(&inner);
162 2 : }
163 :
164 : /* Test 3 — reply_to referencing msg_id=12345. */
165 2 : static void on_history_reply_to(MtRpcContext *ctx) {
166 2 : TlWriter inner; tl_writer_init(&inner);
167 2 : tl_write_uint32(&inner, TL_message);
168 2 : tl_write_uint32(&inner, MSG_FLAG_REPLY_TO);
169 2 : tl_write_uint32(&inner, 0);
170 2 : tl_write_int32 (&inner, 12346);
171 2 : tl_write_uint32(&inner, TL_peerUser);
172 2 : tl_write_int64 (&inner, 1LL);
173 : /* messageReplyHeader with reply_to_msg_id. */
174 2 : tl_write_uint32(&inner, CRC_messageReplyHeader);
175 2 : tl_write_uint32(&inner, REPLY_HAS_MSG_ID);
176 2 : tl_write_int32 (&inner, 12345); /* reply_to_msg_id */
177 2 : tl_write_int32 (&inner, 1700000800); /* date */
178 2 : tl_write_string(&inner, "yes");
179 :
180 2 : TlWriter w; tl_writer_init(&w);
181 2 : wrap_messages_messages(&w, inner.data, inner.len);
182 2 : mt_server_reply_result(ctx, w.data, w.len);
183 2 : tl_writer_free(&w);
184 2 : tl_writer_free(&inner);
185 2 : }
186 :
187 : /* Test 4 — via_bot_id (e.g. @gif). */
188 2 : static void on_history_via_bot(MtRpcContext *ctx) {
189 2 : TlWriter inner; tl_writer_init(&inner);
190 2 : tl_write_uint32(&inner, TL_message);
191 2 : tl_write_uint32(&inner, MSG_FLAG_VIA_BOT);
192 2 : tl_write_uint32(&inner, 0);
193 2 : tl_write_int32 (&inner, 20001);
194 2 : tl_write_uint32(&inner, TL_peerUser);
195 2 : tl_write_int64 (&inner, 1LL);
196 2 : tl_write_int64 (&inner, 7777001LL); /* via_bot_id */
197 2 : tl_write_int32 (&inner, 1700001000); /* date */
198 2 : tl_write_string(&inner, "<gif>");
199 :
200 2 : TlWriter w; tl_writer_init(&w);
201 2 : wrap_messages_messages(&w, inner.data, inner.len);
202 2 : mt_server_reply_result(ctx, w.data, w.len);
203 2 : tl_writer_free(&w);
204 2 : tl_writer_free(&inner);
205 2 : }
206 :
207 : /* Test 5 — via_business_bot_id (flags2 bit 0). */
208 2 : static void on_history_via_business_bot(MtRpcContext *ctx) {
209 2 : TlWriter inner; tl_writer_init(&inner);
210 2 : tl_write_uint32(&inner, TL_message);
211 2 : tl_write_uint32(&inner, 0); /* flags */
212 2 : tl_write_uint32(&inner, MSG2_FLAG_VIA_BIZ_BOT); /* flags2 */
213 2 : tl_write_int32 (&inner, 20101);
214 2 : tl_write_uint32(&inner, TL_peerUser);
215 2 : tl_write_int64 (&inner, 1LL);
216 2 : tl_write_int64 (&inner, 8888001LL); /* via_business_bot_id */
217 2 : tl_write_int32 (&inner, 1700001100);
218 2 : tl_write_string(&inner, "auto reply");
219 :
220 2 : TlWriter w; tl_writer_init(&w);
221 2 : wrap_messages_messages(&w, inner.data, inner.len);
222 2 : mt_server_reply_result(ctx, w.data, w.len);
223 2 : tl_writer_free(&w);
224 2 : tl_writer_free(&inner);
225 2 : }
226 :
227 : /* Test 6 — saved_peer_id (flags.28). */
228 2 : static void on_history_saved_peer(MtRpcContext *ctx) {
229 2 : TlWriter inner; tl_writer_init(&inner);
230 2 : tl_write_uint32(&inner, TL_message);
231 2 : tl_write_uint32(&inner, MSG_FLAG_SAVED_PEER);
232 2 : tl_write_uint32(&inner, 0);
233 2 : tl_write_int32 (&inner, 30001);
234 2 : tl_write_uint32(&inner, TL_peerUser);
235 2 : tl_write_int64 (&inner, 1LL);
236 2 : tl_write_uint32(&inner, TL_peerUser); /* saved_peer_id */
237 2 : tl_write_int64 (&inner, 42LL);
238 2 : tl_write_int32 (&inner, 1700001200);
239 2 : tl_write_string(&inner, "topic-A msg");
240 :
241 2 : TlWriter w; tl_writer_init(&w);
242 2 : wrap_messages_messages(&w, inner.data, inner.len);
243 2 : mt_server_reply_result(ctx, w.data, w.len);
244 2 : tl_writer_free(&w);
245 2 : tl_writer_free(&inner);
246 2 : }
247 :
248 : /* Test 7 — reply_to AND via_bot on the same message. */
249 2 : static void on_history_reply_and_via_bot(MtRpcContext *ctx) {
250 2 : TlWriter inner; tl_writer_init(&inner);
251 2 : tl_write_uint32(&inner, TL_message);
252 2 : tl_write_uint32(&inner, MSG_FLAG_REPLY_TO | MSG_FLAG_VIA_BOT);
253 2 : tl_write_uint32(&inner, 0);
254 2 : tl_write_int32 (&inner, 40001);
255 2 : tl_write_uint32(&inner, TL_peerUser);
256 2 : tl_write_int64 (&inner, 1LL);
257 : /* via_bot_id comes BEFORE reply_to in schema order. */
258 2 : tl_write_int64 (&inner, 9990001LL);
259 2 : tl_write_uint32(&inner, CRC_messageReplyHeader);
260 2 : tl_write_uint32(&inner, REPLY_HAS_MSG_ID);
261 2 : tl_write_int32 (&inner, 39000);
262 2 : tl_write_int32 (&inner, 1700001300);
263 2 : tl_write_string(&inner, "combo reply");
264 :
265 2 : TlWriter w; tl_writer_init(&w);
266 2 : wrap_messages_messages(&w, inner.data, inner.len);
267 2 : mt_server_reply_result(ctx, w.data, w.len);
268 2 : tl_writer_free(&w);
269 2 : tl_writer_free(&inner);
270 2 : }
271 :
272 : /* Test 8 — via_bot_id set but id not resolvable (no users vector entry).
273 : * Parser must still land the line with text intact, not drop it. */
274 2 : static void on_history_via_bot_unresolvable(MtRpcContext *ctx) {
275 2 : TlWriter inner; tl_writer_init(&inner);
276 2 : tl_write_uint32(&inner, TL_message);
277 2 : tl_write_uint32(&inner, MSG_FLAG_VIA_BOT);
278 2 : tl_write_uint32(&inner, 0);
279 2 : tl_write_int32 (&inner, 50001);
280 2 : tl_write_uint32(&inner, TL_peerUser);
281 2 : tl_write_int64 (&inner, 1LL);
282 2 : tl_write_int64 (&inner, 424242LL); /* unknown bot id */
283 2 : tl_write_int32 (&inner, 1700001400);
284 2 : tl_write_string(&inner, "orphan bot msg");
285 :
286 2 : TlWriter w; tl_writer_init(&w);
287 2 : wrap_messages_messages(&w, inner.data, inner.len);
288 2 : mt_server_reply_result(ctx, w.data, w.len);
289 2 : tl_writer_free(&w);
290 2 : tl_writer_free(&inner);
291 2 : }
292 :
293 : /* ================================================================ */
294 : /* Tests */
295 : /* ================================================================ */
296 :
297 2 : static void test_forwarded_from_channel_labelled(void) {
298 2 : with_tmp_home("fwd-chan");
299 2 : mt_server_init(); mt_server_reset();
300 2 : MtProtoSession s; load_session(&s);
301 2 : mt_server_expect(CRC_messages_getHistory, on_history_fwd_from_channel, NULL);
302 :
303 2 : ApiConfig cfg; init_cfg(&cfg);
304 2 : Transport t; connect_mock(&t);
305 :
306 : HistoryEntry rows[4];
307 2 : int n = 0;
308 2 : ASSERT(domain_get_history_self(&cfg, &s, &t, 0, 4, rows, &n) == 0,
309 : "get_history with fwd_from channel succeeds");
310 2 : ASSERT(n == 1, "one fwd message parsed");
311 2 : ASSERT(rows[0].id == 10347, "id preserved past fwd header");
312 2 : ASSERT(rows[0].date == 1700000200, "outer date preserved");
313 2 : ASSERT(strcmp(rows[0].text, "fresh news") == 0,
314 : "text after fwd_from skipped correctly");
315 2 : ASSERT(rows[0].complex == 0, "not flagged complex — fwd header fully skipped");
316 :
317 2 : transport_close(&t);
318 2 : mt_server_reset();
319 : }
320 :
321 2 : static void test_forwarded_from_hidden_user_labelled(void) {
322 2 : with_tmp_home("fwd-hidden");
323 2 : mt_server_init(); mt_server_reset();
324 2 : MtProtoSession s; load_session(&s);
325 2 : mt_server_expect(CRC_messages_getHistory, on_history_fwd_from_hidden, NULL);
326 :
327 2 : ApiConfig cfg; init_cfg(&cfg);
328 2 : Transport t; connect_mock(&t);
329 :
330 : HistoryEntry rows[4];
331 2 : int n = 0;
332 2 : ASSERT(domain_get_history_self(&cfg, &s, &t, 0, 4, rows, &n) == 0,
333 : "get_history with fwd from_name succeeds");
334 2 : ASSERT(n == 1, "hidden-sender fwd preserved");
335 2 : ASSERT(rows[0].id == 10348, "id preserved");
336 2 : ASSERT(strcmp(rows[0].text, "hidden fwd body") == 0,
337 : "text after from_name string skipped correctly");
338 2 : ASSERT(rows[0].complex == 0, "from_name path does not bail");
339 :
340 2 : transport_close(&t);
341 2 : mt_server_reset();
342 : }
343 :
344 2 : static void test_reply_to_labelled_with_msg_id(void) {
345 2 : with_tmp_home("reply-to");
346 2 : mt_server_init(); mt_server_reset();
347 2 : MtProtoSession s; load_session(&s);
348 2 : mt_server_expect(CRC_messages_getHistory, on_history_reply_to, NULL);
349 :
350 2 : ApiConfig cfg; init_cfg(&cfg);
351 2 : Transport t; connect_mock(&t);
352 :
353 : HistoryEntry rows[4];
354 2 : int n = 0;
355 2 : ASSERT(domain_get_history_self(&cfg, &s, &t, 0, 4, rows, &n) == 0,
356 : "reply_to parse succeeds");
357 2 : ASSERT(n == 1, "reply-to message preserved");
358 2 : ASSERT(rows[0].id == 12346, "id preserved");
359 2 : ASSERT(rows[0].date == 1700000800, "outer date preserved");
360 2 : ASSERT(strcmp(rows[0].text, "yes") == 0,
361 : "text after reply header skipped correctly");
362 2 : ASSERT(rows[0].complex == 0, "reply_to branch does not bail");
363 :
364 2 : transport_close(&t);
365 2 : mt_server_reset();
366 : }
367 :
368 2 : static void test_via_bot_labelled_with_username(void) {
369 2 : with_tmp_home("via-bot");
370 2 : mt_server_init(); mt_server_reset();
371 2 : MtProtoSession s; load_session(&s);
372 2 : mt_server_expect(CRC_messages_getHistory, on_history_via_bot, NULL);
373 :
374 2 : ApiConfig cfg; init_cfg(&cfg);
375 2 : Transport t; connect_mock(&t);
376 :
377 : HistoryEntry rows[4];
378 2 : int n = 0;
379 2 : ASSERT(domain_get_history_self(&cfg, &s, &t, 0, 4, rows, &n) == 0,
380 : "via_bot parse succeeds");
381 2 : ASSERT(n == 1, "via-bot message preserved");
382 2 : ASSERT(rows[0].id == 20001, "id preserved past via_bot int64");
383 2 : ASSERT(strcmp(rows[0].text, "<gif>") == 0,
384 : "text after via_bot_id i64 skipped correctly");
385 2 : ASSERT(rows[0].complex == 0, "via_bot branch does not bail");
386 :
387 2 : transport_close(&t);
388 2 : mt_server_reset();
389 : }
390 :
391 2 : static void test_via_business_bot_labelled(void) {
392 2 : with_tmp_home("via-bizbot");
393 2 : mt_server_init(); mt_server_reset();
394 2 : MtProtoSession s; load_session(&s);
395 2 : mt_server_expect(CRC_messages_getHistory,
396 : on_history_via_business_bot, NULL);
397 :
398 2 : ApiConfig cfg; init_cfg(&cfg);
399 2 : Transport t; connect_mock(&t);
400 :
401 : HistoryEntry rows[4];
402 2 : int n = 0;
403 2 : ASSERT(domain_get_history_self(&cfg, &s, &t, 0, 4, rows, &n) == 0,
404 : "via_business_bot parse succeeds");
405 2 : ASSERT(n == 1, "biz-bot message preserved");
406 2 : ASSERT(rows[0].id == 20101, "id preserved past flags2.0 i64");
407 2 : ASSERT(strcmp(rows[0].text, "auto reply") == 0,
408 : "text after via_business_bot_id skipped correctly");
409 2 : ASSERT(rows[0].complex == 0, "flags2.0 branch does not bail");
410 :
411 2 : transport_close(&t);
412 2 : mt_server_reset();
413 : }
414 :
415 2 : static void test_saved_peer_id_suffix(void) {
416 2 : with_tmp_home("saved-peer");
417 2 : mt_server_init(); mt_server_reset();
418 2 : MtProtoSession s; load_session(&s);
419 2 : mt_server_expect(CRC_messages_getHistory, on_history_saved_peer, NULL);
420 :
421 2 : ApiConfig cfg; init_cfg(&cfg);
422 2 : Transport t; connect_mock(&t);
423 :
424 : HistoryEntry rows[4];
425 2 : int n = 0;
426 2 : ASSERT(domain_get_history_self(&cfg, &s, &t, 0, 4, rows, &n) == 0,
427 : "saved_peer_id parse succeeds");
428 2 : ASSERT(n == 1, "saved-peer message preserved");
429 2 : ASSERT(rows[0].id == 30001, "id preserved past saved_peer_id skipper");
430 2 : ASSERT(strcmp(rows[0].text, "topic-A msg") == 0,
431 : "text after saved_peer_id skipped correctly");
432 2 : ASSERT(rows[0].complex == 0, "flags.28 branch does not bail");
433 :
434 2 : transport_close(&t);
435 2 : mt_server_reset();
436 : }
437 :
438 2 : static void test_multiple_flags_all_rendered(void) {
439 2 : with_tmp_home("combo");
440 2 : mt_server_init(); mt_server_reset();
441 2 : MtProtoSession s; load_session(&s);
442 2 : mt_server_expect(CRC_messages_getHistory,
443 : on_history_reply_and_via_bot, NULL);
444 :
445 2 : ApiConfig cfg; init_cfg(&cfg);
446 2 : Transport t; connect_mock(&t);
447 :
448 : HistoryEntry rows[4];
449 2 : int n = 0;
450 2 : ASSERT(domain_get_history_self(&cfg, &s, &t, 0, 4, rows, &n) == 0,
451 : "reply+via_bot combo parse succeeds");
452 2 : ASSERT(n == 1, "combo message preserved");
453 2 : ASSERT(rows[0].id == 40001, "id preserved with two flag branches");
454 2 : ASSERT(strcmp(rows[0].text, "combo reply") == 0,
455 : "text survives after via_bot + reply header sequence");
456 2 : ASSERT(rows[0].complex == 0, "combo does not bail");
457 :
458 2 : transport_close(&t);
459 2 : mt_server_reset();
460 : }
461 :
462 2 : static void test_unresolvable_bot_falls_back_to_raw_id(void) {
463 2 : with_tmp_home("unresolvable-bot");
464 2 : mt_server_init(); mt_server_reset();
465 2 : MtProtoSession s; load_session(&s);
466 2 : mt_server_expect(CRC_messages_getHistory,
467 : on_history_via_bot_unresolvable, NULL);
468 :
469 2 : ApiConfig cfg; init_cfg(&cfg);
470 2 : Transport t; connect_mock(&t);
471 :
472 : HistoryEntry rows[4];
473 2 : int n = 0;
474 2 : ASSERT(domain_get_history_self(&cfg, &s, &t, 0, 4, rows, &n) == 0,
475 : "unresolved bot parse succeeds");
476 2 : ASSERT(n == 1, "message not dropped when bot id cannot be resolved");
477 2 : ASSERT(rows[0].id == 50001, "id preserved for orphan bot line");
478 2 : ASSERT(strcmp(rows[0].text, "orphan bot msg") == 0,
479 : "text preserved — graceful degradation, not drop");
480 2 : ASSERT(rows[0].complex == 0, "orphan-bot path does not flag complex");
481 :
482 2 : transport_close(&t);
483 2 : mt_server_reset();
484 : }
485 :
486 2 : void run_history_rich_metadata_tests(void) {
487 2 : RUN_TEST(test_forwarded_from_channel_labelled);
488 2 : RUN_TEST(test_forwarded_from_hidden_user_labelled);
489 2 : RUN_TEST(test_reply_to_labelled_with_msg_id);
490 2 : RUN_TEST(test_via_bot_labelled_with_username);
491 2 : RUN_TEST(test_via_business_bot_labelled);
492 2 : RUN_TEST(test_saved_peer_id_suffix);
493 2 : RUN_TEST(test_multiple_flags_all_rendered);
494 2 : RUN_TEST(test_unresolvable_bot_falls_back_to_raw_id);
495 2 : }
|