Skip to content

Commit

Permalink
test: added tests for config flow and refactored some api and init stuff
Browse files Browse the repository at this point in the history
  • Loading branch information
Jadon committed Aug 12, 2023
1 parent cc07f87 commit 52c145d
Show file tree
Hide file tree
Showing 8 changed files with 218 additions and 26 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -162,3 +162,5 @@ cython_debug/
.DS_Store

test

test_creds.py
12 changes: 6 additions & 6 deletions custom_components/audiobookshelf/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import logging

from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, Config
from homeassistant.core import Config, HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
Expand All @@ -17,6 +17,7 @@
CONF_HOST,
DOMAIN,
ISSUE_URL,
PLATFORMS,
SCAN_INTERVAL,
VERSION,
)
Expand Down Expand Up @@ -55,7 +56,6 @@ async def _async_update_data(self) -> dict[str, None]:
update["connectivity"] = "ConnectionError: Unable to connect."
except (TimeoutError, Timeout):
update["connectivity"] = "TimeoutError: Request timed out."
print("I ran\n\n\n\n\n")
except HTTPError as http_error:
update["connectivity"] = f"HTTPError: Generic HTTP Error happened {http_error}"
try:
Expand Down Expand Up @@ -88,7 +88,7 @@ async def _async_update_data(self) -> dict[str, None]:
update["sessions"] = f"HTTPError: Generic HTTP Error happened {http_error}"
return update

async def async_setup(hass: HomeAssistant, config: Config):
async def async_setup(hass: HomeAssistant, config: Config) -> bool:
"""Set up this integration using YAML is not supported."""
return True

Expand Down Expand Up @@ -127,7 +127,7 @@ async def async_setup_entry(

hass.data[DOMAIN][entry.entry_id] = coordinator

for platform in ["binary_sensor", "sensor"]:
for platform in PLATFORMS:
if entry.options.get(platform, True):
coordinator.platforms.append(platform)
hass.async_add_job(
Expand All @@ -144,15 +144,15 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(entry, platform)
for platform in ["binary_sensor", "sensor"]
for platform in PLATFORMS
if platform in coordinator.platforms
],
),
)
if unloaded:
hass.data[DOMAIN].pop(entry.entry_id)

return unloaded
return unloaded

