Remove the HA admin services and the service permission gate
Since Feature 18 the integration is a client of the NestQuest API service and the admin surface is the PWA plus the API's admin routes, so the integration must not keep HA services for admin operations. Drop every admin service handler and its registration (uncomplete, create/update/set-active quest definition, presence pattern and overrides, CSV export, manage_child, regenerate) together with the Feature 09 permission gate that existed only for them: permissions.py, service_policy.py and the core SERVICE_POLICY registry, the regenerate handler plumbing in __init__, the dead payload schemas and services.yaml entries, and the service-name constants they alone used. Only the panel completion path (nestquest.complete_quest) remains, still proxying to the entry's shared API client. The four automation blueprints are untouched — they only read sensors and listen to bus events.
This commit is contained in:
+33
-60
@@ -9,7 +9,11 @@ re-fires them. The service-driven tests below therefore deliver the
|
||||
frames through :func:`conftest.refire_api_transitions` (the
|
||||
production event path, driven synchronously) before asserting the
|
||||
bus; the shim-level tests drive :mod:`custom_components.nestquest
|
||||
.events` directly, the way its remaining callers do.
|
||||
.events` directly, the way its remaining callers do. The HA
|
||||
``uncomplete_quest`` service is also gone (the API service's admin
|
||||
uncomplete route is the reversal path), so the reversal tests drive
|
||||
the core completion layer the route calls and deliver the published
|
||||
frame through the same SSE subscription helper.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -19,6 +23,7 @@ from conftest import refire_api_transitions, wire_entry_to_registry
|
||||
|
||||
from custom_components.nestquest import async_setup_entry
|
||||
from custom_components.nestquest.children import create_child
|
||||
from custom_components.nestquest.completion import uncomplete_instance
|
||||
from custom_components.nestquest.const import (
|
||||
CONF_ADMIN_USER_IDS,
|
||||
DOMAIN,
|
||||
@@ -27,10 +32,9 @@ from custom_components.nestquest.const import (
|
||||
EVENT_QUEST_MISSED,
|
||||
EVENT_QUEST_UNCOMPLETED,
|
||||
SERVICE_COMPLETE_QUEST,
|
||||
SERVICE_CREATE_QUEST_DEFINITION,
|
||||
SERVICE_MANAGE_CHILD,
|
||||
SERVICE_REGENERATE,
|
||||
SERVICE_UNCOMPLETE_QUEST,
|
||||
)
|
||||
from custom_components.nestquest.core.events import (
|
||||
build_quest_uncompleted_events,
|
||||
)
|
||||
from custom_components.nestquest.dao_instances import QuestInstancesDao
|
||||
from custom_components.nestquest.materialize import materialize
|
||||
@@ -39,7 +43,6 @@ from custom_components.nestquest.quest_definitions import (
|
||||
)
|
||||
from custom_components.nestquest.recurrence import ScheduleRule
|
||||
|
||||
ADMIN_CTX = {"user_id": "admin-1"}
|
||||
_TIMESTAMP = re.compile(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\+00:00$")
|
||||
|
||||
|
||||
@@ -61,15 +64,7 @@ async def _setup_and_seed(hass, make_entry, *, windows=("morning",), seed_date=N
|
||||
)
|
||||
assert await async_setup_entry(hass, entry) is True
|
||||
database = entry.runtime_data.database
|
||||
await hass.services.call(
|
||||
DOMAIN,
|
||||
SERVICE_MANAGE_CHILD,
|
||||
{"action": "create", "display_name": "Ada"},
|
||||
context=ADMIN_CTX,
|
||||
)
|
||||
from custom_components.nestquest.children import list_children
|
||||
|
||||
child = (await list_children(database))[0]
|
||||
child = await create_child(database, "Ada")
|
||||
await create_quest_definition(
|
||||
database,
|
||||
"Brush teeth",
|
||||
@@ -205,32 +200,44 @@ async def test_child_day_complete_does_not_fire_partway(
|
||||
|
||||
|
||||
async def test_uncompleted_fires_on_reversal_only(hass, make_entry) -> None:
|
||||
"""An actual reversal fires the uncompleted event — but only once,
|
||||
and only through the API service's transition stream: the HA
|
||||
``uncomplete_quest`` service is gone, so the reversal is driven
|
||||
through the core completion layer the API's admin uncomplete route
|
||||
calls, with the published frame delivered by the SSE subscription."""
|
||||
entry, child, instances = await _setup_and_seed(hass, make_entry)
|
||||
instance_id = instances[0].id
|
||||
database = entry.runtime_data.database
|
||||
client = entry.runtime_data.coordinator.api_client
|
||||
await hass.services.call(
|
||||
DOMAIN,
|
||||
SERVICE_COMPLETE_QUEST,
|
||||
{"instance_id": instance_id, "actor": "panel", "actor_child_id": child.id},
|
||||
)
|
||||
# Un-completing an open instance is a no-op and fires nothing.
|
||||
await hass.services.call(
|
||||
DOMAIN,
|
||||
SERVICE_UNCOMPLETE_QUEST,
|
||||
{"instance_id": instance_id, "actor": "user"},
|
||||
context=ADMIN_CTX,
|
||||
# The reversal appends through the core completion layer; the API
|
||||
# route publishes the transition on its SSE stream and the
|
||||
# subscription re-fires it on the bus.
|
||||
result = await uncomplete_instance(
|
||||
database, instance_id, actor_source="user", actor_user_id="admin-1"
|
||||
)
|
||||
assert result.appended is True
|
||||
client.published_frames.extend(
|
||||
await build_quest_uncompleted_events(database, instance_id)
|
||||
)
|
||||
await refire_api_transitions(hass, entry, client)
|
||||
fired = hass.bus.fired(EVENT_QUEST_UNCOMPLETED)
|
||||
assert len(fired) == 1
|
||||
payload = fired[0]
|
||||
assert payload["instance_id"] == instance_id
|
||||
assert payload["quest_title"] == "Brush teeth"
|
||||
assert _TIMESTAMP.match(payload["occurred_at"])
|
||||
await hass.services.call(
|
||||
DOMAIN,
|
||||
SERVICE_UNCOMPLETE_QUEST,
|
||||
{"instance_id": instance_id, "actor": "user"},
|
||||
context=ADMIN_CTX,
|
||||
# Un-completing an already-open instance is a no-op: nothing is
|
||||
# appended, nothing is published, and nothing further fires.
|
||||
no_op = await uncomplete_instance(
|
||||
database, instance_id, actor_source="user", actor_user_id="admin-1"
|
||||
)
|
||||
assert no_op.appended is False
|
||||
await refire_api_transitions(hass, entry, client)
|
||||
assert len(hass.bus.fired(EVENT_QUEST_UNCOMPLETED)) == 1
|
||||
|
||||
|
||||
@@ -390,40 +397,6 @@ async def test_completion_updates_entities_immediately(
|
||||
)
|
||||
|
||||
|
||||
async def test_uncompletion_updates_entities_immediately(
|
||||
hass, make_entry
|
||||
) -> None:
|
||||
entry, child, instances = await _setup_and_seed(hass, make_entry)
|
||||
coordinator = entry.runtime_data.coordinator
|
||||
await coordinator.async_refresh()
|
||||
await hass.services.call(
|
||||
DOMAIN,
|
||||
SERVICE_COMPLETE_QUEST,
|
||||
{
|
||||
"instance_id": instances[0].id,
|
||||
"actor": "panel",
|
||||
"actor_child_id": child.id,
|
||||
},
|
||||
)
|
||||
completed_after_complete = hass.entities[
|
||||
f"nestquest_child_{child.id}_quests_completed_today"
|
||||
].native_value
|
||||
assert completed_after_complete == 1
|
||||
|
||||
await hass.services.call(
|
||||
DOMAIN,
|
||||
SERVICE_UNCOMPLETE_QUEST,
|
||||
{"instance_id": instances[0].id, "actor": "user"},
|
||||
context=ADMIN_CTX,
|
||||
)
|
||||
completed_after_uncomplete = hass.entities[
|
||||
f"nestquest_child_{child.id}_quests_completed_today"
|
||||
].native_value
|
||||
assert completed_after_uncomplete == 0, (
|
||||
"sensor must reflect the reversal without a manual refresh"
|
||||
)
|
||||
|
||||
|
||||
async def test_forced_refreshes_serialize_no_stale_publish(
|
||||
hass, make_entry
|
||||
) -> None:
|
||||
|
||||
Reference in New Issue
Block a user