351 lines
11 KiB
Python
351 lines
11 KiB
Python
"""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
|