You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
212 lines
6.0 KiB
Python
212 lines
6.0 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import date
|
|
|
|
import pytest
|
|
|
|
import app.calendar_agent as calendar_module
|
|
from app.calendar_agent import CalendarAvailabilityAgent
|
|
|
|
|
|
class _FakeFreeBusy:
|
|
def __init__(self, payload: dict[str, object]) -> None:
|
|
self.payload = payload
|
|
|
|
def query(self, body: dict[str, object]) -> _FakeFreeBusy:
|
|
return self
|
|
|
|
def execute(self) -> dict[str, object]:
|
|
return self.payload
|
|
|
|
|
|
class _FakeCalendarService:
|
|
def __init__(self, payload: dict[str, object]) -> None:
|
|
self._freebusy = _FakeFreeBusy(payload)
|
|
|
|
def freebusy(self) -> _FakeFreeBusy:
|
|
return self._freebusy
|
|
|
|
|
|
def _build_agent(payload: dict[str, object]) -> CalendarAvailabilityAgent:
|
|
return CalendarAvailabilityAgent(calendar_service=_FakeCalendarService(payload))
|
|
|
|
|
|
def test_weekday_boundaries_and_busy_subtraction() -> None:
|
|
agent = _build_agent(
|
|
{
|
|
"calendars": {
|
|
"primary": {
|
|
"busy": [
|
|
{
|
|
"start": "2026-03-09T09:00:00+01:00",
|
|
"end": "2026-03-09T10:00:00+01:00",
|
|
}
|
|
]
|
|
},
|
|
"work": {
|
|
"busy": [
|
|
{
|
|
"start": "2026-03-09T09:30:00+01:00",
|
|
"end": "2026-03-09T11:00:00+01:00",
|
|
},
|
|
{
|
|
"start": "2026-03-09T12:00:00+01:00",
|
|
"end": "2026-03-09T12:20:00+01:00",
|
|
},
|
|
]
|
|
},
|
|
}
|
|
}
|
|
)
|
|
|
|
result = agent.get_available_meeting_intervals(
|
|
start="2026-03-09T07:00:00+01:00",
|
|
end="2026-03-09T22:00:00+01:00",
|
|
calendar_ids=["primary", "work"],
|
|
)
|
|
|
|
assert result.timezone == "Europe/Paris"
|
|
assert result.checked_calendars == ["primary", "work"]
|
|
assert result.meeting_intervals == [
|
|
{
|
|
"start": "2026-03-09T08:30:00+01:00",
|
|
"end": "2026-03-09T09:00:00+01:00",
|
|
},
|
|
{
|
|
"start": "2026-03-09T11:00:00+01:00",
|
|
"end": "2026-03-09T12:00:00+01:00",
|
|
},
|
|
{
|
|
"start": "2026-03-09T12:20:00+01:00",
|
|
"end": "2026-03-09T21:30:00+01:00",
|
|
},
|
|
]
|
|
|
|
|
|
def test_saturday_window_only() -> None:
|
|
agent = _build_agent({"calendars": {"primary": {"busy": []}}})
|
|
result = agent.get_available_meeting_intervals(
|
|
start="2026-03-14T08:00:00+01:00",
|
|
end="2026-03-14T13:00:00+01:00",
|
|
calendar_ids=None,
|
|
)
|
|
|
|
assert result.meeting_intervals == [
|
|
{
|
|
"start": "2026-03-14T09:30:00+01:00",
|
|
"end": "2026-03-14T12:00:00+01:00",
|
|
}
|
|
]
|
|
|
|
|
|
def test_sunday_excluded() -> None:
|
|
agent = _build_agent({"calendars": {"primary": {"busy": []}}})
|
|
result = agent.get_available_meeting_intervals(
|
|
start="2026-03-15T08:00:00+01:00",
|
|
end="2026-03-15T14:00:00+01:00",
|
|
calendar_ids=None,
|
|
)
|
|
|
|
assert result.meeting_intervals == []
|
|
|
|
|
|
def test_france_holiday_excluded(monkeypatch) -> None:
|
|
def _fake_country_holidays(
|
|
country: str,
|
|
*,
|
|
years: range,
|
|
observed: bool,
|
|
subdiv: str | None = None,
|
|
) -> dict[date, str]:
|
|
assert observed is False
|
|
if country == "FR":
|
|
return {date(2026, 7, 14): "Bastille Day"}
|
|
if country == "CA" and subdiv == "QC":
|
|
return {}
|
|
return {}
|
|
|
|
monkeypatch.setattr(calendar_module.holidays, "country_holidays", _fake_country_holidays)
|
|
agent = _build_agent({"calendars": {"primary": {"busy": []}}})
|
|
|
|
result = agent.get_available_meeting_intervals(
|
|
start="2026-07-14T08:00:00+02:00",
|
|
end="2026-07-14T22:00:00+02:00",
|
|
calendar_ids=None,
|
|
)
|
|
|
|
assert result.meeting_intervals == []
|
|
|
|
|
|
def test_quebec_holiday_excluded(monkeypatch) -> None:
|
|
def _fake_country_holidays(
|
|
country: str,
|
|
*,
|
|
years: range,
|
|
observed: bool,
|
|
subdiv: str | None = None,
|
|
) -> dict[date, str]:
|
|
assert observed is False
|
|
if country == "FR":
|
|
return {}
|
|
if country == "CA" and subdiv == "QC":
|
|
return {date(2026, 6, 24): "Saint-Jean-Baptiste Day"}
|
|
return {}
|
|
|
|
monkeypatch.setattr(calendar_module.holidays, "country_holidays", _fake_country_holidays)
|
|
agent = _build_agent({"calendars": {"primary": {"busy": []}}})
|
|
|
|
result = agent.get_available_meeting_intervals(
|
|
start="2026-06-24T08:00:00+02:00",
|
|
end="2026-06-24T22:00:00+02:00",
|
|
calendar_ids=None,
|
|
)
|
|
|
|
assert result.meeting_intervals == []
|
|
|
|
|
|
def test_intervals_shorter_than_30_minutes_are_filtered() -> None:
|
|
agent = _build_agent(
|
|
{
|
|
"calendars": {
|
|
"primary": {
|
|
"busy": [
|
|
{
|
|
"start": "2026-03-09T08:40:00+01:00",
|
|
"end": "2026-03-09T09:30:00+01:00",
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
)
|
|
|
|
result = agent.get_available_meeting_intervals(
|
|
start="2026-03-09T08:30:00+01:00",
|
|
end="2026-03-09T09:30:00+01:00",
|
|
calendar_ids=None,
|
|
)
|
|
|
|
assert result.meeting_intervals == []
|
|
|
|
|
|
def test_range_greater_than_90_days_rejected() -> None:
|
|
agent = _build_agent({"calendars": {"primary": {"busy": []}}})
|
|
|
|
with pytest.raises(ValueError, match="cannot exceed 90 days"):
|
|
agent.get_available_meeting_intervals(
|
|
start="2026-01-01T08:00:00+01:00",
|
|
end="2026-04-02T08:00:01+02:00",
|
|
calendar_ids=None,
|
|
)
|
|
|
|
|
|
def test_requires_timezone_offset() -> None:
|
|
agent = _build_agent({"calendars": {"primary": {"busy": []}}})
|
|
|
|
with pytest.raises(ValueError, match="timezone offset"):
|
|
agent.get_available_meeting_intervals(
|
|
start="2026-03-10T08:00:00",
|
|
end="2026-03-10T10:00:00+01:00",
|
|
calendar_ids=None,
|
|
)
|