added config flow test and workflow for tests

This commit is contained in:
tomaae 2020-04-14 20:57:34 +02:00
parent f17fefb5d8
commit 1dd4f52524
10 changed files with 1706 additions and 5 deletions

View file

@ -70,6 +70,8 @@ def get_integration_commits(github, skip=True):
continue
if " workflow" in msg:
continue
if " test" in msg:
continue
if "docs" in msg:
continue
if "dev debug" in msg:

22
.github/generate_requirements.py vendored Normal file
View file

@ -0,0 +1,22 @@
import configparser
def main():
parser = configparser.ConfigParser()
parser.read("Pipfile")
packages = "packages"
with open("requirements.txt", "w") as f:
for key in parser[packages]:
value = parser[packages][key]
f.write(key + value.replace('"', "") + "\n")
devpackages = "dev-packages"
with open("requirements_tests.txt", "w") as f:
for key in parser[devpackages]:
value = parser[devpackages][key]
f.write(key + value.replace('"', "") + "\n")
if __name__ == "__main__":
main()

View file

@ -12,7 +12,7 @@ on:
jobs:
black:
name: Black
name: Python Code Format Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
@ -29,10 +29,14 @@ jobs:
uses: actions/setup-python@v1
with:
python-version: 3.8
- name: Generate Requirements lists
run: |
python3 .github/generate_requirements.py
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -r requirements_tests.txt
- name: Lint with flake8
run: |
pip install flake8
@ -40,10 +44,10 @@ jobs:
flake8 . --count --select=E9,F63,F7,F82 --ignore W503,E722 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=15 --max-line-length=127 --statistics
#- name: Test with pytest
# run: |
# pip install pytest
# pytest
- name: Test with pytest
run: |
pip install pytest
pytest
sonarcloud:
name: SonarCloud
runs-on: ubuntu-latest

27
Pipfile Normal file
View file

@ -0,0 +1,27 @@
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true
[dev-packages]
wheel = ">=0.34"
pygithub = ">=1.47"
homeassistant = ">=0.108.4"
sqlalchemy = "==1.3.16"
codecov = "==2.0.15"
mock-open = "==1.3.1"
mypy = "==0.770"
pre-commit = "==2.2.0"
pylint = "==2.4.4"
astroid = "==2.3.3"
pylint-strict-informational = "==0.1"
pytest-aiohttp = "==0.3.0"
pytest-cov = "==2.8.1"
pytest-sugar = "==0.9.2"
pytest-timeout = "==1.3.3"
pytest = "==5.3.5"
requests_mock = "==1.7.0"
responses = "==0.10.6"
[packages]
librouteros = "==3.0.0"

View file

@ -30,6 +30,7 @@ from .const import (
CONF_SCAN_INTERVAL,
DEFAULT_SCAN_INTERVAL,
DEFAULT_UNIT_OF_MEASUREMENT,
CONF_TRACK_HOSTS_TIMEOUT,
)
from .exceptions import ApiEntryNotFound
from .helper import parse_api

20
tests/__init__.py Normal file
View file

@ -0,0 +1,20 @@
"""Tests for the Mikrotik Router component."""
from custom_components.mikrotik_router import config_flow
MOCK_DATA = {
config_flow.CONF_NAME: config_flow.DEFAULT_DEVICE_NAME,
config_flow.CONF_HOST: config_flow.DEFAULT_HOST,
config_flow.CONF_USERNAME: config_flow.DEFAULT_USERNAME,
config_flow.CONF_PASSWORD: config_flow.DEFAULT_PASSWORD,
config_flow.CONF_PORT: config_flow.DEFAULT_PORT,
config_flow.CONF_SSL: config_flow.DEFAULT_SSL,
}
MOCK_OPTIONS = {
config_flow.CONF_SCAN_INTERVAL: config_flow.DEFAULT_SCAN_INTERVAL,
config_flow.CONF_UNIT_OF_MEASUREMENT: config_flow.DEFAULT_UNIT_OF_MEASUREMENT,
config_flow.CONF_TRACK_IFACE_CLIENTS: config_flow.DEFAULT_TRACK_IFACE_CLIENTS,
config_flow.CONF_TRACK_HOSTS: config_flow.DEFAULT_TRACK_HOSTS,
config_flow.CONF_TRACK_HOSTS_TIMEOUT: config_flow.DEFAULT_TRACK_HOST_TIMEOUT,
}

