Line data Source code
1 : /* SPDX-License-Identifier: GPL-3.0-or-later */
2 : /* Copyright 2026 Peter Csaszar */
3 :
4 : /**
5 : * @file test_login_migrate.c
6 : * @brief TEST-86 / US-35 — functional coverage for PHONE_MIGRATE and
7 : * USER_MIGRATE during the first-time login flow.
8 : *
9 : * rpc_parse_error already parses `PHONE_MIGRATE_<dc>`,
10 : * `USER_MIGRATE_<dc>`, and `NETWORK_MIGRATE_<dc>` into
11 : * err.migrate_dc. These functional tests drive a real login against
12 : * the in-process mock Telegram server so every migrate branch is
13 : * exercised end-to-end (TL framing + IGE/AES + rpc_parse_error +
14 : * auth_session consumer) rather than only through a unit test on
15 : * rpc_parse_error.
16 : *
17 : * Scenarios:
18 : * 1. test_phone_migrate_first_send_code_switches_home_dc —
19 : * auth.sendCode on DC2 replies PHONE_MIGRATE_4; the retry on DC4
20 : * succeeds and session.bin's home_dc becomes 4.
21 : * 2. test_user_migrate_after_sign_in_switches_home_dc —
22 : * auth.signIn on DC2 replies USER_MIGRATE_5; the retry on DC5
23 : * succeeds and session.bin's home_dc becomes 5.
24 : * 3. test_network_migrate_is_per_rpc_not_home —
25 : * auth.sendCode replies NETWORK_MIGRATE_3; only the failing RPC
26 : * retries on DC3, home DC stays unchanged at 2.
27 : * 4. test_ghost_migrate_loop_bails_at_3_hops —
28 : * auth.sendCode keeps replying PHONE_MIGRATE_<n> even after each
29 : * hop; the login flow gives up after AUTH_MAX_MIGRATIONS (3) hops
30 : * with a clear failure state rather than spinning forever.
31 : */
32 :
33 : #include "test_helpers.h"
34 :
35 : #include "mock_socket.h"
36 : #include "mock_tel_server.h"
37 :
38 : #include "api_call.h"
39 : #include "auth_session.h"
40 : #include "mtproto_rpc.h"
41 : #include "mtproto_session.h"
42 : #include "transport.h"
43 : #include "tl_registry.h"
44 : #include "tl_serial.h"
45 : #include "app/session_store.h"
46 : #include "app/credentials.h"
47 :
48 : #include <stdio.h>
49 : #include <stdlib.h>
50 : #include <string.h>
51 : #include <sys/stat.h>
52 : #include <unistd.h>
53 :
54 : /* CRC for sentCodeTypeSms — used by the happy-path sendCode reply.
55 : * Duplicated here so this suite does not reach into private headers
56 : * beyond auth_session.h. */
57 : #define CRC_sentCodeTypeSms 0xc000bba2U
58 :
59 : /* Match the cap in src/app/auth_flow.c (AUTH_MAX_MIGRATIONS). */
60 : #define LOCAL_MAX_MIGRATIONS 3
61 :
62 : /* ================================================================ */
63 : /* Helpers */
64 : /* ================================================================ */
65 :
66 20 : static void with_tmp_home(const char *tag) {
67 : char tmp[256];
68 20 : snprintf(tmp, sizeof(tmp), "/tmp/tg-cli-ft-migrate-%s", tag);
69 : char cfg_dir[512];
70 20 : snprintf(cfg_dir, sizeof(cfg_dir), "%s/.config/tg-cli", tmp);
71 20 : (void)mkdir(tmp, 0700);
72 : char parent[512];
73 20 : snprintf(parent, sizeof(parent), "%s/.config", tmp);
74 20 : (void)mkdir(parent, 0700);
75 20 : (void)mkdir(cfg_dir, 0700);
76 : char bin[600];
77 20 : snprintf(bin, sizeof(bin), "%s/session.bin", cfg_dir);
78 20 : (void)unlink(bin);
79 20 : setenv("HOME", tmp, 1);
80 : /* CI runners export these; clear so platform_config_dir() derives
81 : * from $HOME. */
82 20 : unsetenv("XDG_CONFIG_HOME");
83 20 : unsetenv("XDG_CACHE_HOME");
84 20 : }
85 :
86 20 : static void init_cfg(ApiConfig *cfg) {
87 20 : api_config_init(cfg);
88 20 : cfg->api_id = 12345;
89 20 : cfg->api_hash = "deadbeefcafebabef00dbaadfeedc0de";
90 20 : }
91 :
92 32 : static void connect_mock(Transport *t) {
93 32 : transport_init(t);
94 32 : ASSERT(transport_connect(t, "127.0.0.1", 443) == 0, "transport connects");
95 : }
96 :
97 : /* ---- Responders ---- */
98 :
99 : /** Happy-path auth.sentCode reply. */
100 6 : static void on_send_code_happy(MtRpcContext *ctx) {
101 : TlWriter w;
102 6 : tl_writer_init(&w);
103 6 : tl_write_uint32(&w, CRC_auth_sentCode);
104 6 : tl_write_uint32(&w, 0); /* flags = 0 */
105 6 : tl_write_uint32(&w, CRC_sentCodeTypeSms);
106 6 : tl_write_int32 (&w, 5); /* length */
107 6 : tl_write_string(&w, "abc123"); /* phone_code_hash */
108 6 : mt_server_reply_result(ctx, w.data, w.len);
109 6 : tl_writer_free(&w);
110 6 : }
111 :
112 : /** Happy-path auth.authorization reply with a fixed user_id. */
113 2 : static void on_sign_in_happy(MtRpcContext *ctx) {
114 : TlWriter w;
115 2 : tl_writer_init(&w);
116 2 : tl_write_uint32(&w, TL_auth_authorization);
117 2 : tl_write_uint32(&w, 0); /* outer flags = 0 */
118 2 : tl_write_uint32(&w, TL_user);
119 2 : tl_write_uint32(&w, 0); /* user.flags = 0 */
120 2 : tl_write_int64 (&w, 55555LL); /* user.id */
121 2 : mt_server_reply_result(ctx, w.data, w.len);
122 2 : tl_writer_free(&w);
123 2 : }
124 :
125 : /**
126 : * Helper that mirrors what auth_flow_login does on a migrate error:
127 : * 1. close the current transport
128 : * 2. reset the in-memory session (auth_key is DC-scoped)
129 : * 3. reconnect to the new DC (all DCs resolve to the mock loopback
130 : * via dc_lookup, but we also need a seeded session at that DC)
131 : * 4. arm the mock's reconnect parser so the second 0xEF marker is
132 : * treated as a fresh connection rather than a frame-length byte
133 : *
134 : * In production, auth_flow's migrate() would also run the full DH
135 : * handshake on the new DC. The mock server does not emulate the
136 : * unencrypted DH flow; instead the test pre-seeds the secondary DC's
137 : * auth_key via mt_server_seed_session + mt_server_seed_extra_dc so
138 : * the tested layer (auth_session) lands on an already-authenticated
139 : * transport after the switch. This keeps the test focused on the
140 : * migrate-loop semantics (retry count, final home DC) exactly where
141 : * the ticket requires coverage.
142 : */
143 12 : static void simulate_migrate(Transport *t, MtProtoSession *s, int new_dc) {
144 12 : transport_close(t);
145 12 : mtproto_session_init(s);
146 : /* Load the pre-seeded auth_key for the target DC. */
147 12 : ASSERT(session_store_load_dc(new_dc, s) == 0,
148 : "load pre-seeded secondary DC key");
149 : /* Let the mock-server parser handle the second 0xEF marker the
150 : * fresh transport sends. */
151 12 : mt_server_arm_reconnect();
152 12 : connect_mock(t);
153 12 : t->dc_id = new_dc;
154 : }
155 :
156 : /* ================================================================ */
157 : /* Scenario 1 — PHONE_MIGRATE on first auth.sendCode */
158 : /* ================================================================ */
159 :
160 2 : static void test_phone_migrate_first_send_code_switches_home_dc(void) {
161 2 : with_tmp_home("phone-migrate");
162 2 : mt_server_init();
163 2 : mt_server_reset();
164 :
165 : /* Seed DC2 as the starting home DC and DC4 as the migration target. */
166 2 : ASSERT(mt_server_seed_session(2, NULL, NULL, NULL) == 0, "seed home DC2");
167 2 : ASSERT(mt_server_seed_extra_dc(4) == 0, "seed foreign DC4 key");
168 :
169 : /* Arm the server: first auth.sendCode replies PHONE_MIGRATE_4. */
170 2 : mt_server_reply_phone_migrate(4);
171 :
172 2 : ApiConfig cfg; init_cfg(&cfg);
173 2 : MtProtoSession s; mtproto_session_init(&s);
174 2 : int dc = 0;
175 2 : ASSERT(session_store_load(&s, &dc) == 0, "session loaded from DC2");
176 2 : ASSERT(dc == 2, "home DC starts at 2");
177 :
178 2 : Transport t; connect_mock(&t);
179 2 : t.dc_id = 2;
180 :
181 2 : AuthSentCode sent = {0};
182 2 : RpcError err = {0};
183 2 : int rc = auth_send_code(&cfg, &s, &t, "+861234567890", &sent, &err);
184 2 : ASSERT(rc == -1, "sendCode fails with PHONE_MIGRATE");
185 2 : ASSERT(err.error_code == 303, "error_code 303");
186 2 : ASSERT(err.migrate_dc == 4,
187 : "rpc_parse_error extracts migrate_dc=4 from PHONE_MIGRATE_4");
188 2 : ASSERT(strncmp(err.error_msg, "PHONE_MIGRATE_", 14) == 0,
189 : "error_msg begins with PHONE_MIGRATE_");
190 :
191 : /* Mirror auth_flow_login: switch DC and retry. */
192 2 : simulate_migrate(&t, &s, err.migrate_dc);
193 :
194 : /* Now the responder table still has the migrate entry — swap it
195 : * for a happy-path responder so the retry on DC4 succeeds. */
196 2 : mt_server_expect(CRC_auth_sendCode, on_send_code_happy, NULL);
197 :
198 2 : AuthSentCode sent2 = {0};
199 2 : RpcError err2 = {0};
200 2 : ASSERT(auth_send_code(&cfg, &s, &t, "+861234567890", &sent2, &err2) == 0,
201 : "sendCode succeeds after migrate to DC4");
202 2 : ASSERT(strcmp(sent2.phone_code_hash, "abc123") == 0,
203 : "phone_code_hash roundtrips on the migrated DC");
204 :
205 : /* Persist the post-migration session — emulates auth_flow_login's
206 : * final session_store_save() with current_dc=4. */
207 2 : ASSERT(session_store_save(&s, 4) == 0, "persist session on new home DC4");
208 :
209 : /* Reload and verify session.bin's home DC is now 4. */
210 2 : MtProtoSession r; mtproto_session_init(&r);
211 2 : int reloaded_dc = 0;
212 2 : ASSERT(session_store_load(&r, &reloaded_dc) == 0, "reload post-migrate");
213 2 : ASSERT(reloaded_dc == 4,
214 : "session.bin home_dc is 4 after PHONE_MIGRATE retry");
215 :
216 2 : transport_close(&t);
217 2 : mt_server_reset();
218 : }
219 :
220 : /* ================================================================ */
221 : /* Scenario 2 — USER_MIGRATE after auth.signIn */
222 : /* ================================================================ */
223 :
224 2 : static void test_user_migrate_after_sign_in_switches_home_dc(void) {
225 2 : with_tmp_home("user-migrate");
226 2 : mt_server_init();
227 2 : mt_server_reset();
228 :
229 2 : ASSERT(mt_server_seed_session(2, NULL, NULL, NULL) == 0, "seed home DC2");
230 2 : ASSERT(mt_server_seed_extra_dc(5) == 0, "seed foreign DC5 key");
231 :
232 : /* First step succeeds; signIn returns USER_MIGRATE_5. */
233 2 : mt_server_expect(CRC_auth_sendCode, on_send_code_happy, NULL);
234 2 : mt_server_reply_user_migrate(5);
235 :
236 2 : ApiConfig cfg; init_cfg(&cfg);
237 2 : MtProtoSession s; mtproto_session_init(&s);
238 2 : int dc = 0;
239 2 : ASSERT(session_store_load(&s, &dc) == 0, "session loaded");
240 2 : ASSERT(dc == 2, "home DC starts at 2");
241 :
242 2 : Transport t; connect_mock(&t);
243 2 : t.dc_id = 2;
244 :
245 2 : AuthSentCode sent = {0};
246 2 : RpcError sc_err = {0};
247 2 : ASSERT(auth_send_code(&cfg, &s, &t, "+15551234567", &sent, &sc_err) == 0,
248 : "sendCode happy-path on DC2");
249 :
250 2 : int64_t uid = 0;
251 2 : RpcError si_err = {0};
252 2 : int rc = auth_sign_in(&cfg, &s, &t, "+15551234567",
253 : sent.phone_code_hash, "12345", &uid, &si_err);
254 2 : ASSERT(rc == -1, "signIn fails with USER_MIGRATE");
255 2 : ASSERT(si_err.error_code == 303, "error_code 303");
256 2 : ASSERT(si_err.migrate_dc == 5,
257 : "rpc_parse_error extracts migrate_dc=5 from USER_MIGRATE_5");
258 2 : ASSERT(strncmp(si_err.error_msg, "USER_MIGRATE_", 13) == 0,
259 : "error_msg begins with USER_MIGRATE_");
260 :
261 : /* Switch to DC5 and retry the signIn there. */
262 2 : simulate_migrate(&t, &s, si_err.migrate_dc);
263 2 : mt_server_expect(CRC_auth_signIn, on_sign_in_happy, NULL);
264 :
265 2 : int64_t uid2 = 0;
266 2 : RpcError si_err2 = {0};
267 2 : ASSERT(auth_sign_in(&cfg, &s, &t, "+15551234567",
268 : sent.phone_code_hash, "12345", &uid2, &si_err2) == 0,
269 : "signIn succeeds after migrate to DC5");
270 2 : ASSERT(uid2 == 55555LL, "authenticated user_id returned from migrated DC");
271 :
272 2 : ASSERT(session_store_save(&s, 5) == 0, "persist on new home DC5");
273 :
274 2 : MtProtoSession r; mtproto_session_init(&r);
275 2 : int reloaded_dc = 0;
276 2 : ASSERT(session_store_load(&r, &reloaded_dc) == 0, "reload post-migrate");
277 2 : ASSERT(reloaded_dc == 5,
278 : "session.bin home_dc is 5 after USER_MIGRATE retry");
279 :
280 2 : transport_close(&t);
281 2 : mt_server_reset();
282 : }
283 :
284 : /* ================================================================ */
285 : /* Scenario 3 — NETWORK_MIGRATE is per-RPC, not per-home */
286 : /* ================================================================ */
287 :
288 2 : static void test_network_migrate_is_per_rpc_not_home(void) {
289 2 : with_tmp_home("network-migrate");
290 2 : mt_server_init();
291 2 : mt_server_reset();
292 :
293 2 : ASSERT(mt_server_seed_session(2, NULL, NULL, NULL) == 0, "seed home DC2");
294 2 : ASSERT(mt_server_seed_extra_dc(3) == 0, "seed foreign DC3 key");
295 :
296 : /* First auth.sendCode replies NETWORK_MIGRATE_3. */
297 2 : mt_server_reply_network_migrate(3);
298 :
299 2 : ApiConfig cfg; init_cfg(&cfg);
300 2 : MtProtoSession s; mtproto_session_init(&s);
301 2 : int dc_before = 0;
302 2 : ASSERT(session_store_load(&s, &dc_before) == 0, "session loaded");
303 2 : ASSERT(dc_before == 2, "home DC starts at 2");
304 :
305 2 : Transport t; connect_mock(&t);
306 2 : t.dc_id = 2;
307 :
308 2 : AuthSentCode sent = {0};
309 2 : RpcError err = {0};
310 2 : int rc = auth_send_code(&cfg, &s, &t, "+12025550000", &sent, &err);
311 2 : ASSERT(rc == -1, "sendCode fails with NETWORK_MIGRATE");
312 2 : ASSERT(err.error_code == 303, "error_code 303");
313 2 : ASSERT(err.migrate_dc == 3,
314 : "rpc_parse_error extracts migrate_dc=3 from NETWORK_MIGRATE_3");
315 2 : ASSERT(strncmp(err.error_msg, "NETWORK_MIGRATE_", 16) == 0,
316 : "error_msg begins with NETWORK_MIGRATE_");
317 :
318 : /* Retry the same RPC on DC3 using session_store_save_dc (which does
319 : * NOT promote DC3 to home) — this mirrors what a NETWORK_MIGRATE
320 : * handler in the infrastructure layer would do. */
321 2 : simulate_migrate(&t, &s, err.migrate_dc);
322 2 : mt_server_expect(CRC_auth_sendCode, on_send_code_happy, NULL);
323 :
324 2 : AuthSentCode sent2 = {0};
325 2 : RpcError err2 = {0};
326 2 : ASSERT(auth_send_code(&cfg, &s, &t, "+12025550000", &sent2, &err2) == 0,
327 : "sendCode succeeds after per-RPC retry on DC3");
328 :
329 : /* Critical assertion: home DC is unchanged at 2. NETWORK_MIGRATE
330 : * is a transient redirect that rebinds only the current RPC, not
331 : * the user's home DC (contrast with PHONE_MIGRATE / USER_MIGRATE). */
332 2 : ASSERT(session_store_save_dc(3, &s) == 0,
333 : "save DC3 entry without changing home");
334 :
335 2 : MtProtoSession r; mtproto_session_init(&r);
336 2 : int reloaded_dc = 0;
337 2 : ASSERT(session_store_load(&r, &reloaded_dc) == 0, "reload after retry");
338 2 : ASSERT(reloaded_dc == 2,
339 : "home DC stays at 2 after NETWORK_MIGRATE retry");
340 :
341 2 : transport_close(&t);
342 2 : mt_server_reset();
343 : }
344 :
345 : /* ================================================================ */
346 : /* Scenario 4 — Ghost migrate loop bails at AUTH_MAX_MIGRATIONS */
347 : /* ================================================================ */
348 :
349 : /*
350 : * The mock is armed so every auth.sendCode responds PHONE_MIGRATE_X
351 : * with X incrementing by one each hop. This test mirrors the loop
352 : * logic in src/app/auth_flow.c (AUTH_MAX_MIGRATIONS = 3): after 3
353 : * migrations the client must give up and surface a failure rather
354 : * than spinning forever or recursing indefinitely.
355 : *
356 : * We seed DC2..DC5 so the simulate_migrate() helper can load an
357 : * auth_key for every hop — the test's goal is to drive the loop
358 : * count past the cap, not to observe a DH handshake at each DC.
359 : */
360 2 : static void test_ghost_migrate_loop_bails_at_3_hops(void) {
361 2 : with_tmp_home("ghost-migrate");
362 2 : mt_server_init();
363 2 : mt_server_reset();
364 :
365 2 : ASSERT(mt_server_seed_session(2, NULL, NULL, NULL) == 0, "seed home DC2");
366 2 : ASSERT(mt_server_seed_extra_dc(3) == 0, "seed DC3");
367 2 : ASSERT(mt_server_seed_extra_dc(4) == 0, "seed DC4");
368 2 : ASSERT(mt_server_seed_extra_dc(5) == 0, "seed DC5");
369 :
370 2 : ApiConfig cfg; init_cfg(&cfg);
371 2 : MtProtoSession s; mtproto_session_init(&s);
372 2 : int dc_before = 0;
373 2 : ASSERT(session_store_load(&s, &dc_before) == 0, "session loaded");
374 2 : ASSERT(dc_before == 2, "home DC starts at 2");
375 :
376 2 : Transport t; connect_mock(&t);
377 2 : t.dc_id = 2;
378 :
379 2 : int migrations = 0;
380 2 : int last_migrate_dc = 0;
381 2 : int bailed_out = 0;
382 :
383 : /* Arm the first reply and run the migrate loop mirroring
384 : * auth_flow_login: each sendCode returns PHONE_MIGRATE_<next_dc>.
385 : * After AUTH_MAX_MIGRATIONS hops the caller MUST stop retrying. */
386 2 : int next_dc = 3;
387 2 : mt_server_reply_phone_migrate(next_dc);
388 :
389 6 : for (;;) {
390 8 : AuthSentCode sent = {0};
391 8 : RpcError err = {0};
392 8 : int rc = auth_send_code(&cfg, &s, &t, "+861234567890", &sent, &err);
393 8 : if (rc == 0) {
394 : /* Unexpected — the server keeps replying PHONE_MIGRATE. */
395 0 : break;
396 : }
397 8 : if (err.migrate_dc > 0 && migrations < LOCAL_MAX_MIGRATIONS) {
398 6 : migrations++;
399 6 : last_migrate_dc = err.migrate_dc;
400 6 : simulate_migrate(&t, &s, err.migrate_dc);
401 :
402 : /* Arm the next PHONE_MIGRATE to a fresh DC so we can
403 : * distinguish the hops in assertions. */
404 6 : next_dc = (next_dc == 3) ? 4 : ((next_dc == 4) ? 5 : 3);
405 6 : mt_server_reply_phone_migrate(next_dc);
406 6 : continue;
407 : }
408 : /* Either no migrate_dc or we hit the cap — this is the bail
409 : * branch the production loop takes. */
410 2 : bailed_out = 1;
411 2 : break;
412 : }
413 :
414 2 : ASSERT(migrations == LOCAL_MAX_MIGRATIONS,
415 : "client performed exactly AUTH_MAX_MIGRATIONS hops (3)");
416 2 : ASSERT(bailed_out == 1,
417 : "loop bailed out rather than continuing indefinitely");
418 2 : ASSERT(last_migrate_dc > 0,
419 : "last migrate_dc was observed (loop did see a PHONE_MIGRATE on hop 3)");
420 :
421 : /* The session should NOT have been persisted with a bogus home DC.
422 : * session.bin may still exist from the seed — but if we re-load,
423 : * home DC must not match the ghost migrations (it stays at 2 as
424 : * seeded). This guards against a future bug where the migrate
425 : * loop commits an intermediate DC on exhaustion. */
426 2 : MtProtoSession r; mtproto_session_init(&r);
427 2 : int reloaded_dc = 0;
428 2 : ASSERT(session_store_load(&r, &reloaded_dc) == 0, "reload after bail");
429 2 : ASSERT(reloaded_dc == 2,
430 : "home DC stays at 2 after ghost-migrate bail (no partial commit)");
431 :
432 2 : transport_close(&t);
433 2 : mt_server_reset();
434 : }
435 :
436 : /* ================================================================ */
437 : /* Ancillary coverage — auth_session.c migrate-adjacent branches */
438 : /* */
439 : /* These are kept here rather than in test_login_flow.c so the TEST-86 */
440 : /* suite captures everything needed to meet the US-35 coverage bar */
441 : /* (auth_session.c ≥ 80 %). Each case targets a branch rpc_parse_error */
442 : /* feeds into and that the migrate tests above exercise indirectly. */
443 : /* ================================================================ */
444 :
445 : /** auth.sentCode with flags.2 set — exercises the timeout-parsing branch. */
446 2 : static void on_send_code_with_timeout(MtRpcContext *ctx) {
447 : TlWriter w;
448 2 : tl_writer_init(&w);
449 2 : tl_write_uint32(&w, CRC_auth_sentCode);
450 2 : tl_write_uint32(&w, 1u << 2); /* flags.2 → timeout present */
451 2 : tl_write_uint32(&w, CRC_sentCodeTypeSms);
452 2 : tl_write_int32 (&w, 5);
453 2 : tl_write_string(&w, "xyz789");
454 2 : tl_write_int32 (&w, 120); /* timeout seconds */
455 2 : mt_server_reply_result(ctx, w.data, w.len);
456 2 : tl_writer_free(&w);
457 2 : }
458 :
459 2 : static void test_send_code_timeout_flag_parses(void) {
460 2 : with_tmp_home("timeout-flag");
461 2 : mt_server_init();
462 2 : mt_server_reset();
463 2 : ASSERT(mt_server_seed_session(2, NULL, NULL, NULL) == 0, "seed");
464 2 : mt_server_expect(CRC_auth_sendCode, on_send_code_with_timeout, NULL);
465 :
466 2 : ApiConfig cfg; init_cfg(&cfg);
467 2 : MtProtoSession s; mtproto_session_init(&s);
468 2 : int dc = 0; ASSERT(session_store_load(&s, &dc) == 0, "load");
469 :
470 2 : Transport t; connect_mock(&t);
471 :
472 2 : AuthSentCode sent = {0};
473 2 : RpcError err = {0};
474 2 : ASSERT(auth_send_code(&cfg, &s, &t, "+15551234567", &sent, &err) == 0,
475 : "sendCode ok with timeout flag");
476 2 : ASSERT(sent.timeout == 120,
477 : "timeout parsed from flags.2 branch of auth.sentCode");
478 2 : ASSERT(strcmp(sent.phone_code_hash, "xyz789") == 0,
479 : "phone_code_hash roundtrips alongside timeout");
480 :
481 2 : transport_close(&t);
482 2 : mt_server_reset();
483 : }
484 :
485 : /** Reply with neither rpc_error nor auth_sentCode to hit the
486 : * "unexpected constructor" diagnostic in auth_send_code. */
487 2 : static void on_send_code_unexpected_constructor(MtRpcContext *ctx) {
488 : /* Emit something harmless but clearly-not-sentCode: an auth.authorization
489 : * constructor (reserved for signIn). The reader will land in the
490 : * unexpected-constructor branch. */
491 : TlWriter w;
492 2 : tl_writer_init(&w);
493 2 : tl_write_uint32(&w, TL_auth_authorization);
494 2 : tl_write_uint32(&w, 0);
495 2 : tl_write_uint32(&w, TL_user);
496 2 : tl_write_uint32(&w, 0);
497 2 : tl_write_int64 (&w, 0);
498 2 : mt_server_reply_result(ctx, w.data, w.len);
499 2 : tl_writer_free(&w);
500 2 : }
501 :
502 2 : static void test_send_code_rejects_unexpected_constructor(void) {
503 2 : with_tmp_home("unexpected-ctor");
504 2 : mt_server_init();
505 2 : mt_server_reset();
506 2 : ASSERT(mt_server_seed_session(2, NULL, NULL, NULL) == 0, "seed");
507 2 : mt_server_expect(CRC_auth_sendCode, on_send_code_unexpected_constructor, NULL);
508 :
509 2 : ApiConfig cfg; init_cfg(&cfg);
510 2 : MtProtoSession s; mtproto_session_init(&s);
511 2 : int dc = 0; ASSERT(session_store_load(&s, &dc) == 0, "load");
512 :
513 2 : Transport t; connect_mock(&t);
514 :
515 2 : AuthSentCode sent = {0};
516 2 : RpcError err = {0};
517 2 : int rc = auth_send_code(&cfg, &s, &t, "+15551234567", &sent, &err);
518 2 : ASSERT(rc == -1,
519 : "auth_send_code fails on unexpected (non-sentCode, non-error) constructor");
520 :
521 2 : transport_close(&t);
522 2 : mt_server_reset();
523 : }
524 :
525 : /** Same coverage for the signIn path. */
526 2 : static void on_sign_in_unexpected_constructor(MtRpcContext *ctx) {
527 : /* Emit an auth.sentCode instead of auth.authorization. */
528 : TlWriter w;
529 2 : tl_writer_init(&w);
530 2 : tl_write_uint32(&w, CRC_auth_sentCode);
531 2 : tl_write_uint32(&w, 0);
532 2 : tl_write_uint32(&w, CRC_sentCodeTypeSms);
533 2 : tl_write_int32 (&w, 5);
534 2 : tl_write_string(&w, "wrong");
535 2 : mt_server_reply_result(ctx, w.data, w.len);
536 2 : tl_writer_free(&w);
537 2 : }
538 :
539 2 : static void test_sign_in_rejects_unexpected_constructor(void) {
540 2 : with_tmp_home("signin-unexpected-ctor");
541 2 : mt_server_init();
542 2 : mt_server_reset();
543 2 : ASSERT(mt_server_seed_session(2, NULL, NULL, NULL) == 0, "seed");
544 2 : mt_server_expect(CRC_auth_signIn, on_sign_in_unexpected_constructor, NULL);
545 :
546 2 : ApiConfig cfg; init_cfg(&cfg);
547 2 : MtProtoSession s; mtproto_session_init(&s);
548 2 : int dc = 0; ASSERT(session_store_load(&s, &dc) == 0, "load");
549 :
550 2 : Transport t; connect_mock(&t);
551 :
552 2 : int64_t uid = 0;
553 2 : RpcError err = {0};
554 2 : int rc = auth_sign_in(&cfg, &s, &t, "+15551234567", "hash", "12345",
555 : &uid, &err);
556 2 : ASSERT(rc == -1,
557 : "auth_sign_in fails on unexpected (non-authorization, non-error) constructor");
558 :
559 2 : transport_close(&t);
560 2 : mt_server_reset();
561 : }
562 :
563 : /** auth.sentCode with sentCodeTypeFlashCall — covers the alternate
564 : * sub-object branch in skip_sent_code_type(). */
565 : #define CRC_auth_sentCodeTypeFlashCall_local 0xab03c6d9U
566 2 : static void on_send_code_flashcall(MtRpcContext *ctx) {
567 : TlWriter w;
568 2 : tl_writer_init(&w);
569 2 : tl_write_uint32(&w, CRC_auth_sentCode);
570 2 : tl_write_uint32(&w, 0);
571 2 : tl_write_uint32(&w, CRC_auth_sentCodeTypeFlashCall_local);
572 2 : tl_write_string(&w, "+1202XXX####"); /* pattern:string */
573 2 : tl_write_string(&w, "flash123"); /* phone_code_hash */
574 2 : mt_server_reply_result(ctx, w.data, w.len);
575 2 : tl_writer_free(&w);
576 2 : }
577 :
578 2 : static void test_send_code_flashcall_type_parses(void) {
579 2 : with_tmp_home("flashcall");
580 2 : mt_server_init();
581 2 : mt_server_reset();
582 2 : ASSERT(mt_server_seed_session(2, NULL, NULL, NULL) == 0, "seed");
583 2 : mt_server_expect(CRC_auth_sendCode, on_send_code_flashcall, NULL);
584 :
585 2 : ApiConfig cfg; init_cfg(&cfg);
586 2 : MtProtoSession s; mtproto_session_init(&s);
587 2 : int dc = 0; ASSERT(session_store_load(&s, &dc) == 0, "load");
588 :
589 2 : Transport t; connect_mock(&t);
590 :
591 2 : AuthSentCode sent = {0};
592 2 : RpcError err = {0};
593 2 : ASSERT(auth_send_code(&cfg, &s, &t, "+12025550000", &sent, &err) == 0,
594 : "sendCode ok with sentCodeTypeFlashCall");
595 2 : ASSERT(strcmp(sent.phone_code_hash, "flash123") == 0,
596 : "phone_code_hash parsed past flash-call pattern string");
597 :
598 2 : transport_close(&t);
599 2 : mt_server_reset();
600 : }
601 :
602 : /** auth.sentCode with unknown sentCodeType CRC — skip_sent_code_type's
603 : * default branch rejects the response. */
604 2 : static void on_send_code_unknown_codetype(MtRpcContext *ctx) {
605 : TlWriter w;
606 2 : tl_writer_init(&w);
607 2 : tl_write_uint32(&w, CRC_auth_sentCode);
608 2 : tl_write_uint32(&w, 0);
609 2 : tl_write_uint32(&w, 0xFADEBABEu); /* no such sentCodeType */
610 2 : tl_write_int32 (&w, 5);
611 2 : tl_write_string(&w, "ignored");
612 2 : mt_server_reply_result(ctx, w.data, w.len);
613 2 : tl_writer_free(&w);
614 2 : }
615 :
616 2 : static void test_send_code_rejects_unknown_sentcode_type(void) {
617 2 : with_tmp_home("unknown-codetype");
618 2 : mt_server_init();
619 2 : mt_server_reset();
620 2 : ASSERT(mt_server_seed_session(2, NULL, NULL, NULL) == 0, "seed");
621 2 : mt_server_expect(CRC_auth_sendCode, on_send_code_unknown_codetype, NULL);
622 :
623 2 : ApiConfig cfg; init_cfg(&cfg);
624 2 : MtProtoSession s; mtproto_session_init(&s);
625 2 : int dc = 0; ASSERT(session_store_load(&s, &dc) == 0, "load");
626 :
627 2 : Transport t; connect_mock(&t);
628 :
629 2 : AuthSentCode sent = {0};
630 2 : RpcError err = {0};
631 2 : ASSERT(auth_send_code(&cfg, &s, &t, "+12025550000", &sent, &err) == -1,
632 : "sendCode rejects unknown sentCodeType constructor");
633 :
634 2 : transport_close(&t);
635 2 : mt_server_reset();
636 : }
637 :
638 : /** auth.authorization body with an unknown user constructor — the signIn
639 : * parser logs a warning but still reports success with user_id=0. */
640 2 : static void on_sign_in_unknown_user_ctor(MtRpcContext *ctx) {
641 : TlWriter w;
642 2 : tl_writer_init(&w);
643 2 : tl_write_uint32(&w, TL_auth_authorization);
644 2 : tl_write_uint32(&w, 0); /* outer flags = 0 */
645 2 : tl_write_uint32(&w, 0xDEADBEEFu); /* bogus user constructor */
646 : /* No body — parser bails after reading the constructor. */
647 2 : mt_server_reply_result(ctx, w.data, w.len);
648 2 : tl_writer_free(&w);
649 2 : }
650 :
651 2 : static void test_sign_in_unknown_user_ctor_still_succeeds(void) {
652 2 : with_tmp_home("unknown-user-ctor");
653 2 : mt_server_init();
654 2 : mt_server_reset();
655 2 : ASSERT(mt_server_seed_session(2, NULL, NULL, NULL) == 0, "seed");
656 2 : mt_server_expect(CRC_auth_signIn, on_sign_in_unknown_user_ctor, NULL);
657 :
658 2 : ApiConfig cfg; init_cfg(&cfg);
659 2 : MtProtoSession s; mtproto_session_init(&s);
660 2 : int dc = 0; ASSERT(session_store_load(&s, &dc) == 0, "load");
661 :
662 2 : Transport t; connect_mock(&t);
663 :
664 2 : int64_t uid = 0xAAAAAAAAAAAAAAAALL; /* sentinel */
665 2 : RpcError err = {0};
666 2 : int rc = auth_sign_in(&cfg, &s, &t, "+15551234567", "hash", "12345",
667 : &uid, &err);
668 : /* The parser treats an unknown user constructor as "authenticated but
669 : * we do not know the id" — rc == 0, uid reset to 0. */
670 2 : ASSERT(rc == 0,
671 : "auth_sign_in still reports success on unknown user constructor");
672 2 : ASSERT(uid == 0,
673 : "unknown user constructor sets user_id=0");
674 :
675 2 : transport_close(&t);
676 2 : mt_server_reset();
677 : }
678 :
679 : /* ================================================================ */
680 : /* Suite entry point */
681 : /* ================================================================ */
682 :
683 2 : void run_login_migrate_tests(void) {
684 2 : RUN_TEST(test_phone_migrate_first_send_code_switches_home_dc);
685 2 : RUN_TEST(test_user_migrate_after_sign_in_switches_home_dc);
686 2 : RUN_TEST(test_network_migrate_is_per_rpc_not_home);
687 2 : RUN_TEST(test_ghost_migrate_loop_bails_at_3_hops);
688 2 : RUN_TEST(test_send_code_timeout_flag_parses);
689 2 : RUN_TEST(test_send_code_rejects_unexpected_constructor);
690 2 : RUN_TEST(test_sign_in_rejects_unexpected_constructor);
691 2 : RUN_TEST(test_sign_in_unknown_user_ctor_still_succeeds);
692 2 : RUN_TEST(test_send_code_flashcall_type_parses);
693 2 : RUN_TEST(test_send_code_rejects_unknown_sentcode_type);
694 2 : }
|