"""The tests for the facebox component.""" from unittest.mock import Mock, mock_open, patch import pytest import requests import requests_mock from homeassistant.core import callback from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_NAME, CONF_FRIENDLY_NAME, CONF_PASSWORD, CONF_USERNAME, CONF_IP_ADDRESS, CONF_PORT, HTTP_BAD_REQUEST, HTTP_OK, HTTP_UNAUTHORIZED, STATE_UNKNOWN, ) from homeassistant.setup import async_setup_component import homeassistant.components.image_processing as ip import homeassistant.components.facebox.image_processing as fb MOCK_IP = "192.168.0.1" MOCK_PORT = "8080" # Mock data returned by the facebox API. MOCK_BOX_ID = "b893cc4f7fd6" MOCK_ERROR_NO_FACE = "No face found" MOCK_FACE = { "confidence": 0.5812028911604818, "id": "john.jpg", "matched": True, "name": "John Lennon", "rect": {"height": 75, "left": 63, "top": 262, "width": 74}, } MOCK_FILE_PATH = "/images/mock.jpg" MOCK_HEALTH = { "success": True, "hostname": "b893cc4f7fd6", "metadata": {"boxname": "facebox", "build": "development"}, "errors": [], } MOCK_JSON = {"facesCount": 1, "success": True, "faces": [MOCK_FACE]} MOCK_NAME = "mock_name" MOCK_USERNAME = "mock_username" MOCK_PASSWORD = "mock_password" # Faces data after parsing. PARSED_FACES = [ { fb.FACEBOX_NAME: "John Lennon", fb.ATTR_IMAGE_ID: "john.jpg", fb.ATTR_CONFIDENCE: 58.12, fb.ATTR_MATCHED: True, fb.ATTR_BOUNDING_BOX: {"height": 75, "left": 63, "top": 262, "width": 74}, } ] MATCHED_FACES = {"John Lennon": 58.12} VALID_ENTITY_ID = "image_processing.facebox_demo_camera" VALID_CONFIG = { ip.DOMAIN: { "platform": "facebox", CONF_IP_ADDRESS: MOCK_IP, CONF_PORT: MOCK_PORT, ip.CONF_SOURCE: {ip.CONF_ENTITY_ID: "camera.demo_camera"}, }, "camera": {"platform": "demo"}, } @pytest.fixture def mock_healthybox(): """Mock fb.check_box_health.""" check_box_health = ( "homeassistant.components.facebox.image_processing." "check_box_health" ) with patch(check_box_health, return_value=MOCK_BOX_ID) as _mock_healthybox: yield _mock_healthybox @pytest.fixture def mock_isfile(): """Mock os.path.isfile.""" with patch( "homeassistant.components.facebox.image_processing.cv.isfile", return_value=True ) as _mock_isfile: yield _mock_isfile @pytest.fixture def mock_image(): """Return a mock camera image.""" with patch( "homeassistant.components.demo.camera.DemoCamera.camera_image", return_value=b"Test", ) as image: yield image @pytest.fixture def mock_open_file(): """Mock open.""" mopen = mock_open() with patch( "homeassistant.components.facebox.image_processing.open", mopen, create=True ) as _mock_open: yield _mock_open def test_check_box_health(caplog): """Test check box health.""" with requests_mock.Mocker() as mock_req: url = "http://{}:{}/healthz".format(MOCK_IP, MOCK_PORT) mock_req.get(url, status_code=HTTP_OK, json=MOCK_HEALTH) assert fb.check_box_health(url, "user", "pass") == MOCK_BOX_ID mock_req.get(url, status_code=HTTP_UNAUTHORIZED) assert fb.check_box_health(url, None, None) is None assert "AuthenticationError on facebox" in caplog.text mock_req.get(url, exc=requests.exceptions.ConnectTimeout) fb.check_box_health(url, None, None) assert "ConnectionError: Is facebox running?" in caplog.text def test_encode_image(): """Test that binary data is encoded correctly.""" assert fb.encode_image(b"test") == "dGVzdA==" def test_get_matched_faces(): """Test that matched_faces are parsed correctly.""" assert fb.get_matched_faces(PARSED_FACES) == MATCHED_FACES def test_parse_faces(): """Test parsing of raw face data, and generation of matched_faces.""" assert fb.parse_faces(MOCK_JSON["faces"]) == PARSED_FACES @patch("os.access", Mock(return_value=False)) def test_valid_file_path(): """Test that an invalid file_path is caught.""" assert not fb.valid_file_path("test_path") async def test_setup_platform(hass, mock_healthybox): """Set up platform with one entity.""" await async_setup_component(hass, ip.DOMAIN, VALID_CONFIG) assert hass.states.get(VALID_ENTITY_ID) async def test_setup_platform_with_auth(hass, mock_healthybox): """Set up platform with one entity and auth.""" valid_config_auth = VALID_CONFIG.copy() valid_config_auth[ip.DOMAIN][CONF_USERNAME] = MOCK_USERNAME valid_config_auth[ip.DOMAIN][CONF_PASSWORD] = MOCK_PASSWORD await async_setup_component(hass, ip.DOMAIN, valid_config_auth) assert hass.states.get(VALID_ENTITY_ID) async def test_process_image(hass, mock_healthybox, mock_image): """Test successful processing of an image.""" await async_setup_component(hass, ip.DOMAIN, VALID_CONFIG) assert hass.states.get(VALID_ENTITY_ID) face_events = [] @callback def mock_face_event(event): """Mock event.""" face_events.append(event) hass.bus.async_listen("image_processing.detect_face", mock_face_event) with requests_mock.Mocker() as mock_req: url = "http://{}:{}/facebox/check".format(MOCK_IP, MOCK_PORT) mock_req.post(url, json=MOCK_JSON) data = {ATTR_ENTITY_ID: VALID_ENTITY_ID} await hass.services.async_call(ip.DOMAIN, ip.SERVICE_SCAN, service_data=data) await hass.async_block_till_done() state = hass.states.get(VALID_ENTITY_ID) assert state.state == "1" assert state.attributes.get("matched_faces") == MATCHED_FACES assert state.attributes.get("total_matched_faces") == 1 PARSED_FACES[0][ATTR_ENTITY_ID] = VALID_ENTITY_ID # Update. assert state.attributes.get("faces") == PARSED_FACES assert state.attributes.get(CONF_FRIENDLY_NAME) == "facebox demo_camera" assert len(face_events) == 1 assert face_events[0].data[ATTR_NAME] == PARSED_FACES[0][ATTR_NAME] assert ( face_events[0].data[fb.ATTR_CONFIDENCE] == PARSED_FACES[0][fb.ATTR_CONFIDENCE] ) assert face_events[0].data[ATTR_ENTITY_ID] == VALID_ENTITY_ID assert face_events[0].data[fb.ATTR_IMAGE_ID] == PARSED_FACES[0][fb.ATTR_IMAGE_ID] assert ( face_events[0].data[fb.ATTR_BOUNDING_BOX] == PARSED_FACES[0][fb.ATTR_BOUNDING_BOX] ) async def test_process_image_errors(hass, mock_healthybox, mock_image, caplog): """Test process_image errors.""" await async_setup_component(hass, ip.DOMAIN, VALID_CONFIG) assert hass.states.get(VALID_ENTITY_ID) # Test connection error. with requests_mock.Mocker() as mock_req: url = "http://{}:{}/facebox/check".format(MOCK_IP, MOCK_PORT) mock_req.register_uri("POST", url, exc=requests.exceptions.ConnectTimeout) data = {ATTR_ENTITY_ID: VALID_ENTITY_ID} await hass.services.async_call(ip.DOMAIN, ip.SERVICE_SCAN, service_data=data) await hass.async_block_till_done() assert "ConnectionError: Is facebox running?" in caplog.text state = hass.states.get(VALID_ENTITY_ID) assert state.state == STATE_UNKNOWN assert state.attributes.get("faces") == [] assert state.attributes.get("matched_faces") == {} # Now test with bad auth. with requests_mock.Mocker() as mock_req: url = "http://{}:{}/facebox/check".format(MOCK_IP, MOCK_PORT) mock_req.register_uri("POST", url, status_code=HTTP_UNAUTHORIZED) data = {ATTR_ENTITY_ID: VALID_ENTITY_ID} await hass.services.async_call(ip.DOMAIN, ip.SERVICE_SCAN, service_data=data) await hass.async_block_till_done() assert "AuthenticationError on facebox" in caplog.text async def test_teach_service( hass, mock_healthybox, mock_image, mock_isfile, mock_open_file, caplog ): """Test teaching of facebox.""" await async_setup_component(hass, ip.DOMAIN, VALID_CONFIG) assert hass.states.get(VALID_ENTITY_ID) # Patch out 'is_allowed_path' as the mock files aren't allowed hass.config.is_allowed_path = Mock(return_value=True) # Test successful teach. with requests_mock.Mocker() as mock_req: url = "http://{}:{}/facebox/teach".format(MOCK_IP, MOCK_PORT) mock_req.post(url, status_code=HTTP_OK) data = { ATTR_ENTITY_ID: VALID_ENTITY_ID, ATTR_NAME: MOCK_NAME, fb.FILE_PATH: MOCK_FILE_PATH, } await hass.services.async_call( ip.DOMAIN, fb.SERVICE_TEACH_FACE, service_data=data ) await hass.async_block_till_done() # Now test with bad auth. with requests_mock.Mocker() as mock_req: url = "http://{}:{}/facebox/teach".format(MOCK_IP, MOCK_PORT) mock_req.post(url, status_code=HTTP_UNAUTHORIZED) data = { ATTR_ENTITY_ID: VALID_ENTITY_ID, ATTR_NAME: MOCK_NAME, fb.FILE_PATH: MOCK_FILE_PATH, } await hass.services.async_call( ip.DOMAIN, fb.SERVICE_TEACH_FACE, service_data=data ) await hass.async_block_till_done() assert "AuthenticationError on facebox" in caplog.text # Now test the failed teaching. with requests_mock.Mocker() as mock_req: url = "http://{}:{}/facebox/teach".format(MOCK_IP, MOCK_PORT) mock_req.post(url, status_code=HTTP_BAD_REQUEST, text=MOCK_ERROR_NO_FACE) data = { ATTR_ENTITY_ID: VALID_ENTITY_ID, ATTR_NAME: MOCK_NAME, fb.FILE_PATH: MOCK_FILE_PATH, } await hass.services.async_call( ip.DOMAIN, fb.SERVICE_TEACH_FACE, service_data=data ) await hass.async_block_till_done() assert MOCK_ERROR_NO_FACE in caplog.text # Now test connection error. with requests_mock.Mocker() as mock_req: url = "http://{}:{}/facebox/teach".format(MOCK_IP, MOCK_PORT) mock_req.post(url, exc=requests.exceptions.ConnectTimeout) data = { ATTR_ENTITY_ID: VALID_ENTITY_ID, ATTR_NAME: MOCK_NAME, fb.FILE_PATH: MOCK_FILE_PATH, } await hass.services.async_call( ip.DOMAIN, fb.SERVICE_TEACH_FACE, service_data=data ) await hass.async_block_till_done() assert "ConnectionError: Is facebox running?" in caplog.text async def test_setup_platform_with_name(hass, mock_healthybox): """Set up platform with one entity and a name.""" named_entity_id = "image_processing.{}".format(MOCK_NAME) valid_config_named = VALID_CONFIG.copy() valid_config_named[ip.DOMAIN][ip.CONF_SOURCE][ip.CONF_NAME] = MOCK_NAME await async_setup_component(hass, ip.DOMAIN, valid_config_named) assert hass.states.get(named_entity_id) state = hass.states.get(named_entity_id) assert state.attributes.get(CONF_FRIENDLY_NAME) == MOCK_NAME