1148
tests/common.py Normal file

File diff suppressed because it is too large Load diff

253
tests/conftest.py Normal file
View file

@ -0,0 +1,253 @@
"""Set up some common test helper things."""
import functools
import logging
from unittest.mock import patch
import pytest
import requests_mock as _requests_mock
from homeassistant import util
from homeassistant.auth.const import GROUP_ID_ADMIN, GROUP_ID_READ_ONLY
from homeassistant.auth.providers import homeassistant, legacy_api_password
from homeassistant.components.websocket_api.auth import (
TYPE_AUTH,
TYPE_AUTH_OK,
TYPE_AUTH_REQUIRED,
)
from homeassistant.components.websocket_api.http import URL
from homeassistant.exceptions import ServiceNotFound
from homeassistant.setup import async_setup_component
from homeassistant.util import location
from tests.ignore_uncaught_exceptions import (
IGNORE_UNCAUGHT_EXCEPTIONS,
IGNORE_UNCAUGHT_JSON_EXCEPTIONS,
)
pytest.register_assert_rewrite("tests.common")
from tests.common import ( # noqa: E402, isort:skip
CLIENT_ID,
INSTANCES,
MockUser,
async_test_home_assistant,
mock_coro,
mock_storage as mock_storage,
)
logging.basicConfig(level=logging.DEBUG)
logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO)
def check_real(func):
"""Force a function to require a keyword _test_real to be passed in."""
@functools.wraps(func)
async def guard_func(*args, **kwargs):
real = kwargs.pop("_test_real", None)
if not real:
raise Exception(
'Forgot to mock or pass "_test_real=True" to %s', func.__name__
)
return await func(*args, **kwargs)
return guard_func
# Guard a few functions that would make network connections
location.async_detect_location_info = check_real(location.async_detect_location_info)
util.get_local_ip = lambda: "127.0.0.1"
@pytest.fixture(autouse=True)
def verify_cleanup():
"""Verify that the test has cleaned up resources correctly."""
yield
if len(INSTANCES) >= 2:
count = len(INSTANCES)
for inst in INSTANCES:
inst.stop()
pytest.exit(f"Detected non stopped instances ({count}), aborting test run")
@pytest.fixture
def hass_storage():
"""Fixture to mock storage."""
with mock_storage() as stored_data:
yield stored_data
@pytest.fixture
def hass(loop, hass_storage, request):
"""Fixture to provide a test instance of Home Assistant."""
def exc_handle(loop, context):
"""Handle exceptions by rethrowing them, which will fail the test."""
exceptions.append(context["exception"])
orig_exception_handler(loop, context)
exceptions = []
hass = loop.run_until_complete(async_test_home_assistant(loop))
orig_exception_handler = loop.get_exception_handler()
loop.set_exception_handler(exc_handle)
yield hass
loop.run_until_complete(hass.async_stop(force=True))
for ex in exceptions:
if (
request.module.__name__,
request.function.__name__,
) in IGNORE_UNCAUGHT_EXCEPTIONS:
continue
if isinstance(ex, ServiceNotFound):
continue
if (
isinstance(ex, TypeError)
and "is not JSON serializable" in str(ex)
and (request.module.__name__, request.function.__name__)
in IGNORE_UNCAUGHT_JSON_EXCEPTIONS
):
continue
raise ex
@pytest.fixture
def requests_mock():
"""Fixture to provide a requests mocker."""
with _requests_mock.mock() as m:
yield m
@pytest.fixture
def mock_device_tracker_conf():
"""Prevent device tracker from reading/writing data."""
devices = []
async def mock_update_config(path, id, entity):
devices.append(entity)
with patch(
"homeassistant.components.device_tracker.legacy"
".DeviceTracker.async_update_config",
side_effect=mock_update_config,
), patch(
"homeassistant.components.device_tracker.legacy.async_load_config",
side_effect=lambda *args: mock_coro(devices),
):
yield devices
@pytest.fixture
def hass_access_token(hass, hass_admin_user):
"""Return an access token to access Home Assistant."""
refresh_token = hass.loop.run_until_complete(
hass.auth.async_create_refresh_token(hass_admin_user, CLIENT_ID)
)
return hass.auth.async_create_access_token(refresh_token)
@pytest.fixture
def hass_owner_user(hass, local_auth):
"""Return a Home Assistant admin user."""
return MockUser(is_owner=True).add_to_hass(hass)
@pytest.fixture
def hass_admin_user(hass, local_auth):
"""Return a Home Assistant admin user."""
admin_group = hass.loop.run_until_complete(
hass.auth.async_get_group(GROUP_ID_ADMIN)
)
return MockUser(groups=[admin_group]).add_to_hass(hass)
@pytest.fixture
def hass_read_only_user(hass, local_auth):
"""Return a Home Assistant read only user."""
read_only_group = hass.loop.run_until_complete(
hass.auth.async_get_group(GROUP_ID_READ_ONLY)
)
return MockUser(groups=[read_only_group]).add_to_hass(hass)
@pytest.fixture
def hass_read_only_access_token(hass, hass_read_only_user):
"""Return a Home Assistant read only user."""
refresh_token = hass.loop.run_until_complete(
hass.auth.async_create_refresh_token(hass_read_only_user, CLIENT_ID)
)
return hass.auth.async_create_access_token(refresh_token)
@pytest.fixture
def legacy_auth(hass):
"""Load legacy API password provider."""
prv = legacy_api_password.LegacyApiPasswordAuthProvider(
hass,
hass.auth._store,
{"type": "legacy_api_password", "api_password": "test-password"},
)
hass.auth._providers[(prv.type, prv.id)] = prv
return prv
@pytest.fixture
def local_auth(hass):
"""Load local auth provider."""
prv = homeassistant.HassAuthProvider(
hass, hass.auth._store, {"type": "homeassistant"}
)
hass.auth._providers[(prv.type, prv.id)] = prv
return prv
@pytest.fixture
def hass_client(hass, aiohttp_client, hass_access_token):
"""Return an authenticated HTTP client."""
async def auth_client():
"""Return an authenticated client."""
return await aiohttp_client(
hass.http.app, headers={"Authorization": f"Bearer {hass_access_token}"}
)
return auth_client
@pytest.fixture
def hass_ws_client(aiohttp_client, hass_access_token, hass):
"""Websocket client fixture connected to websocket server."""
async def create_client(hass=hass, access_token=hass_access_token):
"""Create a websocket client."""
assert await async_setup_component(hass, "websocket_api", {})
client = await aiohttp_client(hass.http.app)
with patch("homeassistant.components.http.auth.setup_auth"):
websocket = await client.ws_connect(URL)
auth_resp = await websocket.receive_json()
assert auth_resp["type"] == TYPE_AUTH_REQUIRED
if access_token is None:
await websocket.send_json(
{"type": TYPE_AUTH, "access_token": "incorrect"}
)
else:
await websocket.send_json(
{"type": TYPE_AUTH, "access_token": access_token}
)
auth_ok = await websocket.receive_json()
assert auth_ok["type"] == TYPE_AUTH_OK
# wrap in client
websocket.client = client
return websocket
return create_client

