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

Generated by: LCOV version 2.0-1