From 1d94e66b9c815bffd0643a4101e6a1822cd62c94 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 23 Sep 2024 17:40:19 +0200 Subject: [PATCH] Add NYT Games integration (#126449) * Add NYT Games integration * Add NYT Games integration * Add NYT Games integration * Add NYT Games integration * Add test --- CODEOWNERS | 2 + .../components/nyt_games/__init__.py | 42 +++++++ .../components/nyt_games/config_flow.py | 42 +++++++ homeassistant/components/nyt_games/const.py | 7 ++ .../components/nyt_games/coordinator.py | 38 +++++++ homeassistant/components/nyt_games/entity.py | 21 ++++ homeassistant/components/nyt_games/icons.json | 9 ++ .../components/nyt_games/manifest.json | 10 ++ homeassistant/components/nyt_games/sensor.py | 72 ++++++++++++ .../components/nyt_games/strings.json | 29 +++++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 6 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/nyt_games/__init__.py | 13 +++ tests/components/nyt_games/conftest.py | 54 +++++++++ .../components/nyt_games/fixtures/latest.json | 69 ++++++++++++ .../nyt_games/snapshots/test_init.ambr | 33 ++++++ .../nyt_games/snapshots/test_sensor.ambr | 51 +++++++++ .../components/nyt_games/test_config_flow.py | 104 ++++++++++++++++++ tests/components/nyt_games/test_init.py | 29 +++++ tests/components/nyt_games/test_sensor.py | 55 +++++++++ 22 files changed, 693 insertions(+) create mode 100644 homeassistant/components/nyt_games/__init__.py create mode 100644 homeassistant/components/nyt_games/config_flow.py create mode 100644 homeassistant/components/nyt_games/const.py create mode 100644 homeassistant/components/nyt_games/coordinator.py create mode 100644 homeassistant/components/nyt_games/entity.py create mode 100644 homeassistant/components/nyt_games/icons.json create mode 100644 homeassistant/components/nyt_games/manifest.json create mode 100644 homeassistant/components/nyt_games/sensor.py create mode 100644 homeassistant/components/nyt_games/strings.json create mode 100644 tests/components/nyt_games/__init__.py create mode 100644 tests/components/nyt_games/conftest.py create mode 100644 tests/components/nyt_games/fixtures/latest.json create mode 100644 tests/components/nyt_games/snapshots/test_init.ambr create mode 100644 tests/components/nyt_games/snapshots/test_sensor.ambr create mode 100644 tests/components/nyt_games/test_config_flow.py create mode 100644 tests/components/nyt_games/test_init.py create mode 100644 tests/components/nyt_games/test_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index a144f1b339b..c95c457b27e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1024,6 +1024,8 @@ build.json @home-assistant/supervisor /tests/components/nut/ @bdraco @ollo69 @pestevez /homeassistant/components/nws/ @MatthewFlamm @kamiyo /tests/components/nws/ @MatthewFlamm @kamiyo +/homeassistant/components/nyt_games/ @joostlek +/tests/components/nyt_games/ @joostlek /homeassistant/components/nzbget/ @chriscla /tests/components/nzbget/ @chriscla /homeassistant/components/obihai/ @dshokouhi @ejpenney diff --git a/homeassistant/components/nyt_games/__init__.py b/homeassistant/components/nyt_games/__init__.py new file mode 100644 index 00000000000..ae35b40d29f --- /dev/null +++ b/homeassistant/components/nyt_games/__init__.py @@ -0,0 +1,42 @@ +"""The NYT Games integration.""" + +from __future__ import annotations + +from nyt_games import NYTGamesClient + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_TOKEN, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .coordinator import NYTGamesCoordinator + +PLATFORMS: list[Platform] = [ + Platform.SENSOR, +] + + +type NYTGamesConfigEntry = ConfigEntry[NYTGamesCoordinator] + + +async def async_setup_entry(hass: HomeAssistant, entry: NYTGamesConfigEntry) -> bool: + """Set up NYTGames from a config entry.""" + + client = NYTGamesClient( + entry.data[CONF_TOKEN], session=async_get_clientsession(hass) + ) + + coordinator = NYTGamesCoordinator(hass, client) + + await coordinator.async_config_entry_first_refresh() + + entry.runtime_data = coordinator + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: NYTGamesConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/nyt_games/config_flow.py b/homeassistant/components/nyt_games/config_flow.py new file mode 100644 index 00000000000..b8687e58f72 --- /dev/null +++ b/homeassistant/components/nyt_games/config_flow.py @@ -0,0 +1,42 @@ +"""Config flow for NYT Games.""" + +from typing import Any + +from nyt_games import NYTGamesAuthenticationError, NYTGamesClient, NYTGamesError +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.const import CONF_TOKEN +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DOMAIN + + +class NYTGamesConfigFlow(ConfigFlow, domain=DOMAIN): + """NYT Games config flow.""" + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle a flow initialized by the user.""" + errors: dict[str, str] = {} + if user_input: + session = async_get_clientsession(self.hass) + client = NYTGamesClient(user_input[CONF_TOKEN], session=session) + try: + latest_stats = await client.get_latest_stats() + except NYTGamesAuthenticationError: + errors["base"] = "invalid_auth" + except NYTGamesError: + errors["base"] = "cannot_connect" + except Exception: # noqa: BLE001 + errors["base"] = "unknown" + else: + await self.async_set_unique_id(str(latest_stats.user_id)) + self._abort_if_unique_id_configured() + return self.async_create_entry(title="NYT Games", data=user_input) + return self.async_show_form( + step_id="user", + data_schema=vol.Schema({vol.Required(CONF_TOKEN): str}), + errors=errors, + ) diff --git a/homeassistant/components/nyt_games/const.py b/homeassistant/components/nyt_games/const.py new file mode 100644 index 00000000000..c290e70b283 --- /dev/null +++ b/homeassistant/components/nyt_games/const.py @@ -0,0 +1,7 @@ +"""Constants for the NYT Games integration.""" + +import logging + +DOMAIN = "nyt_games" + +LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/nyt_games/coordinator.py b/homeassistant/components/nyt_games/coordinator.py new file mode 100644 index 00000000000..4234df2e0b1 --- /dev/null +++ b/homeassistant/components/nyt_games/coordinator.py @@ -0,0 +1,38 @@ +"""Define an object to manage fetching NYT Games data.""" + +from __future__ import annotations + +from datetime import timedelta +from typing import TYPE_CHECKING + +from nyt_games import NYTGamesClient, NYTGamesError, Wordle + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import LOGGER + +if TYPE_CHECKING: + from . import NYTGamesConfigEntry + + +class NYTGamesCoordinator(DataUpdateCoordinator[Wordle]): + """Class to manage fetching NYT Games data.""" + + config_entry: NYTGamesConfigEntry + + def __init__(self, hass: HomeAssistant, client: NYTGamesClient) -> None: + """Initialize coordinator.""" + super().__init__( + hass, + logger=LOGGER, + name="NYT Games", + update_interval=timedelta(minutes=15), + ) + self.client = client + + async def _async_update_data(self) -> Wordle: + try: + return (await self.client.get_latest_stats()).stats.wordle + except NYTGamesError as error: + raise UpdateFailed(error) from error diff --git a/homeassistant/components/nyt_games/entity.py b/homeassistant/components/nyt_games/entity.py new file mode 100644 index 00000000000..b5370805e27 --- /dev/null +++ b/homeassistant/components/nyt_games/entity.py @@ -0,0 +1,21 @@ +"""Base class for NYT Games entities.""" + +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import NYTGamesCoordinator + + +class NYTGamesEntity(CoordinatorEntity[NYTGamesCoordinator]): + """Defines a base NYT Games entity.""" + + _attr_has_entity_name = True + + def __init__(self, coordinator: NYTGamesCoordinator) -> None: + """Initialize a NYT Games entity.""" + super().__init__(coordinator) + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, str(coordinator.config_entry.unique_id))}, + manufacturer="New York Times", + ) diff --git a/homeassistant/components/nyt_games/icons.json b/homeassistant/components/nyt_games/icons.json new file mode 100644 index 00000000000..fe18cddc5c7 --- /dev/null +++ b/homeassistant/components/nyt_games/icons.json @@ -0,0 +1,9 @@ +{ + "entity": { + "sensor": { + "wordles_played": { + "default": "mdi:text-long" + } + } + } +} diff --git a/homeassistant/components/nyt_games/manifest.json b/homeassistant/components/nyt_games/manifest.json new file mode 100644 index 00000000000..94a731c52a4 --- /dev/null +++ b/homeassistant/components/nyt_games/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "nyt_games", + "name": "NYT Games", + "codeowners": ["@joostlek"], + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/nyt_games", + "integration_type": "service", + "iot_class": "cloud_polling", + "requirements": ["nyt_games==0.3.0"] +} diff --git a/homeassistant/components/nyt_games/sensor.py b/homeassistant/components/nyt_games/sensor.py new file mode 100644 index 00000000000..157b0311481 --- /dev/null +++ b/homeassistant/components/nyt_games/sensor.py @@ -0,0 +1,72 @@ +"""Support for NYT Games sensors.""" + +from collections.abc import Callable +from dataclasses import dataclass + +from nyt_games import Wordle + +from homeassistant.components.sensor import ( + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType + +from . import NYTGamesConfigEntry +from .coordinator import NYTGamesCoordinator +from .entity import NYTGamesEntity + + +@dataclass(frozen=True, kw_only=True) +class NYTGamesWordleSensorEntityDescription(SensorEntityDescription): + """Describes a NYT Games Wordle sensor entity.""" + + value_fn: Callable[[Wordle], StateType] + + +SENSOR_TYPES: tuple[NYTGamesWordleSensorEntityDescription, ...] = ( + NYTGamesWordleSensorEntityDescription( + key="wordles_played", + translation_key="wordles_played", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement="games", + value_fn=lambda wordle: wordle.games_played, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: NYTGamesConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up NYT Games sensor entities based on a config entry.""" + + coordinator = entry.runtime_data + + async_add_entities( + NYTGamesSensor(coordinator, description) for description in SENSOR_TYPES + ) + + +class NYTGamesSensor(NYTGamesEntity, SensorEntity): + """Defines a NYT Games sensor.""" + + entity_description: NYTGamesWordleSensorEntityDescription + + def __init__( + self, + coordinator: NYTGamesCoordinator, + description: NYTGamesWordleSensorEntityDescription, + ) -> None: + """Initialize NYT Games sensor.""" + super().__init__(coordinator) + self.entity_description = description + self._attr_unique_id = f"{coordinator.config_entry.unique_id}-{description.key}" + + @property + def native_value(self) -> StateType: + """Return the state of the sensor.""" + return self.entity_description.value_fn(self.coordinator.data) diff --git a/homeassistant/components/nyt_games/strings.json b/homeassistant/components/nyt_games/strings.json new file mode 100644 index 00000000000..ff7b0297f22 --- /dev/null +++ b/homeassistant/components/nyt_games/strings.json @@ -0,0 +1,29 @@ +{ + "config": { + "step": { + "user": { + "data": { + "token": "Token" + }, + "data_description": { + "token": "The NYT Games NYT-S cookie value." + } + } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + } + }, + "entity": { + "sensor": { + "wordles_played": { + "name": "Wordles played" + } + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index e126558cc0d..40ddcbd86c0 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -408,6 +408,7 @@ FLOWS = { "nuki", "nut", "nws", + "nyt_games", "nzbget", "obihai", "octoprint", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 528d10aaab8..9ed6ba531da 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -4221,6 +4221,12 @@ "config_flow": false, "iot_class": "local_push" }, + "nyt_games": { + "name": "NYT Games", + "integration_type": "service", + "config_flow": true, + "iot_class": "cloud_polling" + }, "nzbget": { "name": "NZBGet", "integration_type": "hub", diff --git a/requirements_all.txt b/requirements_all.txt index 6a8eb27f67b..3257b170538 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1483,6 +1483,9 @@ numato-gpio==0.13.0 # homeassistant.components.trend numpy==1.26.0 +# homeassistant.components.nyt_games +nyt_games==0.3.0 + # homeassistant.components.oasa_telematics oasatelematics==0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0b7a8c26df6..5943e4b18aa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1231,6 +1231,9 @@ numato-gpio==0.13.0 # homeassistant.components.trend numpy==1.26.0 +# homeassistant.components.nyt_games +nyt_games==0.3.0 + # homeassistant.components.google oauth2client==4.1.3 diff --git a/tests/components/nyt_games/__init__.py b/tests/components/nyt_games/__init__.py new file mode 100644 index 00000000000..46dff12e5a1 --- /dev/null +++ b/tests/components/nyt_games/__init__.py @@ -0,0 +1,13 @@ +"""Tests for the NYT Games integration.""" + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: + """Fixture for setting up the component.""" + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/nyt_games/conftest.py b/tests/components/nyt_games/conftest.py new file mode 100644 index 00000000000..324327174f5 --- /dev/null +++ b/tests/components/nyt_games/conftest.py @@ -0,0 +1,54 @@ +"""NYTGames tests configuration.""" + +from collections.abc import Generator +from unittest.mock import patch + +from nyt_games.models import LatestData +import pytest + +from homeassistant.components.nyt_games.const import DOMAIN +from homeassistant.const import CONF_TOKEN + +from tests.common import MockConfigEntry, load_fixture +from tests.components.smhi.common import AsyncMock + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.nyt_games.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture +def mock_nyt_games_client() -> Generator[AsyncMock]: + """Mock an NYTGames client.""" + with ( + patch( + "homeassistant.components.nyt_games.NYTGamesClient", + autospec=True, + ) as mock_client, + patch( + "homeassistant.components.nyt_games.config_flow.NYTGamesClient", + new=mock_client, + ), + ): + client = mock_client.return_value + client.get_latest_stats.return_value = LatestData.from_json( + load_fixture("latest.json", DOMAIN) + ).player + yield client + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Mock a config entry.""" + return MockConfigEntry( + domain=DOMAIN, + title="NYTGames", + data={CONF_TOKEN: "token"}, + unique_id="218886794", + ) diff --git a/tests/components/nyt_games/fixtures/latest.json b/tests/components/nyt_games/fixtures/latest.json new file mode 100644 index 00000000000..73a6f440fc0 --- /dev/null +++ b/tests/components/nyt_games/fixtures/latest.json @@ -0,0 +1,69 @@ +{ + "states": [], + "user_id": 218886794, + "player": { + "user_id": 218886794, + "last_updated": 1726831978, + "stats": { + "spelling_bee": { + "puzzles_started": 87, + "total_words": 362, + "total_pangrams": 15, + "longest_word": { + "word": "checkable", + "center_letter": "b", + "print_date": "2024-07-27" + }, + "ranks": { + "Beginner": 23, + "Good": 21, + "Good Start": 14, + "Moving Up": 16, + "Nice": 4, + "Solid": 9 + } + }, + "wordle": { + "legacyStats": { + "gamesPlayed": 70, + "gamesWon": 51, + "guesses": { + "1": 0, + "2": 1, + "3": 7, + "4": 11, + "5": 20, + "6": 12, + "fail": 19 + }, + "currentStreak": 1, + "maxStreak": 5, + "lastWonDayOffset": 1189, + "hasPlayed": true, + "autoOptInTimestamp": 1708273168957, + "hasMadeStatsChoice": false, + "timestamp": 1726831978 + }, + "calculatedStats": { + "gamesPlayed": 33, + "gamesWon": 26, + "guesses": { + "1": 0, + "2": 1, + "3": 4, + "4": 7, + "5": 10, + "6": 4, + "fail": 7 + }, + "currentStreak": 1, + "maxStreak": 5, + "lastWonPrintDate": "2024-09-20", + "lastCompletedPrintDate": "2024-09-20", + "hasPlayed": true, + "generation": 1 + } + } + } + } +} diff --git a/tests/components/nyt_games/snapshots/test_init.ambr b/tests/components/nyt_games/snapshots/test_init.ambr new file mode 100644 index 00000000000..10a44f5d150 --- /dev/null +++ b/tests/components/nyt_games/snapshots/test_init.ambr @@ -0,0 +1,33 @@ +# serializer version: 1 +# name: test_device_info + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'nyt_games', + '218886794', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'New York Times', + 'model': None, + 'model_id': None, + 'name': 'NYTGames', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'suggested_area': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- diff --git a/tests/components/nyt_games/snapshots/test_sensor.ambr b/tests/components/nyt_games/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..bb92d08f909 --- /dev/null +++ b/tests/components/nyt_games/snapshots/test_sensor.ambr @@ -0,0 +1,51 @@ +# serializer version: 1 +# name: test_all_entities[sensor.nytgames_wordles_played-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.nytgames_wordles_played', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Wordles played', + 'platform': 'nyt_games', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wordles_played', + 'unique_id': '218886794-wordles_played', + 'unit_of_measurement': 'games', + }) +# --- +# name: test_all_entities[sensor.nytgames_wordles_played-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'NYTGames Wordles played', + 'state_class': , + 'unit_of_measurement': 'games', + }), + 'context': , + 'entity_id': 'sensor.nytgames_wordles_played', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '33', + }) +# --- diff --git a/tests/components/nyt_games/test_config_flow.py b/tests/components/nyt_games/test_config_flow.py new file mode 100644 index 00000000000..0cdd22aa96e --- /dev/null +++ b/tests/components/nyt_games/test_config_flow.py @@ -0,0 +1,104 @@ +"""Tests for the NYT Games config flow.""" + +from unittest.mock import AsyncMock + +from nyt_games import NYTGamesAuthenticationError, NYTGamesError +import pytest + +from homeassistant.components.nyt_games.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_TOKEN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + + +async def test_full_flow( + hass: HomeAssistant, + mock_nyt_games_client: AsyncMock, + mock_setup_entry: AsyncMock, +) -> None: + """Test full flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_TOKEN: "token"}, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "NYT Games" + assert result["data"] == {CONF_TOKEN: "token"} + assert result["result"].unique_id == "218886794" + + +@pytest.mark.parametrize( + ("exception", "error"), + [ + (NYTGamesAuthenticationError, "invalid_auth"), + (NYTGamesError, "cannot_connect"), + (Exception, "unknown"), + ], +) +async def test_flow_errors( + hass: HomeAssistant, + mock_nyt_games_client: AsyncMock, + mock_setup_entry: AsyncMock, + exception: Exception, + error: str, +) -> None: + """Test flow errors.""" + mock_nyt_games_client.get_latest_stats.side_effect = exception + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_TOKEN: "token"}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": error} + + mock_nyt_games_client.get_latest_stats.side_effect = None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_TOKEN: "token"}, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + + +async def test_duplicate( + hass: HomeAssistant, + mock_nyt_games_client: AsyncMock, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test duplicate flow.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_TOKEN: "token"}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" diff --git a/tests/components/nyt_games/test_init.py b/tests/components/nyt_games/test_init.py new file mode 100644 index 00000000000..e8286066319 --- /dev/null +++ b/tests/components/nyt_games/test_init.py @@ -0,0 +1,29 @@ +"""Tests for the NYT Games integration.""" + +from unittest.mock import AsyncMock + +from syrupy import SnapshotAssertion + +from homeassistant.components.nyt_games.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr + +from . import setup_integration + +from tests.common import MockConfigEntry + + +async def test_device_info( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_nyt_games_client: AsyncMock, + mock_config_entry: MockConfigEntry, + device_registry: dr.DeviceRegistry, +) -> None: + """Test device registry integration.""" + await setup_integration(hass, mock_config_entry) + device_entry = device_registry.async_get_device( + identifiers={(DOMAIN, mock_config_entry.unique_id)} + ) + assert device_entry is not None + assert device_entry == snapshot diff --git a/tests/components/nyt_games/test_sensor.py b/tests/components/nyt_games/test_sensor.py new file mode 100644 index 00000000000..198164b56f1 --- /dev/null +++ b/tests/components/nyt_games/test_sensor.py @@ -0,0 +1,55 @@ +"""Tests for the NYT Games sensor platform.""" + +from datetime import timedelta +from unittest.mock import AsyncMock + +from freezegun.api import FrozenDateTimeFactory +from nyt_games import NYTGamesError +from syrupy import SnapshotAssertion + +from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform + + +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_nyt_games_client: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +async def test_updating_exception( + hass: HomeAssistant, + mock_nyt_games_client: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test handling an exception during update.""" + await setup_integration(hass, mock_config_entry) + + mock_nyt_games_client.get_latest_stats.side_effect = NYTGamesError + + freezer.tick(timedelta(minutes=15)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert hass.states.get("sensor.nytgames_wordles_played").state == STATE_UNAVAILABLE + + mock_nyt_games_client.get_latest_stats.side_effect = None + + freezer.tick(timedelta(minutes=15)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert hass.states.get("sensor.nytgames_wordles_played").state != STATE_UNAVAILABLE