| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282 |
- """Tests for Arrived/Departed binary sensors, Guest Arrived/Departed buttons,
- and coordinator arrived/departed flag lifecycle.
- Uses object.__new__ for pure-unit tests and hass fixtures for coordinator integration.
- """
- from datetime import date, time
- from unittest.mock import AsyncMock, MagicMock, patch
- import pytest
- from homeassistant.core import HomeAssistant
- from pytest_homeassistant_custom_component.common import MockConfigEntry
- from custom_components.room_cal_ha.binary_sensor import ArrivedSensor, DepartedSensor
- from custom_components.room_cal_ha.button import (
- GuestArrivedButton,
- GuestDepartedButton,
- GuestEnteredButton,
- )
- from custom_components.room_cal_ha.const import (
- CONF_SYNC_MODE,
- DOMAIN,
- STORE_CURRENT,
- STORE_NEXT,
- SYNC_MODE_CALDAV,
- SYNC_MODE_MANUAL,
- )
- from custom_components.room_cal_ha.coordinator import CoordinatorData, RoomCalCoordinator
- from custom_components.room_cal_ha.models import ReservationData
- _ALICE = 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),
- )
- _BOB = ReservationData(
- guest_name="Bob",
- check_in_date=date(2026, 7, 6),
- check_in_time=time(15, 0),
- check_out_date=date(2026, 7, 10),
- check_out_time=time(11, 0),
- )
- _BASE_DATA = {"room_state_name": "Room State"}
- def _make_entry(hass, options: dict) -> MockConfigEntry:
- entry = MockConfigEntry(domain=DOMAIN, data=_BASE_DATA, options=options)
- entry.add_to_hass(hass)
- return entry
- # ---------------------------------------------------------------------------
- # ArrivedSensor / DepartedSensor unit tests
- # ---------------------------------------------------------------------------
- def _data(arrived=False, departed=False) -> CoordinatorData:
- return CoordinatorData(
- active=ReservationData(),
- current=ReservationData(),
- next=ReservationData(),
- current_last_refresh=None,
- next_last_refresh=None,
- active_arrived=arrived,
- active_departed=departed,
- )
- def _arrived_sensor(arrived=False) -> ArrivedSensor:
- s = object.__new__(ArrivedSensor)
- s.coordinator = MagicMock()
- s.coordinator.data = _data(arrived=arrived)
- return s
- def _departed_sensor(departed=False) -> DepartedSensor:
- s = object.__new__(DepartedSensor)
- s.coordinator = MagicMock()
- s.coordinator.data = _data(departed=departed)
- return s
- def test_arrived_sensor_default_false():
- assert _arrived_sensor(arrived=False).is_on is False
- def test_arrived_sensor_true_when_set():
- assert _arrived_sensor(arrived=True).is_on is True
- def test_departed_sensor_default_false():
- assert _departed_sensor(departed=False).is_on is False
- def test_departed_sensor_true_when_set():
- assert _departed_sensor(departed=True).is_on is True
- # ---------------------------------------------------------------------------
- # GuestArrivedButton / GuestDepartedButton unit tests
- # ---------------------------------------------------------------------------
- def _arrived_button() -> GuestArrivedButton:
- btn = object.__new__(GuestArrivedButton)
- btn._coordinator = MagicMock()
- btn._coordinator.async_set_arrived = AsyncMock()
- return btn
- def _departed_button() -> GuestDepartedButton:
- btn = object.__new__(GuestDepartedButton)
- btn._coordinator = MagicMock()
- btn._coordinator.async_set_departed = AsyncMock()
- return btn
- async def test_arrived_button_calls_set_arrived_true():
- btn = _arrived_button()
- await btn.async_press()
- btn._coordinator.async_set_arrived.assert_awaited_once_with(True)
- async def test_departed_button_calls_set_departed_true():
- btn = _departed_button()
- await btn.async_press()
- btn._coordinator.async_set_departed.assert_awaited_once_with(True)
- # ---------------------------------------------------------------------------
- # GuestEnteredButton unit tests
- # ---------------------------------------------------------------------------
- def _entered_button(arrived=False) -> GuestEnteredButton:
- btn = object.__new__(GuestEnteredButton)
- btn._coordinator = MagicMock()
- btn._coordinator.data = _data(arrived=arrived)
- btn._coordinator.async_set_arrived = AsyncMock()
- return btn
- async def test_entered_button_sets_arrived_when_not_yet_arrived():
- btn = _entered_button(arrived=False)
- await btn.async_press()
- btn._coordinator.async_set_arrived.assert_awaited_once_with(True)
- async def test_entered_button_does_not_re_set_arrived_when_already_arrived():
- btn = _entered_button(arrived=True)
- await btn.async_press()
- btn._coordinator.async_set_arrived.assert_not_awaited()
- # ---------------------------------------------------------------------------
- # Coordinator: async_set_arrived / async_set_departed
- # ---------------------------------------------------------------------------
- @pytest.fixture
- async def coord(hass: HomeAssistant):
- entry = _make_entry(hass, {CONF_SYNC_MODE: SYNC_MODE_MANUAL})
- c = RoomCalCoordinator(hass, entry)
- await c.async_setup()
- await c.async_refresh()
- return c
- async def test_set_arrived_sets_flag(coord: RoomCalCoordinator):
- await coord.async_set_arrived(True)
- assert coord._active_arrived is True
- assert coord.data.active_arrived is True
- async def test_set_arrived_persists_to_store(coord: RoomCalCoordinator):
- await coord.async_set_arrived(True)
- assert coord.store.get_arrived() is True
- async def test_set_departed_sets_flag(coord: RoomCalCoordinator):
- await coord.async_set_departed(True)
- assert coord._active_departed is True
- assert coord.data.active_departed is True
- async def test_set_departed_persists_to_store(coord: RoomCalCoordinator):
- await coord.async_set_departed(True)
- assert coord.store.get_departed() is True
- async def test_flags_load_from_store_on_startup(hass: HomeAssistant):
- entry = _make_entry(hass, {CONF_SYNC_MODE: SYNC_MODE_MANUAL})
- c = RoomCalCoordinator(hass, entry)
- await c.async_setup()
- await c.store.async_set_arrived(True)
- await c.store.async_set_departed(True)
- c2 = RoomCalCoordinator(hass, entry)
- await c2.async_setup()
- assert c2._active_arrived is True
- assert c2._active_departed is True
- # ---------------------------------------------------------------------------
- # Coordinator: flag reset on reservation identity change
- # ---------------------------------------------------------------------------
- async def test_flags_reset_when_reservation_changes(hass: HomeAssistant):
- entry = _make_entry(hass, {CONF_SYNC_MODE: SYNC_MODE_CALDAV, "caldav_url": "https://x", "calendar_name": "R"})
- c = RoomCalCoordinator(hass, entry)
- await c.async_setup()
- # Load Alice as current
- with patch("custom_components.room_cal_ha.coordinator.CalDavBackend") as M:
- M.return_value.async_fetch = AsyncMock(return_value=(_ALICE, None))
- await c.async_refresh()
- await c.async_set_arrived(True)
- assert c._active_arrived is True
- # Bob becomes current — different check-in identity
- with patch("custom_components.room_cal_ha.coordinator.CalDavBackend") as M:
- M.return_value.async_fetch = AsyncMock(return_value=(_BOB, None))
- await c.async_refresh()
- assert c._active_arrived is False
- assert c.data.active_arrived is False
- assert c.store.get_arrived() is False
- async def test_flags_not_reset_when_same_reservation(hass: HomeAssistant):
- entry = _make_entry(hass, {CONF_SYNC_MODE: SYNC_MODE_CALDAV, "caldav_url": "https://x", "calendar_name": "R"})
- c = RoomCalCoordinator(hass, entry)
- await c.async_setup()
- with patch("custom_components.room_cal_ha.coordinator.CalDavBackend") as M:
- M.return_value.async_fetch = AsyncMock(return_value=(_ALICE, None))
- await c.async_refresh()
- await c.async_set_arrived(True)
- # Same Alice again — no identity change
- with patch("custom_components.room_cal_ha.coordinator.CalDavBackend") as M:
- M.return_value.async_fetch = AsyncMock(return_value=(_ALICE, None))
- await c.async_refresh()
- assert c._active_arrived is True
- assert c.data.active_arrived is True
- async def test_flags_reset_on_promote_from_next(coord: RoomCalCoordinator):
- await coord.store.async_set(STORE_CURRENT, _ALICE)
- await coord.store.async_set(STORE_NEXT, _BOB)
- await coord.async_refresh()
- await coord.async_set_arrived(True)
- await coord.async_promote_next_to_active()
- assert coord._active_arrived is False
- assert coord.data.active_arrived is False
- async def test_flags_reset_on_clear_current(coord: RoomCalCoordinator):
- await coord.store.async_set(STORE_CURRENT, _ALICE)
- await coord.async_refresh()
- await coord.async_set_arrived(True)
- await coord.async_clear_active()
- assert coord._active_arrived is False
- assert coord.data.active_arrived is False
- async def test_flags_reset_on_checkout(coord: RoomCalCoordinator):
- await coord.store.async_set(STORE_CURRENT, _ALICE)
- await coord.async_refresh()
- await coord.async_set_arrived(True)
- await coord._async_do_check_out()
- assert coord._active_arrived is False
- assert coord.data.active_arrived is False
|