Add comprehensive test coverage for 8 modules
Increases overall test coverage from 53% to 56% by adding tests for: - accommodation.py (60% → 100%) - birthday.py (24% → 100%) - calendar.py (19% → 100%) - carnival.py (33% → 100%) - domains.py (75% → 100%) - events_yaml.py (50% → 96%) - fx.py (14% → 21% for tested functions) - sun.py (55% → 100%) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
8db777ae8b
commit
e6327780aa
186
tests/test_accommodation.py
Normal file
186
tests/test_accommodation.py
Normal file
|
@ -0,0 +1,186 @@
|
||||||
|
"""Tests for accommodation functionality."""
|
||||||
|
|
||||||
|
import tempfile
|
||||||
|
from datetime import date, datetime
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from agenda.accommodation import get_events
|
||||||
|
from agenda.event import Event
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetEvents:
|
||||||
|
"""Test the get_events function."""
|
||||||
|
|
||||||
|
def test_get_events_airbnb(self) -> None:
|
||||||
|
"""Test getting accommodation events for Airbnb."""
|
||||||
|
accommodation_data = [
|
||||||
|
{
|
||||||
|
"from": date(2024, 6, 1),
|
||||||
|
"to": date(2024, 6, 5),
|
||||||
|
"location": "Paris",
|
||||||
|
"operator": "airbnb",
|
||||||
|
"url": "https://airbnb.com/rooms/123"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
|
||||||
|
yaml.dump(accommodation_data, f)
|
||||||
|
filepath = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
events = get_events(filepath)
|
||||||
|
|
||||||
|
assert len(events) == 1
|
||||||
|
event = events[0]
|
||||||
|
assert event.date == date(2024, 6, 1)
|
||||||
|
assert event.end_date == date(2024, 6, 5)
|
||||||
|
assert event.name == "accommodation"
|
||||||
|
assert event.title == "Paris Airbnb"
|
||||||
|
assert event.url == "https://airbnb.com/rooms/123"
|
||||||
|
|
||||||
|
finally:
|
||||||
|
import os
|
||||||
|
os.unlink(filepath)
|
||||||
|
|
||||||
|
def test_get_events_hotel(self) -> None:
|
||||||
|
"""Test getting accommodation events for hotel."""
|
||||||
|
accommodation_data = [
|
||||||
|
{
|
||||||
|
"from": date(2024, 6, 1),
|
||||||
|
"to": date(2024, 6, 5),
|
||||||
|
"name": "Hilton Hotel",
|
||||||
|
"url": "https://hilton.com"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
|
||||||
|
yaml.dump(accommodation_data, f)
|
||||||
|
filepath = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
events = get_events(filepath)
|
||||||
|
|
||||||
|
assert len(events) == 1
|
||||||
|
event = events[0]
|
||||||
|
assert event.date == date(2024, 6, 1)
|
||||||
|
assert event.end_date == date(2024, 6, 5)
|
||||||
|
assert event.name == "accommodation"
|
||||||
|
assert event.title == "Hilton Hotel"
|
||||||
|
assert event.url == "https://hilton.com"
|
||||||
|
|
||||||
|
finally:
|
||||||
|
import os
|
||||||
|
os.unlink(filepath)
|
||||||
|
|
||||||
|
def test_get_events_no_url(self) -> None:
|
||||||
|
"""Test getting accommodation events without URL."""
|
||||||
|
accommodation_data = [
|
||||||
|
{
|
||||||
|
"from": date(2024, 6, 1),
|
||||||
|
"to": date(2024, 6, 5),
|
||||||
|
"name": "Local B&B"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
|
||||||
|
yaml.dump(accommodation_data, f)
|
||||||
|
filepath = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
events = get_events(filepath)
|
||||||
|
|
||||||
|
assert len(events) == 1
|
||||||
|
event = events[0]
|
||||||
|
assert event.url is None
|
||||||
|
assert event.title == "Local B&B"
|
||||||
|
|
||||||
|
finally:
|
||||||
|
import os
|
||||||
|
os.unlink(filepath)
|
||||||
|
|
||||||
|
def test_get_events_multiple_accommodations(self) -> None:
|
||||||
|
"""Test getting multiple accommodation events."""
|
||||||
|
accommodation_data = [
|
||||||
|
{
|
||||||
|
"from": date(2024, 6, 1),
|
||||||
|
"to": date(2024, 6, 5),
|
||||||
|
"location": "London",
|
||||||
|
"operator": "airbnb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": date(2024, 6, 10),
|
||||||
|
"to": date(2024, 6, 15),
|
||||||
|
"name": "Royal Hotel"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
|
||||||
|
yaml.dump(accommodation_data, f)
|
||||||
|
filepath = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
events = get_events(filepath)
|
||||||
|
|
||||||
|
assert len(events) == 2
|
||||||
|
|
||||||
|
# Check first accommodation (Airbnb)
|
||||||
|
assert events[0].title == "London Airbnb"
|
||||||
|
assert events[0].date == date(2024, 6, 1)
|
||||||
|
|
||||||
|
# Check second accommodation (Hotel)
|
||||||
|
assert events[1].title == "Royal Hotel"
|
||||||
|
assert events[1].date == date(2024, 6, 10)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
import os
|
||||||
|
os.unlink(filepath)
|
||||||
|
|
||||||
|
def test_get_events_empty_file(self) -> None:
|
||||||
|
"""Test getting events from empty file."""
|
||||||
|
accommodation_data: list[dict[str, Any]] = []
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
|
||||||
|
yaml.dump(accommodation_data, f)
|
||||||
|
filepath = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
events = get_events(filepath)
|
||||||
|
assert events == []
|
||||||
|
|
||||||
|
finally:
|
||||||
|
import os
|
||||||
|
os.unlink(filepath)
|
||||||
|
|
||||||
|
def test_get_events_file_not_found(self) -> None:
|
||||||
|
"""Test error handling when file doesn't exist."""
|
||||||
|
with pytest.raises(FileNotFoundError):
|
||||||
|
get_events("/nonexistent/file.yaml")
|
||||||
|
|
||||||
|
def test_get_events_datetime_objects(self) -> None:
|
||||||
|
"""Test with datetime objects instead of dates."""
|
||||||
|
accommodation_data = [
|
||||||
|
{
|
||||||
|
"from": datetime(2024, 6, 1, 15, 0),
|
||||||
|
"to": datetime(2024, 6, 5, 11, 0),
|
||||||
|
"name": "Hotel Example"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
|
||||||
|
yaml.dump(accommodation_data, f)
|
||||||
|
filepath = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
events = get_events(filepath)
|
||||||
|
|
||||||
|
assert len(events) == 1
|
||||||
|
event = events[0]
|
||||||
|
assert event.date == datetime(2024, 6, 1, 15, 0)
|
||||||
|
assert event.end_date == datetime(2024, 6, 5, 11, 0)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
import os
|
||||||
|
os.unlink(filepath)
|
227
tests/test_birthday.py
Normal file
227
tests/test_birthday.py
Normal file
|
@ -0,0 +1,227 @@
|
||||||
|
"""Tests for birthday functionality."""
|
||||||
|
|
||||||
|
import tempfile
|
||||||
|
from datetime import date
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from agenda.birthday import YEAR_NOT_KNOWN, get_birthdays, next_birthday
|
||||||
|
from agenda.event import Event
|
||||||
|
|
||||||
|
|
||||||
|
class TestNextBirthday:
|
||||||
|
"""Test the next_birthday function."""
|
||||||
|
|
||||||
|
def test_birthday_this_year_future(self) -> None:
|
||||||
|
"""Test birthday that hasn't occurred this year."""
|
||||||
|
from_date = date(2024, 3, 15)
|
||||||
|
birth_date = date(1990, 6, 20)
|
||||||
|
|
||||||
|
next_bday, age = next_birthday(from_date, birth_date)
|
||||||
|
|
||||||
|
assert next_bday == date(2024, 6, 20)
|
||||||
|
assert age == 34
|
||||||
|
|
||||||
|
def test_birthday_this_year_past(self) -> None:
|
||||||
|
"""Test birthday that already occurred this year."""
|
||||||
|
from_date = date(2024, 8, 15)
|
||||||
|
birth_date = date(1990, 6, 20)
|
||||||
|
|
||||||
|
next_bday, age = next_birthday(from_date, birth_date)
|
||||||
|
|
||||||
|
assert next_bday == date(2025, 6, 20)
|
||||||
|
assert age == 35
|
||||||
|
|
||||||
|
def test_birthday_today(self) -> None:
|
||||||
|
"""Test when today is the birthday."""
|
||||||
|
from_date = date(2024, 6, 20)
|
||||||
|
birth_date = date(1990, 6, 20)
|
||||||
|
|
||||||
|
next_bday, age = next_birthday(from_date, birth_date)
|
||||||
|
|
||||||
|
assert next_bday == date(2024, 6, 20)
|
||||||
|
assert age == 34
|
||||||
|
|
||||||
|
def test_birthday_unknown_year(self) -> None:
|
||||||
|
"""Test birthday with unknown year."""
|
||||||
|
from_date = date(2024, 3, 15)
|
||||||
|
birth_date = date(YEAR_NOT_KNOWN, 6, 20)
|
||||||
|
|
||||||
|
next_bday, age = next_birthday(from_date, birth_date)
|
||||||
|
|
||||||
|
assert next_bday == date(2024, 6, 20)
|
||||||
|
assert age is None
|
||||||
|
|
||||||
|
def test_birthday_unknown_year_past(self) -> None:
|
||||||
|
"""Test birthday with unknown year that already passed."""
|
||||||
|
from_date = date(2024, 8, 15)
|
||||||
|
birth_date = date(YEAR_NOT_KNOWN, 6, 20)
|
||||||
|
|
||||||
|
next_bday, age = next_birthday(from_date, birth_date)
|
||||||
|
|
||||||
|
assert next_bday == date(2025, 6, 20)
|
||||||
|
assert age is None
|
||||||
|
|
||||||
|
def test_leap_year_birthday(self) -> None:
|
||||||
|
"""Test birthday on leap day."""
|
||||||
|
from_date = date(2024, 1, 15) # 2024 is a leap year
|
||||||
|
birth_date = date(2000, 2, 29) # Born on leap day
|
||||||
|
|
||||||
|
next_bday, age = next_birthday(from_date, birth_date)
|
||||||
|
|
||||||
|
assert next_bday == date(2024, 2, 29)
|
||||||
|
assert age == 24
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetBirthdays:
|
||||||
|
"""Test the get_birthdays function."""
|
||||||
|
|
||||||
|
def test_get_birthdays_with_year(self) -> None:
|
||||||
|
"""Test getting birthdays with known birth years."""
|
||||||
|
birthday_data = [
|
||||||
|
{
|
||||||
|
"label": "John Doe",
|
||||||
|
"birthday": {"year": 1990, "month": 6, "day": 20}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Jane Smith",
|
||||||
|
"birthday": {"year": 1985, "month": 12, "day": 15}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
|
||||||
|
yaml.dump(birthday_data, f)
|
||||||
|
filepath = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
from_date = date(2024, 3, 15)
|
||||||
|
events = get_birthdays(from_date, filepath)
|
||||||
|
|
||||||
|
# Should have 4 events (2 people × 2 years each)
|
||||||
|
assert len(events) == 4
|
||||||
|
|
||||||
|
# Check John's birthdays
|
||||||
|
john_events = [e for e in events if "John Doe" in e.title]
|
||||||
|
assert len(john_events) == 2
|
||||||
|
assert john_events[0].date == date(2024, 6, 20)
|
||||||
|
assert "aged 34" in john_events[0].title
|
||||||
|
assert john_events[1].date == date(2025, 6, 20)
|
||||||
|
assert "aged 35" in john_events[1].title
|
||||||
|
|
||||||
|
# Check Jane's birthdays
|
||||||
|
jane_events = [e for e in events if "Jane Smith" in e.title]
|
||||||
|
assert len(jane_events) == 2
|
||||||
|
assert jane_events[0].date == date(2024, 12, 15)
|
||||||
|
assert "aged 39" in jane_events[0].title
|
||||||
|
|
||||||
|
# All events should be birthday events
|
||||||
|
for event in events:
|
||||||
|
assert event.name == "birthday"
|
||||||
|
assert isinstance(event, Event)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
import os
|
||||||
|
os.unlink(filepath)
|
||||||
|
|
||||||
|
def test_get_birthdays_without_year(self) -> None:
|
||||||
|
"""Test getting birthdays with unknown birth years."""
|
||||||
|
birthday_data = [
|
||||||
|
{
|
||||||
|
"label": "Anonymous Person",
|
||||||
|
"birthday": {"month": 6, "day": 20}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
|
||||||
|
yaml.dump(birthday_data, f)
|
||||||
|
filepath = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
from_date = date(2024, 3, 15)
|
||||||
|
events = get_birthdays(from_date, filepath)
|
||||||
|
|
||||||
|
# Should have 2 events (1 person × 2 years)
|
||||||
|
assert len(events) == 2
|
||||||
|
|
||||||
|
# Check that age is unknown
|
||||||
|
for event in events:
|
||||||
|
assert "age unknown" in event.title
|
||||||
|
assert "Anonymous Person" in event.title
|
||||||
|
assert event.name == "birthday"
|
||||||
|
|
||||||
|
finally:
|
||||||
|
import os
|
||||||
|
os.unlink(filepath)
|
||||||
|
|
||||||
|
def test_get_birthdays_mixed_data(self) -> None:
|
||||||
|
"""Test getting birthdays with mixed known/unknown years."""
|
||||||
|
birthday_data = [
|
||||||
|
{
|
||||||
|
"label": "Known Person",
|
||||||
|
"birthday": {"year": 1990, "month": 6, "day": 20}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Unknown Person",
|
||||||
|
"birthday": {"month": 8, "day": 15}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
|
||||||
|
yaml.dump(birthday_data, f)
|
||||||
|
filepath = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
from_date = date(2024, 3, 15)
|
||||||
|
events = get_birthdays(from_date, filepath)
|
||||||
|
|
||||||
|
# Should have 4 events total
|
||||||
|
assert len(events) == 4
|
||||||
|
|
||||||
|
# Check known person events
|
||||||
|
known_events = [e for e in events if "Known Person" in e.title]
|
||||||
|
assert len(known_events) == 2
|
||||||
|
assert all("aged" in e.title and "age unknown" not in e.title for e in known_events)
|
||||||
|
|
||||||
|
# Check unknown person events
|
||||||
|
unknown_events = [e for e in events if "Unknown Person" in e.title]
|
||||||
|
assert len(unknown_events) == 2
|
||||||
|
assert all("age unknown" in e.title for e in unknown_events)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
import os
|
||||||
|
os.unlink(filepath)
|
||||||
|
|
||||||
|
def test_get_birthdays_empty_file(self) -> None:
|
||||||
|
"""Test getting birthdays from empty file."""
|
||||||
|
birthday_data: list[dict[str, Any]] = []
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
|
||||||
|
yaml.dump(birthday_data, f)
|
||||||
|
filepath = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
from_date = date(2024, 3, 15)
|
||||||
|
events = get_birthdays(from_date, filepath)
|
||||||
|
|
||||||
|
assert events == []
|
||||||
|
|
||||||
|
finally:
|
||||||
|
import os
|
||||||
|
os.unlink(filepath)
|
||||||
|
|
||||||
|
def test_get_birthdays_file_not_found(self) -> None:
|
||||||
|
"""Test error handling when file doesn't exist."""
|
||||||
|
from_date = date(2024, 3, 15)
|
||||||
|
|
||||||
|
with pytest.raises(FileNotFoundError):
|
||||||
|
get_birthdays(from_date, "/nonexistent/file.yaml")
|
||||||
|
|
||||||
|
|
||||||
|
class TestConstants:
|
||||||
|
"""Test module constants."""
|
||||||
|
|
||||||
|
def test_year_not_known_constant(self) -> None:
|
||||||
|
"""Test that YEAR_NOT_KNOWN has expected value."""
|
||||||
|
assert YEAR_NOT_KNOWN == 1900
|
270
tests/test_calendar.py
Normal file
270
tests/test_calendar.py
Normal file
|
@ -0,0 +1,270 @@
|
||||||
|
"""Tests for calendar functionality."""
|
||||||
|
|
||||||
|
from datetime import date, datetime, timedelta
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from agenda.calendar import build_events, colors, event_type_color_map
|
||||||
|
from agenda.event import Event
|
||||||
|
|
||||||
|
|
||||||
|
class TestEventTypeColorMap:
|
||||||
|
"""Test the event type color mapping."""
|
||||||
|
|
||||||
|
def test_event_type_color_map_contains_expected_types(self) -> None:
|
||||||
|
"""Test that color map contains expected event types."""
|
||||||
|
expected_types = {
|
||||||
|
"bank_holiday", "conference", "us_holiday",
|
||||||
|
"birthday", "waste_schedule"
|
||||||
|
}
|
||||||
|
assert set(event_type_color_map.keys()) == expected_types
|
||||||
|
|
||||||
|
def test_event_type_color_map_values_are_valid(self) -> None:
|
||||||
|
"""Test that all color values are valid color names."""
|
||||||
|
expected_colors = {
|
||||||
|
"success-subtle", "primary-subtle", "secondary-subtle",
|
||||||
|
"info-subtle", "danger-subtle"
|
||||||
|
}
|
||||||
|
assert set(event_type_color_map.values()).issubset(expected_colors)
|
||||||
|
|
||||||
|
|
||||||
|
class TestColorsDict:
|
||||||
|
"""Test the colors dictionary."""
|
||||||
|
|
||||||
|
def test_colors_contains_expected_keys(self) -> None:
|
||||||
|
"""Test that colors dict contains expected color keys."""
|
||||||
|
expected_keys = {
|
||||||
|
"primary-subtle", "secondary-subtle", "success-subtle",
|
||||||
|
"info-subtle", "warning-subtle", "danger-subtle"
|
||||||
|
}
|
||||||
|
assert set(colors.keys()) == expected_keys
|
||||||
|
|
||||||
|
def test_colors_values_are_hex_codes(self) -> None:
|
||||||
|
"""Test that all color values are valid hex codes."""
|
||||||
|
for color_value in colors.values():
|
||||||
|
assert color_value.startswith("#")
|
||||||
|
assert len(color_value) == 7
|
||||||
|
# Check that remaining characters are valid hex
|
||||||
|
hex_part = color_value[1:]
|
||||||
|
int(hex_part, 16) # This will raise ValueError if not valid hex
|
||||||
|
|
||||||
|
|
||||||
|
class TestBuildEvents:
|
||||||
|
"""Test the build_events function."""
|
||||||
|
|
||||||
|
def test_build_events_empty_list(self) -> None:
|
||||||
|
"""Test building events with empty list."""
|
||||||
|
result = build_events([])
|
||||||
|
assert result == []
|
||||||
|
|
||||||
|
def test_build_events_today_event_filtered(self) -> None:
|
||||||
|
"""Test that 'today' events are filtered out."""
|
||||||
|
events = [
|
||||||
|
Event(date=date(2024, 1, 1), name="today", title="Today"),
|
||||||
|
Event(date=date(2024, 1, 2), name="birthday", title="Birthday"),
|
||||||
|
]
|
||||||
|
result = build_events(events)
|
||||||
|
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0]["title"] == "🎈 Birthday"
|
||||||
|
|
||||||
|
def test_build_events_simple_event(self) -> None:
|
||||||
|
"""Test building a simple event without time."""
|
||||||
|
events = [
|
||||||
|
Event(
|
||||||
|
date=date(2024, 1, 15),
|
||||||
|
name="birthday",
|
||||||
|
title="John's Birthday"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
result = build_events(events)
|
||||||
|
|
||||||
|
assert len(result) == 1
|
||||||
|
item = result[0]
|
||||||
|
assert item["allDay"] is True
|
||||||
|
assert item["title"] == "🎈 John's Birthday"
|
||||||
|
assert item["start"] == "2024-01-15"
|
||||||
|
assert item["end"] == "2024-01-16" # Next day
|
||||||
|
assert item["color"] == colors["info-subtle"]
|
||||||
|
assert item["textColor"] == "black"
|
||||||
|
|
||||||
|
def test_build_events_with_time(self) -> None:
|
||||||
|
"""Test building an event with time."""
|
||||||
|
event_datetime = datetime(2024, 1, 15, 14, 30)
|
||||||
|
events = [
|
||||||
|
Event(
|
||||||
|
date=event_datetime,
|
||||||
|
name="meeting",
|
||||||
|
title="Team Meeting"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
result = build_events(events)
|
||||||
|
|
||||||
|
assert len(result) == 1
|
||||||
|
item = result[0]
|
||||||
|
assert item["allDay"] is False
|
||||||
|
assert item["title"] == "Team Meeting"
|
||||||
|
assert item["start"] == "2024-01-15T14:30:00"
|
||||||
|
assert item["end"] == "2024-01-15T15:00:00" # 30 minutes later
|
||||||
|
|
||||||
|
def test_build_events_with_end_date(self) -> None:
|
||||||
|
"""Test building an event with end date."""
|
||||||
|
events = [
|
||||||
|
Event(
|
||||||
|
date=date(2024, 1, 15),
|
||||||
|
name="conference",
|
||||||
|
title="Tech Conference",
|
||||||
|
end_date=date(2024, 1, 17)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
result = build_events(events)
|
||||||
|
|
||||||
|
assert len(result) == 1
|
||||||
|
item = result[0]
|
||||||
|
assert item["allDay"] is True
|
||||||
|
assert item["start"] == "2024-01-15"
|
||||||
|
assert item["end"] == "2024-01-18" # End date + 1 day
|
||||||
|
|
||||||
|
def test_build_events_with_time_and_end_date(self) -> None:
|
||||||
|
"""Test building an event with time and end date."""
|
||||||
|
start_datetime = datetime(2024, 1, 15, 9, 0)
|
||||||
|
end_datetime = datetime(2024, 1, 15, 17, 0)
|
||||||
|
events = [
|
||||||
|
Event(
|
||||||
|
date=start_datetime,
|
||||||
|
name="workshop",
|
||||||
|
title="Python Workshop",
|
||||||
|
end_date=end_datetime
|
||||||
|
)
|
||||||
|
]
|
||||||
|
result = build_events(events)
|
||||||
|
|
||||||
|
assert len(result) == 1
|
||||||
|
item = result[0]
|
||||||
|
assert item["allDay"] is False
|
||||||
|
assert item["start"] == "2024-01-15T09:00:00"
|
||||||
|
assert item["end"] == "2024-01-15T17:00:00"
|
||||||
|
|
||||||
|
def test_build_events_with_url(self) -> None:
|
||||||
|
"""Test building an event with URL."""
|
||||||
|
events = [
|
||||||
|
Event(
|
||||||
|
date=date(2024, 1, 15),
|
||||||
|
name="conference",
|
||||||
|
title="Tech Conference",
|
||||||
|
url="https://example.com"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
result = build_events(events)
|
||||||
|
|
||||||
|
assert len(result) == 1
|
||||||
|
item = result[0]
|
||||||
|
assert item["url"] == "https://example.com"
|
||||||
|
|
||||||
|
def test_build_events_accommodation(self) -> None:
|
||||||
|
"""Test building accommodation events."""
|
||||||
|
events = [
|
||||||
|
Event(
|
||||||
|
date=datetime(2024, 1, 15, 15, 0), # Check-in time
|
||||||
|
name="accommodation",
|
||||||
|
title="Hotel Stay",
|
||||||
|
end_date=datetime(2024, 1, 17, 11, 0), # Check-out time
|
||||||
|
url="https://hotel.com"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
result = build_events(events)
|
||||||
|
|
||||||
|
# Should create 3 events: main accommodation + check-in + check-out
|
||||||
|
assert len(result) == 3
|
||||||
|
|
||||||
|
# Main accommodation event
|
||||||
|
main_event = result[0]
|
||||||
|
assert main_event["allDay"] is True
|
||||||
|
assert main_event["title"] == "🏨 Hotel Stay"
|
||||||
|
assert main_event["start"] == "2024-01-15"
|
||||||
|
assert main_event["end"] == "2024-01-18" # End date + 1 day
|
||||||
|
assert main_event["url"] == "https://hotel.com"
|
||||||
|
|
||||||
|
# Check-in event
|
||||||
|
checkin_event = result[1]
|
||||||
|
assert checkin_event["allDay"] is False
|
||||||
|
assert checkin_event["title"] == "check-in: Hotel Stay"
|
||||||
|
assert checkin_event["start"] == "2024-01-15T15:00:00"
|
||||||
|
assert checkin_event["url"] == "https://hotel.com"
|
||||||
|
|
||||||
|
# Check-out event
|
||||||
|
checkout_event = result[2]
|
||||||
|
assert checkout_event["allDay"] is False
|
||||||
|
assert checkout_event["title"] == "checkout: Hotel Stay"
|
||||||
|
assert checkout_event["start"] == "2024-01-17T11:00:00"
|
||||||
|
assert checkout_event["url"] == "https://hotel.com"
|
||||||
|
|
||||||
|
def test_build_events_no_color_for_unknown_type(self) -> None:
|
||||||
|
"""Test that events with unknown types don't get color."""
|
||||||
|
events = [
|
||||||
|
Event(
|
||||||
|
date=date(2024, 1, 15),
|
||||||
|
name="unknown_type",
|
||||||
|
title="Unknown Event"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
result = build_events(events)
|
||||||
|
|
||||||
|
assert len(result) == 1
|
||||||
|
item = result[0]
|
||||||
|
assert "color" not in item
|
||||||
|
assert "textColor" not in item
|
||||||
|
|
||||||
|
def test_build_events_multiple_event_types(self) -> None:
|
||||||
|
"""Test building events with different types and colors."""
|
||||||
|
events = [
|
||||||
|
Event(date=date(2024, 1, 15), name="bank_holiday", title="New Year"),
|
||||||
|
Event(date=date(2024, 1, 16), name="conference", title="PyCon"),
|
||||||
|
Event(date=date(2024, 1, 17), name="birthday", title="Birthday"),
|
||||||
|
Event(date=date(2024, 1, 18), name="waste_schedule", title="Recycling"),
|
||||||
|
Event(date=date(2024, 1, 19), name="us_holiday", title="MLK Day"),
|
||||||
|
]
|
||||||
|
result = build_events(events)
|
||||||
|
|
||||||
|
assert len(result) == 5
|
||||||
|
|
||||||
|
# Check colors are assigned correctly
|
||||||
|
assert result[0]["color"] == colors["success-subtle"] # bank_holiday
|
||||||
|
assert result[1]["color"] == colors["primary-subtle"] # conference
|
||||||
|
assert result[2]["color"] == colors["info-subtle"] # birthday
|
||||||
|
assert result[3]["color"] == colors["danger-subtle"] # waste_schedule
|
||||||
|
assert result[4]["color"] == colors["secondary-subtle"] # us_holiday
|
||||||
|
|
||||||
|
# All should have black text
|
||||||
|
for item in result:
|
||||||
|
assert item["textColor"] == "black"
|
||||||
|
|
||||||
|
def test_build_events_mixed_scenarios(self) -> None:
|
||||||
|
"""Test building events with mixed scenarios."""
|
||||||
|
events = [
|
||||||
|
# Today event (should be filtered)
|
||||||
|
Event(date=date(2024, 1, 1), name="today", title="Today"),
|
||||||
|
# Simple event
|
||||||
|
Event(date=date(2024, 1, 15), name="birthday", title="Birthday"),
|
||||||
|
# Event with time
|
||||||
|
Event(date=datetime(2024, 1, 16, 10, 0), name="meeting", title="Meeting"),
|
||||||
|
# Accommodation
|
||||||
|
Event(
|
||||||
|
date=datetime(2024, 1, 20, 15, 0),
|
||||||
|
name="accommodation",
|
||||||
|
title="Hotel",
|
||||||
|
end_date=datetime(2024, 1, 22, 11, 0),
|
||||||
|
url="https://hotel.com"
|
||||||
|
),
|
||||||
|
]
|
||||||
|
result = build_events(events)
|
||||||
|
|
||||||
|
# Should have 5 events total (today filtered, accommodation creates 3)
|
||||||
|
assert len(result) == 5
|
||||||
|
|
||||||
|
# Verify titles
|
||||||
|
titles = [item["title"] for item in result]
|
||||||
|
assert "🎈 Birthday" in titles
|
||||||
|
assert "Meeting" in titles
|
||||||
|
assert "🏨 Hotel" in titles
|
||||||
|
assert "check-in: Hotel" in titles
|
||||||
|
assert "checkout: Hotel" in titles
|
125
tests/test_carnival.py
Normal file
125
tests/test_carnival.py
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
"""Tests for carnival functionality."""
|
||||||
|
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
from agenda.carnival import rio_carnival_events
|
||||||
|
from agenda.event import Event
|
||||||
|
|
||||||
|
|
||||||
|
class TestRioCarnivalEvents:
|
||||||
|
"""Test the rio_carnival_events function."""
|
||||||
|
|
||||||
|
def test_carnival_events_single_year(self) -> None:
|
||||||
|
"""Test getting carnival events for a single year."""
|
||||||
|
# 2024 Easter is March 31, so carnival should be around Feb 9-14
|
||||||
|
start_date = date(2024, 1, 1)
|
||||||
|
end_date = date(2024, 12, 31)
|
||||||
|
|
||||||
|
events = rio_carnival_events(start_date, end_date)
|
||||||
|
|
||||||
|
assert len(events) == 1
|
||||||
|
event = events[0]
|
||||||
|
assert event.name == "carnival"
|
||||||
|
assert event.title == "Rio Carnival"
|
||||||
|
assert event.url == "https://en.wikipedia.org/wiki/Rio_Carnival"
|
||||||
|
assert event.date.year == 2024
|
||||||
|
assert event.end_date is not None
|
||||||
|
assert event.end_date.year == 2024
|
||||||
|
# Should be about 51 days before Easter (around early-mid February)
|
||||||
|
assert event.date.month == 2
|
||||||
|
assert event.end_date.month == 2
|
||||||
|
|
||||||
|
def test_carnival_events_multiple_years(self) -> None:
|
||||||
|
"""Test getting carnival events for multiple years."""
|
||||||
|
start_date = date(2023, 1, 1)
|
||||||
|
end_date = date(2025, 12, 31)
|
||||||
|
|
||||||
|
events = rio_carnival_events(start_date, end_date)
|
||||||
|
|
||||||
|
# Should have carnival for 2023, 2024, and 2025
|
||||||
|
assert len(events) == 3
|
||||||
|
|
||||||
|
years = [event.date.year for event in events]
|
||||||
|
assert sorted(years) == [2023, 2024, 2025]
|
||||||
|
|
||||||
|
# All events should be carnival events
|
||||||
|
for event in events:
|
||||||
|
assert event.name == "carnival"
|
||||||
|
assert event.title == "Rio Carnival"
|
||||||
|
assert event.url == "https://en.wikipedia.org/wiki/Rio_Carnival"
|
||||||
|
|
||||||
|
def test_carnival_events_no_overlap(self) -> None:
|
||||||
|
"""Test when date range doesn't overlap with carnival."""
|
||||||
|
# Choose a range that's unlikely to include carnival (summer)
|
||||||
|
start_date = date(2024, 6, 1)
|
||||||
|
end_date = date(2024, 8, 31)
|
||||||
|
|
||||||
|
events = rio_carnival_events(start_date, end_date)
|
||||||
|
|
||||||
|
assert events == []
|
||||||
|
|
||||||
|
def test_carnival_events_partial_overlap_start(self) -> None:
|
||||||
|
"""Test when carnival start overlaps with date range."""
|
||||||
|
# 2024 carnival should be around Feb 9-14
|
||||||
|
start_date = date(2024, 2, 10) # Might overlap with carnival start
|
||||||
|
end_date = date(2024, 2, 15)
|
||||||
|
|
||||||
|
events = rio_carnival_events(start_date, end_date)
|
||||||
|
|
||||||
|
# Should include carnival if there's any overlap
|
||||||
|
if events:
|
||||||
|
assert len(events) == 1
|
||||||
|
assert events[0].name == "carnival"
|
||||||
|
|
||||||
|
def test_carnival_events_partial_overlap_end(self) -> None:
|
||||||
|
"""Test when carnival end overlaps with date range."""
|
||||||
|
# 2024 carnival should be around Feb 9-14
|
||||||
|
start_date = date(2024, 2, 12)
|
||||||
|
end_date = date(2024, 2, 20) # Might overlap with carnival end
|
||||||
|
|
||||||
|
events = rio_carnival_events(start_date, end_date)
|
||||||
|
|
||||||
|
# Should include carnival if there's any overlap
|
||||||
|
if events:
|
||||||
|
assert len(events) == 1
|
||||||
|
assert events[0].name == "carnival"
|
||||||
|
|
||||||
|
def test_carnival_dates_relative_to_easter(self) -> None:
|
||||||
|
"""Test that carnival dates are correctly calculated relative to Easter."""
|
||||||
|
start_date = date(2024, 1, 1)
|
||||||
|
end_date = date(2024, 12, 31)
|
||||||
|
|
||||||
|
events = rio_carnival_events(start_date, end_date)
|
||||||
|
|
||||||
|
assert len(events) == 1
|
||||||
|
event = events[0]
|
||||||
|
|
||||||
|
# Carnival should be 5 days long
|
||||||
|
duration = (event.end_date - event.date).days + 1
|
||||||
|
assert duration == 6 # 51 days before to 46 days before Easter (6 days total)
|
||||||
|
|
||||||
|
# Both dates should be in February for 2024
|
||||||
|
assert event.date.month == 2
|
||||||
|
assert event.end_date.month == 2
|
||||||
|
|
||||||
|
# End date should be after start date
|
||||||
|
assert event.end_date > event.date
|
||||||
|
|
||||||
|
def test_carnival_events_empty_date_range(self) -> None:
|
||||||
|
"""Test with empty date range."""
|
||||||
|
start_date = date(2024, 6, 15)
|
||||||
|
end_date = date(2024, 6, 10) # End before start
|
||||||
|
|
||||||
|
events = rio_carnival_events(start_date, end_date)
|
||||||
|
|
||||||
|
# Should return empty list for invalid range
|
||||||
|
assert events == []
|
||||||
|
|
||||||
|
def test_carnival_events_same_start_end_date(self) -> None:
|
||||||
|
"""Test with same start and end date."""
|
||||||
|
# Pick a date that's definitely not carnival
|
||||||
|
test_date = date(2024, 7, 15)
|
||||||
|
|
||||||
|
events = rio_carnival_events(test_date, test_date)
|
||||||
|
|
||||||
|
assert events == []
|
168
tests/test_domains.py
Normal file
168
tests/test_domains.py
Normal file
|
@ -0,0 +1,168 @@
|
||||||
|
"""Tests for domain renewal functionality."""
|
||||||
|
|
||||||
|
import csv
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
from datetime import date
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from agenda.domains import renewal_dates, url
|
||||||
|
from agenda.event import Event
|
||||||
|
|
||||||
|
|
||||||
|
class TestRenewalDates:
|
||||||
|
"""Test the renewal_dates function."""
|
||||||
|
|
||||||
|
def test_renewal_dates_empty_directory(self) -> None:
|
||||||
|
"""Test with empty directory."""
|
||||||
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
with pytest.raises(ValueError, match="max\\(\\) iterable argument is empty"):
|
||||||
|
renewal_dates(temp_dir)
|
||||||
|
|
||||||
|
def test_renewal_dates_no_matching_files(self) -> None:
|
||||||
|
"""Test with directory containing no matching files."""
|
||||||
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
# Create some non-matching files
|
||||||
|
with open(os.path.join(temp_dir, "other_file.csv"), "w") as f:
|
||||||
|
f.write("test")
|
||||||
|
with open(os.path.join(temp_dir, "not_export.csv"), "w") as f:
|
||||||
|
f.write("test")
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="max\\(\\) iterable argument is empty"):
|
||||||
|
renewal_dates(temp_dir)
|
||||||
|
|
||||||
|
def test_renewal_dates_single_file(self) -> None:
|
||||||
|
"""Test with single domain export file."""
|
||||||
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
# Create a domain export file
|
||||||
|
filename = "export_domains_01_15_2024_02_30_PM.csv"
|
||||||
|
filepath = os.path.join(temp_dir, filename)
|
||||||
|
|
||||||
|
# Create CSV content
|
||||||
|
csv_data = [
|
||||||
|
["fqdn", "date_registry_end_utc"],
|
||||||
|
["example.com", "2024-06-15T00:00:00Z"],
|
||||||
|
["test.org", "2024-12-31T00:00:00Z"]
|
||||||
|
]
|
||||||
|
|
||||||
|
with open(filepath, "w", newline="") as f:
|
||||||
|
writer = csv.writer(f)
|
||||||
|
writer.writerows(csv_data)
|
||||||
|
|
||||||
|
result = renewal_dates(temp_dir)
|
||||||
|
|
||||||
|
assert len(result) == 2
|
||||||
|
|
||||||
|
# Check first domain
|
||||||
|
assert result[0].name == "domain"
|
||||||
|
assert result[0].title == "🌐 example.com"
|
||||||
|
assert result[0].date == date(2024, 6, 15)
|
||||||
|
assert result[0].url == url + "example.com"
|
||||||
|
|
||||||
|
# Check second domain
|
||||||
|
assert result[1].name == "domain"
|
||||||
|
assert result[1].title == "🌐 test.org"
|
||||||
|
assert result[1].date == date(2024, 12, 31)
|
||||||
|
assert result[1].url == url + "test.org"
|
||||||
|
|
||||||
|
def test_renewal_dates_multiple_files_latest_used(self) -> None:
|
||||||
|
"""Test that the latest file is used when multiple exist."""
|
||||||
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
# Create older file
|
||||||
|
older_filename = "export_domains_01_15_2024_02_30_PM.csv"
|
||||||
|
older_filepath = os.path.join(temp_dir, older_filename)
|
||||||
|
older_csv_data = [
|
||||||
|
["fqdn", "date_registry_end_utc"],
|
||||||
|
["old.com", "2024-06-15T00:00:00Z"]
|
||||||
|
]
|
||||||
|
with open(older_filepath, "w", newline="") as f:
|
||||||
|
writer = csv.writer(f)
|
||||||
|
writer.writerows(older_csv_data)
|
||||||
|
|
||||||
|
# Create newer file
|
||||||
|
newer_filename = "export_domains_01_16_2024_03_45_PM.csv"
|
||||||
|
newer_filepath = os.path.join(temp_dir, newer_filename)
|
||||||
|
newer_csv_data = [
|
||||||
|
["fqdn", "date_registry_end_utc"],
|
||||||
|
["new.com", "2024-08-20T00:00:00Z"]
|
||||||
|
]
|
||||||
|
with open(newer_filepath, "w", newline="") as f:
|
||||||
|
writer = csv.writer(f)
|
||||||
|
writer.writerows(newer_csv_data)
|
||||||
|
|
||||||
|
result = renewal_dates(temp_dir)
|
||||||
|
|
||||||
|
# Should only get data from newer file
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0].title == "🌐 new.com"
|
||||||
|
assert result[0].date == date(2024, 8, 20)
|
||||||
|
|
||||||
|
def test_renewal_dates_empty_csv(self) -> None:
|
||||||
|
"""Test with empty CSV file."""
|
||||||
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
filename = "export_domains_01_15_2024_02_30_PM.csv"
|
||||||
|
filepath = os.path.join(temp_dir, filename)
|
||||||
|
|
||||||
|
# Create CSV with only headers
|
||||||
|
csv_data = [["fqdn", "date_registry_end_utc"]]
|
||||||
|
|
||||||
|
with open(filepath, "w", newline="") as f:
|
||||||
|
writer = csv.writer(f)
|
||||||
|
writer.writerows(csv_data)
|
||||||
|
|
||||||
|
result = renewal_dates(temp_dir)
|
||||||
|
assert result == []
|
||||||
|
|
||||||
|
def test_renewal_dates_directory_not_found(self) -> None:
|
||||||
|
"""Test error handling when directory doesn't exist."""
|
||||||
|
with pytest.raises(FileNotFoundError):
|
||||||
|
renewal_dates("/nonexistent/directory")
|
||||||
|
|
||||||
|
def test_renewal_dates_malformed_filename(self) -> None:
|
||||||
|
"""Test with malformed filename that can't be parsed."""
|
||||||
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
# Create file with name that starts correctly but can't be parsed as date
|
||||||
|
filename = "export_domains_invalid_date.csv"
|
||||||
|
filepath = os.path.join(temp_dir, filename)
|
||||||
|
|
||||||
|
with open(filepath, "w") as f:
|
||||||
|
f.write("test")
|
||||||
|
|
||||||
|
# Should raise ValueError when trying to parse the malformed date
|
||||||
|
with pytest.raises(ValueError, match="time data .* does not match format"):
|
||||||
|
renewal_dates(temp_dir)
|
||||||
|
|
||||||
|
def test_renewal_dates_various_date_formats(self) -> None:
|
||||||
|
"""Test with various date formats in the CSV."""
|
||||||
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
filename = "export_domains_01_15_2024_02_30_PM.csv"
|
||||||
|
filepath = os.path.join(temp_dir, filename)
|
||||||
|
|
||||||
|
csv_data = [
|
||||||
|
["fqdn", "date_registry_end_utc"],
|
||||||
|
["example1.com", "2024-06-15T12:34:56Z"],
|
||||||
|
["example2.com", "2024-12-31T00:00:00+00:00"],
|
||||||
|
["example3.com", "2024-03-20T23:59:59.999Z"]
|
||||||
|
]
|
||||||
|
|
||||||
|
with open(filepath, "w", newline="") as f:
|
||||||
|
writer = csv.writer(f)
|
||||||
|
writer.writerows(csv_data)
|
||||||
|
|
||||||
|
result = renewal_dates(temp_dir)
|
||||||
|
|
||||||
|
assert len(result) == 3
|
||||||
|
assert result[0].date == date(2024, 6, 15)
|
||||||
|
assert result[1].date == date(2024, 12, 31)
|
||||||
|
assert result[2].date == date(2024, 3, 20)
|
||||||
|
|
||||||
|
|
||||||
|
class TestConstants:
|
||||||
|
"""Test module constants."""
|
||||||
|
|
||||||
|
def test_url_constant(self) -> None:
|
||||||
|
"""Test that URL constant has expected value."""
|
||||||
|
expected_url = "https://admin.gandi.net/domain/01578ef0-a84b-11e7-bdf3-00163e6dc886/"
|
||||||
|
assert url == expected_url
|
49
tests/test_events_yaml.py
Normal file
49
tests/test_events_yaml.py
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
"""Tests for events_yaml functionality."""
|
||||||
|
|
||||||
|
from datetime import date, datetime, time
|
||||||
|
|
||||||
|
from agenda.events_yaml import midnight
|
||||||
|
|
||||||
|
|
||||||
|
class TestMidnight:
|
||||||
|
"""Test the midnight function."""
|
||||||
|
|
||||||
|
def test_midnight_with_date(self) -> None:
|
||||||
|
"""Test converting date to midnight datetime."""
|
||||||
|
test_date = date(2024, 6, 15)
|
||||||
|
result = midnight(test_date)
|
||||||
|
|
||||||
|
assert isinstance(result, datetime)
|
||||||
|
assert result.date() == test_date
|
||||||
|
assert result.time() == time(0, 0, 0) # Midnight
|
||||||
|
assert result == datetime(2024, 6, 15, 0, 0, 0)
|
||||||
|
|
||||||
|
def test_midnight_different_dates(self) -> None:
|
||||||
|
"""Test midnight function with different dates."""
|
||||||
|
test_cases = [
|
||||||
|
date(2024, 1, 1), # New Year's Day
|
||||||
|
date(2024, 12, 31), # New Year's Eve
|
||||||
|
date(2024, 2, 29), # Leap day
|
||||||
|
date(2024, 7, 4), # Independence Day
|
||||||
|
]
|
||||||
|
|
||||||
|
for test_date in test_cases:
|
||||||
|
result = midnight(test_date)
|
||||||
|
assert result.date() == test_date
|
||||||
|
assert result.time() == time(0, 0, 0)
|
||||||
|
assert result.hour == 0
|
||||||
|
assert result.minute == 0
|
||||||
|
assert result.second == 0
|
||||||
|
assert result.microsecond == 0
|
||||||
|
|
||||||
|
def test_midnight_preserves_year_month_day(self) -> None:
|
||||||
|
"""Test that midnight preserves the year, month, and day."""
|
||||||
|
test_date = date(2023, 11, 27)
|
||||||
|
result = midnight(test_date)
|
||||||
|
|
||||||
|
assert result.year == 2023
|
||||||
|
assert result.month == 11
|
||||||
|
assert result.day == 27
|
||||||
|
assert result.hour == 0
|
||||||
|
assert result.minute == 0
|
||||||
|
assert result.second == 0
|
182
tests/test_fx.py
Normal file
182
tests/test_fx.py
Normal file
|
@ -0,0 +1,182 @@
|
||||||
|
"""Tests for foreign exchange functionality."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
from decimal import Decimal
|
||||||
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from agenda.fx import read_cached_rates
|
||||||
|
|
||||||
|
|
||||||
|
class TestReadCachedRates:
|
||||||
|
"""Test the read_cached_rates function."""
|
||||||
|
|
||||||
|
def test_read_cached_rates_none_filename(self) -> None:
|
||||||
|
"""Test with None filename returns empty dict."""
|
||||||
|
result = read_cached_rates(None, ["USD", "EUR"])
|
||||||
|
assert result == {}
|
||||||
|
|
||||||
|
def test_read_cached_rates_valid_file(self) -> None:
|
||||||
|
"""Test reading valid cached rates file."""
|
||||||
|
currencies = ["USD", "EUR", "JPY"]
|
||||||
|
data = {
|
||||||
|
"quotes": {
|
||||||
|
"GBPUSD": 1.25,
|
||||||
|
"GBPEUR": 1.15,
|
||||||
|
"GBPJPY": 150.0,
|
||||||
|
"GBPCAD": 1.70 # Not requested
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
||||||
|
json.dump(data, f)
|
||||||
|
filepath = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = read_cached_rates(filepath, currencies)
|
||||||
|
|
||||||
|
assert len(result) == 3
|
||||||
|
assert result["USD"] == Decimal("1.25")
|
||||||
|
assert result["EUR"] == Decimal("1.15")
|
||||||
|
assert result["JPY"] == Decimal("150.0")
|
||||||
|
assert "CAD" not in result # Not requested
|
||||||
|
|
||||||
|
finally:
|
||||||
|
os.unlink(filepath)
|
||||||
|
|
||||||
|
def test_read_cached_rates_missing_currencies(self) -> None:
|
||||||
|
"""Test with some currencies missing from data."""
|
||||||
|
currencies = ["USD", "EUR", "CHF"] # CHF not in data
|
||||||
|
data = {
|
||||||
|
"quotes": {
|
||||||
|
"GBPUSD": 1.25,
|
||||||
|
"GBPEUR": 1.15
|
||||||
|
# GBPCHF missing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
||||||
|
json.dump(data, f)
|
||||||
|
filepath = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = read_cached_rates(filepath, currencies)
|
||||||
|
|
||||||
|
assert len(result) == 2
|
||||||
|
assert result["USD"] == Decimal("1.25")
|
||||||
|
assert result["EUR"] == Decimal("1.15")
|
||||||
|
assert "CHF" not in result # Missing from data
|
||||||
|
|
||||||
|
finally:
|
||||||
|
os.unlink(filepath)
|
||||||
|
|
||||||
|
def test_read_cached_rates_empty_currencies_list(self) -> None:
|
||||||
|
"""Test with empty currencies list."""
|
||||||
|
data = {
|
||||||
|
"quotes": {
|
||||||
|
"GBPUSD": 1.25,
|
||||||
|
"GBPEUR": 1.15
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
||||||
|
json.dump(data, f)
|
||||||
|
filepath = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = read_cached_rates(filepath, [])
|
||||||
|
assert result == {}
|
||||||
|
|
||||||
|
finally:
|
||||||
|
os.unlink(filepath)
|
||||||
|
|
||||||
|
def test_read_cached_rates_no_quotes_key(self) -> None:
|
||||||
|
"""Test with data missing quotes key."""
|
||||||
|
currencies = ["USD", "EUR"]
|
||||||
|
data = {"other_key": "value"} # No quotes key
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
||||||
|
json.dump(data, f)
|
||||||
|
filepath = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
with pytest.raises(KeyError):
|
||||||
|
read_cached_rates(filepath, currencies)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
os.unlink(filepath)
|
||||||
|
|
||||||
|
def test_read_cached_rates_file_not_found(self) -> None:
|
||||||
|
"""Test error handling when file doesn't exist."""
|
||||||
|
with pytest.raises(FileNotFoundError):
|
||||||
|
read_cached_rates("/nonexistent/file.json", ["USD"])
|
||||||
|
|
||||||
|
def test_read_cached_rates_invalid_json(self) -> None:
|
||||||
|
"""Test error handling with invalid JSON."""
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
||||||
|
f.write("invalid json content")
|
||||||
|
filepath = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
with pytest.raises(json.JSONDecodeError):
|
||||||
|
read_cached_rates(filepath, ["USD"])
|
||||||
|
|
||||||
|
finally:
|
||||||
|
os.unlink(filepath)
|
||||||
|
|
||||||
|
def test_read_cached_rates_decimal_precision(self) -> None:
|
||||||
|
"""Test that rates are returned as Decimal with proper precision."""
|
||||||
|
currencies = ["USD"]
|
||||||
|
data = {
|
||||||
|
"quotes": {
|
||||||
|
"GBPUSD": 1.234567890123456789 # High precision
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
||||||
|
json.dump(data, f)
|
||||||
|
filepath = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = read_cached_rates(filepath, currencies)
|
||||||
|
|
||||||
|
assert isinstance(result["USD"], Decimal)
|
||||||
|
# Should preserve reasonable precision from the JSON
|
||||||
|
# Python's JSON precision may be limited to float precision
|
||||||
|
expected = Decimal("1.234567890123456789")
|
||||||
|
assert abs(result["USD"] - expected) < Decimal("0.0000000000000001")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
os.unlink(filepath)
|
||||||
|
|
||||||
|
def test_read_cached_rates_various_currency_codes(self) -> None:
|
||||||
|
"""Test with various currency codes."""
|
||||||
|
currencies = ["USD", "EUR", "JPY", "CHF", "CAD", "AUD"]
|
||||||
|
data = {
|
||||||
|
"quotes": {
|
||||||
|
"GBPUSD": 1.25,
|
||||||
|
"GBPEUR": 1.15,
|
||||||
|
"GBPJPY": 150.0,
|
||||||
|
"GBPCHF": 1.12,
|
||||||
|
"GBPCAD": 1.70,
|
||||||
|
"GBPAUD": 1.85
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
||||||
|
json.dump(data, f)
|
||||||
|
filepath = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = read_cached_rates(filepath, currencies)
|
||||||
|
|
||||||
|
assert len(result) == 6
|
||||||
|
for currency in currencies:
|
||||||
|
assert currency in result
|
||||||
|
assert isinstance(result[currency], Decimal)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
os.unlink(filepath)
|
183
tests/test_sun.py
Normal file
183
tests/test_sun.py
Normal file
|
@ -0,0 +1,183 @@
|
||||||
|
"""Tests for sun functionality."""
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from agenda.sun import bristol, sunrise, sunset
|
||||||
|
|
||||||
|
|
||||||
|
class TestBristol:
|
||||||
|
"""Test the bristol function."""
|
||||||
|
|
||||||
|
def test_bristol_returns_observer(self) -> None:
|
||||||
|
"""Test that bristol returns an ephem.Observer with correct coordinates."""
|
||||||
|
observer = bristol()
|
||||||
|
|
||||||
|
# Check that it's an observer object with the right attributes
|
||||||
|
assert hasattr(observer, 'lat')
|
||||||
|
assert hasattr(observer, 'lon')
|
||||||
|
|
||||||
|
# Check coordinates are set to Bristol
|
||||||
|
assert str(observer.lat) == "51:27:16.2" # 51.4545 degrees
|
||||||
|
assert str(observer.lon) == "-2:35:16.4" # -2.5879 degrees
|
||||||
|
|
||||||
|
def test_bristol_observer_type(self) -> None:
|
||||||
|
"""Test that bristol returns the correct type."""
|
||||||
|
observer = bristol()
|
||||||
|
|
||||||
|
# Should have methods needed for sun calculations
|
||||||
|
assert hasattr(observer, 'next_rising')
|
||||||
|
assert hasattr(observer, 'next_setting')
|
||||||
|
|
||||||
|
|
||||||
|
class TestSunrise:
|
||||||
|
"""Test the sunrise function."""
|
||||||
|
|
||||||
|
@patch('agenda.sun.ephem')
|
||||||
|
def test_sunrise_returns_datetime(self, mock_ephem: Mock) -> None:
|
||||||
|
"""Test that sunrise returns a datetime object."""
|
||||||
|
# Mock the observer and sun
|
||||||
|
mock_observer = Mock()
|
||||||
|
mock_sun = Mock()
|
||||||
|
mock_ephem.Sun.return_value = mock_sun
|
||||||
|
|
||||||
|
# Mock the rising calculation
|
||||||
|
mock_rising = Mock()
|
||||||
|
mock_rising.datetime.return_value = datetime(2024, 6, 15, 5, 30, 0)
|
||||||
|
mock_observer.next_rising.return_value = mock_rising
|
||||||
|
|
||||||
|
result = sunrise(mock_observer)
|
||||||
|
|
||||||
|
assert isinstance(result, datetime)
|
||||||
|
assert result == datetime(2024, 6, 15, 5, 30, 0)
|
||||||
|
|
||||||
|
# Verify ephem calls
|
||||||
|
mock_ephem.Sun.assert_called_once_with(mock_observer)
|
||||||
|
mock_observer.next_rising.assert_called_once_with(mock_sun)
|
||||||
|
|
||||||
|
def test_sunrise_with_real_observer(self) -> None:
|
||||||
|
"""Test sunrise with a real observer (integration test)."""
|
||||||
|
observer = bristol()
|
||||||
|
|
||||||
|
# Set a specific date for the observer
|
||||||
|
observer.date = "2024/6/21" # Summer solstice
|
||||||
|
|
||||||
|
result = sunrise(observer)
|
||||||
|
|
||||||
|
# Should return a datetime object
|
||||||
|
assert isinstance(result, datetime)
|
||||||
|
|
||||||
|
# On summer solstice in Bristol, sunrise should be early morning
|
||||||
|
assert 3 <= result.hour <= 6 # Roughly between 3-6 AM
|
||||||
|
|
||||||
|
# Should be in 2024
|
||||||
|
assert result.year == 2024
|
||||||
|
|
||||||
|
def test_sunrise_different_dates(self) -> None:
|
||||||
|
"""Test sunrise for different dates."""
|
||||||
|
observer = bristol()
|
||||||
|
|
||||||
|
# Summer solstice (longest day)
|
||||||
|
observer.date = "2024/6/21"
|
||||||
|
summer_sunrise = sunrise(observer)
|
||||||
|
|
||||||
|
# Winter solstice (shortest day)
|
||||||
|
observer.date = "2024/12/21"
|
||||||
|
winter_sunrise = sunrise(observer)
|
||||||
|
|
||||||
|
# Summer sunrise should be earlier than winter sunrise
|
||||||
|
assert summer_sunrise.hour < winter_sunrise.hour
|
||||||
|
|
||||||
|
|
||||||
|
class TestSunset:
|
||||||
|
"""Test the sunset function."""
|
||||||
|
|
||||||
|
@patch('agenda.sun.ephem')
|
||||||
|
def test_sunset_returns_datetime(self, mock_ephem: Mock) -> None:
|
||||||
|
"""Test that sunset returns a datetime object."""
|
||||||
|
# Mock the observer and sun
|
||||||
|
mock_observer = Mock()
|
||||||
|
mock_sun = Mock()
|
||||||
|
mock_ephem.Sun.return_value = mock_sun
|
||||||
|
|
||||||
|
# Mock the setting calculation
|
||||||
|
mock_setting = Mock()
|
||||||
|
mock_setting.datetime.return_value = datetime(2024, 6, 15, 20, 30, 0)
|
||||||
|
mock_observer.next_setting.return_value = mock_setting
|
||||||
|
|
||||||
|
result = sunset(mock_observer)
|
||||||
|
|
||||||
|
assert isinstance(result, datetime)
|
||||||
|
assert result == datetime(2024, 6, 15, 20, 30, 0)
|
||||||
|
|
||||||
|
# Verify ephem calls
|
||||||
|
mock_ephem.Sun.assert_called_once_with(mock_observer)
|
||||||
|
mock_observer.next_setting.assert_called_once_with(mock_sun)
|
||||||
|
|
||||||
|
def test_sunset_with_real_observer(self) -> None:
|
||||||
|
"""Test sunset with a real observer (integration test)."""
|
||||||
|
observer = bristol()
|
||||||
|
|
||||||
|
# Set a specific date for the observer
|
||||||
|
observer.date = "2024/6/21" # Summer solstice
|
||||||
|
|
||||||
|
result = sunset(observer)
|
||||||
|
|
||||||
|
# Should return a datetime object
|
||||||
|
assert isinstance(result, datetime)
|
||||||
|
|
||||||
|
# On summer solstice in Bristol, sunset should be in evening
|
||||||
|
assert 19 <= result.hour <= 22 # Roughly between 7-10 PM
|
||||||
|
|
||||||
|
# Should be in 2024
|
||||||
|
assert result.year == 2024
|
||||||
|
|
||||||
|
def test_sunset_different_dates(self) -> None:
|
||||||
|
"""Test sunset for different dates."""
|
||||||
|
observer = bristol()
|
||||||
|
|
||||||
|
# Summer solstice (longest day)
|
||||||
|
observer.date = "2024/6/21"
|
||||||
|
summer_sunset = sunset(observer)
|
||||||
|
|
||||||
|
# Winter solstice (shortest day)
|
||||||
|
observer.date = "2024/12/21"
|
||||||
|
winter_sunset = sunset(observer)
|
||||||
|
|
||||||
|
# Summer sunset should be later than winter sunset
|
||||||
|
assert summer_sunset.hour > winter_sunset.hour
|
||||||
|
|
||||||
|
|
||||||
|
class TestSunIntegration:
|
||||||
|
"""Integration tests for sun calculations."""
|
||||||
|
|
||||||
|
def test_day_length_summer_vs_winter(self) -> None:
|
||||||
|
"""Test that summer days are longer than winter days."""
|
||||||
|
observer = bristol()
|
||||||
|
|
||||||
|
# Summer solstice
|
||||||
|
observer.date = "2024/6/21"
|
||||||
|
summer_sunrise = sunrise(observer)
|
||||||
|
summer_sunset = sunset(observer)
|
||||||
|
summer_day_length = summer_sunset - summer_sunrise
|
||||||
|
|
||||||
|
# Winter solstice
|
||||||
|
observer.date = "2024/12/21"
|
||||||
|
winter_sunrise = sunrise(observer)
|
||||||
|
winter_sunset = sunset(observer)
|
||||||
|
winter_day_length = winter_sunset - winter_sunrise
|
||||||
|
|
||||||
|
# Summer day should be longer than winter day
|
||||||
|
assert summer_day_length > winter_day_length
|
||||||
|
|
||||||
|
def test_sunrise_before_sunset(self) -> None:
|
||||||
|
"""Test that sunrise is always before sunset."""
|
||||||
|
observer = bristol()
|
||||||
|
observer.date = "2024/6/15" # Arbitrary date
|
||||||
|
|
||||||
|
sunrise_time = sunrise(observer)
|
||||||
|
sunset_time = sunset(observer)
|
||||||
|
|
||||||
|
assert sunrise_time < sunset_time
|
Loading…
Reference in a new issue