fix(collect): handle login target close race
Tests / Python 3.11 / Windows (push) Has been cancelled

This commit is contained in:
chengma
2026-07-14 12:08:12 +08:00
parent cbfd36af56
commit f99cde8e05
11 changed files with 588 additions and 88 deletions
+138 -5
View File
@@ -382,6 +382,104 @@ class EditorLoginTests(unittest.TestCase):
self.assertIn("SPC_ST", status["cookie_names"])
self.assertTrue(FakeCDP.instances[0].closed)
def test_login_status_reselects_live_target_after_closing_target_fails(self):
pages = [
{
"id": "target-closing",
"type": "page",
"url": "https://seller.shopee.tw/portal/product/48663456321",
"webSocketDebuggerUrl": "ws-closing",
},
{
"id": "target-live",
"type": "page",
"url": "https://seller.shopee.tw/portal/product/52313423890",
"webSocketDebuggerUrl": "ws-live",
},
]
def cdp_factory(ws):
instance = FakeCDP(
ws,
url=next(page["url"] for page in pages if page["webSocketDebuggerUrl"] == ws),
cookies=[cookie("SPC_ST")] if ws == "ws-live" else [],
)
if ws == "ws-closing":
original_send = instance.send
def send(method, params=None):
if method == "Network.getAllCookies":
raise RuntimeError("Target closed")
return original_send(method, params)
instance.send = send
return instance
with mock.patch("app.editor.http_get", return_value=pages), mock.patch(
"app.editor.CDP",
side_effect=cdp_factory,
), mock.patch("app.editor.time.sleep"):
status = editor.login_status({"debug_port": 9222}, timeout=1)
self.assertTrue(status["logged_in"])
self.assertIsNone(status["reason"])
self.assertEqual(2, status["probe_attempts"])
self.assertEqual(["ws-closing", "ws-live"], [item.ws for item in FakeCDP.instances])
self.assertTrue(all(item.closed for item in FakeCDP.instances))
def test_login_status_target_failure_is_not_no_session_cookie(self):
page = {
"id": "target-closing",
"type": "page",
"url": "https://seller.shopee.tw/portal/product/48663456321",
"webSocketDebuggerUrl": "ws-closing",
}
def cdp_factory(ws):
instance = FakeCDP(ws, url=page["url"])
def send(method, params=None):
if method == "Network.getAllCookies":
raise RuntimeError("Target closed")
return {}
instance.send = send
return instance
with mock.patch("app.editor.http_get", return_value=[page]), mock.patch(
"app.editor.CDP",
side_effect=cdp_factory,
):
status = editor.login_status({"debug_port": 9222}, timeout=0)
self.assertFalse(status["logged_in"])
self.assertEqual("LOGIN_CHECK_TARGET_UNAVAILABLE", status["reason"])
self.assertFalse(status["cookie_read_succeeded"])
self.assertNotEqual("NO_SESSION_COOKIE", status["reason"])
def test_login_status_url_probe_failure_is_not_no_session_cookie(self):
page = {
"id": "target-closing",
"type": "page",
"url": "https://seller.shopee.tw/portal/product/48663456321",
"webSocketDebuggerUrl": "ws-closing",
}
instance = FakeCDP("ws-closing", url=page["url"], cookies=[])
def fail_url_probe(_expression):
raise RuntimeError("Target closed")
instance.val = fail_url_probe
with mock.patch("app.editor.http_get", return_value=[page]), mock.patch(
"app.editor.CDP",
return_value=instance,
):
status = editor.login_status({"debug_port": 9222}, timeout=0)
self.assertFalse(status["logged_in"])
self.assertEqual("LOGIN_CHECK_TARGET_UNAVAILABLE", status["reason"])
self.assertFalse(status["cookie_read_succeeded"])
def test_login_status_false_without_session_cookie(self):
with mock.patch(
"app.editor.http_get",
@@ -604,6 +702,35 @@ class EditorLoginTests(unittest.TestCase):
close_tab.assert_not_called()
create_tab_info.assert_not_called()
def test_open_product_background_failure_confirms_created_target_closed(self):
fake = FakeProductCDP(
"ws-new",
ready=False,
toasts=[{"text": "please input correct product id", "visible": False}],
)
with mock.patch("app.editor.find_product_tab", return_value=None), mock.patch(
"app.editor.create_tab_info",
return_value={"id": "target-new", "webSocketDebuggerUrl": "ws-new"},
), mock.patch("app.editor.CDP", return_value=fake), mock.patch(
"app.editor._ensure_page_domains"
), mock.patch(
"app.editor.close_tab_and_wait",
return_value=True,
) as close_tab_and_wait:
with self.assertRaises(editor.EditorError):
editor.open_product(
{"debug_port": 9223},
"bad-item",
bring_to_front=False,
)
self.assertTrue(fake.closed)
close_tab_and_wait.assert_called_once_with(
"target-new",
host="127.0.0.1:9223",
timeout=2.0,
)
def test_collect_closes_only_auto_created_product_tab(self):
cdp = FakeProductCDP("ws-new")
cdp.target_id = "target-new"
@@ -619,7 +746,7 @@ class EditorLoginTests(unittest.TestCase):
), mock.patch(
"app.editor.download_cover",
return_value="images/main/51100639510_old.jpg",
), mock.patch("app.editor.close_tab", return_value=True) as close_tab:
), mock.patch("app.editor.close_tab_and_wait", return_value=True) as close_tab_and_wait:
result = editor.collect(
{"debug_port": 9222},
{"item_id": "51100639510", "old_cover_path": "old.jpg"},
@@ -633,7 +760,12 @@ class EditorLoginTests(unittest.TestCase):
bring_to_front=False,
)
self.assertTrue(cdp.closed)
close_tab.assert_called_once_with("target-new", host="127.0.0.1:9222")
self.assertTrue(result["close_target_confirmed"])
close_tab_and_wait.assert_called_once_with(
"target-new",
host="127.0.0.1:9222",
timeout=2.0,
)
def test_collect_keeps_reused_product_tab_open(self):
cdp = FakeProductCDP("ws-existing")
@@ -650,14 +782,15 @@ class EditorLoginTests(unittest.TestCase):
), mock.patch(
"app.editor.download_cover",
return_value="images/main/51100639510_old.jpg",
), mock.patch("app.editor.close_tab") as close_tab:
editor.collect(
), mock.patch("app.editor.close_tab_and_wait") as close_tab_and_wait:
result = editor.collect(
{"debug_port": 9222},
{"item_id": "51100639510", "old_cover_path": "old.jpg"},
)
self.assertTrue(cdp.closed)
close_tab.assert_not_called()
self.assertIsNone(result["close_target_confirmed"])
close_tab_and_wait.assert_not_called()
def test_read_product_image_urls_returns_all_images_in_page_order(self):
cdp = FakeProductCDP("ws-existing", rects=cover_rects(3, prefix="main"))