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

            Line data    Source code
       1              : /* SPDX-License-Identifier: GPL-3.0-or-later */
       2              : /* Copyright 2026 Peter Csaszar */
       3              : 
       4              : /**
       5              :  * @file test_service_frames.c
       6              :  * @brief TEST-88 / US-37 — functional coverage for server-side service frames.
       7              :  *
       8              :  * src/infrastructure/api_call.c::classify_service_frame handles five
       9              :  * non-result CRCs — bad_server_salt, bad_msg_notification,
      10              :  * new_session_created, msgs_ack, pong — plus the SERVICE_FRAME_LIMIT (8)
      11              :  * drain loop. Until now no functional test injected them; salt rotation
      12              :  * on long-running `watch` / TUI sessions was the most likely untested
      13              :  * failure mode in the wild.
      14              :  *
      15              :  * Coverage here exercises the full api_call() pipeline end-to-end:
      16              :  *   - bad_server_salt    → automatic retry with refreshed salt succeeds.
      17              :  *   - new_session_created → transparent salt refresh, real result surfaces.
      18              :  *   - msgs_ack           → transparent skip.
      19              :  *   - pong               → transparent skip.
      20              :  *   - bad_msg_notification → surfaces as -1 without dropping auth_key.
      21              :  *   - service-frame storm → 9 queued frames exceed SERVICE_FRAME_LIMIT and
      22              :  *                           api_call returns -1 cleanly.
      23              :  *
      24              :  * Uses the six `mt_server_reply_*` / `mt_server_stack_service_frames`
      25              :  * helpers added to tests/mocks/mock_tel_server.{h,c} alongside this suite.
      26              :  */
      27              : 
      28              : #include "test_helpers.h"
      29              : 
      30              : #include "mock_socket.h"
      31              : #include "mock_tel_server.h"
      32              : 
      33              : #include "api_call.h"
      34              : #include "mtproto_rpc.h"
      35              : #include "mtproto_session.h"
      36              : #include "transport.h"
      37              : #include "app/session_store.h"
      38              : #include "tl_registry.h"
      39              : #include "tl_serial.h"
      40              : 
      41              : #include <stdio.h>
      42              : #include <stdlib.h>
      43              : #include <string.h>
      44              : #include <unistd.h>
      45              : 
      46              : /* help.getConfig#c4f9186b — a convenient "any RPC" the mock can reply to. */
      47              : #define CRC_help_getConfig  0xc4f9186bU
      48              : /* Mirror the production constants so we can assert on the refreshed salt. */
      49              : #define SVC_NEW_SESSION_SALT 0xCAFEF00DBAADC0DEULL
      50              : 
      51              : /* ================================================================ */
      52              : /* Boilerplate                                                       */
      53              : /* ================================================================ */
      54              : 
      55           12 : static void with_tmp_home(const char *tag) {
      56              :     char tmp[256];
      57           12 :     snprintf(tmp, sizeof(tmp), "/tmp/tg-cli-ft-svcfr-%s", tag);
      58              :     char bin[512];
      59           12 :     snprintf(bin, sizeof(bin), "%s/.config/tg-cli/session.bin", tmp);
      60           12 :     (void)unlink(bin);
      61           12 :     setenv("HOME", tmp, 1);
      62           12 : }
      63              : 
      64           12 : static void connect_mock(Transport *t) {
      65           12 :     transport_init(t);
      66           12 :     ASSERT(transport_connect(t, "127.0.0.1", 443) == 0, "connect");
      67              : }
      68              : 
      69           12 : static void init_cfg(ApiConfig *cfg) {
      70           12 :     api_config_init(cfg);
      71           12 :     cfg->api_id = 12345;
      72           12 :     cfg->api_hash = "deadbeefcafebabef00dbaadfeedc0de";
      73           12 : }
      74              : 
      75           12 : static void load_session(MtProtoSession *s, uint64_t *initial_salt_out) {
      76           12 :     uint64_t salt = 0;
      77           12 :     ASSERT(mt_server_seed_session(2, NULL, &salt, NULL) == 0, "seed");
      78           12 :     mtproto_session_init(s);
      79           12 :     int dc = 0;
      80           12 :     ASSERT(session_store_load(s, &dc) == 0, "load session");
      81           12 :     if (initial_salt_out) *initial_salt_out = salt;
      82              : }
      83              : 
      84              : /* Build a minimal help.getConfig query (just the CRC + no arguments). */
      85           12 : static void build_get_config(TlWriter *w) {
      86           12 :     tl_writer_init(w);
      87           12 :     tl_write_uint32(w, CRC_help_getConfig);
      88           12 : }
      89              : 
      90              : /* Responder: return a 4-byte sentinel as the rpc_result body so tests can
      91              :  * verify the payload survived service-frame drain. 0x5A5AA5A5 is arbitrary
      92              :  * but distinguishable; api_call unwraps rpc_result and hands the sentinel
      93              :  * to the caller through the resp buffer. */
      94              : #define RESULT_SENTINEL 0x5A5AA5A5U
      95           12 : static void on_get_config_sentinel(MtRpcContext *ctx) {
      96              :     uint8_t body[8];
      97           12 :     TlWriter pw; tl_writer_init(&pw);
      98           12 :     tl_write_uint32(&pw, RESULT_SENTINEL);
      99           12 :     tl_write_uint32(&pw, 0);  /* keep 4-byte alignment */
     100           12 :     memcpy(body, pw.data, pw.len);
     101           12 :     size_t body_len = pw.len;
     102           12 :     tl_writer_free(&pw);
     103           12 :     mt_server_reply_result(ctx, body, body_len);
     104           12 : }
     105              : 
     106              : /* Check that the decoded api_call response carries our sentinel. */
     107            8 : static void assert_sentinel(const uint8_t *resp, size_t resp_len) {
     108            8 :     ASSERT(resp_len >= 4, "response contains at least one uint32");
     109              :     uint32_t first;
     110            8 :     memcpy(&first, resp, 4);
     111            8 :     ASSERT(first == RESULT_SENTINEL,
     112              :            "response payload is the handler sentinel — real reply surfaced");
     113              : }
     114              : 
     115              : /* ================================================================ */
     116              : /* Tests                                                             */
     117              : /* ================================================================ */
     118              : 
     119              : /* 1. bad_server_salt → automatic retry succeeds, salt updated. */
     120            2 : static void test_bad_server_salt_auto_retries_and_succeeds(void) {
     121            2 :     with_tmp_home("bad-salt");
     122            2 :     mt_server_init(); mt_server_reset();
     123            2 :     mt_server_expect(CRC_help_getConfig, on_get_config_sentinel, NULL);
     124              : 
     125            2 :     ApiConfig cfg; init_cfg(&cfg);
     126            2 :     Transport t; connect_mock(&t);
     127            2 :     MtProtoSession s; uint64_t original_salt = 0;
     128            2 :     load_session(&s, &original_salt);
     129              : 
     130              :     /* Arm: the first RPC the client sends will bounce off a bad_server_salt
     131              :      * reply carrying THIS new salt; the client retries and the handler
     132              :      * serves the sentinel. */
     133            2 :     const uint64_t NEW_SALT = 0x1122334455667788ULL;
     134            2 :     mt_server_reply_bad_server_salt(NEW_SALT);
     135              : 
     136            2 :     TlWriter q; build_get_config(&q);
     137              :     uint8_t resp[256];
     138            2 :     size_t resp_len = 0;
     139            2 :     int rc = api_call(&cfg, &s, &t, q.data, q.len, resp, sizeof(resp), &resp_len);
     140            2 :     tl_writer_free(&q);
     141              : 
     142            2 :     ASSERT(rc == 0, "api_call returns 0 after salt-rotation retry");
     143            2 :     assert_sentinel(resp, resp_len);
     144            2 :     ASSERT(s.server_salt == NEW_SALT,
     145              :            "s.server_salt replaced with server-issued new_salt");
     146            2 :     ASSERT(s.server_salt != original_salt,
     147              :            "salt actually changed (sanity vs initial)");
     148            2 :     ASSERT(mt_server_rpc_call_count() == 1,
     149              :            "exactly one handler dispatch — bad_salt preempted the first frame "
     150              :            "so only the retry hit the handler");
     151            2 :     ASSERT(s.has_auth_key == 1,
     152              :            "auth_key retained across salt rotation");
     153              : 
     154            2 :     transport_close(&t);
     155            2 :     mt_server_reset();
     156              : }
     157              : 
     158              : /* 2. new_session_created → transparent; salt refreshed; real result surfaces. */
     159            2 : static void test_new_session_created_refreshes_salt(void) {
     160            2 :     with_tmp_home("new-session");
     161            2 :     mt_server_init(); mt_server_reset();
     162            2 :     mt_server_expect(CRC_help_getConfig, on_get_config_sentinel, NULL);
     163              : 
     164            2 :     ApiConfig cfg; init_cfg(&cfg);
     165            2 :     Transport t; connect_mock(&t);
     166            2 :     MtProtoSession s; uint64_t original_salt = 0;
     167            2 :     load_session(&s, &original_salt);
     168              : 
     169            2 :     mt_server_reply_new_session_created();
     170              : 
     171            2 :     TlWriter q; build_get_config(&q);
     172              :     uint8_t resp[256];
     173            2 :     size_t resp_len = 0;
     174            2 :     int rc = api_call(&cfg, &s, &t, q.data, q.len, resp, sizeof(resp), &resp_len);
     175            2 :     tl_writer_free(&q);
     176              : 
     177            2 :     ASSERT(rc == 0, "api_call surfaces real result past new_session_created");
     178            2 :     assert_sentinel(resp, resp_len);
     179            2 :     ASSERT(s.server_salt == SVC_NEW_SESSION_SALT,
     180              :            "s.server_salt picked up from new_session_created frame");
     181            2 :     ASSERT(s.server_salt != original_salt,
     182              :            "salt moved from seeded value");
     183            2 :     ASSERT(mt_server_rpc_call_count() == 1,
     184              :            "single RPC round-trip despite preceding service frame");
     185              : 
     186            2 :     transport_close(&t);
     187            2 :     mt_server_reset();
     188              : }
     189              : 
     190              : /* 3. msgs_ack → transparent; caller sees only the real result. */
     191            2 : static void test_msgs_ack_is_transparent(void) {
     192            2 :     with_tmp_home("msgs-ack");
     193            2 :     mt_server_init(); mt_server_reset();
     194            2 :     mt_server_expect(CRC_help_getConfig, on_get_config_sentinel, NULL);
     195              : 
     196            2 :     ApiConfig cfg; init_cfg(&cfg);
     197            2 :     Transport t; connect_mock(&t);
     198            2 :     MtProtoSession s; uint64_t original_salt = 0;
     199            2 :     load_session(&s, &original_salt);
     200              : 
     201              :     /* Three distinct msg_ids — simulates the server acking a small batch
     202              :      * of previously-sent client frames. */
     203            2 :     uint64_t ids[] = {0xA1A1A1A100000001ULL,
     204              :                      0xA1A1A1A100000002ULL,
     205              :                      0xA1A1A1A100000003ULL};
     206            2 :     mt_server_reply_msgs_ack(ids, 3);
     207              : 
     208            2 :     TlWriter q; build_get_config(&q);
     209              :     uint8_t resp[256];
     210            2 :     size_t resp_len = 0;
     211            2 :     int rc = api_call(&cfg, &s, &t, q.data, q.len, resp, sizeof(resp), &resp_len);
     212            2 :     tl_writer_free(&q);
     213              : 
     214            2 :     ASSERT(rc == 0, "api_call succeeds through transparent msgs_ack");
     215            2 :     assert_sentinel(resp, resp_len);
     216            2 :     ASSERT(s.server_salt == original_salt,
     217              :            "msgs_ack does not touch the session salt");
     218              : 
     219            2 :     transport_close(&t);
     220            2 :     mt_server_reset();
     221              : }
     222              : 
     223              : /* 4. pong → transparent; caller sees only the real result. */
     224            2 : static void test_pong_is_transparent(void) {
     225            2 :     with_tmp_home("pong");
     226            2 :     mt_server_init(); mt_server_reset();
     227            2 :     mt_server_expect(CRC_help_getConfig, on_get_config_sentinel, NULL);
     228              : 
     229            2 :     ApiConfig cfg; init_cfg(&cfg);
     230            2 :     Transport t; connect_mock(&t);
     231            2 :     MtProtoSession s; uint64_t original_salt = 0;
     232            2 :     load_session(&s, &original_salt);
     233              : 
     234            2 :     mt_server_reply_pong(0xDEADBEEF01020304ULL,
     235              :                          0xCAFE00DD12345678ULL);
     236              : 
     237            2 :     TlWriter q; build_get_config(&q);
     238              :     uint8_t resp[256];
     239            2 :     size_t resp_len = 0;
     240            2 :     int rc = api_call(&cfg, &s, &t, q.data, q.len, resp, sizeof(resp), &resp_len);
     241            2 :     tl_writer_free(&q);
     242              : 
     243            2 :     ASSERT(rc == 0, "api_call succeeds through transparent pong");
     244            2 :     assert_sentinel(resp, resp_len);
     245            2 :     ASSERT(s.server_salt == original_salt, "pong does not touch the session salt");
     246              : 
     247            2 :     transport_close(&t);
     248            2 :     mt_server_reset();
     249              : }
     250              : 
     251              : /* 5. bad_msg_notification → specific RPC fails, session retained intact. */
     252            2 : static void test_bad_msg_notification_surfaces_error_without_dropping_session(void) {
     253            2 :     with_tmp_home("bad-msg");
     254            2 :     mt_server_init(); mt_server_reset();
     255              :     /* Register a handler so the dispatch path is complete — classify returns
     256              :      * SVC_ERROR before the queued handler reply is reached, so the handler
     257              :      * never actually surfaces a payload to the client. But the server still
     258              :      * computes a reply and queues it (harmless). */
     259            2 :     mt_server_expect(CRC_help_getConfig, on_get_config_sentinel, NULL);
     260              : 
     261            2 :     ApiConfig cfg; init_cfg(&cfg);
     262            2 :     Transport t; connect_mock(&t);
     263            2 :     MtProtoSession s; uint64_t original_salt = 0;
     264            2 :     load_session(&s, &original_salt);
     265              : 
     266              :     /* Capture a copy of the auth_key so we can verify it is preserved. */
     267              :     uint8_t auth_key_before[MTPROTO_AUTH_KEY_SIZE];
     268            2 :     memcpy(auth_key_before, s.auth_key, sizeof(auth_key_before));
     269              : 
     270              :     /* Queue the bad_msg_notification ahead of the real reply.
     271              :      * bad_msg_id = 0xFFEE (synthetic), error_code = 16 (msg_id too low). */
     272            2 :     mt_server_reply_bad_msg_notification(0xFFEEFFEEFFEEFFEEULL, 16);
     273              : 
     274            2 :     TlWriter q; build_get_config(&q);
     275              :     uint8_t resp[256];
     276            2 :     size_t resp_len = 0;
     277            2 :     int rc = api_call(&cfg, &s, &t, q.data, q.len, resp, sizeof(resp), &resp_len);
     278            2 :     tl_writer_free(&q);
     279              : 
     280            2 :     ASSERT(rc == -1,
     281              :            "api_call surfaces bad_msg_notification as -1");
     282              :     /* Session retained: auth_key bytes unchanged, salt unchanged. */
     283            2 :     ASSERT(memcmp(s.auth_key, auth_key_before, sizeof(auth_key_before)) == 0,
     284              :            "auth_key preserved across bad_msg_notification");
     285            2 :     ASSERT(s.server_salt == original_salt,
     286              :            "server_salt preserved across bad_msg_notification");
     287            2 :     ASSERT(s.has_auth_key == 1,
     288              :            "has_auth_key flag retained — session not discarded");
     289              : 
     290            2 :     transport_close(&t);
     291            2 :     mt_server_reset();
     292              : }
     293              : 
     294              : /* 6. Service-frame storm — 9 queued acks exceed SERVICE_FRAME_LIMIT (8).
     295              :  *    api_call returns -1 cleanly instead of looping forever. */
     296            2 : static void test_service_frame_storm_bails_at_limit(void) {
     297            2 :     with_tmp_home("storm");
     298            2 :     mt_server_init(); mt_server_reset();
     299            2 :     mt_server_expect(CRC_help_getConfig, on_get_config_sentinel, NULL);
     300              : 
     301            2 :     ApiConfig cfg; init_cfg(&cfg);
     302            2 :     Transport t; connect_mock(&t);
     303            2 :     MtProtoSession s; uint64_t original_salt = 0;
     304            2 :     load_session(&s, &original_salt);
     305              : 
     306              :     /* Stack exactly 9 msgs_ack frames — one more than the client's
     307              :      * 8-frame drain limit. The 9th classify iteration hits the loop
     308              :      * cap and api_call_once falls out of the for-loop with whatever
     309              :      * happened to be in `raw_resp` (still a msgs_ack frame) — rpc_unwrap_gzip
     310              :      * refuses to decode that as a real payload, so api_call returns -1. */
     311            2 :     mt_server_stack_service_frames(9);
     312              : 
     313            2 :     TlWriter q; build_get_config(&q);
     314              :     uint8_t resp[256];
     315            2 :     size_t resp_len = 0;
     316            2 :     int rc = api_call(&cfg, &s, &t, q.data, q.len, resp, sizeof(resp), &resp_len);
     317            2 :     tl_writer_free(&q);
     318              : 
     319            2 :     ASSERT(rc == -1,
     320              :            "api_call returns -1 after SERVICE_FRAME_LIMIT is exceeded");
     321              :     /* Session untouched even though the call failed. */
     322            2 :     ASSERT(s.server_salt == original_salt,
     323              :            "salt unchanged after service-frame storm");
     324            2 :     ASSERT(s.has_auth_key == 1,
     325              :            "auth_key retained through storm");
     326              : 
     327            2 :     transport_close(&t);
     328            2 :     mt_server_reset();
     329              : }
     330              : 
     331              : /* ================================================================ */
     332              : /* Suite entry point                                                */
     333              : /* ================================================================ */
     334              : 
     335            2 : void run_service_frames_tests(void) {
     336            2 :     RUN_TEST(test_bad_server_salt_auto_retries_and_succeeds);
     337            2 :     RUN_TEST(test_new_session_created_refreshes_salt);
     338            2 :     RUN_TEST(test_msgs_ack_is_transparent);
     339            2 :     RUN_TEST(test_pong_is_transparent);
     340            2 :     RUN_TEST(test_bad_msg_notification_surfaces_error_without_dropping_session);
     341            2 :     RUN_TEST(test_service_frame_storm_bails_at_limit);
     342            2 : }
        

Generated by: LCOV version 2.0-1