View file

@ -0,0 +1,38 @@
"""List of modules that have uncaught exceptions today. Will be shrunk over time."""
IGNORE_UNCAUGHT_EXCEPTIONS = [
("tests.components.dyson.test_air_quality", "test_purecool_aiq_attributes"),
("tests.components.dyson.test_air_quality", "test_purecool_aiq_update_state"),
(
"tests.components.dyson.test_air_quality",
"test_purecool_component_setup_only_once",
),
("tests.components.dyson.test_air_quality", "test_purecool_aiq_without_discovery"),
(
"tests.components.dyson.test_air_quality",
"test_purecool_aiq_empty_environment_state",
),
(
"tests.components.dyson.test_climate",
"test_setup_component_with_parent_discovery",
),
("tests.components.dyson.test_fan", "test_purecoollink_attributes"),
("tests.components.dyson.test_fan", "test_purecool_turn_on"),
("tests.components.dyson.test_fan", "test_purecool_set_speed"),
("tests.components.dyson.test_fan", "test_purecool_turn_off"),
("tests.components.dyson.test_fan", "test_purecool_set_dyson_speed"),
("tests.components.dyson.test_fan", "test_purecool_oscillate"),
("tests.components.dyson.test_fan", "test_purecool_set_night_mode"),
("tests.components.dyson.test_fan", "test_purecool_set_auto_mode"),
("tests.components.dyson.test_fan", "test_purecool_set_angle"),
("tests.components.dyson.test_fan", "test_purecool_set_flow_direction_front"),
("tests.components.dyson.test_fan", "test_purecool_set_timer"),
("tests.components.dyson.test_fan", "test_purecool_update_state"),
("tests.components.dyson.test_fan", "test_purecool_update_state_filter_inv"),
("tests.components.dyson.test_fan", "test_purecool_component_setup_only_once"),
("tests.components.dyson.test_sensor", "test_purecool_component_setup_only_once"),
("tests.components.ios.test_init", "test_creating_entry_sets_up_sensor"),
("tests.components.ios.test_init", "test_not_configuring_ios_not_creates_entry"),
("tests.components.local_file.test_camera", "test_file_not_readable"),
]
IGNORE_UNCAUGHT_JSON_EXCEPTIONS = []

