Improve coverage for LastFM (#93661)

* Improve coverage for LastFM

* Improve tests

* Improve tests
This commit is contained in:
Joost Lekkerkerker 2023-06-08 22:55:16 +02:00 committed by GitHub
parent ca936d0b38
commit b3a001996d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 291 additions and 134 deletions

View File

@ -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

View File

@ -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:

View File

@ -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=[])

View File

@ -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()

View File

@ -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

View File

@ -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