From 6cc17f869cf1506f282071b8c59fe582eb9d9789 Mon Sep 17 00:00:00 2001 From: Jens <1742418+1cu@users.noreply.github.com> Date: Sat, 24 Jan 2026 15:31:22 +0100 Subject: [PATCH] fix: keep shipping_type SHIPPING for individual postage (#785) --- src/kleinanzeigen_bot/extract.py | 6 +-- tests/unit/test_extract.py | 78 +++++++++++++++++++++++++++++++- 2 files changed, 80 insertions(+), 4 deletions(-) diff --git a/src/kleinanzeigen_bot/extract.py b/src/kleinanzeigen_bot/extract.py index e4ba8f1..03def4b 100644 --- a/src/kleinanzeigen_bot/extract.py +++ b/src/kleinanzeigen_bot/extract.py @@ -553,7 +553,7 @@ class AdExtractor(WebScrapingMixin): # Find all options with the same price to determine the package size matching_options = [opt for opt in shipping_costs if opt["priceInEuroCent"] == price_in_cent] if not matching_options: - return "NOT_APPLICABLE", ship_costs, shipping_options + return "SHIPPING", ship_costs, None # Use the package size of the first matching option matching_size = matching_options[0]["packageSize"] @@ -570,11 +570,11 @@ class AdExtractor(WebScrapingMixin): # Only use the matching option if it's not excluded matching_option = next((x for x in shipping_costs if x["priceInEuroCent"] == price_in_cent), None) if not matching_option: - return "NOT_APPLICABLE", ship_costs, shipping_options + return "SHIPPING", ship_costs, None shipping_option = shipping_option_mapping.get(matching_option["id"]) if not shipping_option or shipping_option in self.config.download.excluded_shipping_options: - return "NOT_APPLICABLE", ship_costs, shipping_options + return "SHIPPING", ship_costs, None shipping_options = [shipping_option] except TimeoutError: # no pricing box -> no shipping given diff --git a/tests/unit/test_extract.py b/tests/unit/test_extract.py index a189e53..626ab7d 100644 --- a/tests/unit/test_extract.py +++ b/tests/unit/test_extract.py @@ -304,6 +304,38 @@ class TestAdExtractorShipping: else: assert options is None + @pytest.mark.asyncio + # pylint: disable=protected-access + async def test_extract_shipping_info_with_all_matching_options_no_match(self, test_extractor:AdExtractor) -> None: + """Test shipping extraction when include-all is enabled but no option matches the price.""" + shipping_response = { + "content": json.dumps( + { + "data": { + "shippingOptionsResponse": { + "options": [ + {"id": "DHL_001", "priceInEuroCent": 500, "packageSize": "SMALL"}, + {"id": "HERMES_001", "priceInEuroCent": 600, "packageSize": "SMALL"}, + ] + } + } + } + ) + } + + test_extractor.config.download = DownloadConfig.model_validate({"include_all_matching_shipping_options": True}) + + with ( + patch.object(test_extractor, "page", MagicMock()), + patch.object(test_extractor, "web_text", new_callable = AsyncMock, return_value = "+ Versand ab 4,89 €"), + patch.object(test_extractor, "web_request", new_callable = AsyncMock, return_value = shipping_response), + ): + shipping_type, costs, options = await test_extractor._extract_shipping_info_from_ad_page() + + assert shipping_type == "SHIPPING" + assert costs == 4.89 + assert options is None + @pytest.mark.asyncio # pylint: disable=protected-access async def test_extract_shipping_info_with_excluded_options(self, test_extractor:AdExtractor) -> None: @@ -370,10 +402,54 @@ class TestAdExtractorShipping: ): shipping_type, costs, options = await test_extractor._extract_shipping_info_from_ad_page() - assert shipping_type == "NOT_APPLICABLE" + assert shipping_type == "SHIPPING" assert costs == 4.89 assert options is None + @pytest.mark.asyncio + # pylint: disable=protected-access + async def test_extract_shipping_info_with_no_matching_option(self, test_extractor:AdExtractor) -> None: + """Test shipping info extraction when price exists but NO matching option in API response.""" + shipping_response = { + "content": json.dumps( + { + "data": { + "shippingOptionsResponse": { + "options": [ + {"id": "DHL_001", "priceInEuroCent": 500, "packageSize": "SMALL"}, + {"id": "HERMES_001", "priceInEuroCent": 600, "packageSize": "SMALL"}, + ] + } + } + } + ) + } + + with ( + patch.object(test_extractor, "page", MagicMock()), + patch.object(test_extractor, "web_text", new_callable = AsyncMock, return_value = "+ Versand ab 7,00 €"), + patch.object(test_extractor, "web_request", new_callable = AsyncMock, return_value = shipping_response), + ): + shipping_type, costs, options = await test_extractor._extract_shipping_info_from_ad_page() + + assert shipping_type == "SHIPPING" + assert costs == 7.0 + assert options is None + + @pytest.mark.asyncio + # pylint: disable=protected-access + async def test_extract_shipping_info_timeout(self, test_extractor:AdExtractor) -> None: + """Test shipping info extraction when shipping element is missing (TimeoutError).""" + with ( + patch.object(test_extractor, "page", MagicMock()), + patch.object(test_extractor, "web_text", new_callable = AsyncMock, side_effect = TimeoutError), + ): + shipping_type, costs, options = await test_extractor._extract_shipping_info_from_ad_page() + + assert shipping_type == "NOT_APPLICABLE" + assert costs is None + assert options is None + class TestAdExtractorNavigation: """Tests for navigation related functionality."""