LCOV - code coverage report
Current view: top level - tests/functional - test_login_flow.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 97.9 % 380 372
Test Date: 2026-04-20 19:54:22 Functions: 93.3 % 30 28

            Line data    Source code
       1              : /**
       2              :  * @file test_login_flow.c
       3              :  * @brief FT-03 — login flow functional tests through the in-process
       4              :  *        Telegram server emulator.
       5              :  *
       6              :  * Exercises production code paths end-to-end using real OpenSSL on both
       7              :  * sides (client + mock server). Covers:
       8              :  *   - auth.sendCode happy path
       9              :  *   - auth.sendCode with PHONE_NUMBER_INVALID
      10              :  *   - auth.sendCode with PHONE_MIGRATE_X (migration signal)
      11              :  *   - auth.signIn success (returns user_id)
      12              :  *   - auth.signIn SIGN_UP_REQUIRED rejection
      13              :  *   - auth.signIn SESSION_PASSWORD_NEEDED signal (2FA switch)
      14              :  *   - account.getPassword parsing of SRP params
      15              :  *   - auth.checkPassword rejection (PASSWORD_HASH_INVALID)
      16              :  *   - auth.checkPassword acceptance (server trusts the proof)
      17              :  *   - bad_server_salt retry — client swaps salt and resends transparently
      18              :  *   - session persistence roundtrip across save/load
      19              :  *   - session_store_clear() simulates --logout
      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 "auth_session.h"
      29              : #include "infrastructure/auth_2fa.h"
      30              : #include "mtproto_rpc.h"
      31              : #include "mtproto_session.h"
      32              : #include "transport.h"
      33              : #include "app/session_store.h"
      34              : #include "app/credentials.h"
      35              : #include "app/auth_flow.h"
      36              : #include "tl_registry.h"
      37              : #include "tl_serial.h"
      38              : 
      39              : #include <fcntl.h>
      40              : #include <stdio.h>
      41              : #include <stdlib.h>
      42              : #include <string.h>
      43              : #include <sys/stat.h>
      44              : #include <unistd.h>
      45              : 
      46              : /* CRCs that are not already exposed by a public header. Keys that would
      47              :  * collide with macros from auth_session.h / tl_registry.h are intentionally
      48              :  * reused by including those headers above. */
      49              : #define CRC_sentCodeTypeSms        0xc000bba2U
      50              : #define CRC_account_getPassword    0x548a30f5U
      51              : #define CRC_KdfAlgoPBKDF2          0x3a912d4aU
      52              : #define CRC_auth_checkPassword     0xd18b4d16U
      53              : 
      54              : /* ---- test helpers ---- */
      55              : 
      56           24 : static void with_tmp_home(const char *tag) {
      57              :     char tmp[256];
      58           24 :     snprintf(tmp, sizeof(tmp), "/tmp/tg-cli-ft-login-%s", tag);
      59              :     char bin[512];
      60           24 :     snprintf(bin, sizeof(bin), "%s/.config/tg-cli/session.bin", tmp);
      61           24 :     (void)unlink(bin);
      62           24 :     setenv("HOME", tmp, 1);
      63           24 : }
      64              : 
      65              : /** Dial a fresh Transport to the mock-socket loopback. */
      66           20 : static void connect_mock(Transport *t) {
      67           20 :     transport_init(t);
      68           20 :     ASSERT(transport_connect(t, "127.0.0.1", 443) == 0, "transport connects");
      69              : }
      70              : 
      71              : /** Initialise ApiConfig with fake credentials for tests. */
      72           20 : static void init_cfg(ApiConfig *cfg) {
      73           20 :     api_config_init(cfg);
      74           20 :     cfg->api_id = 12345;
      75           20 :     cfg->api_hash = "deadbeefcafebabef00dbaadfeedc0de";
      76           20 : }
      77              : 
      78              : /* ================================================================ */
      79              : /* Responders                                                       */
      80              : /* ================================================================ */
      81              : 
      82              : /* auth.sentCode reply: type=sms, length=5, hash="abc123", no timeout. */
      83            4 : static void on_send_code_happy(MtRpcContext *ctx) {
      84              :     TlWriter w;
      85            4 :     tl_writer_init(&w);
      86            4 :     tl_write_uint32(&w, CRC_auth_sentCode);
      87            4 :     tl_write_uint32(&w, 0);                      /* flags = 0 */
      88            4 :     tl_write_uint32(&w, CRC_sentCodeTypeSms);    /* sentCodeTypeSms */
      89            4 :     tl_write_int32 (&w, 5);                      /* length */
      90            4 :     tl_write_string(&w, "abc123");               /* phone_code_hash */
      91            4 :     mt_server_reply_result(ctx, w.data, w.len);
      92            4 :     tl_writer_free(&w);
      93            4 : }
      94              : 
      95            2 : static void on_send_code_invalid_phone(MtRpcContext *ctx) {
      96            2 :     mt_server_reply_error(ctx, 400, "PHONE_NUMBER_INVALID");
      97            2 : }
      98              : 
      99            2 : static void on_send_code_migrate_4(MtRpcContext *ctx) {
     100            2 :     mt_server_reply_error(ctx, 303, "PHONE_MIGRATE_4");
     101            2 : }
     102              : 
     103              : /* auth.authorization with user having id=77777 */
     104            2 : static void on_sign_in_happy(MtRpcContext *ctx) {
     105              :     TlWriter w;
     106            2 :     tl_writer_init(&w);
     107            2 :     tl_write_uint32(&w, TL_auth_authorization);
     108            2 :     tl_write_uint32(&w, 0);                  /* outer flags = 0 */
     109            2 :     tl_write_uint32(&w, TL_user);           /* user constructor */
     110            2 :     tl_write_uint32(&w, 0);                  /* user.flags = 0 */
     111            2 :     tl_write_int64 (&w, 77777LL);            /* user.id */
     112              :     /* The parser stops after id, so trailing fields don't need to be
     113              :      * schema-perfect — but include enough so nothing memory-walks. */
     114            2 :     mt_server_reply_result(ctx, w.data, w.len);
     115            2 :     tl_writer_free(&w);
     116            2 : }
     117              : 
     118            2 : static void on_sign_up_required(MtRpcContext *ctx) {
     119            2 :     mt_server_reply_error(ctx, 401, "SIGN_UP_REQUIRED");
     120            2 : }
     121              : 
     122            2 : static void on_session_password_needed(MtRpcContext *ctx) {
     123            2 :     mt_server_reply_error(ctx, 401, "SESSION_PASSWORD_NEEDED");
     124            2 : }
     125              : 
     126              : /* Deterministic SRP fixture for tests — prime / salts / srp_B are
     127              :  * byte-patterns with the right lengths. The client only validates
     128              :  * lengths, not mathematical correctness; auth_2fa_srp_compute will still
     129              :  * run the math, and the server accepts whatever M1 we receive. */
     130              : static uint8_t g_test_salt1[16];
     131              : static uint8_t g_test_salt2[16];
     132              : static uint8_t g_test_prime[256];
     133              : static uint8_t g_test_srpB[256];
     134              : 
     135            6 : static void init_srp_fixture(void) {
     136          102 :     for (int i = 0; i < 16;  ++i) g_test_salt1[i] = (uint8_t)(i + 1);
     137          102 :     for (int i = 0; i < 16;  ++i) g_test_salt2[i] = (uint8_t)(i + 17);
     138         1542 :     for (int i = 0; i < 256; ++i) g_test_prime[i] = (uint8_t)((i * 7 + 3) | 0x80u);
     139              :     /* Force odd-highest-bit so p looks like a >= 2047-bit prime. */
     140            6 :     g_test_prime[0] = 0xC7;
     141            6 :     g_test_prime[255] = 0x7F;  /* odd so gcd(p,2)=1 — still just length-valid */
     142         1542 :     for (int i = 0; i < 256; ++i) g_test_srpB[i] = (uint8_t)((i * 5 + 11));
     143            6 :     g_test_srpB[0] = 0x01;     /* ensure B < p */
     144            6 : }
     145              : 
     146              : /* account.password with has_password=true and fixture SRP params. */
     147            6 : static void on_get_password(MtRpcContext *ctx) {
     148              :     TlWriter w;
     149            6 :     tl_writer_init(&w);
     150            6 :     tl_write_uint32(&w, TL_account_password);
     151            6 :     tl_write_uint32(&w, 1u << 2);                           /* flags: has_password */
     152            6 :     tl_write_uint32(&w, CRC_KdfAlgoPBKDF2);                 /* current_algo */
     153            6 :     tl_write_bytes (&w, g_test_salt1, sizeof(g_test_salt1));
     154            6 :     tl_write_bytes (&w, g_test_salt2, sizeof(g_test_salt2));
     155            6 :     tl_write_int32 (&w, 2);                                 /* generator g=2 */
     156            6 :     tl_write_bytes (&w, g_test_prime, sizeof(g_test_prime));
     157            6 :     tl_write_bytes (&w, g_test_srpB,  sizeof(g_test_srpB)); /* srp_B */
     158            6 :     tl_write_int64 (&w, 0x1234567890ABCDEFLL);              /* srp_id */
     159              :     /* new_algo / new_secure_algo / secure_random — reader skips them. */
     160            6 :     mt_server_reply_result(ctx, w.data, w.len);
     161            6 :     tl_writer_free(&w);
     162            6 : }
     163              : 
     164            2 : static void on_check_password_wrong(MtRpcContext *ctx) {
     165            2 :     mt_server_reply_error(ctx, 400, "PASSWORD_HASH_INVALID");
     166            2 : }
     167              : 
     168            2 : static void on_check_password_accept(MtRpcContext *ctx) {
     169              :     /* Server doesn't verify M1 — we just want to drive the client code to
     170              :      * success so the authorization reply parser runs. */
     171              :     TlWriter w;
     172            2 :     tl_writer_init(&w);
     173            2 :     tl_write_uint32(&w, TL_auth_authorization);
     174            2 :     tl_write_uint32(&w, 0);
     175            2 :     tl_write_uint32(&w, TL_user);
     176            2 :     tl_write_uint32(&w, 0);
     177            2 :     tl_write_int64 (&w, 424242LL);
     178            2 :     mt_server_reply_result(ctx, w.data, w.len);
     179            2 :     tl_writer_free(&w);
     180            2 : }
     181              : 
     182              : /* ================================================================ */
     183              : /* Test cases                                                       */
     184              : /* ================================================================ */
     185              : 
     186            2 : static void test_send_code_happy(void) {
     187            2 :     with_tmp_home("send-code");
     188            2 :     mt_server_init();
     189            2 :     mt_server_reset();
     190            2 :     ASSERT(mt_server_seed_session(2, NULL, NULL, NULL) == 0, "seed");
     191            2 :     mt_server_expect(CRC_auth_sendCode, on_send_code_happy, NULL);
     192              : 
     193            2 :     ApiConfig cfg; init_cfg(&cfg);
     194              :     MtProtoSession s;
     195            2 :     mtproto_session_init(&s);
     196            2 :     int dc = 0;
     197            2 :     ASSERT(session_store_load(&s, &dc) == 0, "session loaded");
     198              : 
     199            2 :     Transport t; connect_mock(&t);
     200              : 
     201            2 :     AuthSentCode sent = {0};
     202            2 :     RpcError err = {0};
     203            2 :     ASSERT(auth_send_code(&cfg, &s, &t, "+15551234567", &sent, &err) == 0,
     204              :            "sendCode succeeds");
     205            2 :     ASSERT(strcmp(sent.phone_code_hash, "abc123") == 0,
     206              :            "phone_code_hash roundtrips");
     207              : 
     208            2 :     transport_close(&t);
     209            2 :     mt_server_reset();
     210              : }
     211              : 
     212            2 : static void test_send_code_invalid_phone(void) {
     213            2 :     with_tmp_home("bad-phone");
     214            2 :     mt_server_init();
     215            2 :     mt_server_reset();
     216            2 :     ASSERT(mt_server_seed_session(2, NULL, NULL, NULL) == 0, "seed");
     217            2 :     mt_server_expect(CRC_auth_sendCode, on_send_code_invalid_phone, NULL);
     218              : 
     219            2 :     ApiConfig cfg; init_cfg(&cfg);
     220            2 :     MtProtoSession s; mtproto_session_init(&s);
     221            2 :     int dc = 0; ASSERT(session_store_load(&s, &dc) == 0, "load");
     222              : 
     223            2 :     Transport t; connect_mock(&t);
     224              : 
     225            2 :     AuthSentCode sent = {0};
     226            2 :     RpcError err = {0};
     227            2 :     ASSERT(auth_send_code(&cfg, &s, &t, "+00000000", &sent, &err) == -1,
     228              :            "sendCode returns -1 on RPC error");
     229            2 :     ASSERT(err.error_code == 400, "error_code is 400");
     230            2 :     ASSERT(strcmp(err.error_msg, "PHONE_NUMBER_INVALID") == 0,
     231              :            "error_msg is PHONE_NUMBER_INVALID");
     232              : 
     233            2 :     transport_close(&t);
     234            2 :     mt_server_reset();
     235              : }
     236              : 
     237            2 : static void test_send_code_phone_migrate(void) {
     238            2 :     with_tmp_home("migrate");
     239            2 :     mt_server_init();
     240            2 :     mt_server_reset();
     241            2 :     ASSERT(mt_server_seed_session(2, NULL, NULL, NULL) == 0, "seed");
     242            2 :     mt_server_expect(CRC_auth_sendCode, on_send_code_migrate_4, NULL);
     243              : 
     244            2 :     ApiConfig cfg; init_cfg(&cfg);
     245            2 :     MtProtoSession s; mtproto_session_init(&s);
     246            2 :     int dc = 0; ASSERT(session_store_load(&s, &dc) == 0, "load");
     247              : 
     248            2 :     Transport t; connect_mock(&t);
     249              : 
     250            2 :     AuthSentCode sent = {0};
     251            2 :     RpcError err = {0};
     252            2 :     ASSERT(auth_send_code(&cfg, &s, &t, "+861234", &sent, &err) == -1,
     253              :            "sendCode fails with migration");
     254            2 :     ASSERT(err.error_code == 303, "error_code is 303");
     255            2 :     ASSERT(err.migrate_dc == 4, "migrate_dc is 4");
     256              : 
     257            2 :     transport_close(&t);
     258            2 :     mt_server_reset();
     259              : }
     260              : 
     261            2 : static void test_sign_in_happy(void) {
     262            2 :     with_tmp_home("sign-in");
     263            2 :     mt_server_init();
     264            2 :     mt_server_reset();
     265            2 :     ASSERT(mt_server_seed_session(2, NULL, NULL, NULL) == 0, "seed");
     266            2 :     mt_server_expect(CRC_auth_signIn, on_sign_in_happy, NULL);
     267              : 
     268            2 :     ApiConfig cfg; init_cfg(&cfg);
     269            2 :     MtProtoSession s; mtproto_session_init(&s);
     270            2 :     int dc = 0; ASSERT(session_store_load(&s, &dc) == 0, "load");
     271              : 
     272            2 :     Transport t; connect_mock(&t);
     273              : 
     274            2 :     int64_t uid = 0;
     275            2 :     RpcError err = {0};
     276            2 :     ASSERT(auth_sign_in(&cfg, &s, &t,
     277              :                         "+15551234567", "abc123", "12345",
     278              :                         &uid, &err) == 0, "signIn succeeds");
     279            2 :     ASSERT(uid == 77777LL, "user_id = 77777");
     280              : 
     281            2 :     transport_close(&t);
     282            2 :     mt_server_reset();
     283              : }
     284              : 
     285            2 : static void test_sign_in_sign_up_required(void) {
     286            2 :     with_tmp_home("sign-up");
     287            2 :     mt_server_init();
     288            2 :     mt_server_reset();
     289            2 :     ASSERT(mt_server_seed_session(2, NULL, NULL, NULL) == 0, "seed");
     290            2 :     mt_server_expect(CRC_auth_signIn, on_sign_up_required, NULL);
     291              : 
     292            2 :     ApiConfig cfg; init_cfg(&cfg);
     293            2 :     MtProtoSession s; mtproto_session_init(&s);
     294            2 :     int dc = 0; ASSERT(session_store_load(&s, &dc) == 0, "load");
     295              : 
     296            2 :     Transport t; connect_mock(&t);
     297              : 
     298            2 :     int64_t uid = 0;
     299            2 :     RpcError err = {0};
     300            2 :     ASSERT(auth_sign_in(&cfg, &s, &t, "+15551234567", "abc123", "12345",
     301              :                         &uid, &err) == -1, "signIn fails");
     302            2 :     ASSERT(err.error_code == 401, "error_code 401");
     303            2 :     ASSERT(strcmp(err.error_msg, "SIGN_UP_REQUIRED") == 0,
     304              :            "error_msg SIGN_UP_REQUIRED");
     305              : 
     306            2 :     transport_close(&t);
     307            2 :     mt_server_reset();
     308              : }
     309              : 
     310            2 : static void test_sign_in_password_needed(void) {
     311            2 :     with_tmp_home("need-pwd");
     312            2 :     mt_server_init();
     313            2 :     mt_server_reset();
     314            2 :     ASSERT(mt_server_seed_session(2, NULL, NULL, NULL) == 0, "seed");
     315            2 :     mt_server_expect(CRC_auth_signIn, on_session_password_needed, NULL);
     316              : 
     317            2 :     ApiConfig cfg; init_cfg(&cfg);
     318            2 :     MtProtoSession s; mtproto_session_init(&s);
     319            2 :     int dc = 0; ASSERT(session_store_load(&s, &dc) == 0, "load");
     320              : 
     321            2 :     Transport t; connect_mock(&t);
     322              : 
     323            2 :     int64_t uid = 0;
     324            2 :     RpcError err = {0};
     325            2 :     ASSERT(auth_sign_in(&cfg, &s, &t, "+15551234567", "abc123", "12345",
     326              :                         &uid, &err) == -1, "signIn fails");
     327            2 :     ASSERT(err.error_code == 401, "error_code 401");
     328            2 :     ASSERT(strcmp(err.error_msg, "SESSION_PASSWORD_NEEDED") == 0,
     329              :            "error_msg SESSION_PASSWORD_NEEDED");
     330              : 
     331            2 :     transport_close(&t);
     332            2 :     mt_server_reset();
     333              : }
     334              : 
     335            2 : static void test_2fa_get_password(void) {
     336            2 :     with_tmp_home("get-pwd");
     337            2 :     mt_server_init();
     338            2 :     mt_server_reset();
     339            2 :     init_srp_fixture();
     340            2 :     ASSERT(mt_server_seed_session(2, NULL, NULL, NULL) == 0, "seed");
     341            2 :     mt_server_expect(CRC_account_getPassword, on_get_password, NULL);
     342              : 
     343            2 :     ApiConfig cfg; init_cfg(&cfg);
     344            2 :     MtProtoSession s; mtproto_session_init(&s);
     345            2 :     int dc = 0; ASSERT(session_store_load(&s, &dc) == 0, "load");
     346              : 
     347            2 :     Transport t; connect_mock(&t);
     348              : 
     349            2 :     Account2faPassword params = {0};
     350            2 :     RpcError err = {0};
     351            2 :     ASSERT(auth_2fa_get_password(&cfg, &s, &t, &params, &err) == 0,
     352              :            "account.getPassword succeeds");
     353            2 :     ASSERT(params.has_password == 1, "has_password set");
     354            2 :     ASSERT(params.g == 2, "generator g=2");
     355            2 :     ASSERT(params.salt1_len == sizeof(g_test_salt1), "salt1 length");
     356            2 :     ASSERT(params.salt2_len == sizeof(g_test_salt2), "salt2 length");
     357            2 :     ASSERT(memcmp(params.p, g_test_prime, sizeof(g_test_prime)) == 0,
     358              :            "prime roundtrips");
     359            2 :     ASSERT(memcmp(params.srp_B, g_test_srpB, sizeof(g_test_srpB)) == 0,
     360              :            "srp_B roundtrips");
     361            2 :     ASSERT(params.srp_id == 0x1234567890ABCDEFLL, "srp_id roundtrips");
     362              : 
     363            2 :     transport_close(&t);
     364            2 :     mt_server_reset();
     365              : }
     366              : 
     367            2 : static void test_2fa_check_password_wrong(void) {
     368            2 :     with_tmp_home("pwd-wrong");
     369            2 :     mt_server_init();
     370            2 :     mt_server_reset();
     371            2 :     init_srp_fixture();
     372            2 :     ASSERT(mt_server_seed_session(2, NULL, NULL, NULL) == 0, "seed");
     373            2 :     mt_server_expect(CRC_account_getPassword, on_get_password, NULL);
     374            2 :     mt_server_expect(CRC_auth_checkPassword, on_check_password_wrong, NULL);
     375              : 
     376            2 :     ApiConfig cfg; init_cfg(&cfg);
     377            2 :     MtProtoSession s; mtproto_session_init(&s);
     378            2 :     int dc = 0; ASSERT(session_store_load(&s, &dc) == 0, "load");
     379              : 
     380            2 :     Transport t; connect_mock(&t);
     381              : 
     382            2 :     Account2faPassword params = {0};
     383            2 :     RpcError gp_err = {0};
     384            2 :     ASSERT(auth_2fa_get_password(&cfg, &s, &t, &params, &gp_err) == 0, "gp ok");
     385              : 
     386            2 :     int64_t uid = 0;
     387            2 :     RpcError cp_err = {0};
     388            2 :     ASSERT(auth_2fa_check_password(&cfg, &s, &t, &params, "wrong-pwd",
     389              :                                    &uid, &cp_err) == -1, "check fails");
     390            2 :     ASSERT(cp_err.error_code == 400, "400");
     391            2 :     ASSERT(strcmp(cp_err.error_msg, "PASSWORD_HASH_INVALID") == 0,
     392              :            "PASSWORD_HASH_INVALID");
     393              : 
     394            2 :     transport_close(&t);
     395            2 :     mt_server_reset();
     396              : }
     397              : 
     398            2 : static void test_2fa_check_password_accept(void) {
     399            2 :     with_tmp_home("pwd-ok");
     400            2 :     mt_server_init();
     401            2 :     mt_server_reset();
     402            2 :     init_srp_fixture();
     403            2 :     ASSERT(mt_server_seed_session(2, NULL, NULL, NULL) == 0, "seed");
     404            2 :     mt_server_expect(CRC_account_getPassword, on_get_password, NULL);
     405            2 :     mt_server_expect(CRC_auth_checkPassword, on_check_password_accept, NULL);
     406              : 
     407            2 :     ApiConfig cfg; init_cfg(&cfg);
     408            2 :     MtProtoSession s; mtproto_session_init(&s);
     409            2 :     int dc = 0; ASSERT(session_store_load(&s, &dc) == 0, "load");
     410              : 
     411            2 :     Transport t; connect_mock(&t);
     412              : 
     413            2 :     Account2faPassword params = {0};
     414            2 :     RpcError gp_err = {0};
     415            2 :     ASSERT(auth_2fa_get_password(&cfg, &s, &t, &params, &gp_err) == 0, "gp ok");
     416              : 
     417            2 :     int64_t uid = 0;
     418            2 :     RpcError cp_err = {0};
     419            2 :     ASSERT(auth_2fa_check_password(&cfg, &s, &t, &params, "secret",
     420              :                                    &uid, &cp_err) == 0, "check succeeds");
     421            2 :     ASSERT(uid == 424242LL, "user_id = 424242");
     422              : 
     423            2 :     transport_close(&t);
     424            2 :     mt_server_reset();
     425              : }
     426              : 
     427            2 : static void test_bad_server_salt_retry(void) {
     428            2 :     with_tmp_home("bad-salt");
     429            2 :     mt_server_init();
     430            2 :     mt_server_reset();
     431            2 :     ASSERT(mt_server_seed_session(2, NULL, NULL, NULL) == 0, "seed");
     432            2 :     mt_server_expect(CRC_auth_sendCode, on_send_code_happy, NULL);
     433              : 
     434              :     /* Arm a one-shot bad_server_salt for the first incoming RPC. */
     435            2 :     mt_server_set_bad_salt_once(0xFEDCBA9876543210ULL);
     436              : 
     437            2 :     ApiConfig cfg; init_cfg(&cfg);
     438            2 :     MtProtoSession s; mtproto_session_init(&s);
     439            2 :     int dc = 0; ASSERT(session_store_load(&s, &dc) == 0, "load");
     440            2 :     uint64_t salt_before = s.server_salt;
     441            2 :     ASSERT(salt_before != 0xFEDCBA9876543210ULL, "starting salt != new salt");
     442              : 
     443            2 :     Transport t; connect_mock(&t);
     444              : 
     445            2 :     AuthSentCode sent = {0};
     446            2 :     RpcError err = {0};
     447            2 :     ASSERT(auth_send_code(&cfg, &s, &t, "+15551234567", &sent, &err) == 0,
     448              :            "sendCode succeeds after transparent retry");
     449            2 :     ASSERT(strcmp(sent.phone_code_hash, "abc123") == 0, "hash ok");
     450            2 :     ASSERT(s.server_salt == 0xFEDCBA9876543210ULL,
     451              :            "client picked up the new salt");
     452              :     /* bad_salt round doesn't reach the handler — rpc_call_count only
     453              :      * increments for successful dispatches. The retry is the one
     454              :      * dispatch the handler sees. */
     455            2 :     ASSERT(mt_server_rpc_call_count() == 1,
     456              :            "retry dispatches exactly one RPC to the handler");
     457              : 
     458            2 :     transport_close(&t);
     459            2 :     mt_server_reset();
     460              : }
     461              : 
     462            2 : static void test_session_persistence_roundtrip(void) {
     463            2 :     with_tmp_home("persist");
     464            2 :     mt_server_init();
     465            2 :     mt_server_reset();
     466              : 
     467              :     uint8_t seeded_key[MT_SERVER_AUTH_KEY_SIZE];
     468            2 :     uint64_t seeded_salt = 0, seeded_sid = 0;
     469            2 :     ASSERT(mt_server_seed_session(3, seeded_key,
     470              :                                   &seeded_salt, &seeded_sid) == 0, "seed");
     471              : 
     472              :     MtProtoSession s1;
     473            2 :     mtproto_session_init(&s1);
     474            2 :     int dc1 = 0;
     475            2 :     ASSERT(session_store_load(&s1, &dc1) == 0, "first load");
     476            2 :     ASSERT(dc1 == 3, "home DC persisted");
     477            2 :     ASSERT(s1.server_salt == seeded_salt, "salt persisted");
     478            2 :     ASSERT(s1.session_id == seeded_sid, "session_id persisted");
     479            2 :     ASSERT(memcmp(s1.auth_key, seeded_key, MT_SERVER_AUTH_KEY_SIZE) == 0,
     480              :            "auth_key persisted");
     481              : 
     482              :     /* Re-save from a fresh session and reload — value should still match. */
     483              :     MtProtoSession s2;
     484            2 :     mtproto_session_init(&s2);
     485            2 :     mtproto_session_set_auth_key(&s2, seeded_key);
     486            2 :     mtproto_session_set_salt(&s2, seeded_salt);
     487            2 :     s2.session_id = seeded_sid;
     488            2 :     ASSERT(session_store_save(&s2, 3) == 0, "re-save");
     489              : 
     490              :     MtProtoSession s3;
     491            2 :     mtproto_session_init(&s3);
     492            2 :     int dc3 = 0;
     493            2 :     ASSERT(session_store_load(&s3, &dc3) == 0, "second load");
     494            2 :     ASSERT(dc3 == 3, "home DC still 3");
     495            2 :     ASSERT(memcmp(s3.auth_key, seeded_key, MT_SERVER_AUTH_KEY_SIZE) == 0,
     496              :            "auth_key stable across round-trip");
     497              : }
     498              : 
     499            2 : static void test_logout_clears_session(void) {
     500            2 :     with_tmp_home("logout");
     501            2 :     mt_server_init();
     502            2 :     mt_server_reset();
     503            2 :     ASSERT(mt_server_seed_session(2, NULL, NULL, NULL) == 0, "seed");
     504              : 
     505            2 :     MtProtoSession s; mtproto_session_init(&s);
     506            2 :     int dc = 0;
     507            2 :     ASSERT(session_store_load(&s, &dc) == 0, "load before clear");
     508              : 
     509            2 :     session_store_clear();
     510              : 
     511            2 :     MtProtoSession s2; mtproto_session_init(&s2);
     512            2 :     int dc2 = 0;
     513            2 :     ASSERT(session_store_load(&s2, &dc2) == -1,
     514              :            "load returns -1 after clear");
     515              : }
     516              : 
     517              : /**
     518              :  * @brief TEST-01 — batch mode must reject missing credentials without
     519              :  *        reading stdin.
     520              :  *
     521              :  * Scenario: no config.ini in HOME, no TG_CLI_API_ID/TG_CLI_API_HASH env
     522              :  * vars, and batch callbacks that supply no phone number.
     523              :  *
     524              :  * Asserts:
     525              :  *   1. credentials_load() returns -1 (missing api_id/api_hash).
     526              :  *   2. auth_flow_login() with NULL-phone batch callbacks returns -1.
     527              :  *   3. Neither call blocks on or reads from stdin (stdin is redirected to
     528              :  *      /dev/null; if either call read stdin it would return EOF and likely
     529              :  *      cause a different failure path — the ASAN/Valgrind run catches reads
     530              :  *      from uninitialised data in the same way).
     531              :  */
     532              : 
     533              : /* Batch callback that has no phone — simulates --batch without --phone. */
     534            0 : static int cb_no_phone(void *u, char *out, size_t cap) {
     535              :     (void)u; (void)out; (void)cap;
     536            0 :     return -1;   /* signals "not available" */
     537              : }
     538            0 : static int cb_no_code(void *u, char *out, size_t cap) {
     539              :     (void)u; (void)out; (void)cap;
     540            0 :     return -1;
     541              : }
     542              : 
     543            2 : static void test_batch_rejects_missing_credentials(void) {
     544              :     /* Point HOME at a fresh empty directory — no config.ini present. */
     545              :     char tmp[256];
     546            2 :     snprintf(tmp, sizeof(tmp), "/tmp/tg-cli-ft-login-batch-no-creds");
     547              :     /* Ensure the directory exists but has no config file. */
     548              :     char cfg_dir[512];
     549            2 :     snprintf(cfg_dir, sizeof(cfg_dir), "%s/.config/tg-cli", tmp);
     550              :     /* Remove any stale state from a previous run. */
     551              :     char ini[600];
     552            2 :     snprintf(ini, sizeof(ini), "%s/config.ini", cfg_dir);
     553            2 :     (void)unlink(ini);
     554              :     char session_path[600];
     555            2 :     snprintf(session_path, sizeof(session_path), "%s/session.bin", cfg_dir);
     556            2 :     (void)unlink(session_path);
     557              : 
     558            2 :     setenv("HOME", tmp, 1);
     559              :     /* Clear env-var credentials so credentials_load() cannot find them. */
     560            2 :     unsetenv("TG_CLI_API_ID");
     561            2 :     unsetenv("TG_CLI_API_HASH");
     562              : 
     563              :     /* Redirect stdin to /dev/null so any accidental read() returns EOF
     564              :      * immediately rather than blocking the test run. */
     565            2 :     int devnull = open("/dev/null", O_RDONLY);
     566            2 :     int saved_stdin = dup(STDIN_FILENO);
     567            2 :     if (devnull >= 0) {
     568            2 :         dup2(devnull, STDIN_FILENO);
     569            2 :         close(devnull);
     570              :     }
     571              : 
     572              :     /* --- Assertion 1: credentials_load() must fail --- */
     573              :     ApiConfig cfg;
     574            2 :     int rc = credentials_load(&cfg);
     575            2 :     ASSERT(rc == -1, "credentials_load returns -1 when no api_id/api_hash");
     576              : 
     577              :     /* --- Assertion 2: auth_flow_login() with no-phone callbacks must fail
     578              :      *     before touching the network (the mock socket is not seeded, so
     579              :      *     any accidental connect attempt would itself fail). --- */
     580              :     Transport t;
     581            2 :     transport_init(&t);
     582              :     MtProtoSession s;
     583            2 :     mtproto_session_init(&s);
     584              : 
     585              :     /* Provide dummy credentials so auth_flow_login proceeds past the
     586              :      * credential check and reaches the callback stage. */
     587              :     ApiConfig dummy_cfg;
     588            2 :     api_config_init(&dummy_cfg);
     589            2 :     dummy_cfg.api_id   = 99999;
     590            2 :     dummy_cfg.api_hash = "dummyhashfortesting";
     591              : 
     592            2 :     AuthFlowCallbacks cb = {
     593              :         .get_phone    = cb_no_phone,
     594              :         .get_code     = cb_no_code,
     595              :         .get_password = NULL,
     596              :         .user         = NULL,
     597              :     };
     598              : 
     599              :     /* mt_server is not seeded — transport_connect will fail, which causes
     600              :      * auth_flow_connect_dc to return -1 before get_phone is even reached.
     601              :      * Either way, auth_flow_login must return -1 without prompting stdin. */
     602            2 :     int flow_rc = auth_flow_login(&dummy_cfg, &cb, &t, &s, NULL);
     603            2 :     ASSERT(flow_rc == -1,
     604              :            "auth_flow_login returns -1 when server unreachable in batch mode");
     605              : 
     606            2 :     transport_close(&t);
     607              : 
     608              :     /* Restore stdin. */
     609            2 :     if (saved_stdin >= 0) {
     610            2 :         dup2(saved_stdin, STDIN_FILENO);
     611            2 :         close(saved_stdin);
     612              :     }
     613              : }
     614              : 
     615            2 : static void test_credentials_env_override(void) {
     616              :     /* Write a config.ini with known values that should be overridden. */
     617              :     char tmp[256];
     618            2 :     snprintf(tmp, sizeof(tmp), "/tmp/tg-cli-ft-login-env-override");
     619              :     char cfg_dir[512];
     620            2 :     snprintf(cfg_dir, sizeof(cfg_dir), "%s/.config/tg-cli", tmp);
     621              :     char ini[600];
     622            2 :     snprintf(ini, sizeof(ini), "%s/config.ini", cfg_dir);
     623              : 
     624              :     /* Create directory and write INI with values that differ from env. */
     625            2 :     (void)mkdir(tmp, 0700);
     626            2 :     (void)mkdir(cfg_dir, 0700);
     627              :     {
     628            2 :         FILE *fp = fopen(ini, "w");
     629            2 :         if (fp) {
     630            0 :             fprintf(fp, "api_id=1111\napi_hash=aaahash\n");
     631            0 :             fclose(fp);
     632              :         }
     633              :     }
     634              : 
     635              :     /* Override HOME so platform_config_dir() uses our tmp tree. */
     636              :     char saved_home[512];
     637            2 :     const char *orig_home = getenv("HOME");
     638            2 :     if (orig_home) snprintf(saved_home, sizeof(saved_home), "%s", orig_home);
     639            0 :     else saved_home[0] = '\0';
     640            2 :     setenv("HOME", tmp, 1);
     641              : 
     642              :     /* Set env vars that must take precedence over the INI file.
     643              :      * TEST-84 / US-33 introduced api_hash length+hex validation in
     644              :      * credentials_load(), so use a valid 32-char lowercase hex marker
     645              :      * distinct from the INI value (`aaahash`) to prove precedence. */
     646            2 :     setenv("TG_CLI_API_ID",   "9999",                              1);
     647            2 :     setenv("TG_CLI_API_HASH", "abcd0123abcd0123abcd0123abcd0123", 1);
     648              : 
     649              :     ApiConfig cfg;
     650            2 :     int rc = credentials_load(&cfg);
     651            2 :     ASSERT(rc == 0, "credentials_load returns 0 when env vars are set");
     652            2 :     ASSERT(cfg.api_id == 9999,
     653              :            "credentials_load returns api_id from env, not INI");
     654            2 :     ASSERT(cfg.api_hash != NULL,
     655              :            "credentials_load returns non-NULL api_hash from env");
     656            2 :     ASSERT(strcmp(cfg.api_hash, "abcd0123abcd0123abcd0123abcd0123") == 0,
     657              :            "credentials_load returns api_hash value from env, not INI");
     658              : 
     659              :     /* Cleanup. */
     660            2 :     unsetenv("TG_CLI_API_ID");
     661            2 :     unsetenv("TG_CLI_API_HASH");
     662            2 :     if (saved_home[0]) setenv("HOME", saved_home, 1);
     663            0 :     else unsetenv("HOME");
     664            2 :     (void)unlink(ini);
     665              : }
     666              : 
     667            2 : void run_login_flow_tests(void) {
     668            2 :     RUN_TEST(test_send_code_happy);
     669            2 :     RUN_TEST(test_send_code_invalid_phone);
     670            2 :     RUN_TEST(test_send_code_phone_migrate);
     671            2 :     RUN_TEST(test_sign_in_happy);
     672            2 :     RUN_TEST(test_sign_in_sign_up_required);
     673            2 :     RUN_TEST(test_sign_in_password_needed);
     674            2 :     RUN_TEST(test_2fa_get_password);
     675            2 :     RUN_TEST(test_2fa_check_password_wrong);
     676            2 :     RUN_TEST(test_2fa_check_password_accept);
     677            2 :     RUN_TEST(test_bad_server_salt_retry);
     678            2 :     RUN_TEST(test_session_persistence_roundtrip);
     679            2 :     RUN_TEST(test_logout_clears_session);
     680            2 :     RUN_TEST(test_batch_rejects_missing_credentials);
     681            2 :     RUN_TEST(test_credentials_env_override);
     682            2 : }
        

Generated by: LCOV version 2.0-1