Skip to content

Commit

Permalink
Improve es_gateway coverage
Browse files Browse the repository at this point in the history
  • Loading branch information
strawgate committed Jan 10, 2025
1 parent 6dc7257 commit 92ed07e
Show file tree
Hide file tree
Showing 3 changed files with 124 additions and 100 deletions.
24 changes: 19 additions & 5 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ def es_aioclient_mock():
with mock_es_aiohttp_client() as mock_session:
yield mock_session


def self_signed_tls_error():
"""Return a self-signed certificate error."""
connection_key = MagicMock()
Expand All @@ -206,12 +207,10 @@ def self_signed_tls_error():
certificate_error.strerror = "[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate in certificate chain (_ssl.c:1000)"
certificate_error.errno = 1

ssl_exception = client_exceptions.ClientConnectorCertificateError(
return client_exceptions.ClientConnectorCertificateError(
connection_key=connection_key, certificate_error=certificate_error
)

return ssl_exception


class es_mocker:
"""Mock builder for Elasticsearch integration tests."""
Expand Down Expand Up @@ -241,8 +240,11 @@ def clear(self):

return self

def with_server_error(self, status, exc=None):
def with_server_error(self, status=None, exc=None):
"""Mock Elasticsearch being unreachable."""
if status is None and exc is None:
self.mocker.get(f"{const.TEST_CONFIG_ENTRY_DATA_URL}", status=HTTPStatus.INTERNAL_SERVER_ERROR)

if exc is None:
self.mocker.get(f"{const.TEST_CONFIG_ENTRY_DATA_URL}", status=status)
else:
Expand Down Expand Up @@ -331,6 +333,18 @@ def as_elasticsearch_8_14(self, with_security: bool = True):

return self._as_elasticsearch_stateful(const.CLUSTER_INFO_8DOT14_RESPONSE_BODY, with_security)

def as_fake_elasticsearch(self) -> es_mocker:
"""Mock a fake elasticsearch node response."""

self.mocker.get(
f"{self.base_url}",
status=200,
# No x-elastic-product header
json=const.CLUSTER_INFO_8DOT14_RESPONSE_BODY,
)

return self

def as_elasticsearch_serverless(self) -> es_mocker:
"""Mock Elasticsearch version."""

Expand Down Expand Up @@ -514,7 +528,7 @@ async def add_to_hass() -> bool:


@pytest.fixture(autouse=True)
async def fix_location(hass: HomeAssistant):
async def _fix_location(hass: HomeAssistant):
"""Return whether to fix the location."""

hass.config.latitude = 1.0
Expand Down
17 changes: 17 additions & 0 deletions tests/test_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,23 @@ async def test_setup_authentication_failure(
assert config_entry.state is ConfigEntryState.SETUP_ERROR
assert config_entry.reason == "could not authenticate"

async def test_setup_fake_elasticsearch_error(
self, hass: HomeAssistant, integration_setup, es_mock_builder, config_entry
):
"""Test the scenario where we are not connecting to an authentic Elasticsearch endpoint."""

es_mock_builder.as_fake_elasticsearch()

assert await integration_setup() is False

assert config_entry.version == ElasticFlowHandler.VERSION

assert config_entry.state is ConfigEntryState.SETUP_RETRY
assert (
config_entry.reason
== "Error retrieving cluster info from Elasticsearch. Unsupported product error connecting to Elasticsearch"
)

async def test_setup_server_error(
self, hass: HomeAssistant, integration_setup, es_mock_builder, config_entry
):
Expand Down
183 changes: 88 additions & 95 deletions tests/test_es_gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import os
import ssl
from ssl import SSLCertVerificationError
from typing import Any
from unittest.mock import AsyncMock, MagicMock

Expand Down Expand Up @@ -34,6 +35,26 @@
)


def self_signed_tls_error():
"""Return a self-signed certificate error."""
connection_key = MagicMock()
connection_key.host = "mock_es_integration"
connection_key.port = 9200
connection_key.is_ssl = True

certificate_error = SSLCertVerificationError()
certificate_error.verify_code = 19
certificate_error.verify_message = "'self-signed certificate in certificate chain'"
certificate_error.library = "SSL"
certificate_error.reason = "CERTIFICATE_VERIFY_FAILED"
certificate_error.strerror = "[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate in certificate chain (_ssl.c:1000)"
certificate_error.errno = 1

return client_exceptions.ClientConnectorCertificateError(
connection_key=connection_key, certificate_error=certificate_error
)


def mock_es_exception(exception, message="None"):
"""Return an AsyncMock that mocks an Elasticsearch API response."""

Expand Down Expand Up @@ -249,34 +270,6 @@ async def test_async_init_unreachable(self, gateway_mock_client) -> None:
with pytest.raises(CannotConnect):
assert await gateway_mock_client.async_init() is None

class Test_Failures:
"""Test failure scenarios during initialization."""

@pytest.mark.asyncio
async def test_async_init_ssl_error(self, gateway, es_mock_builder):
"""Test async_init when there are insufficient privileges."""

es_mock_builder.with_selfsigned_certificate()

with pytest.raises(UntrustedCertificate):
await gateway.async_init()

async def test_async_init_unauthorized(self, gateway: ElasticsearchGateway, es_mock_builder) -> None:
"""Test the async_init method with unauthorized user."""

# es_mock_builder.as_elasticsearch_8_17().with_incorrect_permissions()
es_mock_builder.with_server_error(status=403)
with pytest.raises(InsufficientPrivileges):
assert await gateway.async_init() is None

async def test_async_init_unreachable(self, gateway: ElasticsearchGateway, es_mock_builder) -> None:
"""Test the async_init method with unreachable Elasticsearch."""

es_mock_builder.with_server_timeout()

with pytest.raises(CannotConnect):
assert await gateway.async_init() is None


class Test_Public_Functions:
"""Public function tests for the Elasticsearch Gateway."""
Expand All @@ -302,6 +295,25 @@ async def test_ping(self, gateway: ElasticsearchGateway, es_aioclient_mock: Aioh

assert await gateway.ping() is True


async def test_ping_auth_required(
self, gateway: ElasticsearchGateway, es_aioclient_mock: AiohttpClientMocker
) -> None:
"""Test the ping method."""
temp = gateway.ping
gateway.ping = AsyncMock(return_value=True)
gateway.has_security = AsyncMock(return_value=True)
gateway._has_required_privileges = AsyncMock(return_value=True)

es_aioclient_mock.get(
f"{TEST_CONFIG_ENTRY_DATA_URL}/",
status=403,
)

gateway.ping = temp

assert await gateway.ping() is False

Check failure on line 315 in tests/test_es_gateway.py

View workflow job for this annotation

GitHub Actions / Run tests

Test_Public_Functions.test_ping_auth_required[es8] custom_components.elasticsearch.errors.InsufficientPrivileges: Error retrieving cluster info from Elasticsearch. Authorization error connecting to Elasticsearch

async def test_ping_fail(
self, gateway: ElasticsearchGateway, es_aioclient_mock: AiohttpClientMocker
) -> None:
Expand Down Expand Up @@ -360,10 +372,10 @@ async def test_get_index_template(

assert await initialized_gateway.get_index_template("test_template") == {}

async def test_get_index_template_fail(
async def test_get_index_template_missing(
self, initialized_gateway: ElasticsearchGateway, es_aioclient_mock: AiohttpClientMocker
) -> None:
"""Test the get_index_template method."""
"""Test the get_index_template method when the template is missing."""

es_aioclient_mock.get(
f"{TEST_CONFIG_ENTRY_DATA_URL}/_index_template/test_template",
Expand All @@ -373,22 +385,6 @@ async def test_get_index_template_fail(

assert await initialized_gateway.get_index_template("test_template", ignore=[404]) == {}

async def test_get_index_template_exception(
self,
initialized_gateway: ElasticsearchGateway,
es_aioclient_mock: AiohttpClientMocker,
cannot_connect_error,
) -> None:
"""Test the get_index_template method."""
es_aioclient_mock.get(
f"{TEST_CONFIG_ENTRY_DATA_URL}/_index_template/test_template",
exc=cannot_connect_error,
)

# type of cannot_connect_error
with pytest.raises(CannotConnect):
await initialized_gateway.get_index_template("test_template")

async def test_put_index_template(
self, initialized_gateway: ElasticsearchGateway, es_aioclient_mock: AiohttpClientMocker
) -> None:
Expand Down Expand Up @@ -486,8 +482,30 @@ async def test_check_connection_reestablished(self, gateway: ElasticsearchGatewa
class Test_Exception_Conversion:
"""Test the conversion of Elasticsearch exceptions to custom exceptions."""

@pytest.fixture
def gateway_settings(self) -> Gateway8Settings:
"""Return a Gateway8Settings instance."""
return Gateway8Settings(
url=const.TEST_CONFIG_ENTRY_DATA_URL,
username="username",
password="password",
verify_certs=True,
ca_certs=None,
request_timeout=30,
minimum_version=None,
)

@pytest.fixture
async def gateway(self, gateway_settings, mock_elasticsearch_client):
"""Return a mock Elasticsearch client."""
gateway = Elasticsearch8Gateway(gateway_settings=gateway_settings)

yield gateway

await gateway.stop()

@pytest.mark.parametrize(
("status_code", "expected_response"),
("status_code", "expected_exception"),
[
(404, CannotConnect),
(401, AuthenticationRequired),
Expand All @@ -496,96 +514,71 @@ class Test_Exception_Conversion:
(400, CannotConnect),
(502, CannotConnect),
(503, CannotConnect),
(200, None),
],
ids=[
"404 to CannotConnect",
"401 to AuthenticationRequired",
"403 to InsufficientPrivileges",
"500 to ServerError",
"400 to ClientError",
"500 to CannotConnect",
"400 to CannotConnect",
"502 to CannotConnect",
"503 to CannotConnect",
"200 to None",
],
)
async def test_simple_return_codes(
async def test_http_error_codes(
self,
gateway: ElasticsearchGateway,
es_aioclient_mock,
es_mock_builder,
status_code: int,
expected_response: Any,
expected_exception: Any,
) -> None:
"""Test the error converter."""
temp = gateway.info
gateway.info = AsyncMock(return_value=CLUSTER_INFO_8DOT14_RESPONSE_BODY)
gateway.has_security = AsyncMock(return_value=True)
gateway._has_required_privileges = AsyncMock(return_value=True)
await gateway.async_init()
gateway.info = temp
es_mock_builder.with_server_error(status=status_code)

es_aioclient_mock.get(
f"{TEST_CONFIG_ENTRY_DATA_URL}",
status=status_code,
json=CLUSTER_INFO_8DOT14_RESPONSE_BODY,
headers={"x-elastic-product": "Elasticsearch"},
)

if expected_response is None:
assert await gateway.info() == CLUSTER_INFO_8DOT14_RESPONSE_BODY
else:
with pytest.raises(expected_response):
await gateway.info()
with pytest.raises(expected_exception):
await gateway.info()

@pytest.mark.parametrize(
("aiohttp_exception", "expected_exception"),
[
(client_exceptions.ServerConnectionError(), CannotConnect),
# child exceptions of ServerConnectionError
(
client_exceptions.ServerFingerprintMismatch(expected=b"", got=b"", host="host", port=0),
client_exceptions.ServerFingerprintMismatch(
expected=b"expected", got=b"actual", host="host", port=9200
),
CannotConnect,
),
(client_exceptions.ServerDisconnectedError(), CannotConnect),
(client_exceptions.ServerTimeoutError(), CannotConnect),
# (client_exceptions.ClientError(), ClientError),
(client_exceptions.ClientError(), CannotConnect),
# child exceptions of ClientError
# (client_exceptions.ClientResponseError(), ClientError),
(
client_exceptions.ClientResponseError(request_info=MagicMock(), history=MagicMock()),
CannotConnect,
),
(client_exceptions.ClientPayloadError(), CannotConnect),
(client_exceptions.ClientConnectionError(), CannotConnect),
# child exceptions of ClientConnectionError
# (
# client_exceptions.ClientSSLError(connection_key=MagicMock(), os_error=Exception("AHHHHH")),
# SSLError,
# ),
# child exceptions of ClientSSLError
# (client_exceptions.ClientConnectorSSLError(), SSLError),
(self_signed_tls_error(), UntrustedCertificate),
],
ids=[
"ServerConnectionError to CannotConnect",
"ServerFingerprintMismatch to CannotConnect",
"ServerDisconnectedError to CannotConnect",
"ServerTimeoutError to CannotConnect",
# "ClientError to ClientError",
# "ClientResponseError to ClientError",
"ClientError to CannotConnect",
"ClientResponseError to CannotConnect",
"ClientPayloadError to CannotConnect",
"ClientConnectionError to CannotConnect",
# "ClientSSLError to SSLConnectionError",
# "ClientConnectorSSLError to CannotConnect",
"SSLCertVerificationError to UntrustedCertificate",
],
)
async def test_simple_web_exceptions(
self, aiohttp_exception, expected_exception, es_aioclient_mock, gateway
async def test_aiohttp_web_exceptions(
self, aiohttp_exception, expected_exception, gateway, es_mock_builder
) -> None:
"""Test the error converter."""
temp = gateway.info
gateway.info = AsyncMock(return_value=CLUSTER_INFO_8DOT14_RESPONSE_BODY)
gateway.has_security = AsyncMock(return_value=True)
gateway._has_required_privileges = AsyncMock(return_value=True)
await gateway.async_init()
gateway.info = temp

es_aioclient_mock.get(f"{TEST_CONFIG_ENTRY_DATA_URL}", exc=aiohttp_exception)
es_mock_builder.with_server_error(exc=aiohttp_exception)

with pytest.raises(expected_exception):
await gateway.info()

0 comments on commit 92ed07e

Please sign in to comment.