mirror of
https://github.com/Second-Hand-Friends/kleinanzeigen-bot.git
synced 2026-03-12 02:31:45 +01:00
refact: apply consistent formatting
This commit is contained in:
@@ -12,31 +12,31 @@ from kleinanzeigen_bot.utils.web_scraping_mixin import Browser, By, Element
|
||||
|
||||
|
||||
class _DimensionsDict(TypedDict):
|
||||
dimension108: str
|
||||
dimension108:str
|
||||
|
||||
|
||||
class _UniversalAnalyticsOptsDict(TypedDict):
|
||||
dimensions: _DimensionsDict
|
||||
dimensions:_DimensionsDict
|
||||
|
||||
|
||||
class _BelenConfDict(TypedDict):
|
||||
universalAnalyticsOpts: _UniversalAnalyticsOptsDict
|
||||
universalAnalyticsOpts:_UniversalAnalyticsOptsDict
|
||||
|
||||
|
||||
class _SpecialAttributesDict(TypedDict, total = False):
|
||||
art_s: str
|
||||
condition_s: str
|
||||
art_s:str
|
||||
condition_s:str
|
||||
|
||||
|
||||
class _TestCaseDict(TypedDict): # noqa: PYI049 Private TypedDict `...` is never used
|
||||
belen_conf: _BelenConfDict
|
||||
expected: _SpecialAttributesDict
|
||||
belen_conf:_BelenConfDict
|
||||
expected:_SpecialAttributesDict
|
||||
|
||||
|
||||
class TestAdExtractorBasics:
|
||||
"""Basic synchronous tests for AdExtractor."""
|
||||
|
||||
def test_constructor(self, browser_mock: MagicMock, sample_config: dict[str, Any]) -> None:
|
||||
def test_constructor(self, browser_mock:MagicMock, sample_config:dict[str, Any]) -> None:
|
||||
"""Test the constructor of AdExtractor"""
|
||||
extractor = AdExtractor(browser_mock, sample_config)
|
||||
assert extractor.browser == browser_mock
|
||||
@@ -48,7 +48,7 @@ class TestAdExtractorBasics:
|
||||
("https://www.kleinanzeigen.de/s-anzeige/invalid-id/abc", -1),
|
||||
("https://www.kleinanzeigen.de/invalid-url", -1),
|
||||
])
|
||||
def test_extract_ad_id_from_ad_url(self, test_extractor: AdExtractor, url: str, expected_id: int) -> None:
|
||||
def test_extract_ad_id_from_ad_url(self, test_extractor:AdExtractor, url:str, expected_id:int) -> None:
|
||||
"""Test extraction of ad ID from different URL formats."""
|
||||
assert test_extractor.extract_ad_id_from_ad_url(url) == expected_id
|
||||
|
||||
@@ -66,19 +66,19 @@ class TestAdExtractorPricing:
|
||||
@pytest.mark.asyncio
|
||||
# pylint: disable=protected-access
|
||||
async def test_extract_pricing_info(
|
||||
self, test_extractor: AdExtractor, price_text: str, expected_price: int | None, expected_type: str
|
||||
self, test_extractor:AdExtractor, price_text:str, expected_price:int | None, expected_type:str
|
||||
) -> None:
|
||||
"""Test price extraction with different formats"""
|
||||
with patch.object(test_extractor, 'web_text', new_callable = AsyncMock, return_value = price_text):
|
||||
with patch.object(test_extractor, "web_text", new_callable = AsyncMock, return_value = price_text):
|
||||
price, price_type = await test_extractor._extract_pricing_info_from_ad_page()
|
||||
assert price == expected_price
|
||||
assert price_type == expected_type
|
||||
|
||||
@pytest.mark.asyncio
|
||||
# pylint: disable=protected-access
|
||||
async def test_extract_pricing_info_timeout(self, test_extractor: AdExtractor) -> None:
|
||||
async def test_extract_pricing_info_timeout(self, test_extractor:AdExtractor) -> None:
|
||||
"""Test price extraction when element is not found"""
|
||||
with patch.object(test_extractor, 'web_text', new_callable = AsyncMock, side_effect = TimeoutError):
|
||||
with patch.object(test_extractor, "web_text", new_callable = AsyncMock, side_effect = TimeoutError):
|
||||
price, price_type = await test_extractor._extract_pricing_info_from_ad_page()
|
||||
assert price is None
|
||||
assert price_type == "NOT_APPLICABLE"
|
||||
@@ -95,15 +95,15 @@ class TestAdExtractorShipping:
|
||||
@pytest.mark.asyncio
|
||||
# pylint: disable=protected-access
|
||||
async def test_extract_shipping_info(
|
||||
self, test_extractor: AdExtractor, shipping_text: str, expected_type: str, expected_cost: float | None
|
||||
self, test_extractor:AdExtractor, shipping_text:str, expected_type:str, expected_cost:float | None
|
||||
) -> None:
|
||||
"""Test shipping info extraction with different text formats."""
|
||||
with patch.object(test_extractor, 'page', MagicMock()), \
|
||||
patch.object(test_extractor, 'web_text', new_callable = AsyncMock, return_value = shipping_text), \
|
||||
patch.object(test_extractor, 'web_request', new_callable = AsyncMock) as mock_web_request:
|
||||
with patch.object(test_extractor, "page", MagicMock()), \
|
||||
patch.object(test_extractor, "web_text", new_callable = AsyncMock, return_value = shipping_text), \
|
||||
patch.object(test_extractor, "web_request", new_callable = AsyncMock) as mock_web_request:
|
||||
|
||||
if expected_cost:
|
||||
shipping_response: dict[str, Any] = {
|
||||
shipping_response:dict[str, Any] = {
|
||||
"data": {
|
||||
"shippingOptionsResponse": {
|
||||
"options": [
|
||||
@@ -125,7 +125,7 @@ class TestAdExtractorShipping:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
# pylint: disable=protected-access
|
||||
async def test_extract_shipping_info_with_options(self, test_extractor: AdExtractor) -> None:
|
||||
async def test_extract_shipping_info_with_options(self, test_extractor:AdExtractor) -> None:
|
||||
"""Test shipping info extraction with shipping options."""
|
||||
shipping_response = {
|
||||
"content": json.dumps({
|
||||
@@ -139,9 +139,9 @@ class TestAdExtractorShipping:
|
||||
})
|
||||
}
|
||||
|
||||
with patch.object(test_extractor, 'page', MagicMock()), \
|
||||
patch.object(test_extractor, 'web_text', new_callable = AsyncMock, return_value = "+ Versand ab 5,49 €"), \
|
||||
patch.object(test_extractor, 'web_request', new_callable = AsyncMock, return_value = shipping_response):
|
||||
with patch.object(test_extractor, "page", MagicMock()), \
|
||||
patch.object(test_extractor, "web_text", new_callable = AsyncMock, return_value = "+ Versand ab 5,49 €"), \
|
||||
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()
|
||||
|
||||
@@ -154,21 +154,21 @@ class TestAdExtractorNavigation:
|
||||
"""Tests for navigation related functionality."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_navigate_to_ad_page_with_url(self, test_extractor: AdExtractor) -> None:
|
||||
async def test_navigate_to_ad_page_with_url(self, test_extractor:AdExtractor) -> None:
|
||||
"""Test navigation to ad page using a URL."""
|
||||
page_mock = AsyncMock()
|
||||
page_mock.url = "https://www.kleinanzeigen.de/s-anzeige/test/12345"
|
||||
|
||||
with patch.object(test_extractor, 'page', page_mock), \
|
||||
patch.object(test_extractor, 'web_open', new_callable = AsyncMock) as mock_web_open, \
|
||||
patch.object(test_extractor, 'web_find', new_callable = AsyncMock, side_effect = TimeoutError):
|
||||
with patch.object(test_extractor, "page", page_mock), \
|
||||
patch.object(test_extractor, "web_open", new_callable = AsyncMock) as mock_web_open, \
|
||||
patch.object(test_extractor, "web_find", new_callable = AsyncMock, side_effect = TimeoutError):
|
||||
|
||||
result = await test_extractor.naviagte_to_ad_page("https://www.kleinanzeigen.de/s-anzeige/test/12345")
|
||||
assert result is True
|
||||
mock_web_open.assert_called_with("https://www.kleinanzeigen.de/s-anzeige/test/12345")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_navigate_to_ad_page_with_id(self, test_extractor: AdExtractor) -> None:
|
||||
async def test_navigate_to_ad_page_with_id(self, test_extractor:AdExtractor) -> None:
|
||||
"""Test navigation to ad page using an ID."""
|
||||
page_mock = AsyncMock()
|
||||
page_mock.url = "https://www.kleinanzeigen.de/s-anzeige/test/12345"
|
||||
@@ -186,7 +186,7 @@ class TestAdExtractorNavigation:
|
||||
popup_close_mock.click = AsyncMock()
|
||||
popup_close_mock.apply = AsyncMock(return_value = True)
|
||||
|
||||
def find_mock(selector_type: By, selector_value: str, **_: Any) -> Element | None:
|
||||
def find_mock(selector_type:By, selector_value:str, **_:Any) -> Element | None:
|
||||
if selector_type == By.ID and selector_value == "site-search-query":
|
||||
return input_mock
|
||||
if selector_type == By.ID and selector_value == "site-search-submit":
|
||||
@@ -195,20 +195,20 @@ class TestAdExtractorNavigation:
|
||||
return popup_close_mock
|
||||
return None
|
||||
|
||||
with patch.object(test_extractor, 'page', page_mock), \
|
||||
patch.object(test_extractor, 'web_open', new_callable = AsyncMock) as mock_web_open, \
|
||||
patch.object(test_extractor, 'web_input', new_callable = AsyncMock), \
|
||||
patch.object(test_extractor, 'web_check', new_callable = AsyncMock, return_value = True), \
|
||||
patch.object(test_extractor, 'web_find', new_callable = AsyncMock, side_effect = find_mock):
|
||||
with patch.object(test_extractor, "page", page_mock), \
|
||||
patch.object(test_extractor, "web_open", new_callable = AsyncMock) as mock_web_open, \
|
||||
patch.object(test_extractor, "web_input", new_callable = AsyncMock), \
|
||||
patch.object(test_extractor, "web_check", new_callable = AsyncMock, return_value = True), \
|
||||
patch.object(test_extractor, "web_find", new_callable = AsyncMock, side_effect = find_mock):
|
||||
|
||||
result = await test_extractor.naviagte_to_ad_page(12345)
|
||||
assert result is True
|
||||
mock_web_open.assert_called_with('https://www.kleinanzeigen.de/')
|
||||
mock_web_open.assert_called_with("https://www.kleinanzeigen.de/")
|
||||
submit_button_mock.click.assert_awaited_once()
|
||||
popup_close_mock.click.assert_awaited_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_navigate_to_ad_page_with_popup(self, test_extractor: AdExtractor) -> None:
|
||||
async def test_navigate_to_ad_page_with_popup(self, test_extractor:AdExtractor) -> None:
|
||||
"""Test navigation to ad page with popup handling."""
|
||||
page_mock = AsyncMock()
|
||||
page_mock.url = "https://www.kleinanzeigen.de/s-anzeige/test/12345"
|
||||
@@ -218,18 +218,18 @@ class TestAdExtractorNavigation:
|
||||
input_mock.send_keys = AsyncMock()
|
||||
input_mock.apply = AsyncMock(return_value = True)
|
||||
|
||||
with patch.object(test_extractor, 'page', page_mock), \
|
||||
patch.object(test_extractor, 'web_open', new_callable = AsyncMock), \
|
||||
patch.object(test_extractor, 'web_find', new_callable = AsyncMock, return_value = input_mock), \
|
||||
patch.object(test_extractor, 'web_click', new_callable = AsyncMock) as mock_web_click, \
|
||||
patch.object(test_extractor, 'web_check', new_callable = AsyncMock, return_value = True):
|
||||
with patch.object(test_extractor, "page", page_mock), \
|
||||
patch.object(test_extractor, "web_open", new_callable = AsyncMock), \
|
||||
patch.object(test_extractor, "web_find", new_callable = AsyncMock, return_value = input_mock), \
|
||||
patch.object(test_extractor, "web_click", new_callable = AsyncMock) as mock_web_click, \
|
||||
patch.object(test_extractor, "web_check", new_callable = AsyncMock, return_value = True):
|
||||
|
||||
result = await test_extractor.naviagte_to_ad_page(12345)
|
||||
assert result is True
|
||||
mock_web_click.assert_called_with(By.CLASS_NAME, 'mfp-close')
|
||||
mock_web_click.assert_called_with(By.CLASS_NAME, "mfp-close")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_navigate_to_ad_page_invalid_id(self, test_extractor: AdExtractor) -> None:
|
||||
async def test_navigate_to_ad_page_invalid_id(self, test_extractor:AdExtractor) -> None:
|
||||
"""Test navigation to ad page with invalid ID."""
|
||||
page_mock = AsyncMock()
|
||||
page_mock.url = "https://www.kleinanzeigen.de/s-suchen.html?k0"
|
||||
@@ -240,22 +240,22 @@ class TestAdExtractorNavigation:
|
||||
input_mock.apply = AsyncMock(return_value = True)
|
||||
input_mock.attrs = {}
|
||||
|
||||
with patch.object(test_extractor, 'page', page_mock), \
|
||||
patch.object(test_extractor, 'web_open', new_callable = AsyncMock), \
|
||||
patch.object(test_extractor, 'web_find', new_callable = AsyncMock, return_value = input_mock):
|
||||
with patch.object(test_extractor, "page", page_mock), \
|
||||
patch.object(test_extractor, "web_open", new_callable = AsyncMock), \
|
||||
patch.object(test_extractor, "web_find", new_callable = AsyncMock, return_value = input_mock):
|
||||
|
||||
result = await test_extractor.naviagte_to_ad_page(99999)
|
||||
assert result is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_extract_own_ads_urls(self, test_extractor: AdExtractor) -> None:
|
||||
async def test_extract_own_ads_urls(self, test_extractor:AdExtractor) -> None:
|
||||
"""Test extraction of own ads URLs - basic test."""
|
||||
with patch.object(test_extractor, 'web_open', new_callable=AsyncMock), \
|
||||
patch.object(test_extractor, 'web_sleep', new_callable=AsyncMock), \
|
||||
patch.object(test_extractor, 'web_find', new_callable=AsyncMock) as mock_web_find, \
|
||||
patch.object(test_extractor, 'web_find_all', new_callable=AsyncMock) as mock_web_find_all, \
|
||||
patch.object(test_extractor, 'web_scroll_page_down', new_callable=AsyncMock), \
|
||||
patch.object(test_extractor, 'web_execute', new_callable=AsyncMock):
|
||||
with patch.object(test_extractor, "web_open", new_callable = AsyncMock), \
|
||||
patch.object(test_extractor, "web_sleep", new_callable = AsyncMock), \
|
||||
patch.object(test_extractor, "web_find", new_callable = AsyncMock) as mock_web_find, \
|
||||
patch.object(test_extractor, "web_find_all", new_callable = AsyncMock) as mock_web_find_all, \
|
||||
patch.object(test_extractor, "web_scroll_page_down", new_callable = AsyncMock), \
|
||||
patch.object(test_extractor, "web_execute", new_callable = AsyncMock):
|
||||
|
||||
# --- Setup mock objects for DOM elements ---
|
||||
# Mocks needed for the actual execution flow
|
||||
@@ -263,7 +263,7 @@ class TestAdExtractorNavigation:
|
||||
pagination_section_mock = MagicMock()
|
||||
cardbox_mock = MagicMock() # Represents the <li> element
|
||||
link_mock = MagicMock() # Represents the <a> element
|
||||
link_mock.attrs = {'href': '/s-anzeige/test/12345'} # Configure the desired output
|
||||
link_mock.attrs = {"href": "/s-anzeige/test/12345"} # Configure the desired output
|
||||
|
||||
# Mocks for elements potentially checked but maybe not strictly needed for output
|
||||
# (depending on how robust the mocking is)
|
||||
@@ -287,7 +287,7 @@ class TestAdExtractorNavigation:
|
||||
# 2. Find all '.cardbox' elements (inside loop)
|
||||
mock_web_find_all.side_effect = [
|
||||
[], # Call 1: find 'button[aria-label="Nächste"]' -> No next button = single page
|
||||
[cardbox_mock] # Call 2: find .cardbox -> One ad item
|
||||
[cardbox_mock] # Call 2: find .cardbox -> One ad item
|
||||
# Add more mocks here if pagination navigation calls web_find_all
|
||||
]
|
||||
|
||||
@@ -295,20 +295,20 @@ class TestAdExtractorNavigation:
|
||||
refs = await test_extractor.extract_own_ads_urls()
|
||||
|
||||
# --- Assertions ---
|
||||
assert refs == ['/s-anzeige/test/12345'] # Now it should match
|
||||
assert refs == ["/s-anzeige/test/12345"] # Now it should match
|
||||
|
||||
# Optional: Verify calls were made as expected
|
||||
mock_web_find.assert_has_calls([
|
||||
call(By.ID, 'my-manageitems-adlist'),
|
||||
call(By.CSS_SELECTOR, '.Pagination', timeout=10),
|
||||
call(By.ID, 'my-manageitems-adlist'),
|
||||
call(By.CSS_SELECTOR, 'div.manageitems-item-ad h3 a.text-onSurface', parent=cardbox_mock),
|
||||
], any_order=False) # Check order if important
|
||||
call(By.ID, "my-manageitems-adlist"),
|
||||
call(By.CSS_SELECTOR, ".Pagination", timeout = 10),
|
||||
call(By.ID, "my-manageitems-adlist"),
|
||||
call(By.CSS_SELECTOR, "div.manageitems-item-ad h3 a.text-onSurface", parent = cardbox_mock),
|
||||
], any_order = False) # Check order if important
|
||||
|
||||
mock_web_find_all.assert_has_calls([
|
||||
call(By.CSS_SELECTOR, 'button[aria-label="Nächste"]', parent=pagination_section_mock),
|
||||
call(By.CLASS_NAME, 'cardbox', parent=ad_list_container_mock),
|
||||
], any_order=False)
|
||||
call(By.CSS_SELECTOR, 'button[aria-label="Nächste"]', parent = pagination_section_mock),
|
||||
call(By.CLASS_NAME, "cardbox", parent = ad_list_container_mock),
|
||||
], any_order = False)
|
||||
|
||||
|
||||
class TestAdExtractorContent:
|
||||
@@ -318,14 +318,14 @@ class TestAdExtractorContent:
|
||||
@pytest.fixture
|
||||
def extractor_with_config(self) -> AdExtractor:
|
||||
"""Create extractor with specific config for testing prefix/suffix handling."""
|
||||
browser_mock = MagicMock(spec=Browser)
|
||||
browser_mock = MagicMock(spec = Browser)
|
||||
return AdExtractor(browser_mock, {}) # Empty config, will be overridden in tests
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_extract_description_with_affixes(
|
||||
self,
|
||||
test_extractor: AdExtractor,
|
||||
description_test_cases: list[tuple[dict[str, Any], str, str]]
|
||||
test_extractor:AdExtractor,
|
||||
description_test_cases:list[tuple[dict[str, Any], str, str]]
|
||||
) -> None:
|
||||
"""Test extraction of description with various prefix/suffix configurations."""
|
||||
# Mock the page
|
||||
@@ -337,18 +337,18 @@ class TestAdExtractorContent:
|
||||
test_extractor.config = config
|
||||
|
||||
with patch.multiple(test_extractor,
|
||||
web_text=AsyncMock(side_effect=[
|
||||
web_text = AsyncMock(side_effect = [
|
||||
"Test Title", # Title
|
||||
raw_description, # Raw description (without affixes)
|
||||
"03.02.2025" # Creation date
|
||||
]),
|
||||
_extract_category_from_ad_page=AsyncMock(return_value="160"),
|
||||
_extract_special_attributes_from_ad_page=AsyncMock(return_value={}),
|
||||
_extract_pricing_info_from_ad_page=AsyncMock(return_value=(None, "NOT_APPLICABLE")),
|
||||
_extract_shipping_info_from_ad_page=AsyncMock(return_value=("NOT_APPLICABLE", None, None)),
|
||||
_extract_sell_directly_from_ad_page=AsyncMock(return_value=False),
|
||||
_download_images_from_ad_page=AsyncMock(return_value=[]),
|
||||
_extract_contact_from_ad_page=AsyncMock(return_value={})
|
||||
_extract_category_from_ad_page = AsyncMock(return_value = "160"),
|
||||
_extract_special_attributes_from_ad_page = AsyncMock(return_value = {}),
|
||||
_extract_pricing_info_from_ad_page = AsyncMock(return_value = (None, "NOT_APPLICABLE")),
|
||||
_extract_shipping_info_from_ad_page = AsyncMock(return_value = ("NOT_APPLICABLE", None, None)),
|
||||
_extract_sell_directly_from_ad_page = AsyncMock(return_value = False),
|
||||
_download_images_from_ad_page = AsyncMock(return_value = []),
|
||||
_extract_contact_from_ad_page = AsyncMock(return_value = {})
|
||||
):
|
||||
info = await test_extractor._extract_ad_page_info("/some/dir", 12345)
|
||||
assert info["description"] == raw_description
|
||||
@@ -356,7 +356,7 @@ class TestAdExtractorContent:
|
||||
@pytest.mark.asyncio
|
||||
async def test_extract_description_with_affixes_timeout(
|
||||
self,
|
||||
test_extractor: AdExtractor
|
||||
test_extractor:AdExtractor
|
||||
) -> None:
|
||||
"""Test handling of timeout when extracting description."""
|
||||
# Mock the page
|
||||
@@ -365,18 +365,18 @@ class TestAdExtractorContent:
|
||||
test_extractor.page = page_mock
|
||||
|
||||
with patch.multiple(test_extractor,
|
||||
web_text=AsyncMock(side_effect=[
|
||||
web_text = AsyncMock(side_effect = [
|
||||
"Test Title", # Title succeeds
|
||||
TimeoutError("Timeout"), # Description times out
|
||||
"03.02.2025" # Date succeeds
|
||||
]),
|
||||
_extract_category_from_ad_page=AsyncMock(return_value="160"),
|
||||
_extract_special_attributes_from_ad_page=AsyncMock(return_value={}),
|
||||
_extract_pricing_info_from_ad_page=AsyncMock(return_value=(None, "NOT_APPLICABLE")),
|
||||
_extract_shipping_info_from_ad_page=AsyncMock(return_value=("NOT_APPLICABLE", None, None)),
|
||||
_extract_sell_directly_from_ad_page=AsyncMock(return_value=False),
|
||||
_download_images_from_ad_page=AsyncMock(return_value=[]),
|
||||
_extract_contact_from_ad_page=AsyncMock(return_value={})
|
||||
_extract_category_from_ad_page = AsyncMock(return_value = "160"),
|
||||
_extract_special_attributes_from_ad_page = AsyncMock(return_value = {}),
|
||||
_extract_pricing_info_from_ad_page = AsyncMock(return_value = (None, "NOT_APPLICABLE")),
|
||||
_extract_shipping_info_from_ad_page = AsyncMock(return_value = ("NOT_APPLICABLE", None, None)),
|
||||
_extract_sell_directly_from_ad_page = AsyncMock(return_value = False),
|
||||
_download_images_from_ad_page = AsyncMock(return_value = []),
|
||||
_extract_contact_from_ad_page = AsyncMock(return_value = {})
|
||||
):
|
||||
try:
|
||||
info = await test_extractor._extract_ad_page_info("/some/dir", 12345)
|
||||
@@ -388,7 +388,7 @@ class TestAdExtractorContent:
|
||||
@pytest.mark.asyncio
|
||||
async def test_extract_description_with_affixes_no_affixes(
|
||||
self,
|
||||
test_extractor: AdExtractor
|
||||
test_extractor:AdExtractor
|
||||
) -> None:
|
||||
"""Test extraction of description without any affixes in config."""
|
||||
# Mock the page
|
||||
@@ -399,24 +399,24 @@ class TestAdExtractorContent:
|
||||
raw_description = "Original Description"
|
||||
|
||||
with patch.multiple(test_extractor,
|
||||
web_text=AsyncMock(side_effect=[
|
||||
web_text = AsyncMock(side_effect = [
|
||||
"Test Title", # Title
|
||||
raw_description, # Description without affixes
|
||||
"03.02.2025" # Creation date
|
||||
]),
|
||||
_extract_category_from_ad_page=AsyncMock(return_value="160"),
|
||||
_extract_special_attributes_from_ad_page=AsyncMock(return_value={}),
|
||||
_extract_pricing_info_from_ad_page=AsyncMock(return_value=(None, "NOT_APPLICABLE")),
|
||||
_extract_shipping_info_from_ad_page=AsyncMock(return_value=("NOT_APPLICABLE", None, None)),
|
||||
_extract_sell_directly_from_ad_page=AsyncMock(return_value=False),
|
||||
_download_images_from_ad_page=AsyncMock(return_value=[]),
|
||||
_extract_contact_from_ad_page=AsyncMock(return_value={})
|
||||
_extract_category_from_ad_page = AsyncMock(return_value = "160"),
|
||||
_extract_special_attributes_from_ad_page = AsyncMock(return_value = {}),
|
||||
_extract_pricing_info_from_ad_page = AsyncMock(return_value = (None, "NOT_APPLICABLE")),
|
||||
_extract_shipping_info_from_ad_page = AsyncMock(return_value = ("NOT_APPLICABLE", None, None)),
|
||||
_extract_sell_directly_from_ad_page = AsyncMock(return_value = False),
|
||||
_download_images_from_ad_page = AsyncMock(return_value = []),
|
||||
_extract_contact_from_ad_page = AsyncMock(return_value = {})
|
||||
):
|
||||
info = await test_extractor._extract_ad_page_info("/some/dir", 12345)
|
||||
assert info["description"] == raw_description
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_extract_sell_directly(self, test_extractor: AdExtractor) -> None:
|
||||
async def test_extract_sell_directly(self, test_extractor:AdExtractor) -> None:
|
||||
"""Test extraction of sell directly option."""
|
||||
test_cases = [
|
||||
("Direkt kaufen", True),
|
||||
@@ -424,11 +424,11 @@ class TestAdExtractorContent:
|
||||
]
|
||||
|
||||
for text, expected in test_cases:
|
||||
with patch.object(test_extractor, 'web_text', new_callable=AsyncMock, return_value=text):
|
||||
with patch.object(test_extractor, "web_text", new_callable = AsyncMock, return_value = text):
|
||||
result = await test_extractor._extract_sell_directly_from_ad_page()
|
||||
assert result is expected
|
||||
|
||||
with patch.object(test_extractor, 'web_text', new_callable=AsyncMock, side_effect=TimeoutError):
|
||||
with patch.object(test_extractor, "web_text", new_callable = AsyncMock, side_effect = TimeoutError):
|
||||
result = await test_extractor._extract_sell_directly_from_ad_page()
|
||||
assert result is None
|
||||
|
||||
@@ -451,15 +451,15 @@ class TestAdExtractorCategory:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
# pylint: disable=protected-access
|
||||
async def test_extract_category(self, extractor: AdExtractor) -> None:
|
||||
async def test_extract_category(self, extractor:AdExtractor) -> None:
|
||||
"""Test category extraction from breadcrumb."""
|
||||
category_line = MagicMock()
|
||||
first_part = MagicMock()
|
||||
first_part.attrs = {'href': '/s-familie-kind-baby/c17'}
|
||||
first_part.attrs = {"href": "/s-familie-kind-baby/c17"}
|
||||
second_part = MagicMock()
|
||||
second_part.attrs = {'href': '/s-spielzeug/c23'}
|
||||
second_part.attrs = {"href": "/s-spielzeug/c23"}
|
||||
|
||||
with patch.object(extractor, 'web_find', new_callable = AsyncMock) as mock_web_find:
|
||||
with patch.object(extractor, "web_find", new_callable = AsyncMock) as mock_web_find:
|
||||
mock_web_find.side_effect = [
|
||||
category_line,
|
||||
first_part,
|
||||
@@ -469,15 +469,15 @@ class TestAdExtractorCategory:
|
||||
result = await extractor._extract_category_from_ad_page()
|
||||
assert result == "17/23"
|
||||
|
||||
mock_web_find.assert_any_call(By.ID, 'vap-brdcrmb')
|
||||
mock_web_find.assert_any_call(By.CSS_SELECTOR, 'a:nth-of-type(2)', parent = category_line)
|
||||
mock_web_find.assert_any_call(By.CSS_SELECTOR, 'a:nth-of-type(3)', parent = category_line)
|
||||
mock_web_find.assert_any_call(By.ID, "vap-brdcrmb")
|
||||
mock_web_find.assert_any_call(By.CSS_SELECTOR, "a:nth-of-type(2)", parent = category_line)
|
||||
mock_web_find.assert_any_call(By.CSS_SELECTOR, "a:nth-of-type(3)", parent = category_line)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
# pylint: disable=protected-access
|
||||
async def test_extract_special_attributes_empty(self, extractor: AdExtractor) -> None:
|
||||
async def test_extract_special_attributes_empty(self, extractor:AdExtractor) -> None:
|
||||
"""Test extraction of special attributes when empty."""
|
||||
with patch.object(extractor, 'web_execute', new_callable = AsyncMock) as mock_web_execute:
|
||||
with patch.object(extractor, "web_execute", new_callable = AsyncMock) as mock_web_execute:
|
||||
mock_web_execute.return_value = {
|
||||
"universalAnalyticsOpts": {
|
||||
"dimensions": {
|
||||
@@ -507,11 +507,11 @@ class TestAdExtractorContact:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
# pylint: disable=protected-access
|
||||
async def test_extract_contact_info(self, extractor: AdExtractor) -> None:
|
||||
async def test_extract_contact_info(self, extractor:AdExtractor) -> None:
|
||||
"""Test extraction of contact information."""
|
||||
with patch.object(extractor, 'page', MagicMock()), \
|
||||
patch.object(extractor, 'web_text', new_callable = AsyncMock) as mock_web_text, \
|
||||
patch.object(extractor, 'web_find', new_callable = AsyncMock) as mock_web_find:
|
||||
with patch.object(extractor, "page", MagicMock()), \
|
||||
patch.object(extractor, "web_text", new_callable = AsyncMock) as mock_web_text, \
|
||||
patch.object(extractor, "web_find", new_callable = AsyncMock) as mock_web_find:
|
||||
|
||||
mock_web_text.side_effect = [
|
||||
"12345 Berlin - Mitte",
|
||||
@@ -535,22 +535,22 @@ class TestAdExtractorContact:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
# pylint: disable=protected-access
|
||||
async def test_extract_contact_info_timeout(self, extractor: AdExtractor) -> None:
|
||||
async def test_extract_contact_info_timeout(self, extractor:AdExtractor) -> None:
|
||||
"""Test contact info extraction when elements are not found."""
|
||||
with patch.object(extractor, 'page', MagicMock()), \
|
||||
patch.object(extractor, 'web_text', new_callable = AsyncMock, side_effect = TimeoutError()), \
|
||||
patch.object(extractor, 'web_find', new_callable = AsyncMock, side_effect = TimeoutError()), \
|
||||
with patch.object(extractor, "page", MagicMock()), \
|
||||
patch.object(extractor, "web_text", new_callable = AsyncMock, side_effect = TimeoutError()), \
|
||||
patch.object(extractor, "web_find", new_callable = AsyncMock, side_effect = TimeoutError()), \
|
||||
pytest.raises(TimeoutError):
|
||||
|
||||
await extractor._extract_contact_from_ad_page()
|
||||
await extractor._extract_contact_from_ad_page()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
# pylint: disable=protected-access
|
||||
async def test_extract_contact_info_with_phone(self, extractor: AdExtractor) -> None:
|
||||
async def test_extract_contact_info_with_phone(self, extractor:AdExtractor) -> None:
|
||||
"""Test extraction of contact information including phone number."""
|
||||
with patch.object(extractor, 'page', MagicMock()), \
|
||||
patch.object(extractor, 'web_text', new_callable = AsyncMock) as mock_web_text, \
|
||||
patch.object(extractor, 'web_find', new_callable = AsyncMock) as mock_web_find:
|
||||
with patch.object(extractor, "page", MagicMock()), \
|
||||
patch.object(extractor, "web_text", new_callable = AsyncMock) as mock_web_text, \
|
||||
patch.object(extractor, "web_find", new_callable = AsyncMock) as mock_web_find:
|
||||
|
||||
mock_web_text.side_effect = [
|
||||
"12345 Berlin - Mitte",
|
||||
@@ -588,19 +588,19 @@ class TestAdExtractorDownload:
|
||||
return AdExtractor(browser_mock, config_mock)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_download_ad_existing_directory(self, extractor: AdExtractor) -> None:
|
||||
async def test_download_ad_existing_directory(self, extractor:AdExtractor) -> None:
|
||||
"""Test downloading an ad when the directory already exists."""
|
||||
with patch('os.path.exists') as mock_exists, \
|
||||
patch('os.path.isdir') as mock_isdir, \
|
||||
patch('os.makedirs') as mock_makedirs, \
|
||||
patch('os.mkdir') as mock_mkdir, \
|
||||
patch('shutil.rmtree') as mock_rmtree, \
|
||||
patch('kleinanzeigen_bot.extract.dicts.save_dict', autospec = True) as mock_save_dict, \
|
||||
patch.object(extractor, '_extract_ad_page_info', new_callable = AsyncMock) as mock_extract:
|
||||
with patch("os.path.exists") as mock_exists, \
|
||||
patch("os.path.isdir") as mock_isdir, \
|
||||
patch("os.makedirs") as mock_makedirs, \
|
||||
patch("os.mkdir") as mock_mkdir, \
|
||||
patch("shutil.rmtree") as mock_rmtree, \
|
||||
patch("kleinanzeigen_bot.extract.dicts.save_dict", autospec = True) as mock_save_dict, \
|
||||
patch.object(extractor, "_extract_ad_page_info", new_callable = AsyncMock) as mock_extract:
|
||||
|
||||
base_dir = 'downloaded-ads'
|
||||
ad_dir = os.path.join(base_dir, 'ad_12345')
|
||||
yaml_path = os.path.join(ad_dir, 'ad_12345.yaml')
|
||||
base_dir = "downloaded-ads"
|
||||
ad_dir = os.path.join(base_dir, "ad_12345")
|
||||
yaml_path = os.path.join(ad_dir, "ad_12345.yaml")
|
||||
|
||||
# Configure mocks for directory checks
|
||||
existing_paths = {base_dir, ad_dir}
|
||||
@@ -632,32 +632,32 @@ class TestAdExtractorDownload:
|
||||
# Workaround for hard-coded path in download_ad
|
||||
actual_call = mock_save_dict.call_args
|
||||
assert actual_call is not None
|
||||
actual_path = actual_call[0][0].replace('/', os.path.sep)
|
||||
actual_path = actual_call[0][0].replace("/", os.path.sep)
|
||||
assert actual_path == yaml_path
|
||||
assert actual_call[0][1] == mock_extract.return_value
|
||||
|
||||
@pytest.mark.asyncio
|
||||
# pylint: disable=protected-access
|
||||
async def test_download_images_no_images(self, extractor: AdExtractor) -> None:
|
||||
async def test_download_images_no_images(self, extractor:AdExtractor) -> None:
|
||||
"""Test image download when no images are found."""
|
||||
with patch.object(extractor, 'web_find', new_callable = AsyncMock, side_effect = TimeoutError):
|
||||
with patch.object(extractor, "web_find", new_callable = AsyncMock, side_effect = TimeoutError):
|
||||
image_paths = await extractor._download_images_from_ad_page("/some/dir", 12345)
|
||||
assert len(image_paths) == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_download_ad(self, extractor: AdExtractor) -> None:
|
||||
async def test_download_ad(self, extractor:AdExtractor) -> None:
|
||||
"""Test downloading an entire ad."""
|
||||
with patch('os.path.exists') as mock_exists, \
|
||||
patch('os.path.isdir') as mock_isdir, \
|
||||
patch('os.makedirs') as mock_makedirs, \
|
||||
patch('os.mkdir') as mock_mkdir, \
|
||||
patch('shutil.rmtree') as mock_rmtree, \
|
||||
patch('kleinanzeigen_bot.extract.dicts.save_dict', autospec = True) as mock_save_dict, \
|
||||
patch.object(extractor, '_extract_ad_page_info', new_callable = AsyncMock) as mock_extract:
|
||||
with patch("os.path.exists") as mock_exists, \
|
||||
patch("os.path.isdir") as mock_isdir, \
|
||||
patch("os.makedirs") as mock_makedirs, \
|
||||
patch("os.mkdir") as mock_mkdir, \
|
||||
patch("shutil.rmtree") as mock_rmtree, \
|
||||
patch("kleinanzeigen_bot.extract.dicts.save_dict", autospec = True) as mock_save_dict, \
|
||||
patch.object(extractor, "_extract_ad_page_info", new_callable = AsyncMock) as mock_extract:
|
||||
|
||||
base_dir = 'downloaded-ads'
|
||||
ad_dir = os.path.join(base_dir, 'ad_12345')
|
||||
yaml_path = os.path.join(ad_dir, 'ad_12345.yaml')
|
||||
base_dir = "downloaded-ads"
|
||||
ad_dir = os.path.join(base_dir, "ad_12345")
|
||||
yaml_path = os.path.join(ad_dir, "ad_12345.yaml")
|
||||
|
||||
# Configure mocks for directory checks
|
||||
mock_exists.return_value = False
|
||||
@@ -690,6 +690,6 @@ class TestAdExtractorDownload:
|
||||
# Get the actual call arguments
|
||||
actual_call = mock_save_dict.call_args
|
||||
assert actual_call is not None
|
||||
actual_path = actual_call[0][0].replace('/', os.path.sep)
|
||||
actual_path = actual_call[0][0].replace("/", os.path.sep)
|
||||
assert actual_path == yaml_path
|
||||
assert actual_call[0][1] == mock_extract.return_value
|
||||
|
||||
Reference in New Issue
Block a user