From a2187477cd09c414451a6ad57d3777c3df7768ed Mon Sep 17 00:00:00 2001 From: Trung Thanh Phan Date: Sat, 20 Jan 2024 13:56:52 +0100 Subject: [PATCH] Add unit tests, set coverage target to 95 --- pyproject.toml | 2 +- tests/unit/test_agent.py | 137 +++++++++++++++++++++++++++++++++++++ tests/unit/test_charm.py | 93 +++++++++++++++++++------ tests/unit/test_service.py | 114 ++++++++++++++++++++++++++++++ tests/unit/test_state.py | 32 +++++++++ 5 files changed, 356 insertions(+), 22 deletions(-) create mode 100644 tests/unit/test_agent.py create mode 100644 tests/unit/test_state.py diff --git a/pyproject.toml b/pyproject.toml index eba55c5..0cf41a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ skips = ["*/*test.py", "*/test_*.py", "*tests/*.py"] branch = true [tool.coverage.report] -# fail_under = 99 +fail_under = 95 show_missing = true [tool.pytest.ini_options] diff --git a/tests/unit/test_agent.py b/tests/unit/test_agent.py new file mode 100644 index 0000000..45b9506 --- /dev/null +++ b/tests/unit/test_agent.py @@ -0,0 +1,137 @@ +# Copyright 2024 Canonical Ltd. +# See LICENSE file for licensing details. +# +# Learn more about testing at: https://juju.is/docs/sdk/testing + +# pylint: disable=protected-access +"""Test for agent relations.""" + +import secrets +from unittest.mock import MagicMock, PropertyMock + +import ops.testing +import pytest +from charms.operator_libs_linux.v1 import systemd + +import service +from charm import JenkinsAgentCharm + +agent_relation_data = {"url": "http://example.com", "jenkins-agent-0_secret": secrets.token_hex(4)} + + +def test_agent_relation_joined(harness: ops.testing.Harness): + """ + arrange: patched State.from_charm that raises an InvalidState Error. + act: when the JenkinsAgentCharm is initialized. + assert: The agent falls into BlockedStatus. + """ + harness.begin() + relation_id = harness.add_relation("agent", "jenkins-k8s", unit_data=agent_relation_data) + charm: JenkinsAgentCharm = harness.charm + assert ( + harness.get_relation_data(relation_id, app_or_unit="jenkins-agent/0") + == charm.state.agent_meta.as_dict() + ) + + +def test_agent_relation_changed_service_restart( + harness: ops.testing.Harness, monkeypatch: pytest.MonkeyPatch +): + """ + arrange: patched State.from_charm that raises an InvalidState Error. + act: when the JenkinsAgentCharm is initialized. + assert: The agent falls into BlockedStatus. + """ + _ = harness.add_relation("agent", "jenkins-k8s", unit_data=agent_relation_data) + harness.begin() + charm: JenkinsAgentCharm = harness.charm + assert charm.state.agent_relation_credentials + assert ( + charm.state.agent_relation_credentials.secret + == agent_relation_data["jenkins-agent-0_secret"] + ) + assert charm.state.agent_relation_credentials.address == agent_relation_data["url"] + + monkeypatch.setattr(charm.jenkins_agent_service, "restart", MagicMock()) + charm.agent_observer._on_agent_relation_changed(MagicMock()) + assert charm.unit.status.name == ops.ActiveStatus.name + + +def test_agent_relation_changed_service_restart_error( + harness: ops.testing.Harness, monkeypatch: pytest.MonkeyPatch +): + """ + arrange: patched State.from_charm that raises an InvalidState Error. + act: when the JenkinsAgentCharm is initialized. + assert: The agent falls into BlockedStatus. + """ + _ = harness.add_relation("agent", "jenkins-k8s", unit_data=agent_relation_data) + harness.begin() + charm: JenkinsAgentCharm = harness.charm + assert charm.state.agent_relation_credentials + assert ( + charm.state.agent_relation_credentials.secret + == agent_relation_data["jenkins-agent-0_secret"] + ) + assert charm.state.agent_relation_credentials.address == agent_relation_data["url"] + + monkeypatch.setattr( + charm.jenkins_agent_service, "restart", MagicMock(side_effect=service.ServiceRestartError) + ) + with pytest.raises(RuntimeError, match="Error restarting the agent service."): + charm.agent_observer._on_agent_relation_changed(MagicMock()) + + +def test_agent_relation_changed_service_already_active( + harness: ops.testing.Harness, monkeypatch: pytest.MonkeyPatch +): + """ + arrange: patched State.from_charm that raises an InvalidState Error. + act: when the JenkinsAgentCharm is initialized. + assert: The agent falls into BlockedStatus. + """ + service_restart_mock = MagicMock() + service_is_active_mock = PropertyMock(return_value=True) + monkeypatch.setattr(service.JenkinsAgentService, "restart", service_restart_mock) + monkeypatch.setattr(service.JenkinsAgentService, "is_active", service_is_active_mock) + + _ = harness.add_relation("agent", "jenkins-k8s", unit_data=agent_relation_data) + harness.begin() + charm: JenkinsAgentCharm = harness.charm + charm.agent_observer._on_agent_relation_changed(MagicMock()) + assert service_is_active_mock.call_count == 1 + assert service_restart_mock.call_count == 0 + + +def test_agent_relation_departed_service_stop_error( + harness: ops.testing.Harness, monkeypatch: pytest.MonkeyPatch +): + """ + arrange: patched State.from_charm that raises an InvalidState Error. + act: when the JenkinsAgentCharm is initialized. + assert: The agent falls into BlockedStatus. + """ + monkeypatch.setattr(systemd, "service_stop", MagicMock(side_effect=systemd.SystemdError)) + + relation_id = harness.add_relation("agent", "jenkins-k8s", unit_data=agent_relation_data) + harness.begin() + harness.remove_relation(relation_id=relation_id) + charm: JenkinsAgentCharm = harness.charm + assert charm.unit.status.name == ops.BlockedStatus.name + assert charm.unit.status.message == "Error stopping the agent service" + + +def test_agent_relation_departed(harness: ops.testing.Harness, monkeypatch: pytest.MonkeyPatch): + """ + arrange: patched State.from_charm that raises an InvalidState Error. + act: when the JenkinsAgentCharm is initialized. + assert: The agent falls into BlockedStatus. + """ + monkeypatch.setattr(systemd, "service_stop", MagicMock()) + + relation_id = harness.add_relation("agent", "jenkins-k8s", unit_data=agent_relation_data) + harness.begin() + harness.remove_relation(relation_id=relation_id) + charm: JenkinsAgentCharm = harness.charm + assert charm.unit.status.name == ops.BlockedStatus.name + assert charm.unit.status.message == "Waiting for config/relation." diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py index 81a8e6b..34ef714 100644 --- a/tests/unit/test_charm.py +++ b/tests/unit/test_charm.py @@ -17,18 +17,6 @@ from charm import JenkinsAgentCharm -def raise_exception(exception: Exception): - """Raise exception function for monkeypatching. - - Args: - exception: The exception to raise. - - Raises: - exception: . - """ - raise exception - - def test___init___invalid_state(harness: ops.testing.Harness, monkeypatch: pytest.MonkeyPatch): """ arrange: patched State.from_charm that raises an InvalidState Error. @@ -43,9 +31,9 @@ def test___init___invalid_state(harness: ops.testing.Harness, monkeypatch: pytes harness.begin() - jenkins_charm: JenkinsAgentCharm = harness.charm - assert jenkins_charm.unit.status.name == ops.BlockedStatus.name - assert jenkins_charm.unit.status.message == "Invalid executor message" + charm: JenkinsAgentCharm = harness.charm + assert charm.unit.status.name == ops.BlockedStatus.name + assert charm.unit.status.message == "Invalid executor message" def test__on_upgrade_charm(harness: ops.testing.Harness, monkeypatch: pytest.MonkeyPatch): @@ -58,12 +46,12 @@ def test__on_upgrade_charm(harness: ops.testing.Harness, monkeypatch: pytest.Mon monkeypatch.setattr(service.JenkinsAgentService, "restart", MagicMock()) harness.begin() - jenkins_charm: JenkinsAgentCharm = harness.charm + charm: JenkinsAgentCharm = harness.charm upgrade_charm_event = MagicMock(spec=ops.UpgradeCharmEvent) - jenkins_charm._on_upgrade_charm(upgrade_charm_event) + charm._on_upgrade_charm(upgrade_charm_event) - assert jenkins_charm.unit.status.message == "Waiting for relation." - assert jenkins_charm.unit.status.name == ops.BlockedStatus.name + assert charm.unit.status.message == "Waiting for relation." + assert charm.unit.status.name == ops.BlockedStatus.name def test__on_config_changed(harness: ops.testing.Harness, monkeypatch: pytest.MonkeyPatch): @@ -77,8 +65,71 @@ def test__on_config_changed(harness: ops.testing.Harness, monkeypatch: pytest.Mo get_relation_mock = MagicMock() monkeypatch.setattr(ops.Model, "get_relation", get_relation_mock) - jenkins_charm: JenkinsAgentCharm = harness.charm - jenkins_charm._on_config_changed(config_changed_event) + charm: JenkinsAgentCharm = harness.charm + charm._on_config_changed(config_changed_event) agent_relation = get_relation_mock.return_value assert agent_relation.data[harness._unit_name].update.call_count == 1 + + +def test_restart_agent_service(harness: ops.testing.Harness, monkeypatch: pytest.MonkeyPatch): + """ + arrange: given a charm with patched relation. + act: when _on_config_changed is called. + assert: The charm correctly updates the relation databag. + """ + get_relation_mock = MagicMock() + monkeypatch.setattr(ops.Model, "get_relation", get_relation_mock) + get_credentials_mock = MagicMock() + restart_mock = MagicMock() + monkeypatch.setattr(service.JenkinsAgentService, "restart", restart_mock) + + harness.begin() + + charm: JenkinsAgentCharm = harness.charm + monkeypatch.setattr(charm.state, "agent_relation_credentials", get_credentials_mock) + charm.restart_agent_service() + + assert restart_mock.call_count == 1 + assert charm.unit.status.name == ops.ActiveStatus.name + + +def test_restart_agent_service_incomplete_relation_data( + harness: ops.testing.Harness, monkeypatch: pytest.MonkeyPatch +): + """ + arrange: given a charm with patched relation. + act: when _on_config_changed is called. + assert: The charm correctly updates the relation databag. + """ + get_relation_mock = MagicMock() + monkeypatch.setattr(ops.Model, "get_relation", get_relation_mock) + harness.begin() + + charm: JenkinsAgentCharm = harness.charm + monkeypatch.setattr(charm.state, "agent_relation_credentials", None) + charm.restart_agent_service() + + assert charm.unit.status.name == ops.WaitingStatus.name + + +def test_restart_agent_service_service_restart_error( + harness: ops.testing.Harness, monkeypatch: pytest.MonkeyPatch +): + """ + arrange: given a charm with patched relation. + act: when _on_config_changed is called. + assert: The charm correctly updates the relation databag. + """ + get_relation_mock = MagicMock() + monkeypatch.setattr(ops.Model, "get_relation", get_relation_mock) + get_credentials_mock = MagicMock() + restart_mock = MagicMock(side_effect=service.ServiceRestartError) + monkeypatch.setattr(service.JenkinsAgentService, "restart", restart_mock) + + harness.begin() + + charm: JenkinsAgentCharm = harness.charm + monkeypatch.setattr(charm.state, "agent_relation_credentials", get_credentials_mock) + with pytest.raises(RuntimeError, match="Error restarting the agent service"): + charm.restart_agent_service() diff --git a/tests/unit/test_service.py b/tests/unit/test_service.py index 3d366ca..72afda5 100644 --- a/tests/unit/test_service.py +++ b/tests/unit/test_service.py @@ -1,15 +1,28 @@ # Copyright 2024 Canonical Ltd. # See LICENSE file for licensing details. """Test for service interaction.""" + +import os +from pathlib import Path + # pylint: disable=protected-access from unittest.mock import MagicMock import ops.testing import pytest from charms.operator_libs_linux.v0 import apt +from charms.operator_libs_linux.v1 import systemd +import service from charm import JenkinsAgentCharm +from .test_agent import agent_relation_data + +service_config_template = f'''[Service] +Environment="JENKINS_TOKEN={agent_relation_data.get('jenkins-agent-0_secret')}" +Environment="JENKINS_URL={agent_relation_data.get('url')}" +Environment="JENKINS_AGENT=jenkins-agent-0"''' + @pytest.mark.parametrize( "f,error_thrown", @@ -38,3 +51,104 @@ def test_install_apt_package_gpg_key_error( with pytest.raises(RuntimeError, match="Error installing the agent service"): charm._on_install(MagicMock(spec=ops.InstallEvent)) + + +def test_on_install_add_ppa(harness: ops.testing.Harness, monkeypatch: pytest.MonkeyPatch): + """ + arrange: Harness with mocked apt module. + act: run _on_install hook with methods raising different errors. + assert: The charm should be in an error state. + """ + apt_repository_mapping_mock = MagicMock() + apt_import_key_mock = MagicMock() + apt_update_mock = MagicMock() + apt_add_package_mock = MagicMock() + + apt_repository_mapping_mock.__contains__.return_value = False + monkeypatch.setattr(apt, "RepositoryMapping", apt_repository_mapping_mock) + monkeypatch.setattr(apt, "import_key", apt_import_key_mock) + monkeypatch.setattr(apt, "update", apt_update_mock) + monkeypatch.setattr(apt, "add_package", apt_add_package_mock) + + harness.begin_with_initial_hooks() + assert apt_add_package_mock.call_count == 2 + + +def test_restart_service(harness: ops.testing.Harness, monkeypatch: pytest.MonkeyPatch): + """ + arrange: Harness with mocked apt module. + act: run _on_install hook with methods raising different errors. + assert: The charm should be in an error state. + """ + pathlib_write_text_mock = MagicMock() + monkeypatch.setattr(Path, "write_text", pathlib_write_text_mock) + monkeypatch.setattr(os, "chmod", MagicMock) + monkeypatch.setattr(os, "chown", MagicMock) + monkeypatch.setattr(systemd, "daemon_reload", MagicMock) + monkeypatch.setattr(systemd, "service_restart", MagicMock) + monkeypatch.setattr(service.JenkinsAgentService, "is_active", MagicMock) + + harness.add_relation("agent", "jenkins-k8s", unit_data=agent_relation_data) + harness.begin() + charm: JenkinsAgentCharm = harness.charm + charm.jenkins_agent_service.restart() + assert pathlib_write_text_mock.call_args[0][0] == service_config_template + + +def test_restart_service_write_config_type_error( + harness: ops.testing.Harness, monkeypatch: pytest.MonkeyPatch +): + """ + arrange: Harness with mocked apt module. + act: run _on_install hook with methods raising different errors. + assert: The charm should be in an error state. + """ + monkeypatch.setattr(Path, "write_text", MagicMock(side_effect=TypeError)) + harness.add_relation("agent", "jenkins-k8s", unit_data=agent_relation_data) + harness.begin() + charm: JenkinsAgentCharm = harness.charm + with pytest.raises( + service.ServiceRestartError, + match="Error interacting with the filesystem when rendering configuration file", + ): + charm.jenkins_agent_service.restart() + + +def test_restart_service_systemd_error( + harness: ops.testing.Harness, monkeypatch: pytest.MonkeyPatch +): + """ + arrange: Harness with mocked apt module. + act: run _on_install hook with methods raising different errors. + assert: The charm should be in an error state. + """ + systemd_error_message = "Mock systemd error" + monkeypatch.setattr(service.JenkinsAgentService, "_render_file", MagicMock) + monkeypatch.setattr( + systemd, + "daemon_reload", + MagicMock(side_effect=systemd.SystemdError(systemd_error_message)), + ) + + harness.add_relation("agent", "jenkins-k8s", unit_data=agent_relation_data) + harness.begin() + charm: JenkinsAgentCharm = harness.charm + with pytest.raises( + service.ServiceRestartError, + match=f"Error starting the agent service:\n{systemd_error_message}", + ): + charm.jenkins_agent_service.restart() + + +def test_service_is_active_systemd_error( + harness: ops.testing.Harness, monkeypatch: pytest.MonkeyPatch +): + """ + arrange: Harness with mocked apt module. + act: run _on_install hook with methods raising different errors. + assert: The charm should be in an error state. + """ + harness.begin() + monkeypatch.setattr(systemd, "service_running", MagicMock(side_effect=SystemError)) + charm: JenkinsAgentCharm = harness.charm + assert not charm.jenkins_agent_service.is_active diff --git a/tests/unit/test_state.py b/tests/unit/test_state.py new file mode 100644 index 0000000..04b865b --- /dev/null +++ b/tests/unit/test_state.py @@ -0,0 +1,32 @@ +# Copyright 2024 Canonical Ltd. +# See LICENSE file for licensing details. +# +# Learn more about testing at: https://juju.is/docs/sdk/testing + +# pylint: disable=protected-access +"""Test for charm state.""" + +import os +from unittest.mock import MagicMock + +import ops +import ops.testing +import pytest + +import charm_state +from charm import JenkinsAgentCharm + + +def test_from_charm_invalid_metadata( + harness: ops.testing.Harness, monkeypatch: pytest.MonkeyPatch +): + """ + arrange: patched State.from_charm that raises an InvalidState Error. + act: when the JenkinsAgentCharm is initialized. + assert: The agent falls into BlockedStatus. + """ + monkeypatch.setattr(os, "cpu_count", MagicMock(return_value=0)) + harness.begin() + charm: JenkinsAgentCharm = harness.charm + with pytest.raises(charm_state.InvalidStateError, match="Invalid executor state."): + charm_state.State.from_charm(charm=charm)