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, ¶ms, &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, ¶ms, "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, ¶ms, &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, ¶ms, "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, ¶ms, &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, ¶ms, "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 : }
|