"""Test the google config flow.""" import datetime from unittest.mock import Mock, patch from oauth2client.client import ( FlowExchangeError, OAuth2Credentials, OAuth2DeviceCodeError, ) import pytest from homeassistant import config_entries from homeassistant.components.google.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.util.dt import utcnow from .conftest import ComponentSetup, YieldFixture from tests.common import MockConfigEntry, async_fire_time_changed CODE_CHECK_INTERVAL = 1 CODE_CHECK_ALARM_TIMEDELTA = datetime.timedelta(seconds=CODE_CHECK_INTERVAL * 2) @pytest.fixture(autouse=True) async def request_setup(current_request_with_host) -> None: """Request setup.""" return @pytest.fixture async def code_expiration_delta() -> datetime.timedelta: """Fixture for code expiration time, defaulting to the future.""" return datetime.timedelta(minutes=3) @pytest.fixture async def mock_code_flow( code_expiration_delta: datetime.timedelta, ) -> YieldFixture[Mock]: """Fixture for initiating OAuth flow.""" with patch( "oauth2client.client.OAuth2WebServerFlow.step1_get_device_and_user_codes", ) as mock_flow: mock_flow.return_value.user_code_expiry = utcnow() + code_expiration_delta mock_flow.return_value.interval = CODE_CHECK_INTERVAL yield mock_flow @pytest.fixture async def mock_exchange(creds: OAuth2Credentials) -> YieldFixture[Mock]: """Fixture for mocking out the exchange for credentials.""" with patch( "oauth2client.client.OAuth2WebServerFlow.step2_exchange", return_value=creds ) as mock: yield mock async def fire_alarm(hass, point_in_time): """Fire an alarm and wait for callbacks to run.""" with patch("homeassistant.util.dt.utcnow", return_value=point_in_time): async_fire_time_changed(hass, point_in_time) await hass.async_block_till_done() async def test_full_flow( hass: HomeAssistant, mock_code_flow: Mock, mock_exchange: Mock, component_setup: ComponentSetup, ) -> None: """Test successful creds setup.""" assert await component_setup() result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result.get("type") == "progress" assert result.get("step_id") == "auth" assert "description_placeholders" in result assert "url" in result["description_placeholders"] with patch( "homeassistant.components.google.async_setup_entry", return_value=True ) as mock_setup: # Run one tick to invoke the credential exchange check now = utcnow() await fire_alarm(hass, now + CODE_CHECK_ALARM_TIMEDELTA) await hass.async_block_till_done() result = await hass.config_entries.flow.async_configure( flow_id=result["flow_id"] ) assert result.get("type") == "create_entry" assert result.get("title") == "Configuration.yaml" assert "data" in result data = result["data"] assert "token" in data data["token"].pop("expires_at") data["token"].pop("expires_in") assert data == { "auth_implementation": "device_auth", "token": { "access_token": "ACCESS_TOKEN", "refresh_token": "REFRESH_TOKEN", "scope": "https://www.googleapis.com/auth/calendar", "token_type": "Bearer", }, } assert len(mock_setup.mock_calls) == 1 entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 async def test_code_error( hass: HomeAssistant, mock_code_flow: Mock, component_setup: ComponentSetup, ) -> None: """Test successful creds setup.""" assert await component_setup() with patch( "oauth2client.client.OAuth2WebServerFlow.step1_get_device_and_user_codes", side_effect=OAuth2DeviceCodeError("Test Failure"), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result.get("type") == "abort" assert result.get("reason") == "oauth_error" @pytest.mark.parametrize("code_expiration_delta", [datetime.timedelta(minutes=-5)]) async def test_expired_after_exchange( hass: HomeAssistant, mock_code_flow: Mock, component_setup: ComponentSetup, ) -> None: """Test successful creds setup.""" assert await component_setup() result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result.get("type") == "progress" assert result.get("step_id") == "auth" assert "description_placeholders" in result assert "url" in result["description_placeholders"] # Run one tick to invoke the credential exchange check now = utcnow() await fire_alarm(hass, now + CODE_CHECK_ALARM_TIMEDELTA) await hass.async_block_till_done() result = await hass.config_entries.flow.async_configure(flow_id=result["flow_id"]) assert result.get("type") == "abort" assert result.get("reason") == "code_expired" async def test_exchange_error( hass: HomeAssistant, mock_code_flow: Mock, mock_exchange: Mock, component_setup: ComponentSetup, ) -> None: """Test an error while exchanging the code for credentials.""" assert await component_setup() result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result.get("type") == "progress" assert result.get("step_id") == "auth" assert "description_placeholders" in result assert "url" in result["description_placeholders"] # Run one tick to invoke the credential exchange check now = utcnow() with patch( "oauth2client.client.OAuth2WebServerFlow.step2_exchange", side_effect=FlowExchangeError(), ): now += CODE_CHECK_ALARM_TIMEDELTA await fire_alarm(hass, now) await hass.async_block_till_done() # Status has not updated, will retry result = await hass.config_entries.flow.async_configure(flow_id=result["flow_id"]) assert result.get("type") == "progress" assert result.get("step_id") == "auth" # Run another tick, which attempts credential exchange again with patch( "homeassistant.components.google.async_setup_entry", return_value=True ) as mock_setup: now += CODE_CHECK_ALARM_TIMEDELTA await fire_alarm(hass, now) await hass.async_block_till_done() result = await hass.config_entries.flow.async_configure( flow_id=result["flow_id"] ) assert result.get("type") == "create_entry" assert result.get("title") == "Configuration.yaml" assert "data" in result data = result["data"] assert "token" in data data["token"].pop("expires_at") data["token"].pop("expires_in") assert data == { "auth_implementation": "device_auth", "token": { "access_token": "ACCESS_TOKEN", "refresh_token": "REFRESH_TOKEN", "scope": "https://www.googleapis.com/auth/calendar", "token_type": "Bearer", }, } assert len(mock_setup.mock_calls) == 1 entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 async def test_existing_config_entry( hass: HomeAssistant, config_entry: MockConfigEntry, component_setup: ComponentSetup, ) -> None: """Test can't configure when config entry already exists.""" config_entry.add_to_hass(hass) entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 assert await component_setup() result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result.get("type") == "abort" assert result.get("reason") == "already_configured" async def test_missing_configuration( hass: HomeAssistant, ) -> None: """Test can't configure when config entry already exists.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result.get("type") == "abort" assert result.get("reason") == "missing_configuration" async def test_import_config_entry_from_existing_token( hass: HomeAssistant, mock_token_read: None, component_setup: ComponentSetup, ) -> None: """Test setup with an existing token file.""" assert await component_setup() entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 data = entries[0].data assert "token" in data data["token"].pop("expires_at") data["token"].pop("expires_in") assert data == { "auth_implementation": "device_auth", "token": { "access_token": "ACCESS_TOKEN", "refresh_token": "REFRESH_TOKEN", "scope": "https://www.googleapis.com/auth/calendar", "token_type": "Bearer", }, } async def test_reauth_flow( hass: HomeAssistant, mock_code_flow: Mock, mock_exchange: Mock, component_setup: ComponentSetup, ) -> None: """Test can't configure when config entry already exists.""" config_entry = MockConfigEntry( domain=DOMAIN, data={ "auth_implementation": "device_auth", "token": {"access_token": "OLD_ACCESS_TOKEN"}, }, ) config_entry.add_to_hass(hass) entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 assert await component_setup() result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=config_entry.data ) assert result["type"] == "form" assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( flow_id=result["flow_id"], user_input={}, ) assert result.get("type") == "progress" assert result.get("step_id") == "auth" assert "description_placeholders" in result assert "url" in result["description_placeholders"] with patch( "homeassistant.components.google.async_setup_entry", return_value=True ) as mock_setup: # Run one tick to invoke the credential exchange check now = utcnow() await fire_alarm(hass, now + CODE_CHECK_ALARM_TIMEDELTA) await hass.async_block_till_done() result = await hass.config_entries.flow.async_configure( flow_id=result["flow_id"] ) assert result.get("type") == "abort" assert result.get("reason") == "reauth_successful" entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 data = entries[0].data assert "token" in data data["token"].pop("expires_at") data["token"].pop("expires_in") assert data == { "auth_implementation": "device_auth", "token": { "access_token": "ACCESS_TOKEN", "refresh_token": "REFRESH_TOKEN", "scope": "https://www.googleapis.com/auth/calendar", "token_type": "Bearer", }, } assert len(mock_setup.mock_calls) == 1