Files
ha-babytracker/tests/test_integration.py
T
arneandClaude Opus 5.5 6466f002e0 Add Baby Tracker client and Home Assistant integration
Python client for the Baby Tracker sync server, reverse engineered from
the Android app (4.53), and a HACS-installable Home Assistant integration
built on it. Each baby gets a log calendar and summary sensors, and actions
add entries that sync back to the group.

Reading has been run against the real server. Uploading and running inside
real Home Assistant have not.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 10:04:39 +00:00

634 lines
29 KiB
Python

"""Runs the integration's own code against stand-ins for Home Assistant's APIs.
See tests/ha_stubs/README.md for what this can and cannot show. The client is
always pointed at the local mock server, so these tests never reach the real one.
"""
import base64
import json
import sys
import tempfile
import time
import unittest
from datetime import datetime, timedelta, timezone
from functools import partial
from pathlib import Path
from unittest import mock
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "tests"))
sys.path.insert(0, str(ROOT / "tests" / "ha_stubs"))
sys.path.insert(0, str(ROOT))
from mock_server import MockServer
from seed_fixture import BABY_ID, build_seed_zip
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import AbortFlow
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady, HomeAssistantError, ServiceValidationError
from homeassistant.helpers import device_registry as dr
from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM
import custom_components.babytracker as integration
from custom_components.babytracker import calendar as calendar_platform
from custom_components.babytracker import config_flow, sensor as sensor_platform
from custom_components.babytracker.babytracker_api import BabyTrackerClient, encode_object
from custom_components.babytracker.const import CONF_DEVICE_UUID, DOMAIN
DEVICE_UUID = "ha-device"
class _ConfigEntries:
def __init__(self):
self.entries = []
def async_entries(self, domain):
return self.entries
async def async_forward_entry_setups(self, entry, platforms):
pass
async def async_unload_platforms(self, entry, platforms):
return True
class IntegrationTestCase(unittest.IsolatedAsyncioTestCase):
units = METRIC_SYSTEM
async def asyncSetUp(self):
self.server = MockServer()
self.server.__enter__()
self.addCleanup(self.server.__exit__)
self.state = self.server.state
self.state.log_uploads = True
self.folder = tempfile.TemporaryDirectory()
self.addCleanup(self.folder.cleanup)
self.now = time.time()
self.state.seed = build_seed_zip(Path(self.folder.name), self.now)
self.state.devices = [{"DeviceUUID": "phone", "LastSyncID": 3}]
client = partial(BabyTrackerClient, base_url=self.server.url)
for target in (integration, config_flow):
patcher = mock.patch.object(target, "BabyTrackerClient", client)
patcher.start()
self.addCleanup(patcher.stop)
dr.REGISTRY.devices.clear()
self.hass = HomeAssistant(self.units)
self.hass.config_entries = _ConfigEntries()
await integration.async_setup(self.hass, {})
def make_entry(self):
return ConfigEntry(
{"email": self.state.email, "password": self.state.password, CONF_DEVICE_UUID: DEVICE_UUID},
str(self.state.account_id),
)
async def load(self):
entry = self.make_entry()
await integration.async_setup_entry(self.hass, entry)
entry.state = ConfigEntryState.LOADED
self.hass.config_entries.entries = [entry]
self.entry = entry
self.coordinator = entry.runtime_data
self.sensors = {}
self.calendars = []
def add_sensors(entities):
for entity in entities:
self.sensors[entity.unique_id if hasattr(entity, "unique_id") else entity._attr_unique_id] = entity
await sensor_platform.async_setup_entry(self.hass, entry, add_sensors)
await calendar_platform.async_setup_entry(self.hass, entry, self.calendars.extend)
return entry
def value(self, key, baby_id=BABY_ID):
return self.sensors[f"{baby_id}_{key}"].native_value
async def call(self, service, data=None, return_response=False):
return await self.hass.services.async_call(DOMAIN, service, data, return_response=return_response)
def last_upload(self):
upload = self.state.uploads[-1]
return upload, json.loads(base64.b64decode(upload["Transaction"]))
def remote_entry(self, sync_id, record):
return {"SyncID": sync_id, "OPCode": "0", "Transaction": encode_object(record)}
class SetupTests(IntegrationTestCase):
async def test_loads_seed_into_sensors(self):
await self.load()
last = self.value("last_diaper")
self.assertEqual(last, datetime.fromtimestamp(self.now - 60, timezone.utc))
attributes = self.sensors[f"{BABY_ID}_last_diaper"].extra_state_attributes
self.assertEqual((attributes["status"], attributes["amount"], attributes["note"]), ("poopy", "heavy", "messy"))
self.assertAlmostEqual(self.value("temperature"), 37.0, places=3)
self.assertAlmostEqual(self.value("weight"), 6.0, places=3)
self.assertEqual(self.value("length"), 60)
self.assertIn(("POST", "/session"), self.state.requests)
async def test_entities_have_unique_ids_and_a_device_per_baby(self):
await self.load()
self.assertEqual(len(self.sensors), len(sensor_platform.SENSORS) + 1)
sensor = self.sensors[f"{BABY_ID}_weight"]
self.assertEqual(sensor._attr_device_info["identifiers"], {(DOMAIN, BABY_ID)})
self.assertEqual(sensor._attr_device_info["name"], "Sam")
self.assertEqual(self.calendars[0]._attr_translation_key, "log")
async def test_bad_password_starts_reauth(self):
entry = self.make_entry()
self.state.password = "changed"
with self.assertRaises(ConfigEntryAuthFailed):
await integration.async_setup_entry(self.hass, entry)
async def test_server_down_retries_setup(self):
self.state.fail_with = 500
with self.assertRaises(ConfigEntryNotReady):
await integration.async_setup_entry(self.hass, self.make_entry())
async def test_account_without_seed_still_loads(self):
self.state.seed = None
self.state.devices = []
await self.load()
self.assertEqual(self.coordinator.data.babies, {})
async def test_seed_files_are_not_left_behind(self):
import glob
import tempfile as tf
await self.load()
self.assertEqual(glob.glob(f"{tf.gettempdir()}/babytracker*"), [])
class SyncTests(IntegrationTestCase):
async def test_replays_entries_newer_than_the_seed_and_skips_older(self):
record = {
"BCObjectType": "Sleep", "objectID": "s-remote", "baby": {"objectID": BABY_ID},
"time": "2030-01-01 00:00:00 +0000", "duration": 10,
}
self.state.logs["phone"] = [
self.remote_entry(3, {**record, "objectID": "already-in-seed"}),
self.remote_entry(4, record),
]
self.state.devices = [{"DeviceUUID": "phone", "LastSyncID": 4}]
await self.load()
ids = {entry.id for entry in self.coordinator.data.entries(BABY_ID)}
self.assertIn("s-remote", ids)
self.assertNotIn("already-in-seed", ids)
async def test_refresh_fetches_new_entries_only(self):
await self.load()
self.state.requests.clear()
self.state.logs["phone"] = [
self.remote_entry(4, {
"BCObjectType": "Diaper", "objectID": "d-remote", "baby": {"objectID": BABY_ID},
"time": "2030-01-01 00:00:00 +0000", "status": "0", "amount": "2",
})
]
self.state.devices = [{"DeviceUUID": "phone", "LastSyncID": 4}]
await self.coordinator.async_refresh()
self.assertEqual(self.coordinator.data.latest(BABY_ID).id, "d-remote")
self.assertIn(("GET", "/account/transaction/phone/3"), self.state.requests)
self.assertNotIn(("GET", "/account/seed"), self.state.requests)
self.state.requests.clear()
await self.coordinator.async_refresh()
self.assertFalse([r for r in self.state.requests if "transaction" in r[1]])
async def test_failed_refresh_loses_nothing_and_recovers(self):
await self.load()
self.state.logs["phone"] = [self.remote_entry(4, {
"BCObjectType": "Bath", "objectID": "b-remote", "baby": {"objectID": BABY_ID},
"time": "2030-01-01 00:00:00 +0000",
})]
self.state.devices = [{"DeviceUUID": "phone", "LastSyncID": 4}]
self.state.fail_with = 503
await self.coordinator.async_refresh()
self.assertFalse(self.coordinator.last_update_success)
self.assertFalse(self.sensors[f"{BABY_ID}_weight"].available)
self.state.fail_with = None
await self.coordinator.async_refresh()
self.assertTrue(self.coordinator.last_update_success)
self.assertEqual(self.coordinator.data.latest(BABY_ID).id, "b-remote")
async def test_unreadable_entry_is_skipped(self):
await self.load()
self.state.logs["phone"] = [
{"SyncID": 4, "OPCode": "0", "Transaction": "not base64 json!"},
self.remote_entry(5, {"BCObjectType": "Bath", "objectID": "good", "baby": {"objectID": BABY_ID},
"time": "2030-01-01 00:00:00 +0000"}),
self.remote_entry(6, {"BCObjectType": "Bath", "objectID": "bad-date", "baby": {"objectID": BABY_ID},
"time": "whenever"}),
]
self.state.devices = [{"DeviceUUID": "phone", "LastSyncID": 6}]
with self.assertLogs("custom_components.babytracker.coordinator", "WARNING"):
await self.coordinator.async_refresh()
self.assertTrue(self.coordinator.last_update_success)
self.assertEqual(self.coordinator.data.latest(BABY_ID).id, "good")
async def test_new_baby_from_the_log_gets_entities(self):
await self.load()
self.state.logs["phone"] = [self.remote_entry(4, {"BCObjectType": "Baby", "objectID": "b2", "name": "Alex"})]
self.state.devices = [{"DeviceUUID": "phone", "LastSyncID": 4}]
await self.coordinator.async_refresh()
self.assertIn("b2_last_diaper", self.sensors)
self.assertEqual(len(self.calendars), 2)
async def test_deleted_baby_becomes_unavailable(self):
await self.load()
self.state.logs["phone"] = [{
"SyncID": 4, "OPCode": "2", "Transaction": encode_object({"BCObjectType": "Baby", "objectID": BABY_ID})
}]
self.state.devices = [{"DeviceUUID": "phone", "LastSyncID": 4}]
await self.coordinator.async_refresh()
self.assertFalse(self.sensors[f"{BABY_ID}_weight"].available)
async def test_expired_password_on_refresh_starts_reauth(self):
await self.load()
self.state.valid_sessions = False
self.state.password = "changed"
with self.assertRaises(ConfigEntryAuthFailed):
await self.coordinator.async_refresh()
async def test_different_account_is_refused(self):
self.state.account_id = 777
with self.assertRaises(ConfigEntryAuthFailed):
await integration.async_setup_entry(self.hass, ConfigEntry(
{"email": self.state.email, "password": self.state.password, CONF_DEVICE_UUID: DEVICE_UUID}, "42"
))
async def test_restart_keeps_what_this_device_uploaded(self):
await self.load()
await self.call("log_bath", {"note": "first"})
await self.call("log_bath", {"note": "second"})
self.assertEqual([u["SyncID"] for u in self.state.uploads], [1, 2])
await self.load() # A new client and replica, as after a restart.
notes = [e.note for e in self.coordinator.data.entries(BABY_ID, {"Bath"})]
self.assertCountEqual(notes, ["first", "second"])
await self.call("log_bath", {"note": "third"})
self.assertEqual(self.state.uploads[-1]["SyncID"], 3)
class ServiceTests(IntegrationTestCase):
async def asyncSetUp(self):
await super().asyncSetUp()
await self.load()
async def test_every_service_is_registered_with_optional_response(self):
names = {name for _, name in self.hass.services.registered}
self.assertEqual(names, {
"log_diaper", "log_sleep", "log_bottle", "log_nursing", "log_temperature",
"log_growth", "log_bath", "log_joy", "log_journal",
})
async def test_diaper_uploads_and_updates_sensors_at_once(self):
before = self.value("diapers_today")
response = await self.call("log_diaper", {"status": "mixed", "amount": "light", "note": "n"}, return_response=True)
upload, body = self.last_upload()
self.assertEqual(upload["OPCode"], "0")
self.assertEqual((body["BCObjectType"], body["status"], body["amount"], body["note"]), ("Diaper", "2", "1", "n"))
self.assertEqual(body["baby"]["objectID"], BABY_ID)
self.assertEqual(body["baby"]["name"], "Sam")
self.assertNotIn("BCObjectType", body["baby"])
self.assertEqual(response, {"id": body["objectID"]})
self.assertEqual(self.value("diapers_today"), before + 1)
self.assertEqual(self.sensors[f"{BABY_ID}_last_diaper"].extra_state_attributes["status"], "mixed")
async def test_no_response_unless_asked(self):
self.assertIsNone(await self.call("log_diaper", {"status": "wet"}))
async def test_diaper_without_amount_is_not_counted_dry(self):
await self.call("log_diaper", {"status": "wet"})
_, body = self.last_upload()
self.assertEqual(body["amount"], "5")
self.assertEqual(self.sensors[f"{BABY_ID}_last_diaper"].extra_state_attributes["status"], "wet")
async def test_sleep_defaults_to_having_just_finished(self):
before = self.value("sleep_today")
await self.call("log_sleep", {"duration": 30})
_, body = self.last_upload()
started = datetime.strptime(body["time"], "%Y-%m-%d %H:%M:%S %z")
self.assertAlmostEqual((datetime.now(timezone.utc) - started).total_seconds(), 1800, delta=5)
self.assertEqual(body["duration"], 30)
if started.date() == datetime.now(timezone.utc).date():
self.assertEqual(self.value("sleep_today"), before + 30)
async def test_explicit_time_is_read_as_local_and_sent_as_gmt(self):
await self.call("log_bath", {"time": "2026-03-01T10:00:00"})
_, body = self.last_upload()
self.assertEqual(body["time"], "2026-03-01 10:00:00 +0000")
async def test_bottle_in_metric_by_default(self):
before = self.value("bottle_volume_today")
await self.call("log_bottle", {"amount": 120})
_, body = self.last_upload()
self.assertEqual(body["BCObjectType"], "Formula")
self.assertEqual(body["amount"], {"englishMeasure": False, "value": 120.0})
self.assertEqual(self.value("bottle_volume_today"), before + 120)
self.assertEqual(self.sensors[f"{BABY_ID}_last_feeding"].extra_state_attributes["type"], "Formula")
async def test_pumped_bottle_with_explicit_ounces(self):
await self.call("log_bottle", {"amount": 4, "kind": "pumped", "unit": "oz"})
_, body = self.last_upload()
self.assertEqual(body["BCObjectType"], "Pumped")
self.assertEqual(body["amount"], {"englishMeasure": True, "value": 4.0})
async def test_nursing(self):
await self.call("log_nursing", {"left_minutes": 5, "right_minutes": 7, "finish_side": "right"})
_, body = self.last_upload()
self.assertEqual((body["leftDuration"], body["rightDuration"], body["bothDuration"], body["finishSide"]), (5, 7, 0, "2"))
started = datetime.strptime(body["time"], "%Y-%m-%d %H:%M:%S %z")
self.assertAlmostEqual((datetime.now(timezone.utc) - started).total_seconds(), 720, delta=5)
async def test_temperature_and_growth(self):
await self.call("log_temperature", {"value": 38.5})
self.assertEqual(self.value("temperature"), 38.5)
await self.call("log_growth", {"weight": 7.1})
_, body = self.last_upload()
self.assertEqual(body["weight"], {"englishMeasure": False, "value": 7.1})
self.assertNotIn("length", body)
self.assertEqual(self.value("weight"), 7.1)
self.assertEqual(self.value("length"), 60)
async def test_journal_keeps_its_text(self):
await self.call("log_journal", {"note": "First laugh"})
_, body = self.last_upload()
self.assertEqual((body["BCObjectType"], body["note"]), ("Journal", "First laugh"))
async def test_invalid_input_is_rejected_before_upload(self):
for service, data in (
("log_diaper", {}),
("log_diaper", {"status": "sparkly"}),
("log_diaper", {"status": "wet", "amount": "noset"}),
("log_sleep", {}),
("log_sleep", {"duration": -1}),
("log_bottle", {"amount": -5}),
("log_nursing", {}),
("log_nursing", {"left_minutes": 0}),
("log_growth", {}),
("log_journal", {}),
("log_journal", {"note": ""}),
("log_bath", {"unknown": 1}),
):
with self.subTest(service=service, data=data), self.assertRaises(Exception):
await self.call(service, data)
self.assertEqual(self.state.uploads, [])
async def test_baby_by_device_id_and_by_name(self):
dr.REGISTRY.devices["device-1"] = dr.DeviceEntry({(DOMAIN, BABY_ID)})
await self.call("log_bath", {"baby": "device-1"})
await self.call("log_bath", {"baby": "sam"})
self.assertEqual(len(self.state.uploads), 2)
async def test_unknown_baby_is_an_error(self):
with self.assertRaises(ServiceValidationError) as caught:
await self.call("log_bath", {"baby": "Nobody"})
self.assertEqual(caught.exception.translation_key, "baby_not_found")
async def test_baby_is_required_with_two_babies(self):
self.state.logs["phone"] = [self.remote_entry(4, {"BCObjectType": "Baby", "objectID": "b2", "name": "Alex"})]
self.state.devices = [{"DeviceUUID": "phone", "LastSyncID": 4}]
await self.coordinator.async_refresh()
with self.assertRaises(ServiceValidationError) as caught:
await self.call("log_bath")
self.assertEqual(caught.exception.translation_key, "baby_required")
await self.call("log_bath", {"baby": "Alex"})
self.assertEqual(self.last_upload()[1]["baby"]["objectID"], "b2")
async def test_failed_upload_raises_and_changes_nothing(self):
before = self.value("diapers_today")
self.state.fail_with = 500
with self.assertRaises(HomeAssistantError) as caught:
await self.call("log_diaper", {"status": "wet"})
self.assertEqual(caught.exception.translation_key, "upload_failed")
self.assertEqual(self.value("diapers_today"), before)
self.state.fail_with = None
await self.call("log_diaper", {"status": "wet"})
self.assertEqual(self.state.uploads[-1]["SyncID"], 1)
async def test_uploads_keep_order_when_called_together(self):
import asyncio
await asyncio.gather(*(self.call("log_bath", {"note": str(i)}) for i in range(5)))
self.assertEqual([u["SyncID"] for u in self.state.uploads], [1, 2, 3, 4, 5])
async def test_nothing_loaded_means_no_baby(self):
self.entry.state = ConfigEntryState.NOT_LOADED
with self.assertRaises(ServiceValidationError):
await self.call("log_bath")
class UsCustomaryTests(IntegrationTestCase):
units = US_CUSTOMARY_SYSTEM
async def test_units_default_to_home_assistants_system(self):
await self.load()
await self.call("log_bottle", {"amount": 4})
self.assertIs(self.last_upload()[1]["amount"]["englishMeasure"], True)
await self.call("log_temperature", {"value": 100.4})
self.assertAlmostEqual(self.value("temperature"), 38.0, places=3)
await self.call("log_growth", {"weight": 11, "length": 20})
body = self.last_upload()[1]
self.assertIs(body["weight"]["englishMeasure"], True)
self.assertAlmostEqual(self.value("length"), 50.8, places=3)
await self.call("log_bottle", {"amount": 100, "unit": "ml"})
self.assertIs(self.last_upload()[1]["amount"]["englishMeasure"], False)
class DisplayTests(IntegrationTestCase):
async def asyncSetUp(self):
await super().asyncSetUp()
await self.load()
async def test_today_counts_use_the_local_day(self):
from homeassistant.util import dt as dt_util
now = datetime.now(timezone.utc)
start = now - timedelta(hours=1)
with mock.patch.object(dt_util, "start_of_local_day", return_value=start):
self.assertEqual(self.value("diapers_today"), 1)
with mock.patch.object(dt_util, "start_of_local_day", return_value=now - timedelta(hours=3)):
self.assertEqual(self.value("diapers_today"), 2)
self.assertEqual(self.value("feedings_today"), 2)
async def test_recent_activity_is_bounded_and_unrecorded(self):
sensor = self.sensors[f"{BABY_ID}_recent_activity"]
self.assertEqual(sensor._unrecorded_attributes, frozenset({"entries"}))
for i in range(25):
await self.call("log_bath", {"note": str(i)})
entries = sensor.extra_state_attributes["entries"]
self.assertEqual(len(entries), 20)
self.assertEqual(set(entries[0]), {"id", "type", "summary", "time", "note"})
json.dumps(entries)
self.assertEqual(sensor.native_value, self.coordinator.data.latest(BABY_ID).time)
async def test_calendar_window_and_durations(self):
calendar = self.calendars[0]
now = datetime.now(timezone.utc)
events = await calendar.async_get_events(self.hass, now - timedelta(hours=4), now)
by_uid = {event.uid: event for event in events}
self.assertIn("s-1", by_uid)
self.assertEqual(by_uid["s-1"].end - by_uid["s-1"].start, timedelta(minutes=45))
self.assertEqual(by_uid["d-new"].end - by_uid["d-new"].start, timedelta(minutes=1))
self.assertEqual(by_uid["d-new"].description, "messy")
self.assertNotIn("n-1", by_uid)
self.assertEqual(calendar.event.uid, "d-new")
async def test_calendar_includes_sleep_overlapping_the_window(self):
calendar = self.calendars[0]
start = datetime.fromtimestamp(self.now - 3 * 3600 + 600, timezone.utc)
events = await calendar.async_get_events(self.hass, start, start + timedelta(minutes=5))
self.assertEqual([event.uid for event in events], ["s-1"])
async def test_no_entries(self):
self.state.seed = None
self.state.devices = []
self.state.logs = {"phone": [self.remote_entry(4, {"BCObjectType": "Baby", "objectID": "b3", "name": "Kim"})]}
self.state.devices = [{"DeviceUUID": "phone", "LastSyncID": 4}]
await self.load()
self.assertIsNone(self.value("last_diaper", "b3"))
self.assertEqual(self.value("diapers_today", "b3"), 0)
self.assertEqual(self.value("sleep_today", "b3"), 0)
self.assertIsNone(self.value("weight", "b3"))
self.assertEqual(self.sensors["b3_last_diaper"].extra_state_attributes, {})
calendar = next(c for c in self.calendars if c.baby_id == "b3")
self.assertIsNone(calendar.event)
class ConfigFlowTests(IntegrationTestCase):
def flow(self, existing=()):
flow = config_flow.BabyTrackerConfigFlow()
flow.hass = self.hass
flow.existing_unique_ids = set(existing)
return flow
async def test_shows_form_first(self):
result = await self.flow().async_step_user()
self.assertEqual((result["type"], result["step_id"]), ("form", "user"))
async def test_creates_entry_with_a_lasting_device_id(self):
result = await self.flow().async_step_user({"email": self.state.email, "password": self.state.password})
self.assertEqual(result["type"], "create_entry")
self.assertEqual(result["title"], self.state.email)
self.assertEqual(result["data"][CONF_DEVICE_UUID], self.state.login_bodies[0]["Device"]["DeviceUUID"])
self.assertEqual(self.state.login_bodies[0]["Device"]["DeviceName"], "Home Assistant")
async def test_wrong_password(self):
result = await self.flow().async_step_user({"email": self.state.email, "password": "nope"})
self.assertEqual(result["errors"], {"base": "invalid_auth"})
async def test_server_error(self):
self.state.login_status = 500
result = await self.flow().async_step_user({"email": self.state.email, "password": self.state.password})
self.assertEqual(result["errors"], {"base": "cannot_connect"})
async def test_307(self):
self.state.login_status = 307
result = await self.flow().async_step_user({"email": self.state.email, "password": self.state.password})
self.assertEqual(result["errors"], {"base": "account_state"})
async def test_unreachable_server(self):
client = partial(BabyTrackerClient, base_url="http://127.0.0.1:9", timeout=2)
with mock.patch.object(config_flow, "BabyTrackerClient", client):
result = await self.flow().async_step_user({"email": "a@b.c", "password": "x"})
self.assertEqual(result["errors"], {"base": "cannot_connect"})
async def test_same_account_twice_aborts(self):
with self.assertRaises(AbortFlow) as caught:
await self.flow(existing={"42"}).async_step_user({"email": self.state.email, "password": self.state.password})
self.assertEqual(caught.exception.reason, "already_configured")
async def test_reauth_updates_password_and_keeps_device_id(self):
entry = self.make_entry()
flow = self.flow()
flow.reauth_entry = entry
result = await flow.async_step_reauth(entry.data)
self.assertEqual(result["step_id"], "reauth_confirm")
self.assertEqual(result["description_placeholders"], {"email": self.state.email})
self.state.password = "new-password"
result = await flow.async_step_reauth_confirm({"password": "wrong"})
self.assertEqual(result["errors"], {"base": "invalid_auth"})
result = await flow.async_step_reauth_confirm({"password": "new-password"})
self.assertEqual(result["reason"], "reauth_successful")
self.assertEqual(entry.data["password"], "new-password")
self.assertEqual(entry.data[CONF_DEVICE_UUID], DEVICE_UUID)
self.assertEqual(self.state.login_bodies[-1]["Device"]["DeviceUUID"], DEVICE_UUID)
async def test_reauth_with_another_account_aborts(self):
entry = ConfigEntry({"email": self.state.email, "password": "x", CONF_DEVICE_UUID: DEVICE_UUID}, "999")
flow = self.flow()
flow.reauth_entry = entry
with self.assertRaises(AbortFlow) as caught:
await flow.async_step_reauth_confirm({"password": self.state.password})
self.assertEqual(caught.exception.reason, "wrong_account")
class PackagingTests(unittest.TestCase):
"""Checks that the integration's data files agree with its code."""
BASE = ROOT / "custom_components" / "babytracker"
@classmethod
def setUpClass(cls):
import yaml
cls.services = yaml.safe_load((cls.BASE / "services.yaml").read_text())
cls.translations = json.loads((cls.BASE / "translations" / "en.json").read_text())
cls.icons = json.loads((cls.BASE / "icons.json").read_text())
cls.manifest = json.loads((cls.BASE / "manifest.json").read_text())
from custom_components.babytracker import services
cls.schemas = {}
for name, (_, schema) in services._SERVICES.items():
schema = schema.validators[0] if hasattr(schema, "validators") else schema
cls.schemas[name] = {marker.key for marker in schema.schema}
def test_services_yaml_matches_the_schemas(self):
declared = {name: set(body["fields"]) for name, body in self.services.items() if not name.startswith(".")}
self.assertEqual(declared, self.schemas)
def test_every_service_field_is_translated_and_has_an_icon(self):
for name, fields in self.schemas.items():
translated = self.translations["services"][name]
self.assertEqual(set(translated["fields"]), fields, name)
self.assertIn(name, self.icons["services"])
def test_every_sensor_is_translated(self):
keys = {description.key for description in sensor_platform.SENSORS} | {"recent_activity"}
self.assertEqual(set(self.translations["entity"]["sensor"]), keys)
self.assertLessEqual(set(self.icons["entity"]["sensor"]), keys)
def test_every_error_is_translated(self):
source = (self.BASE / "config_flow.py").read_text()
for key in self.translations["config"]["error"]:
self.assertIn(f'"{key}"', source)
import re
services_source = (self.BASE / "services.py").read_text()
keys = set(re.findall(r'translation_key="(\w+)"', services_source))
self.assertEqual(keys, set(self.translations["exceptions"]))
def test_manifest_has_what_hacs_requires(self):
for key in ("domain", "name", "codeowners", "documentation", "issue_tracker", "version", "config_flow"):
self.assertIn(key, self.manifest)
self.assertEqual(self.manifest["domain"], DOMAIN)
hacs = json.loads((ROOT / "hacs.json").read_text())
self.assertIn("name", hacs)
def test_library_needs_only_what_the_manifest_declares(self):
import re
for path in (self.BASE / "babytracker_api").glob("*.py"):
for module in re.findall(r"^(?:from|import) (\w+)", path.read_text(), re.M):
self.assertNotIn(module, ("homeassistant", "custom_components"), path.name)
self.assertEqual(self.manifest["requirements"], ["requests>=2.25"])
if __name__ == "__main__":
unittest.main()