Add extra tests to flick_electric (#138017)

Add extra tests to flick_electric
This commit is contained in:
Brynley McDonald 2025-02-10 11:40:34 +13:00 committed by GitHub
parent 9467709068
commit 0408e732d7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 795 additions and 581 deletions

View File

@ -7,15 +7,26 @@ from homeassistant.components.flick_electric.const import (
CONF_SUPPLY_NODE_REF, CONF_SUPPLY_NODE_REF,
) )
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
CONF = { CONF = {
CONF_USERNAME: "test-username", CONF_USERNAME: "9973debf-963f-49b0-9a73-ba9c3400cbed@anonymised.example.com",
CONF_PASSWORD: "test-password", CONF_PASSWORD: "test-password",
CONF_ACCOUNT_ID: "1234", CONF_ACCOUNT_ID: "134800",
CONF_SUPPLY_NODE_REF: "123", CONF_SUPPLY_NODE_REF: "/network/nz/supply_nodes/ed7617df-4b10-4c8a-a05d-deadbeef8299",
} }
async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None:
"""Fixture for setting up the component."""
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
def _mock_flick_price(): def _mock_flick_price():
return FlickPrice( return FlickPrice(
{ {

View File

@ -0,0 +1,105 @@
"""Flick Electric tests configuration."""
from collections.abc import Generator
from unittest.mock import AsyncMock, patch
import json_api_doc
from pyflick import FlickPrice
import pytest
from homeassistant.components.flick_electric.const import CONF_ACCOUNT_ID, DOMAIN
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from . import CONF
from tests.common import MockConfigEntry, load_json_value_fixture
@pytest.fixture
def mock_config_entry() -> MockConfigEntry:
"""Mock a config entry."""
return MockConfigEntry(
domain=DOMAIN,
title="123 Fake Street, Newtown, Wellington 6021",
data={**CONF},
version=2,
entry_id="974e52a5c0724d17b7ed876dd6ff4bc8",
unique_id=CONF[CONF_ACCOUNT_ID],
)
@pytest.fixture
def mock_old_config_entry() -> MockConfigEntry:
"""Mock an outdated config entry."""
return MockConfigEntry(
domain=DOMAIN,
data={
CONF_USERNAME: CONF[CONF_USERNAME],
CONF_PASSWORD: CONF[CONF_PASSWORD],
},
title=CONF[CONF_USERNAME],
unique_id=CONF[CONF_USERNAME],
version=1,
)
@pytest.fixture
def mock_flick_client() -> Generator[AsyncMock]:
"""Mock a Flick Electric client."""
with (
patch(
"homeassistant.components.flick_electric.FlickAPI",
autospec=True,
) as mock_api,
patch(
"homeassistant.components.flick_electric.config_flow.FlickAPI",
new=mock_api,
),
patch(
"homeassistant.components.flick_electric.config_flow.SimpleFlickAuth.async_get_access_token",
return_value="123456789abcdef",
),
):
api = mock_api.return_value
api.getCustomerAccounts.return_value = json_api_doc.deserialize(
load_json_value_fixture("accounts.json", DOMAIN)
)
api.getPricing.return_value = FlickPrice(
json_api_doc.deserialize(
load_json_value_fixture("rated_period.json", DOMAIN)
)
)
yield api
@pytest.fixture
def mock_flick_client_multiple() -> Generator[AsyncMock]:
"""Mock a Flick Electric with multiple accounts."""
with (
patch(
"homeassistant.components.flick_electric.FlickAPI",
autospec=True,
) as mock_api,
patch(
"homeassistant.components.flick_electric.config_flow.FlickAPI",
new=mock_api,
),
patch(
"homeassistant.components.flick_electric.config_flow.SimpleFlickAuth.async_get_access_token",
return_value="123456789abcdef",
),
):
api = mock_api.return_value
api.getCustomerAccounts.return_value = json_api_doc.deserialize(
load_json_value_fixture("accounts_multi.json", DOMAIN)
)
api.getPricing.return_value = FlickPrice(
json_api_doc.deserialize(
load_json_value_fixture("rated_period.json", DOMAIN)
)
)
yield api

View File

@ -0,0 +1,105 @@
{
"data": [
{
"id": "134800",
"type": "customer_account",
"attributes": {
"account_number": "10123404",
"billing_name": "9973debf-963f-49b0-9a73-Ba9c3400cbed@Anonymised Example",
"billing_email": null,
"address": "123 Fake Street, Newtown, Wellington 6021",
"brand": "flick",
"vulnerability_state": "none",
"medical_dependency": false,
"status": "active",
"start_at": "2023-03-02T00:00:00.000+13:00",
"end_at": null,
"application_id": "5dfc4978-07de-4d18-8ef7-055603805ba6",
"active": true,
"on_join_journey": false,
"placeholder": false
},
"relationships": {
"user": {
"data": {
"id": "106676",
"type": "customer_user"
}
},
"sign_up": {
"data": {
"id": "877039",
"type": "customer_sign_up"
}
},
"main_customer": {
"data": {
"id": "108335",
"type": "customer_customer"
}
},
"main_consumer": {
"data": {
"id": "108291",
"type": "customer_icp_consumer"
}
},
"primary_contact": {
"data": {
"id": "121953",
"type": "customer_contact"
}
},
"default_payment_method": {
"data": {
"id": "602801",
"type": "customer_payment_method"
}
},
"phone_numbers": {
"data": [
{
"id": "111604",
"type": "customer_phone_number"
}
]
},
"payment_methods": {
"data": [
{
"id": "602801",
"type": "customer_payment_method"
}
]
}
}
}
],
"included": [
{
"id": "108291",
"type": "customer_icp_consumer",
"attributes": {
"start_date": "2023-03-02",
"end_date": null,
"icp_number": "0001234567UNB12",
"supply_node_ref": "/network/nz/supply_nodes/ed7617df-4b10-4c8a-a05d-deadbeef8299",
"physical_address": "123 FAKE STREET,NEWTOWN,WELLINGTON,6021"
}
}
],
"meta": {
"verb": "get",
"type": "customer_account",
"params": [],
"permission": {
"uri": "flick:customer_app:resource:account:list",
"data_context": null
},
"host": "https://api.flickuat.com",
"service": "customer",
"path": "/accounts",
"description": "Returns the accounts viewable by the current user",
"respond_with_array": true
}
}

View File

@ -0,0 +1,144 @@
{
"data": [
{
"id": "134800",
"type": "customer_account",
"attributes": {
"account_number": "10123404",
"billing_name": "9973debf-963f-49b0-9a73-Ba9c3400cbed@Anonymised Example",
"billing_email": null,
"address": "123 Fake Street, Newtown, Wellington 6021",
"brand": "flick",
"vulnerability_state": "none",
"medical_dependency": false,
"status": "active",
"start_at": "2023-03-02T00:00:00.000+13:00",
"end_at": null,
"application_id": "5dfc4978-07de-4d18-8ef7-055603805ba6",
"active": true,
"on_join_journey": false,
"placeholder": false
},
"relationships": {
"user": {
"data": {
"id": "106676",
"type": "customer_user"
}
},
"sign_up": {
"data": {
"id": "877039",
"type": "customer_sign_up"
}
},
"main_customer": {
"data": {
"id": "108335",
"type": "customer_customer"
}
},
"main_consumer": {
"data": {
"id": "108291",
"type": "customer_icp_consumer"
}
},
"primary_contact": {
"data": {
"id": "121953",
"type": "customer_contact"
}
},
"default_payment_method": {
"data": {
"id": "602801",
"type": "customer_payment_method"
}
},
"phone_numbers": {
"data": [
{
"id": "111604",
"type": "customer_phone_number"
}
]
},
"payment_methods": {
"data": [
{
"id": "602801",
"type": "customer_payment_method"
}
]
}
}
},
{
"id": "123456",
"type": "customer_account",
"attributes": {
"account_number": "123123123",
"billing_name": "9973debf-963f-49b0-9a73-Ba9c3400cbed@Anonymised Example",
"billing_email": null,
"address": "456 Fake Street, Newtown, Wellington 6021",
"brand": "flick",
"vulnerability_state": "none",
"medical_dependency": false,
"status": "active",
"start_at": "2023-03-02T00:00:00.000+13:00",
"end_at": null,
"application_id": "5dfc4978-07de-4d18-8ef7-055603805ba6",
"active": true,
"on_join_journey": false,
"placeholder": false
},
"relationships": {
"main_consumer": {
"data": {
"id": "11223344",
"type": "customer_icp_consumer"
}
}
}
}
],
"included": [
{
"id": "108291",
"type": "customer_icp_consumer",
"attributes": {
"start_date": "2023-03-02",
"end_date": null,
"icp_number": "0001234567UNB12",
"supply_node_ref": "/network/nz/supply_nodes/ed7617df-4b10-4c8a-a05d-deadbeef8299",
"physical_address": "123 FAKE STREET,NEWTOWN,WELLINGTON,6021"
}
},
{
"id": "11223344",
"type": "customer_icp_consumer",
"attributes": {
"start_date": "2023-03-02",
"end_date": null,
"icp_number": "9991234567UNB12",
"supply_node_ref": "/network/nz/supply_nodes/ed7617df-4b10-4c8a-a05d-deadbeef1234",
"physical_address": "456 FAKE STREET,NEWTOWN,WELLINGTON,6021"
}
}
],
"meta": {
"verb": "get",
"type": "customer_account",
"params": [],
"permission": {
"uri": "flick:customer_app:resource:account:list",
"data_context": null
},
"host": "https://api.flickuat.com",
"service": "customer",
"path": "/accounts",
"description": "Returns the accounts viewable by the current user",
"respond_with_array": true
}
}

View File

@ -0,0 +1,112 @@
{
"data": {
"id": "_2025-02-09 05:30:00 UTC..2025-02-09 05:59:59 UTC",
"type": "rating_rated_period",
"attributes": {
"start_at": "2025-02-09T05:30:00.000Z",
"end_at": "2025-02-09T05:59:59.000Z",
"status": "final",
"cost": "0.20011",
"import_cost": "0.20011",
"export_cost": null,
"cost_unit": "NZD",
"quantity": "1.0",
"import_quantity": "1.0",
"export_quantity": null,
"quantity_unit": "kwh",
"renewable_quantity": null,
"generation_price_contract": null
},
"relationships": {
"components": {
"data": [
{
"id": "213507464_1_kwh_generation_UN_24_default_2025-02-09 05:30:00 UTC..2025-02-09 05:59:59 UTC",
"type": "rating_component"
},
{
"id": "213507464_1_kwh_network_UN_24_offpeak_2025-02-09 05:30:00 UTC..2025-02-09 05:59:59 UTC",
"type": "rating_component"
}
]
}
}
},
"included": [
{
"id": "213507464_1_kwh_generation_UN_24_default_2025-02-09 05:30:00 UTC..2025-02-09 05:59:59 UTC",
"type": "rating_component",
"attributes": {
"charge_method": "kwh",
"charge_setter": "generation",
"value": "0.20011",
"quantity": "1.0",
"unit_code": "NZD",
"charge_per": "kwh",
"flow_direction": "import",
"content_code": "UN",
"hours_of_availability": 24,
"channel_number": 1,
"meter_serial_number": "213507464",
"price_name": "default",
"applicable_periods": [],
"single_unit_price": "0.20011",
"billable": true,
"renewable_quantity": null,
"generation_price_contract": "FLICK_FLAT_2024_04_01_midpoint"
}
},
{
"id": "213507464_1_kwh_network_UN_24_offpeak_2025-02-09 05:30:00 UTC..2025-02-09 05:59:59 UTC",
"type": "rating_component",
"attributes": {
"charge_method": "kwh",
"charge_setter": "network",
"value": "0.0406",
"quantity": "1.0",
"unit_code": "NZD",
"charge_per": "kwh",
"flow_direction": "import",
"content_code": "UN",
"hours_of_availability": 24,
"channel_number": 1,
"meter_serial_number": "213507464",
"price_name": "offpeak",
"applicable_periods": [],
"single_unit_price": "0.0406",
"billable": false,
"renewable_quantity": null,
"generation_price_contract": "FLICK_FLAT_2024_04_01_midpoint"
}
}
],
"meta": {
"verb": "get",
"type": "rating_rated_period",
"params": [
{
"name": "supply_node_ref",
"type": "String",
"description": "The supply node to rate",
"example": "/network/nz/supply_nodes/bccd6f52-448b-4edf-a0c1-459ee67d215b",
"required": true
},
{
"name": "as_at",
"type": "DateTime",
"description": "The time to rate the supply node at; defaults to the current time",
"example": "2023-04-01T15:20:15-07:00",
"required": false
}
],
"permission": {
"uri": "flick:rating:resource:rated_period:show",
"data_context": "supply_node"
},
"host": "https://api.flickuat.com",
"service": "rating",
"path": "/rated_period",
"description": "Fetch a rated period for a supply node in a specific point in time",
"respond_with_array": false
}
}

View File

@ -1,6 +1,6 @@
"""Test the Flick Electric config flow.""" """Test the Flick Electric config flow."""
from unittest.mock import patch from unittest.mock import AsyncMock, patch
from pyflick.authentication import AuthException from pyflick.authentication import AuthException
from pyflick.types import APIException from pyflick.types import APIException
@ -16,10 +16,16 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType from homeassistant.data_entry_flow import FlowResultType
from . import CONF, _mock_flick_price from . import CONF, setup_integration
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
# From test fixtures
ACCOUNT_NAME_1 = "123 Fake Street, Newtown, Wellington 6021"
ACCOUNT_NAME_2 = "456 Fake Street, Newtown, Wellington 6021"
ACCOUNT_ID_2 = "123456"
SUPPLY_NODE_REF_2 = "/network/nz/supply_nodes/ed7617df-4b10-4c8a-a05d-deadbeef1234"
async def _flow_submit(hass: HomeAssistant) -> ConfigFlowResult: async def _flow_submit(hass: HomeAssistant) -> ConfigFlowResult:
return await hass.config_entries.flow.async_init( return await hass.config_entries.flow.async_init(
@ -32,7 +38,7 @@ async def _flow_submit(hass: HomeAssistant) -> ConfigFlowResult:
) )
async def test_form(hass: HomeAssistant) -> None: async def test_form(hass: HomeAssistant, mock_flick_client: AsyncMock) -> None:
"""Test we get the form with only one, with no account picker.""" """Test we get the form with only one, with no account picker."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
@ -41,48 +47,24 @@ async def test_form(hass: HomeAssistant) -> None:
assert result["type"] is FlowResultType.FORM assert result["type"] is FlowResultType.FORM
assert result["errors"] == {} assert result["errors"] == {}
with ( result2 = await hass.config_entries.flow.async_configure(
patch( result["flow_id"],
"homeassistant.components.flick_electric.config_flow.SimpleFlickAuth.async_get_access_token", {
return_value="123456789abcdef", CONF_USERNAME: CONF[CONF_USERNAME],
), CONF_PASSWORD: CONF[CONF_PASSWORD],
patch( },
"homeassistant.components.flick_electric.config_flow.FlickAPI.getCustomerAccounts", )
return_value=[ await hass.async_block_till_done()
{
"id": "1234",
"status": "active",
"address": "123 Fake St",
"main_consumer": {"supply_node_ref": "123"},
}
],
),
patch(
"homeassistant.components.flick_electric.config_flow.FlickAPI.getPricing",
return_value=_mock_flick_price(),
),
patch(
"homeassistant.components.flick_electric.async_setup_entry",
return_value=True,
) as mock_setup_entry,
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_USERNAME: "test-username",
CONF_PASSWORD: "test-password",
},
)
await hass.async_block_till_done()
assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["type"] is FlowResultType.CREATE_ENTRY
assert result2["title"] == "123 Fake St" assert result2["title"] == ACCOUNT_NAME_1
assert result2["data"] == CONF assert result2["data"] == CONF
assert result2["result"].unique_id == "1234" assert result2["result"].unique_id == CONF[CONF_ACCOUNT_ID]
assert len(mock_setup_entry.mock_calls) == 1
async def test_form_multi_account(hass: HomeAssistant) -> None: async def test_form_multi_account(
hass: HomeAssistant, mock_flick_client_multiple: AsyncMock
) -> None:
"""Test the form when multiple accounts are available.""" """Test the form when multiple accounts are available."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
@ -91,272 +73,114 @@ async def test_form_multi_account(hass: HomeAssistant) -> None:
assert result["type"] is FlowResultType.FORM assert result["type"] is FlowResultType.FORM
assert result["errors"] == {} assert result["errors"] == {}
with ( result2 = await hass.config_entries.flow.async_configure(
patch( result["flow_id"],
"homeassistant.components.flick_electric.config_flow.SimpleFlickAuth.async_get_access_token", {
return_value="123456789abcdef", CONF_USERNAME: CONF[CONF_USERNAME],
), CONF_PASSWORD: CONF[CONF_PASSWORD],
patch( },
"homeassistant.components.flick_electric.config_flow.FlickAPI.getCustomerAccounts", )
return_value=[ await hass.async_block_till_done()
{
"id": "1234",
"status": "active",
"address": "123 Fake St",
"main_consumer": {"supply_node_ref": "123"},
},
{
"id": "5678",
"status": "active",
"address": "456 Fake St",
"main_consumer": {"supply_node_ref": "456"},
},
],
),
patch(
"homeassistant.components.flick_electric.config_flow.FlickAPI.getPricing",
return_value=_mock_flick_price(),
),
patch(
"homeassistant.components.flick_electric.async_setup_entry",
return_value=True,
) as mock_setup_entry,
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_USERNAME: "test-username",
CONF_PASSWORD: "test-password",
},
)
await hass.async_block_till_done()
assert result2["type"] is FlowResultType.FORM assert result2["type"] is FlowResultType.FORM
assert result2["step_id"] == "select_account" assert result2["step_id"] == "select_account"
assert len(mock_setup_entry.mock_calls) == 0
result3 = await hass.config_entries.flow.async_configure( result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"], result2["flow_id"],
{"account_id": "5678"}, {CONF_ACCOUNT_ID: ACCOUNT_ID_2},
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["type"] is FlowResultType.CREATE_ENTRY
assert result3["title"] == "456 Fake St" assert result3["title"] == ACCOUNT_NAME_2
assert result3["data"] == { assert result3["data"] == {
**CONF, **CONF,
CONF_SUPPLY_NODE_REF: "456", CONF_SUPPLY_NODE_REF: SUPPLY_NODE_REF_2,
CONF_ACCOUNT_ID: "5678", CONF_ACCOUNT_ID: ACCOUNT_ID_2,
} }
assert result3["result"].unique_id == "5678" assert result3["result"].unique_id == ACCOUNT_ID_2
assert len(mock_setup_entry.mock_calls) == 1
async def test_reauth_token(hass: HomeAssistant) -> None: async def test_reauth_token(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_flick_client: AsyncMock,
) -> None:
"""Test reauth flow when username/password is wrong.""" """Test reauth flow when username/password is wrong."""
entry = MockConfigEntry( await setup_integration(hass, mock_config_entry)
domain=DOMAIN,
data={**CONF}, with patch(
title="123 Fake St", "homeassistant.components.flick_electric.config_flow.SimpleFlickAuth.async_get_access_token",
unique_id="1234", side_effect=AuthException,
version=2, ):
result = await mock_config_entry.start_reauth_flow(hass)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {"base": "invalid_auth"}
assert result["step_id"] == "user"
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_USERNAME: CONF[CONF_USERNAME], CONF_PASSWORD: CONF[CONF_PASSWORD]},
) )
entry.add_to_hass(hass)
with ( assert result2["type"] is FlowResultType.ABORT
patch( assert result2["reason"] == "reauth_successful"
"homeassistant.components.flick_electric.config_flow.SimpleFlickAuth.async_get_access_token",
side_effect=AuthException,
),
):
result = await entry.start_reauth_flow(hass)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {"base": "invalid_auth"}
assert result["step_id"] == "user"
with (
patch(
"homeassistant.components.flick_electric.config_flow.SimpleFlickAuth.async_get_access_token",
return_value="123456789abcdef",
),
patch(
"homeassistant.components.flick_electric.config_flow.FlickAPI.getCustomerAccounts",
return_value=[
{
"id": "1234",
"status": "active",
"address": "123 Fake St",
"main_consumer": {"supply_node_ref": "123"},
},
],
),
patch(
"homeassistant.components.flick_electric.config_flow.FlickAPI.getPricing",
return_value=_mock_flick_price(),
),
patch(
"homeassistant.config_entries.ConfigEntries.async_update_entry",
return_value=True,
) as mock_update_entry,
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_USERNAME: "test-username",
CONF_PASSWORD: "test-password",
},
)
assert result2["type"] is FlowResultType.ABORT
assert result2["reason"] == "reauth_successful"
assert len(mock_update_entry.mock_calls) > 0
async def test_form_reauth_migrate(hass: HomeAssistant) -> None: async def test_form_reauth_migrate(
hass: HomeAssistant,
mock_old_config_entry: MockConfigEntry,
mock_flick_client: AsyncMock,
) -> None:
"""Test reauth flow for v1 with single account.""" """Test reauth flow for v1 with single account."""
entry = MockConfigEntry( mock_old_config_entry.add_to_hass(hass)
domain=DOMAIN, result = await mock_old_config_entry.start_reauth_flow(hass)
data={
CONF_USERNAME: "test-username",
CONF_PASSWORD: "test-password",
},
title="123 Fake St",
unique_id="test-username",
version=1,
)
entry.add_to_hass(hass)
with ( assert result["type"] is FlowResultType.ABORT
patch( assert result["reason"] == "reauth_successful"
"homeassistant.components.flick_electric.config_flow.SimpleFlickAuth.async_get_access_token", assert mock_old_config_entry.version == 2
return_value="123456789abcdef", assert mock_old_config_entry.unique_id == CONF[CONF_ACCOUNT_ID]
), assert mock_old_config_entry.data == CONF
patch(
"homeassistant.components.flick_electric.config_flow.FlickAPI.getCustomerAccounts",
return_value=[
{
"id": "1234",
"status": "active",
"address": "123 Fake St",
"main_consumer": {"supply_node_ref": "123"},
},
],
),
patch(
"homeassistant.components.flick_electric.config_flow.FlickAPI.getPricing",
return_value=_mock_flick_price(),
),
):
result = await entry.start_reauth_flow(hass)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "reauth_successful"
assert entry.version == 2
assert entry.unique_id == "1234"
assert entry.data == CONF
async def test_form_reauth_migrate_multi_account(hass: HomeAssistant) -> None: async def test_form_reauth_migrate_multi_account(
hass: HomeAssistant,
mock_old_config_entry: MockConfigEntry,
mock_flick_client_multiple: AsyncMock,
) -> None:
"""Test the form when multiple accounts are available.""" """Test the form when multiple accounts are available."""
mock_old_config_entry.add_to_hass(hass)
result = await mock_old_config_entry.start_reauth_flow(hass)
entry = MockConfigEntry( assert result["type"] is FlowResultType.FORM
domain=DOMAIN, assert result["step_id"] == "select_account"
data={
CONF_USERNAME: "test-username", result2 = await hass.config_entries.flow.async_configure(
CONF_PASSWORD: "test-password", result["flow_id"],
}, {CONF_ACCOUNT_ID: CONF[CONF_ACCOUNT_ID]},
title="123 Fake St",
unique_id="test-username",
version=1,
) )
entry.add_to_hass(hass)
with ( await hass.async_block_till_done()
patch(
"homeassistant.components.flick_electric.config_flow.SimpleFlickAuth.async_get_access_token",
return_value="123456789abcdef",
),
patch(
"homeassistant.components.flick_electric.config_flow.FlickAPI.getCustomerAccounts",
return_value=[
{
"id": "1234",
"status": "active",
"address": "123 Fake St",
"main_consumer": {"supply_node_ref": "123"},
},
{
"id": "5678",
"status": "active",
"address": "456 Fake St",
"main_consumer": {"supply_node_ref": "456"},
},
],
),
patch(
"homeassistant.components.flick_electric.config_flow.FlickAPI.getPricing",
return_value=_mock_flick_price(),
),
):
result = await entry.start_reauth_flow(hass)
assert result["type"] is FlowResultType.FORM assert result2["type"] is FlowResultType.ABORT
assert result["step_id"] == "select_account" assert result2["reason"] == "reauth_successful"
result2 = await hass.config_entries.flow.async_configure( assert mock_old_config_entry.version == 2
result["flow_id"], assert mock_old_config_entry.unique_id == CONF[CONF_ACCOUNT_ID]
{"account_id": "5678"}, assert mock_old_config_entry.data == CONF
)
await hass.async_block_till_done()
assert result2["type"] is FlowResultType.ABORT
assert result2["reason"] == "reauth_successful"
assert entry.version == 2
assert entry.unique_id == "5678"
assert entry.data == {
**CONF,
CONF_ACCOUNT_ID: "5678",
CONF_SUPPLY_NODE_REF: "456",
}
async def test_form_duplicate_account(hass: HomeAssistant) -> None: async def test_form_duplicate_account(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_flick_client: AsyncMock,
) -> None:
"""Test uniqueness for account_id.""" """Test uniqueness for account_id."""
entry = MockConfigEntry( await setup_integration(hass, mock_config_entry)
domain=DOMAIN,
data={**CONF, CONF_ACCOUNT_ID: "1234", CONF_SUPPLY_NODE_REF: "123"},
title="123 Fake St",
unique_id="1234",
version=2,
)
entry.add_to_hass(hass)
with ( result = await _flow_submit(hass)
patch(
"homeassistant.components.flick_electric.config_flow.SimpleFlickAuth.async_get_access_token",
return_value="123456789abcdef",
),
patch(
"homeassistant.components.flick_electric.config_flow.FlickAPI.getCustomerAccounts",
return_value=[
{
"id": "1234",
"status": "active",
"address": "123 Fake St",
"main_consumer": {"supply_node_ref": "123"},
}
],
),
patch(
"homeassistant.components.flick_electric.config_flow.FlickAPI.getPricing",
return_value=_mock_flick_price(),
),
):
result = await _flow_submit(hass)
assert result["type"] is FlowResultType.ABORT assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured" assert result["reason"] == "already_configured"
@ -398,7 +222,9 @@ async def test_form_generic_exception(hass: HomeAssistant) -> None:
assert result["errors"] == {"base": "unknown"} assert result["errors"] == {"base": "unknown"}
async def test_form_select_account_cannot_connect(hass: HomeAssistant) -> None: async def test_form_select_account_cannot_connect(
hass: HomeAssistant, mock_flick_client_multiple: AsyncMock
) -> None:
"""Test we handle connection errors for select account.""" """Test we handle connection errors for select account."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
@ -406,38 +232,16 @@ async def test_form_select_account_cannot_connect(hass: HomeAssistant) -> None:
assert result["type"] is FlowResultType.FORM assert result["type"] is FlowResultType.FORM
assert result["errors"] == {} assert result["errors"] == {}
with ( with patch.object(
patch( mock_flick_client_multiple,
"homeassistant.components.flick_electric.config_flow.SimpleFlickAuth.async_get_access_token", "getPricing",
return_value="123456789abcdef", side_effect=APIException,
),
patch(
"homeassistant.components.flick_electric.config_flow.FlickAPI.getCustomerAccounts",
return_value=[
{
"id": "1234",
"status": "active",
"address": "123 Fake St",
"main_consumer": {"supply_node_ref": "123"},
},
{
"id": "5678",
"status": "active",
"address": "456 Fake St",
"main_consumer": {"supply_node_ref": "456"},
},
],
),
patch(
"homeassistant.components.flick_electric.config_flow.FlickAPI.getPricing",
side_effect=APIException,
),
): ):
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
{ {
CONF_USERNAME: "test-username", CONF_USERNAME: CONF[CONF_USERNAME],
CONF_PASSWORD: "test-password", CONF_PASSWORD: CONF[CONF_PASSWORD],
}, },
) )
await hass.async_block_till_done() await hass.async_block_till_done()
@ -447,7 +251,7 @@ async def test_form_select_account_cannot_connect(hass: HomeAssistant) -> None:
result3 = await hass.config_entries.flow.async_configure( result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"], result2["flow_id"],
{"account_id": "5678"}, {CONF_ACCOUNT_ID: CONF[CONF_ACCOUNT_ID]},
) )
assert result3["type"] is FlowResultType.FORM assert result3["type"] is FlowResultType.FORM
@ -455,7 +259,9 @@ async def test_form_select_account_cannot_connect(hass: HomeAssistant) -> None:
assert result3["errors"] == {"base": "cannot_connect"} assert result3["errors"] == {"base": "cannot_connect"}
async def test_form_select_account_invalid_auth(hass: HomeAssistant) -> None: async def test_form_select_account_invalid_auth(
hass: HomeAssistant, mock_flick_client_multiple: AsyncMock
) -> None:
"""Test we handle auth errors for select account.""" """Test we handle auth errors for select account."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
@ -463,65 +269,41 @@ async def test_form_select_account_invalid_auth(hass: HomeAssistant) -> None:
assert result["type"] is FlowResultType.FORM assert result["type"] is FlowResultType.FORM
assert result["errors"] == {} assert result["errors"] == {}
with ( result2 = await hass.config_entries.flow.async_configure(
patch( result["flow_id"],
"homeassistant.components.flick_electric.config_flow.SimpleFlickAuth.async_get_access_token", {
return_value="123456789abcdef", CONF_USERNAME: CONF[CONF_USERNAME],
), CONF_PASSWORD: CONF[CONF_PASSWORD],
patch( },
"homeassistant.components.flick_electric.config_flow.FlickAPI.getCustomerAccounts", )
return_value=[ await hass.async_block_till_done()
{
"id": "1234",
"status": "active",
"address": "123 Fake St",
"main_consumer": {"supply_node_ref": "123"},
},
{
"id": "5678",
"status": "active",
"address": "456 Fake St",
"main_consumer": {"supply_node_ref": "456"},
},
],
),
patch(
"homeassistant.components.flick_electric.config_flow.FlickAPI.getPricing",
side_effect=AuthException,
),
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_USERNAME: "test-username",
CONF_PASSWORD: "test-password",
},
)
await hass.async_block_till_done()
assert result2["type"] is FlowResultType.FORM assert result2["type"] is FlowResultType.FORM
assert result2["step_id"] == "select_account" assert result2["step_id"] == "select_account"
with ( with (
patch( patch(
"homeassistant.components.flick_electric.config_flow.SimpleFlickAuth.async_get_access_token", "homeassistant.components.flick_electric.config_flow.SimpleFlickAuth.async_get_access_token",
side_effect=AuthException, side_effect=AuthException,
), ),
patch( patch.object(
"homeassistant.components.flick_electric.config_flow.FlickAPI.getCustomerAccounts", mock_flick_client_multiple,
"getPricing",
side_effect=AuthException, side_effect=AuthException,
), ),
): ):
result3 = await hass.config_entries.flow.async_configure( result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"], result2["flow_id"],
{"account_id": "5678"}, {CONF_ACCOUNT_ID: CONF[CONF_ACCOUNT_ID]},
) )
assert result3["type"] is FlowResultType.ABORT assert result3["type"] is FlowResultType.ABORT
assert result3["reason"] == "no_permissions" assert result3["reason"] == "no_permissions"
async def test_form_select_account_failed_to_connect(hass: HomeAssistant) -> None: async def test_form_select_account_failed_to_connect(
hass: HomeAssistant, mock_flick_client_multiple: AsyncMock
) -> None:
"""Test we handle connection errors for select account.""" """Test we handle connection errors for select account."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
@ -529,115 +311,56 @@ async def test_form_select_account_failed_to_connect(hass: HomeAssistant) -> Non
assert result["type"] is FlowResultType.FORM assert result["type"] is FlowResultType.FORM
assert result["errors"] == {} assert result["errors"] == {}
with ( result2 = await hass.config_entries.flow.async_configure(
patch( result["flow_id"],
"homeassistant.components.flick_electric.config_flow.SimpleFlickAuth.async_get_access_token", {
return_value="123456789abcdef", CONF_USERNAME: CONF[CONF_USERNAME],
), CONF_PASSWORD: CONF[CONF_PASSWORD],
patch( },
"homeassistant.components.flick_electric.config_flow.FlickAPI.getCustomerAccounts", )
return_value=[ await hass.async_block_till_done()
{
"id": "1234",
"status": "active",
"address": "123 Fake St",
"main_consumer": {"supply_node_ref": "123"},
},
{
"id": "5678",
"status": "active",
"address": "456 Fake St",
"main_consumer": {"supply_node_ref": "456"},
},
],
),
patch(
"homeassistant.components.flick_electric.config_flow.FlickAPI.getPricing",
side_effect=AuthException,
),
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_USERNAME: "test-username",
CONF_PASSWORD: "test-password",
},
)
await hass.async_block_till_done()
assert result2["type"] is FlowResultType.FORM assert result2["type"] is FlowResultType.FORM
assert result2["step_id"] == "select_account" assert result2["step_id"] == "select_account"
with ( with (
patch( patch.object(
"homeassistant.components.flick_electric.config_flow.SimpleFlickAuth.async_get_access_token", mock_flick_client_multiple,
return_value="123456789abcdef", "getCustomerAccounts",
),
patch(
"homeassistant.components.flick_electric.config_flow.FlickAPI.getCustomerAccounts",
side_effect=APIException, side_effect=APIException,
), ),
patch( patch.object(
"homeassistant.components.flick_electric.config_flow.FlickAPI.getPricing", mock_flick_client_multiple,
"getPricing",
side_effect=APIException, side_effect=APIException,
), ),
): ):
result3 = await hass.config_entries.flow.async_configure( result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"], result2["flow_id"],
{"account_id": "5678"}, {CONF_ACCOUNT_ID: CONF[CONF_ACCOUNT_ID]},
) )
assert result3["type"] is FlowResultType.FORM assert result3["type"] is FlowResultType.FORM
assert result3["errors"] == {"base": "cannot_connect"} assert result3["errors"] == {"base": "cannot_connect"}
with ( result4 = await hass.config_entries.flow.async_configure(
patch( result3["flow_id"],
"homeassistant.components.flick_electric.config_flow.SimpleFlickAuth.async_get_access_token", {CONF_ACCOUNT_ID: ACCOUNT_ID_2},
return_value="123456789abcdef", )
),
patch(
"homeassistant.components.flick_electric.config_flow.FlickAPI.getCustomerAccounts",
return_value=[
{
"id": "1234",
"status": "active",
"address": "123 Fake St",
"main_consumer": {"supply_node_ref": "123"},
},
{
"id": "5678",
"status": "active",
"address": "456 Fake St",
"main_consumer": {"supply_node_ref": "456"},
},
],
),
patch(
"homeassistant.components.flick_electric.config_flow.FlickAPI.getPricing",
return_value=_mock_flick_price(),
),
patch(
"homeassistant.components.flick_electric.async_setup_entry",
return_value=True,
) as mock_setup_entry,
):
result4 = await hass.config_entries.flow.async_configure(
result3["flow_id"],
{"account_id": "5678"},
)
assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["type"] is FlowResultType.CREATE_ENTRY
assert result4["title"] == "456 Fake St" assert result4["title"] == ACCOUNT_NAME_2
assert result4["data"] == { assert result4["data"] == {
**CONF, **CONF,
CONF_SUPPLY_NODE_REF: "456", CONF_SUPPLY_NODE_REF: SUPPLY_NODE_REF_2,
CONF_ACCOUNT_ID: "5678", CONF_ACCOUNT_ID: ACCOUNT_ID_2,
} }
assert result4["result"].unique_id == "5678" assert result4["result"].unique_id == ACCOUNT_ID_2
assert len(mock_setup_entry.mock_calls) == 1
async def test_form_select_account_no_accounts(hass: HomeAssistant) -> None: async def test_form_select_account_no_accounts(
hass: HomeAssistant, mock_flick_client: AsyncMock
) -> None:
"""Test we handle connection errors for select account.""" """Test we handle connection errors for select account."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
@ -645,28 +368,23 @@ async def test_form_select_account_no_accounts(hass: HomeAssistant) -> None:
assert result["type"] is FlowResultType.FORM assert result["type"] is FlowResultType.FORM
assert result["errors"] == {} assert result["errors"] == {}
with ( with patch.object(
patch( mock_flick_client,
"homeassistant.components.flick_electric.config_flow.SimpleFlickAuth.async_get_access_token", "getCustomerAccounts",
return_value="123456789abcdef", return_value=[
), {
patch( "id": "1234",
"homeassistant.components.flick_electric.config_flow.FlickAPI.getCustomerAccounts", "status": "closed",
return_value=[ "address": "123 Fake St",
{ "main_consumer": {"supply_node_ref": "123"},
"id": "1234", },
"status": "closed", ],
"address": "123 Fake St",
"main_consumer": {"supply_node_ref": "123"},
},
],
),
): ):
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
{ {
CONF_USERNAME: "test-username", CONF_USERNAME: CONF[CONF_USERNAME],
CONF_PASSWORD: "test-password", CONF_PASSWORD: CONF[CONF_PASSWORD],
}, },
) )
await hass.async_block_till_done() await hass.async_block_till_done()

