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