feat: add type safe Ad model

This commit is contained in:
sebthom
2025-05-15 00:10:45 +02:00
committed by Sebastian Thomschke
parent 1369da1c34
commit 6ede14596d
15 changed files with 817 additions and 459 deletions

View File

@@ -40,7 +40,8 @@ def test_bot_config() -> Config:
return Config.model_validate({
"ad_defaults": {
"contact": {
"name": "dummy_name"
"name": "dummy_name",
"zipcode": "12345"
},
},
"login": {

View File

@@ -8,6 +8,7 @@ from unittest.mock import AsyncMock, MagicMock, call, patch
import pytest
from kleinanzeigen_bot.extract import AdExtractor
from kleinanzeigen_bot.model.ad_model import AdPartial, ContactPartial
from kleinanzeigen_bot.model.config_model import Config, DownloadConfig
from kleinanzeigen_bot.utils.web_scraping_mixin import Browser, By, Element
@@ -441,7 +442,7 @@ class TestAdExtractorContent:
_extract_contact_from_ad_page = AsyncMock(return_value = {})
):
info = await test_extractor._extract_ad_page_info("/some/dir", 12345)
assert info["description"] == raw_description
assert info.description == raw_description
@pytest.mark.asyncio
async def test_extract_description_with_affixes_timeout(
@@ -466,11 +467,11 @@ class TestAdExtractorContent:
_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_contact_from_ad_page = AsyncMock(return_value = ContactPartial())
):
try:
info = await test_extractor._extract_ad_page_info("/some/dir", 12345)
assert not info["description"]
assert not info.description
except TimeoutError:
# This is also acceptable - depends on how we want to handle timeouts
pass
@@ -499,10 +500,10 @@ class TestAdExtractorContent:
_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_contact_from_ad_page = AsyncMock(return_value = ContactPartial())
):
info = await test_extractor._extract_ad_page_info("/some/dir", 12345)
assert info["description"] == raw_description
assert info.description == raw_description
@pytest.mark.asyncio
async def test_extract_sell_directly(self, test_extractor:AdExtractor) -> None:
@@ -615,12 +616,11 @@ class TestAdExtractorContact:
]
contact_info = await extractor._extract_contact_from_ad_page()
assert isinstance(contact_info, dict)
assert contact_info["street"] == "Example Street 123"
assert contact_info["zipcode"] == "12345"
assert contact_info["location"] == "Berlin - Mitte"
assert contact_info["name"] == "Test User"
assert contact_info["phone"] is None
assert contact_info.street == "Example Street 123"
assert contact_info.zipcode == "12345"
assert contact_info.location == "Berlin - Mitte"
assert contact_info.name == "Test User"
assert contact_info.phone is None
@pytest.mark.asyncio
# pylint: disable=protected-access
@@ -656,8 +656,7 @@ class TestAdExtractorContact:
]
contact_info = await extractor._extract_contact_from_ad_page()
assert isinstance(contact_info, dict)
assert contact_info["phone"] == "01234567890" # Normalized phone number
assert contact_info.phone == "01234567890" # Normalized phone number
class TestAdExtractorDownload:
@@ -696,9 +695,10 @@ class TestAdExtractorDownload:
mock_exists.side_effect = lambda path: path in existing_paths
mock_isdir.side_effect = lambda path: path == base_dir
mock_extract.return_value = {
mock_extract.return_value = AdPartial.model_validate({
"title": "Test Advertisement Title",
"description": "Test Description",
"category": "Dienstleistungen",
"price": 100,
"images": [],
"contact": {
@@ -707,7 +707,7 @@ class TestAdExtractorDownload:
"zipcode": "12345",
"location": "Test City"
}
}
})
await extractor.download_ad(12345)
@@ -723,7 +723,7 @@ class TestAdExtractorDownload:
assert actual_call is not None
actual_path = actual_call[0][0].replace("/", os.path.sep)
assert actual_path == yaml_path
assert actual_call[0][1] == mock_extract.return_value
assert actual_call[0][1] == mock_extract.return_value.model_dump()
@pytest.mark.asyncio
# pylint: disable=protected-access
@@ -752,9 +752,10 @@ class TestAdExtractorDownload:
mock_exists.return_value = False
mock_isdir.return_value = False
mock_extract.return_value = {
mock_extract.return_value = AdPartial.model_validate({
"title": "Test Advertisement Title",
"description": "Test Description",
"category": "Dienstleistungen",
"price": 100,
"images": [],
"contact": {
@@ -763,7 +764,7 @@ class TestAdExtractorDownload:
"zipcode": "12345",
"location": "Test City"
}
}
})
await extractor.download_ad(12345)
@@ -781,4 +782,4 @@ class TestAdExtractorDownload:
assert actual_call is not None
actual_path = actual_call[0][0].replace("/", os.path.sep)
assert actual_path == yaml_path
assert actual_call[0][1] == mock_extract.return_value
assert actual_call[0][1] == mock_extract.return_value.model_dump()

