mirror of
				https://github.com/home-assistant/core.git
				synced 2025-11-04 00:19:31 +00:00 
			
		
		
		
	Add application credentials platform (#69148)
* Initial developer credentials scaffolding - Support websocket list/add/delete - Add developer credentials protocol from yaml config - Handle OAuth credential registration and de-registration - Tests for websocket and integration based registration * Fix pydoc text * Remove translations and update owners * Update homeassistant/components/developer_credentials/__init__.py Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io> * Update homeassistant/components/developer_credentials/__init__.py Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io> * Remove _async_get_developer_credential * Rename to application credentials platform * Fix race condition and add import support * Increase code coverage (92%) * Increase test coverage 93% * Increase test coverage (94%) * Increase test coverage (97%) * Increase test covearge (98%) * Increase test coverage (99%) * Increase test coverage (100%) * Remove http router frozen comment * Remove auth domain override on import * Remove debug statement * Don't import the same client id multiple times * Add auth dependency for local oauth implementation * Revert older oauth2 changes from merge * Update homeassistant/components/application_credentials/__init__.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Move config credential import to its own fixture * Override the mock_application_credentials_integration fixture instead per test * Update application credentials * Add dictionary typing * Use f-strings as per feedback * Add additional structure needed for an MVP application credential Add additional structure needed for an MVP, including a target component Xbox * Add websocket to list supported integrations for frontend selector * Application credentials config * Import xbox credentials * Remove unnecessary async calls * Update script/hassfest/application_credentials.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update script/hassfest/application_credentials.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update script/hassfest/application_credentials.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update script/hassfest/application_credentials.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Import credentials with a fixed auth domain Resolve an issue with compatibility of exisiting config entries when importing client credentials Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io> Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
		
							
								
								
									
										623
									
								
								tests/components/application_credentials/test_init.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										623
									
								
								tests/components/application_credentials/test_init.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,623 @@
 | 
			
		||||
"""Test the Developer Credentials integration."""
 | 
			
		||||
 | 
			
		||||
from __future__ import annotations
 | 
			
		||||
 | 
			
		||||
from collections.abc import Callable, Generator
 | 
			
		||||
import logging
 | 
			
		||||
from typing import Any
 | 
			
		||||
from unittest.mock import AsyncMock, Mock, patch
 | 
			
		||||
 | 
			
		||||
from aiohttp import ClientWebSocketResponse
 | 
			
		||||
import pytest
 | 
			
		||||
 | 
			
		||||
from homeassistant import config_entries, data_entry_flow
 | 
			
		||||
from homeassistant.components.application_credentials import (
 | 
			
		||||
    CONF_AUTH_DOMAIN,
 | 
			
		||||
    DOMAIN,
 | 
			
		||||
    AuthorizationServer,
 | 
			
		||||
    ClientCredential,
 | 
			
		||||
    async_import_client_credential,
 | 
			
		||||
)
 | 
			
		||||
from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_DOMAIN
 | 
			
		||||
from homeassistant.core import HomeAssistant
 | 
			
		||||
from homeassistant.helpers import config_entry_oauth2_flow
 | 
			
		||||
from homeassistant.setup import async_setup_component
 | 
			
		||||
 | 
			
		||||
from tests.common import mock_platform
 | 
			
		||||
 | 
			
		||||
CLIENT_ID = "some-client-id"
 | 
			
		||||
CLIENT_SECRET = "some-client-secret"
 | 
			
		||||
DEVELOPER_CREDENTIAL = ClientCredential(CLIENT_ID, CLIENT_SECRET)
 | 
			
		||||
ID = "fake_integration_some_client_id"
 | 
			
		||||
AUTHORIZE_URL = "https://example.com/auth"
 | 
			
		||||
TOKEN_URL = "https://example.com/oauth2/v4/token"
 | 
			
		||||
REFRESH_TOKEN = "mock-refresh-token"
 | 
			
		||||
ACCESS_TOKEN = "mock-access-token"
 | 
			
		||||
 | 
			
		||||
TEST_DOMAIN = "fake_integration"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture
 | 
			
		||||
async def authorization_server() -> AuthorizationServer:
 | 
			
		||||
    """Fixture AuthorizationServer for mock application_credentials integration."""
 | 
			
		||||
    return AuthorizationServer(AUTHORIZE_URL, TOKEN_URL)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture
 | 
			
		||||
async def config_credential() -> ClientCredential | None:
 | 
			
		||||
    """Fixture ClientCredential for mock application_credentials integration."""
 | 
			
		||||
    return None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture
 | 
			
		||||
async def import_config_credential(
 | 
			
		||||
    hass: HomeAssistant, config_credential: ClientCredential
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Fixture to import the yaml based credential."""
 | 
			
		||||
    await async_import_client_credential(hass, TEST_DOMAIN, config_credential)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def setup_application_credentials_integration(
 | 
			
		||||
    hass: HomeAssistant,
 | 
			
		||||
    domain: str,
 | 
			
		||||
    authorization_server: AuthorizationServer,
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Set up a fake application_credentials integration."""
 | 
			
		||||
    hass.config.components.add(domain)
 | 
			
		||||
    mock_platform(
 | 
			
		||||
        hass,
 | 
			
		||||
        f"{domain}.application_credentials",
 | 
			
		||||
        Mock(
 | 
			
		||||
            async_get_authorization_server=AsyncMock(return_value=authorization_server),
 | 
			
		||||
        ),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture(autouse=True)
 | 
			
		||||
async def mock_application_credentials_integration(
 | 
			
		||||
    hass: HomeAssistant,
 | 
			
		||||
    authorization_server: AuthorizationServer,
 | 
			
		||||
):
 | 
			
		||||
    """Mock a application_credentials integration."""
 | 
			
		||||
    assert await async_setup_component(hass, "application_credentials", {})
 | 
			
		||||
    await setup_application_credentials_integration(
 | 
			
		||||
        hass, TEST_DOMAIN, authorization_server
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class FakeConfigFlow(config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN):
 | 
			
		||||
    """Config flow used during tests."""
 | 
			
		||||
 | 
			
		||||
    DOMAIN = TEST_DOMAIN
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def logger(self) -> logging.Logger:
 | 
			
		||||
        """Return logger."""
 | 
			
		||||
        return logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture(autouse=True)
 | 
			
		||||
def config_flow_handler(
 | 
			
		||||
    hass: HomeAssistant, current_request_with_host: Any
 | 
			
		||||
) -> Generator[FakeConfigFlow, None, None]:
 | 
			
		||||
    """Fixture for a test config flow."""
 | 
			
		||||
    mock_platform(hass, f"{TEST_DOMAIN}.config_flow")
 | 
			
		||||
    with patch.dict(config_entries.HANDLERS, {TEST_DOMAIN: FakeConfigFlow}):
 | 
			
		||||
        yield FakeConfigFlow
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class OAuthFixture:
 | 
			
		||||
    """Fixture to facilitate testing an OAuth flow."""
 | 
			
		||||
 | 
			
		||||
    def __init__(self, hass, hass_client, aioclient_mock):
 | 
			
		||||
        """Initialize OAuthFixture."""
 | 
			
		||||
        self.hass = hass
 | 
			
		||||
        self.hass_client = hass_client
 | 
			
		||||
        self.aioclient_mock = aioclient_mock
 | 
			
		||||
        self.client_id = CLIENT_ID
 | 
			
		||||
 | 
			
		||||
    async def complete_external_step(
 | 
			
		||||
        self, result: data_entry_flow.FlowResult
 | 
			
		||||
    ) -> data_entry_flow.FlowResult:
 | 
			
		||||
        """Fixture method to complete the OAuth flow and return the completed result."""
 | 
			
		||||
        client = await self.hass_client()
 | 
			
		||||
        state = config_entry_oauth2_flow._encode_jwt(
 | 
			
		||||
            self.hass,
 | 
			
		||||
            {
 | 
			
		||||
                "flow_id": result["flow_id"],
 | 
			
		||||
                "redirect_uri": "https://example.com/auth/external/callback",
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
        assert result["url"] == (
 | 
			
		||||
            f"{AUTHORIZE_URL}?response_type=code&client_id={self.client_id}"
 | 
			
		||||
            "&redirect_uri=https://example.com/auth/external/callback"
 | 
			
		||||
            f"&state={state}"
 | 
			
		||||
        )
 | 
			
		||||
        resp = await client.get(f"/auth/external/callback?code=abcd&state={state}")
 | 
			
		||||
        assert resp.status == 200
 | 
			
		||||
        assert resp.headers["content-type"] == "text/html; charset=utf-8"
 | 
			
		||||
 | 
			
		||||
        self.aioclient_mock.post(
 | 
			
		||||
            TOKEN_URL,
 | 
			
		||||
            json={
 | 
			
		||||
                "refresh_token": REFRESH_TOKEN,
 | 
			
		||||
                "access_token": ACCESS_TOKEN,
 | 
			
		||||
                "type": "bearer",
 | 
			
		||||
                "expires_in": 60,
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        result = await self.hass.config_entries.flow.async_configure(result["flow_id"])
 | 
			
		||||
        assert result.get("type") == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
 | 
			
		||||
        assert result.get("title") == self.client_id
 | 
			
		||||
        assert "data" in result
 | 
			
		||||
        assert "token" in result["data"]
 | 
			
		||||
        return result
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture
 | 
			
		||||
async def oauth_fixture(
 | 
			
		||||
    hass: HomeAssistant, hass_client_no_auth: Any, aioclient_mock: Any
 | 
			
		||||
) -> OAuthFixture:
 | 
			
		||||
    """Fixture for testing the OAuth flow."""
 | 
			
		||||
    return OAuthFixture(hass, hass_client_no_auth, aioclient_mock)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Client:
 | 
			
		||||
    """Test client with helper methods for application credentials websocket."""
 | 
			
		||||
 | 
			
		||||
    def __init__(self, client):
 | 
			
		||||
        """Initialize Client."""
 | 
			
		||||
        self.client = client
 | 
			
		||||
        self.id = 0
 | 
			
		||||
 | 
			
		||||
    async def cmd(self, cmd: str, payload: dict[str, Any] = None) -> dict[str, Any]:
 | 
			
		||||
        """Send a command and receive the json result."""
 | 
			
		||||
        self.id += 1
 | 
			
		||||
        await self.client.send_json(
 | 
			
		||||
            {
 | 
			
		||||
                "id": self.id,
 | 
			
		||||
                "type": f"{DOMAIN}/{cmd}",
 | 
			
		||||
                **(payload if payload is not None else {}),
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
        resp = await self.client.receive_json()
 | 
			
		||||
        assert resp.get("id") == self.id
 | 
			
		||||
        return resp
 | 
			
		||||
 | 
			
		||||
    async def cmd_result(self, cmd: str, payload: dict[str, Any] = None) -> Any:
 | 
			
		||||
        """Send a command and parse the result."""
 | 
			
		||||
        resp = await self.cmd(cmd, payload)
 | 
			
		||||
        assert resp.get("success")
 | 
			
		||||
        assert resp.get("type") == "result"
 | 
			
		||||
        return resp.get("result")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
ClientFixture = Callable[[], Client]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture
 | 
			
		||||
async def ws_client(
 | 
			
		||||
    hass_ws_client: Callable[[...], ClientWebSocketResponse]
 | 
			
		||||
) -> ClientFixture:
 | 
			
		||||
    """Fixture for creating the test websocket client."""
 | 
			
		||||
 | 
			
		||||
    async def create_client() -> Client:
 | 
			
		||||
        ws_client = await hass_ws_client()
 | 
			
		||||
        return Client(ws_client)
 | 
			
		||||
 | 
			
		||||
    return create_client
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def test_websocket_list_empty(ws_client: ClientFixture):
 | 
			
		||||
    """Test websocket list command."""
 | 
			
		||||
    client = await ws_client()
 | 
			
		||||
    assert await client.cmd_result("list") == []
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def test_websocket_create(ws_client: ClientFixture):
 | 
			
		||||
    """Test websocket create command."""
 | 
			
		||||
    client = await ws_client()
 | 
			
		||||
    result = await client.cmd_result(
 | 
			
		||||
        "create",
 | 
			
		||||
        {
 | 
			
		||||
            CONF_DOMAIN: TEST_DOMAIN,
 | 
			
		||||
            CONF_CLIENT_ID: CLIENT_ID,
 | 
			
		||||
            CONF_CLIENT_SECRET: CLIENT_SECRET,
 | 
			
		||||
        },
 | 
			
		||||
    )
 | 
			
		||||
    assert result == {
 | 
			
		||||
        CONF_DOMAIN: TEST_DOMAIN,
 | 
			
		||||
        CONF_CLIENT_ID: CLIENT_ID,
 | 
			
		||||
        CONF_CLIENT_SECRET: CLIENT_SECRET,
 | 
			
		||||
        "id": ID,
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    result = await client.cmd_result("list")
 | 
			
		||||
    assert result == [
 | 
			
		||||
        {
 | 
			
		||||
            CONF_DOMAIN: TEST_DOMAIN,
 | 
			
		||||
            CONF_CLIENT_ID: CLIENT_ID,
 | 
			
		||||
            CONF_CLIENT_SECRET: CLIENT_SECRET,
 | 
			
		||||
            "id": ID,
 | 
			
		||||
        }
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def test_websocket_create_invalid_domain(ws_client: ClientFixture):
 | 
			
		||||
    """Test websocket create command."""
 | 
			
		||||
    client = await ws_client()
 | 
			
		||||
    resp = await client.cmd(
 | 
			
		||||
        "create",
 | 
			
		||||
        {
 | 
			
		||||
            CONF_DOMAIN: "other-domain",
 | 
			
		||||
            CONF_CLIENT_ID: CLIENT_ID,
 | 
			
		||||
            CONF_CLIENT_SECRET: CLIENT_SECRET,
 | 
			
		||||
        },
 | 
			
		||||
    )
 | 
			
		||||
    assert not resp.get("success")
 | 
			
		||||
    assert "error" in resp
 | 
			
		||||
    assert resp["error"].get("code") == "invalid_format"
 | 
			
		||||
    assert (
 | 
			
		||||
        resp["error"].get("message")
 | 
			
		||||
        == "No application_credentials platform for other-domain"
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def test_websocket_update_not_supported(ws_client: ClientFixture):
 | 
			
		||||
    """Test websocket update command in unsupported."""
 | 
			
		||||
    client = await ws_client()
 | 
			
		||||
    result = await client.cmd_result(
 | 
			
		||||
        "create",
 | 
			
		||||
        {
 | 
			
		||||
            CONF_DOMAIN: TEST_DOMAIN,
 | 
			
		||||
            CONF_CLIENT_ID: CLIENT_ID,
 | 
			
		||||
            CONF_CLIENT_SECRET: CLIENT_SECRET,
 | 
			
		||||
        },
 | 
			
		||||
    )
 | 
			
		||||
    assert result == {
 | 
			
		||||
        CONF_DOMAIN: TEST_DOMAIN,
 | 
			
		||||
        CONF_CLIENT_ID: CLIENT_ID,
 | 
			
		||||
        CONF_CLIENT_SECRET: CLIENT_SECRET,
 | 
			
		||||
        "id": ID,
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    resp = await client.cmd("update", {"application_credentials_id": ID})
 | 
			
		||||
    assert not resp.get("success")
 | 
			
		||||
    assert "error" in resp
 | 
			
		||||
    assert resp["error"].get("code") == "invalid_format"
 | 
			
		||||
    assert resp["error"].get("message") == "Updates not supported"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def test_websocket_delete(ws_client: ClientFixture):
 | 
			
		||||
    """Test websocket delete command."""
 | 
			
		||||
    client = await ws_client()
 | 
			
		||||
 | 
			
		||||
    await client.cmd_result(
 | 
			
		||||
        "create",
 | 
			
		||||
        {
 | 
			
		||||
            CONF_DOMAIN: TEST_DOMAIN,
 | 
			
		||||
            CONF_CLIENT_ID: CLIENT_ID,
 | 
			
		||||
            CONF_CLIENT_SECRET: CLIENT_SECRET,
 | 
			
		||||
        },
 | 
			
		||||
    )
 | 
			
		||||
    assert await client.cmd_result("list") == [
 | 
			
		||||
        {
 | 
			
		||||
            CONF_DOMAIN: TEST_DOMAIN,
 | 
			
		||||
            CONF_CLIENT_ID: CLIENT_ID,
 | 
			
		||||
            CONF_CLIENT_SECRET: CLIENT_SECRET,
 | 
			
		||||
            "id": ID,
 | 
			
		||||
        }
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    await client.cmd_result("delete", {"application_credentials_id": ID})
 | 
			
		||||
    assert await client.cmd_result("list") == []
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def test_websocket_delete_item_not_found(ws_client: ClientFixture):
 | 
			
		||||
    """Test websocket delete command."""
 | 
			
		||||
    client = await ws_client()
 | 
			
		||||
 | 
			
		||||
    resp = await client.cmd("delete", {"application_credentials_id": ID})
 | 
			
		||||
    assert not resp.get("success")
 | 
			
		||||
    assert "error" in resp
 | 
			
		||||
    assert resp["error"].get("code") == "not_found"
 | 
			
		||||
    assert (
 | 
			
		||||
        resp["error"].get("message")
 | 
			
		||||
        == f"Unable to find application_credentials_id {ID}"
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.parametrize("config_credential", [DEVELOPER_CREDENTIAL])
 | 
			
		||||
async def test_websocket_import_config(
 | 
			
		||||
    ws_client: ClientFixture,
 | 
			
		||||
    config_credential: ClientCredential,
 | 
			
		||||
    import_config_credential: Any,
 | 
			
		||||
):
 | 
			
		||||
    """Test websocket list command for an imported credential."""
 | 
			
		||||
    client = await ws_client()
 | 
			
		||||
 | 
			
		||||
    # Imported creds returned from websocket
 | 
			
		||||
    assert await client.cmd_result("list") == [
 | 
			
		||||
        {
 | 
			
		||||
            CONF_DOMAIN: TEST_DOMAIN,
 | 
			
		||||
            CONF_CLIENT_ID: CLIENT_ID,
 | 
			
		||||
            CONF_CLIENT_SECRET: CLIENT_SECRET,
 | 
			
		||||
            "id": ID,
 | 
			
		||||
            CONF_AUTH_DOMAIN: TEST_DOMAIN,
 | 
			
		||||
        }
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    # Imported credential can be deleted
 | 
			
		||||
    await client.cmd_result("delete", {"application_credentials_id": ID})
 | 
			
		||||
    assert await client.cmd_result("list") == []
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.parametrize("config_credential", [DEVELOPER_CREDENTIAL])
 | 
			
		||||
async def test_import_duplicate_credentials(
 | 
			
		||||
    hass: HomeAssistant,
 | 
			
		||||
    ws_client: ClientFixture,
 | 
			
		||||
    config_credential: ClientCredential,
 | 
			
		||||
    import_config_credential: Any,
 | 
			
		||||
):
 | 
			
		||||
    """Exercise duplicate credentials are ignored."""
 | 
			
		||||
 | 
			
		||||
    # Import the test credential again and verify it is not imported twice
 | 
			
		||||
    await async_import_client_credential(hass, TEST_DOMAIN, DEVELOPER_CREDENTIAL)
 | 
			
		||||
    client = await ws_client()
 | 
			
		||||
    assert await client.cmd_result("list") == [
 | 
			
		||||
        {
 | 
			
		||||
            CONF_DOMAIN: TEST_DOMAIN,
 | 
			
		||||
            CONF_CLIENT_ID: CLIENT_ID,
 | 
			
		||||
            CONF_CLIENT_SECRET: CLIENT_SECRET,
 | 
			
		||||
            "id": ID,
 | 
			
		||||
            CONF_AUTH_DOMAIN: TEST_DOMAIN,
 | 
			
		||||
        }
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def test_config_flow_no_credentials(hass):
 | 
			
		||||
    """Test config flow base case with no credentials registered."""
 | 
			
		||||
    result = await hass.config_entries.flow.async_init(
 | 
			
		||||
        TEST_DOMAIN, context={"source": config_entries.SOURCE_USER}
 | 
			
		||||
    )
 | 
			
		||||
    assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT
 | 
			
		||||
    assert result.get("reason") == "missing_configuration"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def test_config_flow_other_domain(
 | 
			
		||||
    hass: HomeAssistant,
 | 
			
		||||
    ws_client: ClientFixture,
 | 
			
		||||
    authorization_server: AuthorizationServer,
 | 
			
		||||
):
 | 
			
		||||
    """Test config flow ignores credentials for another domain."""
 | 
			
		||||
    await setup_application_credentials_integration(
 | 
			
		||||
        hass,
 | 
			
		||||
        "other_domain",
 | 
			
		||||
        authorization_server,
 | 
			
		||||
    )
 | 
			
		||||
    client = await ws_client()
 | 
			
		||||
    await client.cmd_result(
 | 
			
		||||
        "create",
 | 
			
		||||
        {
 | 
			
		||||
            CONF_DOMAIN: "other_domain",
 | 
			
		||||
            CONF_CLIENT_ID: CLIENT_ID,
 | 
			
		||||
            CONF_CLIENT_SECRET: CLIENT_SECRET,
 | 
			
		||||
        },
 | 
			
		||||
    )
 | 
			
		||||
    result = await hass.config_entries.flow.async_init(
 | 
			
		||||
        TEST_DOMAIN, context={"source": config_entries.SOURCE_USER}
 | 
			
		||||
    )
 | 
			
		||||
    assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT
 | 
			
		||||
    assert result.get("reason") == "missing_configuration"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def test_config_flow(
 | 
			
		||||
    hass: HomeAssistant,
 | 
			
		||||
    ws_client: ClientFixture,
 | 
			
		||||
    oauth_fixture: OAuthFixture,
 | 
			
		||||
):
 | 
			
		||||
    """Test config flow with application credential registered."""
 | 
			
		||||
    client = await ws_client()
 | 
			
		||||
 | 
			
		||||
    await client.cmd_result(
 | 
			
		||||
        "create",
 | 
			
		||||
        {
 | 
			
		||||
            CONF_DOMAIN: TEST_DOMAIN,
 | 
			
		||||
            CONF_CLIENT_ID: CLIENT_ID,
 | 
			
		||||
            CONF_CLIENT_SECRET: CLIENT_SECRET,
 | 
			
		||||
        },
 | 
			
		||||
    )
 | 
			
		||||
    result = await hass.config_entries.flow.async_init(
 | 
			
		||||
        TEST_DOMAIN, context={"source": config_entries.SOURCE_USER}
 | 
			
		||||
    )
 | 
			
		||||
    assert result.get("type") == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP
 | 
			
		||||
    result = await oauth_fixture.complete_external_step(result)
 | 
			
		||||
    assert (
 | 
			
		||||
        result["data"].get("auth_implementation") == "fake_integration_some_client_id"
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    # Verify it is not possible to delete an in-use config entry
 | 
			
		||||
    resp = await client.cmd("delete", {"application_credentials_id": ID})
 | 
			
		||||
    assert not resp.get("success")
 | 
			
		||||
    assert "error" in resp
 | 
			
		||||
    assert resp["error"].get("code") == "unknown_error"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def test_config_flow_multiple_entries(
 | 
			
		||||
    hass: HomeAssistant,
 | 
			
		||||
    ws_client: ClientFixture,
 | 
			
		||||
    oauth_fixture: OAuthFixture,
 | 
			
		||||
):
 | 
			
		||||
    """Test config flow with multiple application credentials registered."""
 | 
			
		||||
    client = await ws_client()
 | 
			
		||||
 | 
			
		||||
    await client.cmd_result(
 | 
			
		||||
        "create",
 | 
			
		||||
        {
 | 
			
		||||
            CONF_DOMAIN: TEST_DOMAIN,
 | 
			
		||||
            CONF_CLIENT_ID: CLIENT_ID,
 | 
			
		||||
            CONF_CLIENT_SECRET: CLIENT_SECRET,
 | 
			
		||||
        },
 | 
			
		||||
    )
 | 
			
		||||
    await client.cmd_result(
 | 
			
		||||
        "create",
 | 
			
		||||
        {
 | 
			
		||||
            CONF_DOMAIN: TEST_DOMAIN,
 | 
			
		||||
            CONF_CLIENT_ID: CLIENT_ID + "2",
 | 
			
		||||
            CONF_CLIENT_SECRET: CLIENT_SECRET + "2",
 | 
			
		||||
        },
 | 
			
		||||
    )
 | 
			
		||||
    result = await hass.config_entries.flow.async_init(
 | 
			
		||||
        TEST_DOMAIN, context={"source": config_entries.SOURCE_USER}
 | 
			
		||||
    )
 | 
			
		||||
    assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM
 | 
			
		||||
    assert result.get("step_id") == "pick_implementation"
 | 
			
		||||
 | 
			
		||||
    result = await hass.config_entries.flow.async_configure(
 | 
			
		||||
        result["flow_id"],
 | 
			
		||||
        user_input={"implementation": "fake_integration_some_client_id2"},
 | 
			
		||||
    )
 | 
			
		||||
    assert result.get("type") == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP
 | 
			
		||||
    oauth_fixture.client_id = CLIENT_ID + "2"
 | 
			
		||||
    result = await oauth_fixture.complete_external_step(result)
 | 
			
		||||
    assert (
 | 
			
		||||
        result["data"].get("auth_implementation") == "fake_integration_some_client_id2"
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def test_config_flow_create_delete_credential(
 | 
			
		||||
    hass: HomeAssistant,
 | 
			
		||||
    ws_client: ClientFixture,
 | 
			
		||||
    oauth_fixture: OAuthFixture,
 | 
			
		||||
):
 | 
			
		||||
    """Test adding and deleting a credential unregisters from the config flow."""
 | 
			
		||||
    client = await ws_client()
 | 
			
		||||
 | 
			
		||||
    await client.cmd_result(
 | 
			
		||||
        "create",
 | 
			
		||||
        {
 | 
			
		||||
            CONF_DOMAIN: TEST_DOMAIN,
 | 
			
		||||
            CONF_CLIENT_ID: CLIENT_ID,
 | 
			
		||||
            CONF_CLIENT_SECRET: CLIENT_SECRET,
 | 
			
		||||
        },
 | 
			
		||||
    )
 | 
			
		||||
    await client.cmd("delete", {"application_credentials_id": ID})
 | 
			
		||||
 | 
			
		||||
    result = await hass.config_entries.flow.async_init(
 | 
			
		||||
        TEST_DOMAIN, context={"source": config_entries.SOURCE_USER}
 | 
			
		||||
    )
 | 
			
		||||
    assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT
 | 
			
		||||
    assert result.get("reason") == "missing_configuration"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.parametrize("config_credential", [DEVELOPER_CREDENTIAL])
 | 
			
		||||
async def test_config_flow_with_config_credential(
 | 
			
		||||
    hass,
 | 
			
		||||
    hass_client_no_auth,
 | 
			
		||||
    aioclient_mock,
 | 
			
		||||
    oauth_fixture,
 | 
			
		||||
    config_credential,
 | 
			
		||||
    import_config_credential,
 | 
			
		||||
):
 | 
			
		||||
    """Test config flow with application credential registered."""
 | 
			
		||||
    result = await hass.config_entries.flow.async_init(
 | 
			
		||||
        TEST_DOMAIN, context={"source": config_entries.SOURCE_USER}
 | 
			
		||||
    )
 | 
			
		||||
    assert result.get("type") == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP
 | 
			
		||||
    result = await oauth_fixture.complete_external_step(result)
 | 
			
		||||
    # Uses the imported auth domain for compatibility
 | 
			
		||||
    assert result["data"].get("auth_implementation") == TEST_DOMAIN
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.parametrize("mock_application_credentials_integration", [None])
 | 
			
		||||
async def test_import_without_setup(hass, config_credential):
 | 
			
		||||
    """Test import of credentials without setting up the integration."""
 | 
			
		||||
 | 
			
		||||
    with pytest.raises(ValueError):
 | 
			
		||||
        await async_import_client_credential(hass, TEST_DOMAIN, config_credential)
 | 
			
		||||
 | 
			
		||||
    # Config flow does not have authentication
 | 
			
		||||
    result = await hass.config_entries.flow.async_init(
 | 
			
		||||
        TEST_DOMAIN, context={"source": config_entries.SOURCE_USER}
 | 
			
		||||
    )
 | 
			
		||||
    assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT
 | 
			
		||||
    assert result.get("reason") == "missing_configuration"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.parametrize("mock_application_credentials_integration", [None])
 | 
			
		||||
async def test_websocket_without_platform(
 | 
			
		||||
    hass: HomeAssistant, ws_client: ClientFixture
 | 
			
		||||
):
 | 
			
		||||
    """Test an integration without the application credential platform."""
 | 
			
		||||
    assert await async_setup_component(hass, "application_credentials", {})
 | 
			
		||||
    hass.config.components.add(TEST_DOMAIN)
 | 
			
		||||
 | 
			
		||||
    client = await ws_client()
 | 
			
		||||
    resp = await client.cmd(
 | 
			
		||||
        "create",
 | 
			
		||||
        {
 | 
			
		||||
            CONF_DOMAIN: TEST_DOMAIN,
 | 
			
		||||
            CONF_CLIENT_ID: CLIENT_ID,
 | 
			
		||||
            CONF_CLIENT_SECRET: CLIENT_SECRET,
 | 
			
		||||
        },
 | 
			
		||||
    )
 | 
			
		||||
    assert not resp.get("success")
 | 
			
		||||
    assert "error" in resp
 | 
			
		||||
    assert resp["error"].get("code") == "invalid_format"
 | 
			
		||||
 | 
			
		||||
    # Config flow does not have authentication
 | 
			
		||||
    result = await hass.config_entries.flow.async_init(
 | 
			
		||||
        TEST_DOMAIN, context={"source": config_entries.SOURCE_USER}
 | 
			
		||||
    )
 | 
			
		||||
    assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT
 | 
			
		||||
    assert result.get("reason") == "missing_configuration"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.parametrize("mock_application_credentials_integration", [None])
 | 
			
		||||
async def test_websocket_without_authorization_server(
 | 
			
		||||
    hass: HomeAssistant, ws_client: ClientFixture
 | 
			
		||||
):
 | 
			
		||||
    """Test platform with incorrect implementation."""
 | 
			
		||||
    assert await async_setup_component(hass, "application_credentials", {})
 | 
			
		||||
    hass.config.components.add(TEST_DOMAIN)
 | 
			
		||||
 | 
			
		||||
    # Platform does not implemenent async_get_authorization_server
 | 
			
		||||
    platform = Mock()
 | 
			
		||||
    del platform.async_get_authorization_server
 | 
			
		||||
    mock_platform(
 | 
			
		||||
        hass,
 | 
			
		||||
        f"{TEST_DOMAIN}.application_credentials",
 | 
			
		||||
        platform,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    client = await ws_client()
 | 
			
		||||
    resp = await client.cmd(
 | 
			
		||||
        "create",
 | 
			
		||||
        {
 | 
			
		||||
            CONF_DOMAIN: TEST_DOMAIN,
 | 
			
		||||
            CONF_CLIENT_ID: CLIENT_ID,
 | 
			
		||||
            CONF_CLIENT_SECRET: CLIENT_SECRET,
 | 
			
		||||
        },
 | 
			
		||||
    )
 | 
			
		||||
    assert not resp.get("success")
 | 
			
		||||
    assert "error" in resp
 | 
			
		||||
    assert resp["error"].get("code") == "invalid_format"
 | 
			
		||||
 | 
			
		||||
    # Config flow does not have authentication
 | 
			
		||||
    with pytest.raises(ValueError):
 | 
			
		||||
        await hass.config_entries.flow.async_init(
 | 
			
		||||
            TEST_DOMAIN, context={"source": config_entries.SOURCE_USER}
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def test_websocket_integration_list(ws_client: ClientFixture):
 | 
			
		||||
    """Test websocket integration list command."""
 | 
			
		||||
    client = await ws_client()
 | 
			
		||||
    with patch(
 | 
			
		||||
        "homeassistant.components.application_credentials.APPLICATION_CREDENTIALS",
 | 
			
		||||
        ["example1", "example2"],
 | 
			
		||||
    ):
 | 
			
		||||
        assert await client.cmd_result("config") == {
 | 
			
		||||
            "domains": ["example1", "example2"]
 | 
			
		||||
        }
 | 
			
		||||
		Reference in New Issue
	
	Block a user