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

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

Generated by: LCOV version 2.0-1