"""Test the Watts Vision config flow.""" from unittest.mock import patch import pytest from homeassistant import config_entries from homeassistant.components.watts.const import DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_oauth2_flow from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker from tests.typing import ClientSessionGenerator @pytest.mark.usefixtures("current_request_with_host", "mock_setup_entry") async def test_full_flow( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, ) -> None: """Test the full OAuth2 config flow.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result.get("type") is FlowResultType.EXTERNAL_STEP assert "url" in result assert OAUTH2_AUTHORIZE in result.get("url", "") assert "response_type=code" in result.get("url", "") assert "scope=" in result.get("url", "") state = config_entry_oauth2_flow._encode_jwt( hass, { "flow_id": result["flow_id"], "redirect_uri": "https://example.com/auth/external/callback", }, ) client = await hass_client_no_auth() 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" aioclient_mock.post( OAUTH2_TOKEN, json={ "refresh_token": "mock-refresh-token", "access_token": "mock-access-token", "token_type": "Bearer", "expires_in": 3600, }, ) with patch( "homeassistant.components.watts.config_flow.WattsVisionAuth.extract_user_id_from_token", return_value="user123", ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("title") == "Watts Vision +" assert "token" in result.get("data", {}) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert hass.config_entries.async_entries(DOMAIN)[0].unique_id == "user123" @pytest.mark.usefixtures("current_request_with_host") async def test_invalid_token_flow( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, ) -> None: """Test the OAuth2 config flow with invalid token.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) state = config_entry_oauth2_flow._encode_jwt( hass, { "flow_id": result["flow_id"], "redirect_uri": "https://example.com/auth/external/callback", }, ) client = await hass_client_no_auth() resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") assert resp.status == 200 aioclient_mock.post( OAUTH2_TOKEN, json={ "refresh_token": "mock-refresh-token", "access_token": "invalid-access-token", "token_type": "Bearer", "expires_in": 3600, }, ) with patch( "homeassistant.components.watts.config_flow.WattsVisionAuth.extract_user_id_from_token", return_value=None, ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "invalid_token" @pytest.mark.usefixtures("current_request_with_host") async def test_oauth_error( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, ) -> None: """Test OAuth error handling.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) state = config_entry_oauth2_flow._encode_jwt( hass, { "flow_id": result["flow_id"], "redirect_uri": "https://example.com/auth/external/callback", }, ) client = await hass_client_no_auth() resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") assert resp.status == 200 aioclient_mock.post( OAUTH2_TOKEN, json={"error": "invalid_grant"}, ) result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "oauth_error" @pytest.mark.usefixtures("current_request_with_host") async def test_oauth_timeout( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, ) -> None: """Test OAuth timeout handling.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) state = config_entry_oauth2_flow._encode_jwt( hass, { "flow_id": result["flow_id"], "redirect_uri": "https://example.com/auth/external/callback", }, ) client = await hass_client_no_auth() resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") assert resp.status == 200 aioclient_mock.post(OAUTH2_TOKEN, exc=TimeoutError()) result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "oauth_timeout" @pytest.mark.usefixtures("current_request_with_host") async def test_oauth_invalid_response( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, ) -> None: """Test OAuth invalid response handling.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) state = config_entry_oauth2_flow._encode_jwt( hass, { "flow_id": result["flow_id"], "redirect_uri": "https://example.com/auth/external/callback", }, ) client = await hass_client_no_auth() resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") assert resp.status == 200 aioclient_mock.post(OAUTH2_TOKEN, status=500, text="invalid json") result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "oauth_failed" @pytest.mark.usefixtures("current_request_with_host") async def test_unique_config_entry( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, ) -> None: """Test that duplicate config entries are not allowed.""" mock_config_entry = MockConfigEntry( domain=DOMAIN, unique_id="user123", ) mock_config_entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) state = config_entry_oauth2_flow._encode_jwt( hass, { "flow_id": result["flow_id"], "redirect_uri": "https://example.com/auth/external/callback", }, ) client = await hass_client_no_auth() resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") assert resp.status == 200 aioclient_mock.post( OAUTH2_TOKEN, json={ "refresh_token": "mock-refresh-token", "access_token": "mock-access-token", "token_type": "Bearer", "expires_in": 3600, }, ) with patch( "homeassistant.components.watts.config_flow.WattsVisionAuth.extract_user_id_from_token", return_value="user123", ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" assert len(hass.config_entries.async_entries(DOMAIN)) == 1