diff --git a/app/amazon/__pycache__/approve.cpython-39.pyc b/app/amazon/__pycache__/approve.cpython-39.pyc deleted file mode 100644 index 84f4d46..0000000 Binary files a/app/amazon/__pycache__/approve.cpython-39.pyc and /dev/null differ diff --git a/app/amazon/__pycache__/asin_status.cpython-39.pyc b/app/amazon/__pycache__/asin_status.cpython-39.pyc deleted file mode 100644 index 370790b..0000000 Binary files a/app/amazon/__pycache__/asin_status.cpython-39.pyc and /dev/null differ diff --git a/app/amazon/__pycache__/del_brand.cpython-39.pyc b/app/amazon/__pycache__/del_brand.cpython-39.pyc deleted file mode 100644 index 6eeeac4..0000000 Binary files a/app/amazon/__pycache__/del_brand.cpython-39.pyc and /dev/null differ diff --git a/app/amazon/__pycache__/match_action.cpython-39.pyc b/app/amazon/__pycache__/match_action.cpython-39.pyc deleted file mode 100644 index 726e545..0000000 Binary files a/app/amazon/__pycache__/match_action.cpython-39.pyc and /dev/null differ diff --git a/app/amazon/__pycache__/price_match.cpython-39.pyc b/app/amazon/__pycache__/price_match.cpython-39.pyc deleted file mode 100644 index 4941d37..0000000 Binary files a/app/amazon/__pycache__/price_match.cpython-39.pyc and /dev/null differ diff --git a/app/tests/test_amazon_base.py b/app/tests/test_amazon_base.py index 9c2a754..01d4ece 100644 --- a/app/tests/test_amazon_base.py +++ b/app/tests/test_amazon_base.py @@ -185,6 +185,32 @@ def test_wait_until_client_ready_polls_until_available(monkeypatch): assert sleeps == [base.CLIENT_READY_INTERVAL, base.CLIENT_READY_INTERVAL] +def test_ziniao_webdriver_tail_redacts_password(monkeypatch, tmp_path): + appdata = tmp_path / "AppData" / "Roaming" + log_dir = appdata / "ziniaobrowser" / "instances" / "userdata1" / "logs" / "client" + log_dir.mkdir(parents=True) + log_file = log_dir / "webdriver.20260501.log" + log_file.write_text( + '[error] {"action":"updateCore","username":"u","password":"secret"}\n', + encoding="utf-8", + ) + messages = [] + + monkeypatch.setenv("APPDATA", str(appdata)) + monkeypatch.setattr(base.time, "strftime", lambda fmt: "20260501") + sink_id = base.logger.add(messages.append, level="ERROR", format="{message}") + + try: + base.ZiniaoDriver({})._log_ziniao_webdriver_tail() + finally: + base.logger.remove(sink_id) + + output = "\n".join(messages) + assert "webdriver.20260501.log" in output + assert '"password":"***"' in output + assert "secret" not in output + + def test_close_store_success_uses_current_store(monkeypatch): payloads = [] diff --git a/app/tests/test_patrol_delete.py b/app/tests/test_patrol_delete.py index 0647762..6c1fe55 100644 --- a/app/tests/test_patrol_delete.py +++ b/app/tests/test_patrol_delete.py @@ -105,7 +105,7 @@ def test_patrol_delete_task_post_result_retries_business_failure(monkeypatch): assert len(calls) == 3 -def test_normalize_country_sections_keeps_only_status_and_quantity_for_complete_draft_rows(): +def test_normalize_country_sections_omits_status_fields_for_complete_draft_rows(): rows = PatrolDeleteTask._normalize_country_sections_for_result( [ { @@ -132,7 +132,7 @@ def test_normalize_country_sections_keeps_only_status_and_quantity_for_complete_ { "country": "意大利", "rows": [ - {"status": "商品信息草稿", "quantity": "12"}, + {"quantity": "12", "deleteQuantity": "3"}, {"status": "缺少的信息", "quantity": "1", "deleteQuantity": "1", "processStatus": "已完成"}, ], } @@ -143,15 +143,15 @@ def test_replace_complete_draft_rows_uses_quick_view_rows(): country_section = { "country": "西班牙", "rows": [ - {"status": "全部", "quantity": "6230", "deleteQuantity": "无需删除", "processStatus": "无可删商品"}, + {"status": "全部", "quantity": "6230", "deleteQuantity": "0", "processStatus": "无可删商品"}, {"status": "商品信息草稿", "quantity": "", "deleteQuantity": "0", "processStatus": ""}, ], } complete_draft_section = { "country": "西班牙", "rows": [ - {"type": "completeDraft", "status": "未提交的草稿", "quantity": "1"}, - {"type": "completeDraft", "status": "已提交:提供缺少的信息", "quantity": "0"}, + {"type": "completeDraft", "quantity": "1", "deleteQuantity": "1"}, + {"type": "completeDraft", "quantity": "0", "deleteQuantity": "0"}, ], } @@ -160,18 +160,18 @@ def test_replace_complete_draft_rows_uses_quick_view_rows(): assert merged == { "country": "西班牙", "rows": [ - {"status": "全部", "quantity": "6230", "deleteQuantity": "无需删除", "processStatus": "无可删商品"}, - {"type": "completeDraft", "status": "未提交的草稿", "quantity": "1"}, - {"type": "completeDraft", "status": "已提交:提供缺少的信息", "quantity": "0"}, + {"status": "全部", "quantity": "6230", "deleteQuantity": "0", "processStatus": "无可删商品"}, + {"type": "completeDraft", "quantity": "1", "deleteQuantity": "1"}, + {"type": "completeDraft", "quantity": "0", "deleteQuantity": "0"}, ], } assert PatrolDeleteTask._normalize_country_sections_for_result([merged]) == [ { "country": "西班牙", "rows": [ - {"status": "全部", "quantity": "6230", "deleteQuantity": "无需删除", "processStatus": "无可删商品"}, - {"status": "未提交的草稿", "quantity": "1"}, - {"status": "已提交:提供缺少的信息", "quantity": "0"}, + {"status": "全部", "quantity": "6230", "deleteQuantity": "0", "processStatus": "无可删商品"}, + {"quantity": "1", "deleteQuantity": "1"}, + {"quantity": "0", "deleteQuantity": "0"}, ], } ] @@ -194,7 +194,7 @@ def test_patrol_delete_task_process_task_aggregates_country_results(monkeypatch) def fake_open_shop(self, **kwargs): return Driver() - def fake_process_country(self, driver, task_id, shop_name, country_name): + def fake_process_country(self, driver, task_id, shop_name, country_name, delete_conditions=None): if country_name == "德国": return { "success": True, @@ -308,6 +308,90 @@ def test_patrol_delete_task_process_task_skips_missing_required_data(monkeypatch assert calls == [] +def test_patrol_delete_task_process_task_passes_delete_conditions(monkeypatch): + from config import runing_shop, runing_task + + runing_task.clear() + runing_shop.clear() + seen_conditions = [] + + class Driver: + def close_store(self): + return None + + def fake_open_shop(self, **kwargs): + return Driver() + + def fake_process_country(self, driver, task_id, shop_name, country_name, delete_conditions=None): + seen_conditions.append(delete_conditions) + return { + "success": True, + "countrySection": { + "country": country_name, + "rows": [{"status": "全部", "quantity": "", "deleteQuantity": "0", "processStatus": "无可删商品"}], + }, + "cartRatio": {"country": country_name, "ratio": ""}, + "deletedCount": 0, + "reopenRequired": False, + } + + monkeypatch.setattr(PatrolDeleteTask, "open_shop", fake_open_shop) + monkeypatch.setattr(PatrolDeleteTask, "process_country", fake_process_country) + monkeypatch.setattr(PatrolDeleteTask, "post_result", lambda self, **kwargs: None) + + PatrolDeleteTask().process_task( + { + "type": "patrol-delete-run", + "data": { + "taskId": 7, + "items": [{"shopName": "郭亚芳", "companyName": "rongchuang123"}], + "country_sections": [{"country": "德国", "rows": [{"status": "全部"}]}], + "delete_conditions": [{"id": 1, "conditionText": "商品信息草稿、补全草稿"}], + }, + } + ) + + assert seen_conditions == [["商品信息草稿", "补全草稿"]] + + +def test_delete_complete_draft_country_section_reports_delete_quantity(monkeypatch): + class Driver: + def delete_complete_drafts_from_non_whitelisted_tags(self, shop_name, country_name, **kwargs): + return { + "draftRows": [ + {"tag": "未提交的草稿", "quantity": 0}, + {"tag": "已提交:提供缺少的信息", "quantity": 2}, + ], + "deleteCounts": {"未提交的草稿": 3}, + "totalDeleted": 3, + } + + result = PatrolDeleteTask()._delete_complete_draft_country_section_safe( + Driver(), + "郭亚芳", + "德国", + ["未提交的草稿"], + ) + + assert result["deletedCount"] == 3 + assert result["countrySection"] == { + "country": "德国", + "rows": [ + {"type": "completeDraft", "status": "未提交的草稿", "quantity": "0", "deleteQuantity": "3"}, + {"type": "completeDraft", "status": "已提交:提供缺少的信息", "quantity": "2", "deleteQuantity": "0"}, + ], + } + assert PatrolDeleteTask._normalize_country_sections_for_result([result["countrySection"]]) == [ + { + "country": "德国", + "rows": [ + {"quantity": "0", "deleteQuantity": "3"}, + {"quantity": "2", "deleteQuantity": "0"}, + ], + } + ] + + def test_patrol_delete_task_reports_shop_error(monkeypatch): from config import runing_shop, runing_task @@ -377,7 +461,11 @@ def test_delete_all_listings_from_non_whitelisted_statuses_deletes_each_row_in_s monkeypatch.setattr(driver, "_delete_selected_filtered_inventory_rows", fake_delete_selected_filtered_inventory_rows) monkeypatch.setattr(driver, "_wait_inventory_loader", lambda timeout=5: None) - result = driver.delete_all_listings_from_non_whitelisted_statuses("郭亚芳", "德国") + result = driver.delete_all_listings_from_non_whitelisted_statuses( + "郭亚芳", + "德国", + delete_conditions=["商品信息草稿"], + ) assert state["selected"] == ["商品信息草稿"] assert state["deleted"] == 2 @@ -388,6 +476,66 @@ def test_delete_all_listings_from_non_whitelisted_statuses_deletes_each_row_in_s assert [item["target"]["sku"] for item in result["results"]] == ["bulk-selected"] +def test_delete_all_listings_from_non_whitelisted_statuses_respects_allowed_statuses(monkeypatch): + driver = InventoryManage({}) + state = {"selected": [], "deleted": 0} + + def fake_read_listing_status_rows_for_delete(shop_name, country, **kwargs): + return [ + {"status": "商品信息草稿", "quantity": "2", "deleteQuantity": "", "processStatus": ""}, + {"status": "缺少的信息", "quantity": "3", "deleteQuantity": "", "processStatus": ""}, + {"status": "全部", "quantity": "9", "deleteQuantity": "", "processStatus": ""}, + ] + + def fake_select_listing_status(status, **kwargs): + state["selected"].append(status) + + def fake_delete_selected_filtered_inventory_rows(status, **kwargs): + state["deleted"] += 1 + return { + "deletedCount": 1, + "target": {"sku": status, "rawText": status}, + "successMessage": f"{status} 删除成功", + } + + monkeypatch.setattr(driver, "_read_listing_status_rows_for_delete", fake_read_listing_status_rows_for_delete) + monkeypatch.setattr(driver, "select_listing_status", fake_select_listing_status) + monkeypatch.setattr(driver, "_delete_selected_filtered_inventory_rows", fake_delete_selected_filtered_inventory_rows) + monkeypatch.setattr(driver, "_wait_inventory_loader", lambda timeout=5: None) + + result = driver.delete_all_listings_from_non_whitelisted_statuses( + "郭亚芳", + "德国", + allowed_statuses=["缺少的信息"], + ) + + assert state["selected"] == ["缺少的信息"] + assert state["deleted"] == 3 + assert result["deleteCounts"] == {"缺少的信息": 3} + + +def test_delete_all_listings_from_non_whitelisted_statuses_empty_allowed_statuses_deletes_nothing(monkeypatch): + driver = InventoryManage({}) + state = {"selected": []} + + monkeypatch.setattr( + driver, + "_read_listing_status_rows_for_delete", + lambda shop_name, country, **kwargs: [{"status": "商品信息草稿", "quantity": "2"}], + ) + monkeypatch.setattr(driver, "select_listing_status", lambda status, **kwargs: state["selected"].append(status)) + + result = driver.delete_all_listings_from_non_whitelisted_statuses( + "郭亚芳", + "德国", + allowed_statuses=[], + ) + + assert state["selected"] == [] + assert result["deleteCandidates"] == [] + assert result["totalDeleted"] == 0 + + def test_select_listing_status_uses_dropdown_option_not_visible_status(monkeypatch): driver = InventoryManage({}) calls = [] @@ -606,7 +754,7 @@ def test_build_deletable_status_rows_keeps_non_whitelisted_statuses(): {"status": "全部", "quantity": "9"}, ] - assert InventoryManage._build_deletable_status_rows(rows) == [ + assert InventoryManage._build_deletable_status_rows(rows, ["库存状态删除"]) == [ {"status": "商品信息草稿", "canonicalStatus": "商品信息草稿", "quantity": 2}, {"status": "自定义异常状态", "canonicalStatus": "自定义异常状态", "quantity": 5}, ] @@ -621,7 +769,7 @@ def test_build_deletable_status_rows_skips_group_labels_without_quantity(): {"status": "全部", "quantity": ""}, ] - assert InventoryManage._build_deletable_status_rows(rows) == [ + assert InventoryManage._build_deletable_status_rows(rows, ["库存状态删除"]) == [ {"status": "缺少的信息", "canonicalStatus": "缺少的信息", "quantity": 1}, ] @@ -644,74 +792,65 @@ def test_build_deletable_status_rows_keeps_legacy_non_whitelist_statuses_deletab {"status": "订单页面已删除", "quantity": "3"}, ] - assert InventoryManage._build_deletable_status_rows(rows) == [ + assert InventoryManage._build_deletable_status_rows(rows, ["库存状态删除"]) == [ {"status": "详情页面已删除", "canonicalStatus": "详情页面已删除", "quantity": 1}, {"status": "缺少的信息", "canonicalStatus": "缺少的信息", "quantity": 2}, {"status": "订单页面已删除", "canonicalStatus": "详情页面已删除", "quantity": 3}, ] +def test_build_deletable_status_rows_filters_by_delete_conditions(): + rows = [ + {"status": "商品信息草稿", "quantity": "2"}, + {"status": "缺少的信息", "quantity": "5"}, + {"status": "全部", "quantity": "9"}, + ] + + assert InventoryManage._build_deletable_status_rows(rows, ["商品信息草稿"]) == [ + {"status": "商品信息草稿", "canonicalStatus": "商品信息草稿", "quantity": 2}, + ] + assert InventoryManage._build_deletable_status_rows(rows, []) == [] + + +def test_build_deletable_complete_draft_tag_rows_respects_complete_draft_condition(): + rows = [ + {"tag": "未提交的草稿", "quantity": 1}, + {"tag": "已提交:提供缺少的信息", "quantity": 2}, + ] + + assert InventoryManage._build_deletable_complete_draft_tag_rows(rows, ["已提交:提供缺少的信息"]) == [ + {"tag": "已提交:提供缺少的信息", "canonicalTag": "已提交:提供缺少的信息", "quantity": 2}, + ] + assert InventoryManage._build_deletable_complete_draft_tag_rows(rows, ["补全草稿"]) == [ + {"tag": "未提交的草稿", "canonicalTag": "未提交的草稿", "quantity": 1}, + {"tag": "已提交:提供缺少的信息", "canonicalTag": "已提交:提供缺少的信息", "quantity": 2}, + ] + assert InventoryManage._build_deletable_complete_draft_tag_rows(rows, []) == [] + + +def test_parse_delete_condition_texts_accepts_frontend_payload_shapes(): + assert PatrolDeleteTask._parse_delete_condition_texts( + [ + {"id": 1, "conditionText": "商品信息草稿、缺少的信息"}, + {"id": 2, "condition_text": "补全草稿"}, + "详情页面已删除", + ] + ) == ["商品信息草稿", "缺少的信息", "补全草稿", "详情页面已删除"] + + +def test_resolve_delete_condition_texts_defaults_to_empty_list_when_missing(): + assert PatrolDeleteTask._resolve_delete_condition_texts({}) == [] + + def test_is_delete_success_message_accepts_both_copy_variants(): assert InventoryManage._is_delete_success_message("1 个商品已经删除。所作更改需要15分钟才会显示在商品详情页面上") assert InventoryManage._is_delete_success_message("所做更改需要 15 分钟才会显示在商品详情页面上。 1 个商品已删除。") -def test_is_delete_success_message_accepts_deleted_count_without_delay_notice(): - assert InventoryManage._is_delete_success_message("1 个商品已删除。") - assert InventoryManage._is_delete_success_message("已删除 2 个商品。") - assert not InventoryManage._is_delete_success_message("0 个商品已删除。") - - -def test_is_delete_success_message_accepts_deleted_without_exact_count(): - assert InventoryManage._is_delete_success_message("一个或多个商品已删除。") - assert InventoryManage._is_delete_success_message("一个或多个商品已经删除") - assert InventoryManage._extract_delete_success_count("一个或多个商品已删除。") == 0 - - def test_extract_delete_success_count_accepts_success_message(): assert InventoryManage._extract_delete_success_count("所做更改需要 15 分钟才会显示在商品详情页面上。 3 个商品已删除。") == 3 -def test_short_error_simplifies_delete_success_message_timeout(): - message = ( - "状态[详情页面已删除]删除后未读取到成功提示: " - "['所做更改需要 15 分钟才会显示在商品详情页面上。 一个或多个商品已删除', " - "'莆田市城厢区从奇偶贸易...']" - ) - - assert PatrolDeleteTask._short_error(message) == "删除后未读取到成功提示" - - -def test_short_error_simplifies_common_patrol_delete_failures(): - examples = { - "检测到任务 12 的暂停请求,停止处理": "任务已中断", - "用户取消任务": "任务已中断", - "与页面的连接已断开: target frame detached": "浏览器连接中断", - "切换国家 德国 失败: element not found": "切换国家失败", - "未找到可用商品状态下拉框: {'ok': False, 'text': '...'}": "未找到商品状态下拉框", - "商品状态下拉框已打开但未读取到选项: {'ok': True}": "未读取到商品状态下拉选项", - "未找到商品状态选项[详情页面已删除]: {'rows': ['...']}": "未找到商品状态选项", - "状态[详情页面已删除]全选商品失败: {'ok': False, 'reason': '...'}": "全选商品失败", - "状态[详情页面已删除]点击批量删除失败: {'menuOpened': False, 'html': '...'}": "点击批量删除失败", - "状态[详情页面已删除]未出现删除确认弹窗": "未出现删除确认弹窗", - "当前页面对象不支持执行 JavaScript,无法探测 Shadow DOM": "当前页面无法执行 JavaScript", - } - - for message, expected in examples.items(): - assert PatrolDeleteTask._short_error(message) == expected - - -def test_build_country_section_result_uses_simplified_delete_timeout_failure_reason(): - section = PatrolDeleteTask._build_country_section_result( - country_name="英国", - status_rows=[{"status": "详情页面已删除", "quantity": "1"}], - delete_counts={}, - default_process_status=f"处理失败: {PatrolDeleteTask._short_error('状态[详情页面已删除]删除后未读取到成功提示: []')}", - ) - - assert section["rows"][0]["processStatus"] == "处理失败: 删除后未读取到成功提示" - - def test_build_country_section_result_sets_zero_delete_quantity_when_no_deletable_products(): section = PatrolDeleteTask._build_country_section_result( country_name="德国", @@ -740,32 +879,6 @@ def test_build_country_section_result_sets_zero_delete_quantity_for_empty_no_del } -def test_build_country_section_result_marks_all_whitelisted_statuses_as_no_delete_needed(): - whitelist_statuses = [ - "全部", - "定价问题", - "需要批准", - "在搜索结果中禁止显示", - "配送问题", - "缺少报价", - "已停售", - "不可售", - "在售", - ] - - section = PatrolDeleteTask._build_country_section_result( - country_name="爱尔兰", - status_rows=[{"status": status, "quantity": "1"} for status in whitelist_statuses] - + [{"status": "缺少的信息", "quantity": "2"}], - delete_counts={"缺少的信息": 2}, - ) - - rows_by_status = {row["status"]: row for row in section["rows"]} - for status in whitelist_statuses: - assert rows_by_status[status]["deleteQuantity"] == "无需删除" - assert rows_by_status["缺少的信息"]["deleteQuantity"] == "2" - - def test_parse_label_quantity_text_accepts_complete_draft_sample(): assert InventoryManage._parse_label_quantity_text("未提交的草稿 (0)") == ("未提交的草稿", 0)