ksjsb/tests/test_sms_login_cli.py
2026-07-30 20:25:56 +08:00

239 lines
8.3 KiB
Python

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_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()