From b3a001996da9717c08b5c0fc71d1e220cf8fbce1 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 8 Jun 2023 22:55:16 +0200 Subject: [PATCH] Improve coverage for LastFM (#93661) * Improve coverage for LastFM * Improve tests * Improve tests --- .coveragerc | 1 - tests/components/lastfm/__init__.py | 69 +++++---- tests/components/lastfm/conftest.py | 67 +++++++++ tests/components/lastfm/test_config_flow.py | 155 ++++++++++---------- tests/components/lastfm/test_init.py | 28 ++-- tests/components/lastfm/test_sensor.py | 105 +++++++++++-- 6 files changed, 291 insertions(+), 134 deletions(-) create mode 100644 tests/components/lastfm/conftest.py diff --git a/.coveragerc b/.coveragerc index 5167decbf8a..c717c1624c3 100644 --- a/.coveragerc +++ b/.coveragerc @@ -618,7 +618,6 @@ omit = homeassistant/components/kwb/sensor.py homeassistant/components/lacrosse/sensor.py homeassistant/components/lannouncer/notify.py - homeassistant/components/lastfm/sensor.py homeassistant/components/launch_library/__init__.py homeassistant/components/launch_library/sensor.py homeassistant/components/lcn/climate.py diff --git a/tests/components/lastfm/__init__.py b/tests/components/lastfm/__init__.py index 7ee8665e28a..dde914d51cc 100644 --- a/tests/components/lastfm/__init__.py +++ b/tests/components/lastfm/__init__.py @@ -22,64 +22,83 @@ CONF_FRIENDS_DATA = {CONF_USERS: [USERNAME_2]} class MockNetwork: """Mock _Network object for pylast.""" - def __init__(self, username: str): + def __init__(self, username: str) -> None: """Initialize the mock.""" self.username = username +class MockTopTrack: + """Mock TopTrack object for pylast.""" + + def __init__(self, item: Track) -> None: + """Initialize the mock.""" + self.item = item + + +class MockLastTrack: + """Mock LastTrack object for pylast.""" + + def __init__(self, track: Track) -> None: + """Initialize the mock.""" + self.track = track + + class MockUser: """Mock User object for pylast.""" - def __init__(self, now_playing_result, error, has_friends, username): + def __init__( + self, + username: str = USERNAME_1, + now_playing_result: Track | None = None, + thrown_error: Exception | None = None, + friends: list = [], + recent_tracks: list[Track] = [], + top_tracks: list[Track] = [], + ) -> None: """Initialize the mock.""" self._now_playing_result = now_playing_result - self._thrown_error = error - self._has_friends = has_friends + self._thrown_error = thrown_error + self._friends = friends + self._recent_tracks = recent_tracks + self._top_tracks = top_tracks self.name = username def get_name(self, capitalized: bool) -> str: """Get name of the user.""" return self.name - def get_playcount(self): + def get_playcount(self) -> int: """Get mock play count.""" if self._thrown_error: raise self._thrown_error - return 1 + return len(self._recent_tracks) - def get_image(self): + def get_image(self) -> str: """Get mock image.""" + return "" - def get_recent_tracks(self, limit): + def get_recent_tracks(self, limit: int) -> list[MockLastTrack]: """Get mock recent tracks.""" - return [] + return [MockLastTrack(track) for track in self._recent_tracks] - def get_top_tracks(self, limit): + def get_top_tracks(self, limit: int) -> list[MockTopTrack]: """Get mock top tracks.""" - return [] + return [MockTopTrack(track) for track in self._recent_tracks] - def get_now_playing(self): + def get_now_playing(self) -> Track: """Get mock now playing.""" return self._now_playing_result - def get_friends(self): + def get_friends(self) -> list[any]: """Get mock friends.""" - if self._has_friends is False: + if len(self._friends) == 0: raise PyLastError("network", "status", "Page not found") - return [MockUser(None, None, True, USERNAME_2)] + return self._friends -def patch_fetch_user( - now_playing: Track | None = None, - thrown_error: Exception | None = None, - has_friends: bool = True, - username: str = USERNAME_1, -) -> MockUser: +def patch_user(user: MockUser) -> MockUser: """Patch interface.""" - return patch( - "pylast.User", - return_value=MockUser(now_playing, thrown_error, has_friends, username), - ) + return patch("pylast.User", return_value=user) def patch_setup_entry() -> bool: diff --git a/tests/components/lastfm/conftest.py b/tests/components/lastfm/conftest.py new file mode 100644 index 00000000000..119d4796f57 --- /dev/null +++ b/tests/components/lastfm/conftest.py @@ -0,0 +1,67 @@ +"""Configure tests for the LastFM integration.""" +from collections.abc import Awaitable, Callable +from unittest.mock import patch + +from pylast import Track +import pytest + +from homeassistant.components.lastfm.const import CONF_MAIN_USER, CONF_USERS, DOMAIN +from homeassistant.const import CONF_API_KEY +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry +from tests.components.lastfm import ( + API_KEY, + USERNAME_1, + USERNAME_2, + MockNetwork, + MockUser, +) + +ComponentSetup = Callable[[MockConfigEntry, MockUser], Awaitable[None]] + + +@pytest.fixture(name="config_entry") +def mock_config_entry() -> MockConfigEntry: + """Create LastFM entry in Home Assistant.""" + return MockConfigEntry( + domain=DOMAIN, + data={}, + options={ + CONF_API_KEY: API_KEY, + CONF_MAIN_USER: USERNAME_1, + CONF_USERS: [USERNAME_1, USERNAME_2], + }, + ) + + +@pytest.fixture(name="setup_integration") +async def mock_setup_integration( + hass: HomeAssistant, +) -> Callable[[MockConfigEntry, MockUser], Awaitable[None]]: + """Fixture for setting up the component.""" + + async def func(mock_config_entry: MockConfigEntry, mock_user: MockUser) -> None: + mock_config_entry.add_to_hass(hass) + with patch("pylast.User", return_value=mock_user): + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + return func + + +@pytest.fixture(name="default_user") +def mock_default_user() -> MockUser: + """Return default mock user.""" + return MockUser( + now_playing_result=Track("artist", "title", MockNetwork("lastfm")), + top_tracks=[Track("artist", "title", MockNetwork("lastfm"))], + recent_tracks=[Track("artist", "title", MockNetwork("lastfm"))], + ) + + +@pytest.fixture(name="first_time_user") +def mock_first_time_user() -> MockUser: + """Return first time mock user.""" + return MockUser(now_playing_result=None, top_tracks=[], recent_tracks=[]) diff --git a/tests/components/lastfm/test_config_flow.py b/tests/components/lastfm/test_config_flow.py index 02168449398..ce28638c3f3 100644 --- a/tests/components/lastfm/test_config_flow.py +++ b/tests/components/lastfm/test_config_flow.py @@ -1,4 +1,6 @@ """Test Lastfm config flow.""" +from unittest.mock import patch + from pylast import WSError import pytest @@ -21,16 +23,17 @@ from . import ( CONF_USER_DATA, USERNAME_1, USERNAME_2, - patch_fetch_user, + MockUser, patch_setup_entry, ) +from .conftest import ComponentSetup from tests.common import MockConfigEntry -async def test_full_user_flow(hass: HomeAssistant) -> None: +async def test_full_user_flow(hass: HomeAssistant, default_user: MockUser) -> None: """Test the full user configuration flow.""" - with patch_fetch_user(), patch_setup_entry(): + with patch("pylast.User", return_value=default_user), patch_setup_entry(): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, @@ -68,9 +71,11 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: (WSError("network", "status", "Something strange"), "unknown"), ], ) -async def test_flow_fails(hass: HomeAssistant, error: Exception, message: str) -> None: +async def test_flow_fails( + hass: HomeAssistant, error: Exception, message: str, default_user: MockUser +) -> None: """Test user initialized flow with invalid username.""" - with patch_fetch_user(thrown_error=error): + with patch("pylast.User", return_value=MockUser(thrown_error=error)): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_USER_DATA ) @@ -78,7 +83,7 @@ async def test_flow_fails(hass: HomeAssistant, error: Exception, message: str) - assert result["step_id"] == "user" assert result["errors"]["base"] == message - with patch_fetch_user(), patch_setup_entry(): + with patch("pylast.User", return_value=default_user), patch_setup_entry(): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=CONF_USER_DATA, @@ -95,9 +100,11 @@ async def test_flow_fails(hass: HomeAssistant, error: Exception, message: str) - assert result["options"] == CONF_DATA -async def test_flow_friends_invalid_username(hass: HomeAssistant) -> None: +async def test_flow_friends_invalid_username( + hass: HomeAssistant, default_user: MockUser +) -> None: """Test user initialized flow with invalid username.""" - with patch_fetch_user(), patch_setup_entry(): + with patch("pylast.User", return_value=default_user), patch_setup_entry(): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, @@ -109,7 +116,12 @@ async def test_flow_friends_invalid_username(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "friends" - with patch_fetch_user(thrown_error=WSError("network", "status", "User not found")): + with patch( + "pylast.User", + return_value=MockUser( + thrown_error=WSError("network", "status", "User not found") + ), + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=CONF_FRIENDS_DATA ) @@ -117,7 +129,7 @@ async def test_flow_friends_invalid_username(hass: HomeAssistant) -> None: assert result["step_id"] == "friends" assert result["errors"]["base"] == "invalid_account" - with patch_fetch_user(), patch_setup_entry(): + with patch("pylast.User", return_value=default_user), patch_setup_entry(): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=CONF_FRIENDS_DATA ) @@ -126,9 +138,11 @@ async def test_flow_friends_invalid_username(hass: HomeAssistant) -> None: assert result["options"] == CONF_DATA -async def test_flow_friends_no_friends(hass: HomeAssistant) -> None: +async def test_flow_friends_no_friends( + hass: HomeAssistant, default_user: MockUser +) -> None: """Test options is empty when user has no friends.""" - with patch_fetch_user(has_friends=False), patch_setup_entry(): + with patch("pylast.User", return_value=default_user), patch_setup_entry(): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, @@ -142,9 +156,9 @@ async def test_flow_friends_no_friends(hass: HomeAssistant) -> None: assert len(result["data_schema"].schema[CONF_USERS].config["options"]) == 0 -async def test_import_flow_success(hass: HomeAssistant) -> None: +async def test_import_flow_success(hass: HomeAssistant, default_user: MockUser) -> None: """Test import flow.""" - with patch_fetch_user(): + with patch("pylast.User", return_value=default_user): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, @@ -160,16 +174,16 @@ async def test_import_flow_success(hass: HomeAssistant) -> None: } -async def test_import_flow_already_exist(hass: HomeAssistant) -> None: +async def test_import_flow_already_exist( + hass: HomeAssistant, + setup_integration: ComponentSetup, + config_entry: MockConfigEntry, + default_user: MockUser, +) -> None: """Test import of yaml already exist.""" + await setup_integration(config_entry, default_user) - MockConfigEntry( - domain=DOMAIN, - data={}, - options={CONF_API_KEY: API_KEY, CONF_USERS: ["test"]}, - ).add_to_hass(hass) - - with patch_fetch_user(): + with patch("pylast.User", return_value=default_user): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, @@ -181,20 +195,16 @@ async def test_import_flow_already_exist(hass: HomeAssistant) -> None: assert result["reason"] == "already_configured" -async def test_options_flow(hass: HomeAssistant) -> None: +async def test_options_flow( + hass: HomeAssistant, + setup_integration: ComponentSetup, + config_entry: MockConfigEntry, + default_user: MockUser, +) -> None: """Test updating options.""" - entry = MockConfigEntry( - domain=DOMAIN, - data={}, - options={ - CONF_API_KEY: API_KEY, - CONF_MAIN_USER: USERNAME_1, - CONF_USERS: [USERNAME_1, USERNAME_2], - }, - ) - entry.add_to_hass(hass) - with patch_fetch_user(): - await hass.config_entries.async_setup(entry.entry_id) + await setup_integration(config_entry, default_user) + with patch("pylast.User", return_value=default_user): + entry = hass.config_entries.async_entries(DOMAIN)[0] result = await hass.config_entries.options.async_init(entry.entry_id) await hass.async_block_till_done() @@ -215,27 +225,28 @@ async def test_options_flow(hass: HomeAssistant) -> None: } -async def test_options_flow_incorrect_username(hass: HomeAssistant) -> None: +async def test_options_flow_incorrect_username( + hass: HomeAssistant, + setup_integration: ComponentSetup, + config_entry: MockConfigEntry, + default_user: MockUser, +) -> None: """Test updating options doesn't work with incorrect username.""" - entry = MockConfigEntry( - domain=DOMAIN, - data={}, - options={ - CONF_API_KEY: API_KEY, - CONF_MAIN_USER: USERNAME_1, - CONF_USERS: [USERNAME_1], - }, - ) - entry.add_to_hass(hass) - with patch_fetch_user(): - await hass.config_entries.async_setup(entry.entry_id) + await setup_integration(config_entry, default_user) + with patch("pylast.User", return_value=default_user): + entry = hass.config_entries.async_entries(DOMAIN)[0] result = await hass.config_entries.options.async_init(entry.entry_id) await hass.async_block_till_done() assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" - with patch_fetch_user(thrown_error=WSError("network", "status", "User not found")): + with patch( + "pylast.User", + return_value=MockUser( + thrown_error=WSError("network", "status", "User not found") + ), + ): result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_USERS: [USERNAME_1]}, @@ -246,7 +257,7 @@ async def test_options_flow_incorrect_username(hass: HomeAssistant) -> None: assert result["step_id"] == "init" assert result["errors"]["base"] == "invalid_account" - with patch_fetch_user(): + with patch("pylast.User", return_value=default_user): result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_USERS: [USERNAME_1]}, @@ -261,20 +272,16 @@ async def test_options_flow_incorrect_username(hass: HomeAssistant) -> None: } -async def test_options_flow_from_import(hass: HomeAssistant) -> None: +async def test_options_flow_from_import( + hass: HomeAssistant, + setup_integration: ComponentSetup, + config_entry: MockConfigEntry, + default_user: MockUser, +) -> None: """Test updating options gained from import.""" - entry = MockConfigEntry( - domain=DOMAIN, - data={}, - options={ - CONF_API_KEY: API_KEY, - CONF_MAIN_USER: None, - CONF_USERS: [USERNAME_1], - }, - ) - entry.add_to_hass(hass) - with patch_fetch_user(): - await hass.config_entries.async_setup(entry.entry_id) + await setup_integration(config_entry, default_user) + with patch("pylast.User", return_value=default_user): + entry = hass.config_entries.async_entries(DOMAIN)[0] result = await hass.config_entries.options.async_init(entry.entry_id) await hass.async_block_till_done() @@ -283,20 +290,16 @@ async def test_options_flow_from_import(hass: HomeAssistant) -> None: assert len(result["data_schema"].schema[CONF_USERS].config["options"]) == 0 -async def test_options_flow_without_friends(hass: HomeAssistant) -> None: +async def test_options_flow_without_friends( + hass: HomeAssistant, + setup_integration: ComponentSetup, + config_entry: MockConfigEntry, + default_user: MockUser, +) -> None: """Test updating options for someone without friends.""" - entry = MockConfigEntry( - domain=DOMAIN, - data={}, - options={ - CONF_API_KEY: API_KEY, - CONF_MAIN_USER: USERNAME_1, - CONF_USERS: [USERNAME_1], - }, - ) - entry.add_to_hass(hass) - with patch_fetch_user(has_friends=False): - await hass.config_entries.async_setup(entry.entry_id) + await setup_integration(config_entry, default_user) + with patch("pylast.User", return_value=default_user): + entry = hass.config_entries.async_entries(DOMAIN)[0] result = await hass.config_entries.options.async_init(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/lastfm/test_init.py b/tests/components/lastfm/test_init.py index 832494f28de..8f731385e6f 100644 --- a/tests/components/lastfm/test_init.py +++ b/tests/components/lastfm/test_init.py @@ -1,30 +1,24 @@ """Test LastFM component setup process.""" from __future__ import annotations -from homeassistant.components.lastfm.const import CONF_MAIN_USER, CONF_USERS, DOMAIN -from homeassistant.const import CONF_API_KEY +from homeassistant.components.lastfm.const import DOMAIN from homeassistant.core import HomeAssistant -from . import USERNAME_1, USERNAME_2, patch_fetch_user +from . import MockUser +from .conftest import ComponentSetup from tests.common import MockConfigEntry -async def test_load_unload_entry(hass: HomeAssistant) -> None: +async def test_load_unload_entry( + hass: HomeAssistant, + setup_integration: ComponentSetup, + config_entry: MockConfigEntry, + default_user: MockUser, +) -> None: """Test load and unload entry.""" - entry = MockConfigEntry( - domain=DOMAIN, - data={}, - options={ - CONF_API_KEY: "12345678", - CONF_MAIN_USER: [USERNAME_1], - CONF_USERS: [USERNAME_1, USERNAME_2], - }, - ) - entry.add_to_hass(hass) - with patch_fetch_user(): - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + await setup_integration(config_entry, default_user) + entry = hass.config_entries.async_entries(DOMAIN)[0] state = hass.states.get("sensor.testaccount1") assert state diff --git a/tests/components/lastfm/test_sensor.py b/tests/components/lastfm/test_sensor.py index 06e8e812ca7..e46cf99ffdc 100644 --- a/tests/components/lastfm/test_sensor.py +++ b/tests/components/lastfm/test_sensor.py @@ -1,22 +1,92 @@ """Tests for the lastfm sensor.""" +from unittest.mock import patch -from pylast import Track +from pylast import WSError -from homeassistant.components.lastfm.const import DOMAIN, STATE_NOT_SCROBBLING +from homeassistant.components.lastfm.const import ( + ATTR_LAST_PLAYED, + ATTR_PLAY_COUNT, + ATTR_TOP_PLAYED, + CONF_USERS, + DOMAIN, + STATE_NOT_SCROBBLING, +) +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_API_KEY, CONF_PLATFORM, Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir +from homeassistant.setup import async_setup_component -from . import CONF_DATA, MockNetwork, patch_fetch_user +from . import API_KEY, USERNAME_1, MockUser +from .conftest import ComponentSetup from tests.common import MockConfigEntry +LEGACY_CONFIG = { + Platform.SENSOR: [ + {CONF_PLATFORM: DOMAIN, CONF_API_KEY: API_KEY, CONF_USERS: [USERNAME_1]} + ] +} -async def test_update_not_playing(hass: HomeAssistant) -> None: - """Test update when no playing song.""" - entry = MockConfigEntry(domain=DOMAIN, data={}, options=CONF_DATA) - entry.add_to_hass(hass) - with patch_fetch_user(None): - await hass.config_entries.async_setup(entry.entry_id) + +async def test_legacy_migration(hass: HomeAssistant) -> None: + """Test migration from yaml to config flow.""" + with patch("pylast.User", return_value=None): + assert await async_setup_component(hass, Platform.SENSOR, LEGACY_CONFIG) await hass.async_block_till_done() + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].state is ConfigEntryState.LOADED + issue_registry = ir.async_get(hass) + assert len(issue_registry.issues) == 1 + + +async def test_user_unavailable( + hass: HomeAssistant, + setup_integration: ComponentSetup, + config_entry: MockConfigEntry, +) -> None: + """Test update when user can't be fetched.""" + await setup_integration( + config_entry, + MockUser(thrown_error=WSError("network", "status", "User not found")), + ) + + entity_id = "sensor.testaccount1" + + state = hass.states.get(entity_id) + + assert state.state == "unavailable" + + +async def test_first_time_user( + hass: HomeAssistant, + setup_integration: ComponentSetup, + config_entry: MockConfigEntry, + first_time_user: MockUser, +) -> None: + """Test first time user.""" + await setup_integration(config_entry, first_time_user) + + entity_id = "sensor.testaccount1" + + state = hass.states.get(entity_id) + + assert state.state == STATE_NOT_SCROBBLING + assert state.attributes[ATTR_LAST_PLAYED] is None + assert state.attributes[ATTR_TOP_PLAYED] is None + assert state.attributes[ATTR_PLAY_COUNT] == 0 + + +async def test_update_not_playing( + hass: HomeAssistant, + setup_integration: ComponentSetup, + config_entry: MockConfigEntry, + first_time_user: MockUser, +) -> None: + """Test update when no playing song.""" + await setup_integration(config_entry, first_time_user) + entity_id = "sensor.testaccount1" state = hass.states.get(entity_id) @@ -24,15 +94,20 @@ async def test_update_not_playing(hass: HomeAssistant) -> None: assert state.state == STATE_NOT_SCROBBLING -async def test_update_playing(hass: HomeAssistant) -> None: +async def test_update_playing( + hass: HomeAssistant, + setup_integration: ComponentSetup, + config_entry: MockConfigEntry, + default_user: MockUser, +) -> None: """Test update when playing a song.""" - entry = MockConfigEntry(domain=DOMAIN, data={}, options=CONF_DATA) - entry.add_to_hass(hass) - with patch_fetch_user(Track("artist", "title", MockNetwork("test"))): - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + await setup_integration(config_entry, default_user) + entity_id = "sensor.testaccount1" state = hass.states.get(entity_id) assert state.state == "artist - title" + assert state.attributes[ATTR_LAST_PLAYED] == "artist - title" + assert state.attributes[ATTR_TOP_PLAYED] == "artist - title" + assert state.attributes[ATTR_PLAY_COUNT] == 1