Remove the integration's database, migrations, DAOs and business modules (#172)

This commit was merged in pull request #172.
This commit is contained in:
2026-09-23 09:11:29 -04:00
parent 2c4113ed61
commit a3f2adcf43
64 changed files with 1394 additions and 5635 deletions
+47 -116
View File
@@ -5,117 +5,69 @@ complete route through the entry's ONE shared API client (the same
instance the coordinator polls and the SSE subscription reads), with
exactly the payload contract the panel cards send (``instance_id``,
``actor=panel``, ``actor_child_id``). The service performs NO local
database completion and fires NO local bus event — the API service
emits the transition on its SSE stream and the integration's
subscription re-fires it (firing it locally too would double-fire) —
and it refreshes the coordinator after a successful call so the
sensors reflect the completion immediately. Every failure (a typed
:class:`NestQuestApiError`, an unconfigured client, a non-panel
actor) surfaces as a :class:`HomeAssistantError`, token-free.
completion and fires NO local bus event — the API service emits the
transition on its SSE stream and the integration's subscription
re-fires it (firing it locally too would double-fire) — and it
refreshes the coordinator after a successful call so the sensors
reflect the completion immediately. Every failure (a typed
:class:`NestQuestApiError`, an unconfigured client, a non-panel actor)
surfaces as a :class:`HomeAssistantError`, token-free.
The scripted clients here stand in for the API service: recording and
failing ``complete_instance`` implementations, no network, no aiohttp.
"""
from __future__ import annotations
import datetime
import pytest
from homeassistant.exceptions import HomeAssistantError
from conftest import (
LazyLocalSnapshotClient,
set_coordinator_client,
wire_entry_to_registry,
)
from conftest import ScriptedPanelClient, set_coordinator_client, wire_entry_to_registry
from custom_components.nestquest import async_setup_entry
from custom_components.nestquest.api_client import NestQuestApiError
from custom_components.nestquest.const import (
CONF_ADMIN_USER_IDS,
CONF_PANEL_TOKEN,
DOMAIN,
EVENT_CHILD_DAY_COMPLETE,
EVENT_QUEST_COMPLETED,
SERVICE_COMPLETE_QUEST,
)
from custom_components.nestquest.dao_instances import QuestInstancesDao
from custom_components.nestquest.materialize import materialize
from custom_components.nestquest.quest_definitions import (
create_quest_definition,
)
from custom_components.nestquest.recurrence import ScheduleRule
ADMIN_CTX = {"user_id": "admin-1"}
class _RecordingApiClient(LazyLocalSnapshotClient):
class _RecordingApiClient(ScriptedPanelClient):
"""Snapshot-only stand-in that RECORDS completion calls.
``complete_instance`` deliberately performs NO database write: the
``complete_instance`` deliberately performs NO local write: the
API service is remote, and the assertions below must be able to
prove the service handler itself completed nothing locally.
"""
def __init__(self, hass, entry_id: str) -> None:
super().__init__(hass, entry_id)
self.complete_calls: list[tuple[int, int]] = []
async def complete_instance(
self, instance_id: int, actor_child_id: int
) -> dict:
self.complete_calls.append((instance_id, actor_child_id))
return {"status": "done"}
class _FailingApiClient(_RecordingApiClient):
"""A stand-in whose completion fails the way the client types it."""
def __init__(self, hass, entry_id: str, error: NestQuestApiError) -> None:
super().__init__(hass, entry_id)
self._error = error
async def complete_instance(
self, instance_id: int, actor_child_id: int
) -> dict:
self.complete_calls.append((instance_id, actor_child_id))
raise self._error
def __init__(self, error: NestQuestApiError, *outcomes) -> None:
super().__init__(*outcomes)
self.fail_complete_with = error
async def _setup_seeded_entry(hass, make_entry, client):
"""Wire, script, and set up an entry with one child's daily quest."""
def _empty_snapshot() -> dict:
return {
"today_iso": "2026-09-23",
"cycle_day": 0,
"children": [],
}
async def _setup_entry(hass, make_entry, client):
"""Wire, script, and set up an entry against the scripted client."""
entry = wire_entry_to_registry(
make_entry(
data={
CONF_ADMIN_USER_IDS: [ADMIN_CTX["user_id"]],
CONF_PANEL_TOKEN: "secret-panel-token",
}
),
make_entry(data={CONF_PANEL_TOKEN: "secret-panel-token"}),
hass.registry,
)
set_coordinator_client(entry, client)
assert await async_setup_entry(hass, entry) is True
database = entry.runtime_data.database
from custom_components.nestquest.children import create_child
child = await create_child(database, "Ada")
today = datetime.date.today()
await create_quest_definition(
database,
"Brush teeth",
ScheduleRule.from_dict(
{"rule_type": "daily", "start_date": today.isoformat()}
),
[child.id],
["morning"],
)
end = (today + datetime.timedelta(days=3)).isoformat()
await materialize(database, today.isoformat(), end, today=today)
instances = await QuestInstancesDao(database).list_by_date_range(
child.id, today.isoformat(), today.isoformat()
)
assert instances
return entry, client, child, instances[0]
return entry
def _record_refreshes(entry) -> list[bool]:
@@ -133,11 +85,10 @@ async def test_complete_quest_reaches_the_shared_api_client(
hass, make_entry
) -> None:
"""A panel tap calls ``complete_instance`` on the entry's shared
client with the tapped child, completes NOTHING locally, fires NO
local event, and refreshes the coordinator immediately."""
entry, client, child, instance = await _setup_seeded_entry(
hass, make_entry, _RecordingApiClient(hass, "test_entry")
)
client with the tapped child, fires NO local event, and refreshes
the coordinator immediately."""
client = _RecordingApiClient(_empty_snapshot())
entry = await _setup_entry(hass, make_entry, client)
assert entry.runtime_data.coordinator.api_client is client, (
"the service must proxy through the entry's ONE shared client"
)
@@ -147,21 +98,13 @@ async def test_complete_quest_reaches_the_shared_api_client(
DOMAIN,
SERVICE_COMPLETE_QUEST,
{
"instance_id": instance.id,
"instance_id": 7,
"actor": "panel",
"actor_child_id": child.id,
"actor_child_id": 1,
},
)
assert client.complete_calls == [(instance.id, child.id)]
# No local completion: the instance is still open in the local
# database — only the API service writes (through its own client).
from custom_components.nestquest.completion import instance_state
assert (
await instance_state(entry.runtime_data.database, instance.id)
== "open"
)
assert client.complete_calls == [(7, 1)]
# No local firing: the completed (and day-complete) events arrive
# over the SSE stream, re-fired by the subscription — never from
# this handler.
@@ -198,13 +141,10 @@ async def test_api_failure_surfaces_as_a_service_error(
) -> None:
"""A typed API failure (non-2xx or transport) raises
``HomeAssistantError`` carrying the typed message — which never
includes the panel token — with nothing completed or fired
locally, and no refresh for a mutation that never happened."""
entry, client, child, instance = await _setup_seeded_entry(
hass,
make_entry,
_FailingApiClient(hass, "test_entry", error),
)
includes the panel token — with nothing fired locally, and no
refresh for a mutation that never happened."""
client = _FailingApiClient(error, _empty_snapshot())
entry = await _setup_entry(hass, make_entry, client)
refreshes = _record_refreshes(entry)
with pytest.raises(HomeAssistantError, match=fragment) as excinfo:
@@ -212,22 +152,16 @@ async def test_api_failure_surfaces_as_a_service_error(
DOMAIN,
SERVICE_COMPLETE_QUEST,
{
"instance_id": instance.id,
"instance_id": 7,
"actor": "panel",
"actor_child_id": child.id,
"actor_child_id": 1,
},
)
# The typed error is chained, and the service error carries the
# typed message only — never the panel token.
assert excinfo.value.__cause__ is error
assert "secret-panel-token" not in str(excinfo.value)
assert client.complete_calls == [(instance.id, child.id)]
from custom_components.nestquest.completion import instance_state
assert (
await instance_state(entry.runtime_data.database, instance.id)
== "open"
)
assert client.complete_calls == [(7, 1)]
assert hass.bus.fired(EVENT_QUEST_COMPLETED) == []
assert hass.bus.fired(EVENT_CHILD_DAY_COMPLETE) == []
assert refreshes == []
@@ -241,9 +175,7 @@ async def test_unconfigured_api_client_surfaces_as_a_service_error(
of reaching an attribute error."""
from custom_components.nestquest.coordinator import _UnconfiguredApiClient
entry, _client, child, instance = await _setup_seeded_entry(
hass, make_entry, _UnconfiguredApiClient()
)
await _setup_entry(hass, make_entry, _UnconfiguredApiClient())
with pytest.raises(
HomeAssistantError, match="NestQuest API is not configured"
@@ -252,9 +184,9 @@ async def test_unconfigured_api_client_surfaces_as_a_service_error(
DOMAIN,
SERVICE_COMPLETE_QUEST,
{
"instance_id": instance.id,
"instance_id": 7,
"actor": "panel",
"actor_child_id": child.id,
"actor_child_id": 1,
},
)
@@ -265,19 +197,18 @@ async def test_non_panel_actor_is_rejected_before_the_client(
"""The API's panel route completes as the tapped child only: a
user-actor call fails fast with a clear error and never reaches
the client."""
entry, client, child, instance = await _setup_seeded_entry(
hass, make_entry, _RecordingApiClient(hass, "test_entry")
)
client = _RecordingApiClient(_empty_snapshot())
await _setup_entry(hass, make_entry, client)
with pytest.raises(HomeAssistantError, match="actor must be 'panel'"):
await hass.services.call(
DOMAIN,
SERVICE_COMPLETE_QUEST,
{
"instance_id": instance.id,
"instance_id": 7,
"actor": "user",
"actor_user_id": ADMIN_CTX["user_id"],
"actor_user_id": "admin-1",
},
context=ADMIN_CTX,
context={"user_id": "admin-1"},
)
assert client.complete_calls == []