diff --git a/tests/test_coordinated_workers/test_coordinator.py b/tests/test_coordinated_workers/test_coordinator.py index b241c3c..a9e4e27 100644 --- a/tests/test_coordinated_workers/test_coordinator.py +++ b/tests/test_coordinated_workers/test_coordinator.py @@ -1,16 +1,19 @@ import dataclasses import json +from unittest.mock import patch import ops import pytest -from ops import testing +from ops import RelationChangedEvent, testing +from cosl.coordinated_workers.interface import ClusterRemovedEvent, DataValidationError from src.cosl.coordinated_workers.coordinator import ( ClusterRolesConfig, Coordinator, S3NotFoundError, ) from src.cosl.interfaces.cluster import ClusterRequirerAppData +from tests.test_coordinated_workers.test_worker import MyCharm @pytest.fixture @@ -348,3 +351,52 @@ def test_invalid_databag_content(coordinator_charm: ops.CharmBase, event): cluster.gather_addresses_by_role() manager.run() assert cluster.model.unit.status == ops.BlockedStatus("[consistency] Cluster inconsistent.") + + +@pytest.mark.parametrize("app", (True, False)) +def test_invalid_app_or_unit_databag( + coordinator_charm: ops.CharmBase, coordinator_state, app: bool +): + # Test that when a relation changes and either the app or unit data is invalid + # the worker emits a ClusterRemovedEvent + + # WHEN you define a properly configured charm + ctx = testing.Context( + MyCharm, + meta={ + "name": "foo", + "requires": {"cluster": {"interface": "cluster"}}, + "containers": {"foo": {"type": "oci-image"}}, + }, + config={"options": {"role-all": {"type": "boolean", "default": True}}}, + ) + + # IF the relation data is invalid (forced by the patched Exception) + object_to_patch = ( + "cosl.coordinated_workers.interface.ClusterProviderAppData.load" + if app + else "cosl.coordinated_workers.interface.ClusterRequirerUnitData.load" + ) + + with patch(object_to_patch, side_effect=DataValidationError("Mock error")): + # AND the relation changes + relation = testing.Relation("cluster") + + ctx.run( + ctx.on.relation_changed(relation), + testing.State( + containers={testing.Container("foo", can_connect=True)}, relations={relation} + ), + ) + + # NOTE: this difference should not exist, and the ClusterRemovedEvent should always + # be emitted in case of corrupted data + + # THEN the charm emits a ClusterRemovedEvent + if app: + assert len(ctx.emitted_events) == 2 + assert isinstance(ctx.emitted_events[0], RelationChangedEvent) + assert isinstance(ctx.emitted_events[1], ClusterRemovedEvent) + else: + assert len(ctx.emitted_events) == 1 + assert isinstance(ctx.emitted_events[0], RelationChangedEvent) diff --git a/tests/test_coordinated_workers/test_worker.py b/tests/test_coordinated_workers/test_worker.py index 9bc31ea..5b04661 100644 --- a/tests/test_coordinated_workers/test_worker.py +++ b/tests/test_coordinated_workers/test_worker.py @@ -7,6 +7,7 @@ import pytest import yaml from ops import testing +from scenario.errors import UncaughtCharmError from cosl.coordinated_workers.worker import ( CERT_FILE, @@ -774,3 +775,131 @@ def test_worker_stop_all_services_if_not_ready(tmp_path): assert all(svc is ops.pebble.ServiceStatus.INACTIVE for svc in service_statuses), [ stat.value for stat in service_statuses ] + + +@patch("socket.getfqdn") +def test_invalid_url(mock_socket_fqdn): + # Test that when socket returns an invalid url as a Fully Qualified Domain Name, + # ClusterRequirer.publish_unit_address raises a ValueError exception + + # GIVEN a properly configured charm + ctx = testing.Context( + MyCharm, + meta={ + "name": "foo", + "requires": {"cluster": {"interface": "cluster"}}, + "containers": {"foo": {"type": "oci-image"}}, + }, + config={"options": {"role-all": {"type": "boolean", "default": True}}}, + ) + + # AND ClusterRequirer is passed an invalid url as FQDN + mock_socket_fqdn.return_value = "http://www.invalid-]url.com" + + # WHEN the charm executes any event + # THEN the charm raises an error with the appropriate cause + with pytest.raises(UncaughtCharmError) as exc: + ctx.run(ctx.on.update_status(), testing.State(containers={testing.Container("foo")})) + assert isinstance(exc.value.__cause__, ValueError) + + +@pytest.mark.parametrize( + "remote_databag, expected", + ( + ( + { + "charm_tracing_receivers": json.dumps({"url": "test-url.com"}), + "worker_config": json.dumps("test"), + }, + {"url": "test-url.com"}, + ), + ( + {"charm_tracing_receivers": json.dumps(None), "worker_config": json.dumps("test")}, + {}, + ), + ), +) +def test_get_charm_tracing_receivers(remote_databag, expected): + # Test that when a relation changes the correct charm_tracing_receivers + # are returned by the ClusterRequirer + + # GIVEN a charm with a relation + ctx = testing.Context( + MyCharm, + meta={ + "name": "foo", + "requires": {"cluster": {"interface": "cluster"}}, + "containers": {"foo": {"type": "oci-image"}}, + }, + config={"options": {"role-all": {"type": "boolean", "default": True}}}, + ) + container = testing.Container( + "foo", + execs={testing.Exec(("update-ca-certificates", "--fresh"))}, + can_connect=True, + ) + + relation = testing.Relation( + "cluster", + remote_app_data=remote_databag, + ) + + # WHEN the relation changes + with ctx( + ctx.on.relation_changed(relation), + testing.State(containers={container}, relations={relation}), + ) as mgr: + charm = mgr.charm + # THEN the charm tracing receivers are picked up correctly + assert charm.worker.cluster.get_charm_tracing_receivers() == expected + + +@pytest.mark.parametrize( + "remote_databag, expected", + ( + ( + { + "workload_tracing_receivers": json.dumps({"url": "test-url.com"}), + "worker_config": json.dumps("test"), + }, + {"url": "test-url.com"}, + ), + ( + {"workload_tracing_receivers": json.dumps(None), "worker_config": json.dumps("test")}, + {}, + ), + ), +) +def test_get_workload_tracing_receivers(remote_databag, expected): + # Test that when a relation changes the correct workload_tracing_receivers + # are returned by the ClusterRequirer + + # GIVEN a charm with a relation + ctx = testing.Context( + MyCharm, + meta={ + "name": "foo", + "requires": {"cluster": {"interface": "cluster"}}, + "containers": {"foo": {"type": "oci-image"}}, + }, + config={"options": {"role-all": {"type": "boolean", "default": True}}}, + ) + container = testing.Container( + "foo", + execs={testing.Exec(("update-ca-certificates", "--fresh"))}, + can_connect=True, + ) + + relation = testing.Relation( + "cluster", + remote_app_data=remote_databag, + ) + + # WHEN the relation changes + with ctx( + ctx.on.relation_changed(relation), + testing.State(containers={container}, relations={relation}), + ) as mgr: + charm = mgr.charm + # THEN the charm tracing receivers are picked up correctly + assert charm.worker.cluster.get_workload_tracing_receivers() == expected