Repository: canonical/haproxy-operator
Pull request #108: add first scenario test
Author: login: Thanhphan1147
Labels: name: Libraries: Out of sync
name: trivial
Created: 2025-05-15T12:56:06Z | Merged: 2025-05-16T08:24:34Z

Add first scenario test to kickstart the transition process from Harness to Scenario

### Checklist

- [x] The [charm style guide](https://juju.is/docs/sdk/styleguide) was applied
- [x] The [contributing guide](https://github.com/canonical/is-charms-contributing-guide) was applied
- [x] The changes are compliant with [ISD054 - Managing Charm Complexity](https://discourse.charmhub.io/t/specification-isd014-managing-charm-complexity/11619)
- [x] The documentation is generated using `src-docs`
- [x] The documentation for charmhub is updated
- [x] The PR is tagged with appropriate label (`urgent`, `trivial`, `complex`)

<!-- Explanation for any unchecked items above -->



Diff:
diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py
index 1a6d43307..13479b6cd 100644
--- a/tests/unit/conftest.py
+++ b/tests/unit/conftest.py
@@ -3,11 +3,12 @@
 
 """Fixtures for haproxy-operator unit tests."""
 import typing
-from unittest.mock import MagicMock
+from unittest.mock import MagicMock, patch
 
 import pytest
+import scenario
 from charms.tls_certificates_interface.v4.tls_certificates import Certificate, PrivateKey
-from ops.testing import Harness
+from ops.testing import Context, Harness
 
 from charm import HAProxyCharm
 
@@ -99,3 +100,46 @@ def ingress_requirer_application_data_fixture() -> dict[str, str]:
 def ingress_requirer_unit_data_fixture() -> dict[str, str]:
     """Mock ingress requirer unit data."""
     return {"host": '"testing.ingress"', "ip": '"10.0.0.1"'}
+
+
+# Scenario
+@pytest.fixture(name="context_with_install_mock")
+def context_with_install_mock_fixture():
+    """Context relation fixture.
+
+    Yield: The modeled haproxy-peers relation.
+    """
+    with patch("haproxy.HAProxyService.install") as install_mock:
+        yield (
+            Context(
+                charm_type=HAProxyCharm,
+            ),
+            install_mock,
+        )
+
+
+@pytest.fixture(name="peer_relation")
+def peer_relation_fixture():
+    """Peer relation fixture.
+
+    Yield: The modeled haproxy-peers relation.
+    """
+    return scenario.PeerRelation(
+        endpoint="haproxy-peers",
+        peers_data={},
+    )
+
+
+@pytest.fixture(name="base_state")
+def base_state_fixture(peer_relation):
+    """Base state fixture.
+
+    Args:
+        peer_relation: peer relation fixture
+
+    Yield: The modeled haproxy-peers relation.
+    """
+    input_state = {
+        "relations": [peer_relation],
+    }
+    yield input_state
diff --git a/tests/unit/test_charm_scenario.py b/tests/unit/test_charm_scenario.py
new file mode 100644
index 000000000..a061c95b0
--- /dev/null
+++ b/tests/unit/test_charm_scenario.py
@@ -0,0 +1,22 @@
+# Copyright 2025 Canonical Ltd.
+# See LICENSE file for licensing details.
+
+"""Unit tests for the haproxy charm."""
+
+import logging
+
+import ops
+
+logger = logging.getLogger(__name__)
+
+
+def test_install(context_with_install_mock, base_state):
+    """
+    arrange: prepare some state with peer relation
+    act: run start
+    assert: status is active
+    """
+    context, install_mock = context_with_install_mock
+    state = ops.testing.State(**base_state)
+    context.run(context.on.install(), state)
+    install_mock.assert_called_once()
diff --git a/tox.ini b/tox.ini
index ed7bde6fe..1a7475b29 100644
--- a/tox.ini
+++ b/tox.ini
@@ -55,6 +55,7 @@ deps =
     snowballstemmer<3.0.0
     types-PyYAML
     types-requests
+    ops[testing]
     -r{toxinidir}/requirements.txt
 commands =
     pydocstyle {[vars]src_path}
@@ -74,6 +75,7 @@ commands =
 description = Run unit tests
 deps =
     coverage[toml]
+    ops[testing]
     pytest
     -r{toxinidir}/requirements.txt
 commands =


Reviews:
user: login: amandahla
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T13:02:23Z
user: login: amandahla
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T13:02:58Z
user: login: amandahla
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T13:03:14Z
user: login: yanksyoon
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-15T13:06:19Z
user: login: Thanhphan1147
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-15T13:42:03Z

Review comments:
diff_hunk: @@ -0,0 +1,22 @@
+# Copyright 2025 Canonical Ltd.
+# See LICENSE file for licensing details.
+
+"""Unit tests for the bind module."""
path: tests/unit/test_charm_scenario.py
user: login: amandahla
body: minor: bind module ;-)
created_at: 2025-05-15T13:02:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,22 @@
+# Copyright 2025 Canonical Ltd.
+# See LICENSE file for licensing details.
+
+"""Unit tests for the bind module."""
+
+import logging
+
+import ops
+
+logger = logging.getLogger(__name__)
+
+
+def test_install(context_with_install_mock, base_state):
+    """
+    arrange: prepare some state with peer relation
+    act: run start
+    assert: status is active
+    """
+    context, install_mock = context_with_install_mock
+    state = ops.testing.State(**base_state)
+    context.run(context.on.install(), state)
+    install_mock.assert_called_once()
path: tests/unit/test_charm_scenario.py
user: login: amandahla
body: nice!
created_at: 2025-05-15T13:02:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -99,3 +100,46 @@ def ingress_requirer_application_data_fixture() -> dict[str, str]:
 def ingress_requirer_unit_data_fixture() -> dict[str, str]:
     """Mock ingress requirer unit data."""
     return {"host": '"testing.ingress"', "ip": '"10.0.0.1"'}
+
+
+# Scenario
+@pytest.fixture(name="context_with_install_mock")
+def context_with_install_mock_fixture():
+    """Context relation fixture.
+
+    Yield: The modeled haproxy-peers relation.
+    """
+    with patch("haproxy.HAProxyService.install") as install_mock:
+        yield (
+            Context(
+                charm_type=HAProxyCharm,
+            ),
+            install_mock,
+        )
+
+
+@pytest.fixture(name="peer_relation")
+def peer_relation_fixture():
+    """Peer relation fixture.
+
+    Yield: The modeled haproxy-peers relation.
+    """
+    yield scenario.PeerRelation(
+        endpoint="haproxy-peers",
+        peers_data={},
+    )
path: tests/unit/conftest.py
user: login: yanksyoon
body: Nit: I think if we're not running cleanup code, we can use `return`
```suggestion
    return scenario.PeerRelation(
        endpoint="haproxy-peers",
        peers_data={},
    )
```
created_at: 2025-05-15T13:05:28Z
author_association: MEMBER
diff_hunk: @@ -99,3 +100,46 @@ def ingress_requirer_application_data_fixture() -> dict[str, str]:
 def ingress_requirer_unit_data_fixture() -> dict[str, str]:
     """Mock ingress requirer unit data."""
     return {"host": '"testing.ingress"', "ip": '"10.0.0.1"'}
+
+
+# Scenario
+@pytest.fixture(name="context_with_install_mock")
+def context_with_install_mock_fixture():
+    """Context relation fixture.
+
+    Yield: The modeled haproxy-peers relation.
+    """
+    with patch("haproxy.HAProxyService.install") as install_mock:
+        yield (
+            Context(
+                charm_type=HAProxyCharm,
+            ),
+            install_mock,
+        )
+
+
+@pytest.fixture(name="peer_relation")
+def peer_relation_fixture():
+    """Peer relation fixture.
+
+    Yield: The modeled haproxy-peers relation.
+    """
+    yield scenario.PeerRelation(
+        endpoint="haproxy-peers",
+        peers_data={},
+    )
path: tests/unit/conftest.py
user: login: yanksyoon
body: same for other functions but up to you!
created_at: 2025-05-15T13:05:40Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,22 @@
+# Copyright 2025 Canonical Ltd.
+# See LICENSE file for licensing details.
+
+"""Unit tests for the bind module."""
path: tests/unit/test_charm_scenario.py
user: login: Thanhphan1147
body: thanks, updated!
created_at: 2025-05-15T13:42:03Z
author_association: COLLABORATOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-15T13:32:18Z
body: ## Test results for commit b3d9f3bcdc23d2a16b55df1a16f8a600e5f4e66a

Test coverage for b3d9f3bcdc23d2a16b55df1a16f8a600e5f4e66a

  ```
Name                         Stmts   Miss Branch BrPart  Cover   Missing
------------------------------------------------------------------------
src/charm.py                   207     99     56      6    43%   87, 162, 167, 175-188, 193, 198, 206, 215-288, 299, 313-317, 324, 338-344, 349-355, 360, 372-386, 391-394, 399, 407-422, 431-437
src/haproxy.py                  70     19      2      1    72%   75, 92-97, 112-119, 132-140, 148-154, 163-172, 182-183, 211
src/http_interface.py           73     25      4      0    62%   74, 83, 92, 106-108, 126, 138, 150, 162, 170-175, 187, 194, 202, 217-227
src/state/config.py             41     12      6      3    68%   53, 60-62, 67-68, 71, 93-95, 107-108
src/state/exception.py           1      0      0      0   100%
src/state/ha.py                 30      1      2      1    94%   50
src/state/haproxy_route.py      98     15     24      4    80%   102, 118, 141-145, 159-167, 215, 251, 259
src/state/ingress.py            35      0      4      0   100%
src/state/tls.py                38     12      8      1    59%   64-78, 105-110
src/state/validation.py         38     13      6      1    59%   64-69, 71->74, 77-83
src/tls_relation.py             48      8     18      7    77%   60, 62, 64, 74-75, 77->76, 100-106, 118->120
------------------------------------------------------------------------
TOTAL                          679    204    130     24    64%
```
  Static code analysis report

  ```
Run started:2025-05-15 13:31:38.832727

Test results:
	No issues identified.

Code scanned:
	Total lines of code: 3008
	Total lines skipped (#nosec): 1
	Total potential issues skipped due to specifically being disabled (e.g., #nosec BXXX): 2

Run metrics:
	Total issues (by severity):
		Undefined: 0
		Low: 0
		Medium: 0
		High: 0
	Total issues (by confidence):
		Undefined: 0
		Low: 0
		Medium: 0
		High: 0
Files skipped (0):
```
author_association: CONTRIBUTOR