186
tests/test_config_flow.py Normal file
View file

@ -0,0 +1,186 @@
from datetime import timedelta
from unittest.mock import patch
import librouteros
import pytest
from homeassistant import data_entry_flow
from custom_components import mikrotik_router
from homeassistant.const import (
CONF_NAME,
CONF_HOST,
CONF_PORT,
CONF_USERNAME,
CONF_PASSWORD,
CONF_SSL,
)
from . import MOCK_DATA
from tests.common import MockConfigEntry
DEMO_USER_INPUT = {
CONF_NAME: "Home router",
CONF_HOST: "0.0.0.0",
CONF_USERNAME: "username",
CONF_PASSWORD: "password",
CONF_PORT: 8278,
CONF_SSL: True,
}
DEMO_CONFIG_ENTRY = {
CONF_NAME: "Home router",
CONF_HOST: "0.0.0.0",
CONF_USERNAME: "username",
CONF_PASSWORD: "password",
CONF_PORT: 8278,
CONF_SSL: True,
mikrotik_router.mikrotik_controller.CONF_SCAN_INTERVAL: 60,
mikrotik_router.mikrotik_controller.CONF_UNIT_OF_MEASUREMENT: "Mbps",
mikrotik_router.mikrotik_controller.CONF_TRACK_IFACE_CLIENTS: True,
mikrotik_router.mikrotik_controller.CONF_TRACK_HOSTS: True,
mikrotik_router.mikrotik_controller.CONF_TRACK_HOSTS_TIMEOUT: 180,
}
@pytest.fixture(name="api")
def mock_mikrotik_api():
"""Mock an api."""
with patch("librouteros.connect"):
yield
@pytest.fixture(name="auth_error")
def mock_api_authentication_error():
"""Mock an api."""
with patch(
"librouteros.connect",
side_effect=librouteros.exceptions.TrapError("invalid user name or password"),
):
yield
@pytest.fixture(name="conn_error")
def mock_api_connection_error():
"""Mock an api."""
with patch(
"librouteros.connect", side_effect=librouteros.exceptions.ConnectionClosed
):
yield
async def test_import(hass, api):
"""Test import step."""
result = await hass.config_entries.flow.async_init(
mikrotik_router.DOMAIN, context={"source": "import"}, data=MOCK_DATA
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["title"] == "Mikrotik"
assert result["data"][CONF_NAME] == "Mikrotik"
assert result["data"][CONF_HOST] == "10.0.0.1"
assert result["data"][CONF_USERNAME] == "admin"
assert result["data"][CONF_PASSWORD] == "admin"
assert result["data"][CONF_PORT] == 0
assert result["data"][CONF_SSL] is False
async def test_flow_works(hass, api):
"""Test config flow."""
result = await hass.config_entries.flow.async_init(
mikrotik_router.DOMAIN, context={"source": "user"}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "user"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=DEMO_USER_INPUT
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["title"] == "Home router"
assert result["data"][CONF_NAME] == "Home router"
assert result["data"][CONF_HOST] == "0.0.0.0"
assert result["data"][CONF_USERNAME] == "username"
assert result["data"][CONF_PASSWORD] == "password"
assert result["data"][CONF_PORT] == 8278
assert result["data"][CONF_SSL] is True
async def test_options(hass):
"""Test updating options."""
entry = MockConfigEntry(domain=mikrotik_router.DOMAIN, data=DEMO_CONFIG_ENTRY)
entry.add_to_hass(hass)
result = await hass.config_entries.options.async_init(entry.entry_id)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "device_tracker"
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={
mikrotik_router.mikrotik_controller.CONF_SCAN_INTERVAL: 30,
mikrotik_router.mikrotik_controller.CONF_UNIT_OF_MEASUREMENT: "Kbps",
mikrotik_router.mikrotik_controller.CONF_TRACK_IFACE_CLIENTS: True,
mikrotik_router.mikrotik_controller.CONF_TRACK_HOSTS: False,
mikrotik_router.mikrotik_controller.CONF_TRACK_HOSTS_TIMEOUT: 180,
},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["data"] == {
mikrotik_router.mikrotik_controller.CONF_SCAN_INTERVAL: 30,
mikrotik_router.mikrotik_controller.CONF_UNIT_OF_MEASUREMENT: "Kbps",
mikrotik_router.mikrotik_controller.CONF_TRACK_IFACE_CLIENTS: True,
mikrotik_router.mikrotik_controller.CONF_TRACK_HOSTS: False,
mikrotik_router.mikrotik_controller.CONF_TRACK_HOSTS_TIMEOUT: 180,
}
async def test_name_exists(hass, api):
"""Test name already configured."""
entry = MockConfigEntry(domain=mikrotik_router.DOMAIN, data=DEMO_CONFIG_ENTRY)
entry.add_to_hass(hass)
user_input = DEMO_USER_INPUT.copy()
user_input[CONF_HOST] = "0.0.0.1"
result = await hass.config_entries.flow.async_init(
mikrotik_router.DOMAIN, context={"source": "user"}
)
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=user_input
)
assert result["type"] == "form"
assert result["errors"] == {"base": "name_exists"}
async def test_connection_error(hass, conn_error):
"""Test error when connection is unsuccessful."""
result = await hass.config_entries.flow.async_init(
mikrotik_router.DOMAIN, context={"source": "user"}
)
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=DEMO_USER_INPUT
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] == {"host": "cannot_connect"}
async def test_wrong_credentials(hass, auth_error):
"""Test error when credentials are wrong."""
result = await hass.config_entries.flow.async_init(
mikrotik_router.DOMAIN, context={"source": "user"}
)
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=DEMO_USER_INPUT
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] == {"host": "cannot_connect"}