View File

@ -1,135 +1,154 @@
"""Test the Flick Electric config flow.""" """Test the Flick Electric config flow."""
from unittest.mock import patch from unittest.mock import AsyncMock, patch
from pyflick.authentication import AuthException import jwt
from pyflick.types import APIException, AuthException
import pytest
from homeassistant.components.flick_electric.const import CONF_ACCOUNT_ID, DOMAIN from homeassistant.components.flick_electric import CONF_ID_TOKEN, HassFlickAuth
from homeassistant.components.flick_electric.const import (
CONF_ACCOUNT_ID,
CONF_TOKEN_EXPIRY,
)
from homeassistant.config_entries import ConfigEntryState from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.util import dt as dt_util
from . import CONF, _mock_flick_price from . import CONF, setup_integration
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
NEW_TOKEN = jwt.encode(
async def test_init_auth_failure_triggers_auth(hass: HomeAssistant) -> None: {"exp": dt_util.now().timestamp() + 86400}, "secret", algorithm="HS256"
"""Test reauth flow is triggered when username/password is wrong.""" )
with ( EXISTING_TOKEN = jwt.encode(
patch( {"exp": dt_util.now().timestamp() + 3600}, "secret", algorithm="HS256"
"homeassistant.components.flick_electric.HassFlickAuth.async_get_access_token", )
side_effect=AuthException, EXPIRED_TOKEN = jwt.encode(
), {"exp": dt_util.now().timestamp() - 3600}, "secret", algorithm="HS256"
): )
entry = MockConfigEntry(
domain=DOMAIN,
data={**CONF},
title="123 Fake St",
unique_id="1234",
version=2,
)
entry.add_to_hass(hass)
# Ensure setup fails
assert not await hass.config_entries.async_setup(entry.entry_id)
assert entry.state is ConfigEntryState.SETUP_ERROR
# Ensure reauth flow is triggered
await hass.async_block_till_done()
assert len(hass.config_entries.flow.async_progress()) == 1
async def test_init_migration_single_account(hass: HomeAssistant) -> None: @pytest.mark.parametrize(
("exception", "config_entry_state"),
[
(AuthException, ConfigEntryState.SETUP_ERROR),
(APIException, ConfigEntryState.SETUP_RETRY),
],
)
async def test_init_auth_failure_triggers_auth(
hass: HomeAssistant,
mock_flick_client: AsyncMock,
mock_config_entry: MockConfigEntry,
exception: Exception,
config_entry_state: ConfigEntryState,
) -> None:
"""Test integration handles initialisation errors."""
with patch.object(mock_flick_client, "getPricing", side_effect=exception):
await setup_integration(hass, mock_config_entry)
assert mock_config_entry.state == config_entry_state
async def test_init_migration_single_account(
hass: HomeAssistant,
mock_old_config_entry: MockConfigEntry,
mock_flick_client: AsyncMock,
) -> None:
"""Test migration with single account.""" """Test migration with single account."""
with ( await setup_integration(hass, mock_old_config_entry)
patch(
"homeassistant.components.flick_electric.HassFlickAuth.async_get_access_token",
return_value="123456789abcdef",
),
patch(
"homeassistant.components.flick_electric.FlickAPI.getCustomerAccounts",
return_value=[
{
"id": "1234",
"status": "active",
"address": "123 Fake St",
"main_consumer": {"supply_node_ref": "123"},
}
],
),
patch(
"homeassistant.components.flick_electric.FlickAPI.getPricing",
return_value=_mock_flick_price(),
),
):
entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_USERNAME: CONF[CONF_USERNAME],
CONF_PASSWORD: CONF[CONF_PASSWORD],
},
title=CONF_USERNAME,
unique_id=CONF_USERNAME,
version=1,
)
entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(entry.entry_id) assert len(hass.config_entries.flow.async_progress()) == 0
await hass.async_block_till_done() assert mock_old_config_entry.state is ConfigEntryState.LOADED
assert len(hass.config_entries.flow.async_progress()) == 0 assert mock_old_config_entry.version == 2
assert entry.state is ConfigEntryState.LOADED assert mock_old_config_entry.unique_id == CONF[CONF_ACCOUNT_ID]
assert entry.version == 2 assert mock_old_config_entry.data == CONF
assert entry.unique_id == CONF[CONF_ACCOUNT_ID]
assert entry.data == CONF
async def test_init_migration_multi_account_reauth(hass: HomeAssistant) -> None: async def test_init_migration_multi_account_reauth(
hass: HomeAssistant,
mock_old_config_entry: MockConfigEntry,
mock_flick_client_multiple: AsyncMock,
) -> None:
"""Test migration triggers reauth with multiple accounts.""" """Test migration triggers reauth with multiple accounts."""
with ( await setup_integration(hass, mock_old_config_entry)
patch(
"homeassistant.components.flick_electric.HassFlickAuth.async_get_access_token",
return_value="123456789abcdef",
),
patch(
"homeassistant.components.flick_electric.FlickAPI.getCustomerAccounts",
return_value=[
{
"id": "1234",
"status": "active",
"address": "123 Fake St",
"main_consumer": {"supply_node_ref": "123"},
},
{
"id": "5678",
"status": "active",
"address": "456 Fake St",
"main_consumer": {"supply_node_ref": "456"},
},
],
),
patch(
"homeassistant.components.flick_electric.FlickAPI.getPricing",
return_value=_mock_flick_price(),
),
):
entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_USERNAME: CONF[CONF_USERNAME],
CONF_PASSWORD: CONF[CONF_PASSWORD],
},
title=CONF_USERNAME,
unique_id=CONF_USERNAME,
version=1,
)
entry.add_to_hass(hass)
# ensure setup fails assert mock_old_config_entry.state is ConfigEntryState.MIGRATION_ERROR
assert not await hass.config_entries.async_setup(entry.entry_id)
assert entry.state is ConfigEntryState.MIGRATION_ERROR
await hass.async_block_till_done()
# Ensure reauth flow is triggered # Ensure reauth flow is triggered
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(hass.config_entries.flow.async_progress()) == 1 assert len(hass.config_entries.flow.async_progress()) == 1
async def test_fetch_fresh_token(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_flick_client: AsyncMock,
) -> None:
"""Test fetching a fresh token."""
await setup_integration(hass, mock_config_entry)
with patch(
"homeassistant.components.flick_electric.config_flow.SimpleFlickAuth.get_new_token",
return_value={CONF_ID_TOKEN: NEW_TOKEN},
) as mock_get_new_token:
auth = HassFlickAuth(hass, mock_config_entry)
assert await auth.async_get_access_token() == NEW_TOKEN
assert mock_get_new_token.call_count == 1
async def test_reuse_token(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_flick_client: AsyncMock,
) -> None:
"""Test reusing entry token."""
await setup_integration(hass, mock_config_entry)
hass.config_entries.async_update_entry(
mock_config_entry,
data={
**mock_config_entry.data,
CONF_ACCESS_TOKEN: {CONF_ID_TOKEN: EXISTING_TOKEN},
CONF_TOKEN_EXPIRY: dt_util.now().timestamp() + 3600,
},
)
with patch(
"homeassistant.components.flick_electric.config_flow.SimpleFlickAuth.get_new_token",
return_value={CONF_ID_TOKEN: NEW_TOKEN},
) as mock_get_new_token:
auth = HassFlickAuth(hass, mock_config_entry)
assert await auth.async_get_access_token() == EXISTING_TOKEN
assert mock_get_new_token.call_count == 0
async def test_fetch_expired_token(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_flick_client: AsyncMock,
) -> None:
"""Test fetching token when existing token is expired."""
await setup_integration(hass, mock_config_entry)
hass.config_entries.async_update_entry(
mock_config_entry,
data={
**mock_config_entry.data,
CONF_ACCESS_TOKEN: {CONF_ID_TOKEN: EXPIRED_TOKEN},
CONF_TOKEN_EXPIRY: dt_util.now().timestamp() - 3600,
},
)
with patch(
"homeassistant.components.flick_electric.config_flow.SimpleFlickAuth.get_new_token",
return_value={CONF_ID_TOKEN: NEW_TOKEN},
) as mock_get_new_token:
auth = HassFlickAuth(hass, mock_config_entry)
assert await auth.async_get_access_token() == NEW_TOKEN
assert mock_get_new_token.call_count == 1