"""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", "mock_setup_entry") async def test_reauth_flow( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, mock_config_entry: MockConfigEntry, ) -> None: """Test the reauthentication flow.""" mock_config_entry.add_to_hass(hass) result = await mock_config_entry.start_reauth_flow(hass) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) 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": "new-refresh-token", "access_token": "new-access-token", "token_type": "Bearer", "expires_in": 3600, }, ) with patch( "homeassistant.components.watts.config_flow.WattsVisionAuth.extract_user_id_from_token", return_value="test-user-id", ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" mock_config_entry.data["token"].pop("expires_at") assert mock_config_entry.data["token"] == { "refresh_token": "new-refresh-token", "access_token": "new-access-token", "token_type": "Bearer", "expires_in": 3600, } @pytest.mark.usefixtures("current_request_with_host") async def test_reauth_account_mismatch( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, mock_config_entry: MockConfigEntry, ) -> None: """Test reauthentication with a different account aborts.""" mock_config_entry.add_to_hass(hass) result = await mock_config_entry.start_reauth_flow(hass) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) 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": "new-refresh-token", "access_token": "new-access-token", "token_type": "Bearer", "expires_in": 3600, }, ) with patch( "homeassistant.components.watts.config_flow.WattsVisionAuth.extract_user_id_from_token", return_value="different-user-id", ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "account_mismatch" @pytest.mark.usefixtures("current_request_with_host", "mock_setup_entry") async def test_reconfigure_flow( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, mock_config_entry: MockConfigEntry, ) -> None: """Test the reconfiguration flow.""" mock_config_entry.add_to_hass(hass) result = await mock_config_entry.start_reconfigure_flow(hass) 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": "new-refresh-token", "access_token": "new-access-token", "token_type": "Bearer", "expires_in": 3600, }, ) with patch( "homeassistant.components.watts.config_flow.WattsVisionAuth.extract_user_id_from_token", return_value="test-user-id", ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reconfigure_successful" mock_config_entry.data["token"].pop("expires_at") assert mock_config_entry.data["token"] == { "refresh_token": "new-refresh-token", "access_token": "new-access-token", "token_type": "Bearer", "expires_in": 3600, } @pytest.mark.usefixtures("current_request_with_host") async def test_reconfigure_account_mismatch( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, mock_config_entry: MockConfigEntry, ) -> None: """Test reconfiguration with a different account aborts.""" mock_config_entry.add_to_hass(hass) result = await mock_config_entry.start_reconfigure_flow(hass) 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": "new-refresh-token", "access_token": "new-access-token", "token_type": "Bearer", "expires_in": 3600, }, ) with patch( "homeassistant.components.watts.config_flow.WattsVisionAuth.extract_user_id_from_token", return_value="different-user-id", ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "account_mismatch" @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