test_apply_rules.py 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. import pytest
  2. from datetime import date, timedelta
  3. from room_cal._utils import get_config, iter_dates, local_today, services as _services
  4. from room_cal.closed import ClosedData
  5. import room_cal.rules as _rules
  6. from room_cal.rules.closed.future_block import RULE_NAME, run as future_block_run
  7. # ── future_block (pure) ────────────────────────────────────────────────────────
  8. def _days_out() -> int:
  9. return get_config().rules.future_block.days_out
  10. @pytest.mark.asyncio
  11. async def test_future_block_rule_registered():
  12. assert RULE_NAME == 'future_block'
  13. assert 'future_block' in _rules.list_rules('closed')
  14. assert 'future_block' in _rules.list_pipelines(type_='closed')
  15. @pytest.mark.asyncio
  16. async def test_future_block_beyond_threshold_all_closed():
  17. """Every returned date strictly beyond today + days_out should have closed=True.
  18. Dates already blocked by a reservation or Closed event are omitted."""
  19. days_out = _days_out()
  20. start = local_today() + timedelta(days=days_out + 1)
  21. end = start + timedelta(days=4)
  22. result = await future_block_run(start, end, {})
  23. assert set(result).issubset(set(iter_dates(start, end)))
  24. assert all(bool(rec.services) for rec in result.values())
  25. @pytest.mark.asyncio
  26. async def test_future_block_within_threshold_all_open():
  27. """Every date on or before today + days_out should have closed=False."""
  28. days_out = _days_out()
  29. start = local_today() + timedelta(days=1)
  30. end = local_today() + timedelta(days=min(days_out - 1, 5))
  31. result = await future_block_run(start, end, {})
  32. assert all(not rec.services for rec in result.values())
  33. @pytest.mark.asyncio
  34. async def test_future_block_threshold_boundary():
  35. """The threshold date itself is not closed; the day after is.
  36. Dates already blocked on the calendar are omitted from results."""
  37. days_out = _days_out()
  38. threshold = local_today() + timedelta(days=days_out)
  39. result = await future_block_run(threshold - timedelta(days=1), threshold + timedelta(days=1), {})
  40. if threshold in result:
  41. assert not result[threshold].services
  42. if threshold + timedelta(days=1) in result:
  43. assert bool(result[threshold + timedelta(days=1)].services)
  44. @pytest.mark.asyncio
  45. async def test_future_block_covers_full_range():
  46. """run() returns at most one record per date, in range, with no duplicates."""
  47. days_out = _days_out()
  48. start = local_today() + timedelta(days=days_out - 2)
  49. end = start + timedelta(days=5)
  50. result = await future_block_run(start, end, {})
  51. assert set(result).issubset(set(iter_dates(start, end)))
  52. assert len(result) == len(set(result))
  53. # ── apply_rules — future_block (live CalDAV) ───────────────────────────────────
  54. @pytest.mark.asyncio
  55. async def test_apply_rules_future_block_creates_closed_events():
  56. """apply_rules with future_block writes Closed events for dates beyond the threshold."""
  57. from room_cal.actions.apply_rules import apply_rules
  58. from room_cal.closed import Closed
  59. closed_cal = Closed()
  60. days_out = _days_out()
  61. start = local_today() + timedelta(days=days_out + 10)
  62. end = start + timedelta(days=2)
  63. try:
  64. result = await apply_rules(start, end, pipeline_name='future_block')
  65. assert 'future_block' in result
  66. assert set(result['future_block']).issubset(set(iter_dates(start, end)))
  67. for d in iter_dates(start, end):
  68. svcs = await closed_cal.get(d)
  69. assert svcs is not None and svcs.services, f"Expected Closed event on {d}"
  70. svc_keys = sorted(s.key for s in _services())
  71. for d in iter_dates(start, end):
  72. assert sorted((await closed_cal.get(d)).services) == svc_keys
  73. finally:
  74. for d in iter_dates(start, end):
  75. await closed_cal.set(d, ClosedData(services=[]))
  76. @pytest.mark.asyncio
  77. async def test_apply_rules_future_block_removes_stale_closed_events():
  78. """Rule-sourced Closed events on dates now inside the threshold are removed on re-run."""
  79. from room_cal.actions.apply_rules import apply_rules
  80. from room_cal.closed import Closed
  81. closed_cal = Closed()
  82. svc_keys = [s.key for s in _services()]
  83. near_date = local_today() + timedelta(days=5)
  84. source = {'domain': 'rules', 'name': 'Rule =>', 'id': 'future_block'}
  85. await closed_cal.set(near_date, ClosedData(services=svc_keys, source=source))
  86. try:
  87. assert await closed_cal.get(near_date) is not None
  88. result = await apply_rules(near_date, near_date, pipeline_name='future_block')
  89. assert result['future_block'] == []
  90. assert await closed_cal.get(near_date) is None, f"Stale Closed event not removed on {near_date}"
  91. finally:
  92. await closed_cal.set(near_date, ClosedData(services=[]))
  93. @pytest.mark.asyncio
  94. async def test_apply_rules_future_block_idempotent():
  95. """Running apply_rules twice on the same beyond-threshold range is idempotent."""
  96. from room_cal.actions.apply_rules import apply_rules
  97. from room_cal.closed import Closed
  98. closed_cal = Closed()
  99. days_out = _days_out()
  100. start = local_today() + timedelta(days=days_out + 20)
  101. end = start + timedelta(days=1)
  102. try:
  103. result1 = await apply_rules(start, end, pipeline_name='future_block')
  104. result2 = await apply_rules(start, end, pipeline_name='future_block')
  105. assert result1 == result2
  106. for d in iter_dates(start, end):
  107. svcs = await closed_cal.get(d)
  108. assert svcs is not None and svcs.services
  109. finally:
  110. for d in iter_dates(start, end):
  111. await closed_cal.set(d, ClosedData(services=[]))