async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Reload config entry."""
Expand Down
3 changes: 2 additions & 1 deletion custom_components/audiobookshelf/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ async def api_wrapper(
async with async_timeout.timeout(TIMEOUT): # loop=asyncio.get_event_loop()
if method == "get":
response = await self._session.get(url, headers=headers)
return await response.json()
if response.status >= 200 and response.status < 300:
return await response.json()

if method == "put":
await self._session.put(url, headers=headers, json=data)
Expand Down
19 changes: 11 additions & 8 deletions custom_components/audiobookshelf/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import logging
from typing import Any

import aiohttp
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.config_entries import ConfigEntry
Expand All @@ -12,7 +13,7 @@
from homeassistant.helpers.aiohttp_client import async_create_clientsession

from .api import AudiobookshelfApiClient
from .const import CONF_ACCESS_TOKEN, CONF_HOST, DOMAIN
from .const import CONF_ACCESS_TOKEN, CONF_HOST, DOMAIN, PLATFORMS

_LOGGER: logging.Logger = logging.getLogger(__package__)

Expand Down Expand Up @@ -88,14 +89,16 @@ async def _test_credentials(
url=api.get_host() + "/api/users",
)
_LOGGER.debug("""test_credentials response was: %s""", response)
return True
except (ConnectionError, TimeoutError) as connection_error:
_LOGGER.debug("Connection or Timeout error: %s", connection_error)
if response:
return True
return False
except (ConnectionError, TimeoutError) as connection_or_timeout_error:
_LOGGER.debug("Connection or Timeout error: %s", connection_or_timeout_error)
return False

except Exception as exception:
_LOGGER.error("test_credentials failed due to: %s", exception)
raise
except aiohttp.ClientResponseError as client_response_error:
_LOGGER.debug("ClientResponse Error: %s - %s", client_response_error.status, client_response_error.message)
return False


class AudiobookshelfOptionsFlowHandler(config_entries.OptionsFlow):
Expand Down Expand Up @@ -127,7 +130,7 @@ async def async_step_user(
data_schema=vol.Schema(
{
vol.Required(x, default=self.options.get(x, True)): bool
for x in sorted(["binary_sensor", "sensor"])
for x in sorted(PLATFORMS)
},
),
)
Expand Down
2 changes: 2 additions & 0 deletions custom_components/audiobookshelf/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@

CONF_ACCESS_TOKEN = "access_token"
CONF_HOST = "host"

PLATFORMS = ["binary_sensor", "sensor"]
20 changes: 16 additions & 4 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
"""pytest fixtures."""
import pytest
from unittest.mock import patch

from requests import HTTPError, Timeout
import aiohttp
import pytest
from _pytest.fixtures import FixtureRequest
from requests import HTTPError


@pytest.fixture(autouse=True)
def auto_enable_custom_integrations(enable_custom_integrations):
def auto_enable_custom_integrations(enable_custom_integrations: FixtureRequest) -> None:
"""Enable custom integrations defined in the test dir."""
yield

Expand Down Expand Up @@ -45,4 +48,13 @@ def http_error_get_data_fixture() -> None:
"custom_components.audiobookshelf.AudiobookshelfApiClient.api_wrapper",
side_effect=HTTPError,
):
yield None
yield None

@pytest.fixture(name="client_error_on_get_data")
def client_error_get_data_fixture() -> None:
"""Simulate error when retrieving data from API."""
with patch(
"custom_components.audiobookshelf.AudiobookshelfApiClient.api_wrapper",
side_effect=aiohttp.ClientResponseError(request_info=None, history=None),
):
yield None
173 changes: 173 additions & 0 deletions tests/test_config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
"""Test Audiobookshelf config flow."""
from unittest.mock import patch

import pytest
from _pytest.fixtures import FixtureRequest
from homeassistant import config_entries, data_entry_flow
from homeassistant.core import HomeAssistant
from pytest_homeassistant_custom_component.common import MockConfigEntry
from pytest_homeassistant_custom_component.test_util.aiohttp import AiohttpClientMocker

from custom_components.audiobookshelf.const import (
DOMAIN,
PLATFORMS,
)

from .const import MOCK_CONFIG


# This fixture bypasses the actual setup of the integration
# since we only want to test the config flow. We test the
# actual functionality of the integration in other test modules.
@pytest.fixture(autouse=True)
def bypass_setup_fixture() -> None:
"""Prevent setup."""
with patch("custom_components.audiobookshelf.async_setup", return_value=True), patch(
"custom_components.audiobookshelf.async_setup_entry",
return_value=True,
):
yield

# Here we simiulate a successful config flow from the backend.
# Note that we use the `bypass_get_data` fixture here because
# we want the config flow validation to succeed during the test.
async def test_successful_config_flow(hass:HomeAssistant, aioclient_mock: AiohttpClientMocker)-> None:
"""Test a successful config flow."""

aioclient_mock.get("some_host/ping", json={"success": True})
aioclient_mock.get("some_host/api/users", json={"users": []})
aioclient_mock.get("some_host/api/users/online", json={"openSessions": []})


# Initialize a config flow
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER},
)

# Check that the config flow shows the user form as the first step
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "user"

# If a user were to enter `some_host` for username and `test_password`
# for password, it would result in this function call
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_CONFIG,
)

# Check that the config flow is complete and a new entry is created with
# the input data
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["title"] == "some_host"
assert result["data"] == MOCK_CONFIG
assert result["result"]

aioclient_mock.clear_requests()


# In this case, we want to simulate a failure during the config flow.
# We use the `error_on_get_data` mock instead of `bypass_get_data`
# (note the function parameters) to raise an Exception during
# validation of the input config.
async def test_failed_config_flow(hass:HomeAssistant, aioclient_mock: AiohttpClientMocker)-> None:
"""Test a failed config flow due to credential validation failure."""
aioclient_mock.get("some_host/ping", json={"success": True})
aioclient_mock.get("some_host/api/users", status=404)
aioclient_mock.get("some_host/api/users/online", status=404)


result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER},
)

assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "user"

result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_CONFIG,
)

assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] == {"base": "auth"}

aioclient_mock.clear_requests()


async def test_timeout_error_config_flow(hass: HomeAssistant, timeout_error_on_get_data: FixtureRequest)-> None:
"""Test a failed config flow due to credential validation failure."""

result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER},
)

assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "user"

result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_CONFIG,
)

assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] == {"base": "auth"}

async def test_connectivity_error_config_flow(hass: HomeAssistant, connectivity_error_on_get_data:FixtureRequest)-> None:
"""Test a failed config flow due to credential validation failure."""

result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER},
)

assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "user"

result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_CONFIG,
)

assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] == {"base": "auth"}

async def test_client_error_config_flow(hass:HomeAssistant, client_error_on_get_data:FixtureRequest)-> None:
"""Test a failed config flow due to credential validation failure."""

result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER},
)

assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "user"

result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_CONFIG,
)

assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] == {"base": "auth"}

# Our config flow also has an options flow, so we must test it as well.
async def test_options_flow(hass:HomeAssistant)-> None:
"""Test an options flow."""
# Create a new MockConfigEntry and add to HASS (we're bypassing config
# flow entirely)
entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG, entry_id="test")
entry.add_to_hass(hass)

# Initialize an options flow
await hass.config_entries.async_setup(entry.entry_id)
result = await hass.config_entries.options.async_init(entry.entry_id)

# Verify that the first options step is a user form
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "user"

# Enter some fake data into the form
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={platform: platform != "sensor" for platform in PLATFORMS},
)

# Verify that the flow finishes
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["title"] == "some_host"

# Verify that the options were updated
assert entry.options == {"binary_sensor": True, "sensor": False}
13 changes: 6 additions & 7 deletions tests/test_init.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
"""Test Audiobookshelf setup process."""

import pytest
from homeassistant.core import HomeAssistant
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from pytest_homeassistant_custom_component.common import MockConfigEntry
from pytest_homeassistant_custom_component.test_util.aiohttp import AiohttpClientMocker

from custom_components.audiobookshelf import (
AudiobookshelfDataUpdateCoordinator,
async_reload_entry,
async_setup,
async_setup_entry,
async_unload_entry,
async_setup,
)
from custom_components.audiobookshelf.const import (
DOMAIN,
Expand All @@ -31,9 +30,9 @@
source="some source",
)

async def test_setup(hass: HomeAssistant,):
async def test_setup(hass: HomeAssistant)->None:
assert (await async_setup(hass, MOCK_CONFIG)) is True

async def test_setup_entry(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
Expand All @@ -44,7 +43,7 @@ async def test_setup_entry(
assert await async_setup_entry(hass, config_entry)
assert DOMAIN in hass.data and config_entry.entry_id in hass.data[DOMAIN]
assert isinstance(
hass.data['audiobookshelf']['test_entry_id_setup'],
hass.data["audiobookshelf"]["test_entry_id_setup"],
AudiobookshelfDataUpdateCoordinator,
)
aioclient_mock.clear_requests()
Expand Down Expand Up @@ -178,4 +177,4 @@ async def test_setup_entry_http_exception(
assert hass.data[DOMAIN][config_entry.entry_id].data.get("sessions", "") == "HTTPError: Generic HTTP Error happened "

assert await async_unload_entry(hass, config_entry)
assert config_entry.entry_id not in hass.data[DOMAIN]
assert config_entry.entry_id not in hass.data[DOMAIN]

0 comments on commit 52c145d

Please sign in to comment.