| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353 |
- from datetime import date, datetime, time
- import pytest
- from custom_components.room_cal_ha.models import GuestStateDiff, OverrideData, ReservationData, is_window_active, reservation_diff
- # ---------------------------------------------------------------------------
- # ReservationData — serialization
- # ---------------------------------------------------------------------------
- def test_to_dict_serializes_dates_and_times():
- res = ReservationData(
- guest_name="Alice",
- check_in_date=date(2026, 7, 1),
- check_in_time=time(14, 0),
- check_out_date=date(2026, 7, 5),
- check_out_time=time(12, 0),
- wifi_key="abc",
- door_code="1234",
- )
- d = res.to_dict()
- assert d["guest_name"] == "Alice"
- assert d["check_in_date"] == "2026-07-01"
- assert d["check_in_time"] == "14:00:00"
- assert d["check_out_date"] == "2026-07-05"
- assert d["check_out_time"] == "12:00:00"
- assert d["wifi_key"] == "abc"
- assert d["door_code"] == "1234"
- def test_to_dict_none_fields_stay_none():
- res = ReservationData(guest_name=None, check_in_date=None, wifi_key=None)
- d = res.to_dict()
- assert d["guest_name"] is None
- assert d["check_in_date"] is None
- assert d["wifi_key"] is None
- def test_from_dict_deserializes_iso_strings():
- d = {
- "guest_name": "Bob",
- "check_in_date": "2026-08-01",
- "check_in_time": "15:00:00",
- "check_out_date": "2026-08-04",
- "check_out_time": "11:00:00",
- "wifi_ssid": "Net",
- "wifi_key": "xyz",
- "door_code": "9876",
- }
- res = ReservationData.from_dict(d)
- assert res.guest_name == "Bob"
- assert res.check_in_date == date(2026, 8, 1)
- assert res.check_in_time == time(15, 0)
- assert res.check_out_date == date(2026, 8, 4)
- assert res.check_out_time == time(11, 0)
- assert res.wifi_key == "xyz"
- assert res.door_code == "9876"
- def test_from_dict_none_fields():
- d = {
- "guest_name": None,
- "check_in_date": None,
- "check_in_time": None,
- "check_out_date": None,
- "check_out_time": None,
- "wifi_ssid": None,
- "wifi_key": None,
- "door_code": None,
- }
- res = ReservationData.from_dict(d)
- assert res.guest_name is None
- assert res.check_in_date is None
- assert res.check_in_time is None
- def test_round_trip():
- original = ReservationData(
- guest_name="Carol",
- check_in_date=date(2026, 9, 10),
- check_in_time=time(14, 30),
- check_out_date=date(2026, 9, 14),
- check_out_time=time(11, 0),
- wifi_ssid="HomeNet",
- wifi_key="pw",
- door_code="0000",
- )
- assert ReservationData.from_dict(original.to_dict()) == original
- # ---------------------------------------------------------------------------
- # ReservationData — apply_partial
- # ---------------------------------------------------------------------------
- SAMPLE = ReservationData(
- guest_name="Alice",
- check_in_date=date(2026, 7, 1),
- check_in_time=time(14, 0),
- check_out_date=date(2026, 7, 5),
- check_out_time=time(12, 0),
- wifi_ssid="4114",
- wifi_key="stone",
- door_code="0472",
- )
- def test_apply_partial_overlays_specified_fields():
- updated = SAMPLE.apply_partial({"wifi_ssid": "GuestNet", "door_code": "9999"})
- assert updated.wifi_ssid == "GuestNet"
- assert updated.door_code == "9999"
- def test_apply_partial_leaves_other_fields_unchanged():
- updated = SAMPLE.apply_partial({"door_code": "1111"})
- assert updated.guest_name == "Alice"
- assert updated.check_in_date == date(2026, 7, 1)
- assert updated.wifi_key == "stone"
- def test_apply_partial_can_clear_a_field_with_none():
- updated = SAMPLE.apply_partial({"guest_name": None})
- assert updated.guest_name is None
- def test_apply_partial_ignores_unknown_keys():
- updated = SAMPLE.apply_partial({"nonexistent_field": "x"})
- assert updated == SAMPLE
- def test_apply_partial_empty_dict_returns_equivalent():
- updated = SAMPLE.apply_partial({})
- assert updated == SAMPLE
- # ---------------------------------------------------------------------------
- # ReservationData — defaults
- # ---------------------------------------------------------------------------
- def test_default_values():
- res = ReservationData()
- assert res.guest_name is None
- assert res.check_in_date is None
- assert res.check_in_time is None
- assert res.check_out_date is None
- assert res.check_out_time is None
- assert res.wifi_ssid is None
- assert res.wifi_key is None
- assert res.door_code is None
- # ---------------------------------------------------------------------------
- # OverrideData
- # ---------------------------------------------------------------------------
- _RES_A = ReservationData(guest_name="Alice")
- _RES_B = ReservationData(guest_name="Bob")
- def test_override_data_requires_current():
- o = OverrideData(current=_RES_A)
- assert o.current.guest_name == "Alice"
- def test_override_data_next_defaults_to_none():
- o = OverrideData(current=_RES_A)
- assert o.next is None
- def test_override_data_with_next():
- o = OverrideData(current=_RES_A, next=_RES_B)
- assert o.current.guest_name == "Alice"
- assert o.next.guest_name == "Bob"
- # ---------------------------------------------------------------------------
- # reservation_diff
- # ---------------------------------------------------------------------------
- def _res(checkin_h: int, checkout_h: int, *, wifi_key="key", wifi_ssid="ssid") -> ReservationData:
- """Build a reservation on 2026-07-01 with given check-in/out hours."""
- return ReservationData(
- guest_name="Guest",
- check_in_date=date(2026, 7, 1),
- check_in_time=time(checkin_h, 0),
- check_out_date=date(2026, 7, 1),
- check_out_time=time(checkout_h, 0),
- wifi_ssid=wifi_ssid,
- wifi_key=wifi_key,
- )
- _DURING = datetime(2026, 7, 1, 15, 0) # 15:00 — inside a 14:00–12:00 window
- _BEFORE = datetime(2026, 7, 1, 12, 0) # 12:00 — before 14:00 check-in
- _AFTER = datetime(2026, 7, 1, 22, 0) # 22:00 — after typical check-out
- def test_diff_both_inactive_returns_no_changes():
- diff = reservation_diff(None, None, _DURING)
- assert not diff.has_changes
- def test_diff_old_none_new_active_returns_checkin():
- res = _res(14, 20)
- diff = reservation_diff(None, res, _DURING)
- assert diff.checkin_needed
- assert not diff.checkout_needed
- assert diff.departing is None
- def test_diff_old_active_new_none_returns_checkout():
- res = _res(14, 20)
- diff = reservation_diff(res, None, _DURING)
- assert diff.checkout_needed
- assert not diff.checkin_needed
- assert diff.departing is res
- def test_diff_both_elapsed_returns_no_changes():
- # Window already passed for both — time-based handler owns this transition, not diff.
- old = _res(14, 20)
- diff = reservation_diff(old, old, _AFTER)
- assert not diff.has_changes
- def test_diff_backend_clears_active_reservation_returns_checkout():
- # Backend returns None while we're still inside the window → checkout needed.
- old = _res(14, 20)
- now = datetime(2026, 7, 1, 16, 0)
- diff = reservation_diff(old, None, now)
- assert diff.checkout_needed
- assert not diff.checkin_needed
- assert diff.departing is old
- def test_diff_same_window_no_changes_returns_empty():
- res = _res(14, 20)
- diff = reservation_diff(res, res, _DURING)
- assert not diff.has_changes
- def test_diff_same_window_wifi_key_changed():
- old = _res(14, 20, wifi_key="old-key")
- new = _res(14, 20, wifi_key="new-key")
- diff = reservation_diff(old, new, _DURING)
- assert diff.wifi_key_changed
- assert not diff.checkin_needed
- assert not diff.checkout_needed
- def test_diff_different_window_both_active_returns_checkout_and_checkin():
- old = ReservationData(
- check_in_date=date(2026, 7, 1), check_in_time=time(14, 0),
- check_out_date=date(2026, 7, 2), check_out_time=time(12, 0),
- wifi_ssid="s", wifi_key="k",
- )
- new = ReservationData(
- check_in_date=date(2026, 7, 1), check_in_time=time(10, 0),
- check_out_date=date(2026, 7, 2), check_out_time=time(12, 0),
- wifi_ssid="s", wifi_key="k",
- )
- now = datetime(2026, 7, 1, 15, 0) # after both check-ins
- diff = reservation_diff(old, new, now)
- assert diff.checkout_needed
- assert diff.checkin_needed
- assert diff.departing is old
- def test_diff_inactive_old_becomes_active_new_returns_checkin():
- res = _res(14, 20)
- diff = reservation_diff(res, res, _BEFORE) # same record, but we're before check-in
- # old is inactive (before window), new is inactive too → no changes
- assert not diff.has_changes
- def test_diff_no_checkin_time_treated_as_inactive():
- res = ReservationData(check_in_date=date(2026, 7, 1)) # no time fields
- diff = reservation_diff(None, res, _DURING)
- assert not diff.has_changes
- def test_guest_state_diff_has_changes_false_when_empty():
- assert not GuestStateDiff().has_changes
- def test_guest_state_diff_has_changes_true_for_checkin():
- assert GuestStateDiff(checkin_needed=True).has_changes
- # ---------------------------------------------------------------------------
- # is_window_active — boundary conditions
- #
- # These directly guard the fix for the midnight-boundary bug where date-only
- # comparisons would make a 14:00 check-in appear "active" at 00:00 that day.
- # ---------------------------------------------------------------------------
- def _stay(checkin_date, checkin_time, checkout_date=None, checkout_time=None) -> ReservationData:
- return ReservationData(
- check_in_date=checkin_date,
- check_in_time=checkin_time,
- check_out_date=checkout_date,
- check_out_time=checkout_time,
- )
- _CHECKIN_DAY = date(2026, 7, 1)
- _CHECKOUT_DAY = date(2026, 7, 5)
- _CHECKIN_TIME = time(14, 0)
- _CHECKOUT_TIME = time(11, 0)
- _STAY = _stay(_CHECKIN_DAY, _CHECKIN_TIME, _CHECKOUT_DAY, _CHECKOUT_TIME)
- def test_is_window_active_false_at_midnight_before_checkin():
- # 00:00 on check-in day — hours before the actual check-in time. This is the
- # exact scenario that triggered the midnight-boundary bug.
- midnight = datetime(_CHECKIN_DAY.year, _CHECKIN_DAY.month, _CHECKIN_DAY.day, 0, 0)
- assert is_window_active(_STAY, midnight) is False
- def test_is_window_active_false_just_before_checkin():
- one_minute_before = datetime(_CHECKIN_DAY.year, _CHECKIN_DAY.month, _CHECKIN_DAY.day, 13, 59)
- assert is_window_active(_STAY, one_minute_before) is False
- def test_is_window_active_true_at_checkin_time():
- at_checkin = datetime.combine(_CHECKIN_DAY, _CHECKIN_TIME)
- assert is_window_active(_STAY, at_checkin) is True
- def test_is_window_active_true_during_stay():
- mid_stay = datetime(2026, 7, 3, 10, 0)
- assert is_window_active(_STAY, mid_stay) is True
- def test_is_window_active_false_at_checkout_time():
- # Checkout boundary is exclusive: exactly at checkout_time → no longer active.
- at_checkout = datetime.combine(_CHECKOUT_DAY, _CHECKOUT_TIME)
- assert is_window_active(_STAY, at_checkout) is False
- def test_is_window_active_true_just_before_checkout():
- one_minute_before = datetime(_CHECKOUT_DAY.year, _CHECKOUT_DAY.month, _CHECKOUT_DAY.day, 10, 59)
- assert is_window_active(_STAY, one_minute_before) is True
- def test_is_window_active_false_when_no_checkin_time():
- # Date present but no time — treated as inactive (guards against date-only comparisons).
- date_only = ReservationData(check_in_date=_CHECKIN_DAY)
- assert is_window_active(date_only, datetime(_CHECKIN_DAY.year, _CHECKIN_DAY.month, _CHECKIN_DAY.day, 15, 0)) is False
- def test_is_window_active_false_when_reservation_is_none():
- assert is_window_active(None, datetime(2026, 7, 1, 15, 0)) is False
|