Line data Source code
1 : /**
2 : * @file test_service_messages.c
3 : * @brief TEST-80 — functional coverage for messageService action variants.
4 : *
5 : * US-29 identifies seventeen `messageAction*` constructors whose wire
6 : * shape was previously dropped by history.c (messageService → complex=1).
7 : * The domain now renders each into a human-readable string so that group
8 : * histories carry their first-class events (join/leave/pin/video-chat
9 : * lifecycle/...).
10 : *
11 : * These scenarios craft a real messageService-shaped messages.Messages
12 : * payload for each action, drive domain_get_history() through the
13 : * in-process mock server, and assert the surfaced string fragment
14 : * matches the US-29 table. Unknown action CRCs fall through to a
15 : * "[service action 0x%08x]" label that keeps forward compatibility.
16 : *
17 : * A final scenario plumbs a messageService through updates.difference
18 : * (the `watch` code path) to prove service events reach the poll loop
19 : * and are no longer filtered out as complex.
20 : */
21 :
22 : #include "test_helpers.h"
23 :
24 : #include "mock_socket.h"
25 : #include "mock_tel_server.h"
26 :
27 : #include "api_call.h"
28 : #include "mtproto_session.h"
29 : #include "transport.h"
30 : #include "app/session_store.h"
31 : #include "tl_registry.h"
32 : #include "tl_serial.h"
33 :
34 : #include "domain/read/history.h"
35 : #include "domain/read/updates.h"
36 :
37 : #include <stdio.h>
38 : #include <stdlib.h>
39 : #include <string.h>
40 : #include <unistd.h>
41 :
42 : /* ---- CRCs not re-exposed from public headers ---- */
43 : #define CRC_messages_getHistory 0x4423e6c5U
44 : #define CRC_updates_getDifference 0x19c2f763U
45 : #define CRC_messageReplyHeader 0xafbc09dbU
46 :
47 : /* ---- MessageAction CRCs (duplicated locally so the test keeps working
48 : * even if history.c renames them). */
49 : #define AC_Empty 0xb6aef7b0U
50 : #define AC_ChatCreate 0xbd47cbadU
51 : #define AC_ChatEditTitle 0xb5a1ce5aU
52 : #define AC_ChatEditPhoto 0x7fcb13a8U
53 : #define AC_ChatDeletePhoto 0x95e3fbefU
54 : #define AC_ChatAddUser 0x15cefd00U
55 : #define AC_ChatDeleteUser 0xa43f30ccU
56 : #define AC_ChatJoinedByLink 0x031224c3U
57 : #define AC_ChannelCreate 0x95d2ac92U
58 : #define AC_ChatMigrateTo 0xe1037f92U
59 : #define AC_ChannelMigrateFrom 0xea3948e9U
60 : #define AC_PinMessage 0x94bd38edU
61 : #define AC_HistoryClear 0x9fbab604U
62 : #define AC_PhoneCall 0x80e11a7fU
63 : #define AC_ScreenshotTaken 0x4792929bU
64 : #define AC_CustomAction 0xfae69f56U
65 : #define AC_GroupCall 0x7a0d7f42U
66 : #define AC_GroupCallScheduled 0xb3a07661U
67 : #define AC_InviteToGroupCall 0x502f92f7U
68 :
69 : /* PhoneCall discard reasons. */
70 : #define DR_Missed 0x85e42301U
71 : #define DR_Disconnect 0xe095c1a0U
72 : #define DR_Hangup 0x57adc690U
73 : #define DR_Busy 0xfaf7e8c9U
74 :
75 : /* inputGroupCall#d8aa840f id:long access_hash:long */
76 : #define CRC_inputGroupCall 0xd8aa840fU
77 :
78 : /* Message flag bits used for messageService here. */
79 : #define MS_FLAG_REPLY_TO (1u << 3)
80 :
81 : /* Reply-header flag bits. */
82 : #define REPLY_HAS_MSG_ID (1u << 4)
83 :
84 : /* ================================================================ */
85 : /* Boilerplate (mirrors test_history_rich_metadata.c) */
86 : /* ================================================================ */
87 :
88 44 : static void with_tmp_home(const char *tag) {
89 : char tmp[256];
90 44 : snprintf(tmp, sizeof(tmp), "/tmp/tg-cli-ft-svc-%s", tag);
91 : char bin[512];
92 44 : snprintf(bin, sizeof(bin), "%s/.config/tg-cli/session.bin", tmp);
93 44 : (void)unlink(bin);
94 44 : setenv("HOME", tmp, 1);
95 44 : }
96 :
97 44 : static void connect_mock(Transport *t) {
98 44 : transport_init(t);
99 44 : ASSERT(transport_connect(t, "127.0.0.1", 443) == 0, "connect");
100 : }
101 :
102 44 : static void init_cfg(ApiConfig *cfg) {
103 44 : api_config_init(cfg);
104 44 : cfg->api_id = 12345;
105 44 : cfg->api_hash = "deadbeefcafebabef00dbaadfeedc0de";
106 44 : }
107 :
108 44 : static void load_session(MtProtoSession *s) {
109 44 : ASSERT(mt_server_seed_session(2, NULL, NULL, NULL) == 0, "seed");
110 44 : mtproto_session_init(s);
111 44 : int dc = 0;
112 44 : ASSERT(session_store_load(s, &dc) == 0, "load session");
113 : }
114 :
115 : /* Envelope: messages.messages { messages: Vector<Message>{1}, chats, users }
116 : * with the caller providing the inner messageService bytes (starting at
117 : * TL_messageService). */
118 42 : static void wrap_messages_messages(TlWriter *w, const uint8_t *msg_bytes,
119 : size_t msg_len) {
120 42 : tl_write_uint32(w, TL_messages_messages);
121 42 : tl_write_uint32(w, TL_vector);
122 42 : tl_write_uint32(w, 1);
123 42 : tl_write_raw(w, msg_bytes, msg_len);
124 42 : tl_write_uint32(w, TL_vector); tl_write_uint32(w, 0); /* chats */
125 42 : tl_write_uint32(w, TL_vector); tl_write_uint32(w, 0); /* users */
126 42 : }
127 :
128 : /* Build the messageService envelope preamble:
129 : * TL_messageService | flags | id(i32) | peer_id(peerUser 1)
130 : * The caller then writes: [reply_to if flags.3] date action.
131 : * messageService on the current schema does NOT carry a flags2 field. */
132 44 : static void write_service_preamble(TlWriter *w, uint32_t flags,
133 : int32_t id) {
134 44 : tl_write_uint32(w, TL_messageService);
135 44 : tl_write_uint32(w, flags);
136 44 : tl_write_int32 (w, id);
137 44 : tl_write_uint32(w, TL_peerUser);
138 44 : tl_write_int64 (w, 1LL);
139 44 : }
140 :
141 : /* Fetch one messageService row through the production domain. */
142 42 : static void fetch_one(HistoryEntry *out_row) {
143 42 : ApiConfig cfg; init_cfg(&cfg);
144 42 : Transport t; connect_mock(&t);
145 42 : MtProtoSession s; load_session(&s);
146 : HistoryEntry rows[2];
147 42 : int n = 0;
148 42 : ASSERT(domain_get_history_self(&cfg, &s, &t, 0, 2, rows, &n) == 0,
149 : "get_history succeeds for service row");
150 42 : ASSERT(n == 1, "exactly one service message parsed");
151 42 : *out_row = rows[0];
152 42 : transport_close(&t);
153 : }
154 :
155 : /* ================================================================ */
156 : /* Action-specific responders */
157 : /* ================================================================ */
158 :
159 2 : static void on_chat_create(MtRpcContext *ctx) {
160 2 : TlWriter inner; tl_writer_init(&inner);
161 2 : write_service_preamble(&inner, 0, 1001);
162 2 : tl_write_int32 (&inner, 1700010000); /* date */
163 2 : tl_write_uint32(&inner, AC_ChatCreate);
164 2 : tl_write_string(&inner, "Planning"); /* title */
165 2 : tl_write_uint32(&inner, TL_vector);
166 2 : tl_write_uint32(&inner, 2); /* 2 users */
167 2 : tl_write_int64 (&inner, 100LL);
168 2 : tl_write_int64 (&inner, 200LL);
169 :
170 2 : TlWriter w; tl_writer_init(&w);
171 2 : wrap_messages_messages(&w, inner.data, inner.len);
172 2 : mt_server_reply_result(ctx, w.data, w.len);
173 2 : tl_writer_free(&w);
174 2 : tl_writer_free(&inner);
175 2 : }
176 :
177 2 : static void on_chat_add_user(MtRpcContext *ctx) {
178 2 : TlWriter inner; tl_writer_init(&inner);
179 2 : write_service_preamble(&inner, 0, 1002);
180 2 : tl_write_int32 (&inner, 1700010100);
181 2 : tl_write_uint32(&inner, AC_ChatAddUser);
182 2 : tl_write_uint32(&inner, TL_vector);
183 2 : tl_write_uint32(&inner, 1);
184 2 : tl_write_int64 (&inner, 4242LL); /* added user id */
185 :
186 2 : TlWriter w; tl_writer_init(&w);
187 2 : wrap_messages_messages(&w, inner.data, inner.len);
188 2 : mt_server_reply_result(ctx, w.data, w.len);
189 2 : tl_writer_free(&w);
190 2 : tl_writer_free(&inner);
191 2 : }
192 :
193 2 : static void on_chat_delete_user(MtRpcContext *ctx) {
194 2 : TlWriter inner; tl_writer_init(&inner);
195 2 : write_service_preamble(&inner, 0, 1003);
196 2 : tl_write_int32 (&inner, 1700010200);
197 2 : tl_write_uint32(&inner, AC_ChatDeleteUser);
198 2 : tl_write_int64 (&inner, 4242LL); /* user_id */
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 2 : static void on_chat_joined_by_link(MtRpcContext *ctx) {
208 2 : TlWriter inner; tl_writer_init(&inner);
209 2 : write_service_preamble(&inner, 0, 1004);
210 2 : tl_write_int32 (&inner, 1700010300);
211 2 : tl_write_uint32(&inner, AC_ChatJoinedByLink);
212 2 : tl_write_int64 (&inner, 9999LL); /* inviter_id */
213 :
214 2 : TlWriter w; tl_writer_init(&w);
215 2 : wrap_messages_messages(&w, inner.data, inner.len);
216 2 : mt_server_reply_result(ctx, w.data, w.len);
217 2 : tl_writer_free(&w);
218 2 : tl_writer_free(&inner);
219 2 : }
220 :
221 2 : static void on_chat_edit_title(MtRpcContext *ctx) {
222 2 : TlWriter inner; tl_writer_init(&inner);
223 2 : write_service_preamble(&inner, 0, 1005);
224 2 : tl_write_int32 (&inner, 1700010400);
225 2 : tl_write_uint32(&inner, AC_ChatEditTitle);
226 2 : tl_write_string(&inner, "Shipping");
227 :
228 2 : TlWriter w; tl_writer_init(&w);
229 2 : wrap_messages_messages(&w, inner.data, inner.len);
230 2 : mt_server_reply_result(ctx, w.data, w.len);
231 2 : tl_writer_free(&w);
232 2 : tl_writer_free(&inner);
233 2 : }
234 :
235 : /* messageActionChatEditPhoto carries a photo:Photo body. For our test we
236 : * use the photoEmpty#2331b22d variant (crc + id:long) — just 12 bytes.
237 : * The renderer only cares that the photo skipper advances past it. */
238 2 : static void on_chat_edit_photo(MtRpcContext *ctx) {
239 2 : TlWriter inner; tl_writer_init(&inner);
240 2 : write_service_preamble(&inner, 0, 1006);
241 2 : tl_write_int32 (&inner, 1700010500);
242 2 : tl_write_uint32(&inner, AC_ChatEditPhoto);
243 2 : tl_write_uint32(&inner, 0x2331b22dU); /* photoEmpty */
244 2 : tl_write_int64 (&inner, 55555LL); /* photo_id */
245 :
246 2 : TlWriter w; tl_writer_init(&w);
247 2 : wrap_messages_messages(&w, inner.data, inner.len);
248 2 : mt_server_reply_result(ctx, w.data, w.len);
249 2 : tl_writer_free(&w);
250 2 : tl_writer_free(&inner);
251 2 : }
252 :
253 : /* Pinned target id travels on reply_to (MessageReplyHeader).
254 : * Layout after (flags, id, peer):
255 : * reply_to = messageReplyHeader flags=REPLY_HAS_MSG_ID
256 : * reply_to_msg_id = 12345
257 : * date
258 : * action = messageActionPinMessage
259 : */
260 2 : static void on_pin_message(MtRpcContext *ctx) {
261 2 : TlWriter inner; tl_writer_init(&inner);
262 2 : write_service_preamble(&inner, MS_FLAG_REPLY_TO, 1007);
263 : /* reply_to */
264 2 : tl_write_uint32(&inner, CRC_messageReplyHeader);
265 2 : tl_write_uint32(&inner, REPLY_HAS_MSG_ID);
266 2 : tl_write_int32 (&inner, 12345);
267 : /* date + action */
268 2 : tl_write_int32 (&inner, 1700010600);
269 2 : tl_write_uint32(&inner, AC_PinMessage);
270 :
271 2 : TlWriter w; tl_writer_init(&w);
272 2 : wrap_messages_messages(&w, inner.data, inner.len);
273 2 : mt_server_reply_result(ctx, w.data, w.len);
274 2 : tl_writer_free(&w);
275 2 : tl_writer_free(&inner);
276 2 : }
277 :
278 2 : static void on_history_clear(MtRpcContext *ctx) {
279 2 : TlWriter inner; tl_writer_init(&inner);
280 2 : write_service_preamble(&inner, 0, 1008);
281 2 : tl_write_int32 (&inner, 1700010700);
282 2 : tl_write_uint32(&inner, AC_HistoryClear);
283 :
284 2 : TlWriter w; tl_writer_init(&w);
285 2 : wrap_messages_messages(&w, inner.data, inner.len);
286 2 : mt_server_reply_result(ctx, w.data, w.len);
287 2 : tl_writer_free(&w);
288 2 : tl_writer_free(&inner);
289 2 : }
290 :
291 2 : static void on_channel_create(MtRpcContext *ctx) {
292 2 : TlWriter inner; tl_writer_init(&inner);
293 2 : write_service_preamble(&inner, 0, 1009);
294 2 : tl_write_int32 (&inner, 1700010800);
295 2 : tl_write_uint32(&inner, AC_ChannelCreate);
296 2 : tl_write_string(&inner, "Releases");
297 :
298 2 : TlWriter w; tl_writer_init(&w);
299 2 : wrap_messages_messages(&w, inner.data, inner.len);
300 2 : mt_server_reply_result(ctx, w.data, w.len);
301 2 : tl_writer_free(&w);
302 2 : tl_writer_free(&inner);
303 2 : }
304 :
305 2 : static void on_channel_migrate_from(MtRpcContext *ctx) {
306 2 : TlWriter inner; tl_writer_init(&inner);
307 2 : write_service_preamble(&inner, 0, 1010);
308 2 : tl_write_int32 (&inner, 1700010900);
309 2 : tl_write_uint32(&inner, AC_ChannelMigrateFrom);
310 2 : tl_write_string(&inner, "OldGroup");
311 2 : tl_write_int64 (&inner, 77777LL); /* chat_id */
312 :
313 2 : TlWriter w; tl_writer_init(&w);
314 2 : wrap_messages_messages(&w, inner.data, inner.len);
315 2 : mt_server_reply_result(ctx, w.data, w.len);
316 2 : tl_writer_free(&w);
317 2 : tl_writer_free(&inner);
318 2 : }
319 :
320 2 : static void on_chat_migrate_to(MtRpcContext *ctx) {
321 2 : TlWriter inner; tl_writer_init(&inner);
322 2 : write_service_preamble(&inner, 0, 1011);
323 2 : tl_write_int32 (&inner, 1700011000);
324 2 : tl_write_uint32(&inner, AC_ChatMigrateTo);
325 2 : tl_write_int64 (&inner, 88888LL); /* channel_id */
326 :
327 2 : TlWriter w; tl_writer_init(&w);
328 2 : wrap_messages_messages(&w, inner.data, inner.len);
329 2 : mt_server_reply_result(ctx, w.data, w.len);
330 2 : tl_writer_free(&w);
331 2 : tl_writer_free(&inner);
332 2 : }
333 :
334 : /* messageActionGroupCall flags=0, call=inputGroupCall. */
335 2 : static void on_group_call(MtRpcContext *ctx) {
336 2 : TlWriter inner; tl_writer_init(&inner);
337 2 : write_service_preamble(&inner, 0, 1012);
338 2 : tl_write_int32 (&inner, 1700011100);
339 2 : tl_write_uint32(&inner, AC_GroupCall);
340 2 : tl_write_uint32(&inner, 0); /* flags */
341 2 : tl_write_uint32(&inner, CRC_inputGroupCall);
342 2 : tl_write_int64 (&inner, 111LL);
343 2 : tl_write_int64 (&inner, 222LL);
344 :
345 2 : TlWriter w; tl_writer_init(&w);
346 2 : wrap_messages_messages(&w, inner.data, inner.len);
347 2 : mt_server_reply_result(ctx, w.data, w.len);
348 2 : tl_writer_free(&w);
349 2 : tl_writer_free(&inner);
350 2 : }
351 :
352 2 : static void on_group_call_scheduled(MtRpcContext *ctx) {
353 2 : TlWriter inner; tl_writer_init(&inner);
354 2 : write_service_preamble(&inner, 0, 1013);
355 2 : tl_write_int32 (&inner, 1700011200);
356 2 : tl_write_uint32(&inner, AC_GroupCallScheduled);
357 2 : tl_write_uint32(&inner, CRC_inputGroupCall);
358 2 : tl_write_int64 (&inner, 111LL);
359 2 : tl_write_int64 (&inner, 222LL);
360 2 : tl_write_int32 (&inner, 1700020000); /* schedule_date */
361 :
362 2 : TlWriter w; tl_writer_init(&w);
363 2 : wrap_messages_messages(&w, inner.data, inner.len);
364 2 : mt_server_reply_result(ctx, w.data, w.len);
365 2 : tl_writer_free(&w);
366 2 : tl_writer_free(&inner);
367 2 : }
368 :
369 2 : static void on_invite_to_group_call(MtRpcContext *ctx) {
370 2 : TlWriter inner; tl_writer_init(&inner);
371 2 : write_service_preamble(&inner, 0, 1014);
372 2 : tl_write_int32 (&inner, 1700011300);
373 2 : tl_write_uint32(&inner, AC_InviteToGroupCall);
374 2 : tl_write_uint32(&inner, CRC_inputGroupCall);
375 2 : tl_write_int64 (&inner, 111LL);
376 2 : tl_write_int64 (&inner, 222LL);
377 2 : tl_write_uint32(&inner, TL_vector);
378 2 : tl_write_uint32(&inner, 1);
379 2 : tl_write_int64 (&inner, 3030LL); /* invited user id */
380 :
381 2 : TlWriter w; tl_writer_init(&w);
382 2 : wrap_messages_messages(&w, inner.data, inner.len);
383 2 : mt_server_reply_result(ctx, w.data, w.len);
384 2 : tl_writer_free(&w);
385 2 : tl_writer_free(&inner);
386 2 : }
387 :
388 : /* messageActionPhoneCall flags=0x3 (has reason + duration), video=no,
389 : * call_id=0xDEADBEEF, reason=hangup, duration=42. */
390 2 : static void on_phone_call(MtRpcContext *ctx) {
391 2 : TlWriter inner; tl_writer_init(&inner);
392 2 : write_service_preamble(&inner, 0, 1015);
393 2 : tl_write_int32 (&inner, 1700011400);
394 2 : tl_write_uint32(&inner, AC_PhoneCall);
395 2 : tl_write_uint32(&inner, 0x3); /* flags: reason + duration */
396 2 : tl_write_int64 (&inner, 0xdeadbeefLL); /* call_id */
397 2 : tl_write_uint32(&inner, DR_Hangup);
398 2 : tl_write_int32 (&inner, 42); /* duration */
399 :
400 2 : TlWriter w; tl_writer_init(&w);
401 2 : wrap_messages_messages(&w, inner.data, inner.len);
402 2 : mt_server_reply_result(ctx, w.data, w.len);
403 2 : tl_writer_free(&w);
404 2 : tl_writer_free(&inner);
405 2 : }
406 :
407 2 : static void on_screenshot_taken(MtRpcContext *ctx) {
408 2 : TlWriter inner; tl_writer_init(&inner);
409 2 : write_service_preamble(&inner, 0, 1016);
410 2 : tl_write_int32 (&inner, 1700011500);
411 2 : tl_write_uint32(&inner, AC_ScreenshotTaken);
412 :
413 2 : TlWriter w; tl_writer_init(&w);
414 2 : wrap_messages_messages(&w, inner.data, inner.len);
415 2 : mt_server_reply_result(ctx, w.data, w.len);
416 2 : tl_writer_free(&w);
417 2 : tl_writer_free(&inner);
418 2 : }
419 :
420 2 : static void on_custom_action(MtRpcContext *ctx) {
421 2 : TlWriter inner; tl_writer_init(&inner);
422 2 : write_service_preamble(&inner, 0, 1017);
423 2 : tl_write_int32 (&inner, 1700011600);
424 2 : tl_write_uint32(&inner, AC_CustomAction);
425 2 : tl_write_string(&inner, "custom boxed action");
426 :
427 2 : TlWriter w; tl_writer_init(&w);
428 2 : wrap_messages_messages(&w, inner.data, inner.len);
429 2 : mt_server_reply_result(ctx, w.data, w.len);
430 2 : tl_writer_free(&w);
431 2 : tl_writer_free(&inner);
432 2 : }
433 :
434 : /* Supplementary action responders — keep the coverage of each branch
435 : * of parse_service_action honest without expanding the US-29 table. */
436 :
437 2 : static void on_action_empty(MtRpcContext *ctx) {
438 2 : TlWriter inner; tl_writer_init(&inner);
439 2 : write_service_preamble(&inner, 0, 1101);
440 2 : tl_write_int32 (&inner, 1700020000);
441 2 : tl_write_uint32(&inner, AC_Empty);
442 :
443 2 : TlWriter w; tl_writer_init(&w);
444 2 : wrap_messages_messages(&w, inner.data, inner.len);
445 2 : mt_server_reply_result(ctx, w.data, w.len);
446 2 : tl_writer_free(&w);
447 2 : tl_writer_free(&inner);
448 2 : }
449 :
450 2 : static void on_chat_delete_photo(MtRpcContext *ctx) {
451 2 : TlWriter inner; tl_writer_init(&inner);
452 2 : write_service_preamble(&inner, 0, 1102);
453 2 : tl_write_int32 (&inner, 1700020100);
454 2 : tl_write_uint32(&inner, AC_ChatDeletePhoto);
455 :
456 2 : TlWriter w; tl_writer_init(&w);
457 2 : wrap_messages_messages(&w, inner.data, inner.len);
458 2 : mt_server_reply_result(ctx, w.data, w.len);
459 2 : tl_writer_free(&w);
460 2 : tl_writer_free(&inner);
461 2 : }
462 :
463 : /* PhoneCall with reason=missed + no duration flag — exercises alternate
464 : * branches in the reason switch and verifies duration defaults to 0s. */
465 2 : static void on_phone_call_missed(MtRpcContext *ctx) {
466 2 : TlWriter inner; tl_writer_init(&inner);
467 2 : write_service_preamble(&inner, 0, 1103);
468 2 : tl_write_int32 (&inner, 1700020200);
469 2 : tl_write_uint32(&inner, AC_PhoneCall);
470 2 : tl_write_uint32(&inner, 0x1); /* flags: reason only */
471 2 : tl_write_int64 (&inner, 42LL); /* call_id */
472 2 : tl_write_uint32(&inner, DR_Missed);
473 :
474 2 : TlWriter w; tl_writer_init(&w);
475 2 : wrap_messages_messages(&w, inner.data, inner.len);
476 2 : mt_server_reply_result(ctx, w.data, w.len);
477 2 : tl_writer_free(&w);
478 2 : tl_writer_free(&inner);
479 2 : }
480 :
481 : /* Fake action CRC — must be labelled safely with its hex. */
482 : #define AC_Fake 0xdeadcafeU
483 2 : static void on_unknown_action(MtRpcContext *ctx) {
484 2 : TlWriter inner; tl_writer_init(&inner);
485 2 : write_service_preamble(&inner, 0, 1018);
486 2 : tl_write_int32 (&inner, 1700011700);
487 2 : tl_write_uint32(&inner, AC_Fake);
488 :
489 2 : TlWriter w; tl_writer_init(&w);
490 2 : wrap_messages_messages(&w, inner.data, inner.len);
491 2 : mt_server_reply_result(ctx, w.data, w.len);
492 2 : tl_writer_free(&w);
493 2 : tl_writer_free(&inner);
494 2 : }
495 :
496 : /* updates.difference payload carrying one messageActionPinMessage to
497 : * prove that the watch poll loop surfaces service events (acceptance
498 : * criterion 3). */
499 2 : static void on_updates_diff_with_service(MtRpcContext *ctx) {
500 2 : TlWriter w; tl_writer_init(&w);
501 2 : tl_write_uint32(&w, TL_updates_difference);
502 :
503 : /* new_messages: Vector<Message>{1} — one messageService. */
504 2 : tl_write_uint32(&w, TL_vector);
505 2 : tl_write_uint32(&w, 1);
506 2 : write_service_preamble(&w, MS_FLAG_REPLY_TO, 2001);
507 2 : tl_write_uint32(&w, CRC_messageReplyHeader);
508 2 : tl_write_uint32(&w, REPLY_HAS_MSG_ID);
509 2 : tl_write_int32 (&w, 777); /* pinned id */
510 2 : tl_write_int32 (&w, 1700012000); /* date */
511 2 : tl_write_uint32(&w, AC_PinMessage);
512 :
513 : /* Remaining difference fields — all empty. */
514 2 : tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0); /* new_encrypted */
515 2 : tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0); /* other_updates */
516 2 : tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0); /* chats */
517 2 : tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0); /* users */
518 : /* state */
519 2 : tl_write_uint32(&w, TL_updates_state);
520 2 : tl_write_int32 (&w, 200); /* pts */
521 2 : tl_write_int32 (&w, 10); /* qts */
522 2 : tl_write_int32 (&w, 1700012000); /* date */
523 2 : tl_write_int32 (&w, 3); /* seq */
524 2 : tl_write_int32 (&w, 0); /* unread */
525 :
526 2 : mt_server_reply_result(ctx, w.data, w.len);
527 2 : tl_writer_free(&w);
528 2 : }
529 :
530 : /* ================================================================ */
531 : /* Tests — one per US-29 row */
532 : /* ================================================================ */
533 :
534 2 : static void test_chat_create(void) {
535 2 : with_tmp_home("chat-create");
536 2 : mt_server_init(); mt_server_reset();
537 2 : mt_server_expect(CRC_messages_getHistory, on_chat_create, NULL);
538 :
539 : HistoryEntry row;
540 2 : fetch_one(&row);
541 2 : ASSERT(row.id == 1001, "id preserved");
542 2 : ASSERT(row.date == 1700010000, "date preserved");
543 2 : ASSERT(row.is_service == 1, "row flagged as service");
544 2 : ASSERT(row.complex == 0, "service row not flagged complex");
545 2 : ASSERT(strstr(row.text, "created group 'Planning'") != NULL,
546 : "renderer surfaces group title");
547 2 : mt_server_reset();
548 : }
549 :
550 2 : static void test_chat_add_user(void) {
551 2 : with_tmp_home("chat-add-user");
552 2 : mt_server_init(); mt_server_reset();
553 2 : mt_server_expect(CRC_messages_getHistory, on_chat_add_user, NULL);
554 :
555 : HistoryEntry row;
556 2 : fetch_one(&row);
557 2 : ASSERT(row.id == 1002, "id preserved");
558 2 : ASSERT(row.is_service == 1, "service flag");
559 2 : ASSERT(strstr(row.text, "added @4242") != NULL,
560 : "renderer surfaces added user id");
561 2 : mt_server_reset();
562 : }
563 :
564 2 : static void test_chat_delete_user(void) {
565 2 : with_tmp_home("chat-del-user");
566 2 : mt_server_init(); mt_server_reset();
567 2 : mt_server_expect(CRC_messages_getHistory, on_chat_delete_user, NULL);
568 :
569 : HistoryEntry row;
570 2 : fetch_one(&row);
571 2 : ASSERT(row.id == 1003, "id preserved");
572 2 : ASSERT(strstr(row.text, "removed @4242") != NULL,
573 : "renderer surfaces removed user id");
574 2 : mt_server_reset();
575 : }
576 :
577 2 : static void test_chat_joined_by_link(void) {
578 2 : with_tmp_home("chat-joined-link");
579 2 : mt_server_init(); mt_server_reset();
580 2 : mt_server_expect(CRC_messages_getHistory, on_chat_joined_by_link, NULL);
581 :
582 : HistoryEntry row;
583 2 : fetch_one(&row);
584 2 : ASSERT(row.id == 1004, "id preserved");
585 2 : ASSERT(strstr(row.text, "joined via invite link") != NULL,
586 : "renderer surfaces invite-link join");
587 2 : mt_server_reset();
588 : }
589 :
590 2 : static void test_chat_edit_title(void) {
591 2 : with_tmp_home("chat-edit-title");
592 2 : mt_server_init(); mt_server_reset();
593 2 : mt_server_expect(CRC_messages_getHistory, on_chat_edit_title, NULL);
594 :
595 : HistoryEntry row;
596 2 : fetch_one(&row);
597 2 : ASSERT(row.id == 1005, "id preserved");
598 2 : ASSERT(strstr(row.text, "changed title to 'Shipping'") != NULL,
599 : "renderer surfaces new title");
600 2 : mt_server_reset();
601 : }
602 :
603 2 : static void test_chat_edit_photo(void) {
604 2 : with_tmp_home("chat-edit-photo");
605 2 : mt_server_init(); mt_server_reset();
606 2 : mt_server_expect(CRC_messages_getHistory, on_chat_edit_photo, NULL);
607 :
608 : HistoryEntry row;
609 2 : fetch_one(&row);
610 2 : ASSERT(row.id == 1006, "id preserved");
611 2 : ASSERT(strstr(row.text, "changed group photo") != NULL,
612 : "renderer labels photo edit");
613 2 : mt_server_reset();
614 : }
615 :
616 2 : static void test_pin_message(void) {
617 2 : with_tmp_home("pin-msg");
618 2 : mt_server_init(); mt_server_reset();
619 2 : mt_server_expect(CRC_messages_getHistory, on_pin_message, NULL);
620 :
621 : HistoryEntry row;
622 2 : fetch_one(&row);
623 2 : ASSERT(row.id == 1007, "id preserved");
624 2 : ASSERT(strstr(row.text, "pinned message 12345") != NULL,
625 : "renderer surfaces pinned target id from reply_to");
626 2 : mt_server_reset();
627 : }
628 :
629 2 : static void test_history_clear(void) {
630 2 : with_tmp_home("hist-clear");
631 2 : mt_server_init(); mt_server_reset();
632 2 : mt_server_expect(CRC_messages_getHistory, on_history_clear, NULL);
633 :
634 : HistoryEntry row;
635 2 : fetch_one(&row);
636 2 : ASSERT(row.id == 1008, "id preserved");
637 2 : ASSERT(strstr(row.text, "history cleared") != NULL,
638 : "renderer surfaces history-clear");
639 2 : mt_server_reset();
640 : }
641 :
642 2 : static void test_channel_create(void) {
643 2 : with_tmp_home("chan-create");
644 2 : mt_server_init(); mt_server_reset();
645 2 : mt_server_expect(CRC_messages_getHistory, on_channel_create, NULL);
646 :
647 : HistoryEntry row;
648 2 : fetch_one(&row);
649 2 : ASSERT(row.id == 1009, "id preserved");
650 2 : ASSERT(strstr(row.text, "created channel 'Releases'") != NULL,
651 : "renderer surfaces channel title");
652 2 : mt_server_reset();
653 : }
654 :
655 2 : static void test_channel_migrate_from(void) {
656 2 : with_tmp_home("chan-migfrom");
657 2 : mt_server_init(); mt_server_reset();
658 2 : mt_server_expect(CRC_messages_getHistory, on_channel_migrate_from, NULL);
659 :
660 : HistoryEntry row;
661 2 : fetch_one(&row);
662 2 : ASSERT(row.id == 1010, "id preserved");
663 2 : ASSERT(strstr(row.text, "migrated from group") != NULL,
664 : "renderer labels channel-migrate-from");
665 2 : ASSERT(strstr(row.text, "77777") != NULL, "chat_id surfaced");
666 2 : mt_server_reset();
667 : }
668 :
669 2 : static void test_chat_migrate_to(void) {
670 2 : with_tmp_home("chat-migto");
671 2 : mt_server_init(); mt_server_reset();
672 2 : mt_server_expect(CRC_messages_getHistory, on_chat_migrate_to, NULL);
673 :
674 : HistoryEntry row;
675 2 : fetch_one(&row);
676 2 : ASSERT(row.id == 1011, "id preserved");
677 2 : ASSERT(strstr(row.text, "migrated to channel 88888") != NULL,
678 : "renderer surfaces channel_id");
679 2 : mt_server_reset();
680 : }
681 :
682 2 : static void test_group_call(void) {
683 2 : with_tmp_home("group-call");
684 2 : mt_server_init(); mt_server_reset();
685 2 : mt_server_expect(CRC_messages_getHistory, on_group_call, NULL);
686 :
687 : HistoryEntry row;
688 2 : fetch_one(&row);
689 2 : ASSERT(row.id == 1012, "id preserved");
690 2 : ASSERT(strstr(row.text, "started video chat") != NULL,
691 : "renderer labels group call start");
692 2 : mt_server_reset();
693 : }
694 :
695 2 : static void test_group_call_scheduled(void) {
696 2 : with_tmp_home("group-call-sched");
697 2 : mt_server_init(); mt_server_reset();
698 2 : mt_server_expect(CRC_messages_getHistory, on_group_call_scheduled, NULL);
699 :
700 : HistoryEntry row;
701 2 : fetch_one(&row);
702 2 : ASSERT(row.id == 1013, "id preserved");
703 2 : ASSERT(strstr(row.text, "scheduled video chat for") != NULL,
704 : "renderer labels scheduled call");
705 2 : ASSERT(strstr(row.text, "1700020000") != NULL, "schedule_date surfaced");
706 2 : mt_server_reset();
707 : }
708 :
709 2 : static void test_invite_to_group_call(void) {
710 2 : with_tmp_home("invite-call");
711 2 : mt_server_init(); mt_server_reset();
712 2 : mt_server_expect(CRC_messages_getHistory, on_invite_to_group_call, NULL);
713 :
714 : HistoryEntry row;
715 2 : fetch_one(&row);
716 2 : ASSERT(row.id == 1014, "id preserved");
717 2 : ASSERT(strstr(row.text, "invited to video chat") != NULL,
718 : "renderer labels video-chat invite");
719 2 : mt_server_reset();
720 : }
721 :
722 2 : static void test_phone_call(void) {
723 2 : with_tmp_home("phone-call");
724 2 : mt_server_init(); mt_server_reset();
725 2 : mt_server_expect(CRC_messages_getHistory, on_phone_call, NULL);
726 :
727 : HistoryEntry row;
728 2 : fetch_one(&row);
729 2 : ASSERT(row.id == 1015, "id preserved");
730 2 : ASSERT(strstr(row.text, "called") != NULL,
731 : "renderer opens with 'called'");
732 2 : ASSERT(strstr(row.text, "42s") != NULL, "duration present");
733 2 : ASSERT(strstr(row.text, "hangup") != NULL, "reason present");
734 2 : mt_server_reset();
735 : }
736 :
737 2 : static void test_screenshot_taken(void) {
738 2 : with_tmp_home("screenshot");
739 2 : mt_server_init(); mt_server_reset();
740 2 : mt_server_expect(CRC_messages_getHistory, on_screenshot_taken, NULL);
741 :
742 : HistoryEntry row;
743 2 : fetch_one(&row);
744 2 : ASSERT(row.id == 1016, "id preserved");
745 2 : ASSERT(strstr(row.text, "took screenshot") != NULL,
746 : "renderer labels screenshot event");
747 2 : mt_server_reset();
748 : }
749 :
750 2 : static void test_custom_action(void) {
751 2 : with_tmp_home("custom-action");
752 2 : mt_server_init(); mt_server_reset();
753 2 : mt_server_expect(CRC_messages_getHistory, on_custom_action, NULL);
754 :
755 : HistoryEntry row;
756 2 : fetch_one(&row);
757 2 : ASSERT(row.id == 1017, "id preserved");
758 2 : ASSERT(strcmp(row.text, "custom boxed action") == 0,
759 : "custom action message passed through verbatim");
760 2 : mt_server_reset();
761 : }
762 :
763 2 : static void test_unknown_action_labelled(void) {
764 2 : with_tmp_home("unknown-action");
765 2 : mt_server_init(); mt_server_reset();
766 2 : mt_server_expect(CRC_messages_getHistory, on_unknown_action, NULL);
767 :
768 : HistoryEntry row;
769 2 : fetch_one(&row);
770 2 : ASSERT(row.id == 1018, "id preserved");
771 2 : ASSERT(row.is_service == 1, "service flag set");
772 2 : ASSERT(strstr(row.text, "[service action 0x") != NULL,
773 : "unknown action carries hex-labelled placeholder");
774 2 : ASSERT(strstr(row.text, "deadcafe") != NULL || strstr(row.text, "DEADCAFE") != NULL,
775 : "placeholder includes the unknown CRC");
776 2 : mt_server_reset();
777 : }
778 :
779 : /* Supplementary tests — exercise remaining parse_service_action branches
780 : * so the service block reaches >90% line coverage. Not counted against
781 : * the 19-row US-29 table. */
782 :
783 2 : static void test_action_empty_renders_blank(void) {
784 2 : with_tmp_home("act-empty");
785 2 : mt_server_init(); mt_server_reset();
786 2 : mt_server_expect(CRC_messages_getHistory, on_action_empty, NULL);
787 :
788 : HistoryEntry row;
789 2 : fetch_one(&row);
790 2 : ASSERT(row.id == 1101, "id preserved for empty action");
791 2 : ASSERT(row.is_service == 1, "service flag set for actionEmpty");
792 2 : ASSERT(row.text[0] == '\0',
793 : "actionEmpty rendered as an empty string (drop-in stub)");
794 2 : mt_server_reset();
795 : }
796 :
797 2 : static void test_chat_delete_photo(void) {
798 2 : with_tmp_home("chat-del-photo");
799 2 : mt_server_init(); mt_server_reset();
800 2 : mt_server_expect(CRC_messages_getHistory, on_chat_delete_photo, NULL);
801 :
802 : HistoryEntry row;
803 2 : fetch_one(&row);
804 2 : ASSERT(row.id == 1102, "id preserved");
805 2 : ASSERT(strstr(row.text, "removed group photo") != NULL,
806 : "renderer labels chat-delete-photo");
807 2 : mt_server_reset();
808 : }
809 :
810 2 : static void test_phone_call_missed_zero_duration(void) {
811 2 : with_tmp_home("phone-missed");
812 2 : mt_server_init(); mt_server_reset();
813 2 : mt_server_expect(CRC_messages_getHistory, on_phone_call_missed, NULL);
814 :
815 : HistoryEntry row;
816 2 : fetch_one(&row);
817 2 : ASSERT(row.id == 1103, "id preserved");
818 2 : ASSERT(strstr(row.text, "missed") != NULL,
819 : "missed-call reason surfaces");
820 2 : ASSERT(strstr(row.text, "0s") != NULL,
821 : "duration defaults to 0s when flag.1 is clear");
822 2 : mt_server_reset();
823 : }
824 :
825 : /* Plumb a messageService through updates.difference — acceptance criterion 3. */
826 2 : static void test_service_shows_in_watch(void) {
827 2 : with_tmp_home("watch-service");
828 2 : mt_server_init(); mt_server_reset();
829 2 : MtProtoSession s; load_session(&s);
830 2 : mt_server_expect(CRC_updates_getDifference,
831 : on_updates_diff_with_service, NULL);
832 :
833 2 : ApiConfig cfg; init_cfg(&cfg);
834 2 : Transport t; connect_mock(&t);
835 :
836 2 : UpdatesState prev = { .pts = 100, .qts = 0, .date = 0, .seq = 0 };
837 2 : UpdatesDifference diff = {0};
838 2 : ASSERT(domain_updates_difference(&cfg, &s, &t, &prev, &diff) == 0,
839 : "updates.difference parse succeeds with service message");
840 2 : ASSERT(diff.new_messages_count == 1,
841 : "service message surfaced — not filtered as complex");
842 2 : const HistoryEntry *m = &diff.new_messages[0];
843 2 : ASSERT(m->id == 2001, "service msg id preserved across watch path");
844 2 : ASSERT(m->is_service == 1, "watch entry flagged as service");
845 2 : ASSERT(strstr(m->text, "pinned message 777") != NULL,
846 : "watch surfaces the rendered action string");
847 :
848 2 : transport_close(&t);
849 2 : mt_server_reset();
850 : }
851 :
852 2 : void run_service_messages_tests(void) {
853 2 : RUN_TEST(test_chat_create);
854 2 : RUN_TEST(test_chat_add_user);
855 2 : RUN_TEST(test_chat_delete_user);
856 2 : RUN_TEST(test_chat_joined_by_link);
857 2 : RUN_TEST(test_chat_edit_title);
858 2 : RUN_TEST(test_chat_edit_photo);
859 2 : RUN_TEST(test_pin_message);
860 2 : RUN_TEST(test_history_clear);
861 2 : RUN_TEST(test_channel_create);
862 2 : RUN_TEST(test_channel_migrate_from);
863 2 : RUN_TEST(test_chat_migrate_to);
864 2 : RUN_TEST(test_group_call);
865 2 : RUN_TEST(test_group_call_scheduled);
866 2 : RUN_TEST(test_invite_to_group_call);
867 2 : RUN_TEST(test_phone_call);
868 2 : RUN_TEST(test_screenshot_taken);
869 2 : RUN_TEST(test_custom_action);
870 2 : RUN_TEST(test_unknown_action_labelled);
871 2 : RUN_TEST(test_service_shows_in_watch);
872 : /* Supplementary — coverage of Empty / DeletePhoto / alt PhoneCall
873 : * branches that the US-29 table does not prescribe. */
874 2 : RUN_TEST(test_action_empty_renders_blank);
875 2 : RUN_TEST(test_chat_delete_photo);
876 2 : RUN_TEST(test_phone_call_missed_zero_duration);
877 2 : }
|