Skip to content

Commit 9d32d31

Browse files
committed
Increased test coverage
1 parent 6d51414 commit 9d32d31

File tree

5 files changed

+324
-207
lines changed

5 files changed

+324
-207
lines changed

custom_components/generac/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
3737
client = GeneracApiClient(session, username, password, session_cookie)
3838

3939
coordinator = GeneracDataUpdateCoordinator(hass, client=client, config_entry=entry)
40-
await coordinator.async_config_entry_first_refresh()
40+
try:
41+
await coordinator.async_config_entry_first_refresh()
42+
except Exception as e:
43+
raise ConfigEntryNotReady from e
4144

4245
if not coordinator.last_update_success:
4346
raise ConfigEntryNotReady

custom_components/generac/api.py

Lines changed: 6 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@
1212
from .models import Apparatus
1313
from .models import ApparatusDetail
1414
from .models import Item
15-
from .models import SelfAssertedResponse
16-
from .models import SignInConfig
1715

1816
API_BASE = "https://app.mobilelinkgen.com/api"
1917
LOGIN_BASE = "https://generacconnectivity.b2clogin.com/generacconnectivity.onmicrosoft.com/B2C_1A_MobileLink_SignIn"
@@ -32,12 +30,6 @@ class SessionExpiredException(Exception):
3230
pass
3331

3432

35-
def get_setting_json(page: str) -> Mapping[str, Any] | None:
36-
for line in page.splitlines():
37-
if line.startswith("var SETTINGS = ") and line.endswith(";"):
38-
return json.loads(line.removeprefix("var SETTINGS = ").removesuffix(";"))
39-
40-
4133
class GeneracApiClient:
4234
def __init__(
4335
self,
@@ -64,16 +56,13 @@ def __init__(
6456

6557
async def async_get_data(self) -> dict[str, Item] | None:
6658
"""Get data from the API."""
67-
try:
68-
if self._session_cookie:
69-
self._headers["Cookie"] = self._session_cookie
70-
self._logged_in = True
71-
elif not self._logged_in:
72-
await self.login()
73-
self._logged_in = True
74-
except SessionExpiredException:
59+
if self._session_cookie:
60+
self._headers["Cookie"] = self._session_cookie
61+
self._logged_in = True
62+
else:
7563
self._logged_in = False
76-
return await self.async_get_data()
64+
_LOGGER.error("No session cookie provided, cannot login")
65+
raise InvalidCredentialsException("No session cookie provided")
7766
return await self.get_device_data()
7867

7968
async def get_device_data(self):
@@ -128,107 +117,3 @@ async def get_endpoint(self, endpoint: str):
128117
raise
129118
except Exception as ex:
130119
raise IOError() from ex
131-
132-
async def login(self) -> None:
133-
"""Login to API"""
134-
headers = {**self._headers}
135-
136-
login_response = await (
137-
await self._session.get(
138-
f"{API_BASE}/Auth/SignIn?email={self._username}",
139-
headers=headers,
140-
allow_redirects=True,
141-
)
142-
).text()
143-
144-
if await self.submit_form(login_response):
145-
return
146-
147-
parse_settings = get_setting_json(login_response)
148-
if parse_settings is None:
149-
_LOGGER.debug(
150-
"Unable to find csrf token in login page:\n%s", login_response
151-
)
152-
raise IOError("Unable to find csrf token in login page")
153-
sign_in_config = from_dict(SignInConfig, parse_settings)
154-
155-
form_data = aiohttp.FormData()
156-
form_data.add_field("request_type", "RESPONSE")
157-
form_data.add_field("signInName", self._username)
158-
form_data.add_field("password", self._password)
159-
if sign_in_config.csrf is None or sign_in_config.transId is None:
160-
raise IOError(
161-
"Missing csrf and/or transId in sign in config %s", sign_in_config
162-
)
163-
self.csrf = sign_in_config.csrf
164-
165-
headers = {**self._headers}
166-
headers["X-Csrf-Token"] = sign_in_config.csrf
167-
168-
self_asserted_response = await self._session.post(
169-
f"{LOGIN_BASE}/SelfAsserted",
170-
headers=headers,
171-
params={
172-
"tx": "StateProperties=" + sign_in_config.transId,
173-
"p": "B2C_1A_SignUpOrSigninOnline",
174-
},
175-
data=form_data,
176-
)
177-
178-
if self_asserted_response.status != 200:
179-
raise IOError(
180-
f"SelfAsserted: Bad response status: {self_asserted_response.status}"
181-
)
182-
satxt = await self_asserted_response.text()
183-
184-
sa = from_dict(SelfAssertedResponse, json.loads(satxt))
185-
186-
if sa.status != "200":
187-
raise InvalidCredentialsException()
188-
189-
confirmed_response = await self._session.get(
190-
f"{LOGIN_BASE}/api/CombinedSigninAndSignup/confirmed",
191-
headers=headers,
192-
params={
193-
"csrf_token": sign_in_config.csrf,
194-
"tx": "StateProperties=" + sign_in_config.transId,
195-
"p": "B2C_1A_SignUpOrSigninOnline",
196-
},
197-
)
198-
199-
if confirmed_response.status != 200:
200-
raise IOError(
201-
f"CombinedSigninAndSignup: Bad response status: {confirmed_response.status}"
202-
)
203-
204-
loginString = await confirmed_response.text()
205-
if not await self.submit_form(loginString):
206-
raise IOError("Error parsing HTML submit form")
207-
208-
async def submit_form(self, response: str) -> bool:
209-
login_page = BeautifulSoup(response, features="html.parser")
210-
form = login_page.select("form")
211-
login_state = login_page.select("input[name=state]")
212-
login_code = login_page.select("input[name=code]")
213-
214-
if len(form) == 0 or len(login_state) == 0 or len(login_code) == 0:
215-
_LOGGER.info("Could not load login page")
216-
return False
217-
218-
form = form[0]
219-
login_state = login_state[0]
220-
login_code = login_code[0]
221-
222-
action = form.attrs["action"]
223-
224-
form_data = aiohttp.FormData()
225-
form_data.add_field("state", login_state.attrs["value"])
226-
form_data.add_field("code", login_code.attrs["value"])
227-
228-
login_response = await self._session.post(
229-
action, data=form_data, headers=self._headers
230-
)
231-
232-
if login_response.status != 200:
233-
raise IOError(f"Bad api login response: {login_response.status}")
234-
return True

tests/conftest.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
"""Global fixtures for generac integration."""
2+
from unittest.mock import patch
3+
24
import pytest
5+
from custom_components.generac.coordinator import GeneracDataUpdateCoordinator
36

47
pytest_plugins = "pytest_homeassistant_custom_component"
58

@@ -8,3 +11,27 @@
811
def auto_enable_custom_integrations(enable_custom_integrations):
912
"""Enable custom integrations defined in the test dir."""
1013
yield
14+
15+
16+
@pytest.fixture
17+
def bypass_get_data():
18+
"""Bypass coordinator get data."""
19+
20+
async def mock_first_refresh(self):
21+
self.last_update_success = True
22+
23+
with patch(
24+
"custom_components.generac.coordinator.GeneracDataUpdateCoordinator.async_config_entry_first_refresh",
25+
mock_first_refresh,
26+
):
27+
yield
28+
29+
30+
@pytest.fixture
31+
def error_on_get_data():
32+
"""Simulate error when coordinator get data."""
33+
with patch(
34+
"custom_components.generac.api.GeneracApiClient.async_get_data",
35+
side_effect=Exception,
36+
):
37+
yield

0 commit comments

Comments
 (0)