From 2f1a3439fd728d7f538e630ca69a5124486b44ce Mon Sep 17 00:00:00 2001 From: Gregory Dosh Date: Wed, 10 Jan 2018 08:42:59 -0600 Subject: [PATCH 01/10] initial https support --- .../components/device_tracker/tomato.py | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/device_tracker/tomato.py b/homeassistant/components/device_tracker/tomato.py index 57e83eaeb94a9..122595d90f649 100644 --- a/homeassistant/components/device_tracker/tomato.py +++ b/homeassistant/components/device_tracker/tomato.py @@ -14,7 +14,9 @@ import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner) -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import ( + CONF_HOST, CONF_PORT, CONF_SSL, CONF_VERIFY_SSL, + CONF_PASSWORD, CONF_USERNAME) CONF_HTTP_ID = 'http_id' @@ -22,6 +24,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=80): cv.port, + vol.Optional(CONF_SSL, default=False): cv.boolean, + vol.Optional(CONF_VERIFY_SSL, default=""): cv.string, vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_HTTP_ID): cv.string @@ -39,10 +44,20 @@ class TomatoDeviceScanner(DeviceScanner): def __init__(self, config): """Initialize the scanner.""" host, http_id = config[CONF_HOST], config[CONF_HTTP_ID] + port = config[CONF_PORT] username, password = config[CONF_USERNAME], config[CONF_PASSWORD] + self.ssl, self.verify_ssl = config[CONF_SSL], config[CONF_VERIFY_SSL] + + if self.ssl: + try: + self.verify_ssl = cv.boolean(self.verify_ssl) + except vol.Invalid: + pass self.req = requests.Request( - 'POST', 'http://{}/update.cgi'.format(host), + 'POST', 'http{}://{}:{}/update.cgi'.format( + "s" if self.ssl else "", host, port + ), data={'_http_id': http_id, 'exec': 'devlist'}, auth=requests.auth.HTTPBasicAuth(username, password)).prepare() @@ -77,7 +92,13 @@ def _update_tomato_info(self): self.logger.info("Scanning") try: - response = requests.Session().send(self.req, timeout=3) + if self.ssl: + response = requests.Session().send(self.req, + timeout=3, + verify=self.verify_ssl) + else: + response = requests.Session().send(self.req, timeout=3) + # Calling and parsing the Tomato api here. We only need the # wldev and dhcpd_lease values. if response.status_code == 200: From e69aa75111bdacb463dbc13f853489975fa2be8d Mon Sep 17 00:00:00 2001 From: Gregory Dosh Date: Wed, 10 Jan 2018 16:26:56 -0600 Subject: [PATCH 02/10] adding tests --- .../components/device_tracker/tomato.py | 24 +- .../components/device_tracker/test_tomato.py | 280 ++++++++++++++++++ 2 files changed, 289 insertions(+), 15 deletions(-) create mode 100644 tests/components/device_tracker/test_tomato.py diff --git a/homeassistant/components/device_tracker/tomato.py b/homeassistant/components/device_tracker/tomato.py index 122595d90f649..1259f78d5d4db 100644 --- a/homeassistant/components/device_tracker/tomato.py +++ b/homeassistant/components/device_tracker/tomato.py @@ -20,13 +20,14 @@ CONF_HTTP_ID = 'http_id' -_LOGGER = logging.getLogger(__name__) +_LOGGER = logging.getLogger("{}.{}".format(__name__, "Tomato")) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_PORT, default=80): cv.port, vol.Optional(CONF_SSL, default=False): cv.boolean, - vol.Optional(CONF_VERIFY_SSL, default=""): cv.string, + vol.Optional(CONF_VERIFY_SSL, default=True): vol.Any( + cv.boolean, cv.isfile), vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_HTTP_ID): cv.string @@ -48,12 +49,6 @@ def __init__(self, config): username, password = config[CONF_USERNAME], config[CONF_PASSWORD] self.ssl, self.verify_ssl = config[CONF_SSL], config[CONF_VERIFY_SSL] - if self.ssl: - try: - self.verify_ssl = cv.boolean(self.verify_ssl) - except vol.Invalid: - pass - self.req = requests.Request( 'POST', 'http{}://{}:{}/update.cgi'.format( "s" if self.ssl else "", host, port @@ -63,7 +58,6 @@ def __init__(self, config): self.parse_api_pattern = re.compile(r"(?P\w*) = (?P.*);") - self.logger = logging.getLogger("{}.{}".format(__name__, "Tomato")) self.last_results = {"wldev": [], "dhcpd_lease": []} self.success_init = self._update_tomato_info() @@ -89,7 +83,7 @@ def _update_tomato_info(self): Return boolean if scanning successful. """ - self.logger.info("Scanning") + _LOGGER.info("Scanning") try: if self.ssl: @@ -113,7 +107,7 @@ def _update_tomato_info(self): elif response.status_code == 401: # Authentication error - self.logger.exception(( + _LOGGER.exception(( "Failed to authenticate, " "please check your username and password")) return False @@ -121,17 +115,17 @@ def _update_tomato_info(self): except requests.exceptions.ConnectionError: # We get this if we could not connect to the router or # an invalid http_id was supplied. - self.logger.exception("Failed to connect to the router or " - "invalid http_id supplied") + _LOGGER.exception("Failed to connect to the router or " + "invalid http_id supplied") return False except requests.exceptions.Timeout: # We get this if we could not connect to the router or # an invalid http_id was supplied. - self.logger.exception("Connection to the router timed out") + _LOGGER.exception("Connection to the router timed out") return False except ValueError: # If JSON decoder could not parse the response. - self.logger.exception("Failed to parse response from router") + _LOGGER.exception("Failed to parse response from router") return False diff --git a/tests/components/device_tracker/test_tomato.py b/tests/components/device_tracker/test_tomato.py new file mode 100644 index 0000000000000..c00364e785d2c --- /dev/null +++ b/tests/components/device_tracker/test_tomato.py @@ -0,0 +1,280 @@ +"""The tests for the Tomato device tracker platform.""" +from unittest import mock +import pytest +import voluptuous as vol + +from homeassistant.components.device_tracker import DOMAIN, tomato as tomato +from homeassistant.const import (CONF_HOST, CONF_USERNAME, CONF_PASSWORD, + CONF_PORT, CONF_SSL, CONF_PLATFORM, + CONF_VERIFY_SSL) + + +def mock_session_response(*args, **kwargs): + class MockSessionResponse: + def __init__(self, text, status_code): + self.text = text + self.status_code = status_code + + # Username: foo + # Password: bar + if args[0].headers['Authorization'] != 'Basic Zm9vOmJhcg==': + return MockSessionResponse(None, 401) + elif "gimmie_bad_data" in args[0].body: + return MockSessionResponse('This shouldn\'t (wldev = be here.;', 200) + elif "gimmie_good_data" in args[0].body: + return MockSessionResponse("wldev = [ ['eth1','F4:F5:D8:AA:AA:AA'," + "-42,5500,1000,7043,0],['eth1','58:EF:68:00:00:00'," + "-42,5500,1000,7043,0]];\n" + "dhcpd_lease = [ ['chromecast','172.10.10.5','F4:F5:D8:AA:AA:AA'," + "'0 days, 16:17:08'],['wemo','172.10.10.6','58:EF:68:00:00:00'," + "'0 days, 12:09:08']];", 200) + + return MockSessionResponse(None, 200) + + +@pytest.fixture +def mock_exception_logger(): + """Mock pyunifi.""" + with mock.patch('homeassistant.components.device_tracker' + '.tomato._LOGGER.exception') as mock_exception_logger: + yield mock_exception_logger + + +@pytest.fixture +def mock_session_send(): + """Mock requests.Session().send.""" + with mock.patch('requests.Session.send') as mock_session_send: + yield mock_session_send + + +def test_config_missing_optional_params(hass, mock_session_send): + """Test the setup without optional parameters.""" + config = { + DOMAIN: tomato.PLATFORM_SCHEMA({ + CONF_PLATFORM: tomato.DOMAIN, + CONF_HOST: 'tomato-router', + CONF_USERNAME: 'foo', + CONF_PASSWORD: 'password', + tomato.CONF_HTTP_ID: '1234567890' + }) + } + result = tomato.get_scanner(hass, config) + assert result.req.url == "http://tomato-router:80/update.cgi" + assert result.req.headers == { + 'Content-Length': '32', + 'Content-Type': 'application/x-www-form-urlencoded', + 'Authorization': 'Basic Zm9vOnBhc3N3b3Jk' + } + assert result.req.body == "_http_id=1234567890&exec=devlist" + + +@mock.patch('os.access', return_value=True) +@mock.patch('os.path.isfile', mock.Mock(return_value=True)) +def test_config_verify_ssl_but_no_ssl_enabled(hass, mock_session_send): + """Test the setup with a string with ssl_verify but ssl not enabled.""" + config = { + DOMAIN: tomato.PLATFORM_SCHEMA({ + CONF_PLATFORM: tomato.DOMAIN, + CONF_HOST: 'tomato-router', + CONF_PORT: 1234, + CONF_SSL: False, + CONF_VERIFY_SSL: "/tmp/tomato.crt", + CONF_USERNAME: 'foo', + CONF_PASSWORD: 'password', + tomato.CONF_HTTP_ID: '1234567890' + }) + } + result = tomato.get_scanner(hass, config) + assert result.req.url == "http://tomato-router:1234/update.cgi" + assert result.req.headers == { + 'Content-Length': '32', + 'Content-Type': 'application/x-www-form-urlencoded', + 'Authorization': 'Basic Zm9vOnBhc3N3b3Jk' + } + assert result.req.body == "_http_id=1234567890&exec=devlist" + assert mock_session_send.call_count == 1 + assert mock_session_send.mock_calls[0] == \ + mock.call(result.req, timeout=3) + + +@mock.patch('os.access', return_value=True) +@mock.patch('os.path.isfile', mock.Mock(return_value=True)) +def test_config_valid_verify_ssl_path(hass, mock_session_send): + """Test the setup with a string for ssl_verify. + + Representing the absolute path to a CA certificate bundle. + """ + config = { + DOMAIN: tomato.PLATFORM_SCHEMA({ + CONF_PLATFORM: tomato.DOMAIN, + CONF_HOST: 'tomato-router', + CONF_PORT: 1234, + CONF_SSL: True, + CONF_VERIFY_SSL: "/tmp/tomato.crt", + CONF_USERNAME: 'bar', + CONF_PASSWORD: 'foo', + tomato.CONF_HTTP_ID: '0987654321' + }) + } + result = tomato.get_scanner(hass, config) + assert result.req.url == "https://tomato-router:1234/update.cgi" + assert result.req.headers == { + 'Content-Length': '32', + 'Content-Type': 'application/x-www-form-urlencoded', + 'Authorization': 'Basic YmFyOmZvbw==' + } + assert result.req.body == "_http_id=0987654321&exec=devlist" + assert mock_session_send.call_count == 1 + assert mock_session_send.mock_calls[0] == \ + mock.call(result.req, timeout=3, verify="/tmp/tomato.crt") + + +def test_config_valid_verify_ssl_bool(hass, mock_session_send): + """Test the setup with a bool for ssl_verify.""" + config = { + DOMAIN: tomato.PLATFORM_SCHEMA({ + CONF_PLATFORM: tomato.DOMAIN, + CONF_HOST: 'tomato-router', + CONF_PORT: 1234, + CONF_SSL: True, + CONF_VERIFY_SSL: "False", + CONF_USERNAME: 'bar', + CONF_PASSWORD: 'foo', + tomato.CONF_HTTP_ID: '0987654321' + }) + } + result = tomato.get_scanner(hass, config) + assert result.req.url == "https://tomato-router:1234/update.cgi" + assert result.req.headers == { + 'Content-Length': '32', + 'Content-Type': 'application/x-www-form-urlencoded', + 'Authorization': 'Basic YmFyOmZvbw==' + } + assert result.req.body == "_http_id=0987654321&exec=devlist" + assert mock_session_send.call_count == 1 + assert mock_session_send.mock_calls[0] == \ + mock.call(result.req, timeout=3, verify=False) + + +def test_config_errors(): + """Test for configuration errors.""" + with pytest.raises(vol.Invalid): + tomato.PLATFORM_SCHEMA({ + CONF_PLATFORM: tomato.DOMAIN, + # No Host, + CONF_PORT: 1234, + CONF_SSL: True, + CONF_VERIFY_SSL: "False", + CONF_USERNAME: 'bar', + CONF_PASSWORD: 'foo', + tomato.CONF_HTTP_ID: '0987654321' + }) + with pytest.raises(vol.Invalid): + tomato.PLATFORM_SCHEMA({ + CONF_PLATFORM: tomato.DOMAIN, + CONF_HOST: 'tomato-router', + CONF_PORT: -123456789, # Bad Port + CONF_SSL: True, + CONF_VERIFY_SSL: "False", + CONF_USERNAME: 'bar', + CONF_PASSWORD: 'foo', + tomato.CONF_HTTP_ID: '0987654321' + }) + with pytest.raises(vol.Invalid): + tomato.PLATFORM_SCHEMA({ + CONF_PLATFORM: tomato.DOMAIN, + CONF_HOST: 'tomato-router', + CONF_PORT: 1234, + CONF_SSL: True, + CONF_VERIFY_SSL: "False", + # No Username + CONF_PASSWORD: 'foo', + tomato.CONF_HTTP_ID: '0987654321' + }) + with pytest.raises(vol.Invalid): + tomato.PLATFORM_SCHEMA({ + CONF_PLATFORM: tomato.DOMAIN, + CONF_HOST: 'tomato-router', + CONF_PORT: 1234, + CONF_SSL: True, + CONF_VERIFY_SSL: "False", + CONF_USERNAME: 'bar', + # No Password + tomato.CONF_HTTP_ID: '0987654321' + }) + with pytest.raises(vol.Invalid): + tomato.PLATFORM_SCHEMA({ + CONF_PLATFORM: tomato.DOMAIN, + CONF_HOST: 'tomato-router', + CONF_PORT: 1234, + CONF_SSL: True, + CONF_VERIFY_SSL: "False", + CONF_USERNAME: 'bar', + CONF_PASSWORD: 'foo', + # No HTTP_ID + }) + + +@mock.patch('requests.Session.send', side_effect=mock_session_response) +def test_config_bad_credentials(hass, mock_exception_logger): + """Test the setup with bad credentials. + + Representing the absolute path to a CA certificate bundle. + """ + config = { + DOMAIN: tomato.PLATFORM_SCHEMA({ + CONF_PLATFORM: tomato.DOMAIN, + CONF_HOST: 'tomato-router', + CONF_USERNAME: 'i_am', + CONF_PASSWORD: 'an_imposter', + tomato.CONF_HTTP_ID: '1234' + }) + } + + tomato.get_scanner(hass, config) + + assert mock_exception_logger.call_count == 1 + assert mock_exception_logger.mock_calls[0] == \ + mock.call("Failed to authenticate, " + "please check your username and password") + + +@mock.patch('requests.Session.send', side_effect=mock_session_response) +def test_scan_devices(hass, mock_exception_logger): + """Test the setup with bad credentials. + + Representing the absolute path to a CA certificate bundle. + """ + config = { + DOMAIN: tomato.PLATFORM_SCHEMA({ + CONF_PLATFORM: tomato.DOMAIN, + CONF_HOST: 'tomato-router', + CONF_USERNAME: 'foo', + CONF_PASSWORD: 'bar', + tomato.CONF_HTTP_ID: 'gimmie_good_data' + }) + } + + scanner = tomato.get_scanner(hass, config) + assert scanner.scan_devices() == ['F4:F5:D8:AA:AA:AA', '58:EF:68:00:00:00'] + + +@mock.patch('requests.Session.send', side_effect=mock_session_response) +def test_get_device_name(hass, mock_exception_logger): + """Test the setup with bad credentials. + + Representing the absolute path to a CA certificate bundle. + """ + config = { + DOMAIN: tomato.PLATFORM_SCHEMA({ + CONF_PLATFORM: tomato.DOMAIN, + CONF_HOST: 'tomato-router', + CONF_USERNAME: 'foo', + CONF_PASSWORD: 'bar', + tomato.CONF_HTTP_ID: 'gimmie_good_data' + }) + } + + scanner = tomato.get_scanner(hass, config) + assert scanner.get_device_name('F4:F5:D8:AA:AA:AA') == 'chromecast' + assert scanner.get_device_name('58:EF:68:00:00:00') == 'wemo' From a53dce5d1d71ac6e9bdc32467682cd4567d0be52 Mon Sep 17 00:00:00 2001 From: Gregory Dosh Date: Wed, 10 Jan 2018 16:30:21 -0600 Subject: [PATCH 03/10] lint errors --- tests/components/device_tracker/test_tomato.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/components/device_tracker/test_tomato.py b/tests/components/device_tracker/test_tomato.py index c00364e785d2c..beb16071876ba 100644 --- a/tests/components/device_tracker/test_tomato.py +++ b/tests/components/device_tracker/test_tomato.py @@ -22,7 +22,8 @@ def __init__(self, text, status_code): elif "gimmie_bad_data" in args[0].body: return MockSessionResponse('This shouldn\'t (wldev = be here.;', 200) elif "gimmie_good_data" in args[0].body: - return MockSessionResponse("wldev = [ ['eth1','F4:F5:D8:AA:AA:AA'," + return MockSessionResponse( + "wldev = [ ['eth1','F4:F5:D8:AA:AA:AA'," "-42,5500,1000,7043,0],['eth1','58:EF:68:00:00:00'," "-42,5500,1000,7043,0]];\n" "dhcpd_lease = [ ['chromecast','172.10.10.5','F4:F5:D8:AA:AA:AA'," From 00b9dcd9c3ac7bce48db51643b5826c0cd83365d Mon Sep 17 00:00:00 2001 From: Gregory Dosh Date: Wed, 10 Jan 2018 16:59:28 -0600 Subject: [PATCH 04/10] missing docstring --- tests/components/device_tracker/test_tomato.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/components/device_tracker/test_tomato.py b/tests/components/device_tracker/test_tomato.py index beb16071876ba..ea5e90391f3f8 100644 --- a/tests/components/device_tracker/test_tomato.py +++ b/tests/components/device_tracker/test_tomato.py @@ -10,6 +10,7 @@ def mock_session_response(*args, **kwargs): + """Mock data generation for session response.""" class MockSessionResponse: def __init__(self, text, status_code): self.text = text From 17c3b27789874b71c6e6202b75323f6f27d5d28b Mon Sep 17 00:00:00 2001 From: Gregory Dosh Date: Wed, 10 Jan 2018 17:42:44 -0600 Subject: [PATCH 05/10] fixing non-deterministic params --- tests/components/device_tracker/test_tomato.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/components/device_tracker/test_tomato.py b/tests/components/device_tracker/test_tomato.py index ea5e90391f3f8..e3e620bc3e028 100644 --- a/tests/components/device_tracker/test_tomato.py +++ b/tests/components/device_tracker/test_tomato.py @@ -125,7 +125,8 @@ def test_config_valid_verify_ssl_path(hass, mock_session_send): 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization': 'Basic YmFyOmZvbw==' } - assert result.req.body == "_http_id=0987654321&exec=devlist" + assert "_http_id=0987654321" in result.req.body + assert "exec=devlist" in result.req.body assert mock_session_send.call_count == 1 assert mock_session_send.mock_calls[0] == \ mock.call(result.req, timeout=3, verify="/tmp/tomato.crt") @@ -152,7 +153,8 @@ def test_config_valid_verify_ssl_bool(hass, mock_session_send): 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization': 'Basic YmFyOmZvbw==' } - assert result.req.body == "_http_id=0987654321&exec=devlist" + assert "_http_id=0987654321" in result.req.body + assert "exec=devlist" in result.req.body assert mock_session_send.call_count == 1 assert mock_session_send.mock_calls[0] == \ mock.call(result.req, timeout=3, verify=False) From 9b0614a003bd04084751ab9e7749488457d14168 Mon Sep 17 00:00:00 2001 From: Gregory Dosh Date: Wed, 10 Jan 2018 17:53:48 -0600 Subject: [PATCH 06/10] fixing non-deterministic params --- tests/components/device_tracker/test_tomato.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/components/device_tracker/test_tomato.py b/tests/components/device_tracker/test_tomato.py index e3e620bc3e028..655f1fdade279 100644 --- a/tests/components/device_tracker/test_tomato.py +++ b/tests/components/device_tracker/test_tomato.py @@ -67,7 +67,8 @@ def test_config_missing_optional_params(hass, mock_session_send): 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization': 'Basic Zm9vOnBhc3N3b3Jk' } - assert result.req.body == "_http_id=1234567890&exec=devlist" + assert "_http_id=1234567890" in result.req.body + assert "exec=devlist" in result.req.body @mock.patch('os.access', return_value=True) @@ -93,7 +94,8 @@ def test_config_verify_ssl_but_no_ssl_enabled(hass, mock_session_send): 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization': 'Basic Zm9vOnBhc3N3b3Jk' } - assert result.req.body == "_http_id=1234567890&exec=devlist" + assert "_http_id=1234567890" in result.req.body + assert "exec=devlist" in result.req.body assert mock_session_send.call_count == 1 assert mock_session_send.mock_calls[0] == \ mock.call(result.req, timeout=3) From 72e5806a5b084a39f427cad4a7ace8ff7d73a2b1 Mon Sep 17 00:00:00 2001 From: Gregory Dosh Date: Sat, 13 Jan 2018 11:37:34 -0600 Subject: [PATCH 07/10] Updating docstrings & added missing tests --- .../components/device_tracker/test_tomato.py | 91 +++++++++++++++---- 1 file changed, 75 insertions(+), 16 deletions(-) diff --git a/tests/components/device_tracker/test_tomato.py b/tests/components/device_tracker/test_tomato.py index 655f1fdade279..569aae62b5c7e 100644 --- a/tests/components/device_tracker/test_tomato.py +++ b/tests/components/device_tracker/test_tomato.py @@ -1,6 +1,8 @@ """The tests for the Tomato device tracker platform.""" from unittest import mock import pytest +import requests +import requests_mock import voluptuous as vol from homeassistant.components.device_tracker import DOMAIN, tomato as tomato @@ -66,7 +68,7 @@ def test_config_missing_optional_params(hass, mock_session_send): 'Content-Length': '32', 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization': 'Basic Zm9vOnBhc3N3b3Jk' - } + } assert "_http_id=1234567890" in result.req.body assert "exec=devlist" in result.req.body @@ -93,7 +95,7 @@ def test_config_verify_ssl_but_no_ssl_enabled(hass, mock_session_send): 'Content-Length': '32', 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization': 'Basic Zm9vOnBhc3N3b3Jk' - } + } assert "_http_id=1234567890" in result.req.body assert "exec=devlist" in result.req.body assert mock_session_send.call_count == 1 @@ -126,7 +128,7 @@ def test_config_valid_verify_ssl_path(hass, mock_session_send): 'Content-Length': '32', 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization': 'Basic YmFyOmZvbw==' - } + } assert "_http_id=0987654321" in result.req.body assert "exec=devlist" in result.req.body assert mock_session_send.call_count == 1 @@ -154,7 +156,7 @@ def test_config_valid_verify_ssl_bool(hass, mock_session_send): 'Content-Length': '32', 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization': 'Basic YmFyOmZvbw==' - } + } assert "_http_id=0987654321" in result.req.body assert "exec=devlist" in result.req.body assert mock_session_send.call_count == 1 @@ -223,10 +225,7 @@ def test_config_errors(): @mock.patch('requests.Session.send', side_effect=mock_session_response) def test_config_bad_credentials(hass, mock_exception_logger): - """Test the setup with bad credentials. - - Representing the absolute path to a CA certificate bundle. - """ + """Test the setup with bad credentials.""" config = { DOMAIN: tomato.PLATFORM_SCHEMA({ CONF_PLATFORM: tomato.DOMAIN, @@ -246,11 +245,28 @@ def test_config_bad_credentials(hass, mock_exception_logger): @mock.patch('requests.Session.send', side_effect=mock_session_response) -def test_scan_devices(hass, mock_exception_logger): - """Test the setup with bad credentials. +def test_bad_response(hass, mock_exception_logger): + """Test the setup with bad response from router.""" + config = { + DOMAIN: tomato.PLATFORM_SCHEMA({ + CONF_PLATFORM: tomato.DOMAIN, + CONF_HOST: 'tomato-router', + CONF_USERNAME: 'foo', + CONF_PASSWORD: 'bar', + tomato.CONF_HTTP_ID: 'gimmie_bad_data' + }) + } - Representing the absolute path to a CA certificate bundle. - """ + tomato.get_scanner(hass, config) + + assert mock_exception_logger.call_count == 1 + assert mock_exception_logger.mock_calls[0] == \ + mock.call("Failed to parse response from router") + + +@mock.patch('requests.Session.send', side_effect=mock_session_response) +def test_scan_devices(hass, mock_exception_logger): + """Test scanning for new devices.""" config = { DOMAIN: tomato.PLATFORM_SCHEMA({ CONF_PLATFORM: tomato.DOMAIN, @@ -266,11 +282,53 @@ def test_scan_devices(hass, mock_exception_logger): @mock.patch('requests.Session.send', side_effect=mock_session_response) -def test_get_device_name(hass, mock_exception_logger): - """Test the setup with bad credentials. +def test_bad_connection(hass, mock_exception_logger): + """Test the router with a connection error.""" + config = { + DOMAIN: tomato.PLATFORM_SCHEMA({ + CONF_PLATFORM: tomato.DOMAIN, + CONF_HOST: 'tomato-router', + CONF_USERNAME: 'foo', + CONF_PASSWORD: 'bar', + tomato.CONF_HTTP_ID: 'gimmie_good_data' + }) + } - Representing the absolute path to a CA certificate bundle. - """ + with requests_mock.Mocker() as adapter: + adapter.register_uri('POST', 'http://tomato-router:80/update.cgi', + exc=requests.exceptions.ConnectionError), + tomato.get_scanner(hass, config) + assert mock_exception_logger.call_count == 1 + assert mock_exception_logger.mock_calls[0] == \ + mock.call("Failed to connect to the router " + "or invalid http_id supplied") + + +@mock.patch('requests.Session.send', side_effect=mock_session_response) +def test_router_timeout(hass, mock_exception_logger): + """Test the router with a timeout error.""" + config = { + DOMAIN: tomato.PLATFORM_SCHEMA({ + CONF_PLATFORM: tomato.DOMAIN, + CONF_HOST: 'tomato-router', + CONF_USERNAME: 'foo', + CONF_PASSWORD: 'bar', + tomato.CONF_HTTP_ID: 'gimmie_good_data' + }) + } + + with requests_mock.Mocker() as adapter: + adapter.register_uri('POST', 'http://tomato-router:80/update.cgi', + exc=requests.exceptions.Timeout), + tomato.get_scanner(hass, config) + assert mock_exception_logger.call_count == 1 + assert mock_exception_logger.mock_calls[0] == \ + mock.call("Connection to the router timed out") + + +@mock.patch('requests.Session.send', side_effect=mock_session_response) +def test_get_device_name(hass, mock_exception_logger): + """Test getting device names.""" config = { DOMAIN: tomato.PLATFORM_SCHEMA({ CONF_PLATFORM: tomato.DOMAIN, @@ -284,3 +342,4 @@ def test_get_device_name(hass, mock_exception_logger): scanner = tomato.get_scanner(hass, config) assert scanner.get_device_name('F4:F5:D8:AA:AA:AA') == 'chromecast' assert scanner.get_device_name('58:EF:68:00:00:00') == 'wemo' + assert scanner.get_device_name('AA:BB:CC:00:00:00') is None From f3d9720efd504c688128c2a57c5c089b2da1f4d6 Mon Sep 17 00:00:00 2001 From: Gregory Dosh Date: Mon, 15 Jan 2018 16:07:39 -0600 Subject: [PATCH 08/10] revert _LOGGER --- homeassistant/components/device_tracker/tomato.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/tomato.py b/homeassistant/components/device_tracker/tomato.py index 1259f78d5d4db..6c87ea381c79b 100644 --- a/homeassistant/components/device_tracker/tomato.py +++ b/homeassistant/components/device_tracker/tomato.py @@ -20,7 +20,7 @@ CONF_HTTP_ID = 'http_id' -_LOGGER = logging.getLogger("{}.{}".format(__name__, "Tomato")) +_LOGGER = logging.getLogger(__name__) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, From 7be55ba50c4dc1279bfcf431dd411f3dfd0d89ba Mon Sep 17 00:00:00 2001 From: Gregory Dosh Date: Sun, 21 Jan 2018 18:32:06 -0600 Subject: [PATCH 09/10] updating default port to reflect ssl/nonssl --- .../components/device_tracker/tomato.py | 6 +++- .../components/device_tracker/test_tomato.py | 35 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/tomato.py b/homeassistant/components/device_tracker/tomato.py index 6c87ea381c79b..7cebf0abdf421 100644 --- a/homeassistant/components/device_tracker/tomato.py +++ b/homeassistant/components/device_tracker/tomato.py @@ -24,7 +24,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_PORT, default=80): cv.port, + vol.Optional(CONF_PORT, default=-1): cv.port, vol.Optional(CONF_SSL, default=False): cv.boolean, vol.Optional(CONF_VERIFY_SSL, default=True): vol.Any( cv.boolean, cv.isfile), @@ -48,6 +48,10 @@ def __init__(self, config): port = config[CONF_PORT] username, password = config[CONF_USERNAME], config[CONF_PASSWORD] self.ssl, self.verify_ssl = config[CONF_SSL], config[CONF_VERIFY_SSL] + if port == -1: + port = 80 + if self.ssl: + port = 443 self.req = requests.Request( 'POST', 'http{}://{}:{}/update.cgi'.format( diff --git a/tests/components/device_tracker/test_tomato.py b/tests/components/device_tracker/test_tomato.py index 569aae62b5c7e..c5af7e1ee0719 100644 --- a/tests/components/device_tracker/test_tomato.py +++ b/tests/components/device_tracker/test_tomato.py @@ -73,6 +73,41 @@ def test_config_missing_optional_params(hass, mock_session_send): assert "exec=devlist" in result.req.body +@mock.patch('os.access', return_value=True) +@mock.patch('os.path.isfile', mock.Mock(return_value=True)) +def test_config_default_nonssl_port(hass, mock_session_send): + """Test the setup with a string with ssl_verify but ssl not enabled.""" + config = { + DOMAIN: tomato.PLATFORM_SCHEMA({ + CONF_PLATFORM: tomato.DOMAIN, + CONF_HOST: 'tomato-router', + CONF_USERNAME: 'foo', + CONF_PASSWORD: 'password', + tomato.CONF_HTTP_ID: '1234567890' + }) + } + result = tomato.get_scanner(hass, config) + assert result.req.url == "http://tomato-router:80/update.cgi" + + +@mock.patch('os.access', return_value=True) +@mock.patch('os.path.isfile', mock.Mock(return_value=True)) +def test_config_default_ssl_port(hass, mock_session_send): + """Test the setup with a string with ssl_verify but ssl not enabled.""" + config = { + DOMAIN: tomato.PLATFORM_SCHEMA({ + CONF_PLATFORM: tomato.DOMAIN, + CONF_HOST: 'tomato-router', + CONF_SSL: True, + CONF_USERNAME: 'foo', + CONF_PASSWORD: 'password', + tomato.CONF_HTTP_ID: '1234567890' + }) + } + result = tomato.get_scanner(hass, config) + assert result.req.url == "https://tomato-router:443/update.cgi" + + @mock.patch('os.access', return_value=True) @mock.patch('os.path.isfile', mock.Mock(return_value=True)) def test_config_verify_ssl_but_no_ssl_enabled(hass, mock_session_send): From 9e5196b3959ac2751d6a0773ce550903615240c1 Mon Sep 17 00:00:00 2001 From: Gregory Dosh Date: Sun, 21 Jan 2018 18:45:39 -0600 Subject: [PATCH 10/10] fixing docstrings for tests --- tests/components/device_tracker/test_tomato.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/device_tracker/test_tomato.py b/tests/components/device_tracker/test_tomato.py index c5af7e1ee0719..cce39ce43a79d 100644 --- a/tests/components/device_tracker/test_tomato.py +++ b/tests/components/device_tracker/test_tomato.py @@ -76,7 +76,7 @@ def test_config_missing_optional_params(hass, mock_session_send): @mock.patch('os.access', return_value=True) @mock.patch('os.path.isfile', mock.Mock(return_value=True)) def test_config_default_nonssl_port(hass, mock_session_send): - """Test the setup with a string with ssl_verify but ssl not enabled.""" + """Test the setup without a default port set without ssl enabled.""" config = { DOMAIN: tomato.PLATFORM_SCHEMA({ CONF_PLATFORM: tomato.DOMAIN, @@ -93,7 +93,7 @@ def test_config_default_nonssl_port(hass, mock_session_send): @mock.patch('os.access', return_value=True) @mock.patch('os.path.isfile', mock.Mock(return_value=True)) def test_config_default_ssl_port(hass, mock_session_send): - """Test the setup with a string with ssl_verify but ssl not enabled.""" + """Test the setup without a default port set with ssl enabled.""" config = { DOMAIN: tomato.PLATFORM_SCHEMA({ CONF_PLATFORM: tomato.DOMAIN,