test_homestay.py 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. import pytest
  2. from datetime import date, timedelta
  3. from room_cal.service_api._homestay import _HomestayClient, HomestayAuthError, _fmt_date
  4. # ── helpers ────────────────────────────────────────────────────────────────────
  5. def _client(raw_data: dict) -> _HomestayClient:
  6. c = _HomestayClient(cookies='x', csrf_token='y', room_id='354616')
  7. c._calendar_data = raw_data
  8. return c
  9. # ── _fmt_date ──────────────────────────────────────────────────────────────────
  10. def test_fmt_date_single_digit_day():
  11. assert _fmt_date('2026-07-05') == '5 Jul 2026'
  12. def test_fmt_date_double_digit_day():
  13. assert _fmt_date('2026-12-27') == '27 Dec 2026'
  14. def test_fmt_date_jan():
  15. assert _fmt_date('2027-01-01') == '1 Jan 2027'
  16. # ── _build_day_map ─────────────────────────────────────────────────────────────
  17. _EMPTY = {'unavailabilities': [], 'room_price_overrides': [], 'leisure_bookings': []}
  18. def test_build_day_map_returns_all_dates_in_range():
  19. c = _client(_EMPTY)
  20. result = c._build_day_map(_EMPTY, '2026-08-01', '2026-08-03')
  21. assert set(result.keys()) == {'2026-08-01', '2026-08-02', '2026-08-03'}
  22. def test_build_day_map_default_state():
  23. c = _client(_EMPTY)
  24. day = c._build_day_map(_EMPTY, '2026-08-01', '2026-08-01')['2026-08-01']
  25. assert day.available is True
  26. assert day.booked is False
  27. assert day.price is None
  28. assert day.rooms_to_sell is None
  29. def test_build_day_map_unavailability_closed():
  30. data = {**_EMPTY, 'unavailabilities': [{'start_date': '2026-08-01', 'end_date': '2026-08-05'}]}
  31. c = _client(data)
  32. result = c._build_day_map(data, '2026-07-31', '2026-08-06')
  33. assert result['2026-07-31'].available is True
  34. assert result['2026-08-01'].available is False
  35. assert result['2026-08-01'].booked is False
  36. assert result['2026-08-05'].available is False
  37. assert result['2026-08-06'].available is True
  38. def test_build_day_map_price_cents_to_usd():
  39. data = {**_EMPTY, 'room_price_overrides': [
  40. {'date': '2026-08-10', 'nightly_rate': {'amount': 8300, 'currency': 'USD'}},
  41. ]}
  42. c = _client(data)
  43. day = c._build_day_map(data, '2026-08-10', '2026-08-10')['2026-08-10']
  44. assert day.price == 83
  45. assert day.available is True
  46. def test_build_day_map_booking_exclusive_end():
  47. data = {**_EMPTY, 'leisure_bookings': [{'start_date': '2026-09-01', 'end_date': '2026-09-03', 'id': 99}]}
  48. c = _client(data)
  49. result = c._build_day_map(data, '2026-08-31', '2026-09-04')
  50. assert result['2026-08-31'].booked is False
  51. assert result['2026-09-01'].booked is True
  52. assert result['2026-09-01'].available is False
  53. assert result['2026-09-02'].booked is True
  54. assert result['2026-09-03'].booked is False # checkout day — exclusive
  55. assert result['2026-09-04'].booked is False
  56. def test_build_day_map_booked_overrides_closed():
  57. data = {
  58. 'unavailabilities': [{'start_date': '2026-09-01', 'end_date': '2026-09-05'}],
  59. 'room_price_overrides': [],
  60. 'leisure_bookings': [{'start_date': '2026-09-02', 'end_date': '2026-09-04', 'id': 1}],
  61. }
  62. c = _client(data)
  63. result = c._build_day_map(data, '2026-09-01', '2026-09-05')
  64. assert result['2026-09-01'].available is False and result['2026-09-01'].booked is False
  65. assert result['2026-09-02'].booked is True
  66. assert result['2026-09-03'].booked is True
  67. assert result['2026-09-04'].booked is False # exclusive end — falls back to closed
  68. assert result['2026-09-04'].available is False
  69. async def test_get_reservations_always_empty():
  70. data = {**_EMPTY, 'leisure_bookings': [
  71. {'start_date': '2026-08-01', 'end_date': '2026-08-05', 'id': 1},
  72. ]}
  73. c = _client(data)
  74. assert await c.get_reservations('2026-08-01', '2026-09-01') == []
  75. # ── live tests (require homestay.env + config.json room_id) ───────────────────
  76. @pytest.fixture(scope='module')
  77. def live():
  78. try:
  79. return _HomestayClient.from_env()
  80. except (ValueError, KeyError):
  81. pytest.skip('homestay credentials not configured')
  82. async def test_get_calendar_returns_full_range(live):
  83. start = (date.today() + timedelta(days=1)).isoformat()
  84. end = (date.today() + timedelta(days=30)).isoformat()
  85. result = await live.get_calendar(start, end)
  86. assert len(result) == 30
  87. for ds, day in result.items():
  88. assert day.date == ds
  89. assert isinstance(day.available, bool)
  90. assert isinstance(day.booked, bool)
  91. assert day.price is None or isinstance(day.price, int)
  92. async def test_get_calendar_uses_cache(live):
  93. start = (date.today() + timedelta(days=1)).isoformat()
  94. end = (date.today() + timedelta(days=7)).isoformat()
  95. first = await live.get_calendar(start, end)
  96. second = await live.get_calendar(start, end)
  97. assert first == second
  98. async def test_set_price_roundtrip(live):
  99. start = date.today() + timedelta(days=1)
  100. end = start + timedelta(days=30)
  101. cal = await live.get_calendar(start.isoformat(), end.isoformat())
  102. target = next(
  103. (ds for ds, d in sorted(cal.items()) if d.available and not d.booked and d.price is not None),
  104. None,
  105. )
  106. if target is None:
  107. pytest.skip('no available priced date found in the next 30 days')
  108. original = cal[target].price
  109. await live.set_price(target, original - 1)
  110. assert (await live.get_calendar(target, target))[target].price == original - 1
  111. await live.set_price(target, original)
  112. assert (await live.get_calendar(target, target))[target].price == original
  113. async def test_set_availability_roundtrip(live):
  114. start = date.today() + timedelta(days=7)
  115. end = start + timedelta(days=30)
  116. cal = await live.get_calendar(start.isoformat(), end.isoformat())
  117. target = next(
  118. (ds for ds, d in sorted(cal.items()) if d.available and not d.booked),
  119. None,
  120. )
  121. if target is None:
  122. pytest.skip('no available unbooked date found in the next 30 days')
  123. await live.set_availability(target, False)
  124. assert (await live.get_calendar(target, target))[target].available is False
  125. await live.set_availability(target, True)
  126. assert (await live.get_calendar(target, target))[target].available is True