diff --git a/tests/interface/conftest.py b/tests/interface/conftest.py index 5df2abb2..c1b688e6 100644 --- a/tests/interface/conftest.py +++ b/tests/interface/conftest.py @@ -5,7 +5,7 @@ import pytest from interface_tester import InterfaceTester from ops.pebble import Layer -from scenario.state import Container, ExecOutput, State +from scenario.state import Container, Exec, State from charm import TraefikIngressCharm @@ -30,20 +30,22 @@ def interface_tester(interface_tester: InterfaceTester): # since we're passing a config, we have to provide all defaulted values "routing_mode": "path", }, - containers=[ + containers={ # unless the traefik service reports active, the # charm won't publish the ingress url. Container( name="traefik", can_connect=True, - exec_mock={ - ( - "find", - "/opt/traefik/juju", - "-name", - "*.yaml", - "-delete", - ): ExecOutput() + execs={ + Exec( + ( + "find", + "/opt/traefik/juju", + "-name", + "*.yaml", + "-delete", + ) + ) }, layers={ "foo": Layer( @@ -61,8 +63,8 @@ def interface_tester(interface_tester: InterfaceTester): } ) }, - ) - ], + ), + }, ), ) yield interface_tester diff --git a/tests/scenario/_utils.py b/tests/scenario/_utils.py index 62521683..d1302035 100644 --- a/tests/scenario/_utils.py +++ b/tests/scenario/_utils.py @@ -126,6 +126,6 @@ def create_ingress_relation( # No `next_relation_id()` nor `get_next_id()` in Relation. if rel_id is not None: - args["relation_id"] = rel_id + args["id"] = rel_id return Relation(**args) diff --git a/tests/scenario/conftest.py b/tests/scenario/conftest.py index 1bc7fce5..b8b48190 100644 --- a/tests/scenario/conftest.py +++ b/tests/scenario/conftest.py @@ -2,7 +2,7 @@ import pytest from ops import pebble -from scenario import Container, Context, ExecOutput, Model, Mount +from scenario import Container, Context, Exec, Model, Mount from charm import TraefikIngressCharm @@ -47,17 +47,17 @@ def traefik_container(tmp_path): } ) - opt = Mount("/opt/", tmp_path) + opt = Mount(location="/opt/", source=tmp_path) return Container( name="traefik", can_connect=True, layers={"traefik": layer}, - exec_mock={ - ("update-ca-certificates", "--fresh"): ExecOutput(), - ("find", "/opt/traefik/juju", "-name", "*.yaml", "-delete"): ExecOutput(), - ("/usr/bin/traefik", "version"): ExecOutput(stdout="42.42"), + execs={ + Exec(command_prefix=("update-ca-certificates", "--fresh")), + Exec(command_prefix=("find", "/opt/traefik/juju", "-name", "*.yaml", "-delete")), + Exec(command_prefix=("/usr/bin/traefik", "version"), stdout="42.42"), }, - service_status={"traefik": pebble.ServiceStatus.ACTIVE}, + service_statuses={"traefik": pebble.ServiceStatus.ACTIVE}, mounts={"opt": opt}, ) diff --git a/tests/scenario/test_config_mgm.py b/tests/scenario/test_config_mgm.py index d89044e0..e7fe902d 100644 --- a/tests/scenario/test_config_mgm.py +++ b/tests/scenario/test_config_mgm.py @@ -1,3 +1,5 @@ +from dataclasses import replace + from scenario import Relation, State @@ -6,7 +8,7 @@ def ipu(): endpoint="ingress-per-unit", interface="ingress_per_unit", remote_app_name="remote", - relation_id=0, + id=0, remote_units_data={ 0: { "port": "9999", @@ -21,13 +23,14 @@ def ipu(): def test_dynamic_config_create(traefik_container, traefik_ctx, tmp_path): rel = ipu() traefik_ctx.run( - rel.created_event, State(relations=[rel], containers=[traefik_container], leader=True) + traefik_ctx.on.relation_created(rel), + State(relations=[rel], containers=[traefik_container], leader=True), ) dynamic_config_dir = tmp_path / "traefik" / "juju" assert dynamic_config_dir.exists() files = list(dynamic_config_dir.iterdir()) assert len(files) == 1 - assert files[0].name == f"juju_ingress_ingress-per-unit_{rel.relation_id}_remote.yaml" + assert files[0].name == f"juju_ingress_ingress-per-unit_{rel.id}_remote.yaml" def test_dynamic_config_remove_on_broken(traefik_container, traefik_ctx, tmp_path): @@ -35,12 +38,13 @@ def test_dynamic_config_remove_on_broken(traefik_container, traefik_ctx, tmp_pat rel = ipu() dynamic_config_dir.mkdir(parents=True) ingress_config_fname = ( - dynamic_config_dir / f"juju_ingress_ingress-per-unit_{rel.relation_id}_remote.yaml" + dynamic_config_dir / f"juju_ingress_ingress-per-unit_{rel.id}_remote.yaml" ) ingress_config_fname.touch() traefik_ctx.run( - rel.broken_event, State(relations=[rel], containers=[traefik_container], leader=True) + traefik_ctx.on.relation_broken(rel), + State(relations=[rel], containers=[traefik_container], leader=True), ) assert dynamic_config_dir.exists() @@ -50,16 +54,16 @@ def test_dynamic_config_remove_on_broken(traefik_container, traefik_ctx, tmp_pat def test_dynamic_config_remove_on_departed(traefik_container, traefik_ctx, tmp_path): dynamic_config_dir = tmp_path / "traefik" / "juju" - rel = ipu().replace(remote_units_data={}) + rel = replace(ipu(), remote_units_data={}) dynamic_config_dir.mkdir(parents=True) ingress_config_fname = ( - dynamic_config_dir / f"juju_ingress_ingress-per-unit_{rel.relation_id}_remote.yaml" + dynamic_config_dir / f"juju_ingress_ingress-per-unit_{rel.id}_remote.yaml" ) ingress_config_fname.touch() traefik_ctx.run( - rel.departed_event(remote_unit_id=0), + traefik_ctx.on.relation_departed(rel, remote_unit=0), State(relations=[rel], containers=[traefik_container], leader=True), ) diff --git a/tests/scenario/test_ingress_per_app.py b/tests/scenario/test_ingress_per_app.py index 89ae9025..94ace9d2 100644 --- a/tests/scenario/test_ingress_per_app.py +++ b/tests/scenario/test_ingress_per_app.py @@ -5,6 +5,7 @@ # THEN the traefik config file is updated import json import tempfile +from dataclasses import replace from pathlib import Path import pytest @@ -16,17 +17,22 @@ ) from ops import CharmBase, Framework from scenario import Context, Mount, Relation, State +from scenario.context import CharmEvents from tests.scenario._utils import create_ingress_relation +on = CharmEvents() + @pytest.mark.parametrize( "port, ip, host", ((80, "1.1.1.1", "1.1.1.1"), (81, "10.1.10.1", "10.1.10.1")) ) -@pytest.mark.parametrize("event_name", ("joined", "changed", "created")) +@pytest.mark.parametrize( + "event_source", (on.relation_joined, on.relation_changed, on.relation_created) +) @pytest.mark.parametrize("scheme", ("http", "https")) def test_ingress_per_app_created( - traefik_ctx, port, ip, host, model, traefik_container, event_name, tmp_path, scheme + traefik_ctx, port, ip, host, model, traefik_container, event_source, tmp_path, scheme ): """Check the config when a new ingress per app is created or changes (single remote unit).""" ipa = create_ingress_relation(port=port, scheme=scheme, hosts=[host], ips=[ip]) @@ -38,12 +44,11 @@ def test_ingress_per_app_created( ) # WHEN any relevant event fires - event = getattr(ipa, f"{event_name}_event") - traefik_ctx.run(event, state) + traefik_ctx.run(event_source(ipa), state) generated_config = yaml.safe_load( traefik_container.get_filesystem(traefik_ctx) - .joinpath(f"opt/traefik/juju/juju_ingress_ingress_{ipa.relation_id}_remote.yaml") + .joinpath(f"opt/traefik/juju/juju_ingress_ingress_{ipa.id}_remote.yaml") .read_text() ) @@ -66,10 +71,10 @@ def test_ingress_per_app_created( "port, ip, host", ((80, "1.1.1.{}", "1.1.1.{}"), (81, "10.1.10.{}", "10.1.10.{}")) ) @pytest.mark.parametrize("n_units", (2, 3, 10)) -@pytest.mark.parametrize("evt_name", ("joined", "changed")) +@pytest.mark.parametrize("event_source", (on.relation_joined, on.relation_changed)) @pytest.mark.parametrize("scheme", ("http", "https")) def test_ingress_per_app_scale( - traefik_ctx, host, ip, port, model, traefik_container, tmp_path, n_units, scheme, evt_name + traefik_ctx, host, ip, port, model, traefik_container, tmp_path, n_units, scheme, event_source ): """Check the config when a new ingress per app unit joins.""" relation_id = 42 @@ -120,7 +125,7 @@ def test_ingress_per_app_scale( relations=[ipa], ) - traefik_ctx.run(getattr(ipa, evt_name + "_event"), state) + traefik_ctx.run(event_source(ipa), state) new_config = yaml.safe_load(cfg_file.read_text()) # verify that the config has changed! @@ -143,11 +148,6 @@ def test_ingress_per_app_scale( # d["service"][svc_name]["loadBalancer"]["servers"][0]["url"] == leader_url -@pytest.mark.parametrize( - "port, ip, host", ((80, "1.1.1.1", "1.1.1.1"), (81, "10.1.10.1", "10.1.10.1")) -) -@pytest.mark.parametrize("evt_name", ("joined", "changed")) -@pytest.mark.parametrize("leader", (True, False)) def get_requirer_ctx(host, ip, port): class MyRequirer(CharmBase): def __init__(self, framework: Framework): @@ -164,9 +164,9 @@ def __init__(self, framework: Framework): @pytest.mark.parametrize( "port, ip, host", ((80, "1.1.1.1", "1.1.1.1"), (81, "10.1.10.1", "1.1.1.1")) ) -@pytest.mark.parametrize("evt_name", ("joined", "changed")) +@pytest.mark.parametrize("event_source", (on.relation_joined, on.relation_changed)) @pytest.mark.parametrize("leader", (True, False)) -def test_ingress_per_app_requirer_with_auto_data(host, ip, port, model, evt_name, leader): +def test_ingress_per_app_requirer_with_auto_data(host, ip, port, model, event_source, leader): ipa = Relation("ingress") state = State( model=model, @@ -174,7 +174,7 @@ def test_ingress_per_app_requirer_with_auto_data(host, ip, port, model, evt_name relations=[ipa], ) requirer_ctx = get_requirer_ctx(host, ip, port) - state_out = requirer_ctx.run(getattr(ipa, evt_name + "_event"), state) + state_out = requirer_ctx.run(event_source(ipa), state) ipa_out = state_out.get_relations("ingress")[0] assert ipa_out.local_unit_data == {"host": json.dumps(host), "ip": json.dumps(ip)} @@ -192,11 +192,13 @@ def test_ingress_per_app_cleanup_on_remove(model, traefik_ctx, traefik_container ipa = create_ingress_relation() td = tempfile.TemporaryDirectory() - filename = f"juju_ingress_ingress_{ipa.relation_id}_remote.yaml" + filename = f"juju_ingress_ingress_{ipa.id}_remote.yaml" conf_file = Path(td.name).joinpath(filename) conf_file.write_text("foobar") - traefik_container = traefik_container.replace(mounts={"conf": Mount("/opt/traefik/", td.name)}) + traefik_container = replace( + traefik_container, mounts={"conf": Mount(location="/opt/traefik/", source=td.name)} + ) state = State( model=model, @@ -206,7 +208,7 @@ def test_ingress_per_app_cleanup_on_remove(model, traefik_ctx, traefik_container ) # WHEN the relation goes - traefik_ctx.run(ipa.broken_event, state) + traefik_ctx.run(on.relation_broken(ipa), state) # THEN the config file was deleted mock_dynamic_config_folder = traefik_container.get_filesystem(traefik_ctx).joinpath( @@ -242,7 +244,7 @@ def test_ingress_per_app_v1_upgrade_v2( ) # WHEN a charm upgrade occurs - with requirer_ctx.manager("upgrade-charm", state) as mgr: + with requirer_ctx(on.upgrade_charm(), state) as mgr: assert not mgr.charm.ipa.is_ready() state_out = mgr.run() assert not mgr.charm.ipa.is_ready() @@ -283,18 +285,14 @@ def test_proxied_endpoints( state = State(leader=True, relations=[ipav1, ipav2, ipu], containers=[traefik_container]) # WHEN we get any event - with traefik_ctx.manager("update-status", state) as mgr: + with traefik_ctx(on.update_status(), state) as mgr: charm = mgr.charm # populate the local app databags - charm.ingress_per_appv1.publish_url( - charm.model.get_relation("ingress", ipav1.relation_id), url1 - ) - charm.ingress_per_appv2.publish_url( - charm.model.get_relation("ingress", ipav2.relation_id), url2 - ) + charm.ingress_per_appv1.publish_url(charm.model.get_relation("ingress", ipav1.id), url1) + charm.ingress_per_appv2.publish_url(charm.model.get_relation("ingress", ipav2.id), url2) charm.ingress_per_unit.publish_url( - charm.model.get_relation("ingress-per-unit", ipu.relation_id), "remote/0", url3 + charm.model.get_relation("ingress-per-unit", ipu.id), "remote/0", url3 ) # THEN the charm can fetch the proxied endpoints without errors diff --git a/tests/scenario/test_ingress_per_unit.py b/tests/scenario/test_ingress_per_unit.py index 0ab1d7f4..1b8e1ebd 100644 --- a/tests/scenario/test_ingress_per_unit.py +++ b/tests/scenario/test_ingress_per_unit.py @@ -1,8 +1,11 @@ import pytest from scenario import Relation, State +from scenario.context import CharmEvents from tests.scenario.conftest import MOCK_EXTERNAL_HOSTNAME +on = CharmEvents() + @pytest.mark.parametrize("leader", (True, False)) @pytest.mark.parametrize("url", ("url.com", "http://foo.bar.baz")) @@ -39,7 +42,7 @@ def test_ingress_unit_provider_request_response( containers=[traefik_container], ) - state_out = traefik_ctx.run(ipu.changed_event, state) + state_out = traefik_ctx.run(on.relation_changed(ipu), state) ipu_out = state_out.get_relations(ipu.endpoint)[0] diff --git a/tests/scenario/test_ingress_per_unit_provider.py b/tests/scenario/test_ingress_per_unit_provider.py index 64cee5a1..3b256968 100644 --- a/tests/scenario/test_ingress_per_unit_provider.py +++ b/tests/scenario/test_ingress_per_unit_provider.py @@ -1,11 +1,14 @@ # Copyright 2022 Canonical Ltd. # See LICENSE file for licensing details. +from dataclasses import replace import pytest from charms.traefik_k8s.v1.ingress_per_unit import IngressPerUnitProvider from ops.charm import CharmBase from scenario import Context, Model, Relation, State -from scenario.sequences import check_builtin_sequences +from scenario.context import CharmEvents + +on = CharmEvents() class MockProviderCharm(CharmBase): @@ -19,16 +22,6 @@ def __init__(self, *args, **kwargs): self.ipu = IngressPerUnitProvider(self) -def test_builtin_sequences(): - check_builtin_sequences( - charm_type=MockProviderCharm, - meta={ - "name": "test-provider", - "provides": {"ingress-per-unit": {"interface": "ingress_per_unit", "limit": 1}}, - }, - ) - - @pytest.fixture def model(): return Model(name="test-model") @@ -40,16 +33,15 @@ def ipu_empty(): endpoint="ingress-per-unit", interface="ingress_per_unit", remote_app_name="remote", - relation_id=0, + id=0, ) @pytest.mark.parametrize("leader", (True, False)) @pytest.mark.parametrize( - "event_name", - ("update-status", "install", "start", "RELCHANGED", "config-changed"), + "event_source", (on.update_status, on.install, on.start, "RELCHANGED", on.config_changed) ) -def test_ingress_unit_provider_related_is_ready(leader, event_name, ipu_empty, model): +def test_ingress_unit_provider_related_is_ready(leader, event_source, ipu_empty, model): # patch the state with leadership state = State(model=model, relations=[ipu_empty], leader=leader) @@ -58,11 +50,11 @@ def test_ingress_unit_provider_related_is_ready(leader, event_name, ipu_empty, m # IPU should report ready because in this context # we can find remote relation data - if event_name == "RELCHANGED": - event = ipu_empty.changed_event + if event_source == "RELCHANGED": + event = on.relation_changed(ipu_empty) # relation events need some extra metadata. else: - event = event_name + event = event_source() Context(charm_type=MockProviderCharm, meta=MockProviderCharm.META).run(event, state) @@ -84,7 +76,15 @@ def test_ingress_unit_provider_request_response(port, host, leader, url, mode, i test_url = "http://foo.com/babooz" - def callback(charm: MockProviderCharm): + ipu_remote_provided = replace(ipu_empty, remote_units_data={0: mock_data}) + state = State(model=model, relations=[ipu_remote_provided], leader=leader) + + with Context(charm_type=MockProviderCharm, meta=MockProviderCharm.META)( + on.relation_changed(ipu_remote_provided), state + ) as mgr: + out = mgr.run() + + charm = mgr.charm ingress = charm.model.get_relation("ingress-per-unit") remote_unit = list(ingress.units)[0] @@ -104,15 +104,8 @@ def callback(charm: MockProviderCharm): with pytest.raises(AssertionError): charm.ipu.publish_url(ingress, remote_unit.name, test_url) - ipu_remote_provided = ipu_empty.replace(remote_units_data={0: mock_data}) - state = State(model=model, relations=[ipu_remote_provided], leader=leader) - - out = Context(charm_type=MockProviderCharm, meta=MockProviderCharm.META).run( - ipu_remote_provided.changed_event, state, post_event=callback - ) - if leader: - local_ipa_data = out.relations[0].local_app_data + local_ipa_data = out.get_relation(ipu_empty.id).local_app_data assert local_ipa_data["ingress"] == f"remote/0:\n url: {test_url}\n" else: - assert not out.relations[0].local_app_data + assert not out.get_relation(ipu_empty.id).local_app_data diff --git a/tests/scenario/test_ingress_tls.py b/tests/scenario/test_ingress_tls.py index b38fd7a8..7b3deacb 100644 --- a/tests/scenario/test_ingress_tls.py +++ b/tests/scenario/test_ingress_tls.py @@ -4,10 +4,13 @@ import ops.pebble import pytest import yaml -from scenario import Container, ExecOutput, Mount, Relation, State +from scenario import Container, Exec, Mount, Relation, State +from scenario.context import CharmEvents from tests.scenario._utils import _render_config, create_ingress_relation +on = CharmEvents() + def _create_tls_relation(*, app_name: str, strip_prefix: bool, redirect_https: bool): app_data = { @@ -36,12 +39,12 @@ def test_middleware_config( Container( name="traefik", can_connect=True, - mounts={"configurations": Mount("/opt/traefik/", td.name)}, - exec_mock={("find", "/opt/traefik/juju", "-name", "*.yaml", "-delete"): ExecOutput()}, + mounts={"configurations": Mount(location="/opt/traefik/", source=td.name)}, + execs={Exec(("find", "/opt/traefik/juju", "-name", "*.yaml", "-delete"))}, layers={ "traefik": ops.pebble.Layer({"services": {"traefik": {"startup": "enabled"}}}) }, - service_status={"traefik": ops.pebble.ServiceStatus.ACTIVE}, + service_statuses={"traefik": ops.pebble.ServiceStatus.ACTIVE}, ) ] @@ -88,7 +91,7 @@ def test_middleware_config( ) # WHEN a `relation-changed` hook fires - out = traefik_ctx.run(ipa.changed_event, state) + out = traefik_ctx.run(on.relation_changed(ipa), state) # THEN the rendered config file contains middlewares with out.get_container("traefik").get_filesystem(traefik_ctx).joinpath( diff --git a/tests/scenario/test_ingress_v1_backwards_compat/test_ingress_per_app_v1.py b/tests/scenario/test_ingress_v1_backwards_compat/test_ingress_per_app_v1.py index 64289716..68a58cc2 100644 --- a/tests/scenario/test_ingress_v1_backwards_compat/test_ingress_per_app_v1.py +++ b/tests/scenario/test_ingress_v1_backwards_compat/test_ingress_per_app_v1.py @@ -9,6 +9,9 @@ import yaml from ops import pebble from scenario import Container, Model, Mount, Relation, State +from scenario.context import CharmEvents + +on = CharmEvents() @pytest.fixture @@ -33,22 +36,24 @@ def traefik_container(tmp_path): } ) - opt = Mount("/opt/", tmp_path) + opt = Mount(location="/opt/", source=tmp_path) return Container( name="traefik", can_connect=True, layers={"traefik": layer}, - service_status={"traefik": pebble.ServiceStatus.ACTIVE}, + service_statuses={"traefik": pebble.ServiceStatus.ACTIVE}, mounts={"opt": opt}, ) @patch("charm.TraefikIngressCharm._static_config_changed", PropertyMock(return_value=False)) @pytest.mark.parametrize("port, host", ((80, "1.1.1.1"), (81, "10.1.10.1"))) -@pytest.mark.parametrize("event_name", ("joined", "changed", "created")) +@pytest.mark.parametrize( + "event_source", (on.relation_joined, on.relation_changed, on.relation_created) +) def test_ingress_per_app_created( - traefik_ctx, port, host, model, traefik_container, event_name, tmp_path, caplog + traefik_ctx, port, host, model, traefik_container, event_source, tmp_path, caplog ): """Check the config when a new ingress per leader is created or changes (single remote unit).""" ipa = Relation( @@ -59,7 +64,7 @@ def test_ingress_per_app_created( "port": str(port), "host": host, }, - relation_id=1, + id=1, ) state = State( model=model, @@ -69,15 +74,13 @@ def test_ingress_per_app_created( ) # WHEN any relevant event fires - event = getattr(ipa, f"{event_name}_event") - with caplog.at_level("WARNING"): - traefik_ctx.run(event, state) + traefik_ctx.run(event_source(ipa), state) assert "is using a deprecated ingress v1 protocol to talk to Traefik." in caplog.text generated_config = yaml.safe_load( traefik_container.get_filesystem(traefik_ctx) - .joinpath(f"opt/traefik/juju/juju_ingress_ingress_{ipa.relation_id}_remote.yaml") + .joinpath(f"opt/traefik/juju/juju_ingress_ingress_{ipa.id}_remote.yaml") .read_text() ) @@ -129,7 +132,7 @@ def test_ingress_per_app_scale( "port": str(port), "host": host, }, - relation_id=1, + id=1, ) state = State( model=model, @@ -139,7 +142,7 @@ def test_ingress_per_app_scale( ) with caplog.at_level("WARNING"): - traefik_ctx.run(ipa.changed_event, state) + traefik_ctx.run(on.relation_changed(ipa), state) assert "is using a deprecated ingress v1 protocol to talk to Traefik." in caplog.text new_config = yaml.safe_load(cfg_file.read_text()) diff --git a/tests/scenario/test_ingress_v1_backwards_compat/test_ingress_tls_v1.py b/tests/scenario/test_ingress_v1_backwards_compat/test_ingress_tls_v1.py index 959e2c24..d2994d7d 100644 --- a/tests/scenario/test_ingress_v1_backwards_compat/test_ingress_tls_v1.py +++ b/tests/scenario/test_ingress_v1_backwards_compat/test_ingress_tls_v1.py @@ -4,9 +4,12 @@ import pytest import yaml from scenario import Container, Mount, Relation, State +from scenario.context import CharmEvents from tests.scenario._utils import _render_config +on = CharmEvents() + def _create_ingress_relation( *, rel_id: int, app_name: str, strip_prefix: bool, redirect_https: bool @@ -23,7 +26,7 @@ def _create_ingress_relation( return Relation( endpoint="ingress", remote_app_name=app_name, - relation_id=rel_id, + id=rel_id, remote_app_data=app_data, ) @@ -52,7 +55,7 @@ def test_middleware_config(traefik_ctx, routing_mode, strip_prefix, redirect_htt Container( name="traefik", can_connect=True, - mounts={"configurations": Mount("/opt/traefik/", td.name)}, + mounts={"configurations": Mount(location="/opt/traefik/", source=td.name)}, ) ] @@ -82,7 +85,7 @@ def test_middleware_config(traefik_ctx, routing_mode, strip_prefix, redirect_htt # WHEN a `relation-changed` hook fires with caplog.at_level("WARNING"): - out = traefik_ctx.run(ipa.changed_event, state) + out = traefik_ctx.run(on.relation_changed(ipa), state) assert "is using a deprecated ingress v1 protocol to talk to Traefik." in caplog.text # THEN the rendered config file contains middlewares diff --git a/tests/scenario/test_ingress_v1_backwards_compat/test_middlewares_v1.py b/tests/scenario/test_ingress_v1_backwards_compat/test_middlewares_v1.py index a0727218..cf9d8ff6 100644 --- a/tests/scenario/test_ingress_v1_backwards_compat/test_middlewares_v1.py +++ b/tests/scenario/test_ingress_v1_backwards_compat/test_middlewares_v1.py @@ -8,9 +8,12 @@ import pytest import yaml from scenario import Container, Mount, Relation, State +from scenario.context import CharmEvents from tests.scenario._utils import _render_config +on = CharmEvents() + def _create_relation( *, rel_id: int, rel_name: str, app_name: str, strip_prefix: bool, redirect_https: bool @@ -29,7 +32,7 @@ def _create_relation( return Relation( endpoint=rel_name, remote_app_name=app_name, - relation_id=rel_id, + id=rel_id, remote_app_data=app_data, ) @@ -46,7 +49,7 @@ def _create_relation( return Relation( endpoint=rel_name, remote_app_name=app_name, - relation_id=rel_id, + id=rel_id, remote_units_data={0: unit_data}, ) @@ -69,7 +72,7 @@ def test_middleware_config( Container( name="traefik", can_connect=True, - mounts={"configurations": Mount("/opt/traefik/", td.name)}, + mounts={"configurations": Mount(location="/opt/traefik/", source=td.name)}, ) ] @@ -94,7 +97,7 @@ def test_middleware_config( # WHEN a `relation-changed` hook fires with caplog.at_level("WARNING"): - out = traefik_ctx.run(relation.changed_event, state) + out = traefik_ctx.run(on.relation_changed(relation), state) if rel_name == "ingress": assert "is using a deprecated ingress v1 protocol to talk to Traefik." in caplog.text diff --git a/tests/scenario/test_ipa.py b/tests/scenario/test_ipa.py index e6b0f3e6..de927c09 100644 --- a/tests/scenario/test_ipa.py +++ b/tests/scenario/test_ipa.py @@ -1,32 +1,37 @@ +from dataclasses import replace from pathlib import Path from typing import List, Tuple import yaml from scenario import Context, Relation, State -from scenario.state import DEFAULT_JUJU_DATABAG +from scenario.context import CharmEvents +from scenario.state import _DEFAULT_JUJU_DATABAG + +on = CharmEvents() def create(traefik_ctx: Context, state: State): """Create the ingress relation.""" ingress = Relation("ingress") - return traefik_ctx.run(ingress.joined_event, state.replace(relations=[ingress])) + return traefik_ctx.run(on.relation_joined(ingress), replace(state, relations=[ingress])) def join(traefik_ctx: Context, state: State): """Simulate a new unit joining the ingress relation.""" ingress = state.get_relations("ingress")[0] - state = traefik_ctx.run(ingress.joined_event, state) + state = traefik_ctx.run(on.relation_joined(ingress), state) remote_units_data = ingress.remote_units_data joining_unit_id = max(remote_units_data) - if set(remote_units_data[joining_unit_id]).difference(DEFAULT_JUJU_DATABAG): + if set(remote_units_data[joining_unit_id]).difference(_DEFAULT_JUJU_DATABAG): joining_unit_id += 1 remote_units_data[joining_unit_id] = { "host": f'"neutron-{joining_unit_id}.neutron-endpoints.zaza-de71889d82db.svc.cluster.local"' } relations = [ - ingress.replace( + replace( + ingress, remote_app_data={ "model": '"zaza"', "name": '"neutron"', @@ -39,8 +44,9 @@ def join(traefik_ctx: Context, state: State): ) ] + state_with_remotes = replace(state, relations=relations) state = traefik_ctx.run( - state.get_relations("ingress")[0].changed_event, state.replace(relations=relations) + on.relation_changed(state_with_remotes.get_relations("ingress")[0]), state_with_remotes ) return state @@ -53,11 +59,11 @@ def _pop(state: State): remote_units_data = ingress.remote_units_data.copy() departing_unit_id = max(remote_units_data) del remote_units_data[departing_unit_id] - return state.replace(relations=[ingress.replace(remote_units_data=remote_units_data)]) + return replace(state, relations=[replace(ingress, remote_units_data=remote_units_data)]) state = _pop(state) - state = traefik_ctx.run(state.get_relations("ingress")[0].departed_event, state) + state = traefik_ctx.run(on.relation_departed(state.get_relations("ingress")[0]), state) return state @@ -68,9 +74,10 @@ def break_(traefik_ctx: Context, state: State): depart(traefik_ctx, state) ingress = state.get_relations("ingress")[0] + cleared_remote_ingress = replace(ingress, remote_app_data={}, remote_units_data={}) return traefik_ctx.run( - ingress.broken_event, - state.replace(relations=[ingress.replace(remote_app_data={}, remote_units_data={})]), + on.relation_broken(cleared_remote_ingress), + replace(state, relations=[cleared_remote_ingress]), ) @@ -95,7 +102,7 @@ def test_traefik_remote_app_scaledown_from_2(traefik_ctx, traefik_container): """ state = State(containers=[traefik_container]) - with traefik_ctx.manager(traefik_container.pebble_ready_event, state) as mgr: + with traefik_ctx(on.pebble_ready(traefik_container), state) as mgr: state = mgr.run() static, dynamic = get_configs(traefik_ctx, state) diff --git a/tests/scenario/test_lib_per_unit_provides_sequences.py b/tests/scenario/test_lib_per_unit_provides_sequences.py index 55febb27..7b82ee4e 100644 --- a/tests/scenario/test_lib_per_unit_provides_sequences.py +++ b/tests/scenario/test_lib_per_unit_provides_sequences.py @@ -1,11 +1,14 @@ # Copyright 2022 Canonical Ltd. # See LICENSE file for licensing details. +from dataclasses import replace import pytest from charms.traefik_k8s.v1.ingress_per_unit import IngressPerUnitProvider from ops.charm import CharmBase from scenario import Context, Model, Relation, State -from scenario.sequences import check_builtin_sequences +from scenario.context import CharmEvents + +on = CharmEvents() class MockProviderCharm(CharmBase): @@ -30,26 +33,15 @@ def ipu_empty(): endpoint="ingress-per-unit", interface="ingress_per_unit", remote_app_name="remote", - relation_id=0, - ) - - -def test_builtin_sequences(): - check_builtin_sequences( - charm_type=MockProviderCharm, - meta={ - "name": "test-provider", - "provides": {"ingress-per-unit": {"interface": "ingress_per_unit", "limit": 1}}, - }, + id=0, ) @pytest.mark.parametrize("leader", (True, False)) @pytest.mark.parametrize( - "event_name", - ("update-status", "install", "start", "RELCHANGED", "config-changed"), + "event_source", (on.update_status, on.install, on.start, "RELCHANGED", on.config_changed) ) -def test_ingress_unit_provider_related_is_ready(leader, event_name, ipu_empty, model): +def test_ingress_unit_provider_related_is_ready(leader, event_source, ipu_empty, model): # patch the state with leadership state = State(model=model, relations=[ipu_empty], leader=leader) @@ -58,11 +50,11 @@ def test_ingress_unit_provider_related_is_ready(leader, event_name, ipu_empty, m # IPU should report ready because in this context # we can find remote relation data - if event_name == "RELCHANGED": - event = ipu_empty.changed_event + if event_source == "RELCHANGED": + event = on.relation_changed(ipu_empty) # relation events need some extra metadata. else: - event = event_name + event = event_source() Context(charm_type=MockProviderCharm, meta=MockProviderCharm.META).run(event, state) @@ -82,8 +74,8 @@ def test_ingress_unit_provider_request_response(port, host, leader, url, mode, i "mode": mode, } - ipu_remote_provided = ipu_empty.replace(remote_units_data={0: mock_data}) + ipu_remote_provided = replace(ipu_empty, remote_units_data={0: mock_data}) state = State(model=model, relations=[ipu_remote_provided], leader=leader) ctx = Context(charm_type=MockProviderCharm, meta=MockProviderCharm.META) - ctx.run(ipu_remote_provided.changed_event, state) + ctx.run(on.relation_changed(ipu_remote_provided), state) diff --git a/tests/scenario/test_middlewares.py b/tests/scenario/test_middlewares.py index de52eaed..5aa57b0a 100644 --- a/tests/scenario/test_middlewares.py +++ b/tests/scenario/test_middlewares.py @@ -9,11 +9,14 @@ import pytest import scenario import yaml -from scenario import Container, ExecOutput, Mount, Relation, State +from scenario import Container, Exec, Mount, Relation, State +from scenario.context import CharmEvents from tests.scenario._utils import _render_config, create_ingress_relation from traefik import DYNAMIC_CONFIG_DIR +on = CharmEvents() + def _create_relation( *, @@ -52,7 +55,7 @@ def _create_relation( return Relation( endpoint=rel_name, remote_app_name=app_name, - relation_id=rel_id, + id=rel_id, remote_units_data={0: unit_data}, ) @@ -77,12 +80,12 @@ def test_middleware_config( Container( name="traefik", can_connect=True, - mounts={"configurations": Mount("/opt/traefik/", td.name)}, - exec_mock={("find", "/opt/traefik/juju", "-name", "*.yaml", "-delete"): ExecOutput()}, + mounts={"configurations": Mount(location="/opt/traefik/", source=td.name)}, + execs={Exec(("find", "/opt/traefik/juju", "-name", "*.yaml", "-delete"))}, layers={ "traefik": ops.pebble.Layer({"services": {"traefik": {"startup": "enabled"}}}) }, - service_status={"traefik": ops.pebble.ServiceStatus.ACTIVE}, + service_statuses={"traefik": ops.pebble.ServiceStatus.ACTIVE}, ) ] @@ -109,7 +112,7 @@ def test_middleware_config( ) # WHEN a `relation-changed` hook fires - out = traefik_ctx.run(relation.changed_event, state) + out = traefik_ctx.run(on.relation_changed(relation), state) # THEN the rendered config file contains middlewares with out.get_container("traefik").get_filesystem(traefik_ctx).joinpath( @@ -141,25 +144,23 @@ def test_basicauth_config(traefik_ctx: scenario.Context): Container( name="traefik", can_connect=True, - exec_mock={ - ("find", "/opt/traefik/juju", "-name", "*.yaml", "-delete"): ExecOutput() - }, + execs={Exec(("find", "/opt/traefik/juju", "-name", "*.yaml", "-delete"))}, layers={ "traefik": ops.pebble.Layer({"services": {"traefik": {"startup": "enabled"}}}) }, - service_status={"traefik": ops.pebble.ServiceStatus.ACTIVE}, + service_statuses={"traefik": ops.pebble.ServiceStatus.ACTIVE}, ) ], ) # WHEN we process a config-changed event - state_out = traefik_ctx.run("config_changed", state) + state_out = traefik_ctx.run(on.config_changed(), state) # THEN traefik writes a dynamic config file with the expected basicauth middleware traefik_fs = state_out.get_container("traefik").get_filesystem(traefik_ctx) dynamic_config_path = ( Path(str(traefik_fs) + DYNAMIC_CONFIG_DIR) - / f"juju_ingress_{ingress.endpoint}_{ingress.relation_id}_{ingress.remote_app_name}.yaml" + / f"juju_ingress_{ingress.endpoint}_{ingress.id}_{ingress.remote_app_name}.yaml" ) assert dynamic_config_path.exists() http_cfg = yaml.safe_load(dynamic_config_path.read_text())["http"] diff --git a/tests/scenario/test_scheme.py b/tests/scenario/test_scheme.py deleted file mode 100644 index 94fd1818..00000000 --- a/tests/scenario/test_scheme.py +++ /dev/null @@ -1,27 +0,0 @@ -# -# TODO: implement -# @pytest.mark.parametrize("scheme", ("http", "https", "h2c")) -# def test_scheme(traefik_ctx, scheme, traefik_container): -# ipa = Relation( -# "ingress", -# remote_app_data={ -# "model": "test-model", -# "name": "remote", -# "port": "42", -# "scheme": scheme, -# }, -# remote_units_data={ -# 1: {"host": "foobar.com"} -# }, -# ) -# state_in = State( -# config={"routing_mode": "path", "external_hostname": "foo.com"}, -# containers=[traefik_container], -# relations=[ipa], -# ) -# -# -# -# @pytest.mark.parametrize("scheme", ("foo", "bar", "1")) -# def test_invalid_scheme(scheme): -# pass diff --git a/tests/scenario/test_setup.py b/tests/scenario/test_setup.py index 6e85d040..70e43cae 100644 --- a/tests/scenario/test_setup.py +++ b/tests/scenario/test_setup.py @@ -5,11 +5,15 @@ from unittest.mock import PropertyMock, patch +from ops import ActiveStatus, WaitingStatus from scenario import Container, Context, State +from scenario.context import CharmEvents from charm import TraefikIngressCharm from traefik import Traefik +on = CharmEvents() + @patch("charm.TraefikIngressCharm._external_host", PropertyMock(return_value="foo.bar")) def test_start_traefik_is_not_running(*_, traefik_ctx): @@ -40,8 +44,8 @@ def test_start_traefik_is_not_running(*_, traefik_ctx): ) ], ) - out = Context(charm_type=TraefikIngressCharm).run("start", state) - assert out.unit_status == ("waiting", f"waiting for service: '{Traefik.service_name}'") + out = Context(charm_type=TraefikIngressCharm).run(on.start(), state) + assert out.unit_status == WaitingStatus(f"waiting for service: '{Traefik.service_name}'") @patch("charm.TraefikIngressCharm._external_host", PropertyMock(return_value=False)) @@ -50,8 +54,8 @@ def test_start_traefik_no_hostname(*_, traefik_ctx): config={"routing_mode": "path"}, containers=[Container(name="traefik", can_connect=False)], ) - out = Context(charm_type=TraefikIngressCharm).run("start", state) - assert out.unit_status == ("waiting", "gateway address unavailable") + out = Context(charm_type=TraefikIngressCharm).run(on.start(), state) + assert out.unit_status == WaitingStatus("gateway address unavailable") @patch("charm.TraefikIngressCharm._external_host", PropertyMock(return_value="foo.bar")) @@ -62,5 +66,5 @@ def test_start_traefik_active(*_, traefik_ctx): config={"routing_mode": "path"}, containers=[Container(name="traefik", can_connect=False)], ) - out = Context(charm_type=TraefikIngressCharm).run("start", state) - assert out.unit_status == ("active", "Serving at foo.bar") + out = Context(charm_type=TraefikIngressCharm).run(on.start(), state) + assert out.unit_status == ActiveStatus("Serving at foo.bar") diff --git a/tests/scenario/test_status.py b/tests/scenario/test_status.py index f386a8a6..a1f44bd3 100644 --- a/tests/scenario/test_status.py +++ b/tests/scenario/test_status.py @@ -5,6 +5,9 @@ from ops import ActiveStatus, WaitingStatus from scenario import Container, State +from scenario.context import CharmEvents + +on = CharmEvents() @patch("charm.TraefikIngressCharm._external_host", PropertyMock(return_value="foo.bar")) @@ -15,7 +18,7 @@ def test_start_traefik_is_not_running(traefik_ctx, *_): containers=[Container(name="traefik", can_connect=True)], ) # WHEN a `start` hook fires - out = traefik_ctx.run("start", state) + out = traefik_ctx.run(on.start(), state) # THEN unit status is `waiting` assert out.unit_status == WaitingStatus("waiting for service: 'traefik'") @@ -29,7 +32,7 @@ def test_start_traefik_no_hostname(traefik_ctx, *_): config={"routing_mode": "path"}, containers=[Container(name="traefik", can_connect=True)], ) - out = traefik_ctx.run("start", state) + out = traefik_ctx.run(on.start(), state) # THEN unit status is `waiting` assert out.unit_status == WaitingStatus("gateway address unavailable") @@ -46,7 +49,7 @@ def test_start_traefik_active(traefik_ctx, *_): ) # WHEN a `start` hook fires - out = traefik_ctx.run("start", state) + out = traefik_ctx.run(on.start(), state) # THEN unit status is `active` assert out.unit_status == ActiveStatus("Serving at foo.bar") diff --git a/tests/scenario/test_tracing_integration.py b/tests/scenario/test_tracing_integration.py index e37ecbca..a15e989c 100644 --- a/tests/scenario/test_tracing_integration.py +++ b/tests/scenario/test_tracing_integration.py @@ -1,3 +1,4 @@ +from dataclasses import replace from unittest.mock import patch import opentelemetry @@ -6,9 +7,12 @@ from charms.tempo_k8s.v1.charm_tracing import charm_tracing_disabled from charms.tempo_k8s.v2.tracing import ProtocolType, Receiver, TracingProviderAppData from scenario import Relation, State +from scenario.context import CharmEvents from traefik import CA_CERT_PATH, DYNAMIC_TRACING_PATH +on = CharmEvents() + @pytest.fixture def tracing_relation(): @@ -36,7 +40,7 @@ def test_charm_trace_collection(traefik_ctx, traefik_container, caplog, tracing_ ) as f: f.return_value = opentelemetry.sdk.trace.export.SpanExportResult.SUCCESS # WHEN traefik receives - traefik_ctx.run(tracing_relation.changed_event, state_in) + traefik_ctx.run(on.relation_changed(tracing_relation), state_in) # assert "Setting up span exporter to endpoint: foo.com:81" in caplog.text # assert "Starting root trace with id=" in caplog.text @@ -50,7 +54,7 @@ def test_traefik_tracing_config(traefik_ctx, traefik_container, tracing_relation state_in = State(relations=[tracing_relation], containers=[traefik_container]) with charm_tracing_disabled(): - traefik_ctx.run(tracing_relation.changed_event, state_in) + traefik_ctx.run(on.relation_changed(tracing_relation), state_in) tracing_cfg = ( traefik_container.get_filesystem(traefik_ctx) @@ -75,7 +79,7 @@ def test_traefik_tracing_config_with_tls(traefik_ctx, traefik_container, tracing tls_enabled.return_value = "True" with charm_tracing_disabled(): - traefik_ctx.run(tracing_relation.changed_event, state_in) + traefik_ctx.run(on.relation_changed(tracing_relation), state_in) tracing_cfg = ( traefik_container.get_filesystem(traefik_ctx) @@ -98,17 +102,20 @@ def test_traefik_tracing_config_removed_if_relation_data_invalid( traefik_ctx, traefik_container, tracing_relation, was_present_before ): if was_present_before: - dt_path = traefik_container.mounts["opt"].src.joinpath("traefik", "juju", "tracing.yaml") + dt_path = traefik_container.mounts["opt"].source.joinpath( + "traefik", "juju", "tracing.yaml" + ) dt_path.parent.mkdir(parents=True) dt_path.write_text("foo") + tracing_relation_with_remote = replace(tracing_relation, remote_app_data={"foo": "bar"}) state_in = State( - relations=[tracing_relation.replace(remote_app_data={"foo": "bar"})], + relations=[tracing_relation_with_remote], containers=[traefik_container], ) with charm_tracing_disabled(): - traefik_ctx.run(tracing_relation.changed_event, state_in) + traefik_ctx.run(on.relation_changed(tracing_relation_with_remote), state_in) # assert file is not there assert ( @@ -121,14 +128,16 @@ def test_traefik_tracing_config_removed_on_relation_broken( traefik_ctx, traefik_container, tracing_relation, was_present_before ): if was_present_before: - dt_path = traefik_container.mounts["opt"].src.joinpath("traefik", "juju", "tracing.yaml") + dt_path = traefik_container.mounts["opt"].source.joinpath( + "traefik", "juju", "tracing.yaml" + ) dt_path.parent.mkdir(parents=True) dt_path.write_text("foo") state_in = State(relations=[tracing_relation], containers=[traefik_container]) with charm_tracing_disabled(): - traefik_ctx.run(tracing_relation.broken_event, state_in) + traefik_ctx.run(on.relation_broken(tracing_relation), state_in) # assert file is not there assert ( diff --git a/tests/scenario/test_workload_version.py b/tests/scenario/test_workload_version.py index 4a10e57c..6c2e7cb7 100644 --- a/tests/scenario/test_workload_version.py +++ b/tests/scenario/test_workload_version.py @@ -1,51 +1,75 @@ # Copyright 2023 Canonical Ltd. # See LICENSE file for licensing details. - -import unittest +from contextlib import ExitStack from unittest.mock import PropertyMock, patch +import pytest from ops.model import ActiveStatus from scenario import Container, Context, State +from scenario.context import CharmEvents from charm import TraefikIngressCharm +on = CharmEvents() + -@patch("charm.KubernetesServicePatch") -@patch("lightkube.core.client.GenericSyncClient") -@patch("charm.TraefikIngressCharm._static_config_changed", PropertyMock(return_value=False)) -@patch("charm.TraefikIngressCharm._external_host", PropertyMock(return_value="foo.bar")) -@patch("traefik.Traefik.is_ready", PropertyMock(return_value=True)) -@patch("charm.TraefikIngressCharm.version", PropertyMock(return_value="1.2.3")) -class TestWorkloadVersion(unittest.TestCase): - def setUp(self) -> None: - self.containers = [Container(name="traefik", can_connect=True)] - self.state = State( - config={"routing_mode": "path"}, - containers=self.containers, +@pytest.fixture(autouse=True) +def patch_all(): + with ExitStack() as stack: + stack.enter_context(patch("charm.KubernetesServicePatch")) + stack.enter_context(patch("lightkube.core.client.GenericSyncClient")) + stack.enter_context( + patch( + "charm.TraefikIngressCharm._static_config_changed", + PropertyMock(return_value=False), + ) + ) + stack.enter_context( + patch("charm.TraefikIngressCharm._external_host", PropertyMock(return_value="foo.bar")) ) - self.context = Context(charm_type=TraefikIngressCharm) + stack.enter_context(patch("traefik.Traefik.is_ready", PropertyMock(return_value=True))) + stack.enter_context( + patch("charm.TraefikIngressCharm.version", PropertyMock(return_value="1.2.3")) + ) + yield + + +@pytest.fixture +def state(): + containers = [Container(name="traefik", can_connect=True)] + return State( + config={"routing_mode": "path"}, + containers=containers, + ) + + +@pytest.fixture +def context(): + return Context(charm_type=TraefikIngressCharm) + + +def test_workload_version_is_set_on_update_status(context, state): + # GIVEN an initial state without the workload version set + out = context.run(on.start(), state) + assert out.unit_status == ActiveStatus("Serving at foo.bar") + assert out.workload_version == "" - def test_workload_version_is_set_on_update_status(self, *_): - # GIVEN an initial state without the workload version set - out = self.context.run("start", self.state) - self.assertEqual(out.unit_status, ActiveStatus("Serving at foo.bar")) - self.assertEqual(out.workload_version, "") + # WHEN update-status is triggered + out = context.run(on.update_status(), out) - # WHEN update-status is triggered - out = self.context.run("update-status", out) + # THEN the workload version is set + assert out.workload_version == "1.2.3" - # THEN the workload version is set - self.assertEqual(out.workload_version, "1.2.3") - def test_workload_version_clears_on_stop(self, *_): - # GIVEN a state after update-status (which we know sets the workload version) - # GIVEN an initial state with the workload version set - out = self.context.run("update-status", self.state) - self.assertEqual(out.unit_status, ActiveStatus("Serving at foo.bar")) - self.assertEqual(out.workload_version, "1.2.3") +def test_workload_version_clears_on_stop(context, state): + # GIVEN a state after update-status (which we know sets the workload version) + # GIVEN an initial state with the workload version set + out = context.run(on.update_status(), state) + assert out.unit_status == ActiveStatus("Serving at foo.bar") + assert out.workload_version == "1.2.3" - # WHEN the charm is stopped - out = self.context.run("stop", out) + # WHEN the charm is stopped + out = context.run(on.stop(), out) - # THEN workload version is cleared - self.assertEqual(out.workload_version, "") + # THEN workload version is cleared + assert out.workload_version == "" diff --git a/tox.ini b/tox.ini index 395ea929..2dc99683 100644 --- a/tox.ini +++ b/tox.ini @@ -87,7 +87,7 @@ allowlist_externals = /usr/bin/env description = Scenario tests deps = pytest - ops-scenario<7.0.0 + ops[testing]>=2.17 -r{toxinidir}/requirements.txt commands = pytest -v --tb native {[vars]tst_path}/scenario --log-cli-level=INFO -s {posargs} @@ -96,7 +96,7 @@ commands = description = Run interface tests deps = pytest - ops-scenario>=5.3.1 + ops[testing]>=2.17 pytest-interface-tester > 0.3 -r{toxinidir}/requirements.txt commands =