import unittest from contextlib import redirect_stderr, redirect_stdout from io import StringIO from types import ModuleType from unittest.mock import MagicMock, call, patch from core.device_profile import DeviceProfileGenerator from core.sig3 import Kwsg10418State from core.sms_login import LoginSession, RegionTicket from tools import sms_login_cli from tools.sms_login_cli import build_parser class SmsLoginCliTests(unittest.TestCase): def test_parser_only_exposes_login_inputs_and_transport(self): parser = build_parser() option_strings = { option for action in parser._actions for option in action.option_strings } self.assertEqual( option_strings, { "-h", "--help", "--mobile", "--code", "--base-url", "--transport", }, ) def test_parser_rejects_removed_device_reuse_options(self): parser = build_parser() for removed_option in ( "--device-profile", "--device-seed", "--no-device-online", "--app-fields", "--dry-run", "--out", ): with self.subTest(option=removed_option): with redirect_stderr(StringIO()), self.assertRaises(SystemExit): parser.parse_args(["--mobile", "13800000000", removed_option]) def test_parser_rejects_mobile_placeholders(self): parser = build_parser() with redirect_stderr(StringIO()), self.assertRaises(SystemExit): parser.parse_args(["--mobile", "MOBILE"]) def test_http_captcha_failure_does_not_open_a_browser(self): self.assertFalse(hasattr(sms_login_cli, "complete_captcha_in_browser")) self.assertFalse(hasattr(sms_login_cli, "webbrowser")) with ( patch.object(sms_login_cli, "_solve_captcha_http", return_value=""), redirect_stdout(StringIO()) as output, ): token = sms_login_cli._complete_captcha_handoff( "https://app.m.kuaishou.com/verify/captcha.html?key=1&type=7", stage="mobileVerifyCode", session=object(), ) self.assertEqual(token, "") self.assertIn("不启用浏览器", output.getvalue()) def test_http_captcha_diagnoses_known_result_codes(self): self.assertEqual( set(sms_login_cli._CAPTCHA_RESULT_HINTS), {350001, 350002, 350005, 350009, 350014, 350017, 350029}, ) solver_module = ModuleType("tools.captcha_solver_http") solver_module.solve_captcha = lambda **kwargs: { "ok": False, "stage": "verify", "result": 350029, "captcha_token": "", "raw": { "result": 350029, "desc": "switch captcha type", "type": "4", "configPath": "/rest/zt/captcha/wordClick/config", }, } with ( patch.dict( sms_login_cli.sys.modules, {"tools.captcha_solver_http": solver_module}, ), redirect_stdout(StringIO()) as output, ): token = sms_login_cli._solve_captcha_http( "https://app.m.kuaishou.com/verify/captcha.html?key=1&type=7", session=object(), ) self.assertEqual(token, "") self.assertIn("result=350029", output.getvalue()) self.assertIn("next_type=4", output.getvalue()) self.assertIn( "config_path=/rest/zt/captcha/wordClick/config", output.getvalue(), ) def test_http_captcha_retries_with_fresh_sn_after_landing_error(self): solver_module = ModuleType("tools.captcha_solver_http") results = iter( ( { "ok": False, "stage": "verify", "result": 350002, "captcha_token": "", "raw": {"result": 350002, "desc": "verify err.try again"}, }, { "ok": True, "stage": "verify", "result": 1, "captcha_token": "CAPTCHA_TOKEN", "raw": {"result": 1, "captchaToken": "CAPTCHA_TOKEN"}, }, ) ) calls = [] def solve_captcha(**kwargs): calls.append(kwargs) return next(results) solver_module.solve_captcha = solve_captcha with ( patch.dict( sms_login_cli.sys.modules, {"tools.captcha_solver_http": solver_module}, ), redirect_stdout(StringIO()) as output, ): token = sms_login_cli._solve_captcha_http( "https://app.m.kuaishou.com/verify/captcha.html?key=1&type=7", session=object(), ) self.assertEqual(token, "CAPTCHA_TOKEN") self.assertEqual(len(calls), 2) self.assertIn("重新获取 captchaSn", output.getvalue()) def test_main_runs_send_then_verify_without_checker(self): profile = DeviceProfileGenerator(seed=20260730).new_profile() http_session = MagicMock() anonymous_region = RegionTicket(uid="0", ticket="RT_ANON", status=200) account_region = RegionTicket(uid="101", ticket="RT_ACCOUNT", status=200) login_session = LoginSession( api_st="API_ST", h5_st="H5_ST", client_salt="SALT", user_id="101", mobile="13800000000", ) requests = [] def record_send(_profile, _mobile, **kwargs): requests.append(("send", kwargs["encrypted_mobile"])) return {"status": 200, "body": {"result": 1}} def record_login(_profile, _mobile, _code, **kwargs): requests.append(("login", kwargs["encrypted_mobile"])) return login_session, [("mobileVerifyCode", login_session)] with ( patch.object(sms_login_cli, "DeviceProfileGenerator") as generator, patch.object(sms_login_cli, "run_online_bootstrap"), patch.object( sms_login_cli, "generate_profile_passport_account_image", return_value="PASSPORT", ), patch.object(sms_login_cli, "WeaponProofProvider"), patch.object( sms_login_cli, "_new_sig3_state", return_value=Kwsg10418State(session_seed=1, counter=0x5F), ), patch.object( sms_login_cli, "create_http_session", return_value=http_session, ), patch.object( sms_login_cli, "refresh_region_ticket", side_effect=(anonymous_region, account_region), ), patch.object( sms_login_cli, "loginhelper_encrypt_mobile", side_effect=("SEND_ENCRYPTED", "LOGIN_ENCRYPTED"), ), patch.object( sms_login_cli, "request_mobile_code", side_effect=record_send, ), patch.object( sms_login_cli, "build_account_security_fields", return_value={"raw": "RAW", "publicKey": "PUB", "secret": "SEC"}, ), patch.object( sms_login_cli, "login_by_code_try_paths", side_effect=record_login, ), patch.object(sms_login_cli, "input", return_value="123456", create=True), patch.object( sms_login_cli.sys, "argv", ["sms_login_cli.py", "--mobile", "13800000000"], ), redirect_stdout(StringIO()), ): generator.return_value.new_profile.return_value = profile self.assertEqual(sms_login_cli.main(), 0) self.assertEqual( requests, [ ("send", "SEND_ENCRYPTED"), ("login", "LOGIN_ENCRYPTED"), ], ) def test_each_main_call_generates_and_registers_a_fresh_device(self): profiles = [ DeviceProfileGenerator(seed=20260730).new_profile(), DeviceProfileGenerator(seed=20260731).new_profile(), ] generators = [MagicMock(), MagicMock()] for generator, profile in zip(generators, profiles, strict=True): generator.new_profile.return_value = profile http_sessions = [MagicMock(), MagicMock()] anonymous_regions = [ RegionTicket(uid="0", ticket="RT_ANON_1", status=200), RegionTicket(uid="0", ticket="RT_ANON_2", status=200), ] account_regions = [ RegionTicket(uid="101", ticket="RT_ACCOUNT_1", status=200), RegionTicket(uid="102", ticket="RT_ACCOUNT_2", status=200), ] login_sessions = [ LoginSession( api_st="API_ST_1", h5_st="H5_ST_1", client_salt="SALT_1", user_id="101", mobile="13800000000", ), LoginSession( api_st="API_ST_2", h5_st="H5_ST_2", client_salt="SALT_2", user_id="102", mobile="13800000000", ), ] output = StringIO() with ( patch.object( sms_login_cli, "DeviceProfileGenerator", side_effect=generators, ), patch.object(sms_login_cli, "run_online_bootstrap") as bootstrap, patch.object( sms_login_cli, "generate_profile_passport_account_image", side_effect=("PASSPORT_1", "PASSPORT_2"), ) as generate_passport, patch.object(sms_login_cli, "WeaponProofProvider"), patch.object( sms_login_cli, "_new_sig3_state", side_effect=( Kwsg10418State(session_seed=1, counter=0x5F), Kwsg10418State(session_seed=2, counter=0x5F), ), ), patch.object( sms_login_cli, "create_http_session", side_effect=http_sessions, ), patch.object( sms_login_cli, "refresh_region_ticket", side_effect=( anonymous_regions[0], account_regions[0], anonymous_regions[1], account_regions[1], ), ), patch.object( sms_login_cli, "loginhelper_encrypt_mobile", side_effect=("ENC_1", "ENC_2"), ), patch.object( sms_login_cli, "build_account_security_fields", return_value={"raw": "RAW", "publicKey": "PUB", "secret": "SEC"}, ), patch.object( sms_login_cli, "login_by_code_try_paths", side_effect=( (login_sessions[0], [("mobileVerifyCode", login_sessions[0])]), (login_sessions[1], [("mobileVerifyCode", login_sessions[1])]), ), ) as login, patch.object( sms_login_cli.sys, "argv", ["sms_login_cli.py", "--mobile", "13800000000", "--code", "123456"], ), redirect_stdout(output), ): self.assertEqual(sms_login_cli.main(), 0) self.assertEqual(sms_login_cli.main(), 0) self.assertNotEqual(profiles[0].did, profiles[1].did) self.assertEqual( bootstrap.call_args_list, [ call(profiles[0], timeout=sms_login_cli.REQUEST_TIMEOUT_SECONDS), call(profiles[1], timeout=sms_login_cli.REQUEST_TIMEOUT_SECONDS), ], ) self.assertEqual( generate_passport.call_args_list, [call(profiles[0]), call(profiles[1])], ) self.assertIs(login.call_args_list[0].args[0], profiles[0]) self.assertIs(login.call_args_list[1].args[0], profiles[1]) text = output.getvalue() self.assertIn('ksck="账号#', text) self.assertIn(f"did={profiles[0].did}", text) self.assertIn("userId=101", text) self.assertIn("kuaishou.api_st=API_ST_1", text) self.assertIn("token=API_ST_1", text) self.assertIn("region_ticket=RT_ACCOUNT_1", text) self.assertIn("kuaishou.h5_st=H5_ST_1", text) self.assertIn("os=android#SALT_1\"", text) self.assertIn("#SALT_1\"", text) if __name__ == "__main__": unittest.main()