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} \