mirror of
https://github.com/Second-Hand-Friends/kleinanzeigen-bot.git
synced 2026-03-12 02:31:45 +01:00
refact: reorganize utility modules
This commit is contained in:
@@ -3,21 +3,21 @@ SPDX-FileCopyrightText: © Jens Bergmann and contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
SPDX-ArtifactOfProjectHomePage: https://github.com/Second-Hand-Friends/kleinanzeigen-bot/
|
||||
"""
|
||||
import logging, os
|
||||
import os
|
||||
from typing import Any, Final
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from kleinanzeigen_bot import KleinanzeigenBot, utils
|
||||
from kleinanzeigen_bot import KleinanzeigenBot
|
||||
from kleinanzeigen_bot.utils import loggers
|
||||
from kleinanzeigen_bot.extract import AdExtractor
|
||||
from kleinanzeigen_bot.i18n import get_translating_logger
|
||||
from kleinanzeigen_bot.web_scraping_mixin import Browser
|
||||
from kleinanzeigen_bot.utils.web_scraping_mixin import Browser
|
||||
|
||||
utils.configure_console_logging()
|
||||
loggers.configure_console_logging()
|
||||
|
||||
LOG: Final[logging.Logger] = get_translating_logger("kleinanzeigen_bot")
|
||||
LOG.setLevel(logging.DEBUG)
|
||||
LOG:Final[loggers.Logger] = loggers.get_logger("kleinanzeigen_bot")
|
||||
LOG.setLevel(loggers.DEBUG)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -85,7 +85,7 @@ def browser_mock() -> MagicMock:
|
||||
This mock is configured with the Browser spec to ensure it has all
|
||||
the required methods and attributes of a real Browser instance.
|
||||
"""
|
||||
return MagicMock(spec=Browser)
|
||||
return MagicMock(spec = Browser)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
@@ -3,18 +3,18 @@ SPDX-FileCopyrightText: © Sebastian Thomschke and contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
SPDX-ArtifactOfProjectHomePage: https://github.com/Second-Hand-Friends/kleinanzeigen-bot/
|
||||
"""
|
||||
import logging, os, platform
|
||||
import os, platform
|
||||
from typing import cast
|
||||
|
||||
import nodriver, pytest
|
||||
|
||||
from kleinanzeigen_bot.utils import ensure
|
||||
from kleinanzeigen_bot.i18n import get_translating_logger
|
||||
from kleinanzeigen_bot.web_scraping_mixin import WebScrapingMixin
|
||||
from kleinanzeigen_bot.utils import loggers
|
||||
from kleinanzeigen_bot.utils.misc import ensure
|
||||
from kleinanzeigen_bot.utils.web_scraping_mixin import WebScrapingMixin
|
||||
|
||||
if os.environ.get("CI"):
|
||||
get_translating_logger("kleinanzeigen_bot").setLevel(logging.DEBUG)
|
||||
get_translating_logger("nodriver").setLevel(logging.DEBUG)
|
||||
loggers.get_logger("kleinanzeigen_bot").setLevel(loggers.DEBUG)
|
||||
loggers.get_logger("nodriver").setLevel(loggers.DEBUG)
|
||||
|
||||
|
||||
async def atest_init() -> None:
|
||||
|
||||
@@ -3,31 +3,7 @@ SPDX-FileCopyrightText: © Sebastian Thomschke and contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
SPDX-ArtifactOfProjectHomePage: https://github.com/Second-Hand-Friends/kleinanzeigen-bot/
|
||||
"""
|
||||
import pytest
|
||||
from kleinanzeigen_bot import utils
|
||||
|
||||
|
||||
def test_ensure() -> None:
|
||||
utils.ensure(True, "TRUE")
|
||||
utils.ensure("Some Value", "TRUE")
|
||||
utils.ensure(123, "TRUE")
|
||||
utils.ensure(-123, "TRUE")
|
||||
utils.ensure(lambda: True, "TRUE")
|
||||
|
||||
with pytest.raises(AssertionError):
|
||||
utils.ensure(False, "FALSE")
|
||||
|
||||
with pytest.raises(AssertionError):
|
||||
utils.ensure(0, "FALSE")
|
||||
|
||||
with pytest.raises(AssertionError):
|
||||
utils.ensure("", "FALSE")
|
||||
|
||||
with pytest.raises(AssertionError):
|
||||
utils.ensure(None, "FALSE")
|
||||
|
||||
with pytest.raises(AssertionError):
|
||||
utils.ensure(lambda: False, "FALSE", timeout = 2)
|
||||
from kleinanzeigen_bot import ads
|
||||
|
||||
|
||||
def test_calculate_content_hash_with_none_values() -> None:
|
||||
@@ -48,6 +24,6 @@ def test_calculate_content_hash_with_none_values() -> None:
|
||||
}
|
||||
|
||||
# Should not raise TypeError
|
||||
hash_value = utils.calculate_content_hash(ad_cfg)
|
||||
hash_value = ads.calculate_content_hash(ad_cfg)
|
||||
assert isinstance(hash_value, str)
|
||||
assert len(hash_value) == 64 # SHA-256 hash is 64 characters long
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
SPDX-FileCopyrightText: © Sebastian Thomschke and contributors
|
||||
SPDX-FileCopyrightText: © Jens Bergmann and contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
SPDX-ArtifactOfProjectHomePage: https://github.com/Second-Hand-Friends/kleinanzeigen-bot/
|
||||
"""
|
||||
@@ -10,7 +10,7 @@ from unittest.mock import AsyncMock, MagicMock, call, patch
|
||||
import pytest
|
||||
|
||||
from kleinanzeigen_bot.extract import AdExtractor
|
||||
from kleinanzeigen_bot.web_scraping_mixin import Browser, By, Element
|
||||
from kleinanzeigen_bot.utils.web_scraping_mixin import Browser, By, Element
|
||||
|
||||
|
||||
class _DimensionsDict(TypedDict):
|
||||
@@ -25,7 +25,7 @@ class _BelenConfDict(TypedDict):
|
||||
universalAnalyticsOpts: _UniversalAnalyticsOptsDict
|
||||
|
||||
|
||||
class _SpecialAttributesDict(TypedDict, total=False):
|
||||
class _SpecialAttributesDict(TypedDict, total = False):
|
||||
art_s: str
|
||||
condition_s: str
|
||||
|
||||
@@ -77,7 +77,7 @@ class TestAdExtractorPricing:
|
||||
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
|
||||
@@ -86,7 +86,7 @@ class TestAdExtractorPricing:
|
||||
# pylint: disable=protected-access
|
||||
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"
|
||||
@@ -110,8 +110,8 @@ class TestAdExtractorShipping:
|
||||
) -> 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:
|
||||
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] = {
|
||||
@@ -151,8 +151,8 @@ 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):
|
||||
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()
|
||||
|
||||
@@ -171,8 +171,8 @@ class TestAdExtractorNavigation:
|
||||
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):
|
||||
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
|
||||
@@ -186,16 +186,16 @@ class TestAdExtractorNavigation:
|
||||
|
||||
submit_button_mock = AsyncMock()
|
||||
submit_button_mock.click = AsyncMock()
|
||||
submit_button_mock.apply = AsyncMock(return_value=True)
|
||||
submit_button_mock.apply = AsyncMock(return_value = True)
|
||||
|
||||
input_mock = AsyncMock()
|
||||
input_mock.clear_input = AsyncMock()
|
||||
input_mock.send_keys = AsyncMock()
|
||||
input_mock.apply = AsyncMock(return_value=True)
|
||||
input_mock.apply = AsyncMock(return_value = True)
|
||||
|
||||
popup_close_mock = AsyncMock()
|
||||
popup_close_mock.click = AsyncMock()
|
||||
popup_close_mock.apply = AsyncMock(return_value=True)
|
||||
popup_close_mock.apply = AsyncMock(return_value = True)
|
||||
|
||||
def find_mock(selector_type: By, selector_value: str, **_: Any) -> Element | None:
|
||||
if selector_type == By.ID and selector_value == "site-search-query":
|
||||
@@ -207,10 +207,10 @@ class TestAdExtractorNavigation:
|
||||
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):
|
||||
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
|
||||
@@ -227,13 +227,13 @@ class TestAdExtractorNavigation:
|
||||
input_mock = AsyncMock()
|
||||
input_mock.clear_input = AsyncMock()
|
||||
input_mock.send_keys = AsyncMock()
|
||||
input_mock.apply = AsyncMock(return_value=True)
|
||||
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):
|
||||
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
|
||||
@@ -248,12 +248,12 @@ class TestAdExtractorNavigation:
|
||||
input_mock = AsyncMock()
|
||||
input_mock.clear_input = AsyncMock()
|
||||
input_mock.send_keys = AsyncMock()
|
||||
input_mock.apply = AsyncMock(return_value=True)
|
||||
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):
|
||||
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
|
||||
@@ -261,12 +261,12 @@ class TestAdExtractorNavigation:
|
||||
@pytest.mark.asyncio
|
||||
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
|
||||
splitpage = MagicMock()
|
||||
@@ -280,18 +280,18 @@ class TestAdExtractorNavigation:
|
||||
|
||||
# Setup mock responses for web_find
|
||||
mock_web_find.side_effect = [
|
||||
splitpage, # .l-splitpage
|
||||
pagination_section, # section:nth-of-type(4)
|
||||
pagination, # div > div:nth-of-type(2) > div:nth-of-type(2) > div
|
||||
pagination_div, # div:nth-of-type(1)
|
||||
ad_list, # my-manageitems-adlist
|
||||
link # article > section > section:nth-of-type(2) > h2 > div > a
|
||||
splitpage, # .l-splitpage
|
||||
pagination_section, # section:nth-of-type(4)
|
||||
pagination, # div > div:nth-of-type(2) > div:nth-of-type(2) > div
|
||||
pagination_div, # div:nth-of-type(1)
|
||||
ad_list, # my-manageitems-adlist
|
||||
link # article > section > section:nth-of-type(2) > h2 > div > a
|
||||
]
|
||||
|
||||
# Setup mock responses for web_find_all
|
||||
mock_web_find_all.side_effect = [
|
||||
[MagicMock()], # buttons in pagination
|
||||
[cardbox] # cardbox elements
|
||||
[MagicMock()], # buttons in pagination
|
||||
[cardbox] # cardbox elements
|
||||
]
|
||||
|
||||
# Execute test and verify results
|
||||
@@ -304,7 +304,7 @@ class TestAdExtractorContent:
|
||||
|
||||
@pytest.fixture
|
||||
def extractor(self) -> AdExtractor:
|
||||
browser_mock = MagicMock(spec=Browser)
|
||||
browser_mock = MagicMock(spec = Browser)
|
||||
config_mock = {
|
||||
"ad_defaults": {
|
||||
"description": {
|
||||
@@ -326,15 +326,15 @@ class TestAdExtractorContent:
|
||||
category_mock.attrs = {'href': '/s-kategorie/c123'}
|
||||
|
||||
with patch.object(extractor, 'page', page_mock), \
|
||||
patch.object(extractor, 'web_text', new_callable=AsyncMock) as mock_web_text, \
|
||||
patch.object(extractor, 'web_find', new_callable=AsyncMock, return_value=category_mock), \
|
||||
patch.object(extractor, '_extract_category_from_ad_page', new_callable=AsyncMock, return_value="17/23"), \
|
||||
patch.object(extractor, '_extract_special_attributes_from_ad_page', new_callable=AsyncMock, return_value={}), \
|
||||
patch.object(extractor, '_extract_pricing_info_from_ad_page', new_callable=AsyncMock, return_value=(None, "NOT_APPLICABLE")), \
|
||||
patch.object(extractor, '_extract_shipping_info_from_ad_page', new_callable=AsyncMock, return_value=("NOT_APPLICABLE", None, None)), \
|
||||
patch.object(extractor, '_extract_sell_directly_from_ad_page', new_callable=AsyncMock, return_value=False), \
|
||||
patch.object(extractor, '_download_images_from_ad_page', new_callable=AsyncMock, return_value=[]), \
|
||||
patch.object(extractor, '_extract_contact_from_ad_page', new_callable=AsyncMock, return_value={}):
|
||||
patch.object(extractor, 'web_text', new_callable = AsyncMock) as mock_web_text, \
|
||||
patch.object(extractor, 'web_find', new_callable = AsyncMock, return_value = category_mock), \
|
||||
patch.object(extractor, '_extract_category_from_ad_page', new_callable = AsyncMock, return_value = "17/23"), \
|
||||
patch.object(extractor, '_extract_special_attributes_from_ad_page', new_callable = AsyncMock, return_value = {}), \
|
||||
patch.object(extractor, '_extract_pricing_info_from_ad_page', new_callable = AsyncMock, return_value = (None, "NOT_APPLICABLE")), \
|
||||
patch.object(extractor, '_extract_shipping_info_from_ad_page', new_callable = AsyncMock, return_value = ("NOT_APPLICABLE", None, None)), \
|
||||
patch.object(extractor, '_extract_sell_directly_from_ad_page', new_callable = AsyncMock, return_value = False), \
|
||||
patch.object(extractor, '_download_images_from_ad_page', new_callable = AsyncMock, return_value = []), \
|
||||
patch.object(extractor, '_extract_contact_from_ad_page', new_callable = AsyncMock, return_value = {}):
|
||||
|
||||
mock_web_text.side_effect = [
|
||||
"Test Title",
|
||||
@@ -358,11 +358,11 @@ class TestAdExtractorContent:
|
||||
]
|
||||
|
||||
for text, expected in test_cases:
|
||||
with patch.object(extractor, 'web_text', new_callable=AsyncMock, return_value=text):
|
||||
with patch.object(extractor, 'web_text', new_callable = AsyncMock, return_value = text):
|
||||
result = await extractor._extract_sell_directly_from_ad_page()
|
||||
assert result is expected
|
||||
|
||||
with patch.object(extractor, 'web_text', new_callable=AsyncMock, side_effect=TimeoutError):
|
||||
with patch.object(extractor, 'web_text', new_callable = AsyncMock, side_effect = TimeoutError):
|
||||
result = await extractor._extract_sell_directly_from_ad_page()
|
||||
assert result is None
|
||||
|
||||
@@ -372,7 +372,7 @@ class TestAdExtractorCategory:
|
||||
|
||||
@pytest.fixture
|
||||
def extractor(self) -> AdExtractor:
|
||||
browser_mock = MagicMock(spec=Browser)
|
||||
browser_mock = MagicMock(spec = Browser)
|
||||
config_mock = {
|
||||
"ad_defaults": {
|
||||
"description": {
|
||||
@@ -393,7 +393,7 @@ class TestAdExtractorCategory:
|
||||
second_part = MagicMock()
|
||||
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,
|
||||
@@ -404,14 +404,14 @@ class TestAdExtractorCategory:
|
||||
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.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:
|
||||
"""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": {
|
||||
@@ -428,7 +428,7 @@ class TestAdExtractorContact:
|
||||
|
||||
@pytest.fixture
|
||||
def extractor(self) -> AdExtractor:
|
||||
browser_mock = MagicMock(spec=Browser)
|
||||
browser_mock = MagicMock(spec = Browser)
|
||||
config_mock = {
|
||||
"ad_defaults": {
|
||||
"description": {
|
||||
@@ -444,8 +444,8 @@ class TestAdExtractorContact:
|
||||
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:
|
||||
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",
|
||||
@@ -472,8 +472,8 @@ class TestAdExtractorContact:
|
||||
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()):
|
||||
patch.object(extractor, 'web_text', new_callable = AsyncMock, side_effect = TimeoutError()), \
|
||||
patch.object(extractor, 'web_find', new_callable = AsyncMock, side_effect = TimeoutError()):
|
||||
|
||||
with pytest.raises(TimeoutError):
|
||||
await extractor._extract_contact_from_ad_page()
|
||||
@@ -483,8 +483,8 @@ class TestAdExtractorContact:
|
||||
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:
|
||||
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",
|
||||
@@ -510,7 +510,7 @@ class TestAdExtractorDownload:
|
||||
|
||||
@pytest.fixture
|
||||
def extractor(self) -> AdExtractor:
|
||||
browser_mock = MagicMock(spec=Browser)
|
||||
browser_mock = MagicMock(spec = Browser)
|
||||
config_mock = {
|
||||
"ad_defaults": {
|
||||
"description": {
|
||||
@@ -529,8 +529,8 @@ class TestAdExtractorDownload:
|
||||
patch('os.makedirs') as mock_makedirs, \
|
||||
patch('os.mkdir') as mock_mkdir, \
|
||||
patch('shutil.rmtree') as mock_rmtree, \
|
||||
patch('kleinanzeigen_bot.extract.save_dict', autospec=True) as mock_save_dict, \
|
||||
patch.object(extractor, '_extract_ad_page_info', new_callable=AsyncMock) as mock_extract:
|
||||
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')
|
||||
@@ -574,7 +574,7 @@ class TestAdExtractorDownload:
|
||||
# pylint: disable=protected-access
|
||||
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
|
||||
|
||||
@@ -586,8 +586,8 @@ class TestAdExtractorDownload:
|
||||
patch('os.makedirs') as mock_makedirs, \
|
||||
patch('os.mkdir') as mock_mkdir, \
|
||||
patch('shutil.rmtree') as mock_rmtree, \
|
||||
patch('kleinanzeigen_bot.extract.save_dict', autospec=True) as mock_save_dict, \
|
||||
patch.object(extractor, '_extract_ad_page_info', new_callable=AsyncMock) as mock_extract:
|
||||
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')
|
||||
|
||||
@@ -5,7 +5,7 @@ SPDX-ArtifactOfProjectHomePage: https://github.com/Second-Hand-Friends/kleinanze
|
||||
"""
|
||||
import pytest
|
||||
from _pytest.monkeypatch import MonkeyPatch # pylint: disable=import-private-name
|
||||
from kleinanzeigen_bot import i18n
|
||||
from kleinanzeigen_bot.utils import i18n
|
||||
|
||||
|
||||
@pytest.mark.parametrize("lang, expected", [
|
||||
|
||||
@@ -15,7 +15,7 @@ from ruamel.yaml import YAML
|
||||
|
||||
from kleinanzeigen_bot import LOG, KleinanzeigenBot
|
||||
from kleinanzeigen_bot._version import __version__
|
||||
from kleinanzeigen_bot.utils import calculate_content_hash
|
||||
from kleinanzeigen_bot.ads import calculate_content_hash
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -31,7 +31,7 @@ def mock_page() -> MagicMock:
|
||||
mock.wait_for_selector = AsyncMock()
|
||||
mock.wait_for_navigation = AsyncMock()
|
||||
mock.wait_for_load_state = AsyncMock()
|
||||
mock.content = AsyncMock(return_value="<html></html>")
|
||||
mock.content = AsyncMock(return_value = "<html></html>")
|
||||
mock.goto = AsyncMock()
|
||||
mock.close = AsyncMock()
|
||||
return mock
|
||||
@@ -132,9 +132,9 @@ def mock_config_setup(test_bot: KleinanzeigenBot) -> Generator[None]:
|
||||
"""Provide a centralized mock configuration setup for tests.
|
||||
This fixture mocks load_config and other essential configuration-related methods."""
|
||||
with patch.object(test_bot, 'load_config'), \
|
||||
patch.object(test_bot, 'create_browser_session', new_callable=AsyncMock), \
|
||||
patch.object(test_bot, 'login', new_callable=AsyncMock), \
|
||||
patch.object(test_bot, 'web_request', new_callable=AsyncMock) as mock_request:
|
||||
patch.object(test_bot, 'create_browser_session', new_callable = AsyncMock), \
|
||||
patch.object(test_bot, 'login', new_callable = AsyncMock), \
|
||||
patch.object(test_bot, 'web_request', new_callable = AsyncMock) as mock_request:
|
||||
# Mock the web request for published ads
|
||||
mock_request.return_value = {"content": '{"ads": []}'}
|
||||
yield
|
||||
@@ -250,15 +250,15 @@ class TestKleinanzeigenBotConfiguration:
|
||||
sample_config_with_categories = sample_config.copy()
|
||||
sample_config_with_categories["categories"] = {}
|
||||
|
||||
with patch('kleinanzeigen_bot.utils.load_dict_if_exists', return_value=None), \
|
||||
with patch('kleinanzeigen_bot.utils.dicts.load_dict_if_exists', return_value = None), \
|
||||
patch.object(LOG, 'warning') as mock_warning, \
|
||||
patch('kleinanzeigen_bot.utils.save_dict') as mock_save, \
|
||||
patch('kleinanzeigen_bot.utils.load_dict_from_module') as mock_load_module:
|
||||
patch('kleinanzeigen_bot.utils.dicts.save_dict') as mock_save, \
|
||||
patch('kleinanzeigen_bot.utils.dicts.load_dict_from_module') as mock_load_module:
|
||||
|
||||
mock_load_module.side_effect = [
|
||||
sample_config_with_categories, # config_defaults.yaml
|
||||
{'cat1': 'id1'}, # categories.yaml
|
||||
{'cat2': 'id2'} # categories_old.yaml
|
||||
{'cat1': 'id1'}, # categories.yaml
|
||||
{'cat2': 'id2'} # categories_old.yaml
|
||||
]
|
||||
|
||||
test_bot.load_config()
|
||||
@@ -279,7 +279,7 @@ login:
|
||||
browser:
|
||||
arguments: []
|
||||
"""
|
||||
with open(config_path, "w", encoding="utf-8") as f:
|
||||
with open(config_path, "w", encoding = "utf-8") as f:
|
||||
f.write(config_content)
|
||||
test_bot.config_file_path = str(config_path)
|
||||
|
||||
@@ -300,13 +300,13 @@ class TestKleinanzeigenBotAuthentication:
|
||||
@pytest.mark.asyncio
|
||||
async def test_assert_free_ad_limit_not_reached_success(self, configured_bot: KleinanzeigenBot) -> None:
|
||||
"""Verify that free ad limit check succeeds when limit not reached."""
|
||||
with patch.object(configured_bot, 'web_find', side_effect=TimeoutError):
|
||||
with patch.object(configured_bot, 'web_find', side_effect = TimeoutError):
|
||||
await configured_bot.assert_free_ad_limit_not_reached()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_assert_free_ad_limit_not_reached_limit_reached(self, configured_bot: KleinanzeigenBot) -> None:
|
||||
"""Verify that free ad limit check fails when limit is reached."""
|
||||
with patch.object(configured_bot, 'web_find', return_value=AsyncMock()):
|
||||
with patch.object(configured_bot, 'web_find', return_value = AsyncMock()):
|
||||
with pytest.raises(AssertionError) as exc_info:
|
||||
await configured_bot.assert_free_ad_limit_not_reached()
|
||||
assert "Cannot publish more ads" in str(exc_info.value)
|
||||
@@ -314,21 +314,21 @@ class TestKleinanzeigenBotAuthentication:
|
||||
@pytest.mark.asyncio
|
||||
async def test_is_logged_in_returns_true_when_logged_in(self, configured_bot: KleinanzeigenBot) -> None:
|
||||
"""Verify that login check returns true when logged in."""
|
||||
with patch.object(configured_bot, 'web_text', return_value='Welcome testuser'):
|
||||
with patch.object(configured_bot, 'web_text', return_value = 'Welcome testuser'):
|
||||
assert await configured_bot.is_logged_in() is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_is_logged_in_returns_false_when_not_logged_in(self, configured_bot: KleinanzeigenBot) -> None:
|
||||
"""Verify that login check returns false when not logged in."""
|
||||
with patch.object(configured_bot, 'web_text', side_effect=TimeoutError):
|
||||
with patch.object(configured_bot, 'web_text', side_effect = TimeoutError):
|
||||
assert await configured_bot.is_logged_in() is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_flow_completes_successfully(self, configured_bot: KleinanzeigenBot) -> None:
|
||||
"""Verify that normal login flow completes successfully."""
|
||||
with patch.object(configured_bot, 'web_open') as mock_open, \
|
||||
patch.object(configured_bot, 'is_logged_in', side_effect=[False, True]) as mock_logged_in, \
|
||||
patch.object(configured_bot, 'web_find', side_effect=TimeoutError), \
|
||||
patch.object(configured_bot, 'is_logged_in', side_effect = [False, True]) as mock_logged_in, \
|
||||
patch.object(configured_bot, 'web_find', side_effect = TimeoutError), \
|
||||
patch.object(configured_bot, 'web_input') as mock_input, \
|
||||
patch.object(configured_bot, 'web_click') as mock_click:
|
||||
|
||||
@@ -343,7 +343,7 @@ class TestKleinanzeigenBotAuthentication:
|
||||
async def test_login_flow_handles_captcha(self, configured_bot: KleinanzeigenBot) -> None:
|
||||
"""Verify that login flow handles captcha correctly."""
|
||||
with patch.object(configured_bot, 'web_open'), \
|
||||
patch.object(configured_bot, 'is_logged_in', return_value=False), \
|
||||
patch.object(configured_bot, 'is_logged_in', return_value = False), \
|
||||
patch.object(configured_bot, 'web_find') as mock_find, \
|
||||
patch.object(configured_bot, 'web_await') as mock_await, \
|
||||
patch.object(configured_bot, 'web_input'), \
|
||||
@@ -351,11 +351,11 @@ class TestKleinanzeigenBotAuthentication:
|
||||
patch('kleinanzeigen_bot.ainput') as mock_ainput:
|
||||
|
||||
mock_find.side_effect = [
|
||||
AsyncMock(), # Captcha iframe
|
||||
TimeoutError(), # Login form
|
||||
TimeoutError(), # Phone verification
|
||||
TimeoutError(), # GDPR banner
|
||||
TimeoutError(), # GDPR banner click
|
||||
AsyncMock(), # Captcha iframe
|
||||
TimeoutError(), # Login form
|
||||
TimeoutError(), # Phone verification
|
||||
TimeoutError(), # GDPR banner
|
||||
TimeoutError(), # GDPR banner click
|
||||
]
|
||||
mock_await.return_value = True
|
||||
mock_ainput.return_value = ""
|
||||
@@ -414,7 +414,7 @@ class TestKleinanzeigenBotBasics:
|
||||
"""Test closing browser session."""
|
||||
mock_close = MagicMock()
|
||||
test_bot.page = MagicMock() # Ensure page exists to trigger cleanup
|
||||
with patch.object(test_bot, 'close_browser_session', new=mock_close):
|
||||
with patch.object(test_bot, 'close_browser_session', new = mock_close):
|
||||
test_bot.close_browser_session() # Call directly instead of relying on __del__
|
||||
mock_close.assert_called_once()
|
||||
|
||||
@@ -554,7 +554,7 @@ class TestKleinanzeigenBotCommands:
|
||||
async def test_verify_command(self, test_bot: KleinanzeigenBot, tmp_path: Any) -> None:
|
||||
"""Test verify command with minimal config."""
|
||||
config_path = Path(tmp_path) / "config.yaml"
|
||||
with open(config_path, "w", encoding="utf-8") as f:
|
||||
with open(config_path, "w", encoding = "utf-8") as f:
|
||||
f.write("""
|
||||
login:
|
||||
username: test
|
||||
@@ -571,21 +571,21 @@ class TestKleinanzeigenBotAdOperations:
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_delete_command_no_ads(self, test_bot: KleinanzeigenBot, mock_config_setup: None) -> None: # pylint: disable=unused-argument
|
||||
"""Test running delete command with no ads."""
|
||||
with patch.object(test_bot, 'load_ads', return_value=[]):
|
||||
with patch.object(test_bot, 'load_ads', return_value = []):
|
||||
await test_bot.run(['script.py', 'delete'])
|
||||
assert test_bot.command == 'delete'
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_publish_command_no_ads(self, test_bot: KleinanzeigenBot, mock_config_setup: None) -> None: # pylint: disable=unused-argument
|
||||
"""Test running publish command with no ads."""
|
||||
with patch.object(test_bot, 'load_ads', return_value=[]):
|
||||
with patch.object(test_bot, 'load_ads', return_value = []):
|
||||
await test_bot.run(['script.py', 'publish'])
|
||||
assert test_bot.command == 'publish'
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_download_command_default_selector(self, test_bot: KleinanzeigenBot, mock_config_setup: None) -> None: # pylint: disable=unused-argument
|
||||
"""Test running download command with default selector."""
|
||||
with patch.object(test_bot, 'download_ads', new_callable=AsyncMock):
|
||||
with patch.object(test_bot, 'download_ads', new_callable = AsyncMock):
|
||||
await test_bot.run(['script.py', 'download'])
|
||||
assert test_bot.ads_selector == 'new'
|
||||
|
||||
@@ -603,21 +603,21 @@ class TestKleinanzeigenBotAdManagement:
|
||||
async def test_download_ads_with_specific_ids(self, test_bot: KleinanzeigenBot, mock_config_setup: None) -> None: # pylint: disable=unused-argument
|
||||
"""Test downloading ads with specific IDs."""
|
||||
test_bot.ads_selector = '123,456'
|
||||
with patch.object(test_bot, 'download_ads', new_callable=AsyncMock):
|
||||
with patch.object(test_bot, 'download_ads', new_callable = AsyncMock):
|
||||
await test_bot.run(['script.py', 'download', '--ads=123,456'])
|
||||
assert test_bot.ads_selector == '123,456'
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_publish_invalid_selector(self, test_bot: KleinanzeigenBot, mock_config_setup: None) -> None: # pylint: disable=unused-argument
|
||||
"""Test running publish with invalid selector."""
|
||||
with patch.object(test_bot, 'load_ads', return_value=[]):
|
||||
with patch.object(test_bot, 'load_ads', return_value = []):
|
||||
await test_bot.run(['script.py', 'publish', '--ads=invalid'])
|
||||
assert test_bot.ads_selector == 'due'
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_download_invalid_selector(self, test_bot: KleinanzeigenBot, mock_config_setup: None) -> None: # pylint: disable=unused-argument
|
||||
"""Test running download with invalid selector."""
|
||||
with patch.object(test_bot, 'download_ads', new_callable=AsyncMock):
|
||||
with patch.object(test_bot, 'download_ads', new_callable = AsyncMock):
|
||||
await test_bot.run(['script.py', 'download', '--ads=invalid'])
|
||||
assert test_bot.ads_selector == 'new'
|
||||
|
||||
@@ -628,7 +628,7 @@ class TestKleinanzeigenBotAdConfiguration:
|
||||
def test_load_config_with_categories(self, test_bot: KleinanzeigenBot, tmp_path: Any) -> None:
|
||||
"""Test loading config with custom categories."""
|
||||
config_path = Path(tmp_path) / "config.yaml"
|
||||
with open(config_path, "w", encoding="utf-8") as f:
|
||||
with open(config_path, "w", encoding = "utf-8") as f:
|
||||
f.write("""
|
||||
login:
|
||||
username: test
|
||||
@@ -651,11 +651,11 @@ categories:
|
||||
# Create a minimal config with empty title to trigger validation
|
||||
ad_cfg = create_ad_config(
|
||||
minimal_ad_config,
|
||||
title="" # Empty title to trigger length validation
|
||||
title = "" # Empty title to trigger length validation
|
||||
)
|
||||
|
||||
yaml = YAML()
|
||||
with open(ad_file, "w", encoding="utf-8") as f:
|
||||
with open(ad_file, "w", encoding = "utf-8") as f:
|
||||
yaml.dump(ad_cfg, f)
|
||||
|
||||
# Set config file path to tmp_path and use relative path for ad_files
|
||||
@@ -675,11 +675,11 @@ categories:
|
||||
# Create config with invalid price type
|
||||
ad_cfg = create_ad_config(
|
||||
minimal_ad_config,
|
||||
price_type="INVALID_TYPE" # Invalid price type
|
||||
price_type = "INVALID_TYPE" # Invalid price type
|
||||
)
|
||||
|
||||
yaml = YAML()
|
||||
with open(ad_file, "w", encoding="utf-8") as f:
|
||||
with open(ad_file, "w", encoding = "utf-8") as f:
|
||||
yaml.dump(ad_cfg, f)
|
||||
|
||||
# Set config file path to tmp_path and use relative path for ad_files
|
||||
@@ -699,11 +699,11 @@ categories:
|
||||
# Create config with invalid shipping type
|
||||
ad_cfg = create_ad_config(
|
||||
minimal_ad_config,
|
||||
shipping_type="INVALID_TYPE" # Invalid shipping type
|
||||
shipping_type = "INVALID_TYPE" # Invalid shipping type
|
||||
)
|
||||
|
||||
yaml = YAML()
|
||||
with open(ad_file, "w", encoding="utf-8") as f:
|
||||
with open(ad_file, "w", encoding = "utf-8") as f:
|
||||
yaml.dump(ad_cfg, f)
|
||||
|
||||
# Set config file path to tmp_path and use relative path for ad_files
|
||||
@@ -723,12 +723,12 @@ categories:
|
||||
# Create config with price for GIVE_AWAY type
|
||||
ad_cfg = create_ad_config(
|
||||
minimal_ad_config,
|
||||
price_type="GIVE_AWAY",
|
||||
price=100 # Price should not be set for GIVE_AWAY
|
||||
price_type = "GIVE_AWAY",
|
||||
price = 100 # Price should not be set for GIVE_AWAY
|
||||
)
|
||||
|
||||
yaml = YAML()
|
||||
with open(ad_file, "w", encoding="utf-8") as f:
|
||||
with open(ad_file, "w", encoding = "utf-8") as f:
|
||||
yaml.dump(ad_cfg, f)
|
||||
|
||||
# Set config file path to tmp_path and use relative path for ad_files
|
||||
@@ -748,12 +748,12 @@ categories:
|
||||
# Create config with FIXED price type but no price
|
||||
ad_cfg = create_ad_config(
|
||||
minimal_ad_config,
|
||||
price_type="FIXED",
|
||||
price=None # Missing required price for FIXED type
|
||||
price_type = "FIXED",
|
||||
price = None # Missing required price for FIXED type
|
||||
)
|
||||
|
||||
yaml = YAML()
|
||||
with open(ad_file, "w", encoding="utf-8") as f:
|
||||
with open(ad_file, "w", encoding = "utf-8") as f:
|
||||
yaml.dump(ad_cfg, f)
|
||||
|
||||
# Set config file path to tmp_path and use relative path for ad_files
|
||||
@@ -773,8 +773,8 @@ categories:
|
||||
# Create config with invalid category and empty description to prevent auto-detection
|
||||
ad_cfg = create_ad_config(
|
||||
minimal_ad_config,
|
||||
category="999999", # Non-existent category
|
||||
description=None # Set description to None to trigger validation
|
||||
category = "999999", # Non-existent category
|
||||
description = None # Set description to None to trigger validation
|
||||
)
|
||||
|
||||
# Mock the config to prevent auto-detection
|
||||
@@ -786,7 +786,7 @@ categories:
|
||||
}
|
||||
|
||||
yaml = YAML()
|
||||
with open(ad_file, "w", encoding="utf-8") as f:
|
||||
with open(ad_file, "w", encoding = "utf-8") as f:
|
||||
yaml.dump(ad_cfg, f)
|
||||
|
||||
# Set config file path to tmp_path and use relative path for ad_files
|
||||
@@ -804,14 +804,14 @@ class TestKleinanzeigenBotAdDeletion:
|
||||
async def test_delete_ad_by_title(self, test_bot: KleinanzeigenBot, minimal_ad_config: dict[str, Any]) -> None:
|
||||
"""Test deleting an ad by title."""
|
||||
test_bot.page = MagicMock()
|
||||
test_bot.page.evaluate = AsyncMock(return_value={"statusCode": 200, "content": "{}"})
|
||||
test_bot.page.evaluate = AsyncMock(return_value = {"statusCode": 200, "content": "{}"})
|
||||
test_bot.page.sleep = AsyncMock()
|
||||
|
||||
# Use minimal config since we only need title for deletion by title
|
||||
ad_cfg = create_ad_config(
|
||||
minimal_ad_config,
|
||||
title="Test Title",
|
||||
id=None # Explicitly set id to None for title-based deletion
|
||||
title = "Test Title",
|
||||
id = None # Explicitly set id to None for title-based deletion
|
||||
)
|
||||
|
||||
published_ads = [
|
||||
@@ -819,10 +819,10 @@ class TestKleinanzeigenBotAdDeletion:
|
||||
{"title": "Other Title", "id": "11111"}
|
||||
]
|
||||
|
||||
with patch.object(test_bot, 'web_open', new_callable=AsyncMock), \
|
||||
patch.object(test_bot, 'web_find', new_callable=AsyncMock) as mock_find, \
|
||||
patch.object(test_bot, 'web_click', new_callable=AsyncMock), \
|
||||
patch.object(test_bot, 'web_check', new_callable=AsyncMock, return_value=True):
|
||||
with patch.object(test_bot, 'web_open', new_callable = AsyncMock), \
|
||||
patch.object(test_bot, 'web_find', new_callable = AsyncMock) as mock_find, \
|
||||
patch.object(test_bot, 'web_click', new_callable = AsyncMock), \
|
||||
patch.object(test_bot, 'web_check', new_callable = AsyncMock, return_value = True):
|
||||
mock_find.return_value.attrs = {"content": "some-token"}
|
||||
result = await test_bot.delete_ad(ad_cfg, True, published_ads)
|
||||
assert result is True
|
||||
@@ -831,13 +831,13 @@ class TestKleinanzeigenBotAdDeletion:
|
||||
async def test_delete_ad_by_id(self, test_bot: KleinanzeigenBot, minimal_ad_config: dict[str, Any]) -> None:
|
||||
"""Test deleting an ad by ID."""
|
||||
test_bot.page = MagicMock()
|
||||
test_bot.page.evaluate = AsyncMock(return_value={"statusCode": 200, "content": "{}"})
|
||||
test_bot.page.evaluate = AsyncMock(return_value = {"statusCode": 200, "content": "{}"})
|
||||
test_bot.page.sleep = AsyncMock()
|
||||
|
||||
# Create config with ID for deletion by ID
|
||||
ad_cfg = create_ad_config(
|
||||
minimal_ad_config,
|
||||
id="12345"
|
||||
id = "12345"
|
||||
)
|
||||
|
||||
published_ads = [
|
||||
@@ -845,10 +845,10 @@ class TestKleinanzeigenBotAdDeletion:
|
||||
{"title": "Other Title", "id": "11111"}
|
||||
]
|
||||
|
||||
with patch.object(test_bot, 'web_open', new_callable=AsyncMock), \
|
||||
patch.object(test_bot, 'web_find', new_callable=AsyncMock) as mock_find, \
|
||||
patch.object(test_bot, 'web_click', new_callable=AsyncMock), \
|
||||
patch.object(test_bot, 'web_check', new_callable=AsyncMock, return_value=True):
|
||||
with patch.object(test_bot, 'web_open', new_callable = AsyncMock), \
|
||||
patch.object(test_bot, 'web_find', new_callable = AsyncMock) as mock_find, \
|
||||
patch.object(test_bot, 'web_click', new_callable = AsyncMock), \
|
||||
patch.object(test_bot, 'web_check', new_callable = AsyncMock, return_value = True):
|
||||
mock_find.return_value.attrs = {"content": "some-token"}
|
||||
result = await test_bot.delete_ad(ad_cfg, False, published_ads)
|
||||
assert result is True
|
||||
@@ -870,10 +870,10 @@ class TestKleinanzeigenBotAdRepublication:
|
||||
# Create ad config with all necessary fields for republication
|
||||
ad_cfg = create_ad_config(
|
||||
base_ad_config,
|
||||
id="12345",
|
||||
updated_on="2024-01-01T00:00:00",
|
||||
created_on="2024-01-01T00:00:00",
|
||||
description="Changed description"
|
||||
id = "12345",
|
||||
updated_on = "2024-01-01T00:00:00",
|
||||
created_on = "2024-01-01T00:00:00",
|
||||
description = "Changed description"
|
||||
)
|
||||
|
||||
# Create a temporary directory and file
|
||||
@@ -884,7 +884,7 @@ class TestKleinanzeigenBotAdRepublication:
|
||||
ad_file = ad_dir / "test_ad.yaml"
|
||||
|
||||
yaml = YAML()
|
||||
with open(ad_file, "w", encoding="utf-8") as f:
|
||||
with open(ad_file, "w", encoding = "utf-8") as f:
|
||||
yaml.dump(ad_cfg, f)
|
||||
|
||||
# Set config file path and use relative path for ad_files
|
||||
@@ -892,7 +892,7 @@ class TestKleinanzeigenBotAdRepublication:
|
||||
test_bot.config['ad_files'] = ["ads/*.yaml"]
|
||||
|
||||
# Mock the loading of the original ad configuration
|
||||
with patch('kleinanzeigen_bot.utils.load_dict', side_effect=[
|
||||
with patch('kleinanzeigen_bot.utils.dicts.load_dict', side_effect = [
|
||||
ad_cfg, # First call returns the original ad config
|
||||
{} # Second call for ad_fields.yaml
|
||||
]):
|
||||
@@ -902,14 +902,14 @@ class TestKleinanzeigenBotAdRepublication:
|
||||
def test_check_ad_republication_no_changes(self, test_bot: KleinanzeigenBot, base_ad_config: dict[str, Any]) -> None:
|
||||
"""Test that unchanged ads within interval are not marked for republication."""
|
||||
current_time = datetime.utcnow()
|
||||
three_days_ago = (current_time - timedelta(days=3)).isoformat()
|
||||
three_days_ago = (current_time - timedelta(days = 3)).isoformat()
|
||||
|
||||
# Create ad config with timestamps for republication check
|
||||
ad_cfg = create_ad_config(
|
||||
base_ad_config,
|
||||
id="12345",
|
||||
updated_on=three_days_ago,
|
||||
created_on=three_days_ago
|
||||
id = "12345",
|
||||
updated_on = three_days_ago,
|
||||
created_on = three_days_ago
|
||||
)
|
||||
|
||||
# Calculate hash before making the copy to ensure they match
|
||||
@@ -919,8 +919,8 @@ class TestKleinanzeigenBotAdRepublication:
|
||||
|
||||
# Mock the config to prevent actual file operations
|
||||
test_bot.config['ad_files'] = ['test.yaml']
|
||||
with patch('kleinanzeigen_bot.utils.load_dict_if_exists', return_value=ad_cfg_orig), \
|
||||
patch('kleinanzeigen_bot.utils.load_dict', return_value={}): # Mock ad_fields.yaml
|
||||
with patch('kleinanzeigen_bot.utils.dicts.load_dict_if_exists', return_value = ad_cfg_orig), \
|
||||
patch('kleinanzeigen_bot.utils.dicts.load_dict', return_value = {}): # Mock ad_fields.yaml
|
||||
ads_to_publish = test_bot.load_ads()
|
||||
assert len(ads_to_publish) == 0 # No ads should be marked for republication
|
||||
|
||||
@@ -939,9 +939,9 @@ class TestKleinanzeigenBotShippingOptions:
|
||||
# Create ad config with specific shipping options
|
||||
ad_cfg = create_ad_config(
|
||||
base_ad_config,
|
||||
shipping_options=["DHL_2", "Hermes_Päckchen"],
|
||||
created_on="2024-01-01T00:00:00", # Add created_on to prevent KeyError
|
||||
updated_on="2024-01-01T00:00:00" # Add updated_on for consistency
|
||||
shipping_options = ["DHL_2", "Hermes_Päckchen"],
|
||||
created_on = "2024-01-01T00:00:00", # Add created_on to prevent KeyError
|
||||
updated_on = "2024-01-01T00:00:00" # Add updated_on for consistency
|
||||
)
|
||||
|
||||
# Create the original ad config and published ads list
|
||||
@@ -959,26 +959,26 @@ class TestKleinanzeigenBotShippingOptions:
|
||||
ad_file = Path(tmp_path) / "test_ad.yaml"
|
||||
|
||||
# Mock the necessary web interaction methods
|
||||
with patch.object(test_bot, 'web_click', new_callable=AsyncMock), \
|
||||
patch.object(test_bot, 'web_find', new_callable=AsyncMock) as mock_find, \
|
||||
patch.object(test_bot, 'web_select', new_callable=AsyncMock), \
|
||||
patch.object(test_bot, 'web_input', new_callable=AsyncMock), \
|
||||
patch.object(test_bot, 'web_open', new_callable=AsyncMock), \
|
||||
patch.object(test_bot, 'web_sleep', new_callable=AsyncMock), \
|
||||
patch.object(test_bot, 'web_check', new_callable=AsyncMock, return_value=True), \
|
||||
patch.object(test_bot, 'web_request', new_callable=AsyncMock), \
|
||||
patch.object(test_bot, 'web_execute', new_callable=AsyncMock), \
|
||||
patch.object(test_bot, 'web_find_all', new_callable=AsyncMock) as mock_find_all, \
|
||||
patch.object(test_bot, 'web_await', new_callable=AsyncMock):
|
||||
with patch.object(test_bot, 'web_click', new_callable = AsyncMock), \
|
||||
patch.object(test_bot, 'web_find', new_callable = AsyncMock) as mock_find, \
|
||||
patch.object(test_bot, 'web_select', new_callable = AsyncMock), \
|
||||
patch.object(test_bot, 'web_input', new_callable = AsyncMock), \
|
||||
patch.object(test_bot, 'web_open', new_callable = AsyncMock), \
|
||||
patch.object(test_bot, 'web_sleep', new_callable = AsyncMock), \
|
||||
patch.object(test_bot, 'web_check', new_callable = AsyncMock, return_value = True), \
|
||||
patch.object(test_bot, 'web_request', new_callable = AsyncMock), \
|
||||
patch.object(test_bot, 'web_execute', new_callable = AsyncMock), \
|
||||
patch.object(test_bot, 'web_find_all', new_callable = AsyncMock) as mock_find_all, \
|
||||
patch.object(test_bot, 'web_await', new_callable = AsyncMock):
|
||||
|
||||
# Mock the shipping options form elements
|
||||
mock_find.side_effect = [
|
||||
TimeoutError(), # First call in assert_free_ad_limit_not_reached
|
||||
AsyncMock(attrs={"content": "csrf-token-123"}), # CSRF token
|
||||
AsyncMock(attrs={"checked": True}), # Size radio button check
|
||||
AsyncMock(attrs={"value": "Klein"}), # Size dropdown
|
||||
AsyncMock(attrs={"value": "Paket 2 kg"}), # Package type dropdown
|
||||
AsyncMock(attrs={"value": "Päckchen"}), # Second package type dropdown
|
||||
AsyncMock(attrs = {"content": "csrf-token-123"}), # CSRF token
|
||||
AsyncMock(attrs = {"checked": True}), # Size radio button check
|
||||
AsyncMock(attrs = {"value": "Klein"}), # Size dropdown
|
||||
AsyncMock(attrs = {"value": "Paket 2 kg"}), # Package type dropdown
|
||||
AsyncMock(attrs = {"value": "Päckchen"}), # Second package type dropdown
|
||||
TimeoutError(), # Captcha check
|
||||
]
|
||||
|
||||
@@ -986,7 +986,7 @@ class TestKleinanzeigenBotShippingOptions:
|
||||
mock_find_all.return_value = []
|
||||
|
||||
# Mock web_check to return True for radio button checked state
|
||||
with patch.object(test_bot, 'web_check', new_callable=AsyncMock) as mock_check:
|
||||
with patch.object(test_bot, 'web_check', new_callable = AsyncMock) as mock_check:
|
||||
mock_check.return_value = True
|
||||
|
||||
# Test through the public interface by publishing an ad
|
||||
|
||||
30
tests/unit/test_utils_misc.py
Normal file
30
tests/unit/test_utils_misc.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""
|
||||
SPDX-FileCopyrightText: © Sebastian Thomschke and contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
SPDX-ArtifactOfProjectHomePage: https://github.com/Second-Hand-Friends/kleinanzeigen-bot/
|
||||
"""
|
||||
import pytest
|
||||
from kleinanzeigen_bot.utils import misc
|
||||
|
||||
|
||||
def test_ensure() -> None:
|
||||
misc.ensure(True, "TRUE")
|
||||
misc.ensure("Some Value", "TRUE")
|
||||
misc.ensure(123, "TRUE")
|
||||
misc.ensure(-123, "TRUE")
|
||||
misc.ensure(lambda: True, "TRUE")
|
||||
|
||||
with pytest.raises(AssertionError):
|
||||
misc.ensure(False, "FALSE")
|
||||
|
||||
with pytest.raises(AssertionError):
|
||||
misc.ensure(0, "FALSE")
|
||||
|
||||
with pytest.raises(AssertionError):
|
||||
misc.ensure("", "FALSE")
|
||||
|
||||
with pytest.raises(AssertionError):
|
||||
misc.ensure(None, "FALSE")
|
||||
|
||||
with pytest.raises(AssertionError):
|
||||
misc.ensure(lambda: False, "FALSE", timeout = 2)
|
||||
Reference in New Issue
Block a user