import base64 import json import os import tempfile import unittest from pathlib import Path from unittest.mock import patch from urllib.parse import parse_qs, urlsplit from core.captured_profile import CapturedProfile from core.sig import sig from main import ( BuiltRequest, HarTemplateStore, HttpResult, KsTaskRunner, SignedClient, apply_cookie_overrides_to_pairs, build_parser, decode_base64_json, extract_ad_material, extract_task_items, load_account_from_args, missing_ad_material_fields, parse_account_spec, parse_reward_template_text, print_normal_output, profile_from_account, require_account_from_args, resolve_har_files, replace_pair_values, ) class FakeSig3State: def __init__(self): self.inputs = [] def sig3_hex(self, input_value, **_kwargs): self.inputs.append(input_value) return "S3" class MainClientTests(unittest.TestCase): def test_replace_pair_values_preserves_order_and_appends_missing(self): pairs = [("a", "1"), ("b", "2"), ("a", "3")] self.assertEqual( replace_pair_values(pairs, {"a": "9", "c": "4"}), [("a", "9"), ("b", "2"), ("a", "9"), ("c", "4")], ) def test_decode_base64_json_accepts_urlsafe_unpadded_input(self): payload = {"pageId": 1, "neoParams": "abc"} raw = json.dumps(payload, separators=(",", ":")).encode() encoded = base64.urlsafe_b64encode(raw).decode().rstrip("=") self.assertEqual(decode_base64_json(encoded), payload) def test_build_form_request_adds_all_native_signatures(self): profile = CapturedProfile( client_salt="salt", api_st="token", did="DID", odid="ODID", rdid="RDID", egid="EGID", client_key="ck", ) state = FakeSig3State() client = SignedClient( profile=profile, sig3_state=state, xfalcon_func=lambda _data: "XF", tokensig_func=lambda sig_value, _salt: "TK_" + sig_value[:6], ) request = client.build_form_request( "api.e.kuaishou.com", "/rest/test", [("foo", "bar")], query_pairs=[("z", "2"), ("sig", "old"), ("__NS_sig3", "old")], include_common_body=False, ) query = parse_qs(urlsplit(request.url).query) body = parse_qs(request.body_text) expected_sig = sig({"z": "2", "foo": "bar"}) self.assertEqual(body["foo"], ["bar"]) self.assertEqual(query["sig"], [expected_sig]) self.assertEqual(query["__NS_sig3"], ["S3"]) self.assertEqual(query["__NStokensig"], ["TK_" + expected_sig[:6]]) self.assertEqual(query["__NS_xfalcon"], ["XF"]) self.assertEqual(state.inputs, ["/rest/test" + expected_sig]) def test_parse_reward_template_text_extracts_latest_reward_material(self): str_e = b"reward-input" body = "client_key=ck&encData=old&sign=" + ("a" * 64) sig3_value = "41" * 24 log_text = "\n".join( [ "[STR][atlasEncrypt in.raw] inputB64=" + base64.b64encode(str_e).decode(), "[STR][atlasSign out] len=64 ret=" + ("b" * 64), "[REQ][reward] url=https://api.e.kuaishou.com/rest/e/reward/mixed/ad" + "?a=1&__NS_sig3=" + sig3_value + " bodyB64=" + base64.b64encode(body.encode()).decode(), ] ) template = parse_reward_template_text(log_text) self.assertEqual(template.str_e_bytes, str_e) self.assertEqual(template.body_sign, "b" * 64) self.assertEqual(template.query_dict["a"], "1") self.assertEqual(template.query_dict["__NS_sig3"], sig3_value) self.assertEqual(template.body_dict["client_key"], "ck") def test_extract_task_items_walks_common_task_lists(self): payload = { "data": { "dailyTasks": [{"name": "watch ad"}], "taskCards": [{"taskName": "card", "subTasks": [{"title": "sub"}]}], "list": [{"notTask": True}], } } names = [item.get("name") or item.get("taskName") or item.get("title") for item in extract_task_items(payload)] self.assertEqual(names, ["watch ad", "card", "sub"]) def test_extract_task_items_ignores_generic_non_task_lists(self): payload = { "data": { "dailyTasks": [{"name": "task"}], "someWidget": {"list": [{"url": "x"}, {"text": "not a task"}]}, } } self.assertEqual(len(extract_task_items(payload)), 1) def test_extract_ad_material_accepts_root_level_reward_response(self): neo_payload = {"businessId": 672, "extParams": "EXT", "pageId": 11101, "subPageId": 100026367} neo_params = base64.b64encode(json.dumps(neo_payload, separators=(",", ":")).encode()).decode() payload = { "llsid": 123, "feeds": [ { "liveStreamId": "feed-1", "ad": { "llsid": 123, "creativeId": 456, "posId": "24067", "adDataV2": {"inspireAdInfo": {"neoParams": neo_params, "adExtInfo": "AD"}}, }, } ], } material = extract_ad_material(payload) self.assertEqual(material.neo_params, neo_params) self.assertEqual(material.neo_ext_params, "EXT") self.assertEqual(material.llsid, 123) self.assertEqual(material.creative_id, 456) self.assertEqual(material.feed_id, "feed-1") self.assertEqual(material.ad_ext_info, "AD") def test_har_template_store_extracts_query_and_headers(self): har = { "log": { "entries": [ { "request": { "method": "GET", "url": ( "https://nebula.kuaishou.com/rest/n/nebula/activity/earn/overview/tasks" "?source=bottom_guide_first&bizParams=%7B%22refreshType%22%3A%22init%22%7D" ), "headers": [ {"name": "Host", "value": "nebula.kuaishou.com"}, {"name": "Content-Length", "value": "123"}, {"name": "Referer", "value": "https://nebula.kuaishou.com/nebula/earning"}, {"name": "Cookie", "value": "kuaishou.api_st=old"}, {"name": "Accept-Encoding", "value": "gzip, deflate, br"}, ], } } ] } } with tempfile.TemporaryDirectory() as tmp: path = Path(tmp) / "sample.har" path.write_text(json.dumps(har), encoding="utf-8") store = HarTemplateStore.from_har_files([path]) template = store.get("nebula.kuaishou.com", "/rest/n/nebula/activity/earn/overview/tasks") self.assertIsNotNone(template) self.assertEqual(template.query_pairs[0], ("source", "bottom_guide_first")) self.assertEqual(template.query_pairs[1], ("bizParams", '{"refreshType":"init"}')) headers = template.sanitized_headers(use_cookie=False) self.assertEqual(headers["Host"], "nebula.kuaishou.com") self.assertEqual(headers["Referer"], "https://nebula.kuaishou.com/nebula/earning") self.assertEqual(headers["Accept-Encoding"], "gzip") self.assertNotIn("Content-Length", headers) self.assertNotIn("Cookie", headers) def test_plain_get_uses_har_template_headers_without_stale_cookie(self): store = HarTemplateStore() store.put( "nebula.kuaishou.com", "/rest/example", "GET", [("source", "har")], [ ("Referer", "https://nebula.kuaishou.com/nebula/earning"), ("Cookie", "old=1"), ("Accept-Encoding", "gzip, deflate, br"), ], ) client = SignedClient(template_store=store) request = client.build_plain_get_request( "nebula.kuaishou.com", "/rest/example", client.template_query_pairs("nebula.kuaishou.com", "/rest/example", []), ) self.assertIn("source=har", request.url) self.assertEqual(request.headers["Referer"], "https://nebula.kuaishou.com/nebula/earning") self.assertEqual(request.headers["Host"], "nebula.kuaishou.com") self.assertEqual(request.headers["Accept-Encoding"], "gzip") self.assertNotIn("Cookie", request.headers) def test_execute_returns_structured_http_error(self): class FailingClient: def send(self, request): return HttpResult(False, 500, request.url, "boom", None, request) request = BuiltRequest("GET", "https://example.test/path", {}, None) runner = KsTaskRunner(FailingClient(), dry_run=False) result = runner._execute("probe", request) self.assertFalse(result["ok"]) self.assertEqual(result["event"], "probe") self.assertEqual(result["status"], 500) self.assertEqual(result["error"], "http_status_500") self.assertEqual(result["responseText"], "boom") def test_watch_ad_skips_followups_when_real_ad_material_is_incomplete(self): class ProbeRunner(KsTaskRunner): def __init__(self): super().__init__(client=object(), dry_run=False) self.called = [] def fetch_reward_ad(self): return { "ok": True, "event": "reward-mixed-ad", "responseJson": {"result": 1, "feeds": []}, } def neo_task_info(self, neo_params): self.called.append("neo_task_info") return {} def report_ad_task(self, material): self.called.append("report_ad_task") return {} def nebula_reward(self, material): self.called.append("nebula_reward") return {} def activity_reward(self, material): self.called.append("activity_reward") return {} def coin_send(self, material): self.called.append("coin_send") return {} runner = ProbeRunner() result = runner.watch_ad_once() self.assertEqual(runner.called, []) self.assertIn("missing_ad_material", result["warnings"]) def test_ad_material_accepts_reward_ad_without_neo_params(self): payload = { "result": 1, "llsid": 2006042943124939954, "feeds": [ { "photo_id": "5194902358263689735", "ad": { "llsid": 2006042943124939954, "creativeId": 211282409779, "pageId": 11101, "subPageId": 100026367, "displayType": 2, "adDataV2": { "posId": 24067, "inspireAdInfo": {"adExtInfo": "AD_EXT"}, }, "extData": json.dumps({"awardCoin": 46}), }, } ], } material = extract_ad_material(payload, allow_fallback=False) self.assertEqual(material.llsid, 2006042943124939954) self.assertEqual(material.creative_id, 211282409779) self.assertEqual(material.feed_id, "5194902358263689735") self.assertEqual(material.display_type, 2) self.assertEqual(material.ad_ext_info, "AD_EXT") self.assertEqual(missing_ad_material_fields(material), []) def test_sign_in_skips_report_when_resource_has_no_event(self): class ProbeRunner(KsTaskRunner): def __init__(self): super().__init__(client=object(), dry_run=False) self.reported = False def get_sign_in_resource(self): return {"ok": True, "responseJson": {"result": 1, "data": {"signInUnionSpecialAreaData": None}}} def _execute(self, event, request): if event == "sign-in-report": self.reported = True return {} runner = ProbeRunner() result = runner.sign_in() self.assertFalse(runner.reported) self.assertTrue(result["skipped"]) self.assertEqual(result["reason"], "no_sign_in_event") def test_treasure_box_skips_report_when_info_is_unavailable(self): class ProbeRunner(KsTaskRunner): def __init__(self): super().__init__(client=object(), dry_run=False) self.reported = False def treasure_box_info(self): return {"ok": True, "responseJson": {"result": 102512, "msg": ""}} def _execute(self, event, request): if event == "treasure-box-report": self.reported = True return {} runner = ProbeRunner() result = runner.open_treasure_box() self.assertFalse(runner.reported) self.assertTrue(result["skipped"]) self.assertEqual(result["reason"], "treasure_box_unavailable") def test_treasure_box_report_uses_h5_json_endpoint(self): class FakeClient: def __init__(self): self.request = None def build_plain_json_request(self, host, path, payload, query_pairs=None): self.request = BuiltRequest( "POST", f"https://{host}{path}", {"Host": host, "Content-Type": "application/json"}, json.dumps(payload).encode(), {"query": query_pairs or []}, ) return self.request def template_query_pairs(self, host, path, fallback): return fallback class ProbeRunner(KsTaskRunner): def treasure_box_info(self): return { "ok": True, "responseJson": { "result": 1, "data": { "eventTrackingLogInfo": { "eventTrackingTaskId": 20035, "resourceId": "externalFeed_externalTreasureChestWidget", } }, }, } def _execute(self, event, request): return {"event": event, "request": request.as_dict(redact=False)} client = FakeClient() runner = ProbeRunner(client=client, dry_run=False) result = runner.open_treasure_box() self.assertEqual(result["event"], "treasure-box-report") self.assertEqual(client.request.url, "https://nebula.kuaishou.com/rest/wd/encourage/unionTask/treasureBox/report") self.assertIn("eventTrackingLogInfo", client.request.body_text) def test_watch_ad_reports_business_error_without_missing_material(self): class ProbeRunner(KsTaskRunner): def __init__(self): super().__init__(client=object(), dry_run=False) def fetch_reward_ad(self): return { "ok": True, "event": "reward-mixed-ad", "responseJson": {"result": 6001, "message": "no ad"}, } result = ProbeRunner().watch_ad_once() self.assertIn("reward_business_error_6001", result["warnings"]) self.assertNotIn("missing_ad_material", result["warnings"]) def test_reward_ad_uses_account_overridden_query_not_old_capture_query(self): body = "client_key=ck&encData=old&sign=" + ("a" * 64) log_text = "\n".join( [ "[STR][atlasEncrypt in.raw] inputB64=" + base64.b64encode(b"reward-input").decode(), "[STR][atlasSign out] len=64 ret=" + ("b" * 64), "[REQ][reward] url=https://api.e.kuaishou.com/rest/e/reward/mixed/ad" + "?ud=OLD_USER&did=DID_OLD&egid=EGID_OLD" + " bodyB64=" + base64.b64encode(body.encode()).decode(), ] ) template = parse_reward_template_text(log_text) class FakeRewardSign: def reward_sign(self, *_args, **_kwargs): return "SIGN_NEW" class FakeClient: def __init__(self): self.base_query_pairs = [("ud", "NEW_USER"), ("did", "DID_NEW"), ("egid", "EGID_NEW")] self.sig3_state = FakeRewardSign() self.query_pairs = None def load_kwsg_tables(self): return b"t1", b"t2" def build_form_request(self, _host, _path, _body_pairs, *, query_pairs=None, include_common_body=True): self.query_pairs = query_pairs return BuiltRequest("POST", "https://api.e.kuaishou.com/rest/e/reward/mixed/ad", {}, b"") client = FakeClient() runner = KsTaskRunner(client=client, reward_template=template, dry_run=True, redact=False) with patch("main.kwsg_10400_raw", return_value=b"ENC"): runner.fetch_reward_ad() self.assertEqual(client.query_pairs, client.base_query_pairs) def test_account_spec_overrides_profile_and_request_pairs(self): account = parse_account_spec( "大号#userId=1579452490; ud=1579452490; did=ANDROID_new; " "kuaishou.api_st=TOKEN; token=TOKEN; egid=EGID_new; " "oDid=ODID_new; rdid=RDID_new; client_key=client_new; appver=14.0#salt_new" ) profile = profile_from_account(account) self.assertEqual(profile.client_salt, "salt_new") self.assertEqual(profile.api_st, "TOKEN") self.assertEqual(profile.did, "ANDROID_new") self.assertEqual(profile.odid, "ODID_new") self.assertEqual(profile.rdid, "RDID_new") self.assertEqual(profile.egid, "EGID_new") self.assertEqual(profile.client_key, "client_new") pairs = apply_cookie_overrides_to_pairs( [("ud", "old"), ("did", "old"), ("appver", "old"), ("kept", "1")], account.cookie_dict, ) self.assertEqual( pairs, [("ud", "1579452490"), ("did", "ANDROID_new"), ("appver", "14.0"), ("kept", "1")], ) def test_parser_defaults_to_run_all_without_cli_noise(self): with patch.dict(os.environ, {}, clear=True): args = build_parser().parse_args([]) self.assertEqual(args.command, "run-all") def test_parser_help_hides_debug_options(self): with patch.dict(os.environ, {}, clear=True): help_text = build_parser().format_help() self.assertIn("run-all", help_text) self.assertIn("coin", help_text) self.assertNotIn("--har", help_text) self.assertNotIn("--reward-log", help_text) self.assertNotIn("--neo-params", help_text) self.assertNotIn("task-center", help_text) def test_env_account_is_used_when_cli_account_is_empty(self): with patch.dict( os.environ, { "KS_ACCOUNT": "大号#userId=1; did=DID_ENV; kuaishou.api_st=TOKEN_ENV#salt_env", }, clear=True, ): args = build_parser().parse_args([]) account = load_account_from_args(args) self.assertIsNotNone(account) self.assertEqual(account.remark, "大号") self.assertEqual(account.cookie_dict["did"], "DID_ENV") self.assertEqual(account.client_salt, "salt_env") def test_normal_cli_requires_account_config(self): with patch.dict(os.environ, {}, clear=True): args = build_parser().parse_args([]) with self.assertRaises(SystemExit) as caught: require_account_from_args(args) self.assertIn("KS_ACCOUNT", str(caught.exception)) def test_har_is_not_loaded_by_default(self): with patch.dict(os.environ, {}, clear=True): args = build_parser().parse_args([]) self.assertEqual(resolve_har_files(args), []) with patch.dict(os.environ, {"KS_USE_HAR": "1"}, clear=True): args = build_parser().parse_args([]) self.assertTrue(resolve_har_files(args)) def test_normal_output_is_concise_summary(self): result = { "taskCount": 3, "balance": {"summary": {"allCash": "56", "totalCoin": 100}}, "actions": [{"signIn": {"ok": True}}, {"watchAd": [{"warnings": ["missing_ad_material"]}]}], } with tempfile.TemporaryFile("w+", encoding="utf-8") as fp: with patch("sys.stdout", fp): print_normal_output("run-all", result) fp.seek(0) text = fp.read() self.assertIn("任务数量: 3", text) self.assertIn("执行动作: 2", text) self.assertIn("余额: 56", text) self.assertIn("异常摘要", text) self.assertNotIn("request", text) if __name__ == "__main__": unittest.main()