Compare commits
4 commits
dev
...
github_rea
Author | SHA1 | Date | |
---|---|---|---|
|
4c78115fb3 | ||
|
3ff5bbe0fd | ||
|
912c74058c | ||
|
d5eab6a56f |
7 changed files with 155 additions and 7 deletions
|
@ -27,6 +27,7 @@ import homeassistant.helpers.config_validation as cv
|
|||
from .const import (
|
||||
CLIENT_ID,
|
||||
CONF_ACCESS_TOKEN,
|
||||
CONF_REPO_SCOPE,
|
||||
CONF_REPOSITORIES,
|
||||
DEFAULT_REPOSITORIES,
|
||||
DOMAIN,
|
||||
|
@ -78,6 +79,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
self._device: GitHubDeviceAPI | None = None
|
||||
self._login: GitHubLoginOauthModel | None = None
|
||||
self._login_device: GitHubLoginDeviceModel | None = None
|
||||
self._repo_scope: bool | None = None
|
||||
self._reauth = False
|
||||
|
||||
async def async_step_user(
|
||||
self,
|
||||
|
@ -87,6 +90,18 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
if self._async_current_entries():
|
||||
return self.async_abort(reason="already_configured")
|
||||
|
||||
if not user_input:
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_REPO_SCOPE, default=False): cv.boolean,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
self._repo_scope = user_input[CONF_REPO_SCOPE]
|
||||
|
||||
return await self.async_step_device(user_input)
|
||||
|
||||
async def async_step_device(
|
||||
|
@ -94,6 +109,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
user_input: dict[str, Any] | None = None,
|
||||
) -> FlowResult:
|
||||
"""Handle device steps."""
|
||||
if existing_entry := self.hass.config_entries.async_get_entry(
|
||||
self.context["entry_id"]
|
||||
):
|
||||
self._repo_scope = existing_entry.options.get(CONF_REPO_SCOPE, False)
|
||||
|
||||
async def _wait_for_login() -> None:
|
||||
# mypy is not aware that we can't get here without having these set already
|
||||
|
@ -118,7 +137,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
)
|
||||
|
||||
try:
|
||||
response = await self._device.register()
|
||||
response = await self._device.register(
|
||||
**{"scope": "repo" if self._repo_scope else ""}
|
||||
)
|
||||
self._login_device = response.data
|
||||
except GitHubException as exception:
|
||||
LOGGER.exception(exception)
|
||||
|
@ -141,6 +162,18 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
LOGGER.exception(exception)
|
||||
return self.async_show_progress_done(next_step_id="could_not_register")
|
||||
|
||||
if existing_entry:
|
||||
# mypy is not aware that we can't get here without having this set already
|
||||
assert self._login is not None
|
||||
|
||||
self.hass.config_entries.async_update_entry(
|
||||
existing_entry, data={CONF_ACCESS_TOKEN: self._login.access_token}
|
||||
)
|
||||
await self.hass.config_entries.async_reload(existing_entry.entry_id)
|
||||
return self.async_show_progress_done(
|
||||
next_step_id="async_step_reauth_completed"
|
||||
)
|
||||
|
||||
return self.async_show_progress_done(next_step_id="repositories")
|
||||
|
||||
async def async_step_repositories(
|
||||
|
@ -170,9 +203,31 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
return self.async_create_entry(
|
||||
title="",
|
||||
data={CONF_ACCESS_TOKEN: self._login.access_token},
|
||||
options={CONF_REPOSITORIES: user_input[CONF_REPOSITORIES]},
|
||||
options={
|
||||
CONF_REPOSITORIES: user_input[CONF_REPOSITORIES],
|
||||
CONF_REPO_SCOPE: self._repo_scope,
|
||||
},
|
||||
)
|
||||
|
||||
async def async_step_reauth(
|
||||
self,
|
||||
user_input: dict[str, Any] | None = None,
|
||||
) -> FlowResult:
|
||||
"""Perform reauth upon an API authentication error."""
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
async def async_step_reauth_confirm(
|
||||
self,
|
||||
user_input: dict[str, Any] | None = None,
|
||||
) -> FlowResult:
|
||||
"""Dialog that informs the user that reauth is required."""
|
||||
if user_input is None:
|
||||
return self.async_show_form(
|
||||
step_id="reauth_confirm",
|
||||
data_schema=vol.Schema({}),
|
||||
)
|
||||
return await self.async_step_device()
|
||||
|
||||
async def async_step_could_not_register(
|
||||
self,
|
||||
user_input: dict[str, Any] | None = None,
|
||||
|
@ -180,6 +235,13 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
"""Handle issues that need transition await from progress step."""
|
||||
return self.async_abort(reason="could_not_register")
|
||||
|
||||
async def async_step_reauth_completed(
|
||||
self,
|
||||
user_input: dict[str, Any] | None = None,
|
||||
) -> FlowResult:
|
||||
"""Abort with success message for reauth complete."""
|
||||
return self.async_abort(reason="reauth_successful")
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
|
@ -222,6 +284,12 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
|||
CONF_REPOSITORIES,
|
||||
default=configured_repositories,
|
||||
): cv.multi_select({k: k for k in repositories}),
|
||||
vol.Optional(
|
||||
CONF_REPO_SCOPE,
|
||||
default=self.config_entry.options.get(
|
||||
CONF_REPO_SCOPE, False
|
||||
),
|
||||
): cv.boolean,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
|
|
@ -18,6 +18,7 @@ DEFAULT_UPDATE_INTERVAL = timedelta(seconds=300)
|
|||
|
||||
CONF_ACCESS_TOKEN = "access_token"
|
||||
CONF_REPOSITORIES = "repositories"
|
||||
CONF_REPO_SCOPE = "repo_scope"
|
||||
|
||||
|
||||
class IssuesPulls(NamedTuple):
|
||||
|
|
|
@ -15,9 +15,10 @@ from aiogithubapi import (
|
|||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, T
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DEFAULT_UPDATE_INTERVAL, DOMAIN, LOGGER, IssuesPulls
|
||||
from .const import CONF_REPO_SCOPE, DEFAULT_UPDATE_INTERVAL, DOMAIN, LOGGER, IssuesPulls
|
||||
|
||||
CoordinatorKeyType = Literal["information", "release", "issue", "commit"]
|
||||
|
||||
|
@ -25,6 +26,8 @@ CoordinatorKeyType = Literal["information", "release", "issue", "commit"]
|
|||
class GitHubBaseDataUpdateCoordinator(DataUpdateCoordinator[T]):
|
||||
"""Base class for GitHub data update coordinators."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
|
@ -84,7 +87,11 @@ class RepositoryInformationDataUpdateCoordinator(
|
|||
|
||||
async def fetch_data(self) -> GitHubResponseModel[GitHubRepositoryModel]:
|
||||
"""Get the latest data from GitHub."""
|
||||
return await self._client.repos.get(self.repository, **{"etag": self._etag})
|
||||
response = await self._client.repos.get(self.repository, **{"etag": self._etag})
|
||||
scope = "repo" if self.config_entry.options.get(CONF_REPO_SCOPE, False) else ""
|
||||
if scope != response.headers.x_oauth_scopes:
|
||||
raise ConfigEntryAuthFailed("Invalid OAuth scopes")
|
||||
return response
|
||||
|
||||
|
||||
class RepositoryReleaseDataUpdateCoordinator(
|
||||
|
|
|
@ -1,11 +1,21 @@
|
|||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "Granting full access to repositories is needed if you want to use this integration with private repositories.",
|
||||
"data": {
|
||||
"repo_scope": "Grant full access to repositories"
|
||||
}
|
||||
},
|
||||
"repositories": {
|
||||
"title": "Configure repositories",
|
||||
"data": {
|
||||
"repositories": "Select repositories to track."
|
||||
}
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"title": "[%key:common::config_flow::title::reauth%]",
|
||||
"description": "The GitHub integration needs to re-authenticate your account to match the selected scopes."
|
||||
}
|
||||
},
|
||||
"progress": {
|
||||
|
@ -13,7 +23,18 @@
|
|||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
|
||||
"could_not_register": "Could not register integration with GitHub"
|
||||
"could_not_register": "Could not register integration with GitHub",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"repositories": "[%key:component::github::config::step::repositories::data::repositories%]",
|
||||
"repo_scope": "[%key:component::github::config::step::user::data::repo_scope%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,17 +2,38 @@
|
|||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Service is already configured",
|
||||
"could_not_register": "Could not register integration with GitHub"
|
||||
"could_not_register": "Could not register integration with GitHub",
|
||||
"reauth_successful": "Re-authentication was successful"
|
||||
},
|
||||
"progress": {
|
||||
"wait_for_device": "1. Open {url} \n2.Paste the following key to authorize the integration: \n```\n{code}\n```\n"
|
||||
},
|
||||
"step": {
|
||||
"reauth_confirm": {
|
||||
"description": "The GitHub integration needs to re-authenticate your account to match the selected scopes.",
|
||||
"title": "Reauthenticate Integration"
|
||||
},
|
||||
"repositories": {
|
||||
"data": {
|
||||
"repositories": "Select repositories to track."
|
||||
},
|
||||
"title": "Configure repositories"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"repo_scope": "Grant full access to repositories"
|
||||
},
|
||||
"description": "Granting full access to repositories is needed if you want to use this integration with private repositories."
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"repo_scope": "Grant full access to repositories",
|
||||
"repositories": "Select repositories to track."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import pytest
|
|||
|
||||
from homeassistant.components.github.const import (
|
||||
CONF_ACCESS_TOKEN,
|
||||
CONF_REPO_SCOPE,
|
||||
CONF_REPOSITORIES,
|
||||
DOMAIN,
|
||||
)
|
||||
|
@ -23,8 +24,9 @@ def mock_config_entry() -> MockConfigEntry:
|
|||
return MockConfigEntry(
|
||||
title="",
|
||||
domain=DOMAIN,
|
||||
unique_id=DOMAIN,
|
||||
data={CONF_ACCESS_TOKEN: MOCK_ACCESS_TOKEN},
|
||||
options={CONF_REPOSITORIES: [TEST_REPOSITORY]},
|
||||
options={CONF_REPOSITORIES: [TEST_REPOSITORY], CONF_REPO_SCOPE: False},
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ from homeassistant import config_entries
|
|||
from homeassistant.components.github.config_flow import starred_repositories
|
||||
from homeassistant.components.github.const import (
|
||||
CONF_ACCESS_TOKEN,
|
||||
CONF_REPO_SCOPE,
|
||||
CONF_REPOSITORIES,
|
||||
DEFAULT_REPOSITORIES,
|
||||
DOMAIN,
|
||||
|
@ -62,6 +63,17 @@ async def test_full_user_flow_implementation(
|
|||
context={"source": config_entries.SOURCE_USER},
|
||||
)
|
||||
|
||||
assert result["step_id"] == "user"
|
||||
assert result["type"] == "form"
|
||||
assert "flow_id" in result
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_REPO_SCOPE: False,
|
||||
},
|
||||
)
|
||||
|
||||
assert result["step_id"] == "device"
|
||||
assert result["type"] == RESULT_TYPE_SHOW_PROGRESS
|
||||
assert "flow_id" in result
|
||||
|
@ -92,10 +104,19 @@ async def test_flow_with_registration_failure(
|
|||
"https://github.com/login/device/code",
|
||||
exc=GitHubException("Registration failed"),
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_USER},
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_REPO_SCOPE: False,
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] == RESULT_TYPE_ABORT
|
||||
assert result.get("reason") == "could_not_register"
|
||||
|
||||
|
@ -124,6 +145,13 @@ async def test_flow_with_activation_failure(
|
|||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_USER},
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_REPO_SCOPE: False,
|
||||
},
|
||||
)
|
||||
|
||||
assert result["step_id"] == "device"
|
||||
assert result["type"] == RESULT_TYPE_SHOW_PROGRESS
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue