mirror of
https://github.com/home-assistant/core.git
synced 2026-08-24 10:13:52 -05:00
Update cookidoo API requirement to version 0.17.2 (#171793)
This commit is contained in:
@@ -42,6 +42,35 @@ async def async_unload_entry(hass: HomeAssistant, entry: CookidooConfigEntry) ->
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
def _migrate_identifiers(
|
||||
hass: HomeAssistant,
|
||||
config_entry: CookidooConfigEntry,
|
||||
old_prefix: str,
|
||||
new_unique_id: str,
|
||||
) -> None:
|
||||
"""Migrate device identifiers and entity unique_ids from old to new prefix."""
|
||||
device_registry = dr.async_get(hass)
|
||||
entity_registry = er.async_get(hass)
|
||||
device_entries = dr.async_entries_for_config_entry(
|
||||
device_registry, config_entry_id=config_entry.entry_id
|
||||
)
|
||||
entity_entries = er.async_entries_for_config_entry(
|
||||
entity_registry, config_entry_id=config_entry.entry_id
|
||||
)
|
||||
for dev in device_entries:
|
||||
new_identifiers = {
|
||||
(DOMAIN, new_unique_id) if domain == DOMAIN else (domain, identifier)
|
||||
for domain, identifier in dev.identifiers
|
||||
}
|
||||
device_registry.async_update_device(dev.id, new_identifiers=new_identifiers)
|
||||
for ent in entity_entries:
|
||||
if ent.unique_id and ent.unique_id.startswith(f"{old_prefix}_"):
|
||||
entity_registry.async_update_entity(
|
||||
ent.entity_id,
|
||||
new_unique_id=f"{new_unique_id}{ent.unique_id[len(old_prefix) :]}",
|
||||
)
|
||||
|
||||
|
||||
async def async_migrate_entry(
|
||||
hass: HomeAssistant, config_entry: CookidooConfigEntry
|
||||
) -> bool:
|
||||
@@ -49,41 +78,37 @@ async def async_migrate_entry(
|
||||
_LOGGER.debug("Migrating from version %s", config_entry.version)
|
||||
|
||||
if config_entry.version == 1 and config_entry.minor_version == 1:
|
||||
# Add the unique uuid
|
||||
# Add the unique uuid (first migration, entities used config_entry_id as prefix)
|
||||
cookidoo = await cookidoo_from_config_entry(hass, config_entry)
|
||||
|
||||
try:
|
||||
auth_data = await cookidoo.login()
|
||||
await cookidoo.login()
|
||||
user_info = await cookidoo.get_user_info()
|
||||
except (CookidooRequestException, CookidooAuthException) as e:
|
||||
_LOGGER.error(
|
||||
"Could not migrate config config_entry: %s",
|
||||
str(e),
|
||||
)
|
||||
_LOGGER.error("Could not migrate config entry: %s", e)
|
||||
return False
|
||||
|
||||
unique_id = auth_data.sub
|
||||
|
||||
device_registry = dr.async_get(hass)
|
||||
entity_registry = er.async_get(hass)
|
||||
device_entries = dr.async_entries_for_config_entry(
|
||||
device_registry, config_entry_id=config_entry.entry_id
|
||||
)
|
||||
entity_entries = er.async_entries_for_config_entry(
|
||||
entity_registry, config_entry_id=config_entry.entry_id
|
||||
)
|
||||
for dev in device_entries:
|
||||
device_registry.async_update_device(
|
||||
dev.id, new_identifiers={(DOMAIN, unique_id)}
|
||||
)
|
||||
for ent in entity_entries:
|
||||
assert ent.config_entry_id
|
||||
entity_registry.async_update_entity(
|
||||
ent.entity_id,
|
||||
new_unique_id=ent.unique_id.replace(ent.config_entry_id, unique_id),
|
||||
)
|
||||
|
||||
_migrate_identifiers(hass, config_entry, config_entry.entry_id, user_info.id)
|
||||
hass.config_entries.async_update_entry(
|
||||
config_entry, unique_id=auth_data.sub, minor_version=2
|
||||
config_entry, unique_id=user_info.id, minor_version=3
|
||||
)
|
||||
|
||||
if config_entry.version == 1 and config_entry.minor_version == 2:
|
||||
# Migrate unique_id from old CIAM sub to community profile id
|
||||
cookidoo = await cookidoo_from_config_entry(hass, config_entry)
|
||||
|
||||
try:
|
||||
await cookidoo.login()
|
||||
user_info = await cookidoo.get_user_info()
|
||||
except (CookidooRequestException, CookidooAuthException) as e:
|
||||
_LOGGER.error("Could not migrate config entry: %s", e)
|
||||
return False
|
||||
|
||||
old_unique_id = config_entry.unique_id
|
||||
if old_unique_id:
|
||||
_migrate_identifiers(hass, config_entry, old_unique_id, user_info.id)
|
||||
hass.config_entries.async_update_entry(
|
||||
config_entry, unique_id=user_info.id, minor_version=3
|
||||
)
|
||||
|
||||
_LOGGER.debug(
|
||||
|
||||
@@ -3,7 +3,11 @@
|
||||
from datetime import date, datetime, timedelta
|
||||
import logging
|
||||
|
||||
from cookidoo_api import CookidooAuthException, CookidooException
|
||||
from cookidoo_api import (
|
||||
CookidooAuthException,
|
||||
CookidooException,
|
||||
CookidooRequestException,
|
||||
)
|
||||
from cookidoo_api.types import CookidooCalendarDayRecipe
|
||||
|
||||
from homeassistant.components.calendar import CalendarEntity, CalendarEvent
|
||||
@@ -74,7 +78,13 @@ class CookidooCalendarEntity(CookidooBaseEntity, CalendarEntity):
|
||||
week_day
|
||||
)
|
||||
except CookidooAuthException:
|
||||
await self.coordinator.cookidoo.refresh_token()
|
||||
try:
|
||||
await self.coordinator.cookidoo.login()
|
||||
except (CookidooAuthException, CookidooRequestException) as exc:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="calendar_fetch_failed",
|
||||
) from exc
|
||||
return await self.coordinator.cookidoo.get_recipes_in_calendar_week(
|
||||
week_day
|
||||
)
|
||||
|
||||
@@ -54,7 +54,7 @@ class CookidooConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Cookidoo."""
|
||||
|
||||
VERSION = 1
|
||||
MINOR_VERSION = 2
|
||||
MINOR_VERSION = 3
|
||||
|
||||
COUNTRY_DATA_SCHEMA: dict
|
||||
LANGUAGE_DATA_SCHEMA: dict
|
||||
@@ -223,8 +223,9 @@ class CookidooConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
cookidoo = await cookidoo_from_config_data(self.hass, data_input)
|
||||
try:
|
||||
auth_data = await cookidoo.login()
|
||||
self.user_uuid = auth_data.sub
|
||||
await cookidoo.login()
|
||||
user_info = await cookidoo.get_user_info()
|
||||
self.user_uuid = user_info.id
|
||||
if language_input:
|
||||
await cookidoo.get_additional_items()
|
||||
except CookidooRequestException:
|
||||
|
||||
@@ -87,7 +87,7 @@ class CookidooDataUpdateCoordinator(DataUpdateCoordinator[CookidooData]):
|
||||
)
|
||||
except CookidooAuthException:
|
||||
try:
|
||||
await self.cookidoo.refresh_token()
|
||||
await self.cookidoo.login()
|
||||
except CookidooAuthException as exc:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
@@ -96,6 +96,11 @@ class CookidooDataUpdateCoordinator(DataUpdateCoordinator[CookidooData]):
|
||||
CONF_EMAIL: self.config_entry.data[CONF_EMAIL]
|
||||
},
|
||||
) from exc
|
||||
except CookidooRequestException as exc:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="setup_request_exception",
|
||||
) from exc
|
||||
_LOGGER.debug(
|
||||
"Authentication failed but re-authentication"
|
||||
" was successful, trying again later"
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
|
||||
from typing import Any
|
||||
|
||||
from aiohttp import CookieJar
|
||||
from cookidoo_api import Cookidoo, CookidooConfig, get_localization_options
|
||||
|
||||
from homeassistant.const import CONF_COUNTRY, CONF_EMAIL, CONF_LANGUAGE, CONF_PASSWORD
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
||||
|
||||
from .coordinator import CookidooConfigEntry
|
||||
|
||||
@@ -21,7 +22,7 @@ async def cookidoo_from_config_data(
|
||||
)
|
||||
|
||||
return Cookidoo(
|
||||
async_get_clientsession(hass),
|
||||
async_create_clientsession(hass, cookie_jar=CookieJar(unsafe=True)),
|
||||
CookidooConfig(
|
||||
email=data[CONF_EMAIL],
|
||||
password=data[CONF_PASSWORD],
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["cookidoo_api"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["cookidoo-api==0.14.0"]
|
||||
"requirements": ["cookidoo-api==0.17.2"]
|
||||
}
|
||||
|
||||
Generated
+1
-1
@@ -769,7 +769,7 @@ connect-box==0.3.1
|
||||
construct==2.10.68
|
||||
|
||||
# homeassistant.components.cookidoo
|
||||
cookidoo-api==0.14.0
|
||||
cookidoo-api==0.17.2
|
||||
|
||||
# homeassistant.components.backup
|
||||
# homeassistant.components.utility_meter
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
"""Common fixtures for the Cookidoo tests."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from typing import cast
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from cookidoo_api import (
|
||||
CookidooAdditionalItem,
|
||||
CookidooAuthResponse,
|
||||
CookidooIngredientItem,
|
||||
CookidooSubscription,
|
||||
CookidooUserInfo,
|
||||
@@ -44,7 +42,7 @@ def mock_cookidoo_client() -> Generator[AsyncMock]:
|
||||
autospec=True,
|
||||
) as mock_client:
|
||||
client = mock_client.return_value
|
||||
client.login.return_value = cast(CookidooAuthResponse, {"name": "Cookidoo"})
|
||||
client.login.return_value = None
|
||||
client.get_ingredient_items.return_value = [
|
||||
CookidooIngredientItem(**item)
|
||||
for item in load_json_object_fixture("ingredient_items.json", DOMAIN)[
|
||||
@@ -63,9 +61,6 @@ def mock_cookidoo_client() -> Generator[AsyncMock]:
|
||||
client.get_user_info.return_value = CookidooUserInfo(
|
||||
**load_json_object_fixture("user_info.json", DOMAIN)["data"]
|
||||
)
|
||||
client.login.return_value = CookidooAuthResponse(
|
||||
**load_json_object_fixture("login.json", DOMAIN)
|
||||
)
|
||||
client.get_recipes_in_calendar_week.return_value = [
|
||||
CookidooCalendarDay(
|
||||
id=day["id"],
|
||||
@@ -75,6 +70,9 @@ def mock_cookidoo_client() -> Generator[AsyncMock]:
|
||||
id=recipe["id"],
|
||||
name=recipe["name"],
|
||||
total_time=recipe["total_time"],
|
||||
thumbnail=recipe["thumbnail"],
|
||||
image=recipe["image"],
|
||||
url=recipe["url"],
|
||||
)
|
||||
for recipe in day["recipes"]
|
||||
],
|
||||
@@ -90,7 +88,7 @@ def mock_cookidoo_config_entry() -> MockConfigEntry:
|
||||
return MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
version=1,
|
||||
minor_version=2,
|
||||
minor_version=3,
|
||||
data={
|
||||
CONF_EMAIL: EMAIL,
|
||||
CONF_PASSWORD: PASSWORD,
|
||||
|
||||
@@ -7,7 +7,10 @@
|
||||
{
|
||||
"id": "r1",
|
||||
"name": "Waffles",
|
||||
"total_time": 1500
|
||||
"total_time": 1500,
|
||||
"thumbnail": "https://assets.tmecosys.com/image/upload/t_web_thumbnail/r1.jpg",
|
||||
"image": "https://assets.tmecosys.com/image/upload/t_web_large/r1.jpg",
|
||||
"url": "https://cookidoo.ch/recipes/recipe/de-CH/r1"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -18,7 +21,10 @@
|
||||
{
|
||||
"id": "r2",
|
||||
"name": "Mint Tea",
|
||||
"total_time": 1500
|
||||
"total_time": 1500,
|
||||
"thumbnail": "https://assets.tmecosys.com/image/upload/t_web_thumbnail/r2.jpg",
|
||||
"image": "https://assets.tmecosys.com/image/upload/t_web_large/r2.jpg",
|
||||
"url": "https://cookidoo.ch/recipes/recipe/de-CH/r2"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"access_token": "eyJhbGci<redacted>",
|
||||
"expires_in": 43199,
|
||||
"refresh_token": "eyJhbGciOiJSUzI1NiI<redacted>",
|
||||
"token_type": "bearer",
|
||||
"sub": "sub_uuid"
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"data": {
|
||||
"id": "sub_uuid",
|
||||
"username": "username_1234",
|
||||
"description": null,
|
||||
"picture": null
|
||||
|
||||
@@ -29,23 +29,33 @@
|
||||
}),
|
||||
'week_plan': list([
|
||||
dict({
|
||||
'customer_recipe_ids': list([
|
||||
]),
|
||||
'id': '2025-03-04',
|
||||
'recipes': list([
|
||||
dict({
|
||||
'id': 'r1',
|
||||
'image': 'https://assets.tmecosys.com/image/upload/t_web_large/r1.jpg',
|
||||
'name': 'Waffles',
|
||||
'thumbnail': 'https://assets.tmecosys.com/image/upload/t_web_thumbnail/r1.jpg',
|
||||
'total_time': 1500,
|
||||
'url': 'https://cookidoo.ch/recipes/recipe/de-CH/r1',
|
||||
}),
|
||||
]),
|
||||
'title': '2025-03-04',
|
||||
}),
|
||||
dict({
|
||||
'customer_recipe_ids': list([
|
||||
]),
|
||||
'id': '2025-03-05',
|
||||
'recipes': list([
|
||||
dict({
|
||||
'id': 'r2',
|
||||
'image': 'https://assets.tmecosys.com/image/upload/t_web_large/r2.jpg',
|
||||
'name': 'Mint Tea',
|
||||
'thumbnail': 'https://assets.tmecosys.com/image/upload/t_web_thumbnail/r2.jpg',
|
||||
'total_time': 1500,
|
||||
'url': 'https://cookidoo.ch/recipes/recipe/de-CH/r2',
|
||||
}),
|
||||
]),
|
||||
'title': '2025-03-05',
|
||||
@@ -60,6 +70,7 @@
|
||||
}),
|
||||
'user': dict({
|
||||
'description': None,
|
||||
'id': 'sub_uuid',
|
||||
'picture': None,
|
||||
'username': 'username_1234',
|
||||
}),
|
||||
|
||||
@@ -4,12 +4,14 @@ from collections.abc import Generator
|
||||
from datetime import UTC, datetime
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from cookidoo_api import CookidooAuthException, CookidooRequestException
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import setup_integration
|
||||
@@ -81,3 +83,51 @@ async def test_get_events(
|
||||
)
|
||||
|
||||
assert resp == snapshot
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"login_exception",
|
||||
[
|
||||
CookidooAuthException(),
|
||||
CookidooRequestException(),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_get_events_login_failure(
|
||||
hass: HomeAssistant,
|
||||
cookidoo_config_entry: MockConfigEntry,
|
||||
mock_cookidoo_client: AsyncMock,
|
||||
entity_registry: er.EntityRegistry,
|
||||
login_exception: Exception,
|
||||
) -> None:
|
||||
"""Test calendar handles login failures gracefully during event fetch."""
|
||||
|
||||
with patch("homeassistant.components.cookidoo.PLATFORMS", [Platform.CALENDAR]):
|
||||
await setup_integration(hass, cookidoo_config_entry)
|
||||
|
||||
assert cookidoo_config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
entities = er.async_entries_for_config_entry(
|
||||
entity_registry, cookidoo_config_entry.entry_id
|
||||
)
|
||||
assert len(entities) == 1
|
||||
entity_id = entities[0].entity_id
|
||||
|
||||
# First call to get_recipes_in_calendar_week raises auth, login also fails
|
||||
mock_cookidoo_client.get_recipes_in_calendar_week.side_effect = (
|
||||
CookidooAuthException()
|
||||
)
|
||||
mock_cookidoo_client.login.side_effect = login_exception
|
||||
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
"calendar",
|
||||
"get_events",
|
||||
{
|
||||
"start_date_time": datetime(2025, 3, 4, tzinfo=UTC),
|
||||
"end_date_time": datetime(2025, 3, 6, tzinfo=UTC),
|
||||
},
|
||||
target={"entity_id": entity_id},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
|
||||
@@ -85,24 +85,36 @@ async def test_config_entry_not_ready(
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("exception", "status"),
|
||||
("login_exception", "status"),
|
||||
[
|
||||
(None, ConfigEntryState.LOADED),
|
||||
(CookidooRequestException, ConfigEntryState.SETUP_RETRY),
|
||||
(CookidooAuthException, ConfigEntryState.SETUP_ERROR),
|
||||
(CookidooRequestException(), ConfigEntryState.SETUP_RETRY),
|
||||
(CookidooAuthException(), ConfigEntryState.SETUP_ERROR),
|
||||
],
|
||||
)
|
||||
async def test_config_entry_not_ready_auth_error(
|
||||
hass: HomeAssistant,
|
||||
cookidoo_config_entry: MockConfigEntry,
|
||||
mock_cookidoo_client: AsyncMock,
|
||||
exception: Exception | None,
|
||||
login_exception: Exception | None,
|
||||
status: ConfigEntryState,
|
||||
) -> None:
|
||||
"""Test config entry not ready from authentication error."""
|
||||
"""Test config entry recovery when data fetch hits an auth error.
|
||||
|
||||
mock_cookidoo_client.get_ingredient_items.side_effect = CookidooAuthException
|
||||
mock_cookidoo_client.refresh_token.side_effect = exception
|
||||
Simulates: initial login succeeds (_async_setup), first data fetch
|
||||
raises CookidooAuthException (expired session), then re-login either
|
||||
succeeds or fails. On success, the next data fetch returns valid data.
|
||||
"""
|
||||
# get_ingredient_items raises auth error once, then returns valid data
|
||||
default_return = mock_cookidoo_client.get_ingredient_items.return_value
|
||||
mock_cookidoo_client.get_ingredient_items.side_effect = [
|
||||
CookidooAuthException(),
|
||||
default_return,
|
||||
]
|
||||
# First login() is _async_setup (succeeds), second is re-login attempt
|
||||
mock_cookidoo_client.login.side_effect = (
|
||||
[None, login_exception] if login_exception else None
|
||||
)
|
||||
|
||||
cookidoo_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(cookidoo_config_entry.entry_id)
|
||||
@@ -135,7 +147,8 @@ OLD_ENTRY_ID = "OLD_OLD_ENTRY_ID"
|
||||
MOCK_CONFIG_ENTRY_MIGRATION,
|
||||
None,
|
||||
),
|
||||
(1, 2, MOCK_CONFIG_ENTRY_MIGRATION, TEST_UUID),
|
||||
(1, 2, MOCK_CONFIG_ENTRY_MIGRATION, "old_ciam_sub_uuid"),
|
||||
(1, 3, MOCK_CONFIG_ENTRY_MIGRATION, TEST_UUID),
|
||||
],
|
||||
)
|
||||
async def test_migration_from(
|
||||
@@ -160,31 +173,32 @@ async def test_migration_from(
|
||||
entry_id=OLD_ENTRY_ID,
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
entity_prefix = unique_id or OLD_ENTRY_ID
|
||||
|
||||
device = device_registry.async_get_or_create(
|
||||
config_entry_id=config_entry.entry_id,
|
||||
identifiers={(DOMAIN, OLD_ENTRY_ID)},
|
||||
identifiers={(DOMAIN, entity_prefix)},
|
||||
entry_type=dr.DeviceEntryType.SERVICE,
|
||||
)
|
||||
entity_registry.async_get_or_create(
|
||||
config_entry=config_entry,
|
||||
platform=DOMAIN,
|
||||
domain="todo",
|
||||
unique_id=f"{OLD_ENTRY_ID}_ingredients",
|
||||
unique_id=f"{entity_prefix}_ingredients",
|
||||
device_id=device.id,
|
||||
)
|
||||
entity_registry.async_get_or_create(
|
||||
config_entry=config_entry,
|
||||
platform=DOMAIN,
|
||||
domain="todo",
|
||||
unique_id=f"{OLD_ENTRY_ID}_additional_items",
|
||||
unique_id=f"{entity_prefix}_additional_items",
|
||||
device_id=device.id,
|
||||
)
|
||||
entity_registry.async_get_or_create(
|
||||
config_entry=config_entry,
|
||||
platform=DOMAIN,
|
||||
domain="button",
|
||||
unique_id=f"{OLD_ENTRY_ID}_todo_clear",
|
||||
unique_id=f"{entity_prefix}_todo_clear",
|
||||
device_id=device.id,
|
||||
)
|
||||
|
||||
@@ -194,7 +208,7 @@ async def test_migration_from(
|
||||
|
||||
# Check change in config entry and verify most recent version
|
||||
assert config_entry.version == 1
|
||||
assert config_entry.minor_version == 2
|
||||
assert config_entry.minor_version == 3
|
||||
assert config_entry.unique_id == TEST_UUID
|
||||
|
||||
assert entity_registry.async_is_registered(
|
||||
@@ -249,6 +263,20 @@ async def test_migration_from(
|
||||
None,
|
||||
CookidooAuthException,
|
||||
),
|
||||
(
|
||||
1,
|
||||
2,
|
||||
MOCK_CONFIG_ENTRY_MIGRATION,
|
||||
"old_ciam_sub_uuid",
|
||||
CookidooRequestException,
|
||||
),
|
||||
(
|
||||
1,
|
||||
2,
|
||||
MOCK_CONFIG_ENTRY_MIGRATION,
|
||||
"old_ciam_sub_uuid",
|
||||
CookidooAuthException,
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_migration_from_with_error(
|
||||
|
||||
Reference in New Issue
Block a user