diff --git a/.trivyignore b/.trivyignore index 28ef13e..083629b 100644 --- a/.trivyignore +++ b/.trivyignore @@ -6,4 +6,5 @@ CVE-2024-34156 CVE-2024-24790 CVE-2024-24788 CVE-2024-45337 +# Pebble CVE-2024-45338 diff --git a/src-docs/charm.py.md b/src-docs/charm.py.md index 57e72e6..620b86c 100644 --- a/src-docs/charm.py.md +++ b/src-docs/charm.py.md @@ -12,6 +12,7 @@ Maubot charm service. - **MAUBOT_CONFIGURATION_PATH** - **MAUBOT_NAME** - **NGINX_NAME** +- **POSTGRESQL_RELATION_NAME** --- @@ -28,7 +29,7 @@ Exception raised when an event fails. ## class `MaubotCharm` Maubot charm. - + ### function `__init__` @@ -89,7 +90,7 @@ Unit that this execution is responsible for. ## class `MissingRelationDataError` Custom exception to be raised in case of malformed/missing relation data. - + ### function `__init__` diff --git a/src/charm.py b/src/charm.py index 9a96887..96088d6 100755 --- a/src/charm.py +++ b/src/charm.py @@ -46,6 +46,7 @@ MAUBOT_CONFIGURATION_PATH = "/data/config.yaml" MAUBOT_NAME = "maubot" NGINX_NAME = "nginx" +POSTGRESQL_RELATION_NAME = "postgresql" class MissingRelationDataError(Exception): @@ -91,7 +92,7 @@ def __init__(self, *args: Any): jobs=self._probes_scraping_job, ) self.postgresql = DatabaseRequires( - self, relation_name="postgresql", database_name=self.app.name + self, relation_name=POSTGRESQL_RELATION_NAME, database_name=self.app.name ) self.matrix_auth = MatrixAuthRequires(self) self.framework.observe(self.on.maubot_pebble_ready, self._on_maubot_pebble_ready) @@ -104,6 +105,10 @@ def __init__(self, *args: Any): # Integrations events handlers self.framework.observe(self.postgresql.on.database_created, self._on_database_created) self.framework.observe(self.postgresql.on.endpoints_changed, self._on_endpoints_changed) + self.framework.observe( + self.on[POSTGRESQL_RELATION_NAME].relation_departed, + self._on_postgresql_relation_departed, + ) self.framework.observe(self.ingress.on.ready, self._on_ingress_ready) self.framework.observe(self.ingress.on.revoked, self._on_ingress_revoked) self.framework.observe( @@ -188,6 +193,10 @@ def _on_endpoints_changed(self, _: DatabaseEndpointsChangedEvent) -> None: """Handle endpoints changed event.""" self._reconcile() + def _on_postgresql_relation_departed(self, _: ops.RelationDepartedEvent) -> None: + """Handle postgresql relation departed event.""" + self._reconcile() + def _on_ingress_ready(self, _: IngressPerAppReadyEvent) -> None: """Handle ingress ready event.""" self._reconcile() diff --git a/tests/unit/test_charm_scenario.py b/tests/unit/test_charm_scenario.py new file mode 100644 index 0000000..ed28479 --- /dev/null +++ b/tests/unit/test_charm_scenario.py @@ -0,0 +1,125 @@ +# Copyright 2025 Canonical Ltd. +# See LICENSE file for licensing details. + +# Copyright 2024 Canonical Ltd. +# See LICENSE file for licensing details. + +"""Unit tests for the Maubot module using Scenario.""" + +import textwrap +from pathlib import Path + +import ops +import pytest +import scenario +from scenario.context import _Event # needed for custom events for now + +from charm import MaubotCharm + + +@pytest.fixture(scope="function", name="base_state") +def base_state_fixture(tmp_path: Path): + """State with container and config file set.""" + config_file_path = tmp_path / "config.yaml" + config_file_path.write_text( + textwrap.dedent( + """ + databases: null + server: + public_url: maubot.local + """ + ), + encoding="utf-8", + ) + yield { + "leader": True, + "containers": { + scenario.Container( + name="maubot", + can_connect=True, + execs={ + scenario.Exec( + command_prefix=["cp"], + return_code=0, + ), + scenario.Exec( + command_prefix=["mkdir"], + return_code=0, + ), + }, + mounts={ + "data": scenario.Mount(location="/data/config.yaml", source=config_file_path) + }, + ) + }, + } + + +def test_config_changed_no_postgresql(base_state: dict): + """ + arrange: prepare maubot container. + act: run config_changed. + assert: status is blocked because there is no postgresql integration. + """ + state = ops.testing.State(**base_state) + context = ops.testing.Context( + charm_type=MaubotCharm, + ) + out = context.run(context.on.config_changed(), state) + assert out.unit_status == ops.testing.BlockedStatus("postgresql integration is required") + + +def test_config_changed_with_postgresql(base_state: dict): + """ + arrange: prepare maubot container. + act: run config_changed. + assert: status is blocked because there is no postgresql integration. + """ + endpoints = "1.2.3.4:5432" + username = "user" + password = "pass" # nosec + database = "maubot" + postgresql_relation = scenario.Relation( + endpoint="postgresql", + interface="postgresql_client", + remote_app_name="postgresql", + remote_app_data={ + "endpoints": endpoints, + "username": username, + "password": password, + "database": database, + }, + ) + base_state["relations"] = [postgresql_relation] + state = ops.testing.State(**base_state) + context = ops.testing.Context( + charm_type=MaubotCharm, + ) + out = context.run(context.on.config_changed(), state) + assert out.unit_status == ops.testing.ActiveStatus() + container_root_fs = list(base_state["containers"])[0].get_filesystem(context) + config_file = container_root_fs / "data" / "config.yaml" + assert f"postgresql://{username}:{password}@{endpoints}/{database}" in config_file.read_text() + + +def test_postgresql_relation_departed(base_state: dict): + """ + arrange: prepare maubot container. + act: run config_changed. + assert: status is blocked because there is no postgresql integration. + """ + postgresql_relation = scenario.Relation( + endpoint="postgresql", + interface="postgresql_client", + remote_app_name="postgresql", + ) + base_state["relations"] = [postgresql_relation] + state = ops.testing.State(**base_state) + context = ops.testing.Context( + charm_type=MaubotCharm, + ) + postgresql_relation_departed_event = _Event( + "postgresql_relation_departed", relation=postgresql_relation + ) + out = context.run(postgresql_relation_departed_event, state) + assert out.unit_status == ops.testing.BlockedStatus("postgresql integration is required") diff --git a/tox.ini b/tox.ini index 6b62022..82ef80e 100644 --- a/tox.ini +++ b/tox.ini @@ -44,6 +44,7 @@ deps = flake8-test-docs>=1.0 isort mypy + ops[testing] pep8-naming pydocstyle>=2.10 pylint @@ -77,6 +78,7 @@ deps = pytest pytest_asyncio pytest_operator + ops[testing] -r{toxinidir}/requirements.txt commands = coverage run --source={[vars]src_path} \