test_caldav_backend.py 5.0 KB

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