test_utils.py 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. import pytest
  2. from datetime import date, datetime, timedelta
  3. from pathlib import Path
  4. from room_cal._utils import safe_json_loads, iter_dates, event_dates, filter_events, read_env
  5. from room_cal.store._backend import EventRecord
  6. # --- safe_json_loads ---
  7. def test_safe_json_loads_none():
  8. assert safe_json_loads(None) is None
  9. def test_safe_json_loads_empty_string():
  10. assert safe_json_loads('') is None
  11. def test_safe_json_loads_invalid():
  12. assert safe_json_loads('not json') is None
  13. def test_safe_json_loads_valid_dict():
  14. assert safe_json_loads('{"k": 1}') == {'k': 1}
  15. def test_safe_json_loads_valid_list():
  16. assert safe_json_loads('[1, 2]') == [1, 2]
  17. def test_safe_json_loads_nested():
  18. assert safe_json_loads('{"a": {"b": true}}') == {'a': {'b': True}}
  19. # --- iter_dates ---
  20. def test_iter_dates_single_day():
  21. d = date(2025, 1, 15)
  22. assert list(iter_dates(d, d)) == [d]
  23. def test_iter_dates_range():
  24. start = date(2025, 3, 1)
  25. end = date(2025, 3, 5)
  26. result = list(iter_dates(start, end))
  27. assert result == [date(2025, 3, d) for d in range(1, 6)]
  28. def test_iter_dates_end_before_start():
  29. start = date(2025, 5, 10)
  30. end = date(2025, 5, 9)
  31. assert list(iter_dates(start, end)) == []
  32. def test_iter_dates_consecutive():
  33. start = date(2025, 1, 31)
  34. end = date(2025, 2, 2)
  35. result = list(iter_dates(start, end))
  36. assert result == [date(2025, 1, 31), date(2025, 2, 1), date(2025, 2, 2)]
  37. # --- event_dates ---
  38. def _allday(start: date, end: date) -> EventRecord:
  39. """All-day event; end is exclusive (CalDAV convention)."""
  40. return EventRecord(
  41. uid='test-uid',
  42. start=datetime(start.year, start.month, start.day),
  43. end=datetime(end.year, end.month, end.day),
  44. all_day=True,
  45. title='',
  46. description='',
  47. x_data=None,
  48. )
  49. def _timed(start: datetime, end: datetime) -> EventRecord:
  50. return EventRecord(
  51. uid='test-uid',
  52. start=start,
  53. end=end,
  54. all_day=False,
  55. title='',
  56. description='',
  57. x_data=None,
  58. )
  59. def test_event_dates_all_day_single():
  60. d = date(2025, 6, 10)
  61. ev = _allday(d, d + timedelta(days=1))
  62. assert list(event_dates(ev, d, d)) == [d]
  63. def test_event_dates_all_day_span():
  64. start = date(2025, 6, 10)
  65. end_ex = date(2025, 6, 14) # exclusive DTEND
  66. ev = _allday(start, end_ex)
  67. window_start = date(2025, 6, 9)
  68. window_end = date(2025, 6, 15)
  69. result = list(event_dates(ev, window_start, window_end))
  70. assert result == [date(2025, 6, d) for d in range(10, 14)]
  71. def test_event_dates_all_day_clamped_by_window():
  72. start = date(2025, 6, 10)
  73. end_ex = date(2025, 6, 14)
  74. ev = _allday(start, end_ex)
  75. window_start = date(2025, 6, 11)
  76. window_end = date(2025, 6, 12)
  77. result = list(event_dates(ev, window_start, window_end))
  78. assert result == [date(2025, 6, 11), date(2025, 6, 12)]
  79. def test_event_dates_all_day_no_overlap():
  80. d = date(2025, 6, 10)
  81. ev = _allday(d, d + timedelta(days=1))
  82. result = list(event_dates(ev, date(2025, 6, 11), date(2025, 6, 15)))
  83. assert result == []
  84. def test_event_dates_timed_event():
  85. dt = datetime(2025, 6, 10, 14, 0)
  86. ev = _timed(dt, dt + timedelta(hours=2))
  87. result = list(event_dates(ev, date(2025, 6, 10), date(2025, 6, 10)))
  88. assert result == [date(2025, 6, 10)]
  89. # --- filter_events ---
  90. def _ev(title: str, *, all_day=True, x_data='{}') -> EventRecord:
  91. d = datetime(2025, 1, 1)
  92. return EventRecord(
  93. uid='test-uid',
  94. start=d,
  95. end=d + timedelta(days=1),
  96. all_day=all_day,
  97. title=title,
  98. description='',
  99. x_data=x_data,
  100. )
  101. def test_filter_events_prefix_match():
  102. events = [_ev('Price: $100'), _ev('Other: thing'), _ev('Price: $200')]
  103. result = filter_events(events, 'Price: ')
  104. assert len(result) == 2
  105. assert all(e.title.startswith('Price: ') for e in result)
  106. def test_filter_events_no_match():
  107. events = [_ev('Closed: All'), _ev('Discount: 10%')]
  108. assert filter_events(events, 'Price: ') == []
  109. def test_filter_events_all_day_true():
  110. events = [_ev('Price: $100', all_day=True), _ev('Price: $200', all_day=False)]
  111. result = filter_events(events, 'Price: ', all_day=True)
  112. assert len(result) == 1
  113. assert result[0].all_day is True
  114. def test_filter_events_all_day_false():
  115. events = [_ev('Price: $100', all_day=True), _ev('Price: $200', all_day=False)]
  116. result = filter_events(events, 'Price: ', all_day=False)
  117. assert len(result) == 1
  118. assert result[0].all_day is False
  119. def test_filter_events_all_day_none_returns_both():
  120. events = [_ev('Price: $100', all_day=True), _ev('Price: $200', all_day=False)]
  121. assert len(filter_events(events, 'Price: ', all_day=None)) == 2
  122. def test_filter_events_require_x_data_excludes_none():
  123. events = [_ev('Price: $100', x_data='{}'), _ev('Price: $200', x_data=None)]
  124. result = filter_events(events, 'Price: ', require_x_data=True)
  125. assert len(result) == 1
  126. def test_filter_events_require_x_data_false_includes_none():
  127. events = [_ev('Price: $100', x_data='{}'), _ev('Price: $200', x_data=None)]
  128. result = filter_events(events, 'Price: ', require_x_data=False)
  129. assert len(result) == 2
  130. # --- read_env ---
  131. def test_read_env_basic(tmp_path):
  132. env_file = tmp_path / 'test.env'
  133. env_file.write_text('FOO=bar\nBAZ=qux\n')
  134. result = read_env(env_file)
  135. assert result == {'FOO': 'bar', 'BAZ': 'qux'}
  136. def test_read_env_strips_quotes(tmp_path):
  137. env_file = tmp_path / 'test.env'
  138. env_file.write_text("A='hello'\nB=\"world\"\n")
  139. result = read_env(env_file)
  140. assert result == {'A': 'hello', 'B': 'world'}
  141. def test_read_env_skips_comments_and_blanks(tmp_path):
  142. env_file = tmp_path / 'test.env'
  143. env_file.write_text('# comment\n\nFOO=bar\n')
  144. result = read_env(env_file)
  145. assert result == {'FOO': 'bar'}
  146. def test_read_env_missing_file(tmp_path):
  147. result = read_env(tmp_path / 'nonexistent.env')
  148. assert result == {}
  149. def test_read_env_value_with_equals(tmp_path):
  150. env_file = tmp_path / 'test.env'
  151. env_file.write_text('TOKEN=abc=def=ghi\n')
  152. result = read_env(env_file)
  153. assert result == {'TOKEN': 'abc=def=ghi'}