diff --git a/charmcraft.yaml b/charmcraft.yaml
index 4d7d4fe..23bfb83 100644
--- a/charmcraft.yaml
+++ b/charmcraft.yaml
@@ -45,6 +45,9 @@ parts:
platforms:
amd64:
requires:
+ matrix-auth:
+ interface: matrix_auth
+ limit: 1
postgresql:
interface: postgresql_client
limit: 1
diff --git a/lib/charms/synapse/v0/matrix_auth.py b/lib/charms/synapse/v0/matrix_auth.py
new file mode 100644
index 0000000..2342b99
--- /dev/null
+++ b/lib/charms/synapse/v0/matrix_auth.py
@@ -0,0 +1,476 @@
+# Copyright 2024 Canonical Ltd.
+# Licensed under the Apache2.0. See LICENSE file in charm source for details.
+
+"""Library to manage the plugin integrations with the Synapse charm.
+
+This library contains the Requires and Provides classes for handling the integration
+between an application and a charm providing the `matrix_plugin` integration.
+
+### Requirer Charm
+
+```python
+
+from charms.synapse.v0.matrix_auth import MatrixAuthRequires
+
+class MatrixAuthRequirerCharm(ops.CharmBase):
+ def __init__(self, *args):
+ super().__init__(*args)
+ self.plugin_auth = MatrixAuthRequires(self)
+ self.framework.observe(self.matrix_auth.on.matrix_auth_request_processed, self._handler)
+ ...
+
+ def _handler(self, events: MatrixAuthRequestProcessed) -> None:
+ ...
+
+```
+
+As shown above, the library provides a custom event to handle the scenario in
+which a matrix authentication (homeserver and shared secret) has been added or updated.
+
+The MatrixAuthRequires provides an `update_relation_data` method to update the relation data by
+passing a `MatrixAuthRequirerData` data object, requesting a new authentication.
+
+### Provider Charm
+
+Following the previous example, this is an example of the provider charm.
+
+```python
+from charms.synapse.v0.matrix_auth import MatrixAuthProvides
+
+class MatrixAuthProviderCharm(ops.CharmBase):
+ def __init__(self, *args):
+ super().__init__(*args)
+ self.plugin_auth = MatrixAuthProvides(self)
+ ...
+
+```
+The MatrixAuthProvides object wraps the list of relations into a `relations` property
+and provides an `update_relation_data` method to update the relation data by passing
+a `MatrixAuthRelationData` data object.
+
+```python
+class MatrixAuthProviderCharm(ops.CharmBase):
+ ...
+
+ def _on_config_changed(self, _) -> None:
+ for relation in self.model.relations[self.plugin_auth.relation_name]:
+ self.plugin_auth.update_relation_data(relation, self._get_matrix_auth_data())
+
+```
+"""
+
+# The unique Charmhub library identifier, never change it
+LIBID = "ff6788c89b204448b3b62ba6f93e2768"
+
+# Increment this major API version when introducing breaking changes
+LIBAPI = 0
+
+# Increment this PATCH version before using `charmcraft publish-lib` or reset
+# to 0 if you are raising the major API version
+LIBPATCH = 3
+
+# pylint: disable=wrong-import-position
+import json
+import logging
+from typing import Dict, List, Optional, Tuple, cast
+
+import ops
+from pydantic import BaseModel, Field, SecretStr
+
+logger = logging.getLogger(__name__)
+
+#### Constants ####
+APP_REGISTRATION_LABEL = "app-registration"
+APP_REGISTRATION_CONTENT_LABEL = "app-registration-content"
+DEFAULT_RELATION_NAME = "matrix-auth"
+SHARED_SECRET_LABEL = "shared-secret"
+SHARED_SECRET_CONTENT_LABEL = "shared-secret-content"
+
+
+#### Data models for Provider and Requirer ####
+class MatrixAuthProviderData(BaseModel):
+ """Represent the MatrixAuth provider data.
+
+ Attributes:
+ homeserver: the homeserver URL.
+ shared_secret: the Matrix shared secret.
+ shared_secret_id: the shared secret Juju secret ID.
+ """
+
+ homeserver: str
+ shared_secret: Optional[SecretStr] = Field(default=None, exclude=True)
+ shared_secret_id: Optional[SecretStr] = Field(default=None)
+
+ def set_shared_secret_id(self, model: ops.Model, relation: ops.Relation) -> None:
+ """Store the Matrix shared secret as a Juju secret.
+
+ Args:
+ model: the Juju model
+ relation: relation to grant access to the secrets to.
+ """
+ # password is always defined since pydantic guarantees it
+ password = cast(SecretStr, self.shared_secret)
+ # pylint doesn't like get_secret_value
+ secret_value = password.get_secret_value() # pylint: disable=no-member
+ try:
+ secret = model.get_secret(label=SHARED_SECRET_LABEL)
+ secret.set_content({SHARED_SECRET_CONTENT_LABEL: secret_value})
+ # secret.id is not None at this point
+ self.shared_secret_id = cast(str, secret.id)
+ except ops.SecretNotFoundError:
+ secret = relation.app.add_secret(
+ {SHARED_SECRET_CONTENT_LABEL: secret_value}, label=SHARED_SECRET_LABEL
+ )
+ secret.grant(relation)
+ self.shared_secret_id = cast(str, secret.id)
+
+ @classmethod
+ def get_shared_secret(
+ cls, model: ops.Model, shared_secret_id: Optional[str]
+ ) -> Optional[SecretStr]:
+ """Retrieve the shared secret corresponding to the shared_secret_id.
+
+ Args:
+ model: the Juju model.
+ shared_secret_id: the secret ID for the shared secret.
+
+ Returns:
+ the shared secret or None if not found.
+ """
+ if not shared_secret_id:
+ return None
+ try:
+ secret = model.get_secret(id=shared_secret_id)
+ password = secret.get_content().get(SHARED_SECRET_CONTENT_LABEL)
+ if not password:
+ return None
+ return SecretStr(password)
+ except ops.SecretNotFoundError:
+ return None
+
+ def to_relation_data(self, model: ops.Model, relation: ops.Relation) -> Dict[str, str]:
+ """Convert an instance of MatrixAuthProviderData to the relation representation.
+
+ Args:
+ model: the Juju model.
+ relation: relation to grant access to the secrets to.
+
+ Returns:
+ Dict containing the representation.
+ """
+ self.set_shared_secret_id(model, relation)
+ return self.model_dump(exclude_unset=True)
+
+ @classmethod
+ def from_relation(cls, model: ops.Model, relation: ops.Relation) -> "MatrixAuthProviderData":
+ """Initialize a new instance of the MatrixAuthProviderData class from the relation.
+
+ Args:
+ relation: the relation.
+
+ Returns:
+ A MatrixAuthProviderData instance.
+
+ Raises:
+ ValueError: if the value is not parseable.
+ """
+ app = cast(ops.Application, relation.app)
+ relation_data = relation.data[app]
+ shared_secret_id = (
+ (relation_data["shared_secret_id"])
+ if "shared_secret_id" in relation_data
+ else None
+ )
+ shared_secret = MatrixAuthProviderData.get_shared_secret(model, shared_secret_id)
+ homeserver = relation_data.get("homeserver")
+ if shared_secret is None or homeserver is None:
+ raise ValueError("Invalid relation data")
+ return MatrixAuthProviderData(
+ homeserver=homeserver,
+ shared_secret=shared_secret,
+ )
+
+
+class MatrixAuthRequirerData(BaseModel):
+ """Represent the MatrixAuth requirer data.
+
+ Attributes:
+ registration: a generated app registration file.
+ registration_id: the registration Juju secret ID.
+ """
+
+ registration: Optional[SecretStr] = Field(default=None, exclude=True)
+ registration_secret_id: Optional[SecretStr] = Field(default=None)
+
+ def set_registration_id(self, model: ops.Model, relation: ops.Relation) -> None:
+ """Store the app registration as a Juju secret.
+
+ Args:
+ model: the Juju model
+ relation: relation to grant access to the secrets to.
+ """
+ # password is always defined since pydantic guarantees it
+ password = cast(SecretStr, self.registration)
+ # pylint doesn't like get_secret_value
+ secret_value = password.get_secret_value() # pylint: disable=no-member
+ try:
+ secret = model.get_secret(label=APP_REGISTRATION_LABEL)
+ secret.set_content({APP_REGISTRATION_CONTENT_LABEL: secret_value})
+ # secret.id is not None at this point
+ self.registration_secret_id = cast(str, secret.id)
+ except ops.SecretNotFoundError:
+ secret = relation.app.add_secret(
+ {APP_REGISTRATION_CONTENT_LABEL: secret_value}, label=APP_REGISTRATION_LABEL
+ )
+ secret.grant(relation)
+ self.registration_secret_id = cast(str, secret.id)
+
+ @classmethod
+ def get_registration(
+ cls, model: ops.Model, registration_secret_id: Optional[str]
+ ) -> Optional[SecretStr]:
+ """Retrieve the registration corresponding to the registration_secret_id.
+
+ Args:
+ model: the Juju model.
+ registration_secret_id: the secret ID for the registration.
+
+ Returns:
+ the registration or None if not found.
+ """
+ if not registration_secret_id:
+ return None
+ try:
+ secret = model.get_secret(id=registration_secret_id)
+ password = secret.get_content().get(APP_REGISTRATION_CONTENT_LABEL)
+ if not password:
+ return None
+ return SecretStr(password)
+ except ops.SecretNotFoundError:
+ return None
+
+ def to_relation_data(self, model: ops.Model, relation: ops.Relation) -> Dict[str, str]:
+ """Convert an instance of MatrixAuthRequirerData to the relation representation.
+
+ Args:
+ model: the Juju model.
+ relation: relation to grant access to the secrets to.
+
+ Returns:
+ Dict containing the representation.
+ """
+ self.set_registration_id(model, relation)
+ dumped_model = self.model_dump(exclude_unset=True)
+ dumped_data = {
+ "registration_secret_id": dumped_model["registration_secret_id"],
+ }
+ return dumped_data
+
+ @classmethod
+ def from_relation(cls, model: ops.Model, relation: ops.Relation) -> "MatrixAuthRequirerData":
+ """Get a MatrixAuthRequirerData from the relation data.
+
+ Args:
+ model: the Juju model.
+ relation: the relation.
+
+ Returns:
+ the relation data and the processed entries for it.
+
+ Raises:
+ ValueError: if the value is not parseable.
+ """
+ app = cast(ops.Application, relation.app)
+ relation_data = relation.data[app]
+ registration_secret_id = relation_data.get("registration_secret_id")
+ registration = MatrixAuthRequirerData.get_registration(model, registration_secret_id)
+ return MatrixAuthRequirerData(
+ registration=registration,
+ )
+
+
+#### Events ####
+class MatrixAuthRequestProcessed(ops.RelationEvent):
+ """MatrixAuth event emitted when a new request is processed."""
+
+ def get_matrix_auth_provider_relation_data(self) -> MatrixAuthProviderData:
+ """Get a MatrixAuthProviderData for the relation data.
+
+ Returns:
+ the MatrixAuthProviderData for the relation data.
+ """
+ return MatrixAuthProviderData.from_relation(self.framework.model, self.relation)
+
+
+class MatrixAuthRequestReceived(ops.RelationEvent):
+ """MatrixAuth event emitted when a new request is made."""
+
+
+class MatrixAuthRequiresEvents(ops.CharmEvents):
+ """MatrixAuth requirer events.
+
+ This class defines the events that a MatrixAuth requirer can emit.
+
+ Attributes:
+ matrix_auth_request_processed: the MatrixAuthRequestProcessed.
+ """
+
+ matrix_auth_request_processed = ops.EventSource(MatrixAuthRequestProcessed)
+
+
+class MatrixAuthProvidesEvents(ops.CharmEvents):
+ """MatrixAuth provider events.
+
+ This class defines the events that a MatrixAuth provider can emit.
+
+ Attributes:
+ matrix_auth_request_received: the MatrixAuthRequestReceived.
+ """
+
+ matrix_auth_request_received = ops.EventSource(MatrixAuthRequestReceived)
+
+
+#### Provides and Requires ####
+class MatrixAuthProvides(ops.Object):
+ """Provider side of the MatrixAuth relation.
+
+ Attributes:
+ on: events the provider can emit.
+ """
+
+ on = MatrixAuthProvidesEvents()
+
+ def __init__(self, charm: ops.CharmBase, relation_name: str = DEFAULT_RELATION_NAME) -> None:
+ """Construct.
+
+ Args:
+ charm: the provider charm.
+ relation_name: the relation name.
+ """
+ super().__init__(charm, relation_name)
+ self.relation_name = relation_name
+ self.framework.observe(charm.on[relation_name].relation_changed, self._on_relation_changed)
+
+ def get_remote_relation_data(self) -> Optional[MatrixAuthRequirerData]:
+ """Retrieve the remote relation data.
+
+ Returns:
+ MatrixAuthRequirerData: the relation data.
+ """
+ relation = self.model.get_relation(self.relation_name)
+ return MatrixAuthRequirerData.from_relation(self.model, relation=relation) if relation else None
+
+ def _is_remote_relation_data_valid(self, relation: ops.Relation) -> bool:
+ """Validate the relation data.
+
+ Args:
+ relation: the relation to validate.
+
+ Returns:
+ true: if the relation data is valid.
+ """
+ try:
+ _ = MatrixAuthRequirerData.from_relation(self.model, relation=relation)
+ return True
+ except ValueError as ex:
+ logger.warning("Error validating the relation data %s", ex)
+ return False
+
+ def _on_relation_changed(self, event: ops.RelationChangedEvent) -> None:
+ """Event emitted when the relation has changed.
+
+ Args:
+ event: event triggering this handler.
+ """
+ assert event.relation.app
+ relation_data = event.relation.data[event.relation.app]
+ if relation_data and self._is_remote_relation_data_valid(event.relation):
+ self.on.matrix_auth_request_received.emit(
+ event.relation, app=event.app, unit=event.unit
+ )
+
+ def update_relation_data(
+ self, relation: ops.Relation, matrix_auth_provider_data: MatrixAuthProviderData
+ ) -> None:
+ """Update the relation data.
+
+ Args:
+ relation: the relation for which to update the data.
+ matrix_auth_provider_data: a MatrixAuthProviderData instance wrapping the data to be
+ updated.
+ """
+ relation_data = matrix_auth_provider_data.to_relation_data(self.model, relation)
+ relation.data[self.model.app].update(relation_data)
+
+
+class MatrixAuthRequires(ops.Object):
+ """Requirer side of the MatrixAuth requires relation.
+
+ Attributes:
+ on: events the provider can emit.
+ """
+
+ on = MatrixAuthRequiresEvents()
+
+ def __init__(self, charm: ops.CharmBase, relation_name: str = DEFAULT_RELATION_NAME) -> None:
+ """Construct.
+
+ Args:
+ charm: the provider charm.
+ relation_name: the relation name.
+ """
+ super().__init__(charm, relation_name)
+ self.relation_name = relation_name
+ self.framework.observe(charm.on[relation_name].relation_changed, self._on_relation_changed)
+
+ def get_remote_relation_data(self) -> Optional[MatrixAuthProviderData]:
+ """Retrieve the remote relation data.
+
+ Returns:
+ MatrixAuthProviderData: the relation data.
+ """
+ relation = self.model.get_relation(self.relation_name)
+ return MatrixAuthProviderData.from_relation(self.model, relation=relation) if relation else None
+
+ def _is_remote_relation_data_valid(self, relation: ops.Relation) -> bool:
+ """Validate the relation data.
+
+ Args:
+ relation: the relation to validate.
+
+ Returns:
+ true: if the relation data is valid.
+ """
+ try:
+ _ = MatrixAuthProviderData.from_relation(self.model, relation=relation)
+ return True
+ except ValueError as ex:
+ logger.warning("Error validating the relation data %s", ex)
+ return False
+
+ def _on_relation_changed(self, event: ops.RelationChangedEvent) -> None:
+ """Event emitted when the relation has changed.
+
+ Args:
+ event: event triggering this handler.
+ """
+ assert event.relation.app
+ relation_data = event.relation.data[event.relation.app]
+ if relation_data and self._is_remote_relation_data_valid(event.relation):
+ self.on.matrix_auth_request_processed.emit(
+ event.relation, app=event.app, unit=event.unit
+ )
+
+ def update_relation_data(
+ self,
+ relation: ops.Relation,
+ matrix_auth_requirer_data: MatrixAuthRequirerData,
+ ) -> None:
+ """Update the relation data.
+
+ Args:
+ relation: the relation for which to update the data.
+ matrix_auth_requirer_data: MatrixAuthRequirerData wrapping the data to be updated.
+ """
+ relation_data = matrix_auth_requirer_data.to_relation_data(self.model, relation)
+ relation.data[self.model.app].update(relation_data)
diff --git a/src-docs/charm.py.md b/src-docs/charm.py.md
index 7940ab6..e1fe295 100644
--- a/src-docs/charm.py.md
+++ b/src-docs/charm.py.md
@@ -26,7 +26,7 @@ Exception raised when an event fails.
## class `MaubotCharm`
Maubot charm.
-
+
### function `__init__`
@@ -84,8 +84,25 @@ Unit that this execution is responsible for.
---
-## class `MissingPostgreSQLRelationDataError`
-Custom exception to be raised in case of malformed/missing Postgresql relation data.
+## class `MissingRelationDataError`
+Custom exception to be raised in case of malformed/missing relation data.
+
+
+
+### function `__init__`
+
+```python
+__init__(message: str, relation_name: str) → None
+```
+
+Init custom exception.
+
+
+
+**Args:**
+
+ - `message`: Exception message.
+ - `relation_name`: Relation name that raised the exception.
diff --git a/src/charm.py b/src/charm.py
index 630196d..950aac9 100755
--- a/src/charm.py
+++ b/src/charm.py
@@ -18,6 +18,7 @@
DatabaseEndpointsChangedEvent,
DatabaseRequires,
)
+from charms.synapse.v0.matrix_auth import MatrixAuthRequestProcessed, MatrixAuthRequires
from charms.traefik_k8s.v2.ingress import (
IngressPerAppReadyEvent,
IngressPerAppRequirer,
@@ -32,8 +33,18 @@
NGINX_NAME = "nginx"
-class MissingPostgreSQLRelationDataError(Exception):
- """Custom exception to be raised in case of malformed/missing Postgresql relation data."""
+class MissingRelationDataError(Exception):
+ """Custom exception to be raised in case of malformed/missing relation data."""
+
+ def __init__(self, message: str, relation_name: str) -> None:
+ """Init custom exception.
+
+ Args:
+ message: Exception message.
+ relation_name: Relation name that raised the exception.
+ """
+ super().__init__(message)
+ self.relation_name = relation_name
class EventFailError(Exception):
@@ -55,6 +66,7 @@ def __init__(self, *args: Any):
self.postgresql = DatabaseRequires(
self, relation_name="postgresql", database_name=self.app.name
)
+ self.matrix_auth = MatrixAuthRequires(self)
self.framework.observe(self.on.maubot_pebble_ready, self._on_maubot_pebble_ready)
self.framework.observe(self.on.config_changed, self._on_config_changed)
# Actions events handlers
@@ -64,6 +76,10 @@ def __init__(self, *args: Any):
self.framework.observe(self.postgresql.on.endpoints_changed, self._on_endpoints_changed)
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(
+ self.matrix_auth.on.matrix_auth_request_processed,
+ self._on_matrix_auth_request_processed,
+ )
def _get_configuration(self) -> Dict[str, Any]:
"""Get Maubot configuration content.
@@ -87,19 +103,32 @@ def _configure_maubot(self) -> None:
process.wait()
config = self._get_configuration()
config["database"] = self._get_postgresql_credentials()
- self.container.push(MAUBOT_CONFIGURATION_PATH, yaml.safe_dump(config))
+ config["homeservers"] = self._get_matrix_credentials()
config["server"]["public_url"] = self.config.get("public-url")
- self.container.push("/data/config.yaml", yaml.safe_dump(config))
+ self.container.push(MAUBOT_CONFIGURATION_PATH, yaml.safe_dump(config))
def _reconcile(self) -> None:
- """Reconcile workload configuration."""
+ # Ignoring DC050 for now since RuntimeError is handled/re-raised only
+ # because a Harness issue.
+ """Reconcile workload configuration.""" # noqa: DCO050
self.unit.status = ops.MaintenanceStatus()
if not self.container.can_connect():
return
try:
self._configure_maubot()
- except MissingPostgreSQLRelationDataError:
- self.unit.status = ops.BlockedStatus("postgresql integration is required")
+ except MissingRelationDataError as e:
+ self.unit.status = ops.BlockedStatus(f"{e.relation_name} integration is required")
+ try:
+ self.container.stop(MAUBOT_NAME)
+ except RuntimeError as re:
+ if str(re) == '400 Bad Request: service "maubot" does not exist':
+ # Remove this once Harness is fixed
+ # See https://github.com/canonical/operator/issues/1310
+ pass
+ else:
+ raise re
+ except (ops.pebble.ChangeError, ops.pebble.APIError) as pe:
+ logging.exception("failed to stop maubot", exc_info=pe)
return
self.container.add_layer(MAUBOT_NAME, self._pebble_layer, combine=True)
self.container.restart(MAUBOT_NAME)
@@ -114,6 +143,7 @@ def _on_config_changed(self, _: ops.ConfigChangedEvent) -> None:
"""Handle changed configuration."""
self._reconcile()
+ # Integrations events handlers
def _on_ingress_ready(self, _: IngressPerAppReadyEvent) -> None:
"""Handle ingress ready event."""
self._reconcile()
@@ -166,6 +196,10 @@ def _on_endpoints_changed(self, _: DatabaseEndpointsChangedEvent) -> None:
"""Handle endpoints changed event."""
self._reconcile()
+ def _on_matrix_auth_request_processed(self, _: MatrixAuthRequestProcessed) -> None:
+ """Handle matrix auth request processed event."""
+ self._reconcile()
+
# Relation data handlers
def _get_postgresql_credentials(self) -> str:
"""Get postgresql credentials from the postgresql integration.
@@ -174,23 +208,51 @@ def _get_postgresql_credentials(self) -> str:
postgresql credentials.
Raises:
- MissingPostgreSQLRelationDataError: if relation is not found.
+ MissingRelationDataError: if relation is not found.
"""
relation = self.model.get_relation("postgresql")
if not relation or not relation.app:
- raise MissingPostgreSQLRelationDataError("No postgresql relation data")
+ raise MissingRelationDataError(
+ "No postgresql relation data", relation_name="postgresql"
+ )
endpoints = self.postgresql.fetch_relation_field(relation.id, "endpoints")
database = self.postgresql.fetch_relation_field(relation.id, "database")
username = self.postgresql.fetch_relation_field(relation.id, "username")
password = self.postgresql.fetch_relation_field(relation.id, "password")
if not endpoints:
- raise MissingPostgreSQLRelationDataError("Missing mandatory relation data")
+ raise MissingRelationDataError(
+ "Missing mandatory relation data", relation_name="postgresql"
+ )
primary_endpoint = endpoints.split(",")[0]
if not all((primary_endpoint, database, username, password)):
- raise MissingPostgreSQLRelationDataError("Missing mandatory relation data")
+ raise MissingRelationDataError(
+ "Missing mandatory relation data", relation_name="postgresql"
+ )
return f"postgresql://{username}:{password}@{primary_endpoint}/{database}"
+ def _get_matrix_credentials(self) -> dict[str, dict[str, str]]:
+ """Get Matrix credentials from the matrix-auth integration.
+
+ Returns:
+ matrix credentials.
+
+ Raises:
+ MissingRelationDataError: if relation is not found.
+ """
+ relation = self.model.get_relation("matrix-auth")
+ if not relation or not relation.app:
+ logging.warning("no matrix-auth relation found, getting default matrix credentials")
+ return {"matrix": {"url": "https://matrix-client.matrix.org", "secret": "null"}}
+ relation_data = self.matrix_auth.get_remote_relation_data()
+ homeserver = relation_data.homeserver
+ shared_secret_id = relation_data.shared_secret.get_secret_value()
+ if not all((homeserver, shared_secret_id)):
+ raise MissingRelationDataError(
+ "Missing mandatory relation data", relation_name="matrix-auth"
+ )
+ return {"synapse": {"url": homeserver, "secret": shared_secret_id}}
+
# Properties
@property
def _pebble_layer(self) -> pebble.LayerDict:
diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py
index 13ed1e4..fecc827 100644
--- a/tests/unit/test_charm.py
+++ b/tests/unit/test_charm.py
@@ -7,29 +7,10 @@
import ops
import ops.testing
+import pytest
+from charms.synapse.v0.matrix_auth import MatrixAuthProviderData
-
-def set_postgresql_integration(harness) -> None:
- """Set postgresql integration.
-
- Args:
- harness: harness instance.
- """
- relation_data = {
- "database": "maubot",
- "endpoints": "dbhost:5432",
- "password": "somepasswd", # nosec
- "username": "someuser",
- }
- db_relation_id = harness.add_relation( # pylint: disable=attribute-defined-outside-init
- "postgresql", "postgresql"
- )
- harness.add_relation_unit(db_relation_id, "postgresql/0")
- harness.update_relation_data(
- db_relation_id,
- "postgresql",
- relation_data,
- )
+from charm import MissingRelationDataError
def test_maubot_pebble_ready_postgresql_required(harness):
@@ -55,7 +36,9 @@ def test_maubot_pebble_ready(harness):
the service is running and the charm is active.
"""
harness.begin()
+ harness.set_can_connect("maubot", True)
set_postgresql_integration(harness)
+
expected_plan = {
"services": {
"maubot": {
@@ -91,6 +74,8 @@ def test_database_created(harness):
assert: postgresql credentials are set as expected.
"""
harness.begin_with_initial_hooks()
+ with pytest.raises(MissingRelationDataError):
+ harness.charm._get_postgresql_credentials()
set_postgresql_integration(harness)
@@ -134,7 +119,7 @@ def test_create_admin_action_failed(harness):
assert e.message == message
-def test_public_url_config_changed(harness):
+def test_public_url_config_changed(harness, monkeypatch):
"""
arrange: initialize harness and set postgresql integration.
act: change public-url config.
@@ -142,9 +127,71 @@ def test_public_url_config_changed(harness):
"""
harness.begin_with_initial_hooks()
set_postgresql_integration(harness)
+ set_matrix_auth_integration(harness, monkeypatch)
harness.update_config({"public-url": "https://example1.com"})
service = harness.model.unit.get_container("maubot").get_service("maubot")
assert service.is_running()
assert harness.model.unit.status == ops.ActiveStatus()
+
+
+def test_matrix_credentials_registered(harness, monkeypatch):
+ """
+ arrange: initialize harness and verify that the credentials are set with default values.
+ act: set matrix-auth integration.
+ assert: matrix credentials are set as expected.
+ """
+ harness.begin_with_initial_hooks()
+ assert harness.charm._get_matrix_credentials() == {
+ "matrix": {"secret": "null", "url": "https://matrix-client.matrix.org"}
+ }
+
+ set_matrix_auth_integration(harness, monkeypatch)
+
+ assert harness.charm._get_matrix_credentials() == {
+ "synapse": {"secret": "test-shared-secret", "url": "https://example.com"}
+ }
+
+
+def set_matrix_auth_integration(harness, monkeypatch) -> None:
+ """Set matrix-auth integration.
+
+ Args:
+ harness: harness instance.
+ monkeypatch: monkeypatch instance.
+ """
+ monkeypatch.setattr(
+ MatrixAuthProviderData, "get_shared_secret", lambda *args: "test-shared-secret"
+ )
+ relation_data = {"homeserver": "https://example.com", "shared_secret_id": "test-secret-id"}
+ matrix_relation_id = harness.add_relation("matrix-auth", "synapse", app_data=relation_data)
+ harness.add_relation_unit(matrix_relation_id, "synapse/0")
+ harness.update_relation_data(
+ matrix_relation_id,
+ "synapse",
+ relation_data,
+ )
+
+
+def set_postgresql_integration(harness) -> None:
+ """Set postgresql integration.
+
+ Args:
+ harness: harness instance.
+ """
+ relation_data = {
+ "database": "maubot",
+ "endpoints": "dbhost:5432",
+ "password": "somepasswd", # nosec
+ "username": "someuser",
+ }
+ db_relation_id = harness.add_relation( # pylint: disable=attribute-defined-outside-init
+ "postgresql", "postgresql"
+ )
+ harness.add_relation_unit(db_relation_id, "postgresql/0")
+ harness.update_relation_data(
+ db_relation_id,
+ "postgresql",
+ relation_data,
+ )