From 60c5e834ae9aed7db21a2a72e2d025a192a60528 Mon Sep 17 00:00:00 2001 From: minulo Date: Tue, 1 Sep 2026 11:07:14 -0400 Subject: [PATCH 1/5] Refactored the test --- haproxy-operator/tests/unit/test_charm.py | 274 +++++++++++----------- 1 file changed, 140 insertions(+), 134 deletions(-) diff --git a/haproxy-operator/tests/unit/test_charm.py b/haproxy-operator/tests/unit/test_charm.py index c1bcd039..4fd9ed89 100644 --- a/haproxy-operator/tests/unit/test_charm.py +++ b/haproxy-operator/tests/unit/test_charm.py @@ -342,144 +342,150 @@ def test_ca_certificates_removed(monkeypatch: pytest.MonkeyPatch, receive_ca_cer @pytest.mark.usefixtures("systemd_mock", "mocks_external_calls") -class TestGetProxiedEndpointsAction: - """Test "get-proxied-endpoints" Action""" - - def test_no_backend_filter(self) -> None: - """ - arrange: create state with one haproxy-route relation containing - hostname, additional_hostnames, and paths. - act: trigger the get-proxied-endpoints action without a backend filter. - assert: returns a list of all proxied endpoints for every hostname/path combination. - """ - context = ops.testing.Context(HAProxyCharm) - - haproxy_route_relation = ops.testing.Relation( - "haproxy-route", - remote_app_data={ - "hostname": f'"{TEST_EXTERNAL_HOSTNAME_CONFIG}"', - "additional_hostnames": json.dumps( - [ - f"ok2.{TEST_EXTERNAL_HOSTNAME_CONFIG}", - f"ok3.{TEST_EXTERNAL_HOSTNAME_CONFIG}", - ] - ), - "paths": '["/v1", "/v2"]', - "ports": "[443]", - "protocol": '"http"', - "service": '"haproxy-tutorial-ingress-configurator"', - }, - remote_units_data={0: {"address": '"10.75.1.129"'}}, - ) - charm_state = ops.testing.State( - relations=[haproxy_route_relation], - leader=True, - model=ops.testing.Model(name="haproxy-tutorial"), - app_status=ops.testing.ActiveStatus(), - unit_status=ops.testing.ActiveStatus(), - ) - context.run(context.on.action("get-proxied-endpoints"), charm_state) - - out = context.action_results - assert out is not None - - assert set(json.loads(out["endpoints"])) == { - "https://haproxy.internal/v1", - "https://haproxy.internal/v2", - "https://ok2.haproxy.internal/v1", - "https://ok2.haproxy.internal/v2", - "https://ok3.haproxy.internal/v1", - "https://ok3.haproxy.internal/v2", - } - - def test_no_backend_filter_no_endpoints(self) -> None: - """ - arrange: create state with no haproxy-route relations. - act: trigger the get-proxied-endpoints action without a backend filter. - assert: returns an empty list. - """ - context = ops.testing.Context(HAProxyCharm) - charm_state = ops.testing.State( - relations=[], - leader=True, - model=ops.testing.Model(name="haproxy-tutorial"), - app_status=ops.testing.ActiveStatus(), - unit_status=ops.testing.ActiveStatus(), - ) - context.run(context.on.action("get-proxied-endpoints"), charm_state) - - out = context.action_results - - assert out == {"endpoints": "[]"} - - def test_with_backend_filter(self) -> None: - """ - arrange: create state with a haproxy-route relation for a specific backend. - act: trigger the get-proxied-endpoints action with the backend filter. - assert: returns a list containing the endpoint for that backend. - """ - service_name = "haproxy-tutorial-ingress-configurator" - context = ops.testing.Context(HAProxyCharm) - haproxy_route_relation = ops.testing.Relation( - "haproxy-route", - remote_app_data={ - "hostname": f'"{TEST_EXTERNAL_HOSTNAME_CONFIG}"', - "ports": "[443]", - "protocol": '"http"', - "service": f'"{service_name}"', - }, - remote_units_data={0: {"address": '"10.75.1.129"'}}, - ) - charm_state = ops.testing.State( - relations=[haproxy_route_relation], - leader=True, - model=ops.testing.Model(name="haproxy-tutorial"), - app_status=ops.testing.ActiveStatus(), - unit_status=ops.testing.ActiveStatus(), - ) - context.run( - context.on.action("get-proxied-endpoints", params={"backend": service_name}), - charm_state, - ) +def test_get_proxied_endpoints_no_backend_filter() -> None: + """ + arrange: create state with one haproxy-route relation containing + hostname, additional_hostnames, and paths. + act: trigger the get-proxied-endpoints action without a backend filter. + assert: returns a list of all proxied endpoints for every hostname/path combination. + """ + context = ops.testing.Context(HAProxyCharm) - out = context.action_results - - assert out == {"endpoints": f'["https://{TEST_EXTERNAL_HOSTNAME_CONFIG}"]'} - - def test_with_backend_filter_non_existing_backend(self) -> None: - """ - arrange: create state with a haproxy-route relation for a specific backend. - act: trigger the get-proxied-endpoints action with a non-existing backend name. - assert: raises ActionFailed indicating the backend does not exist. - """ - service_name = "haproxy-tutorial-ingress-configurator" - context = ops.testing.Context(HAProxyCharm) - haproxy_route_relation = ops.testing.Relation( - "haproxy-route", - remote_app_data={ - "hostname": f'"{TEST_EXTERNAL_HOSTNAME_CONFIG}"', - "ports": "[443]", - "protocol": '"http"', - "service": f'"{service_name}"', - }, - remote_units_data={0: {"address": '"10.75.1.129"'}}, - ) - charm_state = ops.testing.State( - relations=[haproxy_route_relation], - leader=True, - model=ops.testing.Model(name="haproxy-tutorial"), - app_status=ops.testing.ActiveStatus(), - unit_status=ops.testing.ActiveStatus(), - ) + haproxy_route_relation = ops.testing.Relation( + "haproxy-route", + remote_app_data={ + "hostname": f'"{TEST_EXTERNAL_HOSTNAME_CONFIG}"', + "additional_hostnames": json.dumps( + [ + f"ok2.{TEST_EXTERNAL_HOSTNAME_CONFIG}", + f"ok3.{TEST_EXTERNAL_HOSTNAME_CONFIG}", + ] + ), + "paths": '["/v1", "/v2"]', + "ports": "[443]", + "protocol": '"http"', + "service": '"haproxy-tutorial-ingress-configurator"', + }, + remote_units_data={0: {"address": '"10.75.1.129"'}}, + ) + charm_state = ops.testing.State( + relations=[haproxy_route_relation], + leader=True, + model=ops.testing.Model(name="haproxy-tutorial"), + app_status=ops.testing.ActiveStatus(), + unit_status=ops.testing.ActiveStatus(), + ) - context.run( - context.on.action("get-proxied-endpoints", params={"backend": "random_name"}), - charm_state, - ) + context.run(context.on.action("get-proxied-endpoints"), charm_state) + + out = context.action_results + assert out is not None + + assert set(json.loads(out["endpoints"])) == { + "https://haproxy.internal/v1", + "https://haproxy.internal/v2", + "https://ok2.haproxy.internal/v1", + "https://ok2.haproxy.internal/v2", + "https://ok3.haproxy.internal/v1", + "https://ok3.haproxy.internal/v2", + } + + +@pytest.mark.usefixtures("systemd_mock", "mocks_external_calls") +def test_get_proxied_endpoints_no_backend_filter_no_endpoints() -> None: + """ + arrange: create state with no haproxy-route relations. + act: trigger the get-proxied-endpoints action without a backend filter. + assert: returns an empty list. + """ + context = ops.testing.Context(HAProxyCharm) + charm_state = ops.testing.State( + relations=[], + leader=True, + model=ops.testing.Model(name="haproxy-tutorial"), + app_status=ops.testing.ActiveStatus(), + unit_status=ops.testing.ActiveStatus(), + ) + + context.run(context.on.action("get-proxied-endpoints"), charm_state) + + out = context.action_results + + assert out == {"endpoints": "[]"} + + +@pytest.mark.usefixtures("systemd_mock", "mocks_external_calls") +def test_get_proxied_endpoints_with_backend_filter() -> None: + """ + arrange: create state with a haproxy-route relation for a specific backend. + act: trigger the get-proxied-endpoints action with the backend filter. + assert: returns a list containing the endpoint for that backend. + """ + service_name = "haproxy-tutorial-ingress-configurator" + context = ops.testing.Context(HAProxyCharm) + haproxy_route_relation = ops.testing.Relation( + "haproxy-route", + remote_app_data={ + "hostname": f'"{TEST_EXTERNAL_HOSTNAME_CONFIG}"', + "ports": "[443]", + "protocol": '"http"', + "service": f'"{service_name}"', + }, + remote_units_data={0: {"address": '"10.75.1.129"'}}, + ) + charm_state = ops.testing.State( + relations=[haproxy_route_relation], + leader=True, + model=ops.testing.Model(name="haproxy-tutorial"), + app_status=ops.testing.ActiveStatus(), + unit_status=ops.testing.ActiveStatus(), + ) + + context.run( + context.on.action("get-proxied-endpoints", params={"backend": service_name}), + charm_state, + ) + + out = context.action_results + + assert out == {"endpoints": f'["https://{TEST_EXTERNAL_HOSTNAME_CONFIG}"]'} + + +@pytest.mark.usefixtures("systemd_mock", "mocks_external_calls") +def test_get_proxied_endpoints_with_backend_filter_non_existing_backend() -> None: + """ + arrange: create state with a haproxy-route relation for a specific backend. + act: trigger the get-proxied-endpoints action with a non-existing backend name. + assert: raises ActionFailed indicating the backend does not exist. + """ + service_name = "haproxy-tutorial-ingress-configurator" + context = ops.testing.Context(HAProxyCharm) + haproxy_route_relation = ops.testing.Relation( + "haproxy-route", + remote_app_data={ + "hostname": f'"{TEST_EXTERNAL_HOSTNAME_CONFIG}"', + "ports": "[443]", + "protocol": '"http"', + "service": f'"{service_name}"', + }, + remote_units_data={0: {"address": '"10.75.1.129"'}}, + ) + charm_state = ops.testing.State( + relations=[haproxy_route_relation], + leader=True, + model=ops.testing.Model(name="haproxy-tutorial"), + app_status=ops.testing.ActiveStatus(), + unit_status=ops.testing.ActiveStatus(), + ) + + context.run( + context.on.action("get-proxied-endpoints", params={"backend": "random_name"}), + charm_state, + ) - out = context.action_results + out = context.action_results - assert out == {"endpoints": "[]"} + assert out == {"endpoints": "[]"} @pytest.mark.usefixtures("systemd_mock", "mocks_external_calls") From 0699a2aacd3d855ea450fed3ba05255d75487f28 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 1 Sep 2026 15:18:02 +0000 Subject: [PATCH 2/5] Add release notes artifact for PR #646 Co-authored-by: minulo <292215651+minulo@users.noreply.github.com> --- docs/release-notes/artifacts/pr0646.yaml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 docs/release-notes/artifacts/pr0646.yaml diff --git a/docs/release-notes/artifacts/pr0646.yaml b/docs/release-notes/artifacts/pr0646.yaml new file mode 100644 index 00000000..2a36b19b --- /dev/null +++ b/docs/release-notes/artifacts/pr0646.yaml @@ -0,0 +1,17 @@ +version_schema: 2 + +changes: + - title: Refactored get-proxied-endpoints action tests + author: minulo + type: minor + description: > + Refactored the get-proxied-endpoints Juju action tests to follow the + same style as the rest of the test suite. No user-facing behavior + changed. + urls: + pr: + - https://github.com/canonical/haproxy-operator/pull/646 + related_doc: + related_issue: + visibility: internal + highlight: false From 7bec57877d00cd3f923eb3394532b5b99793f47c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 1 Sep 2026 15:45:30 +0000 Subject: [PATCH 3/5] Fix flaky gunicorn snap start by disabling auto-start on install Co-authored-by: minulo <292215651+minulo@users.noreply.github.com> --- haproxy-route-policy/snap/snapcraft.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/haproxy-route-policy/snap/snapcraft.yaml b/haproxy-route-policy/snap/snapcraft.yaml index c512a7ad..5591cd4b 100644 --- a/haproxy-route-policy/snap/snapcraft.yaml +++ b/haproxy-route-policy/snap/snapcraft.yaml @@ -43,6 +43,9 @@ apps: command: bin/gunicorn-start daemon: simple restart-condition: always + # The database configuration is not available until the configure hook + # runs, so avoid auto-starting (and crash-looping) on install. + install-mode: disable plugs: - network - network-bind From 2c27125be5139dd8a12b073fe713a5ffcc96fe3e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 4 Sep 2026 14:11:35 +0000 Subject: [PATCH 4/5] Revert snap auto-start change Co-authored-by: minulo <292215651+minulo@users.noreply.github.com> --- haproxy-route-policy/snap/snapcraft.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/haproxy-route-policy/snap/snapcraft.yaml b/haproxy-route-policy/snap/snapcraft.yaml index 5591cd4b..c512a7ad 100644 --- a/haproxy-route-policy/snap/snapcraft.yaml +++ b/haproxy-route-policy/snap/snapcraft.yaml @@ -43,9 +43,6 @@ apps: command: bin/gunicorn-start daemon: simple restart-condition: always - # The database configuration is not available until the configure hook - # runs, so avoid auto-starting (and crash-looping) on install. - install-mode: disable plugs: - network - network-bind From be1680ec8189cdff3fce0b5810837acbccc39f5f Mon Sep 17 00:00:00 2001 From: minulo Date: Fri, 4 Sep 2026 11:11:37 -0400 Subject: [PATCH 5/5] Update test assertion for non-existing backend case Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- haproxy-operator/tests/unit/test_charm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/haproxy-operator/tests/unit/test_charm.py b/haproxy-operator/tests/unit/test_charm.py index 4fd9ed89..afbb4201 100644 --- a/haproxy-operator/tests/unit/test_charm.py +++ b/haproxy-operator/tests/unit/test_charm.py @@ -456,7 +456,7 @@ def test_get_proxied_endpoints_with_backend_filter_non_existing_backend() -> Non """ arrange: create state with a haproxy-route relation for a specific backend. act: trigger the get-proxied-endpoints action with a non-existing backend name. - assert: raises ActionFailed indicating the backend does not exist. + assert: returns an empty list when the backend does not exist. """ service_name = "haproxy-tutorial-ingress-configurator" context = ops.testing.Context(HAProxyCharm)