View File

@@ -11,13 +11,13 @@ from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from pydantic import ValidationError
from ruamel.yaml import YAML
from kleinanzeigen_bot import LOG, KleinanzeigenBot, misc
from kleinanzeigen_bot._version import __version__
from kleinanzeigen_bot.ads import calculate_content_hash
from kleinanzeigen_bot.model.ad_model import Ad
from kleinanzeigen_bot.model.config_model import AdDefaults, Config, PublishingConfig
from kleinanzeigen_bot.utils import loggers
from kleinanzeigen_bot.utils import dicts, loggers
@pytest.fixture
@@ -68,32 +68,6 @@ def base_ad_config() -> dict[str, Any]:
}
def create_ad_config(base_config:dict[str, Any], **overrides:Any) -> dict[str, Any]:
"""Create a new ad configuration by extending or overriding the base configuration.
Args:
base_config: The base configuration to start from
**overrides: Key-value pairs to override or extend the base configuration
Returns:
A new ad configuration dictionary
"""
config = copy.deepcopy(base_config)
for key, value in overrides.items():
if isinstance(value, dict) and key in config and isinstance(config[key], dict):
config[key].update(value)
elif key in config:
config[key] = value
else:
config[key] = value
# Only check length if description is a string
if isinstance(config.get("description"), str):
assert len(config["description"]) <= 4000, "Length of ad description including prefix and suffix exceeds 4000 chars"
return config
def remove_fields(config:dict[str, Any], *fields:str) -> dict[str, Any]:
"""Create a new ad configuration with specified fields removed.
@@ -669,21 +643,17 @@ categories:
ad_file = ad_dir / "test_ad.yaml"
# 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
)
yaml = YAML()
with open(ad_file, "w", encoding = "utf-8") as f:
yaml.dump(ad_cfg, f)
ad_cfg = minimal_ad_config | {
"title": ""
}
dicts.save_dict(ad_file, ad_cfg)
# Set config file path to tmp_path and use relative path for ad_files
test_bot.config_file_path = str(temp_path / "config.yaml")
test_bot.config.ad_files = ["ads/*.yaml"]
with pytest.raises(AssertionError) as exc_info:
with pytest.raises(ValidationError) as exc_info:
test_bot.load_ads()
assert "must be at least 10 characters long" in str(exc_info.value)
assert "title" in str(exc_info.value)
def test_load_ads_with_invalid_price_type(self, test_bot:KleinanzeigenBot, tmp_path:Any, minimal_ad_config:dict[str, Any]) -> None:
"""Test loading ads with invalid price type."""
@@ -693,21 +663,17 @@ categories:
ad_file = ad_dir / "test_ad.yaml"
# Create config with invalid price type
ad_cfg = create_ad_config(
minimal_ad_config,
price_type = "INVALID_TYPE" # Invalid price type
)
yaml = YAML()
with open(ad_file, "w", encoding = "utf-8") as f:
yaml.dump(ad_cfg, f)
ad_cfg = minimal_ad_config | {
"price_type": "INVALID_TYPE"
}
dicts.save_dict(ad_file, ad_cfg)
# Set config file path to tmp_path and use relative path for ad_files
test_bot.config_file_path = str(temp_path / "config.yaml")
test_bot.config.ad_files = ["ads/*.yaml"]
with pytest.raises(AssertionError) as exc_info:
with pytest.raises(ValidationError) as exc_info:
test_bot.load_ads()
assert "property [price_type] must be one of:" in str(exc_info.value)
assert "price_type" in str(exc_info.value)
def test_load_ads_with_invalid_shipping_type(self, test_bot:KleinanzeigenBot, tmp_path:Any, minimal_ad_config:dict[str, Any]) -> None:
"""Test loading ads with invalid shipping type."""
@@ -717,21 +683,17 @@ categories:
ad_file = ad_dir / "test_ad.yaml"
# Create config with invalid shipping type
ad_cfg = create_ad_config(
minimal_ad_config,
shipping_type = "INVALID_TYPE" # Invalid shipping type
)
yaml = YAML()
with open(ad_file, "w", encoding = "utf-8") as f:
yaml.dump(ad_cfg, f)
ad_cfg = minimal_ad_config | {
"shipping_type": "INVALID_TYPE"
}
dicts.save_dict(ad_file, ad_cfg)
# Set config file path to tmp_path and use relative path for ad_files
test_bot.config_file_path = str(temp_path / "config.yaml")
test_bot.config.ad_files = ["ads/*.yaml"]
with pytest.raises(AssertionError) as exc_info:
with pytest.raises(ValidationError) as exc_info:
test_bot.load_ads()
assert "property [shipping_type] must be one of:" in str(exc_info.value)
assert "shipping_type" in str(exc_info.value)
def test_load_ads_with_invalid_price_config(self, test_bot:KleinanzeigenBot, tmp_path:Any, minimal_ad_config:dict[str, Any]) -> None:
"""Test loading ads with invalid price configuration."""
@@ -741,22 +703,18 @@ categories:
ad_file = ad_dir / "test_ad.yaml"
# 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
)
yaml = YAML()
with open(ad_file, "w", encoding = "utf-8") as f:
yaml.dump(ad_cfg, f)
ad_cfg = minimal_ad_config | {
"price_type": "GIVE_AWAY",
"price": 100 # Price should not be set for GIVE_AWAY
}
dicts.save_dict(ad_file, ad_cfg)
# Set config file path to tmp_path and use relative path for ad_files
test_bot.config_file_path = str(temp_path / "config.yaml")
test_bot.config.ad_files = ["ads/*.yaml"]
with pytest.raises(AssertionError) as exc_info:
with pytest.raises(ValidationError) as exc_info:
test_bot.load_ads()
assert "must not be specified for GIVE_AWAY ad" in str(exc_info.value)
assert "price" in str(exc_info.value)
def test_load_ads_with_missing_price(self, test_bot:KleinanzeigenBot, tmp_path:Any, minimal_ad_config:dict[str, Any]) -> None:
"""Test loading ads with missing price for FIXED price type."""
@@ -766,50 +724,18 @@ categories:
ad_file = ad_dir / "test_ad.yaml"
# 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
)
yaml = YAML()
with open(ad_file, "w", encoding = "utf-8") as f:
yaml.dump(ad_cfg, f)
ad_cfg = minimal_ad_config | {
"price_type": "FIXED",
"price": None # Missing required price for FIXED type
}
dicts.save_dict(ad_file, ad_cfg)
# Set config file path to tmp_path and use relative path for ad_files
test_bot.config_file_path = str(temp_path / "config.yaml")
test_bot.config.ad_files = ["ads/*.yaml"]
with pytest.raises(AssertionError) as exc_info:
with pytest.raises(ValidationError) as exc_info:
test_bot.load_ads()
assert "not specified" in str(exc_info.value)
def test_load_ads_with_invalid_category(self, test_bot:KleinanzeigenBot, tmp_path:Any, minimal_ad_config:dict[str, Any]) -> None:
"""Test loading ads with invalid category."""
temp_path = Path(tmp_path)
ad_dir = temp_path / "ads"
ad_dir.mkdir()
ad_file = ad_dir / "test_ad.yaml"
# 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
)
# Mock the config to prevent auto-detection
test_bot.config.ad_defaults = AdDefaults()
yaml = YAML()
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
test_bot.config_file_path = str(temp_path / "config.yaml")
test_bot.config.ad_files = ["ads/*.yaml"]
with pytest.raises(AssertionError) as exc_info:
test_bot.load_ads()
assert "property [description] not specified" in str(exc_info.value)
assert "price is required when price_type is FIXED" in str(exc_info.value)
class TestKleinanzeigenBotAdDeletion:
@@ -823,11 +749,10 @@ class TestKleinanzeigenBotAdDeletion:
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
)
ad_cfg = Ad.model_validate(minimal_ad_config | {
"title": "Test Title",
"id": None # Explicitly set id to None for title-based deletion
})
published_ads = [
{"title": "Test Title", "id": "67890"},
@@ -850,10 +775,9 @@ class TestKleinanzeigenBotAdDeletion:
test_bot.page.sleep = AsyncMock()
# Create config with ID for deletion by ID
ad_cfg = create_ad_config(
minimal_ad_config,
id = "12345"
)
ad_cfg = Ad.model_validate(minimal_ad_config | {
id: "12345"
})
published_ads = [
{"title": "Different Title", "id": "12345"},
@@ -883,13 +807,12 @@ 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"
)
ad_cfg = Ad.model_validate(base_ad_config | {
"id": "12345",
"updated_on": "2024-01-01T00:00:01",
"created_on": "2024-01-01T00:00:01",
"description": "Changed description"
})
# Create a temporary directory and file
with tempfile.TemporaryDirectory() as temp_dir:
@@ -898,21 +821,14 @@ class TestKleinanzeigenBotAdRepublication:
ad_dir.mkdir()
ad_file = ad_dir / "test_ad.yaml"
yaml = YAML()
with open(ad_file, "w", encoding = "utf-8") as f:
yaml.dump(ad_cfg, f)
dicts.save_dict(ad_file, ad_cfg.model_dump())
# Set config file path and use relative path for ad_files
test_bot.config_file_path = str(temp_path / "config.yaml")
test_bot.config.ad_files = ["ads/*.yaml"]
# Mock the loading of the original ad configuration
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
]):
ads_to_publish = test_bot.load_ads()
assert len(ads_to_publish) == 1
ads_to_publish = test_bot.load_ads()
assert len(ads_to_publish) == 1
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."""
@@ -920,16 +836,15 @@ class TestKleinanzeigenBotAdRepublication:
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
)
ad_cfg = Ad.model_validate(base_ad_config | {
"id": "12345",
"updated_on": three_days_ago,
"created_on": three_days_ago
})
# Calculate hash before making the copy to ensure they match
current_hash = calculate_content_hash(ad_cfg)
ad_cfg_orig = copy.deepcopy(ad_cfg)
ad_cfg_orig = ad_cfg.model_dump()
current_hash = calculate_content_hash(ad_cfg_orig)
ad_cfg_orig["content_hash"] = current_hash
# Mock the config to prevent actual file operations
@@ -952,16 +867,15 @@ class TestKleinanzeigenBotShippingOptions:
test_bot.page.evaluate = AsyncMock()
# 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
)
ad_cfg = Ad.model_validate(base_ad_config | {
"shipping_options": ["DHL_2", "Hermes_Päckchen"],
"updated_on": "2024-01-01T00:00:00", # Add created_on to prevent KeyError
"created_on": "2024-01-01T00:00:00" # Add updated_on for consistency
})
# Create the original ad config and published ads list
ad_cfg_orig = copy.deepcopy(ad_cfg)
ad_cfg_orig["content_hash"] = calculate_content_hash(ad_cfg) # Add content hash to prevent republication
ad_cfg_orig = ad_cfg.model_dump()
ad_cfg_orig["content_hash"] = calculate_content_hash(ad_cfg_orig) # Add content hash to prevent republication
published_ads:list[dict[str, Any]] = []
# Set up default config values needed for the test
@@ -1052,7 +966,13 @@ class TestKleinanzeigenBotPrefixSuffix:
for config, raw_description, expected_description in description_test_cases:
test_bot = KleinanzeigenBot()
test_bot.config = test_bot_config.with_values(config)
ad_cfg = {"description": raw_description, "active": True}
ad_cfg = test_bot.load_ad({
"description": raw_description,
"active": True,
"title": "0123456789",
"category": "whatever",
})
# Access private method using the correct name mangling
description = getattr(test_bot, "_KleinanzeigenBot__get_description")(ad_cfg, with_affixes = True)
assert description == expected_description
@@ -1066,10 +986,12 @@ class TestKleinanzeigenBotPrefixSuffix:
"description_suffix": "S" * 1000
}
})
ad_cfg = {
ad_cfg = test_bot.load_ad({
"description": "D" * 2001, # This plus affixes will exceed 4000 chars
"active": True
}
"active": True,
"title": "0123456789",
"category": "whatever",
})
with pytest.raises(AssertionError) as exc_info:
getattr(test_bot, "_KleinanzeigenBot__get_description")(ad_cfg, with_affixes = True)
@@ -1087,10 +1009,12 @@ class TestKleinanzeigenBotDescriptionHandling:
test_bot.config = test_bot_config
# Test with a simple ad config
ad_cfg = {
ad_cfg = test_bot.load_ad({
"description": "Test Description",
"active": True
}
"active": True,
"title": "0123456789",
"category": "whatever",
})
# The description should be returned as-is without any prefix/suffix
description = getattr(test_bot, "_KleinanzeigenBot__get_description")(ad_cfg, with_affixes = True)
@@ -1106,10 +1030,12 @@ class TestKleinanzeigenBotDescriptionHandling:
}
})
ad_cfg = {
ad_cfg = test_bot.load_ad({
"description": "Test Description",
"active": True
}
"active": True,
"title": "0123456789",
"category": "whatever",
})
description = getattr(test_bot, "_KleinanzeigenBot__get_description")(ad_cfg, with_affixes = True)
assert description == "Prefix: Test Description :Suffix"
@@ -1128,10 +1054,12 @@ class TestKleinanzeigenBotDescriptionHandling:
}
})
ad_cfg = {
ad_cfg = test_bot.load_ad({
"description": "Test Description",
"active": True
}
"active": True,
"title": "0123456789",
"category": "whatever",
})
description = getattr(test_bot, "_KleinanzeigenBot__get_description")(ad_cfg, with_affixes = True)
assert description == "New Prefix: Test Description :New Suffix"
@@ -1146,12 +1074,14 @@ class TestKleinanzeigenBotDescriptionHandling:
}
})
ad_cfg = {
ad_cfg = test_bot.load_ad({
"description": "Test Description",
"description_prefix": "Ad Prefix: ",
"description_suffix": " :Ad Suffix",
"active": True
}
"active": True,
"title": "0123456789",
"category": "whatever",
})
description = getattr(test_bot, "_KleinanzeigenBot__get_description")(ad_cfg, with_affixes = True)
assert description == "Ad Prefix: Test Description :Ad Suffix"
@@ -1170,10 +1100,12 @@ class TestKleinanzeigenBotDescriptionHandling:
}
})
ad_cfg = {
ad_cfg = test_bot.load_ad({
"description": "Test Description",
"active": True
}
"active": True,
"title": "0123456789",
"category": "whatever",
})
description = getattr(test_bot, "_KleinanzeigenBot__get_description")(ad_cfg, with_affixes = True)
assert description == "Test Description"
@@ -1183,10 +1115,12 @@ class TestKleinanzeigenBotDescriptionHandling:
test_bot = KleinanzeigenBot()
test_bot.config = test_bot_config
ad_cfg = {
ad_cfg = test_bot.load_ad({
"description": "Contact: test@example.com",
"active": True
}
"active": True,
"title": "0123456789",
"category": "whatever",
})
description = getattr(test_bot, "_KleinanzeigenBot__get_description")(ad_cfg, with_affixes = True)
assert description == "Contact: test(at)example.com"
@@ -1210,17 +1144,17 @@ class TestKleinanzeigenBotChangedAds:
})
# Create a changed ad
changed_ad = create_ad_config(
base_ad_config,
id = "12345",
title = "Changed Ad",
updated_on = "2024-01-01T00:00:00",
created_on = "2024-01-01T00:00:00",
active = True
)
ad_cfg = Ad.model_validate(base_ad_config | {
"id": "12345",
"title": "Changed Ad",
"updated_on": "2024-01-01T00:00:00",
"created_on": "2024-01-01T00:00:00",
"active": True
})
# Calculate hash for changed_ad and add it to the config
# Then modify the ad to simulate a change
changed_ad = ad_cfg.model_dump()
changed_hash = calculate_content_hash(changed_ad)
changed_ad["content_hash"] = changed_hash
# Now modify the ad to make it "changed"
@@ -1233,10 +1167,7 @@ class TestKleinanzeigenBotChangedAds:
ad_dir.mkdir()
# Write the ad file
yaml = YAML()
changed_file = ad_dir / "changed_ad.yaml"
with open(changed_file, "w", encoding = "utf-8") as f:
yaml.dump(changed_ad, f)
dicts.save_dict(ad_dir / "changed_ad.yaml", changed_ad)
# Set config file path and use relative path for ad_files
test_bot.config_file_path = str(temp_path / "config.yaml")
@@ -1251,7 +1182,7 @@ class TestKleinanzeigenBotChangedAds:
# The changed ad should be loaded
assert len(ads_to_publish) == 1
assert ads_to_publish[0][1]["title"] == "Changed Ad - Modified"
assert ads_to_publish[0][1].title == "Changed Ad - Modified"
def test_load_ads_with_due_selector_includes_all_due_ads(self, test_bot:KleinanzeigenBot, base_ad_config:dict[str, Any]) -> None:
"""Test that 'due' selector includes all ads that are due for republication, regardless of changes."""
@@ -1262,15 +1193,15 @@ class TestKleinanzeigenBotChangedAds:
current_time = misc.now()
old_date = (current_time - timedelta(days = 10)).isoformat() # Past republication interval
changed_ad = create_ad_config(
base_ad_config,
id = "12345",
title = "Changed Ad",
updated_on = old_date,
created_on = old_date,
republication_interval = 7, # Due for republication after 7 days
active = True
)
ad_cfg = Ad.model_validate(base_ad_config | {
"id": "12345",
"title": "Changed Ad",
"updated_on": old_date,
"created_on": old_date,
"republication_interval": 7, # Due for republication after 7 days
"active": True
})
changed_ad = ad_cfg.model_dump()
# Create temporary directory and file
with tempfile.TemporaryDirectory() as temp_dir:
@@ -1279,10 +1210,7 @@ class TestKleinanzeigenBotChangedAds:
ad_dir.mkdir()
# Write the ad file
yaml = YAML()
ad_file = ad_dir / "changed_ad.yaml"
with open(ad_file, "w", encoding = "utf-8") as f:
yaml.dump(changed_ad, f)
dicts.save_dict(ad_dir / "changed_ad.yaml", changed_ad)
# Set config file path and use relative path for ad_files
test_bot.config_file_path = str(temp_path / "config.yaml")