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

            Line data    Source code
       1              : /* SPDX-License-Identifier: GPL-3.0-or-later */
       2              : /* Copyright 2026 Peter Csaszar */
       3              : 
       4              : /**
       5              :  * @file test_login_migrate.c
       6              :  * @brief TEST-86 / US-35 — functional coverage for PHONE_MIGRATE and
       7              :  *        USER_MIGRATE during the first-time login flow.
       8              :  *
       9              :  * rpc_parse_error already parses `PHONE_MIGRATE_<dc>`,
      10              :  * `USER_MIGRATE_<dc>`, and `NETWORK_MIGRATE_<dc>` into
      11              :  * err.migrate_dc.  These functional tests drive a real login against
      12              :  * the in-process mock Telegram server so every migrate branch is
      13              :  * exercised end-to-end (TL framing + IGE/AES + rpc_parse_error +
      14              :  * auth_session consumer) rather than only through a unit test on
      15              :  * rpc_parse_error.
      16              :  *
      17              :  * Scenarios:
      18              :  *   1. test_phone_migrate_first_send_code_switches_home_dc —
      19              :  *      auth.sendCode on DC2 replies PHONE_MIGRATE_4; the retry on DC4
      20              :  *      succeeds and session.bin's home_dc becomes 4.
      21              :  *   2. test_user_migrate_after_sign_in_switches_home_dc —
      22              :  *      auth.signIn on DC2 replies USER_MIGRATE_5; the retry on DC5
      23              :  *      succeeds and session.bin's home_dc becomes 5.
      24              :  *   3. test_network_migrate_is_per_rpc_not_home —
      25              :  *      auth.sendCode replies NETWORK_MIGRATE_3; only the failing RPC
      26              :  *      retries on DC3, home DC stays unchanged at 2.
      27              :  *   4. test_ghost_migrate_loop_bails_at_3_hops —
      28              :  *      auth.sendCode keeps replying PHONE_MIGRATE_<n> even after each
      29              :  *      hop; the login flow gives up after AUTH_MAX_MIGRATIONS (3) hops
      30              :  *      with a clear failure state rather than spinning forever.
      31              :  */
      32              : 
      33              : #include "test_helpers.h"
      34              : 
      35              : #include "mock_socket.h"
      36              : #include "mock_tel_server.h"
      37              : 
      38              : #include "api_call.h"
      39              : #include "auth_session.h"
      40              : #include "mtproto_rpc.h"
      41              : #include "mtproto_session.h"
      42              : #include "transport.h"
      43              : #include "tl_registry.h"
      44              : #include "tl_serial.h"
      45              : #include "app/session_store.h"
      46              : #include "app/credentials.h"
      47              : 
      48              : #include <stdio.h>
      49              : #include <stdlib.h>
      50              : #include <string.h>
      51              : #include <sys/stat.h>
      52              : #include <unistd.h>
      53              : 
      54              : /* CRC for sentCodeTypeSms — used by the happy-path sendCode reply.
      55              :  * Duplicated here so this suite does not reach into private headers
      56              :  * beyond auth_session.h. */
      57              : #define CRC_sentCodeTypeSms  0xc000bba2U
      58              : 
      59              : /* Match the cap in src/app/auth_flow.c (AUTH_MAX_MIGRATIONS). */
      60              : #define LOCAL_MAX_MIGRATIONS 3
      61              : 
      62              : /* ================================================================ */
      63              : /* Helpers                                                          */
      64              : /* ================================================================ */
      65              : 
      66           20 : static void with_tmp_home(const char *tag) {
      67              :     char tmp[256];
      68           20 :     snprintf(tmp, sizeof(tmp), "/tmp/tg-cli-ft-migrate-%s", tag);
      69              :     char cfg_dir[512];
      70           20 :     snprintf(cfg_dir, sizeof(cfg_dir), "%s/.config/tg-cli", tmp);
      71           20 :     (void)mkdir(tmp, 0700);
      72              :     char parent[512];
      73           20 :     snprintf(parent, sizeof(parent), "%s/.config", tmp);
      74           20 :     (void)mkdir(parent, 0700);
      75           20 :     (void)mkdir(cfg_dir, 0700);
      76              :     char bin[600];
      77           20 :     snprintf(bin, sizeof(bin), "%s/session.bin", cfg_dir);
      78           20 :     (void)unlink(bin);
      79           20 :     setenv("HOME", tmp, 1);
      80              :     /* CI runners export these; clear so platform_config_dir() derives
      81              :      * from $HOME. */
      82           20 :     unsetenv("XDG_CONFIG_HOME");
      83           20 :     unsetenv("XDG_CACHE_HOME");
      84           20 : }
      85              : 
      86           20 : static void init_cfg(ApiConfig *cfg) {
      87           20 :     api_config_init(cfg);
      88           20 :     cfg->api_id = 12345;
      89           20 :     cfg->api_hash = "deadbeefcafebabef00dbaadfeedc0de";
      90           20 : }
      91              : 
      92           32 : static void connect_mock(Transport *t) {
      93           32 :     transport_init(t);
      94           32 :     ASSERT(transport_connect(t, "127.0.0.1", 443) == 0, "transport connects");
      95              : }
      96              : 
      97              : /* ---- Responders ---- */
      98              : 
      99              : /** Happy-path auth.sentCode reply. */
     100            6 : static void on_send_code_happy(MtRpcContext *ctx) {
     101              :     TlWriter w;
     102            6 :     tl_writer_init(&w);
     103            6 :     tl_write_uint32(&w, CRC_auth_sentCode);
     104            6 :     tl_write_uint32(&w, 0);                      /* flags = 0 */
     105            6 :     tl_write_uint32(&w, CRC_sentCodeTypeSms);
     106            6 :     tl_write_int32 (&w, 5);                      /* length */
     107            6 :     tl_write_string(&w, "abc123");               /* phone_code_hash */
     108            6 :     mt_server_reply_result(ctx, w.data, w.len);
     109            6 :     tl_writer_free(&w);
     110            6 : }
     111              : 
     112              : /** Happy-path auth.authorization reply with a fixed user_id. */
     113            2 : static void on_sign_in_happy(MtRpcContext *ctx) {
     114              :     TlWriter w;
     115            2 :     tl_writer_init(&w);
     116            2 :     tl_write_uint32(&w, TL_auth_authorization);
     117            2 :     tl_write_uint32(&w, 0);                  /* outer flags = 0 */
     118            2 :     tl_write_uint32(&w, TL_user);
     119            2 :     tl_write_uint32(&w, 0);                  /* user.flags = 0 */
     120            2 :     tl_write_int64 (&w, 55555LL);            /* user.id */
     121            2 :     mt_server_reply_result(ctx, w.data, w.len);
     122            2 :     tl_writer_free(&w);
     123            2 : }
     124              : 
     125              : /**
     126              :  * Helper that mirrors what auth_flow_login does on a migrate error:
     127              :  *   1. close the current transport
     128              :  *   2. reset the in-memory session (auth_key is DC-scoped)
     129              :  *   3. reconnect to the new DC (all DCs resolve to the mock loopback
     130              :  *      via dc_lookup, but we also need a seeded session at that DC)
     131              :  *   4. arm the mock's reconnect parser so the second 0xEF marker is
     132              :  *      treated as a fresh connection rather than a frame-length byte
     133              :  *
     134              :  * In production, auth_flow's migrate() would also run the full DH
     135              :  * handshake on the new DC.  The mock server does not emulate the
     136              :  * unencrypted DH flow; instead the test pre-seeds the secondary DC's
     137              :  * auth_key via mt_server_seed_session + mt_server_seed_extra_dc so
     138              :  * the tested layer (auth_session) lands on an already-authenticated
     139              :  * transport after the switch.  This keeps the test focused on the
     140              :  * migrate-loop semantics (retry count, final home DC) exactly where
     141              :  * the ticket requires coverage.
     142              :  */
     143           12 : static void simulate_migrate(Transport *t, MtProtoSession *s, int new_dc) {
     144           12 :     transport_close(t);
     145           12 :     mtproto_session_init(s);
     146              :     /* Load the pre-seeded auth_key for the target DC. */
     147           12 :     ASSERT(session_store_load_dc(new_dc, s) == 0,
     148              :            "load pre-seeded secondary DC key");
     149              :     /* Let the mock-server parser handle the second 0xEF marker the
     150              :      * fresh transport sends. */
     151           12 :     mt_server_arm_reconnect();
     152           12 :     connect_mock(t);
     153           12 :     t->dc_id = new_dc;
     154              : }
     155              : 
     156              : /* ================================================================ */
     157              : /* Scenario 1 — PHONE_MIGRATE on first auth.sendCode                 */
     158              : /* ================================================================ */
     159              : 
     160            2 : static void test_phone_migrate_first_send_code_switches_home_dc(void) {
     161            2 :     with_tmp_home("phone-migrate");
     162            2 :     mt_server_init();
     163            2 :     mt_server_reset();
     164              : 
     165              :     /* Seed DC2 as the starting home DC and DC4 as the migration target. */
     166            2 :     ASSERT(mt_server_seed_session(2, NULL, NULL, NULL) == 0, "seed home DC2");
     167            2 :     ASSERT(mt_server_seed_extra_dc(4) == 0, "seed foreign DC4 key");
     168              : 
     169              :     /* Arm the server: first auth.sendCode replies PHONE_MIGRATE_4. */
     170            2 :     mt_server_reply_phone_migrate(4);
     171              : 
     172            2 :     ApiConfig cfg; init_cfg(&cfg);
     173            2 :     MtProtoSession s; mtproto_session_init(&s);
     174            2 :     int dc = 0;
     175            2 :     ASSERT(session_store_load(&s, &dc) == 0, "session loaded from DC2");
     176            2 :     ASSERT(dc == 2, "home DC starts at 2");
     177              : 
     178            2 :     Transport t; connect_mock(&t);
     179            2 :     t.dc_id = 2;
     180              : 
     181            2 :     AuthSentCode sent = {0};
     182            2 :     RpcError err = {0};
     183            2 :     int rc = auth_send_code(&cfg, &s, &t, "+861234567890", &sent, &err);
     184            2 :     ASSERT(rc == -1, "sendCode fails with PHONE_MIGRATE");
     185            2 :     ASSERT(err.error_code == 303, "error_code 303");
     186            2 :     ASSERT(err.migrate_dc == 4,
     187              :            "rpc_parse_error extracts migrate_dc=4 from PHONE_MIGRATE_4");
     188            2 :     ASSERT(strncmp(err.error_msg, "PHONE_MIGRATE_", 14) == 0,
     189              :            "error_msg begins with PHONE_MIGRATE_");
     190              : 
     191              :     /* Mirror auth_flow_login: switch DC and retry. */
     192            2 :     simulate_migrate(&t, &s, err.migrate_dc);
     193              : 
     194              :     /* Now the responder table still has the migrate entry — swap it
     195              :      * for a happy-path responder so the retry on DC4 succeeds. */
     196            2 :     mt_server_expect(CRC_auth_sendCode, on_send_code_happy, NULL);
     197              : 
     198            2 :     AuthSentCode sent2 = {0};
     199            2 :     RpcError err2 = {0};
     200            2 :     ASSERT(auth_send_code(&cfg, &s, &t, "+861234567890", &sent2, &err2) == 0,
     201              :            "sendCode succeeds after migrate to DC4");
     202            2 :     ASSERT(strcmp(sent2.phone_code_hash, "abc123") == 0,
     203              :            "phone_code_hash roundtrips on the migrated DC");
     204              : 
     205              :     /* Persist the post-migration session — emulates auth_flow_login's
     206              :      * final session_store_save() with current_dc=4. */
     207            2 :     ASSERT(session_store_save(&s, 4) == 0, "persist session on new home DC4");
     208              : 
     209              :     /* Reload and verify session.bin's home DC is now 4. */
     210            2 :     MtProtoSession r; mtproto_session_init(&r);
     211            2 :     int reloaded_dc = 0;
     212            2 :     ASSERT(session_store_load(&r, &reloaded_dc) == 0, "reload post-migrate");
     213            2 :     ASSERT(reloaded_dc == 4,
     214              :            "session.bin home_dc is 4 after PHONE_MIGRATE retry");
     215              : 
     216            2 :     transport_close(&t);
     217            2 :     mt_server_reset();
     218              : }
     219              : 
     220              : /* ================================================================ */
     221              : /* Scenario 2 — USER_MIGRATE after auth.signIn                       */
     222              : /* ================================================================ */
     223              : 
     224            2 : static void test_user_migrate_after_sign_in_switches_home_dc(void) {
     225            2 :     with_tmp_home("user-migrate");
     226            2 :     mt_server_init();
     227            2 :     mt_server_reset();
     228              : 
     229            2 :     ASSERT(mt_server_seed_session(2, NULL, NULL, NULL) == 0, "seed home DC2");
     230            2 :     ASSERT(mt_server_seed_extra_dc(5) == 0, "seed foreign DC5 key");
     231              : 
     232              :     /* First step succeeds; signIn returns USER_MIGRATE_5. */
     233            2 :     mt_server_expect(CRC_auth_sendCode, on_send_code_happy, NULL);
     234            2 :     mt_server_reply_user_migrate(5);
     235              : 
     236            2 :     ApiConfig cfg; init_cfg(&cfg);
     237            2 :     MtProtoSession s; mtproto_session_init(&s);
     238            2 :     int dc = 0;
     239            2 :     ASSERT(session_store_load(&s, &dc) == 0, "session loaded");
     240            2 :     ASSERT(dc == 2, "home DC starts at 2");
     241              : 
     242            2 :     Transport t; connect_mock(&t);
     243            2 :     t.dc_id = 2;
     244              : 
     245            2 :     AuthSentCode sent = {0};
     246            2 :     RpcError sc_err = {0};
     247            2 :     ASSERT(auth_send_code(&cfg, &s, &t, "+15551234567", &sent, &sc_err) == 0,
     248              :            "sendCode happy-path on DC2");
     249              : 
     250            2 :     int64_t uid = 0;
     251            2 :     RpcError si_err = {0};
     252            2 :     int rc = auth_sign_in(&cfg, &s, &t, "+15551234567",
     253              :                           sent.phone_code_hash, "12345", &uid, &si_err);
     254            2 :     ASSERT(rc == -1, "signIn fails with USER_MIGRATE");
     255            2 :     ASSERT(si_err.error_code == 303, "error_code 303");
     256            2 :     ASSERT(si_err.migrate_dc == 5,
     257              :            "rpc_parse_error extracts migrate_dc=5 from USER_MIGRATE_5");
     258            2 :     ASSERT(strncmp(si_err.error_msg, "USER_MIGRATE_", 13) == 0,
     259              :            "error_msg begins with USER_MIGRATE_");
     260              : 
     261              :     /* Switch to DC5 and retry the signIn there. */
     262            2 :     simulate_migrate(&t, &s, si_err.migrate_dc);
     263            2 :     mt_server_expect(CRC_auth_signIn, on_sign_in_happy, NULL);
     264              : 
     265            2 :     int64_t uid2 = 0;
     266            2 :     RpcError si_err2 = {0};
     267            2 :     ASSERT(auth_sign_in(&cfg, &s, &t, "+15551234567",
     268              :                         sent.phone_code_hash, "12345", &uid2, &si_err2) == 0,
     269              :            "signIn succeeds after migrate to DC5");
     270            2 :     ASSERT(uid2 == 55555LL, "authenticated user_id returned from migrated DC");
     271              : 
     272            2 :     ASSERT(session_store_save(&s, 5) == 0, "persist on new home DC5");
     273              : 
     274            2 :     MtProtoSession r; mtproto_session_init(&r);
     275            2 :     int reloaded_dc = 0;
     276            2 :     ASSERT(session_store_load(&r, &reloaded_dc) == 0, "reload post-migrate");
     277            2 :     ASSERT(reloaded_dc == 5,
     278              :            "session.bin home_dc is 5 after USER_MIGRATE retry");
     279              : 
     280            2 :     transport_close(&t);
     281            2 :     mt_server_reset();
     282              : }
     283              : 
     284              : /* ================================================================ */
     285              : /* Scenario 3 — NETWORK_MIGRATE is per-RPC, not per-home             */
     286              : /* ================================================================ */
     287              : 
     288            2 : static void test_network_migrate_is_per_rpc_not_home(void) {
     289            2 :     with_tmp_home("network-migrate");
     290            2 :     mt_server_init();
     291            2 :     mt_server_reset();
     292              : 
     293            2 :     ASSERT(mt_server_seed_session(2, NULL, NULL, NULL) == 0, "seed home DC2");
     294            2 :     ASSERT(mt_server_seed_extra_dc(3) == 0, "seed foreign DC3 key");
     295              : 
     296              :     /* First auth.sendCode replies NETWORK_MIGRATE_3. */
     297            2 :     mt_server_reply_network_migrate(3);
     298              : 
     299            2 :     ApiConfig cfg; init_cfg(&cfg);
     300            2 :     MtProtoSession s; mtproto_session_init(&s);
     301            2 :     int dc_before = 0;
     302            2 :     ASSERT(session_store_load(&s, &dc_before) == 0, "session loaded");
     303            2 :     ASSERT(dc_before == 2, "home DC starts at 2");
     304              : 
     305            2 :     Transport t; connect_mock(&t);
     306            2 :     t.dc_id = 2;
     307              : 
     308            2 :     AuthSentCode sent = {0};
     309            2 :     RpcError err = {0};
     310            2 :     int rc = auth_send_code(&cfg, &s, &t, "+12025550000", &sent, &err);
     311            2 :     ASSERT(rc == -1, "sendCode fails with NETWORK_MIGRATE");
     312            2 :     ASSERT(err.error_code == 303, "error_code 303");
     313            2 :     ASSERT(err.migrate_dc == 3,
     314              :            "rpc_parse_error extracts migrate_dc=3 from NETWORK_MIGRATE_3");
     315            2 :     ASSERT(strncmp(err.error_msg, "NETWORK_MIGRATE_", 16) == 0,
     316              :            "error_msg begins with NETWORK_MIGRATE_");
     317              : 
     318              :     /* Retry the same RPC on DC3 using session_store_save_dc (which does
     319              :      * NOT promote DC3 to home) — this mirrors what a NETWORK_MIGRATE
     320              :      * handler in the infrastructure layer would do. */
     321            2 :     simulate_migrate(&t, &s, err.migrate_dc);
     322            2 :     mt_server_expect(CRC_auth_sendCode, on_send_code_happy, NULL);
     323              : 
     324            2 :     AuthSentCode sent2 = {0};
     325            2 :     RpcError err2 = {0};
     326            2 :     ASSERT(auth_send_code(&cfg, &s, &t, "+12025550000", &sent2, &err2) == 0,
     327              :            "sendCode succeeds after per-RPC retry on DC3");
     328              : 
     329              :     /* Critical assertion: home DC is unchanged at 2.  NETWORK_MIGRATE
     330              :      * is a transient redirect that rebinds only the current RPC, not
     331              :      * the user's home DC (contrast with PHONE_MIGRATE / USER_MIGRATE). */
     332            2 :     ASSERT(session_store_save_dc(3, &s) == 0,
     333              :            "save DC3 entry without changing home");
     334              : 
     335            2 :     MtProtoSession r; mtproto_session_init(&r);
     336            2 :     int reloaded_dc = 0;
     337            2 :     ASSERT(session_store_load(&r, &reloaded_dc) == 0, "reload after retry");
     338            2 :     ASSERT(reloaded_dc == 2,
     339              :            "home DC stays at 2 after NETWORK_MIGRATE retry");
     340              : 
     341            2 :     transport_close(&t);
     342            2 :     mt_server_reset();
     343              : }
     344              : 
     345              : /* ================================================================ */
     346              : /* Scenario 4 — Ghost migrate loop bails at AUTH_MAX_MIGRATIONS       */
     347              : /* ================================================================ */
     348              : 
     349              : /*
     350              :  * The mock is armed so every auth.sendCode responds PHONE_MIGRATE_X
     351              :  * with X incrementing by one each hop.  This test mirrors the loop
     352              :  * logic in src/app/auth_flow.c (AUTH_MAX_MIGRATIONS = 3): after 3
     353              :  * migrations the client must give up and surface a failure rather
     354              :  * than spinning forever or recursing indefinitely.
     355              :  *
     356              :  * We seed DC2..DC5 so the simulate_migrate() helper can load an
     357              :  * auth_key for every hop — the test's goal is to drive the loop
     358              :  * count past the cap, not to observe a DH handshake at each DC.
     359              :  */
     360            2 : static void test_ghost_migrate_loop_bails_at_3_hops(void) {
     361            2 :     with_tmp_home("ghost-migrate");
     362            2 :     mt_server_init();
     363            2 :     mt_server_reset();
     364              : 
     365            2 :     ASSERT(mt_server_seed_session(2, NULL, NULL, NULL) == 0, "seed home DC2");
     366            2 :     ASSERT(mt_server_seed_extra_dc(3) == 0, "seed DC3");
     367            2 :     ASSERT(mt_server_seed_extra_dc(4) == 0, "seed DC4");
     368            2 :     ASSERT(mt_server_seed_extra_dc(5) == 0, "seed DC5");
     369              : 
     370            2 :     ApiConfig cfg; init_cfg(&cfg);
     371            2 :     MtProtoSession s; mtproto_session_init(&s);
     372            2 :     int dc_before = 0;
     373            2 :     ASSERT(session_store_load(&s, &dc_before) == 0, "session loaded");
     374            2 :     ASSERT(dc_before == 2, "home DC starts at 2");
     375              : 
     376            2 :     Transport t; connect_mock(&t);
     377            2 :     t.dc_id = 2;
     378              : 
     379            2 :     int migrations = 0;
     380            2 :     int last_migrate_dc = 0;
     381            2 :     int bailed_out = 0;
     382              : 
     383              :     /* Arm the first reply and run the migrate loop mirroring
     384              :      * auth_flow_login: each sendCode returns PHONE_MIGRATE_<next_dc>.
     385              :      * After AUTH_MAX_MIGRATIONS hops the caller MUST stop retrying. */
     386            2 :     int next_dc = 3;
     387            2 :     mt_server_reply_phone_migrate(next_dc);
     388              : 
     389            6 :     for (;;) {
     390            8 :         AuthSentCode sent = {0};
     391            8 :         RpcError err = {0};
     392            8 :         int rc = auth_send_code(&cfg, &s, &t, "+861234567890", &sent, &err);
     393            8 :         if (rc == 0) {
     394              :             /* Unexpected — the server keeps replying PHONE_MIGRATE. */
     395            0 :             break;
     396              :         }
     397            8 :         if (err.migrate_dc > 0 && migrations < LOCAL_MAX_MIGRATIONS) {
     398            6 :             migrations++;
     399            6 :             last_migrate_dc = err.migrate_dc;
     400            6 :             simulate_migrate(&t, &s, err.migrate_dc);
     401              : 
     402              :             /* Arm the next PHONE_MIGRATE to a fresh DC so we can
     403              :              * distinguish the hops in assertions. */
     404            6 :             next_dc = (next_dc == 3) ? 4 : ((next_dc == 4) ? 5 : 3);
     405            6 :             mt_server_reply_phone_migrate(next_dc);
     406            6 :             continue;
     407              :         }
     408              :         /* Either no migrate_dc or we hit the cap — this is the bail
     409              :          * branch the production loop takes. */
     410            2 :         bailed_out = 1;
     411            2 :         break;
     412              :     }
     413              : 
     414            2 :     ASSERT(migrations == LOCAL_MAX_MIGRATIONS,
     415              :            "client performed exactly AUTH_MAX_MIGRATIONS hops (3)");
     416            2 :     ASSERT(bailed_out == 1,
     417              :            "loop bailed out rather than continuing indefinitely");
     418            2 :     ASSERT(last_migrate_dc > 0,
     419              :            "last migrate_dc was observed (loop did see a PHONE_MIGRATE on hop 3)");
     420              : 
     421              :     /* The session should NOT have been persisted with a bogus home DC.
     422              :      * session.bin may still exist from the seed — but if we re-load,
     423              :      * home DC must not match the ghost migrations (it stays at 2 as
     424              :      * seeded).  This guards against a future bug where the migrate
     425              :      * loop commits an intermediate DC on exhaustion. */
     426            2 :     MtProtoSession r; mtproto_session_init(&r);
     427            2 :     int reloaded_dc = 0;
     428            2 :     ASSERT(session_store_load(&r, &reloaded_dc) == 0, "reload after bail");
     429            2 :     ASSERT(reloaded_dc == 2,
     430              :            "home DC stays at 2 after ghost-migrate bail (no partial commit)");
     431              : 
     432            2 :     transport_close(&t);
     433            2 :     mt_server_reset();
     434              : }
     435              : 
     436              : /* ================================================================ */
     437              : /* Ancillary coverage — auth_session.c migrate-adjacent branches     */
     438              : /*                                                                   */
     439              : /* These are kept here rather than in test_login_flow.c so the TEST-86 */
     440              : /* suite captures everything needed to meet the US-35 coverage bar    */
     441              : /* (auth_session.c ≥ 80 %). Each case targets a branch rpc_parse_error */
     442              : /* feeds into and that the migrate tests above exercise indirectly.   */
     443              : /* ================================================================ */
     444              : 
     445              : /** auth.sentCode with flags.2 set — exercises the timeout-parsing branch. */
     446            2 : static void on_send_code_with_timeout(MtRpcContext *ctx) {
     447              :     TlWriter w;
     448            2 :     tl_writer_init(&w);
     449            2 :     tl_write_uint32(&w, CRC_auth_sentCode);
     450            2 :     tl_write_uint32(&w, 1u << 2);                /* flags.2 → timeout present */
     451            2 :     tl_write_uint32(&w, CRC_sentCodeTypeSms);
     452            2 :     tl_write_int32 (&w, 5);
     453            2 :     tl_write_string(&w, "xyz789");
     454            2 :     tl_write_int32 (&w, 120);                    /* timeout seconds */
     455            2 :     mt_server_reply_result(ctx, w.data, w.len);
     456            2 :     tl_writer_free(&w);
     457            2 : }
     458              : 
     459            2 : static void test_send_code_timeout_flag_parses(void) {
     460            2 :     with_tmp_home("timeout-flag");
     461            2 :     mt_server_init();
     462            2 :     mt_server_reset();
     463            2 :     ASSERT(mt_server_seed_session(2, NULL, NULL, NULL) == 0, "seed");
     464            2 :     mt_server_expect(CRC_auth_sendCode, on_send_code_with_timeout, NULL);
     465              : 
     466            2 :     ApiConfig cfg; init_cfg(&cfg);
     467            2 :     MtProtoSession s; mtproto_session_init(&s);
     468            2 :     int dc = 0; ASSERT(session_store_load(&s, &dc) == 0, "load");
     469              : 
     470            2 :     Transport t; connect_mock(&t);
     471              : 
     472            2 :     AuthSentCode sent = {0};
     473            2 :     RpcError err = {0};
     474            2 :     ASSERT(auth_send_code(&cfg, &s, &t, "+15551234567", &sent, &err) == 0,
     475              :            "sendCode ok with timeout flag");
     476            2 :     ASSERT(sent.timeout == 120,
     477              :            "timeout parsed from flags.2 branch of auth.sentCode");
     478            2 :     ASSERT(strcmp(sent.phone_code_hash, "xyz789") == 0,
     479              :            "phone_code_hash roundtrips alongside timeout");
     480              : 
     481            2 :     transport_close(&t);
     482            2 :     mt_server_reset();
     483              : }
     484              : 
     485              : /** Reply with neither rpc_error nor auth_sentCode to hit the
     486              :  *  "unexpected constructor" diagnostic in auth_send_code. */
     487            2 : static void on_send_code_unexpected_constructor(MtRpcContext *ctx) {
     488              :     /* Emit something harmless but clearly-not-sentCode: an auth.authorization
     489              :      * constructor (reserved for signIn). The reader will land in the
     490              :      * unexpected-constructor branch. */
     491              :     TlWriter w;
     492            2 :     tl_writer_init(&w);
     493            2 :     tl_write_uint32(&w, TL_auth_authorization);
     494            2 :     tl_write_uint32(&w, 0);
     495            2 :     tl_write_uint32(&w, TL_user);
     496            2 :     tl_write_uint32(&w, 0);
     497            2 :     tl_write_int64 (&w, 0);
     498            2 :     mt_server_reply_result(ctx, w.data, w.len);
     499            2 :     tl_writer_free(&w);
     500            2 : }
     501              : 
     502            2 : static void test_send_code_rejects_unexpected_constructor(void) {
     503            2 :     with_tmp_home("unexpected-ctor");
     504            2 :     mt_server_init();
     505            2 :     mt_server_reset();
     506            2 :     ASSERT(mt_server_seed_session(2, NULL, NULL, NULL) == 0, "seed");
     507            2 :     mt_server_expect(CRC_auth_sendCode, on_send_code_unexpected_constructor, NULL);
     508              : 
     509            2 :     ApiConfig cfg; init_cfg(&cfg);
     510            2 :     MtProtoSession s; mtproto_session_init(&s);
     511            2 :     int dc = 0; ASSERT(session_store_load(&s, &dc) == 0, "load");
     512              : 
     513            2 :     Transport t; connect_mock(&t);
     514              : 
     515            2 :     AuthSentCode sent = {0};
     516            2 :     RpcError err = {0};
     517            2 :     int rc = auth_send_code(&cfg, &s, &t, "+15551234567", &sent, &err);
     518            2 :     ASSERT(rc == -1,
     519              :            "auth_send_code fails on unexpected (non-sentCode, non-error) constructor");
     520              : 
     521            2 :     transport_close(&t);
     522            2 :     mt_server_reset();
     523              : }
     524              : 
     525              : /** Same coverage for the signIn path. */
     526            2 : static void on_sign_in_unexpected_constructor(MtRpcContext *ctx) {
     527              :     /* Emit an auth.sentCode instead of auth.authorization. */
     528              :     TlWriter w;
     529            2 :     tl_writer_init(&w);
     530            2 :     tl_write_uint32(&w, CRC_auth_sentCode);
     531            2 :     tl_write_uint32(&w, 0);
     532            2 :     tl_write_uint32(&w, CRC_sentCodeTypeSms);
     533            2 :     tl_write_int32 (&w, 5);
     534            2 :     tl_write_string(&w, "wrong");
     535            2 :     mt_server_reply_result(ctx, w.data, w.len);
     536            2 :     tl_writer_free(&w);
     537            2 : }
     538              : 
     539            2 : static void test_sign_in_rejects_unexpected_constructor(void) {
     540            2 :     with_tmp_home("signin-unexpected-ctor");
     541            2 :     mt_server_init();
     542            2 :     mt_server_reset();
     543            2 :     ASSERT(mt_server_seed_session(2, NULL, NULL, NULL) == 0, "seed");
     544            2 :     mt_server_expect(CRC_auth_signIn, on_sign_in_unexpected_constructor, NULL);
     545              : 
     546            2 :     ApiConfig cfg; init_cfg(&cfg);
     547            2 :     MtProtoSession s; mtproto_session_init(&s);
     548            2 :     int dc = 0; ASSERT(session_store_load(&s, &dc) == 0, "load");
     549              : 
     550            2 :     Transport t; connect_mock(&t);
     551              : 
     552            2 :     int64_t uid = 0;
     553            2 :     RpcError err = {0};
     554            2 :     int rc = auth_sign_in(&cfg, &s, &t, "+15551234567", "hash", "12345",
     555              :                           &uid, &err);
     556            2 :     ASSERT(rc == -1,
     557              :            "auth_sign_in fails on unexpected (non-authorization, non-error) constructor");
     558              : 
     559            2 :     transport_close(&t);
     560            2 :     mt_server_reset();
     561              : }
     562              : 
     563              : /** auth.sentCode with sentCodeTypeFlashCall — covers the alternate
     564              :  *  sub-object branch in skip_sent_code_type(). */
     565              : #define CRC_auth_sentCodeTypeFlashCall_local 0xab03c6d9U
     566            2 : static void on_send_code_flashcall(MtRpcContext *ctx) {
     567              :     TlWriter w;
     568            2 :     tl_writer_init(&w);
     569            2 :     tl_write_uint32(&w, CRC_auth_sentCode);
     570            2 :     tl_write_uint32(&w, 0);
     571            2 :     tl_write_uint32(&w, CRC_auth_sentCodeTypeFlashCall_local);
     572            2 :     tl_write_string(&w, "+1202XXX####");          /* pattern:string */
     573            2 :     tl_write_string(&w, "flash123");              /* phone_code_hash */
     574            2 :     mt_server_reply_result(ctx, w.data, w.len);
     575            2 :     tl_writer_free(&w);
     576            2 : }
     577              : 
     578            2 : static void test_send_code_flashcall_type_parses(void) {
     579            2 :     with_tmp_home("flashcall");
     580            2 :     mt_server_init();
     581            2 :     mt_server_reset();
     582            2 :     ASSERT(mt_server_seed_session(2, NULL, NULL, NULL) == 0, "seed");
     583            2 :     mt_server_expect(CRC_auth_sendCode, on_send_code_flashcall, NULL);
     584              : 
     585            2 :     ApiConfig cfg; init_cfg(&cfg);
     586            2 :     MtProtoSession s; mtproto_session_init(&s);
     587            2 :     int dc = 0; ASSERT(session_store_load(&s, &dc) == 0, "load");
     588              : 
     589            2 :     Transport t; connect_mock(&t);
     590              : 
     591            2 :     AuthSentCode sent = {0};
     592            2 :     RpcError err = {0};
     593            2 :     ASSERT(auth_send_code(&cfg, &s, &t, "+12025550000", &sent, &err) == 0,
     594              :            "sendCode ok with sentCodeTypeFlashCall");
     595            2 :     ASSERT(strcmp(sent.phone_code_hash, "flash123") == 0,
     596              :            "phone_code_hash parsed past flash-call pattern string");
     597              : 
     598            2 :     transport_close(&t);
     599            2 :     mt_server_reset();
     600              : }
     601              : 
     602              : /** auth.sentCode with unknown sentCodeType CRC — skip_sent_code_type's
     603              :  *  default branch rejects the response. */
     604            2 : static void on_send_code_unknown_codetype(MtRpcContext *ctx) {
     605              :     TlWriter w;
     606            2 :     tl_writer_init(&w);
     607            2 :     tl_write_uint32(&w, CRC_auth_sentCode);
     608            2 :     tl_write_uint32(&w, 0);
     609            2 :     tl_write_uint32(&w, 0xFADEBABEu);  /* no such sentCodeType */
     610            2 :     tl_write_int32 (&w, 5);
     611            2 :     tl_write_string(&w, "ignored");
     612            2 :     mt_server_reply_result(ctx, w.data, w.len);
     613            2 :     tl_writer_free(&w);
     614            2 : }
     615              : 
     616            2 : static void test_send_code_rejects_unknown_sentcode_type(void) {
     617            2 :     with_tmp_home("unknown-codetype");
     618            2 :     mt_server_init();
     619            2 :     mt_server_reset();
     620            2 :     ASSERT(mt_server_seed_session(2, NULL, NULL, NULL) == 0, "seed");
     621            2 :     mt_server_expect(CRC_auth_sendCode, on_send_code_unknown_codetype, NULL);
     622              : 
     623            2 :     ApiConfig cfg; init_cfg(&cfg);
     624            2 :     MtProtoSession s; mtproto_session_init(&s);
     625            2 :     int dc = 0; ASSERT(session_store_load(&s, &dc) == 0, "load");
     626              : 
     627            2 :     Transport t; connect_mock(&t);
     628              : 
     629            2 :     AuthSentCode sent = {0};
     630            2 :     RpcError err = {0};
     631            2 :     ASSERT(auth_send_code(&cfg, &s, &t, "+12025550000", &sent, &err) == -1,
     632              :            "sendCode rejects unknown sentCodeType constructor");
     633              : 
     634            2 :     transport_close(&t);
     635            2 :     mt_server_reset();
     636              : }
     637              : 
     638              : /** auth.authorization body with an unknown user constructor — the signIn
     639              :  *  parser logs a warning but still reports success with user_id=0. */
     640            2 : static void on_sign_in_unknown_user_ctor(MtRpcContext *ctx) {
     641              :     TlWriter w;
     642            2 :     tl_writer_init(&w);
     643            2 :     tl_write_uint32(&w, TL_auth_authorization);
     644            2 :     tl_write_uint32(&w, 0);                  /* outer flags = 0 */
     645            2 :     tl_write_uint32(&w, 0xDEADBEEFu);        /* bogus user constructor */
     646              :     /* No body — parser bails after reading the constructor. */
     647            2 :     mt_server_reply_result(ctx, w.data, w.len);
     648            2 :     tl_writer_free(&w);
     649            2 : }
     650              : 
     651            2 : static void test_sign_in_unknown_user_ctor_still_succeeds(void) {
     652            2 :     with_tmp_home("unknown-user-ctor");
     653            2 :     mt_server_init();
     654            2 :     mt_server_reset();
     655            2 :     ASSERT(mt_server_seed_session(2, NULL, NULL, NULL) == 0, "seed");
     656            2 :     mt_server_expect(CRC_auth_signIn, on_sign_in_unknown_user_ctor, NULL);
     657              : 
     658            2 :     ApiConfig cfg; init_cfg(&cfg);
     659            2 :     MtProtoSession s; mtproto_session_init(&s);
     660            2 :     int dc = 0; ASSERT(session_store_load(&s, &dc) == 0, "load");
     661              : 
     662            2 :     Transport t; connect_mock(&t);
     663              : 
     664            2 :     int64_t uid = 0xAAAAAAAAAAAAAAAALL; /* sentinel */
     665            2 :     RpcError err = {0};
     666            2 :     int rc = auth_sign_in(&cfg, &s, &t, "+15551234567", "hash", "12345",
     667              :                           &uid, &err);
     668              :     /* The parser treats an unknown user constructor as "authenticated but
     669              :      * we do not know the id" — rc == 0, uid reset to 0. */
     670            2 :     ASSERT(rc == 0,
     671              :            "auth_sign_in still reports success on unknown user constructor");
     672            2 :     ASSERT(uid == 0,
     673              :            "unknown user constructor sets user_id=0");
     674              : 
     675            2 :     transport_close(&t);
     676            2 :     mt_server_reset();
     677              : }
     678              : 
     679              : /* ================================================================ */
     680              : /* Suite entry point                                                 */
     681              : /* ================================================================ */
     682              : 
     683            2 : void run_login_migrate_tests(void) {
     684            2 :     RUN_TEST(test_phone_migrate_first_send_code_switches_home_dc);
     685            2 :     RUN_TEST(test_user_migrate_after_sign_in_switches_home_dc);
     686            2 :     RUN_TEST(test_network_migrate_is_per_rpc_not_home);
     687            2 :     RUN_TEST(test_ghost_migrate_loop_bails_at_3_hops);
     688            2 :     RUN_TEST(test_send_code_timeout_flag_parses);
     689            2 :     RUN_TEST(test_send_code_rejects_unexpected_constructor);
     690            2 :     RUN_TEST(test_sign_in_rejects_unexpected_constructor);
     691            2 :     RUN_TEST(test_sign_in_unknown_user_ctor_still_succeeds);
     692            2 :     RUN_TEST(test_send_code_flashcall_type_parses);
     693            2 :     RUN_TEST(test_send_code_rejects_unknown_sentcode_type);
     694            2 : }
        

Generated by: LCOV version 2.0-1