Repository: home-assistant/core
File: tests/components/simplisafe/test_init.py (Python)

Code before:
    simpli = hass.data[DOMAIN][config_entry.entry_id]
    # First, simulate failure to stop websocket
    await _trigger_requesterror_and_refresh(simpli)
    assert simpli._websocket_reconnect_task is None

    # Then patch systems to succeed
    for system in simpli.systems.values():
        system.async_update = AsyncMock(return_value=None)

    await simpli.coordinator.async_refresh()
    assert simpli._websocket_reconnect_task is not None


@pytest.mark.asyncio
async def test_invalid_credentials_raises_configentryauthfailed(
    hass: HomeAssistant, setup_simplisafe_integration, config_entry
) -> None:
    """InvalidCredentialsError triggers ConfigEntryAuthFailed."""
    simpli = hass.data[DOMAIN][config_entry.entry_id]

    for system in simpli.systems.values():
        system.async_update = AsyncMock(
            side_effect=InvalidCredentialsError("Bad token")
        )
    await simpli.coordinator.async_refresh()
    assert isinstance(simpli.coordinator.last_exception, ConfigEntryAuthFailed)


@pytest.mark.asyncio
async def test_simplipyerror_propagates(
    hass: HomeAssistant, setup_simplisafe_integration, config_entry
) -> None:
    """Other SimplipyError exceptions raise UpdateFailed."""
    simpli = hass.data[DOMAIN][config_entry.entry_id]

    for system in simpli.systems.values():
        system.async_update = AsyncMock(side_effect=SimplipyError("Some error"))

    await simpli.coordinator.async_refresh()
    assert isinstance(simpli.coordinator.last_exception, UpdateFailed)


Review comment (bug):
Okay so I understand that the tests probably are not as up to date, but I do want to give the general guideline here. We don't want to touch internals. `hass.data` is considered one.

So what we should be doing is
1. Patch the library to raise an error (which I think we do)
2. Trigger a natural update. In a polling based integration we can skip time to trigger one, in a push based, we could use the mock where the update callback was registered and call that method.

This way we don't hard commit on the implementation details (for example, once simplisafe moves to storing the runtime data in `entry.runtime_data` this test would fail).

As the PR is about websocket, this likely is push based, here's an example of that in niko_home_control
```py
def find_update_callback(
    mock: AsyncMock, identifier: int
) -> Callable[[int], Awaitable[None]]:
    """Find the update callback for a specific identifier."""
    for call in mock.register_callback.call_args_list:
        if call[0][0] == identifier:
            return call[0][1]
    pytest.fail(f"Callback for identifier {identifier} not found")
```

I don't want to make this a blocker per se, as a lot more effort is needed, but I wanted to drop this here anyway :)

Code after:
    patch_simplisafe_api,
    system_v3,
    freezer: FrozenDateTimeFactory,
    exc: type[SimplipyError],
) -> None:
    """Test that a single coordinator failure does not immediately mark entities unavailable."""
    await hass.config_entries.async_setup(config_entry.entry_id)
    await hass.async_block_till_done()

    assert hass.states.get("lock.front_door_lock").state != STATE_UNAVAILABLE

    system_v3.async_update = AsyncMock(side_effect=exc("fail"))

    # Trigger one coordinator failure: error_count goes from 0 to 1, below threshold.
    freezer.tick(DEFAULT_SCAN_INTERVAL)
    async_fire_time_changed(hass)
    await hass.async_block_till_done(wait_background_tasks=True)

    assert hass.states.get("lock.front_door_lock").state != STATE_UNAVAILABLE


async def test_websocket_event_updates_entity_state(
    hass: HomeAssistant,
    config_entry: MockConfigEntry,
    patch_simplisafe_api,
    websocket: Mock,
) -> None:
    """Test that a push update from the websocket changes entity state."""
    await hass.config_entries.async_setup(config_entry.entry_id)
    await hass.async_block_till_done()

    # Retrieve the event callback that was registered with the mock websocket.
    assert websocket.add_event_callback.called
    event_callback = websocket.add_event_callback.call_args[0][0]

    assert hass.states.get("lock.front_door_lock").state == "locked"

    # Fire an "unlock" websocket event for the test lock (system_id=12345, serial="987").
    # CID 9700 maps to EVENT_LOCK_UNLOCKED in the simplipy event mapping.
    event_callback(
        WebsocketEvent(
            event_cid=9700,
            info="Lock unlocked",
            system_id=12345,
            _raw_timestamp=0,
            _video=None,
            _vid=None,
            sensor_serial="987",
        )
    )
    await hass.async_block_till_done()