Azure DevOps integration tests (#114577)

* Add tests to azure devops

* Remove Azure DevOps files from coverage

* Add assertion for entity registration in test_sensors()

* Remove unnecessary code in test_sensor.py

* Refactor test_sensors function

* Fix

* Test unique id

* Refactor

* Refactor reauth_flow test in azure_devops module

* Suggested changes, batched

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Changes

* Use snapshot

* Remove redundant entry fetch

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
Aidan Timson 2024-04-02 20:23:55 +01:00 committed by GitHub
parent 7cb01f75ae
commit 17f0002549
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 501 additions and 223 deletions

View File

@ -108,8 +108,6 @@ omit =
homeassistant/components/avea/light.py homeassistant/components/avea/light.py
homeassistant/components/avion/light.py homeassistant/components/avion/light.py
homeassistant/components/awair/coordinator.py homeassistant/components/awair/coordinator.py
homeassistant/components/azure_devops/__init__.py
homeassistant/components/azure_devops/sensor.py
homeassistant/components/azure_service_bus/* homeassistant/components/azure_service_bus/*
homeassistant/components/baf/__init__.py homeassistant/components/baf/__init__.py
homeassistant/components/baf/climate.py homeassistant/components/baf/climate.py

View File

@ -1 +1,82 @@
"""Tests for the Azure DevOps integration.""" """Tests for the Azure DevOps integration."""
from typing import Final
from aioazuredevops.builds import DevOpsBuild, DevOpsBuildDefinition
from aioazuredevops.core import DevOpsProject
from homeassistant.components.azure_devops.const import CONF_ORG, CONF_PAT, CONF_PROJECT
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
ORGANIZATION: Final[str] = "testorg"
PROJECT: Final[str] = "testproject"
PAT: Final[str] = "abc123"
UNIQUE_ID = f"{ORGANIZATION}_{PROJECT}"
FIXTURE_USER_INPUT = {
CONF_ORG: ORGANIZATION,
CONF_PROJECT: PROJECT,
CONF_PAT: PAT,
}
FIXTURE_REAUTH_INPUT = {
CONF_PAT: PAT,
}
DEVOPS_PROJECT = DevOpsProject(
project_id="1234",
name=PROJECT,
description="Test Description",
url=f"https://dev.azure.com/{ORGANIZATION}/{PROJECT}",
state="wellFormed",
revision=1,
visibility="private",
last_updated=None,
default_team=None,
links=None,
)
DEVOPS_BUILD_DEFINITION = DevOpsBuildDefinition(
build_id=9876,
name="Test Build",
url=f"https://dev.azure.com/{ORGANIZATION}/{PROJECT}/_apis/build/definitions/1",
path="",
build_type="build",
queue_status="enabled",
revision=1,
)
DEVOPS_BUILD = DevOpsBuild(
build_id=5678,
build_number="1",
status="completed",
result="succeeded",
source_branch="main",
source_version="123",
priority="normal",
reason="manual",
queue_time="2021-01-01T00:00:00Z",
start_time="2021-01-01T00:00:00Z",
finish_time="2021-01-01T00:00:00Z",
definition=DEVOPS_BUILD_DEFINITION,
project=DEVOPS_PROJECT,
links=None,
)
async def setup_integration(
hass: HomeAssistant,
config_entry: MockConfigEntry,
) -> bool:
"""Fixture for setting up the component."""
config_entry.add_to_hass(hass)
result = await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
return result

View File

@ -0,0 +1,58 @@
"""Test fixtures for Azure DevOps."""
from collections.abc import AsyncGenerator, Generator
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from homeassistant.components.azure_devops.const import DOMAIN
from . import DEVOPS_BUILD, DEVOPS_PROJECT, FIXTURE_USER_INPUT, PAT, UNIQUE_ID
from tests.common import MockConfigEntry
@pytest.fixture
async def mock_devops_client() -> AsyncGenerator[MagicMock, None]:
"""Mock the Azure DevOps client."""
with (
patch(
"homeassistant.components.azure_devops.DevOpsClient", autospec=True
) as mock_client,
patch(
"homeassistant.components.azure_devops.config_flow.DevOpsClient",
new=mock_client,
),
):
devops_client = mock_client.return_value
devops_client.authorized = True
devops_client.pat = PAT
devops_client.authorize.return_value = True
devops_client.get_project.return_value = DEVOPS_PROJECT
devops_client.get_builds.return_value = [DEVOPS_BUILD]
devops_client.get_build.return_value = DEVOPS_BUILD
devops_client.get_work_items_ids_all.return_value = None
devops_client.get_work_items.return_value = None
yield devops_client
@pytest.fixture
async def mock_config_entry() -> MockConfigEntry:
"""Create a mock config entry."""
return MockConfigEntry(
domain=DOMAIN,
data=FIXTURE_USER_INPUT,
unique_id=UNIQUE_ID,
)
@pytest.fixture
def mock_setup_entry() -> Generator[AsyncMock, None, None]:
"""Override async_setup_entry."""
with patch(
"homeassistant.components.azure_devops.async_setup_entry",
return_value=True,
) as mock_setup_entry:
yield mock_setup_entry

View File

@ -0,0 +1,59 @@
# serializer version: 1
# name: test_sensors[sensor.testproject_test_build_latest_build-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.testproject_test_build_latest_build',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Test Build latest build',
'platform': 'azure_devops',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'latest_build',
'unique_id': 'testorg_1234_9876_latest_build',
'unit_of_measurement': None,
})
# ---
# name: test_sensors[sensor.testproject_test_build_latest_build-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'definition_id': 9876,
'definition_name': 'Test Build',
'finish_time': '2021-01-01T00:00:00Z',
'friendly_name': 'testproject Test Build latest build',
'id': 5678,
'queue_time': '2021-01-01T00:00:00Z',
'reason': 'manual',
'result': 'succeeded',
'source_branch': 'main',
'source_version': '123',
'start_time': '2021-01-01T00:00:00Z',
'status': 'completed',
'url': None,
}),
'context': <ANY>,
'entity_id': 'sensor.testproject_test_build_latest_build',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '1',
})
# ---

View File

@ -1,26 +1,18 @@
"""Test the Azure DevOps config flow.""" """Test the Azure DevOps config flow."""
from unittest.mock import patch from unittest.mock import AsyncMock
from aioazuredevops.core import DevOpsProject from aioazuredevops.core import DevOpsProject
import aiohttp import aiohttp
from homeassistant import config_entries, data_entry_flow from homeassistant import config_entries, data_entry_flow
from homeassistant.components.azure_devops.const import ( from homeassistant.components.azure_devops.const import CONF_ORG, CONF_PROJECT, DOMAIN
CONF_ORG,
CONF_PAT,
CONF_PROJECT,
DOMAIN,
)
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from . import FIXTURE_REAUTH_INPUT, FIXTURE_USER_INPUT
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
FIXTURE_REAUTH_INPUT = {CONF_PAT: "abc123"}
FIXTURE_USER_INPUT = {CONF_ORG: "random", CONF_PROJECT: "project", CONF_PAT: "abc123"}
UNIQUE_ID = "random_project"
async def test_show_user_form(hass: HomeAssistant) -> None: async def test_show_user_form(hass: HomeAssistant) -> None:
"""Test that the setup form is served.""" """Test that the setup form is served."""
@ -32,14 +24,17 @@ async def test_show_user_form(hass: HomeAssistant) -> None:
assert result["step_id"] == "user" assert result["step_id"] == "user"
async def test_authorization_error(hass: HomeAssistant) -> None: async def test_authorization_error(
hass: HomeAssistant,
mock_devops_client: AsyncMock,
) -> None:
"""Test we show user form on Azure DevOps authorization error.""" """Test we show user form on Azure DevOps authorization error."""
with patch( mock_devops_client.authorize.return_value = False
"homeassistant.components.azure_devops.config_flow.DevOpsClient.authorize", mock_devops_client.authorized = False
return_value=False,
):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN,
context={"source": config_entries.SOURCE_USER},
) )
assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["type"] == data_entry_flow.FlowResultType.FORM
@ -56,12 +51,14 @@ async def test_authorization_error(hass: HomeAssistant) -> None:
assert result2["errors"] == {"base": "invalid_auth"} assert result2["errors"] == {"base": "invalid_auth"}
async def test_reauth_authorization_error(hass: HomeAssistant) -> None: async def test_reauth_authorization_error(
hass: HomeAssistant,
mock_devops_client: AsyncMock,
) -> None:
"""Test we show user form on Azure DevOps authorization error.""" """Test we show user form on Azure DevOps authorization error."""
with patch( mock_devops_client.authorize.return_value = False
"homeassistant.components.azure_devops.config_flow.DevOpsClient.authorize", mock_devops_client.authorized = False
return_value=False,
):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
context={"source": config_entries.SOURCE_REAUTH}, context={"source": config_entries.SOURCE_REAUTH},
@ -82,14 +79,17 @@ async def test_reauth_authorization_error(hass: HomeAssistant) -> None:
assert result2["errors"] == {"base": "invalid_auth"} assert result2["errors"] == {"base": "invalid_auth"}
async def test_connection_error(hass: HomeAssistant) -> None: async def test_connection_error(
hass: HomeAssistant,
mock_devops_client: AsyncMock,
) -> None:
"""Test we show user form on Azure DevOps connection error.""" """Test we show user form on Azure DevOps connection error."""
with patch( mock_devops_client.authorize.side_effect = aiohttp.ClientError
"homeassistant.components.azure_devops.config_flow.DevOpsClient.authorize", mock_devops_client.authorized = False
side_effect=aiohttp.ClientError,
):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN,
context={"source": config_entries.SOURCE_USER},
) )
assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["type"] == data_entry_flow.FlowResultType.FORM
@ -106,12 +106,14 @@ async def test_connection_error(hass: HomeAssistant) -> None:
assert result2["errors"] == {"base": "cannot_connect"} assert result2["errors"] == {"base": "cannot_connect"}
async def test_reauth_connection_error(hass: HomeAssistant) -> None: async def test_reauth_connection_error(
hass: HomeAssistant,
mock_devops_client: AsyncMock,
) -> None:
"""Test we show user form on Azure DevOps connection error.""" """Test we show user form on Azure DevOps connection error."""
with patch( mock_devops_client.authorize.side_effect = aiohttp.ClientError
"homeassistant.components.azure_devops.config_flow.DevOpsClient.authorize", mock_devops_client.authorized = False
side_effect=aiohttp.ClientError,
):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
context={"source": config_entries.SOURCE_REAUTH}, context={"source": config_entries.SOURCE_REAUTH},
@ -132,23 +134,18 @@ async def test_reauth_connection_error(hass: HomeAssistant) -> None:
assert result2["errors"] == {"base": "cannot_connect"} assert result2["errors"] == {"base": "cannot_connect"}
async def test_project_error(hass: HomeAssistant) -> None: async def test_project_error(
hass: HomeAssistant,
mock_devops_client: AsyncMock,
) -> None:
"""Test we show user form on Azure DevOps connection error.""" """Test we show user form on Azure DevOps connection error."""
with ( mock_devops_client.authorize.return_value = True
patch( mock_devops_client.authorized = True
"homeassistant.components.azure_devops.config_flow.DevOpsClient.authorized", mock_devops_client.get_project.return_value = None
return_value=True,
),
patch(
"homeassistant.components.azure_devops.config_flow.DevOpsClient.authorize",
),
patch(
"homeassistant.components.azure_devops.config_flow.DevOpsClient.get_project",
return_value=None,
),
):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN,
context={"source": config_entries.SOURCE_USER},
) )
assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["type"] == data_entry_flow.FlowResultType.FORM
@ -165,21 +162,18 @@ async def test_project_error(hass: HomeAssistant) -> None:
assert result2["errors"] == {"base": "project_error"} assert result2["errors"] == {"base": "project_error"}
async def test_reauth_project_error(hass: HomeAssistant) -> None: async def test_reauth_project_error(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_devops_client: AsyncMock,
) -> None:
"""Test we show user form on Azure DevOps project error.""" """Test we show user form on Azure DevOps project error."""
with ( mock_devops_client.authorize.return_value = True
patch( mock_devops_client.authorized = True
"homeassistant.components.azure_devops.config_flow.DevOpsClient.authorize", mock_devops_client.get_project.return_value = None
),
patch( mock_config_entry.add_to_hass(hass)
"homeassistant.components.azure_devops.config_flow.DevOpsClient.authorized",
return_value=True,
),
patch(
"homeassistant.components.azure_devops.config_flow.DevOpsClient.get_project",
return_value=None,
),
):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
context={"source": config_entries.SOURCE_REAUTH}, context={"source": config_entries.SOURCE_REAUTH},
@ -200,16 +194,16 @@ async def test_reauth_project_error(hass: HomeAssistant) -> None:
assert result2["errors"] == {"base": "project_error"} assert result2["errors"] == {"base": "project_error"}
async def test_reauth_flow(hass: HomeAssistant) -> None: async def test_reauth_flow(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_devops_client: AsyncMock,
) -> None:
"""Test reauth works.""" """Test reauth works."""
with patch( mock_devops_client.authorize.return_value = False
"homeassistant.components.azure_devops.config_flow.DevOpsClient.authorize", mock_devops_client.authorized = False
return_value=False,
): mock_config_entry.add_to_hass(hass)
mock_config = MockConfigEntry(
domain=DOMAIN, unique_id=UNIQUE_ID, data=FIXTURE_USER_INPUT
)
mock_config.add_to_hass(hass)
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
@ -221,21 +215,12 @@ async def test_reauth_flow(hass: HomeAssistant) -> None:
assert result["step_id"] == "reauth" assert result["step_id"] == "reauth"
assert result["errors"] == {"base": "invalid_auth"} assert result["errors"] == {"base": "invalid_auth"}
with ( mock_devops_client.authorize.return_value = True
patch( mock_devops_client.authorized = True
"homeassistant.components.azure_devops.config_flow.DevOpsClient.authorize", mock_devops_client.get_project.return_value = DevOpsProject(
),
patch(
"homeassistant.components.azure_devops.config_flow.DevOpsClient.authorized",
return_value=True,
),
patch(
"homeassistant.components.azure_devops.config_flow.DevOpsClient.get_project",
return_value=DevOpsProject(
"abcd-abcd-abcd-abcd", FIXTURE_USER_INPUT[CONF_PROJECT] "abcd-abcd-abcd-abcd", FIXTURE_USER_INPUT[CONF_PROJECT]
), )
),
):
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
FIXTURE_REAUTH_INPUT, FIXTURE_REAUTH_INPUT,
@ -246,29 +231,15 @@ async def test_reauth_flow(hass: HomeAssistant) -> None:
assert result2["reason"] == "reauth_successful" assert result2["reason"] == "reauth_successful"
async def test_full_flow_implementation(hass: HomeAssistant) -> None: async def test_full_flow_implementation(
hass: HomeAssistant,
mock_setup_entry: AsyncMock,
mock_devops_client: AsyncMock,
) -> None:
"""Test registering an integration and finishing flow works.""" """Test registering an integration and finishing flow works."""
with (
patch(
"homeassistant.components.azure_devops.async_setup_entry",
return_value=True,
) as mock_setup_entry,
patch(
"homeassistant.components.azure_devops.config_flow.DevOpsClient.authorized",
return_value=True,
),
patch(
"homeassistant.components.azure_devops.config_flow.DevOpsClient.authorize",
),
patch(
"homeassistant.components.azure_devops.config_flow.DevOpsClient.get_project",
return_value=DevOpsProject(
"abcd-abcd-abcd-abcd", FIXTURE_USER_INPUT[CONF_PROJECT]
),
),
):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN,
context={"source": config_entries.SOURCE_USER},
) )
assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["type"] == data_entry_flow.FlowResultType.FORM

View File

@ -0,0 +1,78 @@
"""Tests for init of Azure DevOps."""
from unittest.mock import AsyncMock, MagicMock
import aiohttp
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from . import setup_integration
from tests.common import MockConfigEntry
async def test_load_unload_entry(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_devops_client: MagicMock,
) -> None:
"""Test a successful setup entry."""
assert await setup_integration(hass, mock_config_entry)
assert mock_devops_client.authorized
assert mock_devops_client.authorize.call_count == 1
assert mock_devops_client.get_builds.call_count == 2
assert mock_config_entry.state == ConfigEntryState.LOADED
await hass.config_entries.async_remove(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state == ConfigEntryState.NOT_LOADED
async def test_auth_failed(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_devops_client: AsyncMock,
) -> None:
"""Test a failed setup entry."""
mock_devops_client.authorize.return_value = False
mock_devops_client.authorized = False
await setup_integration(hass, mock_config_entry)
assert not mock_devops_client.authorized
assert mock_config_entry.state == ConfigEntryState.SETUP_ERROR
async def test_update_failed(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_devops_client: MagicMock,
) -> None:
"""Test a failed update entry."""
mock_devops_client.get_builds.side_effect = aiohttp.ClientError
await setup_integration(hass, mock_config_entry)
assert mock_devops_client.get_builds.call_count == 1
assert mock_config_entry.state == ConfigEntryState.SETUP_RETRY
async def test_no_builds(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_devops_client: MagicMock,
) -> None:
"""Test a failed update entry."""
mock_devops_client.get_builds.return_value = None
await setup_integration(hass, mock_config_entry)
assert mock_devops_client.get_builds.call_count == 1
assert mock_config_entry.state == ConfigEntryState.SETUP_RETRY

View File

@ -0,0 +1,33 @@
"""Tests for init of Azure DevOps."""
from unittest.mock import AsyncMock
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import setup_integration
from tests.common import MockConfigEntry
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_sensors(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
mock_config_entry: MockConfigEntry,
mock_devops_client: AsyncMock,
) -> None:
"""Test the sensor entities."""
assert await setup_integration(hass, mock_config_entry)
assert (
entry := entity_registry.async_get("sensor.testproject_test_build_latest_build")
)
assert entry == snapshot(name=f"{entry.entity_id}-entry")
assert hass.states.get(entry.entity_id) == snapshot(name=f"{entry.entity_id}-state")