Add config flow to Discogs integration (#180531)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
Zack Wagner
2026-09-14 21:41:21 +02:00
committed by GitHub
co-authored by Claude Opus 4.6 Joost Lekkerkerker
parent 1aab95f9b5
commit 15b89b53a3
17 changed files with 826 additions and 46 deletions
+1
View File
@@ -173,6 +173,7 @@ homeassistant.components.devolo_home_control.*
homeassistant.components.devolo_home_network.*
homeassistant.components.dhcp.*
homeassistant.components.diagnostics.*
homeassistant.components.discogs.*
homeassistant.components.discovergy.*
homeassistant.components.dlna_dmr.*
homeassistant.components.dlna_dms.*
Generated
+1
View File
@@ -403,6 +403,7 @@ CLAUDE.md @home-assistant/core
/tests/components/diagnostics/ @home-assistant/core
/homeassistant/components/digital_ocean/ @fabaff
/homeassistant/components/discogs/ @thibmaek
/tests/components/discogs/ @thibmaek
/homeassistant/components/discord/ @tkdrob
/tests/components/discord/ @tkdrob
/homeassistant/components/discovergy/ @jpbede
+39 -1
View File
@@ -1 +1,39 @@
"""The discogs component."""
"""The Discogs integration."""
import discogs_client
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_TOKEN
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import SERVER_SOFTWARE
from .const import PLATFORMS
type DiscogsConfigEntry = ConfigEntry[discogs_client.Client]
async def async_setup_entry(hass: HomeAssistant, entry: DiscogsConfigEntry) -> bool:
"""Set up Discogs from a config entry."""
def _setup_client() -> discogs_client.Client:
client = discogs_client.Client(
SERVER_SOFTWARE, user_token=entry.data[CONF_TOKEN]
)
client.identity()
return client
try:
client = await hass.async_add_executor_job(_setup_client)
except discogs_client.exceptions.HTTPError as err:
raise ConfigEntryNotReady(f"Error communicating with Discogs: {err}") from err
entry.runtime_data = client
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: DiscogsConfigEntry) -> bool:
"""Unload Discogs config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
@@ -0,0 +1,83 @@
"""Config flow for Discogs."""
from typing import Any, override
import discogs_client
import probatio
import requests
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_TOKEN
from homeassistant.helpers.aiohttp_client import SERVER_SOFTWARE
from .const import DOMAIN, LOGGER
CONFIG_SCHEMA = probatio.Schema(
{
probatio.Required(CONF_TOKEN): str,
}
)
class DiscogsConfigFlow(ConfigFlow, domain=DOMAIN):
"""Config flow for Discogs."""
@override
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 is not None:
user_id, username, errors = await self.hass.async_add_executor_job(
_validate_token, user_input[CONF_TOKEN]
)
if not errors:
await self.async_set_unique_id(str(user_id))
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=username,
data={CONF_TOKEN: user_input[CONF_TOKEN]},
)
return self.async_show_form(
step_id="user",
data_schema=self.add_suggested_values_to_schema(CONFIG_SCHEMA, user_input),
errors=errors,
)
async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
"""Handle import from YAML configuration."""
user_id, username, errors = await self.hass.async_add_executor_job(
_validate_token, import_data[CONF_TOKEN]
)
if errors:
return self.async_abort(reason=errors["base"])
await self.async_set_unique_id(str(user_id))
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=import_data.get("name") or username,
data={CONF_TOKEN: import_data[CONF_TOKEN]},
)
def _validate_token(token: str) -> tuple[int | None, str, dict[str, str]]:
"""Validate the token and return the user ID, username, and errors."""
errors: dict[str, str] = {}
user_id = None
username = ""
try:
client = discogs_client.Client(SERVER_SOFTWARE, user_token=token)
identity = client.identity()
user_id = identity.id
username = identity.name
except discogs_client.exceptions.HTTPError as err:
if err.status_code == 401:
errors["base"] = "invalid_auth"
else:
errors["base"] = "cannot_connect"
except requests.RequestException:
errors["base"] = "cannot_connect"
except Exception: # noqa: BLE001
LOGGER.exception("Unexpected error validating Discogs token")
errors["base"] = "unknown"
return user_id, username, errors
+11
View File
@@ -0,0 +1,11 @@
"""Constants for Discogs."""
import logging
from typing import Final
from homeassistant.const import Platform
LOGGER = logging.getLogger(__package__)
DOMAIN: Final = "discogs"
PLATFORMS = [Platform.SENSOR]
DEFAULT_NAME = "Discogs"
@@ -2,7 +2,9 @@
"domain": "discogs",
"name": "Discogs",
"codeowners": ["@thibmaek"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/discogs",
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["discogs_client"],
"quality_scale": "legacy",
+98 -43
View File
@@ -1,7 +1,6 @@
"""Show the amount of records in a user's Discogs collection."""
from datetime import timedelta
import logging
import random
from typing import Any, override
@@ -13,19 +12,23 @@ from homeassistant.components.sensor import (
SensorEntity,
SensorEntityDescription,
)
from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_TOKEN
from homeassistant.core import HomeAssistant
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_TOKEN
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import SERVER_SOFTWARE
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import (
AddConfigEntryEntitiesCallback,
AddEntitiesCallback,
)
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
_LOGGER = logging.getLogger(__name__)
from .const import DEFAULT_NAME, DOMAIN
ATTR_IDENTITY = "identity"
DEFAULT_NAME = "Discogs"
ICON_RECORD = "mdi:album"
ICON_PLAYER = "mdi:record-player"
UNIT_RECORDS = "records"
@@ -39,19 +42,19 @@ SENSOR_RANDOM_RECORD_TYPE = "random_record"
SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription(
key=SENSOR_COLLECTION_TYPE,
name="Collection",
translation_key="collection",
icon=ICON_RECORD,
native_unit_of_measurement=UNIT_RECORDS,
),
SensorEntityDescription(
key=SENSOR_WANTLIST_TYPE,
name="Wantlist",
translation_key="wantlist",
icon=ICON_RECORD,
native_unit_of_measurement=UNIT_RECORDS,
),
SensorEntityDescription(
key=SENSOR_RANDOM_RECORD_TYPE,
name="Random Record",
translation_key="random_record",
icon=ICON_PLAYER,
),
)
@@ -60,61 +63,105 @@ SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES]
PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
{
probatio.Required(CONF_TOKEN): cv.string,
probatio.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
probatio.Optional(CONF_MONITORED_CONDITIONS, default=SENSOR_KEYS): probatio.All(
cv.ensure_list, [probatio.In(SENSOR_KEYS)]
probatio.Optional("name"): cv.string,
probatio.Optional("monitored_conditions"): probatio.All(
cv.ensure_list, [cv.string]
),
}
)
def setup_platform(
async def async_setup_platform(
hass: HomeAssistant,
config: ConfigType,
add_entities: AddEntitiesCallback,
async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Discogs sensor."""
token = config[CONF_TOKEN]
name = config[CONF_NAME]
"""Import YAML configuration and forward to config flow."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": "import"},
data={
CONF_TOKEN: config[CONF_TOKEN],
"name": config.get("name", DEFAULT_NAME),
},
)
try:
_discogs_client = discogs_client.Client(SERVER_SOFTWARE, user_token=token)
discogs_data = {
"user": _discogs_client.identity().name,
"folders": _discogs_client.identity().collection_folders,
"collection_count": _discogs_client.identity().num_collection,
"wantlist_count": _discogs_client.identity().num_wantlist,
}
except discogs_client.exceptions.HTTPError:
_LOGGER.error("API token is not valid")
if (
result.get("type") is FlowResultType.ABORT
and result.get("reason") != "already_configured"
):
async_create_issue(
hass,
DOMAIN,
"deprecated_yaml_import_issue_cannot_connect",
breaks_in_ha_version="2027.2.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_yaml_import_issue_cannot_connect",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "Discogs",
},
)
return
monitored_conditions = config[CONF_MONITORED_CONDITIONS]
entities = [
DiscogsSensor(discogs_data, name, description)
for description in SENSOR_TYPES
if description.key in monitored_conditions
]
async_create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_yaml_{DOMAIN}",
breaks_in_ha_version="2027.2.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_yaml",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "Discogs",
},
)
add_entities(entities, True)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Discogs sensor from a config entry."""
client = entry.runtime_data
async_add_entities(
(DiscogsSensor(entry, client, description) for description in SENSOR_TYPES),
True,
)
class DiscogsSensor(SensorEntity):
"""Create a new Discogs sensor for a specific type."""
_attr_attribution = "Data provided by Discogs"
_attr_has_entity_name = True
def __init__(
self, discogs_data, name, description: SensorEntityDescription
self,
entry: ConfigEntry,
client: discogs_client.Client,
description: SensorEntityDescription,
) -> None:
"""Initialize the Discogs sensor."""
assert entry.unique_id is not None
self.entity_description = description
self._discogs_data = discogs_data
self._attrs: dict = {}
self._attr_name = f"{name} {description.name}"
self._client = client
self._discogs_data: dict[str, Any] = {}
self._attrs: dict[str, Any] = {}
self._attr_unique_id = f"{entry.unique_id}_{description.key}"
self._attr_device_info = DeviceInfo(
configuration_url="https://www.discogs.com",
entry_type=DeviceEntryType.SERVICE,
identifiers={(DOMAIN, entry.unique_id)},
manufacturer=DEFAULT_NAME,
name=entry.title,
)
@property
@override
@@ -143,7 +190,7 @@ class DiscogsSensor(SensorEntity):
ATTR_IDENTITY: self._discogs_data["user"],
}
def get_random_record(self):
def get_random_record(self) -> str | None:
"""Get a random record suggestion from the user's collection."""
# Index 0 in the folders is the 'All' folder
collection = self._discogs_data["folders"][0]
@@ -161,6 +208,14 @@ class DiscogsSensor(SensorEntity):
def update(self) -> None:
"""Set state to the amount of records in user's collection."""
identity = self._client.identity()
self._discogs_data = {
"user": identity.name,
"folders": identity.collection_folders,
"collection_count": identity.num_collection,
"wantlist_count": identity.num_wantlist,
}
if self.entity_description.key == SENSOR_COLLECTION_TYPE:
self._attr_native_value = self._discogs_data["collection_count"]
elif self.entity_description.key == SENSOR_WANTLIST_TYPE:
@@ -0,0 +1,44 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
"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%]"
},
"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%]"
},
"step": {
"user": {
"data": {
"token": "[%key:common::config_flow::data::api_token%]"
},
"data_description": {
"token": "Personal access token from Discogs developer settings."
}
}
}
},
"entity": {
"sensor": {
"collection": {
"name": "Collection"
},
"random_record": {
"name": "Random record"
},
"wantlist": {
"name": "Wantlist"
}
}
},
"issues": {
"deprecated_yaml_import_issue_cannot_connect": {
"description": "Configuring {integration_title} via YAML is deprecated and will be removed in a future release. While importing your YAML configuration, the Discogs API could not be reached or the token was invalid. Please verify your token and restart Home Assistant to retry, or remove the {domain} sensor from your configuration and set up the integration via the UI.",
"title": "The {integration_title} YAML configuration is being removed"
}
}
}
+1
View File
@@ -163,6 +163,7 @@ FLOWS = {
"dexcom",
"dialogflow",
"directv",
"discogs",
"discord",
"discovergy",
"dlink",
+2 -2
View File
@@ -1444,8 +1444,8 @@
},
"discogs": {
"name": "Discogs",
"integration_type": "hub",
"config_flow": false,
"integration_type": "service",
"config_flow": true,
"iot_class": "cloud_polling"
},
"discord": {
Generated
+10
View File
@@ -1488,6 +1488,16 @@ warn_return_any = true
warn_unreachable = true
no_implicit_reexport = true
[mypy-homeassistant.components.discogs.*]
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.discovergy.*]
check_untyped_defs = true
disallow_incomplete_defs = true
+5
View File
@@ -0,0 +1,5 @@
"""Tests for the Discogs integration."""
MOCK_TOKEN = "test_token_123"
MOCK_USERNAME = "testuser"
MOCK_USER_ID = 12345
+99
View File
@@ -0,0 +1,99 @@
"""Configure tests for the Discogs integration."""
from collections.abc import Generator
from unittest.mock import MagicMock, patch
import pytest
from homeassistant.components.discogs.const import DOMAIN
from homeassistant.const import CONF_TOKEN
from homeassistant.core import HomeAssistant
from . import MOCK_TOKEN, MOCK_USER_ID, MOCK_USERNAME
from tests.common import MockConfigEntry
@pytest.fixture
def mock_config_entry() -> MockConfigEntry:
"""Create a mock Discogs config entry."""
return MockConfigEntry(
domain=DOMAIN,
title=MOCK_USERNAME,
data={CONF_TOKEN: MOCK_TOKEN},
unique_id=str(MOCK_USER_ID),
)
@pytest.fixture
def mock_setup_entry() -> Generator[None]:
"""Override async_setup_entry."""
with patch(
"homeassistant.components.discogs.async_setup_entry",
return_value=True,
):
yield
@pytest.fixture
def mock_discogs_data() -> dict:
"""Return mock Discogs data for a random record."""
return {
"artists": [{"name": "Artist Name"}],
"title": "Album Title",
"labels": [{"catno": "CAT001", "name": "Label Name"}],
"cover_image": "https://example.com/cover.jpg",
"formats": [{"name": "Vinyl", "descriptions": ["LP", "Album"]}],
"year": "2023",
}
@pytest.fixture
def mock_identity(mock_discogs_data: dict) -> MagicMock:
"""Return a mock Discogs identity."""
identity = MagicMock()
identity.id = MOCK_USER_ID
identity.name = MOCK_USERNAME
identity.num_collection = 42
identity.num_wantlist = 10
release = MagicMock()
release.release.data = mock_discogs_data
folder = MagicMock()
folder.count = 42
folder.releases.__getitem__ = MagicMock(return_value=release)
identity.collection_folders = [folder]
return identity
@pytest.fixture
def mock_discogs_client(mock_identity: MagicMock) -> Generator[MagicMock]:
"""Mock a Discogs client."""
with (
patch(
"homeassistant.components.discogs.discogs_client.Client",
autospec=True,
) as mock_client,
patch(
"homeassistant.components.discogs.config_flow.discogs_client.Client",
new=mock_client,
),
):
client = mock_client.return_value
client.identity.return_value = mock_identity
yield client
@pytest.fixture
async def setup_integration(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_discogs_client: MagicMock,
) -> MockConfigEntry:
"""Set up the Discogs integration for testing."""
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
return mock_config_entry
@@ -0,0 +1,167 @@
# serializer version: 1
# name: test_sensors[sensor.testuser_collection-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.testuser_collection',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Collection',
'options': dict({
}),
'original_device_class': None,
'original_icon': 'mdi:album',
'original_name': 'Collection',
'platform': 'discogs',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'collection',
'unique_id': '12345_collection',
'unit_of_measurement': 'records',
})
# ---
# name: test_sensors[sensor.testuser_collection-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.ATTRIBUTION: 'attribution'>: 'Data provided by Discogs',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'testuser Collection',
<EntityStateAttribute.ICON: 'icon'>: 'mdi:album',
'identity': 'testuser',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'records',
}),
'context': <ANY>,
'entity_id': 'sensor.testuser_collection',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '42',
})
# ---
# name: test_sensors[sensor.testuser_random_record-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.testuser_random_record',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Random record',
'options': dict({
}),
'original_device_class': None,
'original_icon': 'mdi:record-player',
'original_name': 'Random record',
'platform': 'discogs',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'random_record',
'unique_id': '12345_random_record',
'unit_of_measurement': None,
})
# ---
# name: test_sensors[sensor.testuser_random_record-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.ATTRIBUTION: 'attribution'>: 'Data provided by Discogs',
'cat_no': 'CAT001',
'cover_image': 'https://example.com/cover.jpg',
'format': 'Vinyl (LP)',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'testuser Random record',
<EntityStateAttribute.ICON: 'icon'>: 'mdi:record-player',
'identity': 'testuser',
'label': 'Label Name',
'released': '2023',
}),
'context': <ANY>,
'entity_id': 'sensor.testuser_random_record',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'Artist Name - Album Title',
})
# ---
# name: test_sensors[sensor.testuser_wantlist-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.testuser_wantlist',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Wantlist',
'options': dict({
}),
'original_device_class': None,
'original_icon': 'mdi:album',
'original_name': 'Wantlist',
'platform': 'discogs',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'wantlist',
'unique_id': '12345_wantlist',
'unit_of_measurement': 'records',
})
# ---
# name: test_sensors[sensor.testuser_wantlist-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.ATTRIBUTION: 'attribution'>: 'Data provided by Discogs',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'testuser Wantlist',
<EntityStateAttribute.ICON: 'icon'>: 'mdi:album',
'identity': 'testuser',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'records',
}),
'context': <ANY>,
'entity_id': 'sensor.testuser_wantlist',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '10',
})
# ---
@@ -0,0 +1,150 @@
"""Test Discogs config flow."""
from unittest.mock import MagicMock
import discogs_client
import pytest
import requests
from homeassistant.components.discogs.const import DOMAIN
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER
from homeassistant.const import CONF_TOKEN
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from . import MOCK_TOKEN, MOCK_USER_ID, MOCK_USERNAME
from tests.common import MockConfigEntry
@pytest.mark.usefixtures("mock_setup_entry")
async def test_full_user_flow(
hass: HomeAssistant, mock_discogs_client: MagicMock
) -> None:
"""Test the full user configuration 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"],
user_input={CONF_TOKEN: MOCK_TOKEN},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == MOCK_USERNAME
assert result["data"] == {CONF_TOKEN: MOCK_TOKEN}
assert result["result"].unique_id == str(MOCK_USER_ID)
@pytest.mark.parametrize(
("error", "message"),
[
(discogs_client.exceptions.HTTPError("Unauthorized", 401), "invalid_auth"),
(discogs_client.exceptions.HTTPError("Rate Limited", 429), "cannot_connect"),
(requests.ConnectionError("Connection refused"), "cannot_connect"),
(requests.Timeout("Request timed out"), "cannot_connect"),
(RuntimeError("Something went wrong"), "unknown"),
],
)
@pytest.mark.usefixtures("mock_setup_entry")
async def test_flow_errors_then_success(
hass: HomeAssistant,
mock_discogs_client: MagicMock,
error: Exception,
message: str,
) -> None:
"""Test that errors can be recovered from."""
mock_discogs_client.identity.side_effect = error
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_USER},
)
assert result["type"] is FlowResultType.FORM
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_TOKEN: "bad_token"},
)
assert result["type"] is FlowResultType.FORM
assert result["errors"]["base"] == message
mock_discogs_client.identity.side_effect = None
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_TOKEN: MOCK_TOKEN},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
@pytest.mark.usefixtures("mock_setup_entry")
async def test_flow_already_configured(
hass: HomeAssistant,
mock_discogs_client: MagicMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test flow aborts when account is already configured."""
mock_config_entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_USER},
)
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_TOKEN: MOCK_TOKEN},
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"
@pytest.mark.usefixtures("mock_setup_entry")
async def test_import_flow(hass: HomeAssistant, mock_discogs_client: MagicMock) -> None:
"""Test YAML import creates a config entry."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data={CONF_TOKEN: MOCK_TOKEN},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == MOCK_USERNAME
assert result["data"] == {CONF_TOKEN: MOCK_TOKEN}
assert result["result"].unique_id == str(MOCK_USER_ID)
@pytest.mark.usefixtures("mock_setup_entry")
async def test_import_flow_with_name(
hass: HomeAssistant, mock_discogs_client: MagicMock
) -> None:
"""Test YAML import preserves custom name as entry title."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data={CONF_TOKEN: MOCK_TOKEN, "name": "My Vinyl"},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == "My Vinyl"
assert result["data"] == {CONF_TOKEN: MOCK_TOKEN}
@pytest.mark.usefixtures("mock_setup_entry")
async def test_import_flow_already_configured(
hass: HomeAssistant,
mock_discogs_client: MagicMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test YAML import aborts when already configured."""
mock_config_entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data={CONF_TOKEN: MOCK_TOKEN},
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"
+45
View File
@@ -0,0 +1,45 @@
"""Test Discogs integration init."""
from unittest.mock import MagicMock
import discogs_client
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
async def test_setup_and_unload_entry(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_discogs_client: MagicMock,
) -> None:
"""Test successful setup and unload of config entry."""
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.LOADED
await hass.config_entries.async_unload(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
async def test_setup_entry_not_ready(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_discogs_client: MagicMock,
) -> None:
"""Test setup entry retries when Discogs API is unavailable."""
mock_discogs_client.identity.side_effect = discogs_client.exceptions.HTTPError(
"Service Unavailable", 503
)
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
+68
View File
@@ -0,0 +1,68 @@
"""Test Discogs sensor platform."""
from unittest.mock import MagicMock, patch
from syrupy.assertion import SnapshotAssertion
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from tests.common import MockConfigEntry, snapshot_platform
async def test_sensors(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
mock_config_entry: MockConfigEntry,
mock_discogs_client: MagicMock,
) -> None:
"""Test sensor entities with snapshot."""
mock_config_entry.add_to_hass(hass)
with patch("homeassistant.components.discogs.PLATFORMS", [Platform.SENSOR]):
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
async def test_sensors_empty_collection(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test sensors when the collection is empty."""
mock_identity = MagicMock()
mock_identity.name = "testuser"
mock_identity.num_collection = 0
mock_identity.num_wantlist = 0
folder = MagicMock()
folder.count = 0
mock_identity.collection_folders = [folder]
mock_client = MagicMock()
mock_client.identity.return_value = mock_identity
mock_config_entry.add_to_hass(hass)
with (
patch(
"homeassistant.components.discogs.discogs_client.Client",
return_value=mock_client,
),
):
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("sensor.testuser_collection")
assert state is not None
assert state.state == "0"
state = hass.states.get("sensor.testuser_wantlist")
assert state is not None
assert state.state == "0"
state = hass.states.get("sensor.testuser_random_record")
assert state is not None
assert state.state == "unknown"