"""The tests for the REST sensor platform."""
import unittest
import pytest
from pytest import raises
import requests
from requests.exceptions import RequestException, Timeout
from requests.structures import CaseInsensitiveDict
import requests_mock
import homeassistant.components.rest.sensor as rest
import homeassistant.components.sensor as sensor
from homeassistant.const import DATA_MEGABYTES
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.config_validation import template
from homeassistant.setup import setup_component
from tests.async_mock import Mock, patch
from tests.common import assert_setup_component, get_test_home_assistant
class TestRestSensorSetup(unittest.TestCase):
"""Tests for setting up the REST sensor platform."""
def setUp(self):
"""Set up things to be run when tests are started."""
self.hass = get_test_home_assistant()
def tearDown(self):
"""Stop everything that was started."""
self.hass.stop()
def test_setup_missing_config(self):
"""Test setup with configuration missing required entries."""
with assert_setup_component(0):
assert setup_component(
self.hass, sensor.DOMAIN, {"sensor": {"platform": "rest"}}
)
def test_setup_missing_schema(self):
"""Test setup with resource missing schema."""
with pytest.raises(PlatformNotReady):
rest.setup_platform(
self.hass,
{"platform": "rest", "resource": "localhost", "method": "GET"},
None,
)
@patch("requests.Session.send", side_effect=requests.exceptions.ConnectionError())
def test_setup_failed_connect(self, mock_req):
"""Test setup when connection error occurs."""
with raises(PlatformNotReady):
rest.setup_platform(
self.hass,
{"platform": "rest", "resource": "http://localhost", "method": "GET"},
lambda devices, update=True: None,
)
@patch("requests.Session.send", side_effect=Timeout())
def test_setup_timeout(self, mock_req):
"""Test setup when connection timeout occurs."""
with raises(PlatformNotReady):
rest.setup_platform(
self.hass,
{"platform": "rest", "resource": "http://localhost", "method": "GET"},
lambda devices, update=True: None,
)
@requests_mock.Mocker()
def test_setup_minimum(self, mock_req):
"""Test setup with minimum configuration."""
mock_req.get("http://localhost", status_code=200)
with assert_setup_component(1, "sensor"):
assert setup_component(
self.hass,
"sensor",
{"sensor": {"platform": "rest", "resource": "http://localhost"}},
)
self.hass.block_till_done()
assert 2 == mock_req.call_count
@requests_mock.Mocker()
def test_setup_minimum_resource_template(self, mock_req):
"""Test setup with minimum configuration (resource_template)."""
mock_req.get("http://localhost", status_code=200)
with assert_setup_component(1, "sensor"):
assert setup_component(
self.hass,
"sensor",
{
"sensor": {
"platform": "rest",
"resource_template": "http://localhost",
}
},
)
self.hass.block_till_done()
assert mock_req.call_count == 2
@requests_mock.Mocker()
def test_setup_duplicate_resource(self, mock_req):
"""Test setup with duplicate resources."""
mock_req.get("http://localhost", status_code=200)
with assert_setup_component(0, "sensor"):
assert setup_component(
self.hass,
"sensor",
{
"sensor": {
"platform": "rest",
"resource": "http://localhost",
"resource_template": "http://localhost",
}
},
)
self.hass.block_till_done()
@requests_mock.Mocker()
def test_setup_get(self, mock_req):
"""Test setup with valid configuration."""
mock_req.get("http://localhost", status_code=200)
with assert_setup_component(1, "sensor"):
assert setup_component(
self.hass,
"sensor",
{
"sensor": {
"platform": "rest",
"resource": "http://localhost",
"method": "GET",
"value_template": "{{ value_json.key }}",
"name": "foo",
"unit_of_measurement": DATA_MEGABYTES,
"verify_ssl": "true",
"timeout": 30,
"authentication": "basic",
"username": "my username",
"password": "my password",
"headers": {"Accept": "application/json"},
}
},
)
self.hass.block_till_done()
assert 2 == mock_req.call_count
@requests_mock.Mocker()
def test_setup_post(self, mock_req):
"""Test setup with valid configuration."""
mock_req.post("http://localhost", status_code=200)
with assert_setup_component(1, "sensor"):
assert setup_component(
self.hass,
"sensor",
{
"sensor": {
"platform": "rest",
"resource": "http://localhost",
"method": "POST",
"value_template": "{{ value_json.key }}",
"payload": '{ "device": "toaster"}',
"name": "foo",
"unit_of_measurement": DATA_MEGABYTES,
"verify_ssl": "true",
"timeout": 30,
"authentication": "basic",
"username": "my username",
"password": "my password",
"headers": {"Accept": "application/json"},
}
},
)
self.hass.block_till_done()
assert 2 == mock_req.call_count
@requests_mock.Mocker()
def test_setup_get_xml(self, mock_req):
"""Test setup with valid configuration."""
mock_req.get("http://localhost", status_code=200)
with assert_setup_component(1, "sensor"):
assert setup_component(
self.hass,
"sensor",
{
"sensor": {
"platform": "rest",
"resource": "http://localhost",
"method": "GET",
"value_template": "{{ value_json.key }}",
"name": "foo",
"unit_of_measurement": DATA_MEGABYTES,
"verify_ssl": "true",
"timeout": 30,
"authentication": "basic",
"username": "my username",
"password": "my password",
"headers": {"Accept": "text/xml"},
}
},
)
self.hass.block_till_done()
assert 2 == mock_req.call_count
class TestRestSensor(unittest.TestCase):
"""Tests for REST sensor platform."""
def setUp(self):
"""Set up things to be run when tests are started."""
self.hass = get_test_home_assistant()
self.initial_state = "initial_state"
self.rest = Mock("rest.RestData")
self.rest.update = Mock(
"rest.RestData.update",
side_effect=self.update_side_effect(
'{ "key": "' + self.initial_state + '" }',
CaseInsensitiveDict({"Content-Type": "application/json"}),
),
)
self.name = "foo"
self.unit_of_measurement = DATA_MEGABYTES
self.device_class = None
self.value_template = template("{{ value_json.key }}")
self.json_attrs_path = None
self.value_template.hass = self.hass
self.force_update = False
self.resource_template = None
self.sensor = rest.RestSensor(
self.hass,
self.rest,
self.name,
self.unit_of_measurement,
self.device_class,
self.value_template,
[],
self.force_update,
self.resource_template,
self.json_attrs_path,
)
def tearDown(self):
"""Stop everything that was started."""
self.hass.stop()
def update_side_effect(self, data, headers):
"""Side effect function for mocking RestData.update()."""
self.rest.data = data
self.rest.headers = headers
def test_name(self):
"""Test the name."""
assert self.name == self.sensor.name
def test_unit_of_measurement(self):
"""Test the unit of measurement."""
assert self.unit_of_measurement == self.sensor.unit_of_measurement
def test_force_update(self):
"""Test the unit of measurement."""
assert self.force_update == self.sensor.force_update
def test_state(self):
"""Test the initial state."""
self.sensor.update()
assert self.initial_state == self.sensor.state
def test_update_when_value_is_none(self):
"""Test state gets updated to unknown when sensor returns no data."""
self.rest.update = Mock(
"rest.RestData.update",
side_effect=self.update_side_effect(None, CaseInsensitiveDict()),
)
self.sensor.update()
assert self.sensor.state is None
assert not self.sensor.available
def test_update_when_value_changed(self):
"""Test state gets updated when sensor returns a new status."""
self.rest.update = Mock(
"rest.RestData.update",
side_effect=self.update_side_effect(
'{ "key": "updated_state" }',
CaseInsensitiveDict({"Content-Type": "application/json"}),
),
)
self.sensor.update()
assert "updated_state" == self.sensor.state
assert self.sensor.available
def test_update_with_no_template(self):
"""Test update when there is no value template."""
self.rest.update = Mock(
"rest.RestData.update",
side_effect=self.update_side_effect(
"plain_state", CaseInsensitiveDict({"Content-Type": "application/json"})
),
)
self.sensor = rest.RestSensor(
self.hass,
self.rest,
self.name,
self.unit_of_measurement,
self.device_class,
None,
[],
self.force_update,
self.resource_template,
self.json_attrs_path,
)
self.sensor.update()
assert "plain_state" == self.sensor.state
assert self.sensor.available
def test_update_with_json_attrs(self):
"""Test attributes get extracted from a JSON result."""
self.rest.update = Mock(
"rest.RestData.update",
side_effect=self.update_side_effect(
'{ "key": "some_json_value" }',
CaseInsensitiveDict({"Content-Type": "application/json"}),
),
)
self.sensor = rest.RestSensor(
self.hass,
self.rest,
self.name,
self.unit_of_measurement,
self.device_class,
None,
["key"],
self.force_update,
self.resource_template,
self.json_attrs_path,
)
self.sensor.update()
assert "some_json_value" == self.sensor.device_state_attributes["key"]
def test_update_with_json_attrs_list_dict(self):
"""Test attributes get extracted from a JSON list[0] result."""
self.rest.update = Mock(
"rest.RestData.update",
side_effect=self.update_side_effect(
'[{ "key": "another_value" }]',
CaseInsensitiveDict({"Content-Type": "application/json"}),
),
)
self.sensor = rest.RestSensor(
self.hass,
self.rest,
self.name,
self.unit_of_measurement,
self.device_class,
None,
["key"],
self.force_update,
self.resource_template,
self.json_attrs_path,
)
self.sensor.update()
assert "another_value" == self.sensor.device_state_attributes["key"]
@patch("homeassistant.components.rest.sensor._LOGGER")
def test_update_with_json_attrs_no_data(self, mock_logger):
"""Test attributes when no JSON result fetched."""
self.rest.update = Mock(
"rest.RestData.update",
side_effect=self.update_side_effect(
None, CaseInsensitiveDict({"Content-Type": "application/json"})
),
)
self.sensor = rest.RestSensor(
self.hass,
self.rest,
self.name,
self.unit_of_measurement,
self.device_class,
None,
["key"],
self.force_update,
self.resource_template,
self.json_attrs_path,
)
self.sensor.update()
assert {} == self.sensor.device_state_attributes
assert mock_logger.warning.called
@patch("homeassistant.components.rest.sensor._LOGGER")
def test_update_with_json_attrs_not_dict(self, mock_logger):
"""Test attributes get extracted from a JSON result."""
self.rest.update = Mock(
"rest.RestData.update",
side_effect=self.update_side_effect(
'["list", "of", "things"]',
CaseInsensitiveDict({"Content-Type": "application/json"}),
),
)
self.sensor = rest.RestSensor(
self.hass,
self.rest,
self.name,
self.unit_of_measurement,
self.device_class,
None,
["key"],
self.force_update,
self.resource_template,
self.json_attrs_path,
)
self.sensor.update()
assert {} == self.sensor.device_state_attributes
assert mock_logger.warning.called
@patch("homeassistant.components.rest.sensor._LOGGER")
def test_update_with_json_attrs_bad_JSON(self, mock_logger):
"""Test attributes get extracted from a JSON result."""
self.rest.update = Mock(
"rest.RestData.update",
side_effect=self.update_side_effect(
"This is text rather than JSON data.",
CaseInsensitiveDict({"Content-Type": "text/plain"}),
),
)
self.sensor = rest.RestSensor(
self.hass,
self.rest,
self.name,
self.unit_of_measurement,
self.device_class,
None,
["key"],
self.force_update,
self.resource_template,
self.json_attrs_path,
)
self.sensor.update()
assert {} == self.sensor.device_state_attributes
assert mock_logger.warning.called
assert mock_logger.debug.called
def test_update_with_json_attrs_and_template(self):
"""Test attributes get extracted from a JSON result."""
self.rest.update = Mock(
"rest.RestData.update",
side_effect=self.update_side_effect(
'{ "key": "json_state_updated_value" }',
CaseInsensitiveDict({"Content-Type": "application/json"}),
),
)
self.sensor = rest.RestSensor(
self.hass,
self.rest,
self.name,
self.unit_of_measurement,
self.device_class,
self.value_template,
["key"],
self.force_update,
self.resource_template,
self.json_attrs_path,
)
self.sensor.update()
assert "json_state_updated_value" == self.sensor.state
assert (
"json_state_updated_value" == self.sensor.device_state_attributes["key"]
), self.force_update
def test_update_with_json_attrs_with_json_attrs_path(self):
"""Test attributes get extracted from a JSON result with a template for the attributes."""
json_attrs_path = "$.toplevel.second_level"
value_template = template("{{ value_json.toplevel.master_value }}")
value_template.hass = self.hass
self.rest.update = Mock(
"rest.RestData.update",
side_effect=self.update_side_effect(
'{ "toplevel": {"master_value": "master", "second_level": {"some_json_key": "some_json_value", "some_json_key2": "some_json_value2" } } }',
CaseInsensitiveDict({"Content-Type": "application/json"}),
),
)
self.sensor = rest.RestSensor(
self.hass,
self.rest,
self.name,
self.unit_of_measurement,
self.device_class,
value_template,
["some_json_key", "some_json_key2"],
self.force_update,
self.resource_template,
json_attrs_path,
)
self.sensor.update()
assert "some_json_value" == self.sensor.device_state_attributes["some_json_key"]
assert (
"some_json_value2" == self.sensor.device_state_attributes["some_json_key2"]
)
assert "master" == self.sensor.state
def test_update_with_xml_convert_json_attrs_with_json_attrs_path(self):
"""Test attributes get extracted from a JSON result that was converted from XML with a template for the attributes."""
json_attrs_path = "$.toplevel.second_level"
value_template = template("{{ value_json.toplevel.master_value }}")
value_template.hass = self.hass
self.rest.update = Mock(
"rest.RestData.update",
side_effect=self.update_side_effect(
"mastersome_json_valuesome_json_value2",
CaseInsensitiveDict({"Content-Type": "text/xml+svg"}),
),
)
self.sensor = rest.RestSensor(
self.hass,
self.rest,
self.name,
self.unit_of_measurement,
self.device_class,
value_template,
["some_json_key", "some_json_key2"],
self.force_update,
self.resource_template,
json_attrs_path,
)
self.sensor.update()
assert "some_json_value" == self.sensor.device_state_attributes["some_json_key"]
assert (
"some_json_value2" == self.sensor.device_state_attributes["some_json_key2"]
)
assert "master" == self.sensor.state
def test_update_with_xml_convert_json_attrs_with_jsonattr_template(self):
"""Test attributes get extracted from a JSON result that was converted from XML."""
json_attrs_path = "$.response"
value_template = template("{{ value_json.response.bss.wlan }}")
value_template.hass = self.hass
self.rest.update = Mock(
"rest.RestData.update",
side_effect=self.update_side_effect(
'01255648alexander000bogus000000000upupupup000x0XF0x0XF 0',
CaseInsensitiveDict({"Content-Type": "text/xml"}),
),
)
self.sensor = rest.RestSensor(
self.hass,
self.rest,
self.name,
self.unit_of_measurement,
self.device_class,
value_template,
["led0", "led1", "temp0", "time0", "ver"],
self.force_update,
self.resource_template,
json_attrs_path,
)
self.sensor.update()
assert "0" == self.sensor.device_state_attributes["led0"]
assert "0" == self.sensor.device_state_attributes["led1"]
assert "0x0XF0x0XF" == self.sensor.device_state_attributes["temp0"]
assert "0" == self.sensor.device_state_attributes["time0"]
assert "12556" == self.sensor.device_state_attributes["ver"]
assert "bogus" == self.sensor.state
def test_update_with_application_xml_convert_json_attrs_with_jsonattr_template(
self,
):
"""Test attributes get extracted from a JSON result that was converted from XML with application/xml mime type."""
json_attrs_path = "$.main"
value_template = template("{{ value_json.main.dog }}")
value_template.hass = self.hass
self.rest.update = Mock(
"rest.RestData.update",
side_effect=self.update_side_effect(
"13",
CaseInsensitiveDict({"Content-Type": "application/xml"}),
),
)
self.sensor = rest.RestSensor(
self.hass,
self.rest,
self.name,
self.unit_of_measurement,
self.device_class,
value_template,
["dog", "cat"],
self.force_update,
self.resource_template,
json_attrs_path,
)
self.sensor.update()
assert "3" == self.sensor.device_state_attributes["cat"]
assert "1" == self.sensor.device_state_attributes["dog"]
assert "1" == self.sensor.state
@patch("homeassistant.components.rest.sensor._LOGGER")
def test_update_with_xml_convert_bad_xml(self, mock_logger):
"""Test attributes get extracted from a XML result with bad xml."""
value_template = template("{{ value_json.toplevel.master_value }}")
value_template.hass = self.hass
self.rest.update = Mock(
"rest.RestData.update",
side_effect=self.update_side_effect(
"this is not xml", CaseInsensitiveDict({"Content-Type": "text/xml"})
),
)
self.sensor = rest.RestSensor(
self.hass,
self.rest,
self.name,
self.unit_of_measurement,
self.device_class,
value_template,
["key"],
self.force_update,
self.resource_template,
self.json_attrs_path,
)
self.sensor.update()
assert {} == self.sensor.device_state_attributes
assert mock_logger.warning.called
assert mock_logger.debug.called
@patch("homeassistant.components.rest.sensor._LOGGER")
def test_update_with_failed_get(self, mock_logger):
"""Test attributes get extracted from a XML result with bad xml."""
value_template = template("{{ value_json.toplevel.master_value }}")
value_template.hass = self.hass
self.rest.update = Mock(
"rest.RestData.update", side_effect=self.update_side_effect(None, None),
)
self.sensor = rest.RestSensor(
self.hass,
self.rest,
self.name,
self.unit_of_measurement,
self.device_class,
value_template,
["key"],
self.force_update,
self.resource_template,
self.json_attrs_path,
)
self.sensor.update()
assert {} == self.sensor.device_state_attributes
assert mock_logger.warning.called
assert mock_logger.debug.called
assert self.sensor.state is None
assert self.sensor.available is False
class TestRestData(unittest.TestCase):
"""Tests for RestData."""
def setUp(self):
"""Set up things to be run when tests are started."""
self.method = "GET"
self.resource = "http://localhost"
self.verify_ssl = True
self.timeout = 10
self.rest = rest.RestData(
self.method, self.resource, None, None, None, self.verify_ssl, self.timeout
)
@requests_mock.Mocker()
def test_update(self, mock_req):
"""Test update."""
mock_req.get("http://localhost", text="test data")
self.rest.update()
assert "test data" == self.rest.data
@patch("requests.Session.request", side_effect=RequestException)
def test_update_request_exception(self, mock_req):
"""Test update when a request exception occurs."""
self.rest.update()
assert self.rest.data is None