mirror of
https://github.com/home-assistant/core.git
synced 2026-07-13 17:44:45 +01:00
Add repair to unsubscribe protected topic in ntfy integration (#152009)
This commit is contained in:
@@ -17,9 +17,17 @@ from aiontfy.exceptions import (
|
||||
from homeassistant.components.event import EventEntity, EventEntityDescription
|
||||
from homeassistant.config_entries import ConfigSubentry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import CONF_MESSAGE, CONF_PRIORITY, CONF_TAGS, CONF_TITLE
|
||||
from .const import (
|
||||
CONF_MESSAGE,
|
||||
CONF_PRIORITY,
|
||||
CONF_TAGS,
|
||||
CONF_TITLE,
|
||||
CONF_TOPIC,
|
||||
DOMAIN,
|
||||
)
|
||||
from .coordinator import NtfyConfigEntry
|
||||
from .entity import NtfyBaseEntity
|
||||
|
||||
@@ -100,6 +108,16 @@ class NtfyEventEntity(NtfyBaseEntity, EventEntity):
|
||||
if self._attr_available:
|
||||
_LOGGER.error("Failed to subscribe to topic. Topic is protected")
|
||||
self._attr_available = False
|
||||
ir.async_create_issue(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
f"topic_protected_{self.topic}",
|
||||
is_fixable=True,
|
||||
severity=ir.IssueSeverity.ERROR,
|
||||
translation_key="topic_protected",
|
||||
translation_placeholders={CONF_TOPIC: self.topic},
|
||||
data={"entity_id": self.entity_id, "topic": self.topic},
|
||||
)
|
||||
return
|
||||
except NtfyHTTPError as e:
|
||||
if self._attr_available:
|
||||
|
||||
@@ -63,9 +63,7 @@ rules:
|
||||
exception-translations: done
|
||||
icon-translations: done
|
||||
reconfiguration-flow: done
|
||||
repair-issues:
|
||||
status: exempt
|
||||
comment: the integration has no repairs
|
||||
repair-issues: done
|
||||
stale-devices:
|
||||
status: exempt
|
||||
comment: only one device per entry, is deleted with the entry.
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
"""Repairs for ntfy integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.components.repairs import ConfirmRepairFlow, RepairsFlow
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from .const import CONF_TOPIC
|
||||
|
||||
|
||||
class TopicProtectedRepairFlow(RepairsFlow):
|
||||
"""Handler for protected topic issue fixing flow."""
|
||||
|
||||
def __init__(self, data: dict[str, str]) -> None:
|
||||
"""Initialize."""
|
||||
self.entity_id = data["entity_id"]
|
||||
self.topic = data["topic"]
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, str] | None = None
|
||||
) -> data_entry_flow.FlowResult:
|
||||
"""Init repair flow."""
|
||||
|
||||
return await self.async_step_confirm()
|
||||
|
||||
async def async_step_confirm(
|
||||
self, user_input: dict[str, str] | None = None
|
||||
) -> data_entry_flow.FlowResult:
|
||||
"""Confirm repair flow."""
|
||||
if user_input is not None:
|
||||
er.async_get(self.hass).async_update_entity(
|
||||
self.entity_id,
|
||||
disabled_by=er.RegistryEntryDisabler.USER,
|
||||
)
|
||||
return self.async_create_entry(data={})
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="confirm",
|
||||
data_schema=vol.Schema({}),
|
||||
description_placeholders={CONF_TOPIC: self.topic},
|
||||
)
|
||||
|
||||
|
||||
async def async_create_fix_flow(
|
||||
hass: HomeAssistant,
|
||||
issue_id: str,
|
||||
data: dict[str, str],
|
||||
) -> RepairsFlow:
|
||||
"""Create flow."""
|
||||
if issue_id.startswith("topic_protected"):
|
||||
return TopicProtectedRepairFlow(data)
|
||||
return ConfirmRepairFlow()
|
||||
@@ -354,5 +354,18 @@
|
||||
"5": "Maximum"
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"topic_protected": {
|
||||
"title": "Subscription failed: Topic {topic} is protected",
|
||||
"fix_flow": {
|
||||
"step": {
|
||||
"confirm": {
|
||||
"title": "Topic {topic} is protected",
|
||||
"description": "The topic **{topic}** is protected and requires authentication to subscribe.\n\nTo resolve this issue, you have two options:\n\n1. **Reconfigure the ntfy integration**\nAdd a username and password that has permission to access this topic.\n\n2. **Deactivate the event entity**\nThis will stop Home Assistant from subscribing to the topic.\nClick **Submit** to deactivate the entity."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,12 +17,20 @@ from freezegun.api import FrozenDateTimeFactory, freeze_time
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.ntfy.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers import entity_registry as er, issue_registry as ir
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
|
||||
from tests.components.repairs import (
|
||||
async_process_repairs_platforms,
|
||||
process_repair_fix_flow,
|
||||
start_repair_fix_flow,
|
||||
)
|
||||
from tests.typing import ClientSessionGenerator
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
@@ -156,3 +164,48 @@ async def test_event_exceptions(
|
||||
|
||||
assert (state := hass.states.get("event.mytopic"))
|
||||
assert state.state == expected_state
|
||||
|
||||
|
||||
async def test_event_topic_protected(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_aiontfy: AsyncMock,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
hass_client: ClientSessionGenerator,
|
||||
) -> None:
|
||||
"""Test ntfy events cannot subscribe to protected topic."""
|
||||
mock_aiontfy.subscribe.side_effect = NtfyForbiddenError(403, 403, "forbidden")
|
||||
|
||||
config_entry.add_to_hass(hass)
|
||||
assert await async_setup_component(hass, "repairs", {})
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
freezer.tick(timedelta(seconds=10))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert (state := hass.states.get("event.mytopic"))
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
assert issue_registry.async_get_issue(
|
||||
domain=DOMAIN, issue_id="topic_protected_mytopic"
|
||||
)
|
||||
|
||||
await async_process_repairs_platforms(hass)
|
||||
client = await hass_client()
|
||||
result = await start_repair_fix_flow(client, DOMAIN, "topic_protected_mytopic")
|
||||
|
||||
flow_id = result["flow_id"]
|
||||
assert result["step_id"] == "confirm"
|
||||
|
||||
result = await process_repair_fix_flow(client, flow_id)
|
||||
assert result["type"] == "create_entry"
|
||||
|
||||
assert (entity := entity_registry.async_get("event.mytopic"))
|
||||
assert entity.disabled
|
||||
assert entity.disabled_by is er.RegistryEntryDisabler.USER
|
||||
|
||||
Reference in New Issue
Block a user