diff --git a/homeassistant/components/monzo/services.py b/homeassistant/components/monzo/services.py index 67bcbf416d6f..08040f2e51ae 100644 --- a/homeassistant/components/monzo/services.py +++ b/homeassistant/components/monzo/services.py @@ -88,18 +88,7 @@ TRANSFER_SCHEMA = vol.Schema( @callback -def _async_get_device(call: ServiceCall, field: str) -> dr.AnyDeviceEntry: - """Get a selected device.""" - if (device := dr.async_get(call.hass).async_get(call.data[field])) is None: - raise ServiceValidationError( - translation_domain=DOMAIN, - translation_key="invalid_device", - ) - return device - - -@callback -def _async_get_resource_id(device: dr.AnyDeviceEntry) -> str: +def _async_get_resource_id(device: dr.DeviceEntry) -> str: """Get the Monzo resource ID represented by a device.""" for domain, resource_id in device.identifiers: if domain == DOMAIN: @@ -110,7 +99,7 @@ def _async_get_resource_id(device: dr.AnyDeviceEntry) -> str: ) -def _device_name(device: dr.AnyDeviceEntry) -> str: +def _device_name(device: dr.DeviceEntry) -> str: """Return the best available name for a device.""" return device.name_by_user or device.name or device.id @@ -120,65 +109,61 @@ def _async_resolve_transfer( call: ServiceCall, ) -> tuple[MonzoCoordinator, str, str]: """Resolve and validate the account and pot selected for a transfer.""" - account_device = _async_get_device(call, ATTR_ACCOUNT) - pot_device = _async_get_device(call, ATTR_POT) + account_device, account_entry = service.async_get_device_and_config_entry( + call.hass, DOMAIN, call.data[ATTR_ACCOUNT] + ) + pot_device, pot_entry = service.async_get_device_and_config_entry( + call.hass, DOMAIN, call.data[ATTR_POT] + ) + if account_entry.entry_id != pot_entry.entry_id: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="different_entries", + ) account_id = _async_get_resource_id(account_device) pot_id = _async_get_resource_id(pot_device) - for entry_id in account_device.config_entries & pot_device.config_entries: - config_entry = call.hass.config_entries.async_get_entry(entry_id) - if config_entry is None or config_entry.domain != DOMAIN: - continue - - entry = cast( - MonzoConfigEntry, - service.async_get_config_entry(call.hass, DOMAIN, entry_id), + entry = cast(MonzoConfigEntry, account_entry) + coordinator = entry.runtime_data.coordinator + if (account := coordinator.data.accounts.get(account_id)) is None: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key=( + "pot_selected_as_account" + if account_id in coordinator.data.pots + else "invalid_account" + ), + translation_placeholders={"device_name": _device_name(account_device)}, ) - coordinator = entry.runtime_data.coordinator - if (account := coordinator.data.accounts.get(account_id)) is None: - raise ServiceValidationError( - translation_domain=DOMAIN, - translation_key=( - "pot_selected_as_account" - if account_id in coordinator.data.pots - else "invalid_account" - ), - translation_placeholders={"device_name": _device_name(account_device)}, - ) - if account["type"] in NON_TRANSFER_ACCOUNT_TYPES: - raise ServiceValidationError( - translation_domain=DOMAIN, - translation_key="invalid_transfer_account", - translation_placeholders={"device_name": _device_name(account_device)}, - ) - if (pot := coordinator.data.pots.get(pot_id)) is None: - raise ServiceValidationError( - translation_domain=DOMAIN, - translation_key=( - "account_selected_as_pot" - if pot_id in coordinator.data.accounts - else "invalid_pot" - ), - translation_placeholders={"device_name": _device_name(pot_device)}, - ) - if pot["current_account_id"] != account_id: - raise ServiceValidationError( - translation_domain=DOMAIN, - translation_key="pot_account_mismatch", - translation_placeholders={ - "account_name": account["name"], - "pot_name": pot["name"], - "pot_account_name": coordinator.data.accounts[ - pot["current_account_id"] - ]["name"], - }, - ) - return coordinator, account_id, pot_id - - raise ServiceValidationError( - translation_domain=DOMAIN, - translation_key="different_entries", - ) + if account["type"] in NON_TRANSFER_ACCOUNT_TYPES: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="invalid_transfer_account", + translation_placeholders={"device_name": _device_name(account_device)}, + ) + if (pot := coordinator.data.pots.get(pot_id)) is None: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key=( + "account_selected_as_pot" + if pot_id in coordinator.data.accounts + else "invalid_pot" + ), + translation_placeholders={"device_name": _device_name(pot_device)}, + ) + if pot["current_account_id"] != account_id: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="pot_account_mismatch", + translation_placeholders={ + "account_name": account["name"], + "pot_name": pot["name"], + "pot_account_name": coordinator.data.accounts[ + pot["current_account_id"] + ]["name"], + }, + ) + return coordinator, account_id, pot_id async def _async_transfer( diff --git a/tests/components/monzo/test_services.py b/tests/components/monzo/test_services.py index 4fcdcdc23dc5..ca9650ee6d02 100644 --- a/tests/components/monzo/test_services.py +++ b/tests/components/monzo/test_services.py @@ -23,7 +23,8 @@ from homeassistant.components.monzo.services import ( SERVICE_DEPOSIT_INTO_POT, SERVICE_WITHDRAW_FROM_POT, ) -from homeassistant.core import Context, HomeAssistant +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, Context, HomeAssistant from homeassistant.exceptions import ( HomeAssistantError, OAuth2TokenRequestReauthError, @@ -218,7 +219,7 @@ async def test_missing_device( transfer_devices: TransferDevices, ) -> None: """Test a missing selected device is rejected.""" - with pytest.raises(ServiceValidationError): + with pytest.raises(ServiceValidationError) as error: await hass.services.async_call( DOMAIN, SERVICE_DEPOSIT_INTO_POT, @@ -230,22 +231,76 @@ async def test_missing_device( blocking=True, ) + assert error.value.translation_domain == HOMEASSISTANT_DOMAIN + assert error.value.translation_key == "service_device_not_found" -async def test_devices_from_different_entries( + +async def test_device_from_other_integration( hass: HomeAssistant, device_registry: dr.DeviceRegistry, transfer_devices: TransferDevices, ) -> None: - """Test the account and pot must belong to the same config entry.""" + """Test a device not owned by a Monzo config entry is rejected.""" + other_entry = MockConfigEntry(domain="other") + other_entry.add_to_hass(hass) + other_device = device_registry.async_get_or_create( + config_entry_id=other_entry.entry_id, + identifiers={("other", "other-device")}, + name="Other device", + ) + + with pytest.raises(ServiceValidationError) as error: + await hass.services.async_call( + DOMAIN, + SERVICE_DEPOSIT_INTO_POT, + { + ATTR_ACCOUNT: transfer_devices.account_device_id, + ATTR_POT: other_device.id, + ATTR_AMOUNT: 1, + }, + blocking=True, + ) + + assert error.value.translation_domain == HOMEASSISTANT_DOMAIN + assert error.value.translation_key == "service_device_wrong_domain" + + +@pytest.mark.parametrize( + ("other_entry_state", "translation_domain", "translation_key"), + [ + pytest.param( + ConfigEntryState.LOADED, + DOMAIN, + "different_entries", + id="loaded-entry", + ), + pytest.param( + ConfigEntryState.NOT_LOADED, + HOMEASSISTANT_DOMAIN, + "service_config_entry_not_loaded", + id="not-loaded-entry", + ), + ], +) +async def test_devices_from_different_entries( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + transfer_devices: TransferDevices, + other_entry_state: ConfigEntryState, + translation_domain: str, + translation_key: str, +) -> None: + """Test the account and pot must belong to the same loaded config entry.""" other_entry = MockConfigEntry(domain=DOMAIN) other_entry.add_to_hass(hass) + other_entry.mock_state(hass, other_entry_state) other_pot = device_registry.async_get_or_create( config_entry_id=other_entry.entry_id, identifiers={(DOMAIN, "other-pot")}, name="Other pot", ) - with pytest.raises(ServiceValidationError): + with pytest.raises(ServiceValidationError) as error: await hass.services.async_call( DOMAIN, SERVICE_DEPOSIT_INTO_POT, @@ -257,6 +312,9 @@ async def test_devices_from_different_entries( blocking=True, ) + assert error.value.translation_domain == translation_domain + assert error.value.translation_key == translation_key + async def test_pot_must_belong_to_selected_account( hass: HomeAssistant,