From 5d8fc3214a379d7cbdd1c349c50b52cf5d2ba84c Mon Sep 17 00:00:00 2001 From: Judit Novak Date: Thu, 12 Oct 2023 21:36:41 +0200 Subject: [PATCH] Tests adjusted --- tests/integration/test_charm.py | 10 ++ tests/unit/test_charm.py | 257 +++++++++++++++++++++++++++----- 2 files changed, 230 insertions(+), 37 deletions(-) diff --git a/tests/integration/test_charm.py b/tests/integration/test_charm.py index 0f75d43ce..edeadac87 100644 --- a/tests/integration/test_charm.py +++ b/tests/integration/test_charm.py @@ -220,6 +220,16 @@ async def test_reset_and_get_password_secret_same_as_cli(ops_test: OpsTest) -> N # Getting back the pw programmatically password = await get_password(ops_test, username="monitor") + # + # No way to retrieve a secet by label for now (https://bugs.launchpad.net/juju/+bug/2037104) + # Therefore we take advantage of the fact, that we only have ONE single secret a this point + # So we take the single member of the list + # NOTE: This would BREAK if for instance units had secrets at the start... + # + complete_command = "list-secrets" + _, stdout, _ = await ops_test.juju(*complete_command.split()) + secret_id = stdout.split("\n")[1].split(" ")[0] + # Getting back the pw from juju CLI complete_command = f"show-secret {secret_id} --reveal --format=json" _, stdout, _ = await ops_test.juju(*complete_command.split()) diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py index f11486c7e..01da2163c 100644 --- a/tests/unit/test_charm.py +++ b/tests/unit/test_charm.py @@ -1,9 +1,14 @@ # Copyright 2023 Canonical Ltd. # See LICENSE file for licensing details. +import logging +import pytest +import re import unittest from unittest import mock -from unittest.mock import call, patch +from unittest.mock import call, patch, MagicMock + +from parameterized import parameterized from charms.operator_libs_linux.v1 import snap from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus, WaitingStatus @@ -38,6 +43,15 @@ def setUp(self, *unused): self.harness.begin() self.peer_rel_id = self.harness.add_relation("database-peers", "database-peers") + @pytest.fixture + def use_caplog(self, caplog): + self._caplog = caplog + + def _setup_secrets(self): + self.harness.set_leader(True) + self.harness.charm._generate_secrets() + self.harness.set_leader(False) + @patch("charm.MongodbOperatorCharm.get_secret") @patch_network_get(private_address="1.1.1.1") @patch("charm.MongoDBConnection") @@ -641,61 +655,230 @@ def test_start_init_user_after_second_call(self, run, config): self.harness.charm._init_operator_user() run.assert_called_once() + def test_get_password(self): + self._setup_secrets() + assert isinstance(self.harness.charm.get_secret("app", "monitor-password"), str) + self.harness.charm.get_secret("app", "non-existing-secret") is None + + self.harness.charm.set_secret("unit", "somekey", "bla") + assert isinstance(self.harness.charm.get_secret("unit", "somekey"), str) + self.harness.charm.get_secret("unit", "non-existing-secret") is None + + def test_set_reset_existing_password_app(self): + """NOTE: currently ops.testing seems to allow for non-leader to set secrets too!""" + self._setup_secrets() + + # Getting current password + self.harness.charm.set_secret("app", "monitor-password", "bla") + assert self.harness.charm.get_secret("app", "monitor-password") == "bla" + + self.harness.charm.set_secret("app", "monitor-password", "blablabla") + assert self.harness.charm.get_secret("app", "monitor-password") == "blablabla" + + @parameterized.expand([("app"), ("unit")]) + def test_set_secret_returning_secret_id(self, scope): + secret_id = self.harness.charm.set_secret(scope, "somekey", "bla") + assert re.match(f"mongodb.{scope}", secret_id) + + @parameterized.expand([("app"), ("unit")]) + def test_set_reset_new_secret(self, scope): + """NOTE: currently ops.testing seems to allow for non-leader to set secrets too!""" + # Getting current password + self.harness.charm.set_secret(scope, "new-secret", "bla") + assert self.harness.charm.get_secret(scope, "new-secret") == "bla" + + # Reset new secret + self.harness.charm.set_secret(scope, "new-secret", "blablabla") + assert self.harness.charm.get_secret(scope, "new-secret") == "blablabla" + + # Set another new secret + self.harness.charm.set_secret(scope, "new-secret2", "blablabla") + assert self.harness.charm.get_secret(scope, "new-secret2") == "blablabla" + + @parameterized.expand([("app"), ("unit")]) + def test_invalid_secret(self, scope): + with self.assertRaises(TypeError): + self.harness.charm.set_secret("unit", "somekey", 1) + + self.harness.charm.set_secret("unit", "somekey", "") + assert self.harness.charm.get_secret(scope, "somekey") is None + + @pytest.mark.usefixtures("use_caplog") + def test_delete_password(self): + """NOTE: currently ops.testing seems to allow for non-leader to remove secrets too!""" + self._setup_secrets() + + assert self.harness.charm.get_secret("app", "monitor-password") + self.harness.charm.remove_secret("app", "monitor-password") + assert self.harness.charm.get_secret("app", "monitor-password") is None + + assert self.harness.charm.set_secret("unit", "somekey", "somesecret") + self.harness.charm.remove_secret("unit", "somekey") + assert self.harness.charm.get_secret("unit", "somekey") is None + + with self._caplog.at_level(logging.ERROR): + self.harness.charm.remove_secret("app", "monitor-password") + assert ( + "Non-existing secret app:monitor-password was attempted to be removed." + in self._caplog.text + ) + + self.harness.charm.remove_secret("unit", "somekey") + assert ( + "Non-existing secret unit:somekey was attempted to be removed." + in self._caplog.text + ) + + self.harness.charm.remove_secret("app", "non-existing-secret") + assert ( + "Non-existing secret app:non-existing-secret was attempted to be removed." + in self._caplog.text + ) + + self.harness.charm.remove_secret("unit", "non-existing-secret") + assert ( + "Non-existing secret unit:non-existing-secret was attempted to be removed." + in self._caplog.text + ) + + @parameterized.expand([("app"), ("unit")]) + @patch("charm.MongodbOperatorCharm._connect_mongodb_exporter") + def test_on_secret_changed(self, scope, connect_exporter): + """NOTE: currently ops.testing seems to allow for non-leader to set secrets too!""" + secret_label = self.harness.charm.set_secret(scope, "new-secret", "bla") + secret = self.harness.charm.model.get_secret(label=secret_label) + + event = mock.Mock() + event.secret = secret + secret_label = self.harness.charm._on_secret_changed(event) + connect_exporter.assert_called() + + @parameterized.expand([("app"), ("unit")]) + @pytest.mark.usefixtures("use_caplog") + @patch("charm.MongodbOperatorCharm._connect_mongodb_exporter") + def test_on_other_secret_changed(self, scope, connect_exporter): + """NOTE: currently ops.testing seems to allow for non-leader to set secrets too!""" + # "Hack": creating a secret outside of the normal MongodbOperatorCharm.set_secret workflow + scope_obj = self.harness.charm._scope_obj(scope) + secret = scope_obj.add_secret({"key": "value"}) + + event = mock.Mock() + event.secret = secret + + with self._caplog.at_level(logging.DEBUG): + self.harness.charm._on_secret_changed(event) + assert f"Secret {secret.id} changed, but it's unknown" in self._caplog.text + + connect_exporter.assert_not_called() + @patch_network_get(private_address="1.1.1.1") @patch("charm.MongoDBConnection") - @patch("charm.MongoDBBackups._get_pbm_status") - def test_set_password(self, pbm_status, connection): - """Tests that a new admin password is generated and is returned to the user.""" + @patch("charm.MongodbOperatorCharm._connect_mongodb_exporter") + def test_connect_to_mongo_exporter_on_set_password(self, connect_exporter, connection): + """Test _connect_mongodb_exporter is called when the password is set for 'montior' user.""" + # container = self.harness.model.unit.get_container("mongod") + # self.harness.set_can_connect(container, True) + # self.harness.charm.on.mongod_pebble_ready.emit(container) self.harness.set_leader(True) - pbm_status.return_value = ActiveStatus("pbm") - original_password = self.harness.charm.get_secret("app", "operator-password") + action_event = mock.Mock() - action_event.params = {} + action_event.params = {"username": "monitor"} self.harness.charm._on_set_password(action_event) - new_password = self.harness.charm.get_secret("app", "operator-password") - - # verify app data is updated and results are reported to user - self.assertNotEqual(original_password, new_password) + connect_exporter.assert_called() @patch_network_get(private_address="1.1.1.1") - @patch("charm.MongoDBConnection") @patch("charm.MongoDBBackups._get_pbm_status") - def test_set_password_provided(self, pbm_status, connection): - """Tests that a given password is set as the new mongodb password.""" + @patch("charm.MongodbOperatorCharm.has_backup_service") + @patch("charm.MongoDBConnection") + @patch("charm.MongodbOperatorCharm._connect_mongodb_exporter") + def test_event_set_password_secrets( + self, connect_exporter, connection, has_backup_service, get_pbm_status + ): + """Test _connect_mongodb_exporter is called when the password is set for 'montior' user. + + Furthermore: in Juju 3.x we want to use secrets + """ + pw = "bla" + has_backup_service.return_value = True + get_pbm_status.return_value = ActiveStatus() self.harness.set_leader(True) - pbm_status.return_value = ActiveStatus("pbm") + action_event = mock.Mock() - action_event.params = {"password": "canonical123"} + action_event.set_results = MagicMock() + action_event.params = {"username": "monitor", "password": pw} self.harness.charm._on_set_password(action_event) - new_password = self.harness.charm.get_secret("app", "operator-password") + connect_exporter.assert_called() - # verify app data is updated and results are reported to user - self.assertEqual("canonical123", new_password) - action_event.set_results.assert_called_with( - {"password": "canonical123", "secret-id": mock.ANY} - ) + action_event.set_results.assert_called() + args_pw_set = action_event.set_results.call_args.args[0] + assert "secret-id" in args_pw_set + + action_event.params = {"username": "monitor"} + self.harness.charm._on_get_password(action_event) + args_pw = action_event.set_results.call_args.args[0] + assert "password" in args_pw + assert args_pw["password"] == pw @patch_network_get(private_address="1.1.1.1") - @patch("charm.MongoDBConnection") @patch("charm.MongoDBBackups._get_pbm_status") - def test_set_password_failure(self, pbm_status, connection): - """Tests failure to reset password does not update app data and failure is reported.""" + @patch("charm.MongodbOperatorCharm.has_backup_service") + @patch("charm.MongoDBConnection") + @patch("charm.MongodbOperatorCharm._connect_mongodb_exporter") + def test_event_auto_reset_password_secrets_when_no_pw_value_shipped( + self, connect_exporter, connection, has_backup_service, get_pbm_status + ): + """Test _connect_mongodb_exporter is called when the password is set for 'montior' user. + + Furthermore: in Juju 3.x we want to use secrets + """ + has_backup_service.return_value = True + get_pbm_status.return_value = ActiveStatus() + self._setup_secrets() self.harness.set_leader(True) - pbm_status.return_value = ActiveStatus("pbm") - original_password = self.harness.charm.get_secret("app", "operator-password") + action_event = mock.Mock() - action_event.params = {} + action_event.set_results = MagicMock() - for exception in [PYMONGO_EXCEPTIONS, NotReadyError]: - connection.return_value.__enter__.return_value.set_user_password.side_effect = ( - exception - ) - self.harness.charm._on_set_password(action_event) - current_password = self.harness.charm.get_secret("app", "operator-password") + # Getting current password + action_event.params = {"username": "monitor"} + self.harness.charm._on_get_password(action_event) + args_pw = action_event.set_results.call_args.args[0] + assert "password" in args_pw + pw1 = args_pw["password"] - # verify passwords are not updated. - self.assertEqual(current_password, original_password) - action_event.fail.assert_called() + # No password value was shipped + action_event.params = {"username": "monitor"} + self.harness.charm._on_set_password(action_event) + connect_exporter.assert_called() + + # New password was generated + action_event.params = {"username": "monitor"} + self.harness.charm._on_get_password(action_event) + args_pw = action_event.set_results.call_args.args[0] + assert "password" in args_pw + pw2 = args_pw["password"] + + # a new password was created + assert pw1 != pw2 + + @patch("charm.MongoDBConnection") + @patch("charm.MongodbOperatorCharm._connect_mongodb_exporter") + def test_event_any_unit_can_get_password_secrets(self, connect_exporter, connection): + """Test _connect_mongodb_exporter is called when the password is set for 'montior' user. + + Furthermore: in Juju 3.x we want to use secrets + """ + self._setup_secrets() + + action_event = mock.Mock() + action_event.set_results = MagicMock() + + # Getting current password + action_event.params = {"username": "monitor"} + self.harness.charm._on_get_password(action_event) + args_pw = action_event.set_results.call_args.args[0] + assert "password" in args_pw + assert args_pw["password"] @patch_network_get(private_address="1.1.1.1") @patch("charm.MongoDBBackups._get_pbm_status")