test_caldav_backend.py 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. """Unit tests for _parse_event in the CalDAV backend."""
  2. import json
  3. from datetime import date, time
  4. from unittest.mock import MagicMock
  5. import pytest
  6. from homeassistant.core import HomeAssistant
  7. from custom_components.room_cal_ha.backends.caldav import _parse_event
  8. from custom_components.room_cal_ha.models import ReservationData
  9. # ---------------------------------------------------------------------------
  10. # iCal helpers
  11. # ---------------------------------------------------------------------------
  12. _X_DATA = {
  13. "guest_name": "Alice Smith",
  14. "wifi_ssid": "GuestNetwork",
  15. "wifi_key": "secret-key",
  16. "door_code": "4567",
  17. }
  18. def _make_event(summary: str, dtstart: str, dtend: str, x_data=None, all_day=False):
  19. """Build a mock caldav.Event with the given iCal fields."""
  20. if all_day:
  21. start_line = f"DTSTART;VALUE=DATE:{dtstart}"
  22. end_line = f"DTEND;VALUE=DATE:{dtend}"
  23. else:
  24. start_line = f"DTSTART:{dtstart}Z"
  25. end_line = f"DTEND:{dtend}Z"
  26. lines = [
  27. "BEGIN:VCALENDAR",
  28. "VERSION:2.0",
  29. "BEGIN:VEVENT",
  30. f"SUMMARY:{summary}",
  31. start_line,
  32. end_line,
  33. ]
  34. if x_data is not None:
  35. lines.append(f"X-ROOMCAL-DATA:{json.dumps(x_data)}")
  36. lines += ["END:VEVENT", "END:VCALENDAR", ""]
  37. event = MagicMock()
  38. event.data = "\r\n".join(lines)
  39. return event
  40. # ---------------------------------------------------------------------------
  41. # Valid event
  42. # ---------------------------------------------------------------------------
  43. def test_parse_valid_event_returns_reservation_data(hass: HomeAssistant):
  44. event = _make_event("Guest: Alice", "20260701T140000", "20260705T120000", _X_DATA)
  45. result = _parse_event(event)
  46. assert isinstance(result, ReservationData)
  47. assert result.guest_name == "Alice Smith"
  48. assert result.wifi_ssid == "GuestNetwork"
  49. assert result.wifi_key == "secret-key"
  50. assert result.door_code == "4567"
  51. def test_parse_valid_event_extracts_dates(hass: HomeAssistant):
  52. event = _make_event("Guest: Alice", "20260701T140000", "20260705T120000", _X_DATA)
  53. result = _parse_event(event)
  54. assert result is not None
  55. assert result.check_in_date == date(2026, 7, 1)
  56. assert result.check_out_date == date(2026, 7, 5)
  57. def test_parse_valid_event_extracts_times(hass: HomeAssistant):
  58. from zoneinfo import ZoneInfo
  59. from homeassistant.util import dt as dt_util
  60. dt_util.set_default_time_zone(ZoneInfo("UTC"))
  61. event = _make_event("Guest: Alice", "20260701T140000", "20260705T120000", _X_DATA)
  62. result = _parse_event(event)
  63. assert result is not None
  64. assert result.check_in_time == time(14, 0)
  65. assert result.check_out_time == time(12, 0)
  66. # ---------------------------------------------------------------------------
  67. # Filtering — events that should return None
  68. # ---------------------------------------------------------------------------
  69. def test_parse_wrong_title_prefix_returns_none(hass: HomeAssistant):
  70. event = _make_event("Blocked", "20260701T140000", "20260705T120000", _X_DATA)
  71. assert _parse_event(event) is None
  72. def test_parse_all_day_event_returns_none(hass: HomeAssistant):
  73. event = _make_event("Guest: Alice", "20260701", "20260705", _X_DATA, all_day=True)
  74. assert _parse_event(event) is None
  75. def test_parse_missing_x_data_returns_none(hass: HomeAssistant):
  76. event = _make_event("Guest: Alice", "20260701T140000", "20260705T120000", x_data=None)
  77. assert _parse_event(event) is None
  78. def test_parse_invalid_json_in_x_data_returns_none(hass: HomeAssistant):
  79. event = _make_event("Guest: Alice", "20260701T140000", "20260705T120000")
  80. # Manually inject bad JSON into the iCal
  81. event.data = event.data.replace("END:VEVENT", "X-ROOMCAL-DATA:not-valid-json\r\nEND:VEVENT")
  82. assert _parse_event(event) is None
  83. def test_parse_garbage_ical_returns_none(hass: HomeAssistant):
  84. event = MagicMock()
  85. event.data = "this is not ical at all"
  86. # Should not raise — catches all exceptions
  87. result = _parse_event(event)
  88. assert result is None
  89. # ---------------------------------------------------------------------------
  90. # Partial data
  91. # ---------------------------------------------------------------------------
  92. def test_parse_event_missing_dtend_still_returns_result(hass: HomeAssistant):
  93. """DTEND is optional — event without it should still parse (checkout fields will be None)."""
  94. lines = [
  95. "BEGIN:VCALENDAR",
  96. "VERSION:2.0",
  97. "BEGIN:VEVENT",
  98. "SUMMARY:Guest: Alice",
  99. "DTSTART:20260701T140000Z",
  100. f"X-ROOMCAL-DATA:{json.dumps(_X_DATA)}",
  101. "END:VEVENT",
  102. "END:VCALENDAR",
  103. "",
  104. ]
  105. event = MagicMock()
  106. event.data = "\r\n".join(lines)
  107. result = _parse_event(event)
  108. assert result is not None
  109. assert result.check_in_date == date(2026, 7, 1)
  110. assert result.check_out_date is None
  111. assert result.check_out_time is None
  112. def test_parse_event_x_data_missing_optional_fields(hass: HomeAssistant):
  113. """X-ROOMCAL-DATA may omit wifi_ssid / wifi_key / door_code — those become None."""
  114. sparse = {"guest_name": "Bob"}
  115. event = _make_event("Guest: Bob", "20260801T140000", "20260803T120000", sparse)
  116. result = _parse_event(event)
  117. assert result is not None
  118. assert result.guest_name == "Bob"
  119. assert result.wifi_ssid is None
  120. assert result.wifi_key is None
  121. assert result.door_code is None