293 lines
9.8 KiB
Python
293 lines
9.8 KiB
Python
"""Unit tests for schedule configuration."""
|
|
import os
|
|
from unittest import mock
|
|
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
from config.schedule_config import ScheduleConfig, SchedulesConfig
|
|
from models.listing import FurnishType, ListingType
|
|
|
|
|
|
class TestScheduleConfig:
|
|
"""Tests for ScheduleConfig model."""
|
|
|
|
def test_basic_creation_with_defaults(self):
|
|
"""Test creating a schedule with minimal required fields."""
|
|
schedule = ScheduleConfig(name="Test Schedule", listing_type=ListingType.RENT)
|
|
|
|
assert schedule.name == "Test Schedule"
|
|
assert schedule.enabled is True
|
|
assert schedule.minute == "0"
|
|
assert schedule.hour == "2"
|
|
assert schedule.day_of_week == "*"
|
|
assert schedule.listing_type == ListingType.RENT
|
|
assert schedule.min_bedrooms == 1
|
|
assert schedule.max_bedrooms == 999
|
|
assert schedule.min_price == 0
|
|
assert schedule.max_price == 10_000_000
|
|
assert schedule.district_names == []
|
|
assert schedule.furnish_types is None
|
|
|
|
def test_full_creation(self):
|
|
"""Test creating a schedule with all fields specified."""
|
|
schedule = ScheduleConfig(
|
|
name="Full Schedule",
|
|
enabled=False,
|
|
minute="30",
|
|
hour="4",
|
|
day_of_week="1,3,5",
|
|
listing_type=ListingType.BUY,
|
|
min_bedrooms=2,
|
|
max_bedrooms=3,
|
|
min_price=400000,
|
|
max_price=800000,
|
|
district_names=["Westminster", "Camden"],
|
|
furnish_types=["furnished", "unfurnished"],
|
|
)
|
|
|
|
assert schedule.name == "Full Schedule"
|
|
assert schedule.enabled is False
|
|
assert schedule.minute == "30"
|
|
assert schedule.hour == "4"
|
|
assert schedule.day_of_week == "1,3,5"
|
|
assert schedule.listing_type == ListingType.BUY
|
|
assert schedule.min_bedrooms == 2
|
|
assert schedule.max_bedrooms == 3
|
|
assert schedule.min_price == 400000
|
|
assert schedule.max_price == 800000
|
|
assert schedule.district_names == ["Westminster", "Camden"]
|
|
assert schedule.furnish_types == ["furnished", "unfurnished"]
|
|
|
|
def test_to_query_parameters(self):
|
|
"""Test conversion to QueryParameters."""
|
|
schedule = ScheduleConfig(
|
|
name="Test",
|
|
listing_type=ListingType.RENT,
|
|
min_bedrooms=2,
|
|
max_bedrooms=3,
|
|
min_price=2000,
|
|
max_price=4000,
|
|
district_names=["Westminster"],
|
|
furnish_types=["furnished"],
|
|
)
|
|
|
|
params = schedule.to_query_parameters()
|
|
|
|
assert params.listing_type == ListingType.RENT
|
|
assert params.min_bedrooms == 2
|
|
assert params.max_bedrooms == 3
|
|
assert params.min_price == 2000
|
|
assert params.max_price == 4000
|
|
assert params.district_names == {"Westminster"}
|
|
assert params.furnish_types == [FurnishType.FURNISHED]
|
|
|
|
def test_to_query_parameters_no_furnish_types(self):
|
|
"""Test conversion when furnish_types is None."""
|
|
schedule = ScheduleConfig(
|
|
name="Test",
|
|
listing_type=ListingType.BUY,
|
|
)
|
|
|
|
params = schedule.to_query_parameters()
|
|
|
|
assert params.furnish_types is None
|
|
|
|
|
|
class TestCronValidation:
|
|
"""Tests for cron field validation."""
|
|
|
|
# Valid minute values
|
|
@pytest.mark.parametrize(
|
|
"minute",
|
|
[
|
|
"0",
|
|
"59",
|
|
"*",
|
|
"*/5",
|
|
"*/15",
|
|
"0,15,30,45",
|
|
],
|
|
)
|
|
def test_valid_minute(self, minute: str):
|
|
"""Test valid minute values are accepted."""
|
|
schedule = ScheduleConfig(
|
|
name="Test", listing_type=ListingType.RENT, minute=minute
|
|
)
|
|
assert schedule.minute == minute
|
|
|
|
# Invalid minute values
|
|
@pytest.mark.parametrize(
|
|
"minute",
|
|
[
|
|
"60",
|
|
"-1",
|
|
"abc",
|
|
"*/0",
|
|
],
|
|
)
|
|
def test_invalid_minute(self, minute: str):
|
|
"""Test invalid minute values are rejected."""
|
|
with pytest.raises(ValidationError):
|
|
ScheduleConfig(name="Test", listing_type=ListingType.RENT, minute=minute)
|
|
|
|
# Valid hour values
|
|
@pytest.mark.parametrize(
|
|
"hour",
|
|
[
|
|
"0",
|
|
"23",
|
|
"*",
|
|
"*/6",
|
|
"0,6,12,18",
|
|
],
|
|
)
|
|
def test_valid_hour(self, hour: str):
|
|
"""Test valid hour values are accepted."""
|
|
schedule = ScheduleConfig(
|
|
name="Test", listing_type=ListingType.RENT, hour=hour
|
|
)
|
|
assert schedule.hour == hour
|
|
|
|
# Invalid hour values
|
|
@pytest.mark.parametrize(
|
|
"hour",
|
|
[
|
|
"24",
|
|
"-1",
|
|
"abc",
|
|
"*/0",
|
|
],
|
|
)
|
|
def test_invalid_hour(self, hour: str):
|
|
"""Test invalid hour values are rejected."""
|
|
with pytest.raises(ValidationError):
|
|
ScheduleConfig(name="Test", listing_type=ListingType.RENT, hour=hour)
|
|
|
|
# Valid day_of_week values
|
|
@pytest.mark.parametrize(
|
|
"day_of_week",
|
|
[
|
|
"0",
|
|
"6",
|
|
"*",
|
|
"1,3,5",
|
|
"*/2",
|
|
],
|
|
)
|
|
def test_valid_day_of_week(self, day_of_week: str):
|
|
"""Test valid day_of_week values are accepted."""
|
|
schedule = ScheduleConfig(
|
|
name="Test", listing_type=ListingType.RENT, day_of_week=day_of_week
|
|
)
|
|
assert schedule.day_of_week == day_of_week
|
|
|
|
# Invalid day_of_week values
|
|
@pytest.mark.parametrize(
|
|
"day_of_week",
|
|
[
|
|
"7",
|
|
"-1",
|
|
"abc",
|
|
"*/0",
|
|
],
|
|
)
|
|
def test_invalid_day_of_week(self, day_of_week: str):
|
|
"""Test invalid day_of_week values are rejected."""
|
|
with pytest.raises(ValidationError):
|
|
ScheduleConfig(
|
|
name="Test", listing_type=ListingType.RENT, day_of_week=day_of_week
|
|
)
|
|
|
|
|
|
class TestSchedulesConfig:
|
|
"""Tests for SchedulesConfig container."""
|
|
|
|
def test_from_env_empty(self):
|
|
"""Test loading from empty environment variable."""
|
|
with mock.patch.dict(os.environ, {"SCRAPE_SCHEDULES": ""}, clear=False):
|
|
config = SchedulesConfig.from_env()
|
|
assert config.schedules == []
|
|
|
|
def test_from_env_missing(self):
|
|
"""Test loading when environment variable is not set."""
|
|
with mock.patch.dict(os.environ, {}, clear=True):
|
|
# Ensure SCRAPE_SCHEDULES is not set
|
|
os.environ.pop("SCRAPE_SCHEDULES", None)
|
|
config = SchedulesConfig.from_env()
|
|
assert config.schedules == []
|
|
|
|
def test_from_env_valid_single(self):
|
|
"""Test loading a single valid schedule."""
|
|
json_config = '[{"name":"Daily RENT","listing_type":"RENT","hour":"2"}]'
|
|
with mock.patch.dict(os.environ, {"SCRAPE_SCHEDULES": json_config}):
|
|
config = SchedulesConfig.from_env()
|
|
|
|
assert len(config.schedules) == 1
|
|
assert config.schedules[0].name == "Daily RENT"
|
|
assert config.schedules[0].listing_type == ListingType.RENT
|
|
assert config.schedules[0].hour == "2"
|
|
|
|
def test_from_env_valid_multiple(self):
|
|
"""Test loading multiple valid schedules."""
|
|
json_config = """[
|
|
{"name":"Daily RENT","listing_type":"RENT","hour":"2"},
|
|
{"name":"Daily BUY","listing_type":"BUY","hour":"4","enabled":false}
|
|
]"""
|
|
with mock.patch.dict(os.environ, {"SCRAPE_SCHEDULES": json_config}):
|
|
config = SchedulesConfig.from_env()
|
|
|
|
assert len(config.schedules) == 2
|
|
assert config.schedules[0].name == "Daily RENT"
|
|
assert config.schedules[0].enabled is True
|
|
assert config.schedules[1].name == "Daily BUY"
|
|
assert config.schedules[1].enabled is False
|
|
|
|
def test_from_env_invalid_json(self):
|
|
"""Test error on invalid JSON."""
|
|
with mock.patch.dict(os.environ, {"SCRAPE_SCHEDULES": "not json"}):
|
|
with pytest.raises(ValueError, match="Invalid JSON"):
|
|
SchedulesConfig.from_env()
|
|
|
|
def test_from_env_not_array(self):
|
|
"""Test error when JSON is not an array."""
|
|
with mock.patch.dict(os.environ, {"SCRAPE_SCHEDULES": '{"name":"test"}'}):
|
|
with pytest.raises(ValueError, match="must be a JSON array"):
|
|
SchedulesConfig.from_env()
|
|
|
|
def test_from_env_invalid_schedule(self):
|
|
"""Test error when schedule validation fails."""
|
|
# Missing required listing_type
|
|
json_config = '[{"name":"Invalid"}]'
|
|
with mock.patch.dict(os.environ, {"SCRAPE_SCHEDULES": json_config}):
|
|
with pytest.raises(ValidationError):
|
|
SchedulesConfig.from_env()
|
|
|
|
def test_get_enabled_schedules(self):
|
|
"""Test filtering to only enabled schedules."""
|
|
config = SchedulesConfig(
|
|
schedules=[
|
|
ScheduleConfig(name="Enabled", listing_type=ListingType.RENT, enabled=True),
|
|
ScheduleConfig(name="Disabled", listing_type=ListingType.BUY, enabled=False),
|
|
ScheduleConfig(name="Also Enabled", listing_type=ListingType.RENT, enabled=True),
|
|
]
|
|
)
|
|
|
|
enabled = config.get_enabled_schedules()
|
|
|
|
assert len(enabled) == 2
|
|
assert enabled[0].name == "Enabled"
|
|
assert enabled[1].name == "Also Enabled"
|
|
|
|
def test_get_enabled_schedules_all_disabled(self):
|
|
"""Test when all schedules are disabled."""
|
|
config = SchedulesConfig(
|
|
schedules=[
|
|
ScheduleConfig(name="Disabled1", listing_type=ListingType.RENT, enabled=False),
|
|
ScheduleConfig(name="Disabled2", listing_type=ListingType.BUY, enabled=False),
|
|
]
|
|
)
|
|
|
|
enabled = config.get_enabled_schedules()
|
|
|
|
assert len(enabled) == 0
|