LCOV - code coverage report
Current view: top level - tests/functional - test_auth_flow_errors.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 98.7 % 308 304
Test Date: 2026-04-20 19:54:22 Functions: 95.5 % 44 42

            Line data    Source code
       1              : /* SPDX-License-Identifier: GPL-3.0-or-later */
       2              : /* Copyright 2026 Peter Csaszar */
       3              : 
       4              : /**
       5              :  * @file test_auth_flow_errors.c
       6              :  * @brief TEST-74 / US-23 — functional coverage for login-failure
       7              :  *        error paths in auth_flow.c / auth_session.c / auth_2fa.c.
       8              :  *
       9              :  * The happy path of login is covered by test_login_flow.c; the migrate
      10              :  * branches by test_login_migrate.c.  This suite fills in the long tail
      11              :  * of realistic Telegram failure modes enumerated in US-23:
      12              :  *
      13              :  *   | server reply                  | asserted behaviour                 |
      14              :  *   |-------------------------------|------------------------------------|
      15              :  *   | 400 PHONE_NUMBER_INVALID      | auth_send_code rc=-1, err populated|
      16              :  *   | 400 PHONE_NUMBER_BANNED       | same                               |
      17              :  *   | 400 PHONE_CODE_INVALID        | auth_sign_in rc=-1, err populated  |
      18              :  *   | 400 PHONE_CODE_EXPIRED        | same                               |
      19              :  *   | 400 PHONE_CODE_EMPTY          | same                               |
      20              :  *   | 401 SESSION_PASSWORD_NEEDED   | signals 2FA switch via err         |
      21              :  *   | 400 PASSWORD_HASH_INVALID     | auth_2fa_check_password rc=-1      |
      22              :  *   | 420 FLOOD_WAIT_30             | rpc_parse_error fills flood_wait   |
      23              :  *   | 401 AUTH_RESTART              | rc=-1, err with msg AUTH_RESTART   |
      24              :  *   | 500 SIGN_IN_FAILED            | rc=-1, err with msg SIGN_IN_FAILED |
      25              :  *   | 400 PHONE_NUMBER_FLOOD        | rc=-1, err populated               |
      26              :  *   | 401 SESSION_REVOKED           | rc=-1, err populated               |
      27              :  *
      28              :  * For every fatal path the suite also asserts that no side effect was
      29              :  * committed to the persistent session file (the seeded entry on DC2
      30              :  * remains the only entry, no stray home-DC promotion or new entry).
      31              :  *
      32              :  * On top of the error-string matrix the suite adds two auth_flow.c
      33              :  * end-to-end tests (fast-path success / fast-path dc_lookup NULL) so
      34              :  * the top-level orchestrator actually runs in functional coverage —
      35              :  * the existing suites touched it only through test_batch_rejects_*.
      36              :  */
      37              : 
      38              : #include "test_helpers.h"
      39              : 
      40              : #include "mock_socket.h"
      41              : #include "mock_tel_server.h"
      42              : 
      43              : #include "api_call.h"
      44              : #include "auth_session.h"
      45              : #include "infrastructure/auth_2fa.h"
      46              : #include "mtproto_rpc.h"
      47              : #include "mtproto_session.h"
      48              : #include "transport.h"
      49              : #include "app/auth_flow.h"
      50              : #include "app/session_store.h"
      51              : #include "app/credentials.h"
      52              : #include "app/dc_config.h"
      53              : #include "tl_registry.h"
      54              : #include "tl_serial.h"
      55              : 
      56              : #include <fcntl.h>
      57              : #include <stdio.h>
      58              : #include <stdlib.h>
      59              : #include <string.h>
      60              : #include <sys/stat.h>
      61              : #include <unistd.h>
      62              : 
      63              : /* CRC kept local to this TU to avoid pulling private headers. */
      64              : #define CRC_sentCodeTypeSms         0xc000bba2U
      65              : #define CRC_account_getPassword     0x548a30f5U
      66              : #define CRC_KdfAlgoPBKDF2           0x3a912d4aU
      67              : #define CRC_auth_checkPassword      0xd18b4d16U
      68              : 
      69              : /* ================================================================ */
      70              : /* Helpers                                                          */
      71              : /* ================================================================ */
      72              : 
      73           36 : static void with_tmp_home(const char *tag) {
      74              :     char tmp[256];
      75           36 :     snprintf(tmp, sizeof(tmp), "/tmp/tg-cli-ft-auth-err-%s", tag);
      76              :     char cfg_dir[512];
      77           36 :     snprintf(cfg_dir, sizeof(cfg_dir), "%s/.config/tg-cli", tmp);
      78           36 :     (void)mkdir(tmp, 0700);
      79              :     char parent[512];
      80           36 :     snprintf(parent, sizeof(parent), "%s/.config", tmp);
      81           36 :     (void)mkdir(parent, 0700);
      82           36 :     (void)mkdir(cfg_dir, 0700);
      83              :     char bin[600];
      84           36 :     snprintf(bin, sizeof(bin), "%s/session.bin", cfg_dir);
      85           36 :     (void)unlink(bin);
      86           36 :     setenv("HOME", tmp, 1);
      87              :     /* CI runners export these — clear so platform_config_dir() derives
      88              :      * from $HOME. */
      89           36 :     unsetenv("XDG_CONFIG_HOME");
      90           36 :     unsetenv("XDG_CACHE_HOME");
      91           36 : }
      92              : 
      93           34 : static void init_cfg(ApiConfig *cfg) {
      94           34 :     api_config_init(cfg);
      95           34 :     cfg->api_id = 12345;
      96           34 :     cfg->api_hash = "deadbeefcafebabef00dbaadfeedc0de";
      97           34 : }
      98              : 
      99           30 : static void connect_mock(Transport *t) {
     100           30 :     transport_init(t);
     101           30 :     ASSERT(transport_connect(t, "127.0.0.1", 443) == 0, "transport connects");
     102              : }
     103              : 
     104              : /* Post-fatal-RPC check: reloading the session store must still return
     105              :  * the exact seed we planted (DC2) and MUST NOT have been promoted to a
     106              :  * different home DC or gained extra entries during the failing RPC. */
     107           28 : static void assert_session_not_mutated(void) {
     108           28 :     MtProtoSession r; mtproto_session_init(&r);
     109           28 :     int home = 0;
     110           28 :     ASSERT(session_store_load(&r, &home) == 0,
     111              :            "seeded session file still readable");
     112           28 :     ASSERT(home == 2,
     113              :            "home DC unchanged at 2 after fatal RPC");
     114              : }
     115              : 
     116              : /* ---- Deterministic SRP fixture (mirrors test_login_flow.c) ---- */
     117              : 
     118              : static uint8_t g_test_salt1[16];
     119              : static uint8_t g_test_salt2[16];
     120              : static uint8_t g_test_prime[256];
     121              : static uint8_t g_test_srpB[256];
     122              : 
     123            4 : static void init_srp_fixture(void) {
     124           68 :     for (int i = 0; i < 16;  ++i) g_test_salt1[i] = (uint8_t)(i + 1);
     125           68 :     for (int i = 0; i < 16;  ++i) g_test_salt2[i] = (uint8_t)(i + 17);
     126         1028 :     for (int i = 0; i < 256; ++i) g_test_prime[i] = (uint8_t)((i * 7 + 3) | 0x80u);
     127            4 :     g_test_prime[0] = 0xC7;
     128            4 :     g_test_prime[255] = 0x7F;
     129         1028 :     for (int i = 0; i < 256; ++i) g_test_srpB[i] = (uint8_t)((i * 5 + 11));
     130            4 :     g_test_srpB[0] = 0x01;
     131            4 : }
     132              : 
     133              : /* ================================================================ */
     134              : /* Responders — one per Telegram error string                       */
     135              : /* ================================================================ */
     136              : 
     137              : /* auth.sendCode error responders ------------------------------------ */
     138              : 
     139            2 : static void on_phone_number_invalid(MtRpcContext *ctx) {
     140            2 :     mt_server_reply_error(ctx, 400, "PHONE_NUMBER_INVALID");
     141            2 : }
     142            2 : static void on_phone_number_banned(MtRpcContext *ctx) {
     143            2 :     mt_server_reply_error(ctx, 400, "PHONE_NUMBER_BANNED");
     144            2 : }
     145            2 : static void on_phone_number_flood(MtRpcContext *ctx) {
     146            2 :     mt_server_reply_error(ctx, 400, "PHONE_NUMBER_FLOOD");
     147            2 : }
     148            2 : static void on_send_code_flood_wait_30(MtRpcContext *ctx) {
     149            2 :     mt_server_reply_error(ctx, 420, "FLOOD_WAIT_30");
     150            2 : }
     151            2 : static void on_auth_restart(MtRpcContext *ctx) {
     152            2 :     mt_server_reply_error(ctx, 401, "AUTH_RESTART");
     153            2 : }
     154              : 
     155              : /* auth.signIn error responders -------------------------------------- */
     156              : 
     157            2 : static void on_phone_code_invalid(MtRpcContext *ctx) {
     158            2 :     mt_server_reply_error(ctx, 400, "PHONE_CODE_INVALID");
     159            2 : }
     160            2 : static void on_phone_code_expired(MtRpcContext *ctx) {
     161            2 :     mt_server_reply_error(ctx, 400, "PHONE_CODE_EXPIRED");
     162            2 : }
     163            2 : static void on_phone_code_empty(MtRpcContext *ctx) {
     164            2 :     mt_server_reply_error(ctx, 400, "PHONE_CODE_EMPTY");
     165            2 : }
     166            2 : static void on_session_password_needed(MtRpcContext *ctx) {
     167            2 :     mt_server_reply_error(ctx, 401, "SESSION_PASSWORD_NEEDED");
     168            2 : }
     169            2 : static void on_session_revoked(MtRpcContext *ctx) {
     170            2 :     mt_server_reply_error(ctx, 401, "SESSION_REVOKED");
     171            2 : }
     172            2 : static void on_sign_in_failed(MtRpcContext *ctx) {
     173            2 :     mt_server_reply_error(ctx, 500, "SIGN_IN_FAILED");
     174            2 : }
     175            2 : static void on_sign_in_flood_wait_60(MtRpcContext *ctx) {
     176            2 :     mt_server_reply_error(ctx, 420, "FLOOD_WAIT_60");
     177            2 : }
     178              : 
     179              : /* 2FA (auth.checkPassword) error responders ------------------------- */
     180              : 
     181            2 : static void on_password_hash_invalid(MtRpcContext *ctx) {
     182            2 :     mt_server_reply_error(ctx, 400, "PASSWORD_HASH_INVALID");
     183            2 : }
     184            2 : static void on_srp_id_invalid(MtRpcContext *ctx) {
     185            2 :     mt_server_reply_error(ctx, 400, "SRP_ID_INVALID");
     186            2 : }
     187              : 
     188              : /* account.password responder reused across 2FA tests. */
     189            4 : static void on_get_password(MtRpcContext *ctx) {
     190              :     TlWriter w;
     191            4 :     tl_writer_init(&w);
     192            4 :     tl_write_uint32(&w, TL_account_password);
     193            4 :     tl_write_uint32(&w, 1u << 2);                         /* flags: has_password */
     194            4 :     tl_write_uint32(&w, CRC_KdfAlgoPBKDF2);
     195            4 :     tl_write_bytes (&w, g_test_salt1, sizeof(g_test_salt1));
     196            4 :     tl_write_bytes (&w, g_test_salt2, sizeof(g_test_salt2));
     197            4 :     tl_write_int32 (&w, 2);                               /* g = 2 */
     198            4 :     tl_write_bytes (&w, g_test_prime, sizeof(g_test_prime));
     199            4 :     tl_write_bytes (&w, g_test_srpB,  sizeof(g_test_srpB));
     200            4 :     tl_write_int64 (&w, 0x1234567890ABCDEFLL);
     201            4 :     mt_server_reply_result(ctx, w.data, w.len);
     202            4 :     tl_writer_free(&w);
     203            4 : }
     204              : 
     205              : /* account.password that advertises has_password=0 — drives the
     206              :  * "server says no 2FA set" early-exit in auth_2fa_check_password. */
     207            2 : static void on_get_password_no_password(MtRpcContext *ctx) {
     208              :     TlWriter w;
     209            2 :     tl_writer_init(&w);
     210            2 :     tl_write_uint32(&w, TL_account_password);
     211            2 :     tl_write_uint32(&w, 0);       /* flags = 0 → has_password bit not set */
     212            2 :     mt_server_reply_result(ctx, w.data, w.len);
     213            2 :     tl_writer_free(&w);
     214            2 : }
     215              : 
     216              : /* ================================================================ */
     217              : /* Test helpers — scaffolding shared across the per-error cases     */
     218              : /* ================================================================ */
     219              : 
     220              : /*
     221              :  * Run one auth.sendCode round-trip against @p responder and assert the
     222              :  * error surface: api returns -1, the RpcError is populated, and the
     223              :  * persisted session file is unchanged.  This captures the US-23 ask
     224              :  * ("stderr includes the human message" translates, at the auth_session
     225              :  * layer boundary, to err.error_msg == server_msg).
     226              :  */
     227           10 : static void drive_send_code_error(const char *tag, const char *phone,
     228              :                                    MtResponder responder,
     229              :                                    int expected_code,
     230              :                                    const char *expected_msg,
     231              :                                    int expected_flood_wait) {
     232           10 :     with_tmp_home(tag);
     233           10 :     mt_server_init();
     234           10 :     mt_server_reset();
     235           10 :     ASSERT(mt_server_seed_session(2, NULL, NULL, NULL) == 0, "seed");
     236           10 :     mt_server_expect(CRC_auth_sendCode, responder, NULL);
     237              : 
     238           10 :     ApiConfig cfg; init_cfg(&cfg);
     239           10 :     MtProtoSession s; mtproto_session_init(&s);
     240           10 :     int dc = 0; ASSERT(session_store_load(&s, &dc) == 0, "load");
     241              : 
     242           10 :     Transport t; connect_mock(&t);
     243              : 
     244           10 :     AuthSentCode sent = {0};
     245           10 :     RpcError err = {0};
     246           10 :     ASSERT(auth_send_code(&cfg, &s, &t, phone, &sent, &err) == -1,
     247              :            "auth_send_code returns -1 on rpc_error");
     248           10 :     ASSERT(err.error_code == expected_code,
     249              :            "error_code matches server reply");
     250           10 :     ASSERT(strcmp(err.error_msg, expected_msg) == 0,
     251              :            "error_msg matches server reply");
     252           10 :     ASSERT(err.flood_wait_secs == expected_flood_wait,
     253              :            "flood_wait_secs correct (0 unless FLOOD_WAIT_*)");
     254              :     /* migrate_dc must be -1 for non-migrate errors; rpc_parse_error only
     255              :      * sets it for PHONE/USER/NETWORK/FILE_MIGRATE_X strings. */
     256           10 :     ASSERT(err.migrate_dc == -1,
     257              :            "migrate_dc left at -1 for non-migrate errors");
     258              :     /* phone_code_hash must remain the zero-initialised sentinel — the
     259              :      * failing RPC must not have written stale data into out->phone_code_hash. */
     260           10 :     ASSERT(sent.phone_code_hash[0] == '\0',
     261              :            "phone_code_hash untouched on error");
     262              : 
     263           10 :     assert_session_not_mutated();
     264              : 
     265           10 :     transport_close(&t);
     266           10 :     mt_server_reset();
     267              : }
     268              : 
     269              : /*
     270              :  * auth.signIn equivalent — feeds the happy sendCode first, then the
     271              :  * failing signIn responder.  Asserts error surface + session not mutated.
     272              :  */
     273           14 : static void drive_sign_in_error(const char *tag,
     274              :                                   MtResponder responder,
     275              :                                   int expected_code,
     276              :                                   const char *expected_msg,
     277              :                                   int expected_flood_wait) {
     278           14 :     with_tmp_home(tag);
     279           14 :     mt_server_init();
     280           14 :     mt_server_reset();
     281           14 :     ASSERT(mt_server_seed_session(2, NULL, NULL, NULL) == 0, "seed");
     282           14 :     mt_server_expect(CRC_auth_signIn, responder, NULL);
     283              : 
     284           14 :     ApiConfig cfg; init_cfg(&cfg);
     285           14 :     MtProtoSession s; mtproto_session_init(&s);
     286           14 :     int dc = 0; ASSERT(session_store_load(&s, &dc) == 0, "load");
     287              : 
     288           14 :     Transport t; connect_mock(&t);
     289              : 
     290           14 :     int64_t uid = 0xDEADBEEFLL; /* sentinel — must remain untouched on failure */
     291           14 :     RpcError err = {0};
     292           14 :     ASSERT(auth_sign_in(&cfg, &s, &t, "+15551234567", "abc123", "12345",
     293              :                         &uid, &err) == -1,
     294              :            "auth_sign_in returns -1 on rpc_error");
     295           14 :     ASSERT(err.error_code == expected_code,
     296              :            "error_code matches server reply");
     297           14 :     ASSERT(strcmp(err.error_msg, expected_msg) == 0,
     298              :            "error_msg matches server reply");
     299           14 :     ASSERT(err.flood_wait_secs == expected_flood_wait,
     300              :            "flood_wait_secs correct");
     301           14 :     ASSERT(err.migrate_dc == -1,
     302              :            "migrate_dc left at -1 for non-migrate errors");
     303           14 :     ASSERT(uid == 0xDEADBEEFLL,
     304              :            "user_id_out untouched on signIn failure");
     305              : 
     306           14 :     assert_session_not_mutated();
     307              : 
     308           14 :     transport_close(&t);
     309           14 :     mt_server_reset();
     310              : }
     311              : 
     312              : /* ================================================================ */
     313              : /* auth.sendCode error cases                                         */
     314              : /* ================================================================ */
     315              : 
     316            2 : static void test_phone_number_invalid(void) {
     317            2 :     drive_send_code_error("phone-invalid", "+00000000",
     318              :                           on_phone_number_invalid,
     319              :                           400, "PHONE_NUMBER_INVALID", 0);
     320            2 : }
     321              : 
     322            2 : static void test_phone_number_banned(void) {
     323            2 :     drive_send_code_error("phone-banned", "+15551112222",
     324              :                           on_phone_number_banned,
     325              :                           400, "PHONE_NUMBER_BANNED", 0);
     326            2 : }
     327              : 
     328            2 : static void test_phone_number_flood(void) {
     329            2 :     drive_send_code_error("phone-flood", "+15551112222",
     330              :                           on_phone_number_flood,
     331              :                           400, "PHONE_NUMBER_FLOOD", 0);
     332            2 : }
     333              : 
     334            2 : static void test_send_code_flood_wait(void) {
     335              :     /* FLOOD_WAIT_30: rpc_parse_error must split off the trailing number
     336              :      * and populate err.flood_wait_secs = 30 alongside the raw message. */
     337            2 :     drive_send_code_error("flood-wait-30", "+15551112222",
     338              :                           on_send_code_flood_wait_30,
     339              :                           420, "FLOOD_WAIT_30", 30);
     340            2 : }
     341              : 
     342            2 : static void test_auth_restart(void) {
     343              :     /* AUTH_RESTART isn't a migrate error — it asks the caller to restart
     344              :      * the whole login flow from the phone prompt. At the auth_session
     345              :      * level it surfaces as a plain rpc_error just like any other. */
     346            2 :     drive_send_code_error("auth-restart", "+15551112222",
     347              :                           on_auth_restart,
     348              :                           401, "AUTH_RESTART", 0);
     349            2 : }
     350              : 
     351              : /* ================================================================ */
     352              : /* auth.signIn error cases                                           */
     353              : /* ================================================================ */
     354              : 
     355            2 : static void test_phone_code_invalid(void) {
     356            2 :     drive_sign_in_error("code-invalid", on_phone_code_invalid,
     357              :                         400, "PHONE_CODE_INVALID", 0);
     358            2 : }
     359              : 
     360            2 : static void test_phone_code_expired(void) {
     361            2 :     drive_sign_in_error("code-expired", on_phone_code_expired,
     362              :                         400, "PHONE_CODE_EXPIRED", 0);
     363            2 : }
     364              : 
     365            2 : static void test_phone_code_empty(void) {
     366            2 :     drive_sign_in_error("code-empty", on_phone_code_empty,
     367              :                         400, "PHONE_CODE_EMPTY", 0);
     368            2 : }
     369              : 
     370            2 : static void test_session_password_needed(void) {
     371              :     /* Signals 2FA — surfaces as rpc_error. auth_flow_login observes
     372              :      * err.error_msg=="SESSION_PASSWORD_NEEDED" and switches to the
     373              :      * getPassword + checkPassword path.  We assert the signal reaches
     374              :      * the caller unchanged; the switch itself is exercised in
     375              :      * test_login_flow.c's 2FA cases. */
     376            2 :     drive_sign_in_error("password-needed", on_session_password_needed,
     377              :                         401, "SESSION_PASSWORD_NEEDED", 0);
     378            2 : }
     379              : 
     380            2 : static void test_session_revoked(void) {
     381            2 :     drive_sign_in_error("session-revoked", on_session_revoked,
     382              :                         401, "SESSION_REVOKED", 0);
     383            2 : }
     384              : 
     385            2 : static void test_sign_in_failed_generic(void) {
     386            2 :     drive_sign_in_error("sign-in-failed", on_sign_in_failed,
     387              :                         500, "SIGN_IN_FAILED", 0);
     388            2 : }
     389              : 
     390            2 : static void test_sign_in_flood_wait(void) {
     391            2 :     drive_sign_in_error("signin-flood", on_sign_in_flood_wait_60,
     392              :                         420, "FLOOD_WAIT_60", 60);
     393            2 : }
     394              : 
     395              : /* ================================================================ */
     396              : /* auth.checkPassword (2FA) error cases                              */
     397              : /* ================================================================ */
     398              : 
     399            2 : static void test_password_hash_invalid(void) {
     400            2 :     with_tmp_home("pwd-hash-invalid");
     401            2 :     mt_server_init();
     402            2 :     mt_server_reset();
     403            2 :     init_srp_fixture();
     404            2 :     ASSERT(mt_server_seed_session(2, NULL, NULL, NULL) == 0, "seed");
     405            2 :     mt_server_expect(CRC_account_getPassword, on_get_password, NULL);
     406            2 :     mt_server_expect(CRC_auth_checkPassword, on_password_hash_invalid, NULL);
     407              : 
     408            2 :     ApiConfig cfg; init_cfg(&cfg);
     409            2 :     MtProtoSession s; mtproto_session_init(&s);
     410            2 :     int dc = 0; ASSERT(session_store_load(&s, &dc) == 0, "load");
     411              : 
     412            2 :     Transport t; connect_mock(&t);
     413              : 
     414            2 :     Account2faPassword params = {0};
     415            2 :     RpcError gp_err = {0};
     416            2 :     ASSERT(auth_2fa_get_password(&cfg, &s, &t, &params, &gp_err) == 0,
     417              :            "account.getPassword succeeds");
     418              : 
     419            2 :     int64_t uid = 0xCAFEBABELL;
     420            2 :     RpcError cp_err = {0};
     421            2 :     ASSERT(auth_2fa_check_password(&cfg, &s, &t, &params, "definitely-wrong",
     422              :                                     &uid, &cp_err) == -1,
     423              :            "auth_2fa_check_password returns -1 on wrong password");
     424            2 :     ASSERT(cp_err.error_code == 400, "error_code 400");
     425            2 :     ASSERT(strcmp(cp_err.error_msg, "PASSWORD_HASH_INVALID") == 0,
     426              :            "error_msg PASSWORD_HASH_INVALID");
     427            2 :     ASSERT(uid == 0xCAFEBABELL, "user_id_out untouched on wrong password");
     428              : 
     429            2 :     assert_session_not_mutated();
     430              : 
     431            2 :     transport_close(&t);
     432            2 :     mt_server_reset();
     433              : }
     434              : 
     435            2 : static void test_srp_id_invalid(void) {
     436              :     /* SRP_ID_INVALID is raised when the srp_id the client replays has
     437              :      * expired server-side (a stale getPassword). Assert it surfaces
     438              :      * cleanly via RpcError rather than crashing the SRP math. */
     439            2 :     with_tmp_home("srp-id-invalid");
     440            2 :     mt_server_init();
     441            2 :     mt_server_reset();
     442            2 :     init_srp_fixture();
     443            2 :     ASSERT(mt_server_seed_session(2, NULL, NULL, NULL) == 0, "seed");
     444            2 :     mt_server_expect(CRC_account_getPassword, on_get_password, NULL);
     445            2 :     mt_server_expect(CRC_auth_checkPassword, on_srp_id_invalid, NULL);
     446              : 
     447            2 :     ApiConfig cfg; init_cfg(&cfg);
     448            2 :     MtProtoSession s; mtproto_session_init(&s);
     449            2 :     int dc = 0; ASSERT(session_store_load(&s, &dc) == 0, "load");
     450              : 
     451            2 :     Transport t; connect_mock(&t);
     452              : 
     453            2 :     Account2faPassword params = {0};
     454            2 :     RpcError gp_err = {0};
     455            2 :     ASSERT(auth_2fa_get_password(&cfg, &s, &t, &params, &gp_err) == 0,
     456              :            "getPassword ok");
     457              : 
     458            2 :     int64_t uid = 0;
     459            2 :     RpcError cp_err = {0};
     460            2 :     ASSERT(auth_2fa_check_password(&cfg, &s, &t, &params, "secret",
     461              :                                     &uid, &cp_err) == -1,
     462              :            "checkPassword fails with SRP_ID_INVALID");
     463            2 :     ASSERT(cp_err.error_code == 400, "error_code 400");
     464            2 :     ASSERT(strcmp(cp_err.error_msg, "SRP_ID_INVALID") == 0,
     465              :            "error_msg SRP_ID_INVALID");
     466              : 
     467            2 :     assert_session_not_mutated();
     468              : 
     469            2 :     transport_close(&t);
     470            2 :     mt_server_reset();
     471              : }
     472              : 
     473            2 : static void test_check_password_with_no_password_configured(void) {
     474              :     /* auth_2fa_check_password guards against being called on params that
     475              :      * report has_password=0 (server claims no 2FA set). Assert the guard
     476              :      * fires before the SRP math (no RPC attempted). */
     477            2 :     with_tmp_home("pwd-no-config");
     478            2 :     mt_server_init();
     479            2 :     mt_server_reset();
     480            2 :     ASSERT(mt_server_seed_session(2, NULL, NULL, NULL) == 0, "seed");
     481            2 :     mt_server_expect(CRC_account_getPassword, on_get_password_no_password, NULL);
     482              : 
     483            2 :     ApiConfig cfg; init_cfg(&cfg);
     484            2 :     MtProtoSession s; mtproto_session_init(&s);
     485            2 :     int dc = 0; ASSERT(session_store_load(&s, &dc) == 0, "load");
     486              : 
     487            2 :     Transport t; connect_mock(&t);
     488              : 
     489            2 :     Account2faPassword params = {0};
     490            2 :     RpcError gp_err = {0};
     491            2 :     ASSERT(auth_2fa_get_password(&cfg, &s, &t, &params, &gp_err) == 0,
     492              :            "getPassword ok (has_password=0)");
     493            2 :     ASSERT(params.has_password == 0,
     494              :            "params reflect 'no 2FA on account'");
     495              : 
     496            2 :     int calls_before = mt_server_rpc_call_count();
     497              : 
     498            2 :     int64_t uid = 0;
     499            2 :     RpcError cp_err = {0};
     500            2 :     ASSERT(auth_2fa_check_password(&cfg, &s, &t, &params, "any",
     501              :                                     &uid, &cp_err) == -1,
     502              :            "checkPassword refuses to run when has_password=0");
     503              :     /* Guard fires before api_call — counter must not have incremented. */
     504            2 :     ASSERT(mt_server_rpc_call_count() == calls_before,
     505              :            "checkPassword short-circuited before dispatching RPC");
     506              : 
     507            2 :     transport_close(&t);
     508            2 :     mt_server_reset();
     509              : }
     510              : 
     511              : /* ================================================================ */
     512              : /* auth_flow.c orchestrator — fast path + pre-RPC failure           */
     513              : /* ================================================================ */
     514              : 
     515              : /*
     516              :  * Minimal callback triad that signals "callbacks not available".
     517              :  * auth_flow_login's fast path returns before these fire; use the ones
     518              :  * that return -1 so a regression that skipped the fast path would
     519              :  * surface as a clear failure rather than a hang on stdin.
     520              :  */
     521            0 : static int cb_no_phone(void *u, char *out, size_t cap) {
     522            0 :     (void)u; (void)out; (void)cap; return -1;
     523              : }
     524            0 : static int cb_no_code(void *u, char *out, size_t cap) {
     525            0 :     (void)u; (void)out; (void)cap; return -1;
     526              : }
     527              : 
     528              : /* Assert auth_flow_login returns 0 on a seeded-session fast path and
     529              :  * populates AuthFlowResult with the seeded home DC. Covers lines 70-85
     530              :  * of auth_flow.c (the session-restore fast path). */
     531            2 : static void test_auth_flow_login_fast_path_succeeds(void) {
     532            2 :     with_tmp_home("fast-path");
     533            2 :     mt_server_init();
     534            2 :     mt_server_reset();
     535            2 :     ASSERT(mt_server_seed_session(2, NULL, NULL, NULL) == 0, "seed DC2");
     536              : 
     537            2 :     ApiConfig cfg; init_cfg(&cfg);
     538            2 :     Transport t; transport_init(&t);
     539            2 :     MtProtoSession s; mtproto_session_init(&s);
     540              : 
     541            2 :     AuthFlowCallbacks cb = {
     542              :         .get_phone    = cb_no_phone,
     543              :         .get_code     = cb_no_code,
     544              :         .get_password = NULL,
     545              :         .user         = NULL,
     546              :     };
     547              : 
     548            2 :     AuthFlowResult result = {0};
     549            2 :     int rc = auth_flow_login(&cfg, &cb, &t, &s, &result);
     550            2 :     ASSERT(rc == 0,
     551              :            "auth_flow_login takes fast path when session.bin has auth key");
     552            2 :     ASSERT(result.dc_id == 2,
     553              :            "AuthFlowResult reports the persisted home DC");
     554            2 :     ASSERT(s.has_auth_key == 1,
     555              :            "session populated with auth_key from session.bin");
     556              : 
     557            2 :     transport_close(&t);
     558            2 :     mt_server_reset();
     559              : }
     560              : 
     561              : /* auth_flow_login param-validation guard: missing callbacks → -1.
     562              :  * Covers lines 64-68. */
     563            2 : static void test_auth_flow_login_missing_callbacks_rejected(void) {
     564            2 :     with_tmp_home("no-cb");
     565            2 :     mt_server_init();
     566            2 :     mt_server_reset();
     567              : 
     568            2 :     ApiConfig cfg; init_cfg(&cfg);
     569            2 :     Transport t; transport_init(&t);
     570            2 :     MtProtoSession s; mtproto_session_init(&s);
     571              : 
     572            2 :     AuthFlowCallbacks cb_missing_phone = {
     573              :         .get_phone = NULL, .get_code = cb_no_code,
     574              :         .get_password = NULL, .user = NULL,
     575              :     };
     576            2 :     ASSERT(auth_flow_login(&cfg, &cb_missing_phone, &t, &s, NULL) == -1,
     577              :            "auth_flow_login rejects callbacks with NULL get_phone");
     578              : 
     579            2 :     AuthFlowCallbacks cb_missing_code = {
     580              :         .get_phone = cb_no_phone, .get_code = NULL,
     581              :         .get_password = NULL, .user = NULL,
     582              :     };
     583            2 :     ASSERT(auth_flow_login(&cfg, &cb_missing_code, &t, &s, NULL) == -1,
     584              :            "auth_flow_login rejects callbacks with NULL get_code");
     585              : 
     586            2 :     ASSERT(auth_flow_login(NULL, &cb_missing_code, &t, &s, NULL) == -1,
     587              :            "auth_flow_login rejects NULL cfg");
     588            2 :     ASSERT(auth_flow_login(&cfg, NULL, &t, &s, NULL) == -1,
     589              :            "auth_flow_login rejects NULL cb");
     590            2 :     ASSERT(auth_flow_login(&cfg, &cb_missing_phone, NULL, &s, NULL) == -1,
     591              :            "auth_flow_login rejects NULL transport");
     592            2 :     ASSERT(auth_flow_login(&cfg, &cb_missing_phone, &t, NULL, NULL) == -1,
     593              :            "auth_flow_login rejects NULL session");
     594              : 
     595            2 :     transport_close(&t);
     596            2 :     mt_server_reset();
     597              : }
     598              : 
     599              : /* auth_flow_connect_dc rejects unknown DC IDs (covers the dc_lookup
     600              :  * NULL branch at lines 28-32). */
     601            2 : static void test_auth_flow_connect_dc_unknown_id(void) {
     602            2 :     with_tmp_home("connect-dc-unknown");
     603              : 
     604            2 :     Transport t; transport_init(&t);
     605            2 :     MtProtoSession s; mtproto_session_init(&s);
     606              : 
     607            2 :     ASSERT(auth_flow_connect_dc(999, &t, &s) == -1,
     608              :            "auth_flow_connect_dc rejects unknown DC id");
     609              : 
     610              :     /* NULL-guards. */
     611            2 :     ASSERT(auth_flow_connect_dc(2, NULL, &s) == -1,
     612              :            "auth_flow_connect_dc rejects NULL transport");
     613            2 :     ASSERT(auth_flow_connect_dc(2, &t, NULL) == -1,
     614              :            "auth_flow_connect_dc rejects NULL session");
     615              : }
     616              : 
     617              : /* ================================================================ */
     618              : /* Suite entry point                                                 */
     619              : /* ================================================================ */
     620              : 
     621            2 : void run_auth_flow_errors_tests(void) {
     622              :     /* auth.sendCode error surface */
     623            2 :     RUN_TEST(test_phone_number_invalid);
     624            2 :     RUN_TEST(test_phone_number_banned);
     625            2 :     RUN_TEST(test_phone_number_flood);
     626            2 :     RUN_TEST(test_send_code_flood_wait);
     627            2 :     RUN_TEST(test_auth_restart);
     628              :     /* auth.signIn error surface */
     629            2 :     RUN_TEST(test_phone_code_invalid);
     630            2 :     RUN_TEST(test_phone_code_expired);
     631            2 :     RUN_TEST(test_phone_code_empty);
     632            2 :     RUN_TEST(test_session_password_needed);
     633            2 :     RUN_TEST(test_session_revoked);
     634            2 :     RUN_TEST(test_sign_in_failed_generic);
     635            2 :     RUN_TEST(test_sign_in_flood_wait);
     636              :     /* 2FA error surface */
     637            2 :     RUN_TEST(test_password_hash_invalid);
     638            2 :     RUN_TEST(test_srp_id_invalid);
     639            2 :     RUN_TEST(test_check_password_with_no_password_configured);
     640              :     /* auth_flow.c orchestrator */
     641            2 :     RUN_TEST(test_auth_flow_login_fast_path_succeeds);
     642            2 :     RUN_TEST(test_auth_flow_login_missing_callbacks_rejected);
     643            2 :     RUN_TEST(test_auth_flow_connect_dc_unknown_id);
     644            2 : }
        

Generated by: LCOV version 2.0-1