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