test_utils.py 5.6 KB

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