From 27ce683c0cfbe3da147b1dcfa9a18f76cdfc0b83 Mon Sep 17 00:00:00 2001 From: lucasgameiroborges Date: Tue, 2 Apr 2024 18:08:22 +0000 Subject: [PATCH 01/10] first version --- tests/unit/test_charm.py | 4348 +++++++++++++++++++------------------- 1 file changed, 2154 insertions(+), 2194 deletions(-) diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py index c925274d6f..845893547e 100644 --- a/tests/unit/test_charm.py +++ b/tests/unit/test_charm.py @@ -41,2247 +41,2207 @@ # @pytest.mark.usefixtures("juju_has_secrets") -class TestCharm(unittest.TestCase): - def setUp(self): - self._peer_relation = PEER - self._postgresql_container = "postgresql" - - self.harness = Harness(PostgresqlOperatorCharm) - self.addCleanup(self.harness.cleanup) - self.harness.begin() - self.charm = self.harness.charm - self.rel_id = self.harness.add_relation(self._peer_relation, self.charm.app.name) - self.harness.add_relation("upgrade", self.charm.app.name) - - @pytest.fixture - def use_caplog(self, caplog): - self._caplog = caplog - - @patch_network_get(private_address="1.1.1.1") - @patch("charm.subprocess.check_call") - @patch("charm.snap.SnapCache") - @patch("charm.PostgresqlOperatorCharm._install_snap_packages") - @patch("charm.PostgresqlOperatorCharm._reboot_on_detached_storage") - @patch( - "charm.PostgresqlOperatorCharm._is_storage_attached", - side_effect=[False, True, True], - ) - def test_on_install( - self, - _is_storage_attached, - _reboot_on_detached_storage, - _install_snap_packages, - _snap_cache, - _check_call, - ): - # Test without storage. - self.charm.on.install.emit() - _reboot_on_detached_storage.assert_called_once() - pg_snap = _snap_cache.return_value[POSTGRESQL_SNAP_NAME] - - # Test without adding Patroni resource. - self.charm.on.install.emit() - # Assert that the needed calls were made. - _install_snap_packages.assert_called_once_with(packages=SNAP_PACKAGES) - assert pg_snap.alias.call_count == 2 - pg_snap.alias.assert_any_call("psql") - pg_snap.alias.assert_any_call("patronictl") - - assert _check_call.call_count == 3 - _check_call.assert_any_call("mkdir -p /home/snap_daemon".split()) - _check_call.assert_any_call("chown snap_daemon:snap_daemon /home/snap_daemon".split()) - _check_call.assert_any_call("usermod -d /home/snap_daemon snap_daemon".split()) - - # Assert the status set by the event handler. - self.assertTrue(isinstance(self.harness.model.unit.status, WaitingStatus)) - - @patch_network_get(private_address="1.1.1.1") - @patch("charm.logger.exception") - @patch("charm.subprocess.check_call") - @patch("charm.snap.SnapCache") - @patch("charm.PostgresqlOperatorCharm._install_snap_packages") - @patch("charm.PostgresqlOperatorCharm._reboot_on_detached_storage") - @patch( - "charm.PostgresqlOperatorCharm._is_storage_attached", - side_effect=[False, True, True], - ) - def test_on_install_failed_to_create_home( - self, - _is_storage_attached, - _reboot_on_detached_storage, - _install_snap_packages, - _snap_cache, - _check_call, - _logger_exception, - ): - # Test without storage. - self.charm.on.install.emit() - _reboot_on_detached_storage.assert_called_once() - pg_snap = _snap_cache.return_value[POSTGRESQL_SNAP_NAME] - _check_call.side_effect = [subprocess.CalledProcessError(-1, ["test"])] - - # Test without adding Patroni resource. - self.charm.on.install.emit() - # Assert that the needed calls were made. - _install_snap_packages.assert_called_once_with(packages=SNAP_PACKAGES) - assert pg_snap.alias.call_count == 2 - pg_snap.alias.assert_any_call("psql") - pg_snap.alias.assert_any_call("patronictl") - - _logger_exception.assert_called_once_with("Unable to create snap_daemon home dir") - - # Assert the status set by the event handler. - self.assertTrue(isinstance(self.harness.model.unit.status, WaitingStatus)) - - @patch_network_get(private_address="1.1.1.1") - @patch("charm.PostgresqlOperatorCharm._install_snap_packages") - @patch("charm.PostgresqlOperatorCharm._is_storage_attached", return_value=True) - def test_on_install_snap_failure( - self, - _is_storage_attached, - _install_snap_packages, - ): - # Mock the result of the call. - _install_snap_packages.side_effect = snap.SnapError - # Trigger the hook. - self.charm.on.install.emit() - # Assert that the needed calls were made. - _install_snap_packages.assert_called_once() - self.assertTrue(isinstance(self.harness.model.unit.status, BlockedStatus)) - - @patch_network_get(private_address="1.1.1.1") - def test_patroni_scrape_config_no_tls(self): - result = self.charm.patroni_scrape_config() - - assert result == [ - { - "metrics_path": "/metrics", - "scheme": "http", - "static_configs": [{"targets": ["1.1.1.1:8008"]}], - "tls_config": {"insecure_skip_verify": True}, - }, - ] - @patch_network_get(private_address="1.1.1.1") - @patch( - "charm.PostgresqlOperatorCharm.is_tls_enabled", - return_value=True, - new_callable=PropertyMock, - ) - def test_patroni_scrape_config_tls(self, _): - result = self.charm.patroni_scrape_config() +@pytest.fixture +def harness(): + harness = Harness(PostgresqlOperatorCharm) + harness.begin() + #rel_id = harness.add_relation(PEER, harness.charm.app.name) + harness.add_relation(PEER, harness.charm.app.name) + harness.add_relation("upgrade", harness.charm.app.name) + yield harness + harness.cleanup() + +@pytest.fixture +def use_caplog(self, caplog): + self._caplog = caplog + +@patch_network_get(private_address="1.1.1.1") +@patch("charm.subprocess.check_call") +@patch("charm.snap.SnapCache") +@patch("charm.PostgresqlOperatorCharm._install_snap_packages") +@patch("charm.PostgresqlOperatorCharm._reboot_on_detached_storage") +@patch( + "charm.PostgresqlOperatorCharm._is_storage_attached", + side_effect=[False, True, True], +) +def test_on_install( + self, + _is_storage_attached, + _reboot_on_detached_storage, + _install_snap_packages, + _snap_cache, + _check_call, +): + # Test without storage. + harness.charm.on.install.emit() + _reboot_on_detached_storage.assert_called_once() + pg_snap = _snap_cache.return_value[POSTGRESQL_SNAP_NAME] + + # Test without adding Patroni resource. + harness.charm.on.install.emit() + # Assert that the needed calls were made. + _install_snap_packages.assert_called_once_with(packages=SNAP_PACKAGES) + assert pg_snap.alias.call_count == 2 + pg_snap.alias.assert_any_call("psql") + pg_snap.alias.assert_any_call("patronictl") + + assert _check_call.call_count == 3 + _check_call.assert_any_call("mkdir -p /home/snap_daemon".split()) + _check_call.assert_any_call("chown snap_daemon:snap_daemon /home/snap_daemon".split()) + _check_call.assert_any_call("usermod -d /home/snap_daemon snap_daemon".split()) + + # Assert the status set by the event handler. + assert (isinstance(harness.model.unit.status, WaitingStatus)) + +@patch_network_get(private_address="1.1.1.1") +@patch("charm.logger.exception") +@patch("charm.subprocess.check_call") +@patch("charm.snap.SnapCache") +@patch("charm.PostgresqlOperatorCharm._install_snap_packages") +@patch("charm.PostgresqlOperatorCharm._reboot_on_detached_storage") +@patch( + "charm.PostgresqlOperatorCharm._is_storage_attached", + side_effect=[False, True, True], +) +def test_on_install_failed_to_create_home( + self, + _is_storage_attached, + _reboot_on_detached_storage, + _install_snap_packages, + _snap_cache, + _check_call, + _logger_exception, +): + # Test without storage. + harness.charm.on.install.emit() + _reboot_on_detached_storage.assert_called_once() + pg_snap = _snap_cache.return_value[POSTGRESQL_SNAP_NAME] + _check_call.side_effect = [subprocess.CalledProcessError(-1, ["test"])] + + # Test without adding Patroni resource. + harness.charm.on.install.emit() + # Assert that the needed calls were made. + _install_snap_packages.assert_called_once_with(packages=SNAP_PACKAGES) + assert pg_snap.alias.call_count == 2 + pg_snap.alias.assert_any_call("psql") + pg_snap.alias.assert_any_call("patronictl") + + _logger_exception.assert_called_once_with("Unable to create snap_daemon home dir") + + # Assert the status set by the event handler. + assert (isinstance(harness.model.unit.status, WaitingStatus)) + +@patch_network_get(private_address="1.1.1.1") +@patch("charm.PostgresqlOperatorCharm._install_snap_packages") +@patch("charm.PostgresqlOperatorCharm._is_storage_attached", return_value=True) +def test_on_install_snap_failure( + self, + _is_storage_attached, + _install_snap_packages, +): + # Mock the result of the call. + _install_snap_packages.side_effect = snap.SnapError + # Trigger the hook. + harness.charm.on.install.emit() + # Assert that the needed calls were made. + _install_snap_packages.assert_called_once() + assert (isinstance(harness.model.unit.status, BlockedStatus)) + +@patch_network_get(private_address="1.1.1.1") +def test_patroni_scrape_config_no_tls(self): + result = harness.charm.patroni_scrape_config() + + assert result == [ + { + "metrics_path": "/metrics", + "scheme": "http", + "static_configs": [{"targets": ["1.1.1.1:8008"]}], + "tls_config": {"insecure_skip_verify": True}, + }, + ] + +@patch_network_get(private_address="1.1.1.1") +@patch( + "charm.PostgresqlOperatorCharm.is_tls_enabled", + return_value=True, + new_callable=PropertyMock, +) +def test_patroni_scrape_config_tls(self, _): + result = harness.charm.patroni_scrape_config() + + assert result == [ + { + "metrics_path": "/metrics", + "scheme": "https", + "static_configs": [{"targets": ["1.1.1.1:8008"]}], + "tls_config": {"insecure_skip_verify": True}, + }, + ] + +@patch( + "charm.PostgresqlOperatorCharm._units_ips", + new_callable=PropertyMock, + return_value={"1.1.1.1", "1.1.1.2"}, +) +@patch("charm.PostgresqlOperatorCharm._patroni", new_callable=PropertyMock) +def test_primary_endpoint(self, _patroni, _): + _patroni.return_value.get_member_ip.return_value = "1.1.1.1" + _patroni.return_value.get_primary.return_value = sentinel.primary + assert harness.charm.primary_endpoint == "1.1.1.1" + + _patroni.return_value.get_member_ip.assert_called_once_with(sentinel.primary) + _patroni.return_value.get_primary.assert_called_once_with() + +@patch("charm.PostgresqlOperatorCharm._peers", new_callable=PropertyMock, return_value=None) +@patch( + "charm.PostgresqlOperatorCharm._units_ips", + new_callable=PropertyMock, + return_value={"1.1.1.1", "1.1.1.2"}, +) +@patch("charm.PostgresqlOperatorCharm._patroni", new_callable=PropertyMock) +def test_primary_endpoint_no_peers(self, _patroni, _, __): + assert harness.charm.primary_endpoint is None - assert result == [ - { - "metrics_path": "/metrics", - "scheme": "https", - "static_configs": [{"targets": ["1.1.1.1:8008"]}], - "tls_config": {"insecure_skip_verify": True}, - }, - ] + assert not _patroni.return_value.get_member_ip.called + assert not _patroni.return_value.get_primary.called - @patch( - "charm.PostgresqlOperatorCharm._units_ips", - new_callable=PropertyMock, - return_value={"1.1.1.1", "1.1.1.2"}, +@patch("charm.PostgresqlOperatorCharm._update_relation_endpoints", new_callable=PropertyMock) +@patch( + "charm.PostgresqlOperatorCharm.primary_endpoint", + new_callable=PropertyMock, +) +@patch("charm.PostgresqlOperatorCharm.update_config") +@patch_network_get(private_address="1.1.1.1") +def test_on_leader_elected( + self, _update_config, _primary_endpoint, _update_relation_endpoints +): + # Assert that there is no password in the peer relation. + assert harness.charm._peers.data[harness.charm.app].get("operator-password", None) == None + + # Check that a new password was generated on leader election. + _primary_endpoint.return_value = "1.1.1.1" + harness.set_leader() + password = harness.charm._peers.data[harness.charm.app].get("operator-password", None) + _update_config.assert_called_once() + _update_relation_endpoints.assert_not_called() + assert password != None + + # Mark the cluster as initialised. + harness.charm._peers.data[harness.charm.app].update({"cluster_initialised": "True"}) + + # Trigger a new leader election and check that the password is still the same + # and also that update_endpoints was called after the cluster was initialised. + harness.set_leader(False) + harness.set_leader() + assert harness.charm._peers.data[harness.charm.app].get("operator-password", None) == password + _update_relation_endpoints.assert_called_once() + assert not (isinstance(harness.model.unit.status, BlockedStatus)) + + # Check for a WaitingStatus when the primary is not reachable yet. + _primary_endpoint.return_value = None + harness.set_leader(False) + harness.set_leader() + _update_relation_endpoints.assert_called_once() # Assert it was not called again. + assert (isinstance(harness.model.unit.status, WaitingStatus)) + +def test_is_cluster_initialised(self): + # Test when the cluster was not initialised yet. + assert not (harness.charm.is_cluster_initialised) + + # Test when the cluster was already initialised. + with harness.hooks_disabled(): + harness.update_relation_data( + self.rel_id, harness.charm.app.name, {"cluster_initialised": "True"} + ) + assert (harness.charm.is_cluster_initialised) + +@patch("charm.PostgresqlOperatorCharm._validate_config_options") +@patch("charm.PostgresqlOperatorCharm.update_config") +@patch("relations.db.DbProvides.set_up_relation") +@patch("charm.PostgresqlOperatorCharm.enable_disable_extensions") +@patch("charm.PostgresqlOperatorCharm.is_cluster_initialised", new_callable=PropertyMock) +def test_on_config_changed( + self, + _is_cluster_initialised, + _enable_disable_extensions, + _set_up_relation, + _update_config, + _validate_config_options, +): + # Test when the cluster was not initialised yet. + _is_cluster_initialised.return_value = False + harness.charm.on.config_changed.emit() + _enable_disable_extensions.assert_not_called() + _set_up_relation.assert_not_called() + + # Test when the unit is not the leader. + _is_cluster_initialised.return_value = True + harness.charm.on.config_changed.emit() + _validate_config_options.assert_called_once() + _enable_disable_extensions.assert_not_called() + _set_up_relation.assert_not_called() + + # Test unable to connect to db + _update_config.reset_mock() + _validate_config_options.side_effect = OperationalError + harness.charm.on.config_changed.emit() + assert not _update_config.called + _validate_config_options.side_effect = None + + # Test after the cluster was initialised. + with harness.hooks_disabled(): + harness.set_leader() + harness.charm.on.config_changed.emit() + _enable_disable_extensions.assert_called_once() + _set_up_relation.assert_not_called() + + # Test when the unit is in a blocked state due to extensions request, + # but there are no established legacy relations. + _enable_disable_extensions.reset_mock() + harness.charm.unit.status = BlockedStatus( + "extensions requested through relation, enable them through config options" ) - @patch("charm.PostgresqlOperatorCharm._patroni", new_callable=PropertyMock) - def test_primary_endpoint(self, _patroni, _): - _patroni.return_value.get_member_ip.return_value = "1.1.1.1" - _patroni.return_value.get_primary.return_value = sentinel.primary - assert self.charm.primary_endpoint == "1.1.1.1" - - _patroni.return_value.get_member_ip.assert_called_once_with(sentinel.primary) - _patroni.return_value.get_primary.assert_called_once_with() - - @patch("charm.PostgresqlOperatorCharm._peers", new_callable=PropertyMock, return_value=None) - @patch( - "charm.PostgresqlOperatorCharm._units_ips", - new_callable=PropertyMock, - return_value={"1.1.1.1", "1.1.1.2"}, + harness.charm.on.config_changed.emit() + _enable_disable_extensions.assert_called_once() + _set_up_relation.assert_not_called() + + # Test when the unit is in a blocked state due to extensions request, + # but there are established legacy relations. + _enable_disable_extensions.reset_mock() + _set_up_relation.return_value = False + db_relation_id = harness.add_relation("db", "application") + harness.charm.on.config_changed.emit() + _enable_disable_extensions.assert_called_once() + _set_up_relation.assert_called_once() + harness.remove_relation(db_relation_id) + + _enable_disable_extensions.reset_mock() + _set_up_relation.reset_mock() + harness.add_relation("db-admin", "application") + harness.charm.on.config_changed.emit() + _enable_disable_extensions.assert_called_once() + _set_up_relation.assert_called_once() + + # Test when there are established legacy relations, + # but the charm fails to set up one of them. + _enable_disable_extensions.reset_mock() + _set_up_relation.reset_mock() + _set_up_relation.return_value = False + harness.add_relation("db", "application") + harness.charm.on.config_changed.emit() + _enable_disable_extensions.assert_called_once() + _set_up_relation.assert_called_once() + +@patch("subprocess.check_output", return_value=b"C") +def test_check_extension_dependencies(self, _): + with patch.object(PostgresqlOperatorCharm, "postgresql", Mock()) as _: + # Test when plugins dependencies exception is not caused + config = { + "plugin_address_standardizer_enable": False, + "plugin_postgis_enable": False, + "plugin_address_standardizer_data_us_enable": False, + "plugin_jsonb_plperl_enable": False, + "plugin_plperl_enable": False, + "plugin_postgis_raster_enable": False, + "plugin_postgis_tiger_geocoder_enable": False, + "plugin_fuzzystrmatch_enable": False, + "plugin_postgis_topology_enable": False, + } + harness.update_config(config) + harness.charm.enable_disable_extensions() + assert not (isinstance(harness.model.unit.status, BlockedStatus)) + + # Test when plugins dependencies exception caused + config["plugin_address_standardizer_enable"] = True + harness.update_config(config) + harness.charm.enable_disable_extensions() + assert (isinstance(harness.model.unit.status, BlockedStatus)) + assert harness.model.unit.status.message == EXTENSIONS_DEPENDENCY_MESSAGE + +@patch("subprocess.check_output", return_value=b"C") +def test_enable_disable_extensions(self, _): + with patch.object(PostgresqlOperatorCharm, "postgresql", Mock()) as postgresql_mock: + # Test when all extensions install/uninstall succeed. + postgresql_mock.enable_disable_extension.side_effect = None + with self.assertNoLogs("charm", "ERROR"): + harness.charm.enable_disable_extensions() + assert postgresql_mock.enable_disable_extensions.call_count == 1 + + # Test when one extension install/uninstall fails. + postgresql_mock.reset_mock() + postgresql_mock.enable_disable_extensions.side_effect = ( + PostgreSQLEnableDisableExtensionError + ) + with self.assertLogs("charm", "ERROR") as logs: + harness.charm.enable_disable_extensions() + assert postgresql_mock.enable_disable_extensions.call_count == 1 + assert "failed to change plugins" in logs.output + + # Test when one config option should be skipped (because it's not related + # to a plugin/extension). + postgresql_mock.reset_mock() + postgresql_mock.enable_disable_extensions.side_effect = None + with self.assertNoLogs("charm", "ERROR"): + config = """options: +plugin_citext_enable: +default: false +type: boolean +plugin_hstore_enable: +default: false +type: boolean +plugin_pg_trgm_enable: +default: false +type: boolean +plugin_plpython3u_enable: +default: false +type: boolean +plugin_unaccent_enable: +default: false +type: boolean +plugin_debversion_enable: +default: false +type: boolean +plugin_bloom_enable: +default: false +type: boolean +plugin_btree_gin_enable: +default: false +type: boolean +plugin_btree_gist_enable: +default: false +type: boolean +plugin_cube_enable: +default: false +type: boolean +plugin_dict_int_enable: +default: false +type: boolean +plugin_dict_xsyn_enable: +default: false +type: boolean +plugin_earthdistance_enable: +default: false +type: boolean +plugin_fuzzystrmatch_enable: +default: false +type: boolean +plugin_intarray_enable: +default: false +type: boolean +plugin_isn_enable: +default: false +type: boolean +plugin_lo_enable: +default: false +type: boolean +plugin_ltree_enable: +default: false +type: boolean +plugin_old_snapshot_enable: +default: false +type: boolean +plugin_pg_freespacemap_enable: +default: false +type: boolean +plugin_pgrowlocks_enable: +default: false +type: boolean +plugin_pgstattuple_enable: +default: false +type: boolean +plugin_pg_visibility_enable: +default: false +type: boolean +plugin_seg_enable: +default: false +type: boolean +plugin_tablefunc_enable: +default: false +type: boolean +plugin_tcn_enable: +default: false +type: boolean +plugin_tsm_system_rows_enable: +default: false +type: boolean +plugin_tsm_system_time_enable: +default: false +type: boolean +plugin_uuid_ossp_enable: +default: false +type: boolean +plugin_spi_enable: +default: false +type: boolean +plugin_bool_plperl_enable: +default: false +type: boolean +plugin_hll_enable: +default: false +type: boolean +plugin_hypopg_enable: +default: false +type: boolean +plugin_ip4r_enable: +default: false +type: boolean +plugin_plperl_enable: +default: false +type: boolean +plugin_jsonb_plperl_enable: +default: false +type: boolean +plugin_orafce_enable: +default: false +type: boolean +plugin_pg_similarity_enable: +default: false +type: boolean +plugin_prefix_enable: +default: false +type: boolean +plugin_rdkit_enable: +default: false +type: boolean +plugin_tds_fdw_enable: +default: false +type: boolean +plugin_icu_ext_enable: +default: false +type: boolean +plugin_pltcl_enable: +default: false +type: boolean +plugin_postgis_enable: +default: false +type: boolean +plugin_postgis_raster_enable: +default: false +type: boolean +plugin_address_standardizer_enable: +default: false +type: boolean +plugin_address_standardizer_data_us_enable: +default: false +type: boolean +plugin_postgis_tiger_geocoder_enable: +default: false +type: boolean +plugin_postgis_topology_enable: +default: false +type: boolean +plugin_vector_enable: +default: false +type: boolean +profile: +default: production +type: string""" + harness = Harness(PostgresqlOperatorCharm, config=config) + self.addCleanup(harness.cleanup) + harness.begin() + harness.charm.enable_disable_extensions() + assert postgresql_mock.enable_disable_extensions.call_count == 1 + +@patch("charm.PostgresqlOperatorCharm.enable_disable_extensions") +@patch("charm.snap.SnapCache") +@patch("charm.Patroni.get_postgresql_version") +@patch_network_get(private_address="1.1.1.1") +@patch("charm.PostgreSQLProvider.oversee_users") +@patch("charm.PostgresqlOperatorCharm._update_relation_endpoints", new_callable=PropertyMock) +@patch("charm.PostgresqlOperatorCharm.postgresql") +@patch("charm.PostgreSQLProvider.update_endpoints") +@patch("charm.PostgresqlOperatorCharm.update_config") +@patch( + "charm.Patroni.member_started", + new_callable=PropertyMock, +) +@patch("charm.Patroni.bootstrap_cluster") +@patch("charm.PostgresqlOperatorCharm._replication_password") +@patch("charm.PostgresqlOperatorCharm._get_password") +@patch("charm.PostgresqlOperatorCharm._reboot_on_detached_storage") +@patch("upgrade.PostgreSQLUpgrade.idle", return_value=True) +@patch( + "charm.PostgresqlOperatorCharm._is_storage_attached", + side_effect=[False, True, True, True, True], +) +def test_on_start( + self, + _is_storage_attached, + _idle, + _reboot_on_detached_storage, + _get_password, + _replication_password, + _bootstrap_cluster, + _member_started, + _, + __, + _postgresql, + _update_relation_endpoints, + _oversee_users, + _get_postgresql_version, + _snap_cache, + _enable_disable_extensions, +): + _get_postgresql_version.return_value = "14.0" + + # Test without storage. + harness.charm.on.start.emit() + _reboot_on_detached_storage.assert_called_once() + + # Test before the passwords are generated. + _member_started.return_value = False + _get_password.return_value = None + harness.charm.on.start.emit() + _bootstrap_cluster.assert_not_called() + assert (isinstance(harness.model.unit.status, WaitingStatus)) + + # Mock the passwords. + _get_password.return_value = "fake-operator-password" + _replication_password.return_value = "fake-replication-password" + + # Mock cluster start and postgres user creation success values. + _bootstrap_cluster.side_effect = [False, True, True] + _postgresql.list_users.side_effect = [[], [], []] + _postgresql.create_user.side_effect = [PostgreSQLCreateUserError, None, None, None] + + # Test for a failed cluster bootstrapping. + # TODO: test replicas start (DPE-494). + harness.set_leader() + harness.charm.on.start.emit() + _bootstrap_cluster.assert_called_once() + _oversee_users.assert_not_called() + assert (isinstance(harness.model.unit.status, BlockedStatus)) + # Set an initial waiting status (like after the install hook was triggered). + harness.model.unit.status = WaitingStatus("fake message") + + # Test the event of an error happening when trying to create the default postgres user. + _member_started.return_value = True + harness.charm.on.start.emit() + _postgresql.create_user.assert_called_once() + _oversee_users.assert_not_called() + assert (isinstance(harness.model.unit.status, BlockedStatus)) + + # Set an initial waiting status again (like after the install hook was triggered). + harness.model.unit.status = WaitingStatus("fake message") + + # Then test the event of a correct cluster bootstrapping. + harness.charm.on.start.emit() + assert _postgresql.create_user.call_count == 4 # Considering the previous failed call. + _oversee_users.assert_called_once() + _enable_disable_extensions.assert_called_once() + assert (isinstance(harness.model.unit.status, ActiveStatus)) + +@patch("charm.snap.SnapCache") +@patch("charm.Patroni.get_postgresql_version") +@patch_network_get(private_address="1.1.1.1") +@patch("charm.Patroni.configure_patroni_on_unit") +@patch( + "charm.Patroni.member_started", + new_callable=PropertyMock, +) +@patch("charm.PostgresqlOperatorCharm._update_relation_endpoints", new_callable=PropertyMock) +@patch.object(EventBase, "defer") +@patch("charm.PostgresqlOperatorCharm._replication_password") +@patch("charm.PostgresqlOperatorCharm._get_password") +@patch("upgrade.PostgreSQLUpgrade.idle", return_value=True) +@patch( + "charm.PostgresqlOperatorCharm._is_storage_attached", + return_value=True, +) +def test_on_start_replica( + self, + _is_storage_attached, + _idle, + _get_password, + _replication_password, + _defer, + _update_relation_endpoints, + _member_started, + _configure_patroni_on_unit, + _get_postgresql_version, + _snap_cache, +): + _get_postgresql_version.return_value = "14.0" + + # Set the current unit to be a replica (non leader unit). + harness.set_leader(False) + + # Mock the passwords. + _get_password.return_value = "fake-operator-password" + _replication_password.return_value = "fake-replication-password" + + # Test an uninitialized cluster. + harness.charm._peers.data[harness.charm.app].update({"cluster_initialised": ""}) + harness.charm.on.start.emit() + _defer.assert_called_once() + + # Set an initial waiting status again (like after a machine restart). + harness.model.unit.status = WaitingStatus("fake message") + + # Mark the cluster as initialised and with the workload up and running. + harness.charm._peers.data[harness.charm.app].update({"cluster_initialised": "True"}) + _member_started.return_value = True + harness.charm.on.start.emit() + _configure_patroni_on_unit.assert_not_called() + assert (isinstance(harness.model.unit.status, ActiveStatus)) + + # Set an initial waiting status (like after the install hook was triggered). + harness.model.unit.status = WaitingStatus("fake message") + + # Check that the unit status doesn't change when the workload is not running. + # In that situation only Patroni is configured in the unit (but not started). + _member_started.return_value = False + harness.charm.on.start.emit() + _configure_patroni_on_unit.assert_called_once() + assert (isinstance(harness.model.unit.status, WaitingStatus)) + +@patch_network_get(private_address="1.1.1.1") +@patch("subprocess.check_output", return_value=b"C") +@patch("charm.snap.SnapCache") +@patch("charm.PostgresqlOperatorCharm.postgresql") +@patch("charm.Patroni") +@patch("charm.PostgresqlOperatorCharm._get_password") +@patch("upgrade.PostgreSQLUpgrade.idle", return_value=True) +@patch("charm.PostgresqlOperatorCharm._is_storage_attached", return_value=True) +def test_on_start_no_patroni_member( + self, + _is_storage_attached, + _idle, + _get_password, + patroni, + _postgresql, + _snap_cache, + _, +): + # Mock the passwords. + patroni.return_value.member_started = False + _get_password.return_value = "fake-operator-password" + bootstrap_cluster = patroni.return_value.bootstrap_cluster + bootstrap_cluster.return_value = True + + patroni.return_value.get_postgresql_version.return_value = "14.0" + + harness.set_leader() + harness.charm.on.start.emit() + bootstrap_cluster.assert_called_once() + _postgresql.create_user.assert_not_called() + assert (isinstance(harness.model.unit.status, WaitingStatus)) + assert harness.model.unit.status.message == "awaiting for member to start" + +@patch("charm.Patroni.bootstrap_cluster") +@patch("charm.PostgresqlOperatorCharm._replication_password") +@patch("charm.PostgresqlOperatorCharm._get_password") +@patch("charm.PostgresqlOperatorCharm._is_storage_attached", return_value=True) +def test_on_start_after_blocked_state( + self, _is_storage_attached, _get_password, _replication_password, _bootstrap_cluster +): + # Set an initial blocked status (like after the install hook was triggered). + initial_status = BlockedStatus("fake message") + harness.model.unit.status = initial_status + + # Test for a failed cluster bootstrapping. + harness.charm.on.start.emit() + _get_password.assert_not_called() + _replication_password.assert_not_called() + _bootstrap_cluster.assert_not_called() + # Assert the status didn't change. + assert harness.model.unit.status == initial_status + +@patch_network_get(private_address="1.1.1.1") +@patch("charm.PostgresqlOperatorCharm.update_config") +def test_on_get_password(self, _): + # Create a mock event and set passwords in peer relation data. + harness.set_leader(True) + mock_event = MagicMock(params={}) + harness.update_relation_data( + self.rel_id, + harness.charm.app.name, + { + "operator-password": "test-password", + "replication-password": "replication-test-password", + }, ) - @patch("charm.PostgresqlOperatorCharm._patroni", new_callable=PropertyMock) - def test_primary_endpoint_no_peers(self, _patroni, _, __): - assert self.charm.primary_endpoint is None - assert not _patroni.return_value.get_member_ip.called - assert not _patroni.return_value.get_primary.called + # Test providing an invalid username. + mock_event.params["username"] = "user" + harness.charm._on_get_password(mock_event) + mock_event.fail.assert_called_once() + mock_event.set_results.assert_not_called() + + # Test without providing the username option. + mock_event.reset_mock() + del mock_event.params["username"] + harness.charm._on_get_password(mock_event) + mock_event.set_results.assert_called_once_with({"password": "test-password"}) + + # Also test providing the username option. + mock_event.reset_mock() + mock_event.params["username"] = "replication" + harness.charm._on_get_password(mock_event) + mock_event.set_results.assert_called_once_with({"password": "replication-test-password"}) + +@patch_network_get(private_address="1.1.1.1") +@patch("charm.PostgresqlOperatorCharm.update_config") +@patch("charm.PostgresqlOperatorCharm.set_secret") +@patch("charm.PostgresqlOperatorCharm.postgresql") +@patch("charm.Patroni.are_all_members_ready") +@patch("charm.PostgresqlOperatorCharm._on_leader_elected") +def test_on_set_password( + self, + _, + _are_all_members_ready, + _postgresql, + _set_secret, + __, +): + # Create a mock event. + mock_event = MagicMock(params={}) + + # Set some values for the other mocks. + _are_all_members_ready.side_effect = [False, True, True, True, True] + _postgresql.update_user_password = PropertyMock( + side_effect=[PostgreSQLUpdateUserPasswordError, None, None, None] + ) - @patch("charm.PostgresqlOperatorCharm._update_relation_endpoints", new_callable=PropertyMock) - @patch( - "charm.PostgresqlOperatorCharm.primary_endpoint", - new_callable=PropertyMock, + # Test trying to set a password through a non leader unit. + harness.charm._on_set_password(mock_event) + mock_event.fail.assert_called_once() + _set_secret.assert_not_called() + + # Test providing an invalid username. + harness.set_leader() + mock_event.reset_mock() + mock_event.params["username"] = "user" + harness.charm._on_set_password(mock_event) + mock_event.fail.assert_called_once() + _set_secret.assert_not_called() + + # Test without providing the username option but without all cluster members ready. + mock_event.reset_mock() + del mock_event.params["username"] + harness.charm._on_set_password(mock_event) + mock_event.fail.assert_called_once() + _set_secret.assert_not_called() + + # Test for an error updating when updating the user password in the database. + mock_event.reset_mock() + harness.charm._on_set_password(mock_event) + mock_event.fail.assert_called_once() + _set_secret.assert_not_called() + + # Test without providing the username option. + harness.charm._on_set_password(mock_event) + assert _set_secret.call_args_list[0][0][1] == "operator-password" + + # Also test providing the username option. + _set_secret.reset_mock() + mock_event.params["username"] = "replication" + harness.charm._on_set_password(mock_event) + assert _set_secret.call_args_list[0][0][1] == "replication-password" + + # And test providing both the username and password options. + _set_secret.reset_mock() + mock_event.params["password"] = "replication-test-password" + harness.charm._on_set_password(mock_event) + _set_secret.assert_called_once_with( + "app", "replication-password", "replication-test-password" ) - @patch("charm.PostgresqlOperatorCharm.update_config") - @patch_network_get(private_address="1.1.1.1") - def test_on_leader_elected( - self, _update_config, _primary_endpoint, _update_relation_endpoints - ): - # Assert that there is no password in the peer relation. - self.assertIsNone(self.charm._peers.data[self.charm.app].get("operator-password", None)) - - # Check that a new password was generated on leader election. - _primary_endpoint.return_value = "1.1.1.1" - self.harness.set_leader() - password = self.charm._peers.data[self.charm.app].get("operator-password", None) - _update_config.assert_called_once() - _update_relation_endpoints.assert_not_called() - self.assertIsNotNone(password) - - # Mark the cluster as initialised. - self.charm._peers.data[self.charm.app].update({"cluster_initialised": "True"}) - - # Trigger a new leader election and check that the password is still the same - # and also that update_endpoints was called after the cluster was initialised. - self.harness.set_leader(False) - self.harness.set_leader() - self.assertEqual( - self.charm._peers.data[self.charm.app].get("operator-password", None), password + +@patch_network_get(private_address="1.1.1.1") +@patch("charm.ClusterTopologyObserver.start_observer") +@patch("charm.PostgresqlOperatorCharm._set_primary_status_message") +@patch("charm.Patroni.restart_patroni") +@patch("charm.Patroni.is_member_isolated") +@patch("charm.Patroni.reinitialize_postgresql") +@patch("charm.Patroni.member_replication_lag", new_callable=PropertyMock) +@patch("charm.Patroni.member_started", new_callable=PropertyMock) +@patch("charm.PostgresqlOperatorCharm._update_relation_endpoints") +@patch( + "charm.PostgresqlOperatorCharm.primary_endpoint", + new_callable=PropertyMock(return_value=True), +) +@patch("charm.PostgreSQLProvider.oversee_users") +@patch("upgrade.PostgreSQLUpgrade.idle", return_value=True) +def test_on_update_status( + self, + _, + _oversee_users, + _primary_endpoint, + _update_relation_endpoints, + _member_started, + _member_replication_lag, + _reinitialize_postgresql, + _is_member_isolated, + _restart_patroni, + _set_primary_status_message, + _start_observer, +): + # Test before the cluster is initialised. + harness.charm.on.update_status.emit() + _set_primary_status_message.assert_not_called() + + # Test after the cluster was initialised, but with the unit in a blocked state. + with harness.hooks_disabled(): + harness.update_relation_data( + self.rel_id, harness.charm.app.name, {"cluster_initialised": "True"} ) - _update_relation_endpoints.assert_called_once() - self.assertFalse(isinstance(self.harness.model.unit.status, BlockedStatus)) - - # Check for a WaitingStatus when the primary is not reachable yet. - _primary_endpoint.return_value = None - self.harness.set_leader(False) - self.harness.set_leader() - _update_relation_endpoints.assert_called_once() # Assert it was not called again. - self.assertTrue(isinstance(self.harness.model.unit.status, WaitingStatus)) - - def test_is_cluster_initialised(self): - # Test when the cluster was not initialised yet. - self.assertFalse(self.charm.is_cluster_initialised) - - # Test when the cluster was already initialised. - with self.harness.hooks_disabled(): - self.harness.update_relation_data( - self.rel_id, self.charm.app.name, {"cluster_initialised": "True"} - ) - self.assertTrue(self.charm.is_cluster_initialised) - - @patch("charm.PostgresqlOperatorCharm._validate_config_options") - @patch("charm.PostgresqlOperatorCharm.update_config") - @patch("relations.db.DbProvides.set_up_relation") - @patch("charm.PostgresqlOperatorCharm.enable_disable_extensions") - @patch("charm.PostgresqlOperatorCharm.is_cluster_initialised", new_callable=PropertyMock) - def test_on_config_changed( - self, - _is_cluster_initialised, - _enable_disable_extensions, - _set_up_relation, - _update_config, - _validate_config_options, - ): - # Test when the cluster was not initialised yet. - _is_cluster_initialised.return_value = False - self.charm.on.config_changed.emit() - _enable_disable_extensions.assert_not_called() - _set_up_relation.assert_not_called() - - # Test when the unit is not the leader. - _is_cluster_initialised.return_value = True - self.charm.on.config_changed.emit() - _validate_config_options.assert_called_once() - _enable_disable_extensions.assert_not_called() - _set_up_relation.assert_not_called() - - # Test unable to connect to db - _update_config.reset_mock() - _validate_config_options.side_effect = OperationalError - self.charm.on.config_changed.emit() - assert not _update_config.called - _validate_config_options.side_effect = None - - # Test after the cluster was initialised. - with self.harness.hooks_disabled(): - self.harness.set_leader() - self.charm.on.config_changed.emit() - _enable_disable_extensions.assert_called_once() - _set_up_relation.assert_not_called() - - # Test when the unit is in a blocked state due to extensions request, - # but there are no established legacy relations. - _enable_disable_extensions.reset_mock() - self.charm.unit.status = BlockedStatus( - "extensions requested through relation, enable them through config options" + harness.charm.unit.status = BlockedStatus("fake blocked status") + harness.charm.on.update_status.emit() + _set_primary_status_message.assert_not_called() + + # Test with the unit in a status different that blocked. + harness.charm.unit.status = ActiveStatus() + harness.charm.on.update_status.emit() + _set_primary_status_message.assert_called_once() + + # Test the reinitialisation of the replica when its lag is unknown + # after a restart. + _set_primary_status_message.reset_mock() + _member_started.return_value = False + _is_member_isolated.return_value = False + _member_replication_lag.return_value = "unknown" + with harness.hooks_disabled(): + harness.update_relation_data( + self.rel_id, harness.charm.unit.name, {"postgresql_restarted": "True"} ) - self.charm.on.config_changed.emit() - _enable_disable_extensions.assert_called_once() - _set_up_relation.assert_not_called() - - # Test when the unit is in a blocked state due to extensions request, - # but there are established legacy relations. - _enable_disable_extensions.reset_mock() - _set_up_relation.return_value = False - db_relation_id = self.harness.add_relation("db", "application") - self.charm.on.config_changed.emit() - _enable_disable_extensions.assert_called_once() - _set_up_relation.assert_called_once() - self.harness.remove_relation(db_relation_id) - - _enable_disable_extensions.reset_mock() - _set_up_relation.reset_mock() - self.harness.add_relation("db-admin", "application") - self.charm.on.config_changed.emit() - _enable_disable_extensions.assert_called_once() - _set_up_relation.assert_called_once() - - # Test when there are established legacy relations, - # but the charm fails to set up one of them. - _enable_disable_extensions.reset_mock() - _set_up_relation.reset_mock() - _set_up_relation.return_value = False - self.harness.add_relation("db", "application") - self.charm.on.config_changed.emit() - _enable_disable_extensions.assert_called_once() - _set_up_relation.assert_called_once() - - @patch("subprocess.check_output", return_value=b"C") - def test_check_extension_dependencies(self, _): - with patch.object(PostgresqlOperatorCharm, "postgresql", Mock()) as _: - # Test when plugins dependencies exception is not caused - config = { - "plugin_address_standardizer_enable": False, - "plugin_postgis_enable": False, - "plugin_address_standardizer_data_us_enable": False, - "plugin_jsonb_plperl_enable": False, - "plugin_plperl_enable": False, - "plugin_postgis_raster_enable": False, - "plugin_postgis_tiger_geocoder_enable": False, - "plugin_fuzzystrmatch_enable": False, - "plugin_postgis_topology_enable": False, - } - self.harness.update_config(config) - self.harness.charm.enable_disable_extensions() - self.assertFalse(isinstance(self.harness.model.unit.status, BlockedStatus)) - - # Test when plugins dependencies exception caused - config["plugin_address_standardizer_enable"] = True - self.harness.update_config(config) - self.harness.charm.enable_disable_extensions() - self.assertTrue(isinstance(self.harness.model.unit.status, BlockedStatus)) - self.assertEqual(self.harness.model.unit.status.message, EXTENSIONS_DEPENDENCY_MESSAGE) - - @patch("subprocess.check_output", return_value=b"C") - def test_enable_disable_extensions(self, _): - with patch.object(PostgresqlOperatorCharm, "postgresql", Mock()) as postgresql_mock: - # Test when all extensions install/uninstall succeed. - postgresql_mock.enable_disable_extension.side_effect = None - with self.assertNoLogs("charm", "ERROR"): - self.charm.enable_disable_extensions() - self.assertEqual(postgresql_mock.enable_disable_extensions.call_count, 1) - - # Test when one extension install/uninstall fails. - postgresql_mock.reset_mock() - postgresql_mock.enable_disable_extensions.side_effect = ( - PostgreSQLEnableDisableExtensionError - ) - with self.assertLogs("charm", "ERROR") as logs: - self.charm.enable_disable_extensions() - self.assertEqual(postgresql_mock.enable_disable_extensions.call_count, 1) - self.assertIn("failed to change plugins", "".join(logs.output)) - - # Test when one config option should be skipped (because it's not related - # to a plugin/extension). - postgresql_mock.reset_mock() - postgresql_mock.enable_disable_extensions.side_effect = None - with self.assertNoLogs("charm", "ERROR"): - config = """options: - plugin_citext_enable: - default: false - type: boolean - plugin_hstore_enable: - default: false - type: boolean - plugin_pg_trgm_enable: - default: false - type: boolean - plugin_plpython3u_enable: - default: false - type: boolean - plugin_unaccent_enable: - default: false - type: boolean - plugin_debversion_enable: - default: false - type: boolean - plugin_bloom_enable: - default: false - type: boolean - plugin_btree_gin_enable: - default: false - type: boolean - plugin_btree_gist_enable: - default: false - type: boolean - plugin_cube_enable: - default: false - type: boolean - plugin_dict_int_enable: - default: false - type: boolean - plugin_dict_xsyn_enable: - default: false - type: boolean - plugin_earthdistance_enable: - default: false - type: boolean - plugin_fuzzystrmatch_enable: - default: false - type: boolean - plugin_intarray_enable: - default: false - type: boolean - plugin_isn_enable: - default: false - type: boolean - plugin_lo_enable: - default: false - type: boolean - plugin_ltree_enable: - default: false - type: boolean - plugin_old_snapshot_enable: - default: false - type: boolean - plugin_pg_freespacemap_enable: - default: false - type: boolean - plugin_pgrowlocks_enable: - default: false - type: boolean - plugin_pgstattuple_enable: - default: false - type: boolean - plugin_pg_visibility_enable: - default: false - type: boolean - plugin_seg_enable: - default: false - type: boolean - plugin_tablefunc_enable: - default: false - type: boolean - plugin_tcn_enable: - default: false - type: boolean - plugin_tsm_system_rows_enable: - default: false - type: boolean - plugin_tsm_system_time_enable: - default: false - type: boolean - plugin_uuid_ossp_enable: - default: false - type: boolean - plugin_spi_enable: - default: false - type: boolean - plugin_bool_plperl_enable: - default: false - type: boolean - plugin_hll_enable: - default: false - type: boolean - plugin_hypopg_enable: - default: false - type: boolean - plugin_ip4r_enable: - default: false - type: boolean - plugin_plperl_enable: - default: false - type: boolean - plugin_jsonb_plperl_enable: - default: false - type: boolean - plugin_orafce_enable: - default: false - type: boolean - plugin_pg_similarity_enable: - default: false - type: boolean - plugin_prefix_enable: - default: false - type: boolean - plugin_rdkit_enable: - default: false - type: boolean - plugin_tds_fdw_enable: - default: false - type: boolean - plugin_icu_ext_enable: - default: false - type: boolean - plugin_pltcl_enable: - default: false - type: boolean - plugin_postgis_enable: - default: false - type: boolean - plugin_postgis_raster_enable: - default: false - type: boolean - plugin_address_standardizer_enable: - default: false - type: boolean - plugin_address_standardizer_data_us_enable: - default: false - type: boolean - plugin_postgis_tiger_geocoder_enable: - default: false - type: boolean - plugin_postgis_topology_enable: - default: false - type: boolean - plugin_vector_enable: - default: false - type: boolean - profile: - default: production - type: string""" - harness = Harness(PostgresqlOperatorCharm, config=config) - self.addCleanup(harness.cleanup) - harness.begin() - harness.charm.enable_disable_extensions() - self.assertEqual(postgresql_mock.enable_disable_extensions.call_count, 1) - - @patch("charm.PostgresqlOperatorCharm.enable_disable_extensions") - @patch("charm.snap.SnapCache") - @patch("charm.Patroni.get_postgresql_version") - @patch_network_get(private_address="1.1.1.1") - @patch("charm.PostgreSQLProvider.oversee_users") - @patch("charm.PostgresqlOperatorCharm._update_relation_endpoints", new_callable=PropertyMock) - @patch("charm.PostgresqlOperatorCharm.postgresql") - @patch("charm.PostgreSQLProvider.update_endpoints") - @patch("charm.PostgresqlOperatorCharm.update_config") - @patch( - "charm.Patroni.member_started", - new_callable=PropertyMock, - ) - @patch("charm.Patroni.bootstrap_cluster") - @patch("charm.PostgresqlOperatorCharm._replication_password") - @patch("charm.PostgresqlOperatorCharm._get_password") - @patch("charm.PostgresqlOperatorCharm._reboot_on_detached_storage") - @patch("upgrade.PostgreSQLUpgrade.idle", return_value=True) - @patch( - "charm.PostgresqlOperatorCharm._is_storage_attached", - side_effect=[False, True, True, True, True], + harness.charm.on.update_status.emit() + _reinitialize_postgresql.assert_called_once() + _restart_patroni.assert_not_called() + _set_primary_status_message.assert_not_called() + + # Test call to restart when the member is isolated from the cluster. + _is_member_isolated.return_value = True + with harness.hooks_disabled(): + harness.update_relation_data( + self.rel_id, harness.charm.unit.name, {"postgresql_restarted": ""} + ) + harness.charm.on.update_status.emit() + _restart_patroni.assert_called_once() + _start_observer.assert_called_once() + +@patch_network_get(private_address="1.1.1.1") +@patch("charm.ClusterTopologyObserver.start_observer") +@patch("charm.PostgresqlOperatorCharm._set_primary_status_message") +@patch("charm.PostgresqlOperatorCharm._handle_workload_failures") +@patch("charm.PostgresqlOperatorCharm._update_relation_endpoints") +@patch( + "charm.PostgresqlOperatorCharm.primary_endpoint", + new_callable=PropertyMock(return_value=True), +) +@patch("charm.PostgreSQLProvider.oversee_users") +@patch("charm.PostgresqlOperatorCharm._handle_processes_failures") +@patch("charm.PostgreSQLBackups.can_use_s3_repository") +@patch("charm.PostgresqlOperatorCharm.update_config") +@patch("charm.Patroni.member_started", new_callable=PropertyMock) +@patch("charm.Patroni.get_member_status") +@patch("upgrade.PostgreSQLUpgrade.idle", return_value=True) +def test_on_update_status_after_restore_operation( + self, + _, + _get_member_status, + _member_started, + _update_config, + _can_use_s3_repository, + _handle_processes_failures, + _oversee_users, + _primary_endpoint, + _update_relation_endpoints, + _handle_workload_failures, + _set_primary_status_message, + __, +): + # Test when the restore operation fails. + with harness.hooks_disabled(): + harness.set_leader() + harness.update_relation_data( + self.rel_id, + harness.charm.app.name, + {"cluster_initialised": "True", "restoring-backup": "2023-01-01T09:00:00Z"}, + ) + _get_member_status.return_value = "failed" + harness.charm.on.update_status.emit() + _update_config.assert_not_called() + _handle_processes_failures.assert_not_called() + _oversee_users.assert_not_called() + _update_relation_endpoints.assert_not_called() + _handle_workload_failures.assert_not_called() + _set_primary_status_message.assert_not_called() + assert isinstance(harness.charm.unit.status, BlockedStatus) + + # Test when the restore operation hasn't finished yet. + harness.charm.unit.status = ActiveStatus() + _get_member_status.return_value = "running" + _member_started.return_value = False + harness.charm.on.update_status.emit() + _update_config.assert_not_called() + _handle_processes_failures.assert_not_called() + _oversee_users.assert_not_called() + _update_relation_endpoints.assert_not_called() + _handle_workload_failures.assert_not_called() + _set_primary_status_message.assert_not_called() + assert isinstance(harness.charm.unit.status, ActiveStatus) + + # Assert that the backup id is still in the application relation databag. + assert harness.get_relation_data(self.rel_id, harness.charm.app) == {"cluster_initialised": "True", "restoring-backup": "2023-01-01T09:00:00Z"} + + # Test when the restore operation finished successfully. + _member_started.return_value = True + _can_use_s3_repository.return_value = (True, None) + _handle_processes_failures.return_value = False + _handle_workload_failures.return_value = False + harness.charm.on.update_status.emit() + _update_config.assert_called_once() + _handle_processes_failures.assert_called_once() + _oversee_users.assert_called_once() + _update_relation_endpoints.assert_called_once() + _handle_workload_failures.assert_called_once() + _set_primary_status_message.assert_called_once() + assert isinstance(harness.charm.unit.status, ActiveStatus) + + # Assert that the backup id is not in the application relation databag anymore. + assert harness.get_relation_data(self.rel_id, harness.charm.app) == {"cluster_initialised": "True"} + + # Test when it's not possible to use the configured S3 repository. + _update_config.reset_mock() + _handle_processes_failures.reset_mock() + _oversee_users.reset_mock() + _update_relation_endpoints.reset_mock() + _handle_workload_failures.reset_mock() + _set_primary_status_message.reset_mock() + with harness.hooks_disabled(): + harness.update_relation_data( + self.rel_id, + harness.charm.app.name, + {"restoring-backup": "2023-01-01T09:00:00Z"}, + ) + _can_use_s3_repository.return_value = (False, "fake validation message") + harness.charm.on.update_status.emit() + _update_config.assert_called_once() + _handle_processes_failures.assert_not_called() + _oversee_users.assert_not_called() + _update_relation_endpoints.assert_not_called() + _handle_workload_failures.assert_not_called() + _set_primary_status_message.assert_not_called() + assert isinstance(harness.charm.unit.status, BlockedStatus) + assert harness.charm.unit.status.message == "fake validation message" + + # Assert that the backup id is not in the application relation databag anymore. + assert harness.get_relation_data(self.rel_id, harness.charm.app) == {"cluster_initialised": "True"} + +@patch("charm.snap.SnapCache") +def test_install_snap_packages(self, _snap_cache): + _snap_package = _snap_cache.return_value.__getitem__.return_value + _snap_package.ensure.side_effect = snap.SnapError + _snap_package.present = False + + # Test for problem with snap update. + with pytest.raises(snap.SnapError): + harness.charm._install_snap_packages([("postgresql", {"channel": "14/edge"})]) + _snap_cache.return_value.__getitem__.assert_called_once_with("postgresql") + _snap_cache.assert_called_once_with() + _snap_package.ensure.assert_called_once_with(snap.SnapState.Latest, channel="14/edge") + + # Test with a not found package. + _snap_cache.reset_mock() + _snap_package.reset_mock() + _snap_package.ensure.side_effect = snap.SnapNotFoundError + with pytest.raises(snap.SnapNotFoundError): + harness.charm._install_snap_packages([("postgresql", {"channel": "14/edge"})]) + _snap_cache.return_value.__getitem__.assert_called_once_with("postgresql") + _snap_cache.assert_called_once_with() + _snap_package.ensure.assert_called_once_with(snap.SnapState.Latest, channel="14/edge") + + # Then test a valid one. + _snap_cache.reset_mock() + _snap_package.reset_mock() + _snap_package.ensure.side_effect = None + harness.charm._install_snap_packages([("postgresql", {"channel": "14/edge"})]) + _snap_cache.assert_called_once_with() + _snap_cache.return_value.__getitem__.assert_called_once_with("postgresql") + _snap_package.ensure.assert_called_once_with(snap.SnapState.Latest, channel="14/edge") + _snap_package.hold.assert_not_called() + + # Test revision + _snap_cache.reset_mock() + _snap_package.reset_mock() + _snap_package.ensure.side_effect = None + harness.charm._install_snap_packages([ + ("postgresql", {"revision": {platform.machine(): "42"}}) + ]) + _snap_cache.assert_called_once_with() + _snap_cache.return_value.__getitem__.assert_called_once_with("postgresql") + _snap_package.ensure.assert_called_once_with( + snap.SnapState.Latest, revision="42", channel="" ) - def test_on_start( - self, - _is_storage_attached, - _idle, - _reboot_on_detached_storage, - _get_password, - _replication_password, - _bootstrap_cluster, - _member_started, - _, - __, - _postgresql, - _update_relation_endpoints, - _oversee_users, - _get_postgresql_version, - _snap_cache, - _enable_disable_extensions, - ): - _get_postgresql_version.return_value = "14.0" - - # Test without storage. - self.charm.on.start.emit() - _reboot_on_detached_storage.assert_called_once() - - # Test before the passwords are generated. - _member_started.return_value = False - _get_password.return_value = None - self.charm.on.start.emit() - _bootstrap_cluster.assert_not_called() - self.assertTrue(isinstance(self.harness.model.unit.status, WaitingStatus)) - - # Mock the passwords. - _get_password.return_value = "fake-operator-password" - _replication_password.return_value = "fake-replication-password" - - # Mock cluster start and postgres user creation success values. - _bootstrap_cluster.side_effect = [False, True, True] - _postgresql.list_users.side_effect = [[], [], []] - _postgresql.create_user.side_effect = [PostgreSQLCreateUserError, None, None, None] - - # Test for a failed cluster bootstrapping. - # TODO: test replicas start (DPE-494). - self.harness.set_leader() - self.charm.on.start.emit() - _bootstrap_cluster.assert_called_once() - _oversee_users.assert_not_called() - self.assertTrue(isinstance(self.harness.model.unit.status, BlockedStatus)) - # Set an initial waiting status (like after the install hook was triggered). - self.harness.model.unit.status = WaitingStatus("fake message") - - # Test the event of an error happening when trying to create the default postgres user. - _member_started.return_value = True - self.charm.on.start.emit() - _postgresql.create_user.assert_called_once() - _oversee_users.assert_not_called() - self.assertTrue(isinstance(self.harness.model.unit.status, BlockedStatus)) - - # Set an initial waiting status again (like after the install hook was triggered). - self.harness.model.unit.status = WaitingStatus("fake message") - - # Then test the event of a correct cluster bootstrapping. - self.charm.on.start.emit() - self.assertEqual( - _postgresql.create_user.call_count, 4 - ) # Considering the previous failed call. - _oversee_users.assert_called_once() - _enable_disable_extensions.assert_called_once() - self.assertTrue(isinstance(self.harness.model.unit.status, ActiveStatus)) - - @patch("charm.snap.SnapCache") - @patch("charm.Patroni.get_postgresql_version") - @patch_network_get(private_address="1.1.1.1") - @patch("charm.Patroni.configure_patroni_on_unit") - @patch( - "charm.Patroni.member_started", - new_callable=PropertyMock, + _snap_package.hold.assert_called_once_with() + + # Test with refresh + _snap_cache.reset_mock() + _snap_package.reset_mock() + _snap_package.present = True + harness.charm._install_snap_packages( + [("postgresql", {"revision": {platform.machine(): "42"}, "channel": "latest/test"})], + refresh=True, ) - @patch("charm.PostgresqlOperatorCharm._update_relation_endpoints", new_callable=PropertyMock) - @patch.object(EventBase, "defer") - @patch("charm.PostgresqlOperatorCharm._replication_password") - @patch("charm.PostgresqlOperatorCharm._get_password") - @patch("upgrade.PostgreSQLUpgrade.idle", return_value=True) - @patch( - "charm.PostgresqlOperatorCharm._is_storage_attached", - return_value=True, + _snap_cache.assert_called_once_with() + _snap_cache.return_value.__getitem__.assert_called_once_with("postgresql") + _snap_package.ensure.assert_called_once_with( + snap.SnapState.Latest, revision="42", channel="latest/test" ) - def test_on_start_replica( - self, - _is_storage_attached, - _idle, - _get_password, - _replication_password, - _defer, - _update_relation_endpoints, - _member_started, - _configure_patroni_on_unit, - _get_postgresql_version, - _snap_cache, - ): - _get_postgresql_version.return_value = "14.0" - - # Set the current unit to be a replica (non leader unit). - self.harness.set_leader(False) - - # Mock the passwords. - _get_password.return_value = "fake-operator-password" - _replication_password.return_value = "fake-replication-password" - - # Test an uninitialized cluster. - self.charm._peers.data[self.charm.app].update({"cluster_initialised": ""}) - self.charm.on.start.emit() - _defer.assert_called_once() - - # Set an initial waiting status again (like after a machine restart). - self.harness.model.unit.status = WaitingStatus("fake message") - - # Mark the cluster as initialised and with the workload up and running. - self.charm._peers.data[self.charm.app].update({"cluster_initialised": "True"}) - _member_started.return_value = True - self.charm.on.start.emit() - _configure_patroni_on_unit.assert_not_called() - self.assertTrue(isinstance(self.harness.model.unit.status, ActiveStatus)) - - # Set an initial waiting status (like after the install hook was triggered). - self.harness.model.unit.status = WaitingStatus("fake message") - - # Check that the unit status doesn't change when the workload is not running. - # In that situation only Patroni is configured in the unit (but not started). - _member_started.return_value = False - self.charm.on.start.emit() - _configure_patroni_on_unit.assert_called_once() - self.assertTrue(isinstance(self.harness.model.unit.status, WaitingStatus)) - - @patch_network_get(private_address="1.1.1.1") - @patch("subprocess.check_output", return_value=b"C") - @patch("charm.snap.SnapCache") - @patch("charm.PostgresqlOperatorCharm.postgresql") - @patch("charm.Patroni") - @patch("charm.PostgresqlOperatorCharm._get_password") - @patch("upgrade.PostgreSQLUpgrade.idle", return_value=True) - @patch("charm.PostgresqlOperatorCharm._is_storage_attached", return_value=True) - def test_on_start_no_patroni_member( - self, - _is_storage_attached, - _idle, - _get_password, - patroni, - _postgresql, - _snap_cache, - _, - ): - # Mock the passwords. - patroni.return_value.member_started = False - _get_password.return_value = "fake-operator-password" - bootstrap_cluster = patroni.return_value.bootstrap_cluster - bootstrap_cluster.return_value = True - - patroni.return_value.get_postgresql_version.return_value = "14.0" - - self.harness.set_leader() - self.charm.on.start.emit() - bootstrap_cluster.assert_called_once() - _postgresql.create_user.assert_not_called() - self.assertTrue(isinstance(self.harness.model.unit.status, WaitingStatus)) - self.assertEqual(self.harness.model.unit.status.message, "awaiting for member to start") - - @patch("charm.Patroni.bootstrap_cluster") - @patch("charm.PostgresqlOperatorCharm._replication_password") - @patch("charm.PostgresqlOperatorCharm._get_password") - @patch("charm.PostgresqlOperatorCharm._is_storage_attached", return_value=True) - def test_on_start_after_blocked_state( - self, _is_storage_attached, _get_password, _replication_password, _bootstrap_cluster - ): - # Set an initial blocked status (like after the install hook was triggered). - initial_status = BlockedStatus("fake message") - self.harness.model.unit.status = initial_status - - # Test for a failed cluster bootstrapping. - self.charm.on.start.emit() - _get_password.assert_not_called() - _replication_password.assert_not_called() - _bootstrap_cluster.assert_not_called() - # Assert the status didn't change. - self.assertEqual(self.harness.model.unit.status, initial_status) - - @patch_network_get(private_address="1.1.1.1") - @patch("charm.PostgresqlOperatorCharm.update_config") - def test_on_get_password(self, _): - # Create a mock event and set passwords in peer relation data. - self.harness.set_leader(True) - mock_event = MagicMock(params={}) - self.harness.update_relation_data( - self.rel_id, - self.charm.app.name, - { - "operator-password": "test-password", - "replication-password": "replication-test-password", - }, + _snap_package.hold.assert_called_once_with() + + # Test without refresh + _snap_cache.reset_mock() + _snap_package.reset_mock() + harness.charm._install_snap_packages([ + ("postgresql", {"revision": {platform.machine(): "42"}}) + ]) + _snap_cache.assert_called_once_with() + _snap_cache.return_value.__getitem__.assert_called_once_with("postgresql") + _snap_package.ensure.assert_not_called() + _snap_package.hold.assert_not_called() + + # test missing architecture + _snap_cache.reset_mock() + _snap_package.reset_mock() + _snap_package.present = True + with pytest.raises(KeyError): + harness.charm._install_snap_packages( + [("postgresql", {"revision": {"missingarch": "42"}})], + refresh=True, ) - - # Test providing an invalid username. - mock_event.params["username"] = "user" - self.charm._on_get_password(mock_event) - mock_event.fail.assert_called_once() - mock_event.set_results.assert_not_called() - - # Test without providing the username option. - mock_event.reset_mock() - del mock_event.params["username"] - self.charm._on_get_password(mock_event) - mock_event.set_results.assert_called_once_with({"password": "test-password"}) - - # Also test providing the username option. - mock_event.reset_mock() - mock_event.params["username"] = "replication" - self.charm._on_get_password(mock_event) - mock_event.set_results.assert_called_once_with({"password": "replication-test-password"}) - - @patch_network_get(private_address="1.1.1.1") - @patch("charm.PostgresqlOperatorCharm.update_config") - @patch("charm.PostgresqlOperatorCharm.set_secret") - @patch("charm.PostgresqlOperatorCharm.postgresql") - @patch("charm.Patroni.are_all_members_ready") - @patch("charm.PostgresqlOperatorCharm._on_leader_elected") - def test_on_set_password( - self, - _, - _are_all_members_ready, - _postgresql, - _set_secret, - __, - ): - # Create a mock event. - mock_event = MagicMock(params={}) - - # Set some values for the other mocks. - _are_all_members_ready.side_effect = [False, True, True, True, True] - _postgresql.update_user_password = PropertyMock( - side_effect=[PostgreSQLUpdateUserPasswordError, None, None, None] + _snap_cache.assert_called_once_with() + _snap_cache.return_value.__getitem__.assert_called_once_with("postgresql") + assert not _snap_package.ensure.called + assert not _snap_package.hold.called + +@patch( + "subprocess.check_call", + side_effect=[None, subprocess.CalledProcessError(1, "fake command")], +) +def test_is_storage_attached(self, _check_call): + # Test with attached storage. + is_storage_attached = harness.charm._is_storage_attached() + _check_call.assert_called_once_with(["mountpoint", "-q", harness.charm._storage_path]) + assert (is_storage_attached) + + # Test with detached storage. + is_storage_attached = harness.charm._is_storage_attached() + assert not (is_storage_attached) + +@patch("subprocess.check_call") +def test_reboot_on_detached_storage(self, _check_call): + mock_event = MagicMock() + harness.charm._reboot_on_detached_storage(mock_event) + mock_event.defer.assert_called_once() + assert (isinstance(harness.charm.unit.status, WaitingStatus)) + _check_call.assert_called_once_with(["systemctl", "reboot"]) + +@patch_network_get(private_address="1.1.1.1") +@patch("charm.Patroni.restart_postgresql") +@patch("charm.Patroni.are_all_members_ready") +def test_restart(self, _are_all_members_ready, _restart_postgresql): + _are_all_members_ready.side_effect = [False, True, True] + + # Test when not all members are ready. + mock_event = MagicMock() + harness.charm._restart(mock_event) + mock_event.defer.assert_called_once() + _restart_postgresql.assert_not_called() + + # Test a successful restart. + mock_event.defer.reset_mock() + harness.charm._restart(mock_event) + assert not (isinstance(harness.charm.unit.status, BlockedStatus)) + mock_event.defer.assert_not_called() + + # Test a failed restart. + _restart_postgresql.side_effect = RetryError(last_attempt=1) + harness.charm._restart(mock_event) + assert (isinstance(harness.charm.unit.status, BlockedStatus)) + mock_event.defer.assert_not_called() + +@patch_network_get(private_address="1.1.1.1") +@patch("subprocess.check_output", return_value=b"C") +@patch("charm.snap.SnapCache") +@patch("charm.PostgresqlOperatorCharm._handle_postgresql_restart_need") +@patch("charm.Patroni.bulk_update_parameters_controller_by_patroni") +@patch("charm.Patroni.member_started", new_callable=PropertyMock) +@patch("charm.PostgresqlOperatorCharm._is_workload_running", new_callable=PropertyMock) +@patch("charm.Patroni.render_patroni_yml_file") +@patch("charm.PostgresqlOperatorCharm.is_tls_enabled", new_callable=PropertyMock) +def test_update_config( + self, + _is_tls_enabled, + _render_patroni_yml_file, + _is_workload_running, + _member_started, + _, + _handle_postgresql_restart_need, + __, + ___, +): + with patch.object(PostgresqlOperatorCharm, "postgresql", Mock()) as postgresql_mock: + # Mock some properties. + postgresql_mock.is_tls_enabled = PropertyMock(side_effect=[False, False, False, False]) + _is_workload_running.side_effect = [False, False, True, True, False, True] + _member_started.side_effect = [True, True, False] + postgresql_mock.build_postgresql_parameters.return_value = {"test": "test"} + + # Test when only one of the two config options for profile limit memory is set. + harness.update_config({"profile-limit-memory": 1000}) + harness.charm.update_config() + + # Test when only one of the two config options for profile limit memory is set. + harness.update_config( + {"profile_limit_memory": 1000}, unset={"profile-limit-memory"} ) - - # Test trying to set a password through a non leader unit. - self.charm._on_set_password(mock_event) - mock_event.fail.assert_called_once() - _set_secret.assert_not_called() - - # Test providing an invalid username. - self.harness.set_leader() - mock_event.reset_mock() - mock_event.params["username"] = "user" - self.charm._on_set_password(mock_event) - mock_event.fail.assert_called_once() - _set_secret.assert_not_called() - - # Test without providing the username option but without all cluster members ready. - mock_event.reset_mock() - del mock_event.params["username"] - self.charm._on_set_password(mock_event) - mock_event.fail.assert_called_once() - _set_secret.assert_not_called() - - # Test for an error updating when updating the user password in the database. - mock_event.reset_mock() - self.charm._on_set_password(mock_event) - mock_event.fail.assert_called_once() - _set_secret.assert_not_called() - - # Test without providing the username option. - self.charm._on_set_password(mock_event) - self.assertEqual(_set_secret.call_args_list[0][0][1], "operator-password") - - # Also test providing the username option. - _set_secret.reset_mock() - mock_event.params["username"] = "replication" - self.charm._on_set_password(mock_event) - self.assertEqual(_set_secret.call_args_list[0][0][1], "replication-password") - - # And test providing both the username and password options. - _set_secret.reset_mock() - mock_event.params["password"] = "replication-test-password" - self.charm._on_set_password(mock_event) - _set_secret.assert_called_once_with( - "app", "replication-password", "replication-test-password" + harness.charm.update_config() + + # Test when the two config options for profile limit memory are set at the same time. + _render_patroni_yml_file.reset_mock() + harness.update_config({"profile-limit-memory": 1000}) + with pytest.raises(ValueError): + harness.charm.update_config() + + # Test without TLS files available. + harness.update_config(unset={"profile-limit-memory", "profile_limit_memory"}) + with harness.hooks_disabled(): + harness.update_relation_data(self.rel_id, harness.charm.unit.name, {"tls": ""}) + _is_tls_enabled.return_value = False + harness.charm.update_config() + _render_patroni_yml_file.assert_called_once_with( + connectivity=True, + is_creating_backup=False, + enable_tls=False, + backup_id=None, + stanza=None, + restore_stanza=None, + parameters={"test": "test"}, ) - - @patch_network_get(private_address="1.1.1.1") - @patch("charm.ClusterTopologyObserver.start_observer") - @patch("charm.PostgresqlOperatorCharm._set_primary_status_message") - @patch("charm.Patroni.restart_patroni") - @patch("charm.Patroni.is_member_isolated") - @patch("charm.Patroni.reinitialize_postgresql") - @patch("charm.Patroni.member_replication_lag", new_callable=PropertyMock) - @patch("charm.Patroni.member_started", new_callable=PropertyMock) - @patch("charm.PostgresqlOperatorCharm._update_relation_endpoints") - @patch( - "charm.PostgresqlOperatorCharm.primary_endpoint", - new_callable=PropertyMock(return_value=True), - ) - @patch("charm.PostgreSQLProvider.oversee_users") - @patch("upgrade.PostgreSQLUpgrade.idle", return_value=True) - def test_on_update_status( - self, - _, - _oversee_users, - _primary_endpoint, - _update_relation_endpoints, - _member_started, - _member_replication_lag, - _reinitialize_postgresql, - _is_member_isolated, - _restart_patroni, - _set_primary_status_message, - _start_observer, - ): - # Test before the cluster is initialised. - self.charm.on.update_status.emit() - _set_primary_status_message.assert_not_called() - - # Test after the cluster was initialised, but with the unit in a blocked state. - with self.harness.hooks_disabled(): - self.harness.update_relation_data( - self.rel_id, self.charm.app.name, {"cluster_initialised": "True"} - ) - self.charm.unit.status = BlockedStatus("fake blocked status") - self.charm.on.update_status.emit() - _set_primary_status_message.assert_not_called() - - # Test with the unit in a status different that blocked. - self.charm.unit.status = ActiveStatus() - self.charm.on.update_status.emit() - _set_primary_status_message.assert_called_once() - - # Test the reinitialisation of the replica when its lag is unknown - # after a restart. - _set_primary_status_message.reset_mock() - _member_started.return_value = False - _is_member_isolated.return_value = False - _member_replication_lag.return_value = "unknown" - with self.harness.hooks_disabled(): - self.harness.update_relation_data( - self.rel_id, self.charm.unit.name, {"postgresql_restarted": "True"} - ) - self.charm.on.update_status.emit() - _reinitialize_postgresql.assert_called_once() - _restart_patroni.assert_not_called() - _set_primary_status_message.assert_not_called() - - # Test call to restart when the member is isolated from the cluster. - _is_member_isolated.return_value = True - with self.harness.hooks_disabled(): - self.harness.update_relation_data( - self.rel_id, self.charm.unit.name, {"postgresql_restarted": ""} - ) - self.charm.on.update_status.emit() - _restart_patroni.assert_called_once() - _start_observer.assert_called_once() - - @patch_network_get(private_address="1.1.1.1") - @patch("charm.ClusterTopologyObserver.start_observer") - @patch("charm.PostgresqlOperatorCharm._set_primary_status_message") - @patch("charm.PostgresqlOperatorCharm._handle_workload_failures") - @patch("charm.PostgresqlOperatorCharm._update_relation_endpoints") - @patch( - "charm.PostgresqlOperatorCharm.primary_endpoint", - new_callable=PropertyMock(return_value=True), - ) - @patch("charm.PostgreSQLProvider.oversee_users") - @patch("charm.PostgresqlOperatorCharm._handle_processes_failures") - @patch("charm.PostgreSQLBackups.can_use_s3_repository") - @patch("charm.PostgresqlOperatorCharm.update_config") - @patch("charm.Patroni.member_started", new_callable=PropertyMock) - @patch("charm.Patroni.get_member_status") - @patch("upgrade.PostgreSQLUpgrade.idle", return_value=True) - def test_on_update_status_after_restore_operation( - self, - _, - _get_member_status, - _member_started, - _update_config, - _can_use_s3_repository, - _handle_processes_failures, - _oversee_users, - _primary_endpoint, - _update_relation_endpoints, - _handle_workload_failures, - _set_primary_status_message, - __, - ): - # Test when the restore operation fails. - with self.harness.hooks_disabled(): - self.harness.set_leader() - self.harness.update_relation_data( - self.rel_id, - self.charm.app.name, - {"cluster_initialised": "True", "restoring-backup": "2023-01-01T09:00:00Z"}, - ) - _get_member_status.return_value = "failed" - self.charm.on.update_status.emit() - _update_config.assert_not_called() - _handle_processes_failures.assert_not_called() - _oversee_users.assert_not_called() - _update_relation_endpoints.assert_not_called() - _handle_workload_failures.assert_not_called() - _set_primary_status_message.assert_not_called() - self.assertIsInstance(self.charm.unit.status, BlockedStatus) - - # Test when the restore operation hasn't finished yet. - self.charm.unit.status = ActiveStatus() - _get_member_status.return_value = "running" - _member_started.return_value = False - self.charm.on.update_status.emit() - _update_config.assert_not_called() - _handle_processes_failures.assert_not_called() - _oversee_users.assert_not_called() - _update_relation_endpoints.assert_not_called() - _handle_workload_failures.assert_not_called() - _set_primary_status_message.assert_not_called() - self.assertIsInstance(self.charm.unit.status, ActiveStatus) - - # Assert that the backup id is still in the application relation databag. - self.assertEqual( - self.harness.get_relation_data(self.rel_id, self.charm.app), - {"cluster_initialised": "True", "restoring-backup": "2023-01-01T09:00:00Z"}, + _handle_postgresql_restart_need.assert_called_once_with(False) + assert "tls" not in harness.get_relation_data(self.rel_id, harness.charm.unit.name) + + # Test with TLS files available. + _handle_postgresql_restart_need.reset_mock() + harness.update_relation_data( + self.rel_id, harness.charm.unit.name, {"tls": ""} + ) # Mock some data in the relation to test that it change. + _is_tls_enabled.return_value = True + _render_patroni_yml_file.reset_mock() + harness.charm.update_config() + _render_patroni_yml_file.assert_called_once_with( + connectivity=True, + is_creating_backup=False, + enable_tls=True, + backup_id=None, + stanza=None, + restore_stanza=None, + parameters={"test": "test"}, ) - - # Test when the restore operation finished successfully. - _member_started.return_value = True - _can_use_s3_repository.return_value = (True, None) - _handle_processes_failures.return_value = False - _handle_workload_failures.return_value = False - self.charm.on.update_status.emit() - _update_config.assert_called_once() - _handle_processes_failures.assert_called_once() - _oversee_users.assert_called_once() - _update_relation_endpoints.assert_called_once() - _handle_workload_failures.assert_called_once() - _set_primary_status_message.assert_called_once() - self.assertIsInstance(self.charm.unit.status, ActiveStatus) - - # Assert that the backup id is not in the application relation databag anymore. - self.assertEqual( - self.harness.get_relation_data(self.rel_id, self.charm.app), - {"cluster_initialised": "True"}, + _handle_postgresql_restart_need.assert_called_once() + assert "tls" not in harness.get_relation_data(self.rel_id, harness.charm.unit.name) # The "tls" flag is set in handle_postgresql_restart_need. + + # Test with workload not running yet. + harness.update_relation_data( + self.rel_id, harness.charm.unit.name, {"tls": ""} + ) # Mock some data in the relation to test that it change. + _handle_postgresql_restart_need.reset_mock() + harness.charm.update_config() + _handle_postgresql_restart_need.assert_not_called() + assert harness.get_relation_data(self.rel_id, harness.charm.unit.name)["tls"] == "enabled" + + # Test with member not started yet. + harness.update_relation_data( + self.rel_id, harness.charm.unit.name, {"tls": ""} + ) # Mock some data in the relation to test that it doesn't change. + harness.charm.update_config() + _handle_postgresql_restart_need.assert_not_called() + assert "tls" not in harness.get_relation_data(self.rel_id, harness.charm.unit.name) + +@patch("charm.PostgresqlOperatorCharm._update_relation_endpoints") +@patch("charm.PostgresqlOperatorCharm.primary_endpoint", new_callable=PropertyMock) +def test_on_cluster_topology_change(self, _primary_endpoint, _update_relation_endpoints): + # Mock the property value. + _primary_endpoint.side_effect = [None, "1.1.1.1"] + + # Test without an elected primary. + harness.charm._on_cluster_topology_change(Mock()) + _update_relation_endpoints.assert_not_called() + + # Test with an elected primary. + harness.charm._on_cluster_topology_change(Mock()) + _update_relation_endpoints.assert_called_once() + +@patch( + "charm.PostgresqlOperatorCharm.primary_endpoint", + new_callable=PropertyMock, + return_value=None, +) +@patch("charm.PostgresqlOperatorCharm._update_relation_endpoints") +def test_on_cluster_topology_change_keep_blocked( + self, _update_relation_endpoints, _primary_endpoint +): + harness.model.unit.status = WaitingStatus(PRIMARY_NOT_REACHABLE_MESSAGE) + + harness.charm._on_cluster_topology_change(Mock()) + + _update_relation_endpoints.assert_not_called() + _primary_endpoint.assert_called_once_with() + assert isinstance(harness.model.unit.status, WaitingStatus) + assert harness.model.unit.status.message == PRIMARY_NOT_REACHABLE_MESSAGE + +@patch( + "charm.PostgresqlOperatorCharm.primary_endpoint", + new_callable=PropertyMock, + return_value="fake-unit", +) +@patch("charm.PostgresqlOperatorCharm._update_relation_endpoints") +def test_on_cluster_topology_change_clear_blocked( + self, _update_relation_endpoints, _primary_endpoint +): + harness.model.unit.status = WaitingStatus(PRIMARY_NOT_REACHABLE_MESSAGE) + + harness.charm._on_cluster_topology_change(Mock()) + + _update_relation_endpoints.assert_called_once_with() + _primary_endpoint.assert_called_once_with() + assert (isinstance(harness.model.unit.status, ActiveStatus)) + +@patch("charm.PostgresqlOperatorCharm.postgresql", new_callable=PropertyMock) +@patch("config.subprocess") +def test_validate_config_options(self, _, _charm_lib): + _charm_lib.return_value.get_postgresql_text_search_configs.return_value = [] + _charm_lib.return_value.validate_date_style.return_value = [] + _charm_lib.return_value.get_postgresql_timezones.return_value = [] + + # Test instance_default_text_search_config exception + with harness.hooks_disabled(): + harness.update_config({"instance_default_text_search_config": "pg_catalog.test"}) + + with pytest.raises(ValueError) as e: + harness.charm._validate_config_options() + assert ( + e.msg == "instance_default_text_search_config config option has an invalid value" ) - # Test when it's not possible to use the configured S3 repository. - _update_config.reset_mock() - _handle_processes_failures.reset_mock() - _oversee_users.reset_mock() - _update_relation_endpoints.reset_mock() - _handle_workload_failures.reset_mock() - _set_primary_status_message.reset_mock() - with self.harness.hooks_disabled(): - self.harness.update_relation_data( - self.rel_id, - self.charm.app.name, - {"restoring-backup": "2023-01-01T09:00:00Z"}, - ) - _can_use_s3_repository.return_value = (False, "fake validation message") - self.charm.on.update_status.emit() - _update_config.assert_called_once() - _handle_processes_failures.assert_not_called() - _oversee_users.assert_not_called() - _update_relation_endpoints.assert_not_called() - _handle_workload_failures.assert_not_called() - _set_primary_status_message.assert_not_called() - self.assertIsInstance(self.charm.unit.status, BlockedStatus) - self.assertEqual(self.charm.unit.status.message, "fake validation message") - - # Assert that the backup id is not in the application relation databag anymore. - self.assertEqual( - self.harness.get_relation_data(self.rel_id, self.charm.app), - {"cluster_initialised": "True"}, + _charm_lib.return_value.get_postgresql_text_search_configs.assert_called_once_with() + _charm_lib.return_value.get_postgresql_text_search_configs.return_value = [ + "pg_catalog.test" + ] + + # Test request_date_style exception + with harness.hooks_disabled(): + harness.update_config({"request_date_style": "ISO, TEST"}) + + with pytest.raises(ValueError) as e: + harness.charm._validate_config_options() + assert e.msg == "request_date_style config option has an invalid value" + + _charm_lib.return_value.validate_date_style.assert_called_once_with("ISO, TEST") + _charm_lib.return_value.validate_date_style.return_value = ["ISO, TEST"] + + # Test request_time_zone exception + with harness.hooks_disabled(): + harness.update_config({"request_time_zone": "TEST_ZONE"}) + + with pytest.raises(ValueError) as e: + harness.charm._validate_config_options() + assert e.msg == "request_time_zone config option has an invalid value" + + _charm_lib.return_value.get_postgresql_timezones.assert_called_once_with() + _charm_lib.return_value.get_postgresql_timezones.return_value = ["TEST_ZONE"] + +@patch_network_get(private_address="1.1.1.1") +@patch("charm.snap.SnapCache") +@patch("charm.PostgresqlOperatorCharm._update_relation_endpoints") +@patch("charm.PostgresqlOperatorCharm.primary_endpoint", new_callable=PropertyMock) +@patch("backups.PostgreSQLBackups.check_stanza") +@patch("backups.PostgreSQLBackups.coordinate_stanza_fields") +@patch("backups.PostgreSQLBackups.start_stop_pgbackrest_service") +@patch("charm.Patroni.reinitialize_postgresql") +@patch("charm.Patroni.member_replication_lag", new_callable=PropertyMock) +@patch("charm.PostgresqlOperatorCharm.is_primary") +@patch("charm.Patroni.member_started", new_callable=PropertyMock) +@patch("charm.Patroni.start_patroni") +@patch("charm.PostgresqlOperatorCharm.update_config") +@patch("charm.PostgresqlOperatorCharm._update_member_ip") +@patch("charm.PostgresqlOperatorCharm._reconfigure_cluster") +@patch("ops.framework.EventBase.defer") +def test_on_peer_relation_changed( + self, + _defer, + _reconfigure_cluster, + _update_member_ip, + _update_config, + _start_patroni, + _member_started, + _is_primary, + _member_replication_lag, + _reinitialize_postgresql, + _start_stop_pgbackrest_service, + _coordinate_stanza_fields, + _check_stanza, + _primary_endpoint, + _update_relation_endpoints, + _, +): + # Test an uninitialized cluster. + mock_event = Mock() + with harness.hooks_disabled(): + harness.update_relation_data( + self.rel_id, harness.charm.app.name, {"cluster_initialised": ""} ) - - @patch("charm.snap.SnapCache") - def test_install_snap_packages(self, _snap_cache): - _snap_package = _snap_cache.return_value.__getitem__.return_value - _snap_package.ensure.side_effect = snap.SnapError - _snap_package.present = False - - # Test for problem with snap update. - with self.assertRaises(snap.SnapError): - self.charm._install_snap_packages([("postgresql", {"channel": "14/edge"})]) - _snap_cache.return_value.__getitem__.assert_called_once_with("postgresql") - _snap_cache.assert_called_once_with() - _snap_package.ensure.assert_called_once_with(snap.SnapState.Latest, channel="14/edge") - - # Test with a not found package. - _snap_cache.reset_mock() - _snap_package.reset_mock() - _snap_package.ensure.side_effect = snap.SnapNotFoundError - with self.assertRaises(snap.SnapNotFoundError): - self.charm._install_snap_packages([("postgresql", {"channel": "14/edge"})]) - _snap_cache.return_value.__getitem__.assert_called_once_with("postgresql") - _snap_cache.assert_called_once_with() - _snap_package.ensure.assert_called_once_with(snap.SnapState.Latest, channel="14/edge") - - # Then test a valid one. - _snap_cache.reset_mock() - _snap_package.reset_mock() - _snap_package.ensure.side_effect = None - self.charm._install_snap_packages([("postgresql", {"channel": "14/edge"})]) - _snap_cache.assert_called_once_with() - _snap_cache.return_value.__getitem__.assert_called_once_with("postgresql") - _snap_package.ensure.assert_called_once_with(snap.SnapState.Latest, channel="14/edge") - _snap_package.hold.assert_not_called() - - # Test revision - _snap_cache.reset_mock() - _snap_package.reset_mock() - _snap_package.ensure.side_effect = None - self.charm._install_snap_packages([ - ("postgresql", {"revision": {platform.machine(): "42"}}) - ]) - _snap_cache.assert_called_once_with() - _snap_cache.return_value.__getitem__.assert_called_once_with("postgresql") - _snap_package.ensure.assert_called_once_with( - snap.SnapState.Latest, revision="42", channel="" + harness.charm._on_peer_relation_changed(mock_event) + mock_event.defer.assert_called_once() + _reconfigure_cluster.assert_not_called() + + # Test an initialized cluster and this is the leader unit + # (but it fails to reconfigure the cluster). + mock_event.defer.reset_mock() + with harness.hooks_disabled(): + harness.update_relation_data( + self.rel_id, + harness.charm.app.name, + {"cluster_initialised": "True", "members_ips": '["1.1.1.1"]'}, ) - _snap_package.hold.assert_called_once_with() - - # Test with refresh - _snap_cache.reset_mock() - _snap_package.reset_mock() - _snap_package.present = True - self.charm._install_snap_packages( - [("postgresql", {"revision": {platform.machine(): "42"}, "channel": "latest/test"})], - refresh=True, + harness.set_leader() + _reconfigure_cluster.return_value = False + harness.charm._on_peer_relation_changed(mock_event) + _reconfigure_cluster.assert_called_once_with(mock_event) + mock_event.defer.assert_called_once() + + # Test when the leader can reconfigure the cluster. + mock_event.defer.reset_mock() + _reconfigure_cluster.reset_mock() + _reconfigure_cluster.return_value = True + _update_member_ip.return_value = False + _member_started.return_value = True + _primary_endpoint.return_value = "1.1.1.1" + harness.model.unit.status = WaitingStatus("awaiting for cluster to start") + harness.charm._on_peer_relation_changed(mock_event) + mock_event.defer.assert_not_called() + _reconfigure_cluster.assert_called_once_with(mock_event) + _update_member_ip.assert_called_once() + _update_config.assert_called_once() + _start_patroni.assert_called_once() + _update_relation_endpoints.assert_called_once() + assert isinstance(harness.model.unit.status, ActiveStatus) + + # Test when the cluster member updates its IP. + _update_member_ip.reset_mock() + _update_config.reset_mock() + _start_patroni.reset_mock() + _update_relation_endpoints.reset_mock() + _update_member_ip.return_value = True + harness.charm._on_peer_relation_changed(mock_event) + _update_member_ip.assert_called_once() + _update_config.assert_not_called() + _start_patroni.assert_not_called() + _update_relation_endpoints.assert_not_called() + + # Test when the unit fails to update the Patroni configuration. + _update_member_ip.return_value = False + _update_config.side_effect = RetryError(last_attempt=1) + harness.charm._on_peer_relation_changed(mock_event) + _update_config.assert_called_once() + _start_patroni.assert_not_called() + _update_relation_endpoints.assert_not_called() + assert isinstance(harness.model.unit.status, BlockedStatus) + + # Test when Patroni hasn't started yet in the unit. + _update_config.side_effect = None + _member_started.return_value = False + harness.charm._on_peer_relation_changed(mock_event) + _start_patroni.assert_called_once() + _update_relation_endpoints.assert_not_called() + assert isinstance(harness.model.unit.status, WaitingStatus) + + # Test when Patroni has already started but this is a replica with a + # huge or unknown lag. + self.relation = harness.model.get_relation(PEER, self.rel_id) + _member_started.return_value = True + for values in itertools.product([True, False], ["0", "1000", "1001", "unknown"]): + _defer.reset_mock() + _check_stanza.reset_mock() + _start_stop_pgbackrest_service.reset_mock() + _is_primary.return_value = values[0] + _member_replication_lag.return_value = values[1] + harness.charm.unit.status = ActiveStatus() + harness.charm.on.database_peers_relation_changed.emit(self.relation) + if _is_primary.return_value == values[0] or int(values[1]) <= 1000: + _defer.assert_not_called() + _check_stanza.assert_called_once() + _start_stop_pgbackrest_service.assert_called_once() + assert isinstance(harness.charm.unit.status, ActiveStatus) + else: + _defer.assert_called_once() + _check_stanza.assert_not_called() + _start_stop_pgbackrest_service.assert_not_called() + assert isinstance(harness.charm.unit.status, MaintenanceStatus) + + # Test when it was not possible to start the pgBackRest service yet. + self.relation = harness.model.get_relation(PEER, self.rel_id) + _member_started.return_value = True + _defer.reset_mock() + _coordinate_stanza_fields.reset_mock() + _check_stanza.reset_mock() + _start_stop_pgbackrest_service.return_value = False + harness.charm.on.database_peers_relation_changed.emit(self.relation) + _defer.assert_called_once() + _coordinate_stanza_fields.assert_not_called() + _check_stanza.assert_not_called() + + # Test the last calls been made when it was possible to start the + # pgBackRest service. + _defer.reset_mock() + _start_stop_pgbackrest_service.return_value = True + harness.charm.on.database_peers_relation_changed.emit(self.relation) + _defer.assert_not_called() + _coordinate_stanza_fields.assert_called_once() + _check_stanza.assert_called_once() + +@patch_network_get(private_address="1.1.1.1") +@patch("charm.PostgresqlOperatorCharm._add_members") +@patch("charm.PostgresqlOperatorCharm._remove_from_members_ips") +@patch("charm.Patroni.remove_raft_member") +def test_reconfigure_cluster( + self, _remove_raft_member, _remove_from_members_ips, _add_members +): + # Test when no change is needed in the member IP. + mock_event = Mock() + mock_event.unit = harness.charm.unit + mock_event.relation.data = {mock_event.unit: {}} + assert (harness.charm._reconfigure_cluster(mock_event)) + _remove_raft_member.assert_not_called() + _remove_from_members_ips.assert_not_called() + _add_members.assert_called_once_with(mock_event) + + # Test when a change is needed in the member IP, but it fails. + _remove_raft_member.side_effect = RemoveRaftMemberFailedError + _add_members.reset_mock() + ip_to_remove = "1.1.1.1" + relation_data = {mock_event.unit: {"ip-to-remove": ip_to_remove}} + mock_event.relation.data = relation_data + assert not (harness.charm._reconfigure_cluster(mock_event)) + _remove_raft_member.assert_called_once_with(ip_to_remove) + _remove_from_members_ips.assert_not_called() + _add_members.assert_not_called() + + # Test when a change is needed in the member IP, and it succeeds + # (but the old IP was already been removed). + _remove_raft_member.reset_mock() + _remove_raft_member.side_effect = None + _add_members.reset_mock() + mock_event.relation.data = relation_data + assert (harness.charm._reconfigure_cluster(mock_event)) + _remove_raft_member.assert_called_once_with(ip_to_remove) + _remove_from_members_ips.assert_not_called() + _add_members.assert_called_once_with(mock_event) + + # Test when the old IP wasn't removed yet. + _remove_raft_member.reset_mock() + _add_members.reset_mock() + mock_event.relation.data = relation_data + with harness.hooks_disabled(): + harness.update_relation_data( + self.rel_id, harness.charm.app.name, {"members_ips": '["' + ip_to_remove + '"]'} ) - _snap_cache.assert_called_once_with() - _snap_cache.return_value.__getitem__.assert_called_once_with("postgresql") - _snap_package.ensure.assert_called_once_with( - snap.SnapState.Latest, revision="42", channel="latest/test" + assert (harness.charm._reconfigure_cluster(mock_event)) + _remove_raft_member.assert_called_once_with(ip_to_remove) + _remove_from_members_ips.assert_called_once_with(ip_to_remove) + _add_members.assert_called_once_with(mock_event) + +@patch("charms.postgresql_k8s.v0.postgresql_tls.PostgreSQLTLS._request_certificate") +@patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=False) +def test_update_certificate(self, _, _request_certificate): + # If there is no current TLS files, _request_certificate should be called + # only when the certificates relation is established. + harness.charm._update_certificate() + _request_certificate.assert_not_called() + + # Test with already present TLS files (when they will be replaced by new ones). + ca = "fake CA" + cert = "fake certificate" + key = private_key = "fake private key" + with harness.hooks_disabled(): + harness.update_relation_data( + self.rel_id, + harness.charm.unit.name, + { + "ca": ca, + "cert": cert, + "key": key, + "private-key": private_key, + }, ) - _snap_package.hold.assert_called_once_with() - - # Test without refresh - _snap_cache.reset_mock() - _snap_package.reset_mock() - self.charm._install_snap_packages([ - ("postgresql", {"revision": {platform.machine(): "42"}}) - ]) - _snap_cache.assert_called_once_with() - _snap_cache.return_value.__getitem__.assert_called_once_with("postgresql") - _snap_package.ensure.assert_not_called() - _snap_package.hold.assert_not_called() - - # test missing architecture - _snap_cache.reset_mock() - _snap_package.reset_mock() - _snap_package.present = True - with self.assertRaises(KeyError): - self.charm._install_snap_packages( - [("postgresql", {"revision": {"missingarch": "42"}})], - refresh=True, - ) - _snap_cache.assert_called_once_with() - _snap_cache.return_value.__getitem__.assert_called_once_with("postgresql") - assert not _snap_package.ensure.called - assert not _snap_package.hold.called - - @patch( - "subprocess.check_call", - side_effect=[None, subprocess.CalledProcessError(1, "fake command")], + harness.charm._update_certificate() + _request_certificate.assert_called_once_with(private_key) + + harness.charm.get_secret("unit", "ca") == ca + harness.charm.get_secret("unit", "cert") == cert + harness.charm.get_secret("unit", "key") == key + harness.charm.get_secret("unit", "private-key") == private_key + +@patch("charms.postgresql_k8s.v0.postgresql_tls.PostgreSQLTLS._request_certificate") +@patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=True) +def test_update_certificate_secrets(self, _, _request_certificate): + # If there is no current TLS files, _request_certificate should be called + # only when the certificates relation is established. + harness.charm._update_certificate() + _request_certificate.assert_not_called() + + # Test with already present TLS files (when they will be replaced by new ones). + ca = "fake CA" + cert = "fake certificate" + key = private_key = "fake private key" + harness.charm.set_secret("unit", "ca", ca) + harness.charm.set_secret("unit", "cert", cert) + harness.charm.set_secret("unit", "key", key) + harness.charm.set_secret("unit", "private-key", private_key) + + harness.charm._update_certificate() + _request_certificate.assert_called_once_with(private_key) + + harness.charm.get_secret("unit", "ca") == ca + harness.charm.get_secret("unit", "cert") == cert + harness.charm.get_secret("unit", "key") == key + harness.charm.get_secret("unit", "private-key") == private_key + +@patch_network_get(private_address="1.1.1.1") +@patch("charm.PostgresqlOperatorCharm._update_certificate") +@patch("charm.Patroni.stop_patroni") +def test_update_member_ip(self, _stop_patroni, _update_certificate): + # Test when the IP address of the unit hasn't changed. + with harness.hooks_disabled(): + harness.update_relation_data( + self.rel_id, + harness.charm.unit.name, + { + "ip": "1.1.1.1", + }, + ) + assert not (harness.charm._update_member_ip()) + relation_data = harness.get_relation_data(self.rel_id, harness.charm.unit.name) + assert relation_data.get("ip-to-remove") == None + _stop_patroni.assert_not_called() + _update_certificate.assert_not_called() + + # Test when the IP address of the unit has changed. + with harness.hooks_disabled(): + harness.update_relation_data( + self.rel_id, + harness.charm.unit.name, + { + "ip": "2.2.2.2", + }, + ) + assert (harness.charm._update_member_ip()) + relation_data = harness.get_relation_data(self.rel_id, harness.charm.unit.name) + assert relation_data.get("ip") == "1.1.1.1" + assert relation_data.get("ip-to-remove") == "2.2.2.2" + _stop_patroni.assert_called_once() + _update_certificate.assert_called_once() + +@patch_network_get(private_address="1.1.1.1") +@patch("charm.PostgresqlOperatorCharm.update_config") +@patch("charm.Patroni.render_file") +@patch("charms.postgresql_k8s.v0.postgresql_tls.PostgreSQLTLS.get_tls_files") +def test_push_tls_files_to_workload(self, _get_tls_files, _render_file, _update_config): + _get_tls_files.side_effect = [ + ("key", "ca", "cert"), + ("key", "ca", None), + ("key", None, "cert"), + (None, "ca", "cert"), + ] + _update_config.side_effect = [True, False, False, False] + + # Test when all TLS files are available. + assert (harness.charm.push_tls_files_to_workload()) + assert _render_file.call_count == 3 + + # Test when not all TLS files are available. + for _ in range(3): + _render_file.reset_mock() + assert not (harness.charm.push_tls_files_to_workload()) + assert _render_file.call_count == 2 + +@patch("charm.snap.SnapCache") +def test_is_workload_running(self, _snap_cache): + pg_snap = _snap_cache.return_value[POSTGRESQL_SNAP_NAME] + + pg_snap.present = False + assert not (harness.charm._is_workload_running) + + pg_snap.present = True + assert (harness.charm._is_workload_running) + +def test_get_available_memory(self): + meminfo = ( + "MemTotal: 16089488 kB" + "MemFree: 799284 kB" + "MemAvailable: 3926924 kB" + "Buffers: 187232 kB" + "Cached: 4445936 kB" + "SwapCached: 156012 kB" + "Active: 11890336 kB" ) - def test_is_storage_attached(self, _check_call): - # Test with attached storage. - is_storage_attached = self.charm._is_storage_attached() - _check_call.assert_called_once_with(["mountpoint", "-q", self.charm._storage_path]) - self.assertTrue(is_storage_attached) - - # Test with detached storage. - is_storage_attached = self.charm._is_storage_attached() - self.assertFalse(is_storage_attached) - - @patch("subprocess.check_call") - def test_reboot_on_detached_storage(self, _check_call): - mock_event = MagicMock() - self.charm._reboot_on_detached_storage(mock_event) - mock_event.defer.assert_called_once() - self.assertTrue(isinstance(self.charm.unit.status, WaitingStatus)) - _check_call.assert_called_once_with(["systemctl", "reboot"]) - - @patch_network_get(private_address="1.1.1.1") - @patch("charm.Patroni.restart_postgresql") - @patch("charm.Patroni.are_all_members_ready") - def test_restart(self, _are_all_members_ready, _restart_postgresql): - _are_all_members_ready.side_effect = [False, True, True] - - # Test when not all members are ready. - mock_event = MagicMock() - self.charm._restart(mock_event) - mock_event.defer.assert_called_once() - _restart_postgresql.assert_not_called() - - # Test a successful restart. - mock_event.defer.reset_mock() - self.charm._restart(mock_event) - self.assertFalse(isinstance(self.charm.unit.status, BlockedStatus)) - mock_event.defer.assert_not_called() - - # Test a failed restart. - _restart_postgresql.side_effect = RetryError(last_attempt=1) - self.charm._restart(mock_event) - self.assertTrue(isinstance(self.charm.unit.status, BlockedStatus)) - mock_event.defer.assert_not_called() - - @patch_network_get(private_address="1.1.1.1") - @patch("subprocess.check_output", return_value=b"C") - @patch("charm.snap.SnapCache") - @patch("charm.PostgresqlOperatorCharm._handle_postgresql_restart_need") - @patch("charm.Patroni.bulk_update_parameters_controller_by_patroni") - @patch("charm.Patroni.member_started", new_callable=PropertyMock) - @patch("charm.PostgresqlOperatorCharm._is_workload_running", new_callable=PropertyMock) - @patch("charm.Patroni.render_patroni_yml_file") - @patch("charm.PostgresqlOperatorCharm.is_tls_enabled", new_callable=PropertyMock) - def test_update_config( - self, - _is_tls_enabled, - _render_patroni_yml_file, - _is_workload_running, - _member_started, - _, - _handle_postgresql_restart_need, - __, - ___, - ): - with patch.object(PostgresqlOperatorCharm, "postgresql", Mock()) as postgresql_mock: - # Mock some properties. - postgresql_mock.is_tls_enabled = PropertyMock(side_effect=[False, False, False, False]) - _is_workload_running.side_effect = [False, False, True, True, False, True] - _member_started.side_effect = [True, True, False] - postgresql_mock.build_postgresql_parameters.return_value = {"test": "test"} - - # Test when only one of the two config options for profile limit memory is set. - self.harness.update_config({"profile-limit-memory": 1000}) - self.charm.update_config() - - # Test when only one of the two config options for profile limit memory is set. - self.harness.update_config( - {"profile_limit_memory": 1000}, unset={"profile-limit-memory"} - ) - self.charm.update_config() - - # Test when the two config options for profile limit memory are set at the same time. - _render_patroni_yml_file.reset_mock() - self.harness.update_config({"profile-limit-memory": 1000}) - with self.assertRaises(ValueError): - self.charm.update_config() - - # Test without TLS files available. - self.harness.update_config(unset={"profile-limit-memory", "profile_limit_memory"}) - with self.harness.hooks_disabled(): - self.harness.update_relation_data(self.rel_id, self.charm.unit.name, {"tls": ""}) - _is_tls_enabled.return_value = False - self.charm.update_config() - _render_patroni_yml_file.assert_called_once_with( - connectivity=True, - is_creating_backup=False, - enable_tls=False, - backup_id=None, - stanza=None, - restore_stanza=None, - parameters={"test": "test"}, - ) - _handle_postgresql_restart_need.assert_called_once_with(False) - self.assertNotIn( - "tls", self.harness.get_relation_data(self.rel_id, self.charm.unit.name) - ) - - # Test with TLS files available. - _handle_postgresql_restart_need.reset_mock() - self.harness.update_relation_data( - self.rel_id, self.charm.unit.name, {"tls": ""} - ) # Mock some data in the relation to test that it change. - _is_tls_enabled.return_value = True - _render_patroni_yml_file.reset_mock() - self.charm.update_config() - _render_patroni_yml_file.assert_called_once_with( - connectivity=True, - is_creating_backup=False, - enable_tls=True, - backup_id=None, - stanza=None, - restore_stanza=None, - parameters={"test": "test"}, - ) - _handle_postgresql_restart_need.assert_called_once() - self.assertNotIn( - "tls", - self.harness.get_relation_data( - self.rel_id, self.charm.unit.name - ), # The "tls" flag is set in handle_postgresql_restart_need. - ) - - # Test with workload not running yet. - self.harness.update_relation_data( - self.rel_id, self.charm.unit.name, {"tls": ""} - ) # Mock some data in the relation to test that it change. - _handle_postgresql_restart_need.reset_mock() - self.charm.update_config() - _handle_postgresql_restart_need.assert_not_called() - self.assertEqual( - self.harness.get_relation_data(self.rel_id, self.charm.unit.name)["tls"], "enabled" - ) - - # Test with member not started yet. - self.harness.update_relation_data( - self.rel_id, self.charm.unit.name, {"tls": ""} - ) # Mock some data in the relation to test that it doesn't change. - self.charm.update_config() - _handle_postgresql_restart_need.assert_not_called() - self.assertNotIn( - "tls", self.harness.get_relation_data(self.rel_id, self.charm.unit.name) - ) - - @patch("charm.PostgresqlOperatorCharm._update_relation_endpoints") - @patch("charm.PostgresqlOperatorCharm.primary_endpoint", new_callable=PropertyMock) - def test_on_cluster_topology_change(self, _primary_endpoint, _update_relation_endpoints): - # Mock the property value. - _primary_endpoint.side_effect = [None, "1.1.1.1"] - - # Test without an elected primary. - self.charm._on_cluster_topology_change(Mock()) - _update_relation_endpoints.assert_not_called() - # Test with an elected primary. - self.charm._on_cluster_topology_change(Mock()) - _update_relation_endpoints.assert_called_once() - - @patch( - "charm.PostgresqlOperatorCharm.primary_endpoint", - new_callable=PropertyMock, - return_value=None, + with patch("builtins.open", mock_open(read_data=meminfo)): + assert harness.charm.get_available_memory() == 16475635712 + + with patch("builtins.open", mock_open(read_data="")): + assert harness.charm.get_available_memory() == 0 + +@patch("charm.ClusterTopologyObserver") +@patch("charm.JujuVersion") +def test_juju_run_exec_divergence(self, _juju_version: Mock, _topology_observer: Mock): + # Juju 2 + _juju_version.from_environ.return_value.major = 2 + harness = Harness(PostgresqlOperatorCharm) + harness.begin() + _topology_observer.assert_called_once_with(harness.charm, "/usr/bin/juju-run") + _topology_observer.reset_mock() + + # Juju 3 + _juju_version.from_environ.return_value.major = 3 + harness = Harness(PostgresqlOperatorCharm) + harness.begin() + _topology_observer.assert_called_once_with(harness.charm, "/usr/bin/juju-exec") + +def test_client_relations(self): + # Test when the charm has no relations. + assert len(harness.charm.client_relations) == 0 + + # Test when the charm has some relations. + harness.add_relation("database", "application") + harness.add_relation("db", "legacy-application") + harness.add_relation("db-admin", "legacy-admin-application") + database_relation = harness.model.get_relation("database") + db_relation = harness.model.get_relation("db") + db_admin_relation = harness.model.get_relation("db-admin") + assert harness.charm.client_relations == [database_relation, db_relation, db_admin_relation] + +# +# Secrets +# + +def test_scope_obj(self): + assert harness.charm._scope_obj("app") == harness.charm.framework.model.app + assert harness.charm._scope_obj("unit") == harness.charm.framework.model.unit + assert harness.charm._scope_obj("test") is None + +@patch_network_get(private_address="1.1.1.1") +@patch("charm.PostgresqlOperatorCharm._on_leader_elected") +def test_get_secret(self, _): + # App level changes require leader privileges + harness.set_leader() + # Test application scope. + assert harness.charm.get_secret("app", "password") is None + harness.update_relation_data( + self.rel_id, harness.charm.app.name, {"password": "test-password"} ) - @patch("charm.PostgresqlOperatorCharm._update_relation_endpoints") - def test_on_cluster_topology_change_keep_blocked( - self, _update_relation_endpoints, _primary_endpoint - ): - self.harness.model.unit.status = WaitingStatus(PRIMARY_NOT_REACHABLE_MESSAGE) - - self.charm._on_cluster_topology_change(Mock()) - - _update_relation_endpoints.assert_not_called() - _primary_endpoint.assert_called_once_with() - self.assertTrue(isinstance(self.harness.model.unit.status, WaitingStatus)) - self.assertEqual(self.harness.model.unit.status.message, PRIMARY_NOT_REACHABLE_MESSAGE) - - @patch( - "charm.PostgresqlOperatorCharm.primary_endpoint", - new_callable=PropertyMock, - return_value="fake-unit", + assert harness.charm.get_secret("app", "password") == "test-password" + + # Unit level changes don't require leader privileges + harness.set_leader(False) + # Test unit scope. + assert harness.charm.get_secret("unit", "password") is None + harness.update_relation_data( + self.rel_id, harness.charm.unit.name, {"password": "test-password"} ) - @patch("charm.PostgresqlOperatorCharm._update_relation_endpoints") - def test_on_cluster_topology_change_clear_blocked( - self, _update_relation_endpoints, _primary_endpoint - ): - self.harness.model.unit.status = WaitingStatus(PRIMARY_NOT_REACHABLE_MESSAGE) - - self.charm._on_cluster_topology_change(Mock()) - - _update_relation_endpoints.assert_called_once_with() - _primary_endpoint.assert_called_once_with() - self.assertTrue(isinstance(self.harness.model.unit.status, ActiveStatus)) - - @patch("charm.PostgresqlOperatorCharm.postgresql", new_callable=PropertyMock) - @patch("config.subprocess") - def test_validate_config_options(self, _, _charm_lib): - _charm_lib.return_value.get_postgresql_text_search_configs.return_value = [] - _charm_lib.return_value.validate_date_style.return_value = [] - _charm_lib.return_value.get_postgresql_timezones.return_value = [] - - # Test instance_default_text_search_config exception - with self.harness.hooks_disabled(): - self.harness.update_config({"instance_default_text_search_config": "pg_catalog.test"}) - - with self.assertRaises(ValueError) as e: - self.charm._validate_config_options() - assert ( - e.msg == "instance_default_text_search_config config option has an invalid value" - ) + assert harness.charm.get_secret("unit", "password") == "test-password" + +@patch_network_get(private_address="1.1.1.1") +@patch("charm.PostgresqlOperatorCharm._on_leader_elected") +@patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=True) +def test_on_get_password_secrets(self, mock1, mock2): + # Create a mock event and set passwords in peer relation data. + harness.set_leader() + mock_event = MagicMock(params={}) + harness.charm.set_secret("app", "operator-password", "test-password") + harness.charm.set_secret("app", "replication-password", "replication-test-password") + + # Test providing an invalid username. + mock_event.params["username"] = "user" + harness.charm._on_get_password(mock_event) + mock_event.fail.assert_called_once() + mock_event.set_results.assert_not_called() + + # Test without providing the username option. + mock_event.reset_mock() + del mock_event.params["username"] + harness.charm._on_get_password(mock_event) + mock_event.set_results.assert_called_once_with({"password": "test-password"}) + + # Also test providing the username option. + mock_event.reset_mock() + mock_event.params["username"] = "replication" + harness.charm._on_get_password(mock_event) + mock_event.set_results.assert_called_once_with({"password": "replication-test-password"}) + +@parameterized.expand([("app"), ("unit")]) +@patch_network_get(private_address="1.1.1.1") +@patch("charm.PostgresqlOperatorCharm._on_leader_elected") +@patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=True) +def test_get_secret_secrets(self, scope, _, __): + harness.set_leader() + + assert harness.charm.get_secret(scope, "operator-password") is None + harness.charm.set_secret(scope, "operator-password", "test-password") + assert harness.charm.get_secret(scope, "operator-password") == "test-password" + +@patch_network_get(private_address="1.1.1.1") +@patch("charm.PostgresqlOperatorCharm._on_leader_elected") +def test_set_secret(self, _): + harness.set_leader() + + # Test application scope. + assert "password" not in harness.get_relation_data(self.rel_id, harness.charm.app.name) + harness.charm.set_secret("app", "password", "test-password") + assert ( + harness.get_relation_data(self.rel_id, harness.charm.app.name)["password"] + == "test-password" + ) + harness.charm.set_secret("app", "password", None) + assert "password" not in harness.get_relation_data(self.rel_id, harness.charm.app.name) + + # Test unit scope. + assert "password" not in harness.get_relation_data(self.rel_id, harness.charm.unit.name) + harness.charm.set_secret("unit", "password", "test-password") + assert ( + harness.get_relation_data(self.rel_id, harness.charm.unit.name)["password"] + == "test-password" + ) + harness.charm.set_secret("unit", "password", None) + assert "password" not in harness.get_relation_data(self.rel_id, harness.charm.unit.name) + + with pytest.raises(RuntimeError): + harness.charm.set_secret("test", "password", "test") + +@parameterized.expand([("app", True), ("unit", True), ("unit", False)]) +@patch_network_get(private_address="1.1.1.1") +@patch("charm.PostgresqlOperatorCharm._on_leader_elected") +@patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=True) +def test_set_reset_new_secret(self, scope, is_leader, _, __): + """NOTE: currently ops.testing seems to allow for non-leader to set secrets too!""" + # App has to be leader, unit can be either + harness.set_leader(is_leader) + # Getting current password + harness.charm.set_secret(scope, "new-secret", "bla") + assert harness.charm.get_secret(scope, "new-secret") == "bla" + + # Reset new secret + harness.charm.set_secret(scope, "new-secret", "blablabla") + assert harness.charm.get_secret(scope, "new-secret") == "blablabla" + + # Set another new secret + harness.charm.set_secret(scope, "new-secret2", "blablabla") + assert harness.charm.get_secret(scope, "new-secret2") == "blablabla" + +@parameterized.expand([("app", True), ("unit", True), ("unit", False)]) +@patch_network_get(private_address="1.1.1.1") +@patch("charm.PostgresqlOperatorCharm._on_leader_elected") +@patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=True) +def test_invalid_secret(self, scope, is_leader, _, __): + # App has to be leader, unit can be either + harness.set_leader(is_leader) + + with pytest.raises(RelationDataTypeError): + harness.charm.set_secret(scope, "somekey", 1) + + harness.charm.set_secret(scope, "somekey", "") + assert harness.charm.get_secret(scope, "somekey") is None + +@pytest.mark.usefixtures("use_caplog") +@patch_network_get(private_address="1.1.1.1") +@patch("charm.PostgresqlOperatorCharm._on_leader_elected") +def test_delete_password(self, _): + """NOTE: currently ops.testing seems to allow for non-leader to remove secrets too!""" + harness.set_leader(True) + harness.update_relation_data( + self.rel_id, harness.charm.app.name, {"replication": "somepw"} + ) + harness.charm.remove_secret("app", "replication") + assert harness.charm.get_secret("app", "replication") is None - _charm_lib.return_value.get_postgresql_text_search_configs.assert_called_once_with() - _charm_lib.return_value.get_postgresql_text_search_configs.return_value = [ - "pg_catalog.test" - ] - - # Test request_date_style exception - with self.harness.hooks_disabled(): - self.harness.update_config({"request_date_style": "ISO, TEST"}) - - with self.assertRaises(ValueError) as e: - self.charm._validate_config_options() - assert e.msg == "request_date_style config option has an invalid value" - - _charm_lib.return_value.validate_date_style.assert_called_once_with("ISO, TEST") - _charm_lib.return_value.validate_date_style.return_value = ["ISO, TEST"] - - # Test request_time_zone exception - with self.harness.hooks_disabled(): - self.harness.update_config({"request_time_zone": "TEST_ZONE"}) - - with self.assertRaises(ValueError) as e: - self.charm._validate_config_options() - assert e.msg == "request_time_zone config option has an invalid value" - - _charm_lib.return_value.get_postgresql_timezones.assert_called_once_with() - _charm_lib.return_value.get_postgresql_timezones.return_value = ["TEST_ZONE"] - - @patch_network_get(private_address="1.1.1.1") - @patch("charm.snap.SnapCache") - @patch("charm.PostgresqlOperatorCharm._update_relation_endpoints") - @patch("charm.PostgresqlOperatorCharm.primary_endpoint", new_callable=PropertyMock) - @patch("backups.PostgreSQLBackups.check_stanza") - @patch("backups.PostgreSQLBackups.coordinate_stanza_fields") - @patch("backups.PostgreSQLBackups.start_stop_pgbackrest_service") - @patch("charm.Patroni.reinitialize_postgresql") - @patch("charm.Patroni.member_replication_lag", new_callable=PropertyMock) - @patch("charm.PostgresqlOperatorCharm.is_primary") - @patch("charm.Patroni.member_started", new_callable=PropertyMock) - @patch("charm.Patroni.start_patroni") - @patch("charm.PostgresqlOperatorCharm.update_config") - @patch("charm.PostgresqlOperatorCharm._update_member_ip") - @patch("charm.PostgresqlOperatorCharm._reconfigure_cluster") - @patch("ops.framework.EventBase.defer") - def test_on_peer_relation_changed( - self, - _defer, - _reconfigure_cluster, - _update_member_ip, - _update_config, - _start_patroni, - _member_started, - _is_primary, - _member_replication_lag, - _reinitialize_postgresql, - _start_stop_pgbackrest_service, - _coordinate_stanza_fields, - _check_stanza, - _primary_endpoint, - _update_relation_endpoints, - _, - ): - # Test an uninitialized cluster. - mock_event = Mock() - with self.harness.hooks_disabled(): - self.harness.update_relation_data( - self.rel_id, self.charm.app.name, {"cluster_initialised": ""} - ) - self.charm._on_peer_relation_changed(mock_event) - mock_event.defer.assert_called_once() - _reconfigure_cluster.assert_not_called() - - # Test an initialized cluster and this is the leader unit - # (but it fails to reconfigure the cluster). - mock_event.defer.reset_mock() - with self.harness.hooks_disabled(): - self.harness.update_relation_data( - self.rel_id, - self.charm.app.name, - {"cluster_initialised": "True", "members_ips": '["1.1.1.1"]'}, - ) - self.harness.set_leader() - _reconfigure_cluster.return_value = False - self.charm._on_peer_relation_changed(mock_event) - _reconfigure_cluster.assert_called_once_with(mock_event) - mock_event.defer.assert_called_once() - - # Test when the leader can reconfigure the cluster. - mock_event.defer.reset_mock() - _reconfigure_cluster.reset_mock() - _reconfigure_cluster.return_value = True - _update_member_ip.return_value = False - _member_started.return_value = True - _primary_endpoint.return_value = "1.1.1.1" - self.harness.model.unit.status = WaitingStatus("awaiting for cluster to start") - self.charm._on_peer_relation_changed(mock_event) - mock_event.defer.assert_not_called() - _reconfigure_cluster.assert_called_once_with(mock_event) - _update_member_ip.assert_called_once() - _update_config.assert_called_once() - _start_patroni.assert_called_once() - _update_relation_endpoints.assert_called_once() - self.assertIsInstance(self.harness.model.unit.status, ActiveStatus) - - # Test when the cluster member updates its IP. - _update_member_ip.reset_mock() - _update_config.reset_mock() - _start_patroni.reset_mock() - _update_relation_endpoints.reset_mock() - _update_member_ip.return_value = True - self.charm._on_peer_relation_changed(mock_event) - _update_member_ip.assert_called_once() - _update_config.assert_not_called() - _start_patroni.assert_not_called() - _update_relation_endpoints.assert_not_called() - - # Test when the unit fails to update the Patroni configuration. - _update_member_ip.return_value = False - _update_config.side_effect = RetryError(last_attempt=1) - self.charm._on_peer_relation_changed(mock_event) - _update_config.assert_called_once() - _start_patroni.assert_not_called() - _update_relation_endpoints.assert_not_called() - self.assertIsInstance(self.harness.model.unit.status, BlockedStatus) - - # Test when Patroni hasn't started yet in the unit. - _update_config.side_effect = None - _member_started.return_value = False - self.charm._on_peer_relation_changed(mock_event) - _start_patroni.assert_called_once() - _update_relation_endpoints.assert_not_called() - self.assertIsInstance(self.harness.model.unit.status, WaitingStatus) - - # Test when Patroni has already started but this is a replica with a - # huge or unknown lag. - self.relation = self.harness.model.get_relation(self._peer_relation, self.rel_id) - _member_started.return_value = True - for values in itertools.product([True, False], ["0", "1000", "1001", "unknown"]): - _defer.reset_mock() - _check_stanza.reset_mock() - _start_stop_pgbackrest_service.reset_mock() - _is_primary.return_value = values[0] - _member_replication_lag.return_value = values[1] - self.charm.unit.status = ActiveStatus() - self.charm.on.database_peers_relation_changed.emit(self.relation) - if _is_primary.return_value == values[0] or int(values[1]) <= 1000: - _defer.assert_not_called() - _check_stanza.assert_called_once() - _start_stop_pgbackrest_service.assert_called_once() - self.assertIsInstance(self.charm.unit.status, ActiveStatus) - else: - _defer.assert_called_once() - _check_stanza.assert_not_called() - _start_stop_pgbackrest_service.assert_not_called() - self.assertIsInstance(self.charm.unit.status, MaintenanceStatus) - - # Test when it was not possible to start the pgBackRest service yet. - self.relation = self.harness.model.get_relation(self._peer_relation, self.rel_id) - _member_started.return_value = True - _defer.reset_mock() - _coordinate_stanza_fields.reset_mock() - _check_stanza.reset_mock() - _start_stop_pgbackrest_service.return_value = False - self.charm.on.database_peers_relation_changed.emit(self.relation) - _defer.assert_called_once() - _coordinate_stanza_fields.assert_not_called() - _check_stanza.assert_not_called() - - # Test the last calls been made when it was possible to start the - # pgBackRest service. - _defer.reset_mock() - _start_stop_pgbackrest_service.return_value = True - self.charm.on.database_peers_relation_changed.emit(self.relation) - _defer.assert_not_called() - _coordinate_stanza_fields.assert_called_once() - _check_stanza.assert_called_once() - - @patch_network_get(private_address="1.1.1.1") - @patch("charm.PostgresqlOperatorCharm._add_members") - @patch("charm.PostgresqlOperatorCharm._remove_from_members_ips") - @patch("charm.Patroni.remove_raft_member") - def test_reconfigure_cluster( - self, _remove_raft_member, _remove_from_members_ips, _add_members - ): - # Test when no change is needed in the member IP. - mock_event = Mock() - mock_event.unit = self.charm.unit - mock_event.relation.data = {mock_event.unit: {}} - self.assertTrue(self.charm._reconfigure_cluster(mock_event)) - _remove_raft_member.assert_not_called() - _remove_from_members_ips.assert_not_called() - _add_members.assert_called_once_with(mock_event) - - # Test when a change is needed in the member IP, but it fails. - _remove_raft_member.side_effect = RemoveRaftMemberFailedError - _add_members.reset_mock() - ip_to_remove = "1.1.1.1" - relation_data = {mock_event.unit: {"ip-to-remove": ip_to_remove}} - mock_event.relation.data = relation_data - self.assertFalse(self.charm._reconfigure_cluster(mock_event)) - _remove_raft_member.assert_called_once_with(ip_to_remove) - _remove_from_members_ips.assert_not_called() - _add_members.assert_not_called() - - # Test when a change is needed in the member IP, and it succeeds - # (but the old IP was already been removed). - _remove_raft_member.reset_mock() - _remove_raft_member.side_effect = None - _add_members.reset_mock() - mock_event.relation.data = relation_data - self.assertTrue(self.charm._reconfigure_cluster(mock_event)) - _remove_raft_member.assert_called_once_with(ip_to_remove) - _remove_from_members_ips.assert_not_called() - _add_members.assert_called_once_with(mock_event) - - # Test when the old IP wasn't removed yet. - _remove_raft_member.reset_mock() - _add_members.reset_mock() - mock_event.relation.data = relation_data - with self.harness.hooks_disabled(): - self.harness.update_relation_data( - self.rel_id, self.charm.app.name, {"members_ips": '["' + ip_to_remove + '"]'} - ) - self.assertTrue(self.charm._reconfigure_cluster(mock_event)) - _remove_raft_member.assert_called_once_with(ip_to_remove) - _remove_from_members_ips.assert_called_once_with(ip_to_remove) - _add_members.assert_called_once_with(mock_event) - - @patch("charms.postgresql_k8s.v0.postgresql_tls.PostgreSQLTLS._request_certificate") - @patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=False) - def test_update_certificate(self, _, _request_certificate): - # If there is no current TLS files, _request_certificate should be called - # only when the certificates relation is established. - self.charm._update_certificate() - _request_certificate.assert_not_called() - - # Test with already present TLS files (when they will be replaced by new ones). - ca = "fake CA" - cert = "fake certificate" - key = private_key = "fake private key" - with self.harness.hooks_disabled(): - self.harness.update_relation_data( - self.rel_id, - self.charm.unit.name, - { - "ca": ca, - "cert": cert, - "key": key, - "private-key": private_key, - }, - ) - self.charm._update_certificate() - _request_certificate.assert_called_once_with(private_key) - - self.harness.charm.get_secret("unit", "ca") == ca - self.harness.charm.get_secret("unit", "cert") == cert - self.harness.charm.get_secret("unit", "key") == key - self.harness.charm.get_secret("unit", "private-key") == private_key - - @patch("charms.postgresql_k8s.v0.postgresql_tls.PostgreSQLTLS._request_certificate") - @patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=True) - def test_update_certificate_secrets(self, _, _request_certificate): - # If there is no current TLS files, _request_certificate should be called - # only when the certificates relation is established. - self.charm._update_certificate() - _request_certificate.assert_not_called() - - # Test with already present TLS files (when they will be replaced by new ones). - ca = "fake CA" - cert = "fake certificate" - key = private_key = "fake private key" - self.harness.charm.set_secret("unit", "ca", ca) - self.harness.charm.set_secret("unit", "cert", cert) - self.harness.charm.set_secret("unit", "key", key) - self.harness.charm.set_secret("unit", "private-key", private_key) - - self.charm._update_certificate() - _request_certificate.assert_called_once_with(private_key) - - self.harness.charm.get_secret("unit", "ca") == ca - self.harness.charm.get_secret("unit", "cert") == cert - self.harness.charm.get_secret("unit", "key") == key - self.harness.charm.get_secret("unit", "private-key") == private_key - - @patch_network_get(private_address="1.1.1.1") - @patch("charm.PostgresqlOperatorCharm._update_certificate") - @patch("charm.Patroni.stop_patroni") - def test_update_member_ip(self, _stop_patroni, _update_certificate): - # Test when the IP address of the unit hasn't changed. - with self.harness.hooks_disabled(): - self.harness.update_relation_data( - self.rel_id, - self.charm.unit.name, - { - "ip": "1.1.1.1", - }, - ) - self.assertFalse(self.charm._update_member_ip()) - relation_data = self.harness.get_relation_data(self.rel_id, self.charm.unit.name) - self.assertEqual(relation_data.get("ip-to-remove"), None) - _stop_patroni.assert_not_called() - _update_certificate.assert_not_called() - - # Test when the IP address of the unit has changed. - with self.harness.hooks_disabled(): - self.harness.update_relation_data( - self.rel_id, - self.charm.unit.name, - { - "ip": "2.2.2.2", - }, - ) - self.assertTrue(self.charm._update_member_ip()) - relation_data = self.harness.get_relation_data(self.rel_id, self.charm.unit.name) - self.assertEqual(relation_data.get("ip"), "1.1.1.1") - self.assertEqual(relation_data.get("ip-to-remove"), "2.2.2.2") - _stop_patroni.assert_called_once() - _update_certificate.assert_called_once() - - @patch_network_get(private_address="1.1.1.1") - @patch("charm.PostgresqlOperatorCharm.update_config") - @patch("charm.Patroni.render_file") - @patch("charms.postgresql_k8s.v0.postgresql_tls.PostgreSQLTLS.get_tls_files") - def test_push_tls_files_to_workload(self, _get_tls_files, _render_file, _update_config): - _get_tls_files.side_effect = [ - ("key", "ca", "cert"), - ("key", "ca", None), - ("key", None, "cert"), - (None, "ca", "cert"), - ] - _update_config.side_effect = [True, False, False, False] - - # Test when all TLS files are available. - self.assertTrue(self.charm.push_tls_files_to_workload()) - self.assertEqual(_render_file.call_count, 3) - - # Test when not all TLS files are available. - for _ in range(3): - _render_file.reset_mock() - self.assertFalse(self.charm.push_tls_files_to_workload()) - self.assertEqual(_render_file.call_count, 2) - - @patch("charm.snap.SnapCache") - def test_is_workload_running(self, _snap_cache): - pg_snap = _snap_cache.return_value[POSTGRESQL_SNAP_NAME] - - pg_snap.present = False - self.assertFalse(self.charm._is_workload_running) - - pg_snap.present = True - self.assertTrue(self.charm._is_workload_running) - - def test_get_available_memory(self): - meminfo = ( - "MemTotal: 16089488 kB" - "MemFree: 799284 kB" - "MemAvailable: 3926924 kB" - "Buffers: 187232 kB" - "Cached: 4445936 kB" - "SwapCached: 156012 kB" - "Active: 11890336 kB" - ) + harness.set_leader(False) + harness.update_relation_data( + self.rel_id, harness.charm.unit.name, {"somekey": "somevalue"} + ) + harness.charm.remove_secret("unit", "somekey") + assert harness.charm.get_secret("unit", "somekey") is None - with patch("builtins.open", mock_open(read_data=meminfo)): - self.assertEqual(self.charm.get_available_memory(), 16475635712) - - with patch("builtins.open", mock_open(read_data="")): - self.assertEqual(self.charm.get_available_memory(), 0) - - @patch("charm.ClusterTopologyObserver") - @patch("charm.JujuVersion") - def test_juju_run_exec_divergence(self, _juju_version: Mock, _topology_observer: Mock): - # Juju 2 - _juju_version.from_environ.return_value.major = 2 - harness = Harness(PostgresqlOperatorCharm) - harness.begin() - _topology_observer.assert_called_once_with(harness.charm, "/usr/bin/juju-run") - _topology_observer.reset_mock() - - # Juju 3 - _juju_version.from_environ.return_value.major = 3 - harness = Harness(PostgresqlOperatorCharm) - harness.begin() - _topology_observer.assert_called_once_with(harness.charm, "/usr/bin/juju-exec") - - def test_client_relations(self): - # Test when the charm has no relations. - self.assertEqual(self.charm.client_relations, []) - - # Test when the charm has some relations. - self.harness.add_relation("database", "application") - self.harness.add_relation("db", "legacy-application") - self.harness.add_relation("db-admin", "legacy-admin-application") - database_relation = self.harness.model.get_relation("database") - db_relation = self.harness.model.get_relation("db") - db_admin_relation = self.harness.model.get_relation("db-admin") - self.assertEqual( - self.charm.client_relations, [database_relation, db_relation, db_admin_relation] + harness.set_leader(True) + with self._caplog.at_level(logging.ERROR): + harness.charm.remove_secret("app", "replication") + assert ( + "Non-existing field 'replication' was attempted to be removed" in self._caplog.text ) - # - # Secrets - # - - def test_scope_obj(self): - assert self.charm._scope_obj("app") == self.charm.framework.model.app - assert self.charm._scope_obj("unit") == self.charm.framework.model.unit - assert self.charm._scope_obj("test") is None - - @patch_network_get(private_address="1.1.1.1") - @patch("charm.PostgresqlOperatorCharm._on_leader_elected") - def test_get_secret(self, _): - # App level changes require leader privileges - self.harness.set_leader() - # Test application scope. - assert self.charm.get_secret("app", "password") is None - self.harness.update_relation_data( - self.rel_id, self.charm.app.name, {"password": "test-password"} - ) - assert self.charm.get_secret("app", "password") == "test-password" - - # Unit level changes don't require leader privileges - self.harness.set_leader(False) - # Test unit scope. - assert self.charm.get_secret("unit", "password") is None - self.harness.update_relation_data( - self.rel_id, self.charm.unit.name, {"password": "test-password"} - ) - assert self.charm.get_secret("unit", "password") == "test-password" - - @patch_network_get(private_address="1.1.1.1") - @patch("charm.PostgresqlOperatorCharm._on_leader_elected") - @patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=True) - def test_on_get_password_secrets(self, mock1, mock2): - # Create a mock event and set passwords in peer relation data. - self.harness.set_leader() - mock_event = MagicMock(params={}) - self.harness.charm.set_secret("app", "operator-password", "test-password") - self.harness.charm.set_secret("app", "replication-password", "replication-test-password") - - # Test providing an invalid username. - mock_event.params["username"] = "user" - self.charm._on_get_password(mock_event) - mock_event.fail.assert_called_once() - mock_event.set_results.assert_not_called() - - # Test without providing the username option. - mock_event.reset_mock() - del mock_event.params["username"] - self.charm._on_get_password(mock_event) - mock_event.set_results.assert_called_once_with({"password": "test-password"}) - - # Also test providing the username option. - mock_event.reset_mock() - mock_event.params["username"] = "replication" - self.charm._on_get_password(mock_event) - mock_event.set_results.assert_called_once_with({"password": "replication-test-password"}) - - @parameterized.expand([("app"), ("unit")]) - @patch_network_get(private_address="1.1.1.1") - @patch("charm.PostgresqlOperatorCharm._on_leader_elected") - @patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=True) - def test_get_secret_secrets(self, scope, _, __): - self.harness.set_leader() - - assert self.charm.get_secret(scope, "operator-password") is None - self.charm.set_secret(scope, "operator-password", "test-password") - assert self.charm.get_secret(scope, "operator-password") == "test-password" - - @patch_network_get(private_address="1.1.1.1") - @patch("charm.PostgresqlOperatorCharm._on_leader_elected") - def test_set_secret(self, _): - self.harness.set_leader() - - # Test application scope. - assert "password" not in self.harness.get_relation_data(self.rel_id, self.charm.app.name) - self.charm.set_secret("app", "password", "test-password") + harness.charm.remove_secret("unit", "somekey") + assert "Non-existing field 'somekey' was attempted to be removed" in self._caplog.text + + harness.charm.remove_secret("app", "non-existing-secret") assert ( - self.harness.get_relation_data(self.rel_id, self.charm.app.name)["password"] - == "test-password" + "Non-existing field 'non-existing-secret' was attempted to be removed" + in self._caplog.text ) - self.charm.set_secret("app", "password", None) - assert "password" not in self.harness.get_relation_data(self.rel_id, self.charm.app.name) - # Test unit scope. - assert "password" not in self.harness.get_relation_data(self.rel_id, self.charm.unit.name) - self.charm.set_secret("unit", "password", "test-password") + harness.charm.remove_secret("unit", "non-existing-secret") assert ( - self.harness.get_relation_data(self.rel_id, self.charm.unit.name)["password"] - == "test-password" + "Non-existing field 'non-existing-secret' was attempted to be removed" + in self._caplog.text ) - self.charm.set_secret("unit", "password", None) - assert "password" not in self.harness.get_relation_data(self.rel_id, self.charm.unit.name) - - with self.assertRaises(RuntimeError): - self.charm.set_secret("test", "password", "test") - - @parameterized.expand([("app", True), ("unit", True), ("unit", False)]) - @patch_network_get(private_address="1.1.1.1") - @patch("charm.PostgresqlOperatorCharm._on_leader_elected") - @patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=True) - def test_set_reset_new_secret(self, scope, is_leader, _, __): - """NOTE: currently ops.testing seems to allow for non-leader to set secrets too!""" - # App has to be leader, unit can be either - self.harness.set_leader(is_leader) - # 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", True), ("unit", True), ("unit", False)]) - @patch_network_get(private_address="1.1.1.1") - @patch("charm.PostgresqlOperatorCharm._on_leader_elected") - @patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=True) - def test_invalid_secret(self, scope, is_leader, _, __): - # App has to be leader, unit can be either - self.harness.set_leader(is_leader) - - with self.assertRaises(RelationDataTypeError): - self.harness.charm.set_secret(scope, "somekey", 1) - - self.harness.charm.set_secret(scope, "somekey", "") - assert self.harness.charm.get_secret(scope, "somekey") is None - - @pytest.mark.usefixtures("use_caplog") - @patch_network_get(private_address="1.1.1.1") - @patch("charm.PostgresqlOperatorCharm._on_leader_elected") - def test_delete_password(self, _): - """NOTE: currently ops.testing seems to allow for non-leader to remove secrets too!""" - self.harness.set_leader(True) - self.harness.update_relation_data( - self.rel_id, self.charm.app.name, {"replication": "somepw"} + +@patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=True) +@patch_network_get(private_address="1.1.1.1") +@patch("charm.PostgresqlOperatorCharm._on_leader_elected") +@pytest.mark.usefixtures("use_caplog") +def test_delete_existing_password_secrets(self, _, __): + """NOTE: currently ops.testing seems to allow for non-leader to remove secrets too!""" + harness.set_leader(True) + harness.charm.set_secret("app", "operator-password", "somepw") + harness.charm.remove_secret("app", "operator-password") + assert harness.charm.get_secret("app", "operator-password") is None + + harness.set_leader(False) + harness.charm.set_secret("unit", "operator-password", "somesecret") + harness.charm.remove_secret("unit", "operator-password") + assert harness.charm.get_secret("unit", "operator-password") is None + + harness.set_leader(True) + with self._caplog.at_level(logging.ERROR): + harness.charm.remove_secret("app", "operator-password") + assert ( + "Non-existing secret operator-password was attempted to be removed." + in self._caplog.text ) - self.harness.charm.remove_secret("app", "replication") - assert self.harness.charm.get_secret("app", "replication") is None - self.harness.set_leader(False) - self.harness.update_relation_data( - self.rel_id, self.charm.unit.name, {"somekey": "somevalue"} + harness.charm.remove_secret("unit", "operator-password") + assert ( + "Non-existing secret operator-password was attempted to be removed." + in self._caplog.text ) - self.harness.charm.remove_secret("unit", "somekey") - assert self.harness.charm.get_secret("unit", "somekey") is None - - self.harness.set_leader(True) - with self._caplog.at_level(logging.ERROR): - self.harness.charm.remove_secret("app", "replication") - assert ( - "Non-existing field 'replication' was attempted to be removed" in self._caplog.text - ) - self.harness.charm.remove_secret("unit", "somekey") - assert "Non-existing field 'somekey' was attempted to be removed" in self._caplog.text + harness.charm.remove_secret("app", "non-existing-secret") + assert ( + "Non-existing field 'non-existing-secret' was attempted to be removed" + in self._caplog.text + ) - self.harness.charm.remove_secret("app", "non-existing-secret") - assert ( - "Non-existing field 'non-existing-secret' was attempted to be removed" - in self._caplog.text - ) + harness.charm.remove_secret("unit", "non-existing-secret") + assert ( + "Non-existing field 'non-existing-secret' was attempted to be removed" + in self._caplog.text + ) - self.harness.charm.remove_secret("unit", "non-existing-secret") - assert ( - "Non-existing field 'non-existing-secret' was attempted to be removed" - in self._caplog.text - ) +@parameterized.expand([("app", True), ("unit", True), ("unit", False)]) +@patch_network_get(private_address="1.1.1.1") +@patch("charm.PostgresqlOperatorCharm._on_leader_elected") +@patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=True) +def test_migration_from_databag(self, scope, is_leader, _, __): + """Check if we're moving on to use secrets when live upgrade from databag to Secrets usage.""" + # App has to be leader, unit can be either + harness.set_leader(is_leader) + + # Getting current password + entity = getattr(harness.charm, scope) + harness.update_relation_data(self.rel_id, entity.name, {"operator-password": "bla"}) + assert harness.charm.get_secret(scope, "operator-password") == "bla" + + # Reset new secret + harness.charm.set_secret(scope, "operator-password", "blablabla") + assert harness.charm.model.get_secret(label=f"postgresql.{scope}") + assert harness.charm.get_secret(scope, "operator-password") == "blablabla" + assert "operator-password" not in harness.get_relation_data( + self.rel_id, getattr(harness.charm, scope).name + ) - @patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=True) - @patch_network_get(private_address="1.1.1.1") - @patch("charm.PostgresqlOperatorCharm._on_leader_elected") - @pytest.mark.usefixtures("use_caplog") - def test_delete_existing_password_secrets(self, _, __): - """NOTE: currently ops.testing seems to allow for non-leader to remove secrets too!""" - self.harness.set_leader(True) - self.harness.charm.set_secret("app", "operator-password", "somepw") - self.harness.charm.remove_secret("app", "operator-password") - assert self.harness.charm.get_secret("app", "operator-password") is None - - self.harness.set_leader(False) - self.harness.charm.set_secret("unit", "operator-password", "somesecret") - self.harness.charm.remove_secret("unit", "operator-password") - assert self.harness.charm.get_secret("unit", "operator-password") is None - - self.harness.set_leader(True) - with self._caplog.at_level(logging.ERROR): - self.harness.charm.remove_secret("app", "operator-password") - assert ( - "Non-existing secret operator-password was attempted to be removed." - in self._caplog.text - ) +@parameterized.expand([("app", True), ("unit", True), ("unit", False)]) +@patch_network_get(private_address="1.1.1.1") +@patch("charm.PostgresqlOperatorCharm._on_leader_elected") +@patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=True) +def test_migration_from_single_secret(self, scope, is_leader, _, __): + """Check if we're moving on to use secrets when live upgrade from databag to Secrets usage.""" + # App has to be leader, unit can be either + harness.set_leader(is_leader) + + secret = harness.charm.app.add_secret({"operator-password": "bla"}) + + # Getting current password + entity = getattr(harness.charm, scope) + harness.update_relation_data( + self.rel_id, entity.name, {SECRET_INTERNAL_LABEL: secret.id} + ) + assert harness.charm.get_secret(scope, "operator-password") == "bla" + + # Reset new secret + # Only the leader can set app secret content. + with harness.hooks_disabled(): + harness.set_leader(True) + harness.charm.set_secret(scope, "operator-password", "blablabla") + with harness.hooks_disabled(): + harness.set_leader(is_leader) + assert harness.charm.model.get_secret(label=f"postgresql.{scope}") + assert harness.charm.get_secret(scope, "operator-password") == "blablabla" + assert SECRET_INTERNAL_LABEL not in harness.get_relation_data( + self.rel_id, getattr(harness.charm, scope).name + ) - self.harness.charm.remove_secret("unit", "operator-password") - assert ( - "Non-existing secret operator-password was attempted to be removed." - in self._caplog.text - ) +@patch("charms.rolling_ops.v0.rollingops.RollingOpsManager._on_acquire_lock") +@patch("charm.wait_fixed", return_value=wait_fixed(0)) +@patch("charm.Patroni.reload_patroni_configuration") +@patch("charm.PostgresqlOperatorCharm._unit_ip") +@patch("charm.PostgresqlOperatorCharm.is_tls_enabled", new_callable=PropertyMock) +def test_handle_postgresql_restart_need( + self, _is_tls_enabled, _, _reload_patroni_configuration, __, _restart +): + with patch.object(PostgresqlOperatorCharm, "postgresql", Mock()) as postgresql_mock: + for values in itertools.product( + [True, False], [True, False], [True, False], [True, False], [True, False] + ): + _reload_patroni_configuration.reset_mock() + _restart.reset_mock() + with harness.hooks_disabled(): + harness.update_relation_data( + self.rel_id, harness.charm.unit.name, {"tls": ""} + ) + harness.update_relation_data( + self.rel_id, + harness.charm.unit.name, + {"postgresql_restarted": ("True" if values[4] else "")}, + ) - self.harness.charm.remove_secret("app", "non-existing-secret") - assert ( - "Non-existing field 'non-existing-secret' was attempted to be removed" - in self._caplog.text - ) + _is_tls_enabled.return_value = values[1] + postgresql_mock.is_tls_enabled = PropertyMock(return_value=values[2]) + postgresql_mock.is_restart_pending = PropertyMock(return_value=values[3]) - self.harness.charm.remove_secret("unit", "non-existing-secret") - assert ( - "Non-existing field 'non-existing-secret' was attempted to be removed" - in self._caplog.text + harness.charm._handle_postgresql_restart_need(values[0]) + _reload_patroni_configuration.assert_called_once() + ( + assert "tls" in harness.get_relation_data(self.rel_id, harness.charm.unit) + if values[0] + else assert "tls" not in harness.get_relation_data(self.rel_id, harness.charm.unit) ) - - @parameterized.expand([("app", True), ("unit", True), ("unit", False)]) - @patch_network_get(private_address="1.1.1.1") - @patch("charm.PostgresqlOperatorCharm._on_leader_elected") - @patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=True) - def test_migration_from_databag(self, scope, is_leader, _, __): - """Check if we're moving on to use secrets when live upgrade from databag to Secrets usage.""" - # App has to be leader, unit can be either - self.harness.set_leader(is_leader) - - # Getting current password - entity = getattr(self.charm, scope) - self.harness.update_relation_data(self.rel_id, entity.name, {"operator-password": "bla"}) - assert self.harness.charm.get_secret(scope, "operator-password") == "bla" - - # Reset new secret - self.harness.charm.set_secret(scope, "operator-password", "blablabla") - assert self.harness.charm.model.get_secret(label=f"postgresql.{scope}") - assert self.harness.charm.get_secret(scope, "operator-password") == "blablabla" - assert "operator-password" not in self.harness.get_relation_data( - self.rel_id, getattr(self.charm, scope).name - ) - - @parameterized.expand([("app", True), ("unit", True), ("unit", False)]) - @patch_network_get(private_address="1.1.1.1") - @patch("charm.PostgresqlOperatorCharm._on_leader_elected") - @patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=True) - def test_migration_from_single_secret(self, scope, is_leader, _, __): - """Check if we're moving on to use secrets when live upgrade from databag to Secrets usage.""" - # App has to be leader, unit can be either - self.harness.set_leader(is_leader) - - secret = self.harness.charm.app.add_secret({"operator-password": "bla"}) - - # Getting current password - entity = getattr(self.charm, scope) - self.harness.update_relation_data( - self.rel_id, entity.name, {SECRET_INTERNAL_LABEL: secret.id} - ) - assert self.harness.charm.get_secret(scope, "operator-password") == "bla" - - # Reset new secret - # Only the leader can set app secret content. - with self.harness.hooks_disabled(): - self.harness.set_leader(True) - self.harness.charm.set_secret(scope, "operator-password", "blablabla") - with self.harness.hooks_disabled(): - self.harness.set_leader(is_leader) - assert self.harness.charm.model.get_secret(label=f"postgresql.{scope}") - assert self.harness.charm.get_secret(scope, "operator-password") == "blablabla" - assert SECRET_INTERNAL_LABEL not in self.harness.get_relation_data( - self.rel_id, getattr(self.charm, scope).name - ) - - @patch("charms.rolling_ops.v0.rollingops.RollingOpsManager._on_acquire_lock") - @patch("charm.wait_fixed", return_value=wait_fixed(0)) - @patch("charm.Patroni.reload_patroni_configuration") - @patch("charm.PostgresqlOperatorCharm._unit_ip") - @patch("charm.PostgresqlOperatorCharm.is_tls_enabled", new_callable=PropertyMock) - def test_handle_postgresql_restart_need( - self, _is_tls_enabled, _, _reload_patroni_configuration, __, _restart - ): - with patch.object(PostgresqlOperatorCharm, "postgresql", Mock()) as postgresql_mock: - for values in itertools.product( - [True, False], [True, False], [True, False], [True, False], [True, False] - ): - _reload_patroni_configuration.reset_mock() - _restart.reset_mock() - with self.harness.hooks_disabled(): - self.harness.update_relation_data( - self.rel_id, self.charm.unit.name, {"tls": ""} - ) - self.harness.update_relation_data( - self.rel_id, - self.charm.unit.name, - {"postgresql_restarted": ("True" if values[4] else "")}, - ) - - _is_tls_enabled.return_value = values[1] - postgresql_mock.is_tls_enabled = PropertyMock(return_value=values[2]) - postgresql_mock.is_restart_pending = PropertyMock(return_value=values[3]) - - self.charm._handle_postgresql_restart_need(values[0]) - _reload_patroni_configuration.assert_called_once() + if (values[1] != values[2]) or values[3]: + assert "postgresql_restarted" not in harness.get_relation_data(self.rel_id, harness.charm.unit) + _restart.assert_called_once() + else: ( - self.assertIn( - "tls", self.harness.get_relation_data(self.rel_id, self.charm.unit) - ) - if values[0] - else self.assertNotIn( - "tls", self.harness.get_relation_data(self.rel_id, self.charm.unit) - ) + assert "postgresql_restarted" in harness.get_relation_data(self.rel_id, harness.charm.unit) + if values[4] + else assert "postgresql_restarted" not in harness.get_relation_data(self.rel_id, harness.charm.unit) ) - if (values[1] != values[2]) or values[3]: - self.assertNotIn( - "postgresql_restarted", - self.harness.get_relation_data(self.rel_id, self.charm.unit), - ) - _restart.assert_called_once() - else: - ( - self.assertIn( - "postgresql_restarted", - self.harness.get_relation_data(self.rel_id, self.charm.unit), - ) - if values[4] - else self.assertNotIn( - "postgresql_restarted", - self.harness.get_relation_data(self.rel_id, self.charm.unit), - ) - ) - _restart.assert_not_called() - - @patch("charm.PostgresqlOperatorCharm._update_relation_endpoints") - @patch("charm.PostgresqlOperatorCharm.primary_endpoint", new_callable=PropertyMock) - @patch("charm.PostgresqlOperatorCharm.update_config") - @patch("charm.PostgresqlOperatorCharm._remove_from_members_ips") - @patch("charm.Patroni.are_all_members_ready") - @patch("charm.PostgresqlOperatorCharm._get_ips_to_remove") - @patch("charm.PostgresqlOperatorCharm._updated_synchronous_node_count") - @patch("charm.Patroni.remove_raft_member") - @patch("charm.PostgresqlOperatorCharm._unit_ip") - @patch("charm.Patroni.get_member_ip") - def test_on_peer_relation_departed( - self, - _get_member_ip, - _unit_ip, - _remove_raft_member, - _updated_synchronous_node_count, - _get_ips_to_remove, - _are_all_members_ready, - _remove_from_members_ips, - _update_config, - _primary_endpoint, - _update_relation_endpoints, - ): - # Test when the current unit is the departing unit. - self.charm.unit.status = ActiveStatus() - event = Mock() - event.departing_unit = self.harness.charm.unit - self.charm._on_peer_relation_departed(event) - _remove_raft_member.assert_not_called() - event.defer.assert_not_called() - _updated_synchronous_node_count.assert_not_called() - _get_ips_to_remove.assert_not_called() - _remove_from_members_ips.assert_not_called() - _update_config.assert_not_called() - _update_relation_endpoints.assert_not_called() - self.assertIsInstance(self.charm.unit.status, ActiveStatus) - - # Test when the current unit is not the departing unit, but removing - # the member from the raft cluster fails. - _remove_raft_member.side_effect = RemoveRaftMemberFailedError - event.departing_unit = Unit( - f"{self.charm.app.name}/1", None, self.harness.charm.app._backend, {} + _restart.assert_not_called() + +@patch("charm.PostgresqlOperatorCharm._update_relation_endpoints") +@patch("charm.PostgresqlOperatorCharm.primary_endpoint", new_callable=PropertyMock) +@patch("charm.PostgresqlOperatorCharm.update_config") +@patch("charm.PostgresqlOperatorCharm._remove_from_members_ips") +@patch("charm.Patroni.are_all_members_ready") +@patch("charm.PostgresqlOperatorCharm._get_ips_to_remove") +@patch("charm.PostgresqlOperatorCharm._updated_synchronous_node_count") +@patch("charm.Patroni.remove_raft_member") +@patch("charm.PostgresqlOperatorCharm._unit_ip") +@patch("charm.Patroni.get_member_ip") +def test_on_peer_relation_departed( + self, + _get_member_ip, + _unit_ip, + _remove_raft_member, + _updated_synchronous_node_count, + _get_ips_to_remove, + _are_all_members_ready, + _remove_from_members_ips, + _update_config, + _primary_endpoint, + _update_relation_endpoints, +): + # Test when the current unit is the departing unit. + harness.charm.unit.status = ActiveStatus() + event = Mock() + event.departing_unit = harness.charm.unit + harness.charm._on_peer_relation_departed(event) + _remove_raft_member.assert_not_called() + event.defer.assert_not_called() + _updated_synchronous_node_count.assert_not_called() + _get_ips_to_remove.assert_not_called() + _remove_from_members_ips.assert_not_called() + _update_config.assert_not_called() + _update_relation_endpoints.assert_not_called() + assert isinstance(harness.charm.unit.status, ActiveStatus) + + # Test when the current unit is not the departing unit, but removing + # the member from the raft cluster fails. + _remove_raft_member.side_effect = RemoveRaftMemberFailedError + event.departing_unit = Unit( + f"{harness.charm.app.name}/1", None, harness.charm.app._backend, {} + ) + mock_ip_address = "1.1.1.1" + _get_member_ip.return_value = mock_ip_address + harness.charm._on_peer_relation_departed(event) + _remove_raft_member.assert_called_once_with(mock_ip_address) + event.defer.assert_called_once() + _updated_synchronous_node_count.assert_not_called() + _get_ips_to_remove.assert_not_called() + _remove_from_members_ips.assert_not_called() + _update_config.assert_not_called() + _update_relation_endpoints.assert_not_called() + assert isinstance(harness.charm.unit.status, ActiveStatus) + + # Test when the member is successfully removed from the raft cluster, + # but the unit is not the leader. + _remove_raft_member.reset_mock() + event.defer.reset_mock() + _remove_raft_member.side_effect = None + harness.charm._on_peer_relation_departed(event) + _remove_raft_member.assert_called_once_with(mock_ip_address) + event.defer.assert_not_called() + _updated_synchronous_node_count.assert_not_called() + _get_ips_to_remove.assert_not_called() + _remove_from_members_ips.assert_not_called() + _update_config.assert_not_called() + _update_relation_endpoints.assert_not_called() + assert isinstance(harness.charm.unit.status, ActiveStatus) + + # Test when the unit is the leader, but the cluster hasn't initialized yet, + # or it was unable to set synchronous_node_count. + _remove_raft_member.reset_mock() + with harness.hooks_disabled(): + harness.set_leader() + harness.charm._on_peer_relation_departed(event) + _remove_raft_member.assert_called_once_with(mock_ip_address) + event.defer.assert_called_once() + _updated_synchronous_node_count.assert_not_called() + _get_ips_to_remove.assert_not_called() + _remove_from_members_ips.assert_not_called() + _update_config.assert_not_called() + _update_relation_endpoints.assert_not_called() + assert isinstance(harness.charm.unit.status, ActiveStatus) + + _remove_raft_member.reset_mock() + event.defer.reset_mock() + _updated_synchronous_node_count.return_value = False + with harness.hooks_disabled(): + harness.update_relation_data( + self.rel_id, harness.charm.app.name, {"cluster_initialised": "True"} ) - mock_ip_address = "1.1.1.1" - _get_member_ip.return_value = mock_ip_address - self.charm._on_peer_relation_departed(event) - _remove_raft_member.assert_called_once_with(mock_ip_address) - event.defer.assert_called_once() - _updated_synchronous_node_count.assert_not_called() - _get_ips_to_remove.assert_not_called() - _remove_from_members_ips.assert_not_called() - _update_config.assert_not_called() - _update_relation_endpoints.assert_not_called() - self.assertIsInstance(self.charm.unit.status, ActiveStatus) - - # Test when the member is successfully removed from the raft cluster, - # but the unit is not the leader. - _remove_raft_member.reset_mock() - event.defer.reset_mock() - _remove_raft_member.side_effect = None - self.charm._on_peer_relation_departed(event) - _remove_raft_member.assert_called_once_with(mock_ip_address) - event.defer.assert_not_called() - _updated_synchronous_node_count.assert_not_called() - _get_ips_to_remove.assert_not_called() - _remove_from_members_ips.assert_not_called() - _update_config.assert_not_called() - _update_relation_endpoints.assert_not_called() - self.assertIsInstance(self.charm.unit.status, ActiveStatus) - - # Test when the unit is the leader, but the cluster hasn't initialized yet, - # or it was unable to set synchronous_node_count. - _remove_raft_member.reset_mock() - with self.harness.hooks_disabled(): - self.harness.set_leader() - self.charm._on_peer_relation_departed(event) - _remove_raft_member.assert_called_once_with(mock_ip_address) - event.defer.assert_called_once() - _updated_synchronous_node_count.assert_not_called() - _get_ips_to_remove.assert_not_called() - _remove_from_members_ips.assert_not_called() - _update_config.assert_not_called() - _update_relation_endpoints.assert_not_called() - self.assertIsInstance(self.charm.unit.status, ActiveStatus) - - _remove_raft_member.reset_mock() - event.defer.reset_mock() - _updated_synchronous_node_count.return_value = False - with self.harness.hooks_disabled(): - self.harness.update_relation_data( - self.rel_id, self.charm.app.name, {"cluster_initialised": "True"} - ) - self.charm._on_peer_relation_departed(event) - _remove_raft_member.assert_called_once_with(mock_ip_address) - event.defer.assert_called_once() - _updated_synchronous_node_count.assert_called_once_with(1) - _get_ips_to_remove.assert_not_called() - _remove_from_members_ips.assert_not_called() - _update_config.assert_not_called() - _update_relation_endpoints.assert_not_called() - self.assertIsInstance(self.charm.unit.status, ActiveStatus) - - # Test when there is more units in the cluster. - _remove_raft_member.reset_mock() - event.defer.reset_mock() - _updated_synchronous_node_count.reset_mock() - self.harness.add_relation_unit(self.rel_id, f"{self.charm.app.name}/2") - self.charm._on_peer_relation_departed(event) - _remove_raft_member.assert_called_once_with(mock_ip_address) - event.defer.assert_called_once() - _updated_synchronous_node_count.assert_called_once_with(2) - _get_ips_to_remove.assert_not_called() - _remove_from_members_ips.assert_not_called() - _update_config.assert_not_called() - _update_relation_endpoints.assert_not_called() - self.assertIsInstance(self.charm.unit.status, ActiveStatus) - - # Test when the cluster is initialised, and it could set synchronous_node_count, - # but there is no IPs to be removed from the members list. - _remove_raft_member.reset_mock() - event.defer.reset_mock() - _updated_synchronous_node_count.reset_mock() - _updated_synchronous_node_count.return_value = True - self.charm._on_peer_relation_departed(event) - _remove_raft_member.assert_called_once_with(mock_ip_address) - event.defer.assert_not_called() - _updated_synchronous_node_count.assert_called_once_with(2) - _get_ips_to_remove.assert_called_once() - _remove_from_members_ips.assert_not_called() - _update_config.assert_not_called() - _update_relation_endpoints.assert_not_called() - self.assertIsInstance(self.charm.unit.status, ActiveStatus) - - # Test when there are IPs to be removed from the members list, but not all - # the members are ready yet. - _remove_raft_member.reset_mock() - _updated_synchronous_node_count.reset_mock() - _get_ips_to_remove.reset_mock() - ips_to_remove = ["2.2.2.2", "3.3.3.3"] - _get_ips_to_remove.return_value = ips_to_remove - _are_all_members_ready.return_value = False - self.charm._on_peer_relation_departed(event) - _remove_raft_member.assert_called_once_with(mock_ip_address) - event.defer.assert_called_once() - _updated_synchronous_node_count.assert_called_once_with(2) - _get_ips_to_remove.assert_called_once() - _remove_from_members_ips.assert_not_called() - _update_config.assert_not_called() - _update_relation_endpoints.assert_not_called() - self.assertIsInstance(self.charm.unit.status, ActiveStatus) - - # Test when all members are ready. - _remove_raft_member.reset_mock() - event.defer.reset_mock() - _updated_synchronous_node_count.reset_mock() - _get_ips_to_remove.reset_mock() - _are_all_members_ready.return_value = True - self.charm._on_peer_relation_departed(event) - _remove_raft_member.assert_called_once_with(mock_ip_address) - event.defer.assert_not_called() - _updated_synchronous_node_count.assert_called_once_with(2) - _get_ips_to_remove.assert_called_once() - _remove_from_members_ips.assert_has_calls([call(ips_to_remove[0]), call(ips_to_remove[1])]) - self.assertEqual(_update_config.call_count, 2) - self.assertEqual(_update_relation_endpoints.call_count, 2) - self.assertIsInstance(self.charm.unit.status, ActiveStatus) - - # Test when the primary is not reachable yet. - _remove_raft_member.reset_mock() - event.defer.reset_mock() - _updated_synchronous_node_count.reset_mock() - _get_ips_to_remove.reset_mock() - _remove_from_members_ips.reset_mock() - _update_config.reset_mock() - _update_relation_endpoints.reset_mock() - _primary_endpoint.return_value = None - self.charm._on_peer_relation_departed(event) - _remove_raft_member.assert_called_once_with(mock_ip_address) - event.defer.assert_not_called() - _updated_synchronous_node_count.assert_called_once_with(2) - _get_ips_to_remove.assert_called_once() - _remove_from_members_ips.assert_called_once() - _update_config.assert_called_once() - _update_relation_endpoints.assert_not_called() - self.assertIsInstance(self.charm.unit.status, WaitingStatus) - - @patch("charm.PostgresqlOperatorCharm._update_relation_endpoints") - @patch("charm.PostgresqlOperatorCharm.primary_endpoint", new_callable=PropertyMock) - def test_update_new_unit_status(self, _primary_endpoint, _update_relation_endpoints): - # Test when the charm is blocked. - _primary_endpoint.return_value = "endpoint" - self.charm.unit.status = BlockedStatus("fake blocked status") - self.charm._update_new_unit_status() - _update_relation_endpoints.assert_called_once() - self.assertIsInstance(self.charm.unit.status, BlockedStatus) - - # Test when the charm is not blocked. - _update_relation_endpoints.reset_mock() - self.charm.unit.status = WaitingStatus() - self.charm._update_new_unit_status() - _update_relation_endpoints.assert_called_once() - self.assertIsInstance(self.charm.unit.status, ActiveStatus) - - # Test when the primary endpoint is not reachable yet. - _update_relation_endpoints.reset_mock() - _primary_endpoint.return_value = None - self.charm._update_new_unit_status() - _update_relation_endpoints.assert_not_called() - self.assertIsInstance(self.charm.unit.status, WaitingStatus) + harness.charm._on_peer_relation_departed(event) + _remove_raft_member.assert_called_once_with(mock_ip_address) + event.defer.assert_called_once() + _updated_synchronous_node_count.assert_called_once_with(1) + _get_ips_to_remove.assert_not_called() + _remove_from_members_ips.assert_not_called() + _update_config.assert_not_called() + _update_relation_endpoints.assert_not_called() + assert isinstance(harness.charm.unit.status, ActiveStatus) + + # Test when there is more units in the cluster. + _remove_raft_member.reset_mock() + event.defer.reset_mock() + _updated_synchronous_node_count.reset_mock() + harness.add_relation_unit(self.rel_id, f"{harness.charm.app.name}/2") + harness.charm._on_peer_relation_departed(event) + _remove_raft_member.assert_called_once_with(mock_ip_address) + event.defer.assert_called_once() + _updated_synchronous_node_count.assert_called_once_with(2) + _get_ips_to_remove.assert_not_called() + _remove_from_members_ips.assert_not_called() + _update_config.assert_not_called() + _update_relation_endpoints.assert_not_called() + assert isinstance(harness.charm.unit.status, ActiveStatus) + + # Test when the cluster is initialised, and it could set synchronous_node_count, + # but there is no IPs to be removed from the members list. + _remove_raft_member.reset_mock() + event.defer.reset_mock() + _updated_synchronous_node_count.reset_mock() + _updated_synchronous_node_count.return_value = True + harness.charm._on_peer_relation_departed(event) + _remove_raft_member.assert_called_once_with(mock_ip_address) + event.defer.assert_not_called() + _updated_synchronous_node_count.assert_called_once_with(2) + _get_ips_to_remove.assert_called_once() + _remove_from_members_ips.assert_not_called() + _update_config.assert_not_called() + _update_relation_endpoints.assert_not_called() + assert isinstance(harness.charm.unit.status, ActiveStatus) + + # Test when there are IPs to be removed from the members list, but not all + # the members are ready yet. + _remove_raft_member.reset_mock() + _updated_synchronous_node_count.reset_mock() + _get_ips_to_remove.reset_mock() + ips_to_remove = ["2.2.2.2", "3.3.3.3"] + _get_ips_to_remove.return_value = ips_to_remove + _are_all_members_ready.return_value = False + harness.charm._on_peer_relation_departed(event) + _remove_raft_member.assert_called_once_with(mock_ip_address) + event.defer.assert_called_once() + _updated_synchronous_node_count.assert_called_once_with(2) + _get_ips_to_remove.assert_called_once() + _remove_from_members_ips.assert_not_called() + _update_config.assert_not_called() + _update_relation_endpoints.assert_not_called() + assert isinstance(harness.charm.unit.status, ActiveStatus) + + # Test when all members are ready. + _remove_raft_member.reset_mock() + event.defer.reset_mock() + _updated_synchronous_node_count.reset_mock() + _get_ips_to_remove.reset_mock() + _are_all_members_ready.return_value = True + harness.charm._on_peer_relation_departed(event) + _remove_raft_member.assert_called_once_with(mock_ip_address) + event.defer.assert_not_called() + _updated_synchronous_node_count.assert_called_once_with(2) + _get_ips_to_remove.assert_called_once() + _remove_from_members_ips.assert_has_calls([call(ips_to_remove[0]), call(ips_to_remove[1])]) + assert _update_config.call_count == 2 + assert _update_relation_endpoints.call_count == 2 + assert isinstance(harness.charm.unit.status, ActiveStatus) + + # Test when the primary is not reachable yet. + _remove_raft_member.reset_mock() + event.defer.reset_mock() + _updated_synchronous_node_count.reset_mock() + _get_ips_to_remove.reset_mock() + _remove_from_members_ips.reset_mock() + _update_config.reset_mock() + _update_relation_endpoints.reset_mock() + _primary_endpoint.return_value = None + harness.charm._on_peer_relation_departed(event) + _remove_raft_member.assert_called_once_with(mock_ip_address) + event.defer.assert_not_called() + _updated_synchronous_node_count.assert_called_once_with(2) + _get_ips_to_remove.assert_called_once() + _remove_from_members_ips.assert_called_once() + _update_config.assert_called_once() + _update_relation_endpoints.assert_not_called() + assert isinstance(harness.charm.unit.status, WaitingStatus) + +@patch("charm.PostgresqlOperatorCharm._update_relation_endpoints") +@patch("charm.PostgresqlOperatorCharm.primary_endpoint", new_callable=PropertyMock) +def test_update_new_unit_status(self, _primary_endpoint, _update_relation_endpoints): + # Test when the charm is blocked. + _primary_endpoint.return_value = "endpoint" + harness.charm.unit.status = BlockedStatus("fake blocked status") + harness.charm._update_new_unit_status() + _update_relation_endpoints.assert_called_once() + assert isinstance(harness.charm.unit.status, BlockedStatus) + + # Test when the charm is not blocked. + _update_relation_endpoints.reset_mock() + harness.charm.unit.status = WaitingStatus() + harness.charm._update_new_unit_status() + _update_relation_endpoints.assert_called_once() + assert isinstance(harness.charm.unit.status, ActiveStatus) + + # Test when the primary endpoint is not reachable yet. + _update_relation_endpoints.reset_mock() + _primary_endpoint.return_value = None + harness.charm._update_new_unit_status() + _update_relation_endpoints.assert_not_called() + assert isinstance(harness.charm.unit.status, WaitingStatus) From a4f8b853a0b4f1a9f76bee4d1a9183359b450387 Mon Sep 17 00:00:00 2001 From: lucasgameiroborges Date: Tue, 2 Apr 2024 22:02:19 +0000 Subject: [PATCH 02/10] second version --- tests/unit/test_charm.py | 471 +++++++++++++++++++++------------------ tox.ini | 2 +- 2 files changed, 253 insertions(+), 220 deletions(-) diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py index 845893547e..e391258ace 100644 --- a/tests/unit/test_charm.py +++ b/tests/unit/test_charm.py @@ -46,110 +46,95 @@ def harness(): harness = Harness(PostgresqlOperatorCharm) harness.begin() - #rel_id = harness.add_relation(PEER, harness.charm.app.name) - harness.add_relation(PEER, harness.charm.app.name) harness.add_relation("upgrade", harness.charm.app.name) yield harness harness.cleanup() -@pytest.fixture -def use_caplog(self, caplog): - self._caplog = caplog - @patch_network_get(private_address="1.1.1.1") -@patch("charm.subprocess.check_call") -@patch("charm.snap.SnapCache") -@patch("charm.PostgresqlOperatorCharm._install_snap_packages") -@patch("charm.PostgresqlOperatorCharm._reboot_on_detached_storage") -@patch( - "charm.PostgresqlOperatorCharm._is_storage_attached", - side_effect=[False, True, True], -) -def test_on_install( - self, - _is_storage_attached, - _reboot_on_detached_storage, - _install_snap_packages, - _snap_cache, - _check_call, -): - # Test without storage. - harness.charm.on.install.emit() - _reboot_on_detached_storage.assert_called_once() - pg_snap = _snap_cache.return_value[POSTGRESQL_SNAP_NAME] - - # Test without adding Patroni resource. - harness.charm.on.install.emit() - # Assert that the needed calls were made. - _install_snap_packages.assert_called_once_with(packages=SNAP_PACKAGES) - assert pg_snap.alias.call_count == 2 - pg_snap.alias.assert_any_call("psql") - pg_snap.alias.assert_any_call("patronictl") - - assert _check_call.call_count == 3 - _check_call.assert_any_call("mkdir -p /home/snap_daemon".split()) - _check_call.assert_any_call("chown snap_daemon:snap_daemon /home/snap_daemon".split()) - _check_call.assert_any_call("usermod -d /home/snap_daemon snap_daemon".split()) - - # Assert the status set by the event handler. - assert (isinstance(harness.model.unit.status, WaitingStatus)) +def test_on_install(harness): + with patch("charm.subprocess.check_call") as _check_call, patch( + "charm.snap.SnapCache" + ) as _snap_cache, patch( + "charm.PostgresqlOperatorCharm._install_snap_packages" + ) as _install_snap_packages, patch( + "charm.PostgresqlOperatorCharm._reboot_on_detached_storage" + ) as _reboot_on_detached_storage, patch( + "charm.PostgresqlOperatorCharm._is_storage_attached", + side_effect=[False, True, True], + ) as _is_storage_attached: + rel_id = harness.add_relation(PEER, harness.charm.app.name) + # Test without storage. + harness.charm.on.install.emit() + _reboot_on_detached_storage.assert_called_once() + pg_snap = _snap_cache.return_value[POSTGRESQL_SNAP_NAME] + + # Test without adding Patroni resource. + harness.charm.on.install.emit() + # Assert that the needed calls were made. + _install_snap_packages.assert_called_once_with(packages=SNAP_PACKAGES) + assert pg_snap.alias.call_count == 2 + pg_snap.alias.assert_any_call("psql") + pg_snap.alias.assert_any_call("patronictl") + + assert _check_call.call_count == 3 + _check_call.assert_any_call("mkdir -p /home/snap_daemon".split()) + _check_call.assert_any_call("chown snap_daemon:snap_daemon /home/snap_daemon".split()) + _check_call.assert_any_call("usermod -d /home/snap_daemon snap_daemon".split()) + + # Assert the status set by the event handler. + assert (isinstance(harness.model.unit.status, WaitingStatus)) @patch_network_get(private_address="1.1.1.1") -@patch("charm.logger.exception") -@patch("charm.subprocess.check_call") -@patch("charm.snap.SnapCache") -@patch("charm.PostgresqlOperatorCharm._install_snap_packages") -@patch("charm.PostgresqlOperatorCharm._reboot_on_detached_storage") -@patch( - "charm.PostgresqlOperatorCharm._is_storage_attached", - side_effect=[False, True, True], -) -def test_on_install_failed_to_create_home( - self, - _is_storage_attached, - _reboot_on_detached_storage, - _install_snap_packages, - _snap_cache, - _check_call, - _logger_exception, -): - # Test without storage. - harness.charm.on.install.emit() - _reboot_on_detached_storage.assert_called_once() - pg_snap = _snap_cache.return_value[POSTGRESQL_SNAP_NAME] - _check_call.side_effect = [subprocess.CalledProcessError(-1, ["test"])] - - # Test without adding Patroni resource. - harness.charm.on.install.emit() - # Assert that the needed calls were made. - _install_snap_packages.assert_called_once_with(packages=SNAP_PACKAGES) - assert pg_snap.alias.call_count == 2 - pg_snap.alias.assert_any_call("psql") - pg_snap.alias.assert_any_call("patronictl") - - _logger_exception.assert_called_once_with("Unable to create snap_daemon home dir") - - # Assert the status set by the event handler. - assert (isinstance(harness.model.unit.status, WaitingStatus)) +def test_on_install_failed_to_create_home(harness): + with patch("charm.subprocess.check_call") as _check_call, patch( + "charm.snap.SnapCache" + ) as _snap_cache, patch( + "charm.PostgresqlOperatorCharm._install_snap_packages" + ) as _install_snap_packages, patch( + "charm.PostgresqlOperatorCharm._reboot_on_detached_storage" + ) as _reboot_on_detached_storage, patch( + "charm.PostgresqlOperatorCharm._is_storage_attached", + side_effect=[False, True, True], + ) as _is_storage_attached, patch( + "charm.logger.exception" + ) as _logger_exception: + rel_id = harness.add_relation(PEER, harness.charm.app.name) + # Test without storage. + harness.charm.on.install.emit() + _reboot_on_detached_storage.assert_called_once() + pg_snap = _snap_cache.return_value[POSTGRESQL_SNAP_NAME] + _check_call.side_effect = [subprocess.CalledProcessError(-1, ["test"])] + + # Test without adding Patroni resource. + harness.charm.on.install.emit() + # Assert that the needed calls were made. + _install_snap_packages.assert_called_once_with(packages=SNAP_PACKAGES) + assert pg_snap.alias.call_count == 2 + pg_snap.alias.assert_any_call("psql") + pg_snap.alias.assert_any_call("patronictl") + + _logger_exception.assert_called_once_with("Unable to create snap_daemon home dir") + + # Assert the status set by the event handler. + assert (isinstance(harness.model.unit.status, WaitingStatus)) @patch_network_get(private_address="1.1.1.1") -@patch("charm.PostgresqlOperatorCharm._install_snap_packages") -@patch("charm.PostgresqlOperatorCharm._is_storage_attached", return_value=True) -def test_on_install_snap_failure( - self, - _is_storage_attached, - _install_snap_packages, -): - # Mock the result of the call. - _install_snap_packages.side_effect = snap.SnapError - # Trigger the hook. - harness.charm.on.install.emit() - # Assert that the needed calls were made. - _install_snap_packages.assert_called_once() - assert (isinstance(harness.model.unit.status, BlockedStatus)) +def test_on_install_snap_failure(harness): + with patch("charm.PostgresqlOperatorCharm._install_snap_packages") as _install_snap_packages, patch( + "charm.PostgresqlOperatorCharm._is_storage_attached", return_value=True + ) as _is_storage_attached: + rel_id = harness.add_relation(PEER, harness.charm.app.name) + # Mock the result of the call. + _install_snap_packages.side_effect = snap.SnapError + # Trigger the hook. + harness.charm.on.install.emit() + # Assert that the needed calls were made. + _install_snap_packages.assert_called_once() + assert (isinstance(harness.model.unit.status, BlockedStatus)) @patch_network_get(private_address="1.1.1.1") -def test_patroni_scrape_config_no_tls(self): +def test_patroni_scrape_config_no_tls(harness): + rel_id = harness.add_relation(PEER, harness.charm.app.name) result = harness.charm.patroni_scrape_config() assert result == [ @@ -167,7 +152,8 @@ def test_patroni_scrape_config_no_tls(self): return_value=True, new_callable=PropertyMock, ) -def test_patroni_scrape_config_tls(self, _): +def test_patroni_scrape_config_tls(harness, _): + rel_id = harness.add_relation(PEER, harness.charm.app.name) result = harness.charm.patroni_scrape_config() assert result == [ @@ -185,7 +171,8 @@ def test_patroni_scrape_config_tls(self, _): return_value={"1.1.1.1", "1.1.1.2"}, ) @patch("charm.PostgresqlOperatorCharm._patroni", new_callable=PropertyMock) -def test_primary_endpoint(self, _patroni, _): +def test_primary_endpoint(harness, _patroni, _): + rel_id = harness.add_relation(PEER, harness.charm.app.name) _patroni.return_value.get_member_ip.return_value = "1.1.1.1" _patroni.return_value.get_primary.return_value = sentinel.primary assert harness.charm.primary_endpoint == "1.1.1.1" @@ -200,7 +187,8 @@ def test_primary_endpoint(self, _patroni, _): return_value={"1.1.1.1", "1.1.1.2"}, ) @patch("charm.PostgresqlOperatorCharm._patroni", new_callable=PropertyMock) -def test_primary_endpoint_no_peers(self, _patroni, _, __): +def test_primary_endpoint_no_peers(harness, _patroni, _, __): + rel_id = harness.add_relation(PEER, harness.charm.app.name) assert harness.charm.primary_endpoint is None assert not _patroni.return_value.get_member_ip.called @@ -214,8 +202,9 @@ def test_primary_endpoint_no_peers(self, _patroni, _, __): @patch("charm.PostgresqlOperatorCharm.update_config") @patch_network_get(private_address="1.1.1.1") def test_on_leader_elected( - self, _update_config, _primary_endpoint, _update_relation_endpoints + harness, _update_config, _primary_endpoint, _update_relation_endpoints ): + rel_id = harness.add_relation(PEER, harness.charm.app.name) # Assert that there is no password in the peer relation. assert harness.charm._peers.data[harness.charm.app].get("operator-password", None) == None @@ -245,14 +234,15 @@ def test_on_leader_elected( _update_relation_endpoints.assert_called_once() # Assert it was not called again. assert (isinstance(harness.model.unit.status, WaitingStatus)) -def test_is_cluster_initialised(self): +def test_is_cluster_initialised(harness): + rel_id = harness.add_relation(PEER, harness.charm.app.name) # Test when the cluster was not initialised yet. assert not (harness.charm.is_cluster_initialised) # Test when the cluster was already initialised. with harness.hooks_disabled(): harness.update_relation_data( - self.rel_id, harness.charm.app.name, {"cluster_initialised": "True"} + rel_id, harness.charm.app.name, {"cluster_initialised": "True"} ) assert (harness.charm.is_cluster_initialised) @@ -262,13 +252,14 @@ def test_is_cluster_initialised(self): @patch("charm.PostgresqlOperatorCharm.enable_disable_extensions") @patch("charm.PostgresqlOperatorCharm.is_cluster_initialised", new_callable=PropertyMock) def test_on_config_changed( - self, + harness, _is_cluster_initialised, _enable_disable_extensions, _set_up_relation, _update_config, _validate_config_options, ): + rel_id = harness.add_relation(PEER, harness.charm.app.name) # Test when the cluster was not initialised yet. _is_cluster_initialised.return_value = False harness.charm.on.config_changed.emit() @@ -334,7 +325,8 @@ def test_on_config_changed( _set_up_relation.assert_called_once() @patch("subprocess.check_output", return_value=b"C") -def test_check_extension_dependencies(self, _): +def test_check_extension_dependencies(harness, _): + rel_id = harness.add_relation(PEER, harness.charm.app.name) with patch.object(PostgresqlOperatorCharm, "postgresql", Mock()) as _: # Test when plugins dependencies exception is not caused config = { @@ -360,11 +352,13 @@ def test_check_extension_dependencies(self, _): assert harness.model.unit.status.message == EXTENSIONS_DEPENDENCY_MESSAGE @patch("subprocess.check_output", return_value=b"C") -def test_enable_disable_extensions(self, _): +def test_enable_disable_extensions(harness, caplog, _): + rel_id = harness.add_relation(PEER, harness.charm.app.name) with patch.object(PostgresqlOperatorCharm, "postgresql", Mock()) as postgresql_mock: # Test when all extensions install/uninstall succeed. postgresql_mock.enable_disable_extension.side_effect = None - with self.assertNoLogs("charm", "ERROR"): + with caplog.at_level(logging.ERROR): + assert len(caplog.records) == 0 harness.charm.enable_disable_extensions() assert postgresql_mock.enable_disable_extensions.call_count == 1 @@ -373,16 +367,17 @@ def test_enable_disable_extensions(self, _): postgresql_mock.enable_disable_extensions.side_effect = ( PostgreSQLEnableDisableExtensionError ) - with self.assertLogs("charm", "ERROR") as logs: + with caplog.at_level(logging.ERROR): harness.charm.enable_disable_extensions() assert postgresql_mock.enable_disable_extensions.call_count == 1 - assert "failed to change plugins" in logs.output + assert "failed to change plugins" in [rec.message for rec in caplog.records] # Test when one config option should be skipped (because it's not related # to a plugin/extension). postgresql_mock.reset_mock() postgresql_mock.enable_disable_extensions.side_effect = None - with self.assertNoLogs("charm", "ERROR"): + with caplog.at_level(logging.ERROR): + assert len(caplog.records) == 0 config = """options: plugin_citext_enable: default: false @@ -538,7 +533,7 @@ def test_enable_disable_extensions(self, _): default: production type: string""" harness = Harness(PostgresqlOperatorCharm, config=config) - self.addCleanup(harness.cleanup) + harness.cleanup() harness.begin() harness.charm.enable_disable_extensions() assert postgresql_mock.enable_disable_extensions.call_count == 1 @@ -566,7 +561,7 @@ def test_enable_disable_extensions(self, _): side_effect=[False, True, True, True, True], ) def test_on_start( - self, + harness, _is_storage_attached, _idle, _reboot_on_detached_storage, @@ -583,6 +578,7 @@ def test_on_start( _snap_cache, _enable_disable_extensions, ): + rel_id = harness.add_relation(PEER, harness.charm.app.name) _get_postgresql_version.return_value = "14.0" # Test without storage. @@ -650,7 +646,7 @@ def test_on_start( return_value=True, ) def test_on_start_replica( - self, + harness, _is_storage_attached, _idle, _get_password, @@ -662,6 +658,7 @@ def test_on_start_replica( _get_postgresql_version, _snap_cache, ): + rel_id = harness.add_relation(PEER, harness.charm.app.name) _get_postgresql_version.return_value = "14.0" # Set the current unit to be a replica (non leader unit). @@ -705,7 +702,7 @@ def test_on_start_replica( @patch("upgrade.PostgreSQLUpgrade.idle", return_value=True) @patch("charm.PostgresqlOperatorCharm._is_storage_attached", return_value=True) def test_on_start_no_patroni_member( - self, + harness, _is_storage_attached, _idle, _get_password, @@ -714,6 +711,7 @@ def test_on_start_no_patroni_member( _snap_cache, _, ): + rel_id = harness.add_relation(PEER, harness.charm.app.name) # Mock the passwords. patroni.return_value.member_started = False _get_password.return_value = "fake-operator-password" @@ -734,8 +732,9 @@ def test_on_start_no_patroni_member( @patch("charm.PostgresqlOperatorCharm._get_password") @patch("charm.PostgresqlOperatorCharm._is_storage_attached", return_value=True) def test_on_start_after_blocked_state( - self, _is_storage_attached, _get_password, _replication_password, _bootstrap_cluster + harness, _is_storage_attached, _get_password, _replication_password, _bootstrap_cluster ): + rel_id = harness.add_relation(PEER, harness.charm.app.name) # Set an initial blocked status (like after the install hook was triggered). initial_status = BlockedStatus("fake message") harness.model.unit.status = initial_status @@ -750,12 +749,13 @@ def test_on_start_after_blocked_state( @patch_network_get(private_address="1.1.1.1") @patch("charm.PostgresqlOperatorCharm.update_config") -def test_on_get_password(self, _): +def test_on_get_password(harness, _): + rel_id = harness.add_relation(PEER, harness.charm.app.name) # Create a mock event and set passwords in peer relation data. harness.set_leader(True) mock_event = MagicMock(params={}) harness.update_relation_data( - self.rel_id, + rel_id, harness.charm.app.name, { "operator-password": "test-password", @@ -788,13 +788,14 @@ def test_on_get_password(self, _): @patch("charm.Patroni.are_all_members_ready") @patch("charm.PostgresqlOperatorCharm._on_leader_elected") def test_on_set_password( - self, + harness, _, _are_all_members_ready, _postgresql, _set_secret, __, ): + rel_id = harness.add_relation(PEER, harness.charm.app.name) # Create a mock event. mock_event = MagicMock(params={}) @@ -864,7 +865,7 @@ def test_on_set_password( @patch("charm.PostgreSQLProvider.oversee_users") @patch("upgrade.PostgreSQLUpgrade.idle", return_value=True) def test_on_update_status( - self, + harness, _, _oversee_users, _primary_endpoint, @@ -877,6 +878,7 @@ def test_on_update_status( _set_primary_status_message, _start_observer, ): + rel_id = harness.add_relation(PEER, harness.charm.app.name) # Test before the cluster is initialised. harness.charm.on.update_status.emit() _set_primary_status_message.assert_not_called() @@ -884,7 +886,7 @@ def test_on_update_status( # Test after the cluster was initialised, but with the unit in a blocked state. with harness.hooks_disabled(): harness.update_relation_data( - self.rel_id, harness.charm.app.name, {"cluster_initialised": "True"} + rel_id, harness.charm.app.name, {"cluster_initialised": "True"} ) harness.charm.unit.status = BlockedStatus("fake blocked status") harness.charm.on.update_status.emit() @@ -903,7 +905,7 @@ def test_on_update_status( _member_replication_lag.return_value = "unknown" with harness.hooks_disabled(): harness.update_relation_data( - self.rel_id, harness.charm.unit.name, {"postgresql_restarted": "True"} + rel_id, harness.charm.unit.name, {"postgresql_restarted": "True"} ) harness.charm.on.update_status.emit() _reinitialize_postgresql.assert_called_once() @@ -914,7 +916,7 @@ def test_on_update_status( _is_member_isolated.return_value = True with harness.hooks_disabled(): harness.update_relation_data( - self.rel_id, harness.charm.unit.name, {"postgresql_restarted": ""} + rel_id, harness.charm.unit.name, {"postgresql_restarted": ""} ) harness.charm.on.update_status.emit() _restart_patroni.assert_called_once() @@ -937,7 +939,7 @@ def test_on_update_status( @patch("charm.Patroni.get_member_status") @patch("upgrade.PostgreSQLUpgrade.idle", return_value=True) def test_on_update_status_after_restore_operation( - self, + harness, _, _get_member_status, _member_started, @@ -951,11 +953,12 @@ def test_on_update_status_after_restore_operation( _set_primary_status_message, __, ): + rel_id = harness.add_relation(PEER, harness.charm.app.name) # Test when the restore operation fails. with harness.hooks_disabled(): harness.set_leader() harness.update_relation_data( - self.rel_id, + rel_id, harness.charm.app.name, {"cluster_initialised": "True", "restoring-backup": "2023-01-01T09:00:00Z"}, ) @@ -983,7 +986,7 @@ def test_on_update_status_after_restore_operation( assert isinstance(harness.charm.unit.status, ActiveStatus) # Assert that the backup id is still in the application relation databag. - assert harness.get_relation_data(self.rel_id, harness.charm.app) == {"cluster_initialised": "True", "restoring-backup": "2023-01-01T09:00:00Z"} + assert harness.get_relation_data(rel_id, harness.charm.app) == {"cluster_initialised": "True", "restoring-backup": "2023-01-01T09:00:00Z"} # Test when the restore operation finished successfully. _member_started.return_value = True @@ -1000,7 +1003,7 @@ def test_on_update_status_after_restore_operation( assert isinstance(harness.charm.unit.status, ActiveStatus) # Assert that the backup id is not in the application relation databag anymore. - assert harness.get_relation_data(self.rel_id, harness.charm.app) == {"cluster_initialised": "True"} + assert harness.get_relation_data(rel_id, harness.charm.app) == {"cluster_initialised": "True"} # Test when it's not possible to use the configured S3 repository. _update_config.reset_mock() @@ -1011,7 +1014,7 @@ def test_on_update_status_after_restore_operation( _set_primary_status_message.reset_mock() with harness.hooks_disabled(): harness.update_relation_data( - self.rel_id, + rel_id, harness.charm.app.name, {"restoring-backup": "2023-01-01T09:00:00Z"}, ) @@ -1027,10 +1030,11 @@ def test_on_update_status_after_restore_operation( assert harness.charm.unit.status.message == "fake validation message" # Assert that the backup id is not in the application relation databag anymore. - assert harness.get_relation_data(self.rel_id, harness.charm.app) == {"cluster_initialised": "True"} + assert harness.get_relation_data(rel_id, harness.charm.app) == {"cluster_initialised": "True"} @patch("charm.snap.SnapCache") -def test_install_snap_packages(self, _snap_cache): +def test_install_snap_packages(harness, _snap_cache): + rel_id = harness.add_relation(PEER, harness.charm.app.name) _snap_package = _snap_cache.return_value.__getitem__.return_value _snap_package.ensure.side_effect = snap.SnapError _snap_package.present = False @@ -1120,7 +1124,8 @@ def test_install_snap_packages(self, _snap_cache): "subprocess.check_call", side_effect=[None, subprocess.CalledProcessError(1, "fake command")], ) -def test_is_storage_attached(self, _check_call): +def test_is_storage_attached(harness, _check_call): + rel_id = harness.add_relation(PEER, harness.charm.app.name) # Test with attached storage. is_storage_attached = harness.charm._is_storage_attached() _check_call.assert_called_once_with(["mountpoint", "-q", harness.charm._storage_path]) @@ -1131,7 +1136,8 @@ def test_is_storage_attached(self, _check_call): assert not (is_storage_attached) @patch("subprocess.check_call") -def test_reboot_on_detached_storage(self, _check_call): +def test_reboot_on_detached_storage(harness, _check_call): + rel_id = harness.add_relation(PEER, harness.charm.app.name) mock_event = MagicMock() harness.charm._reboot_on_detached_storage(mock_event) mock_event.defer.assert_called_once() @@ -1141,7 +1147,8 @@ def test_reboot_on_detached_storage(self, _check_call): @patch_network_get(private_address="1.1.1.1") @patch("charm.Patroni.restart_postgresql") @patch("charm.Patroni.are_all_members_ready") -def test_restart(self, _are_all_members_ready, _restart_postgresql): +def test_restart(harness, _are_all_members_ready, _restart_postgresql): + rel_id = harness.add_relation(PEER, harness.charm.app.name) _are_all_members_ready.side_effect = [False, True, True] # Test when not all members are ready. @@ -1172,7 +1179,7 @@ def test_restart(self, _are_all_members_ready, _restart_postgresql): @patch("charm.Patroni.render_patroni_yml_file") @patch("charm.PostgresqlOperatorCharm.is_tls_enabled", new_callable=PropertyMock) def test_update_config( - self, + harness, _is_tls_enabled, _render_patroni_yml_file, _is_workload_running, @@ -1182,6 +1189,7 @@ def test_update_config( __, ___, ): + rel_id = harness.add_relation(PEER, harness.charm.app.name) with patch.object(PostgresqlOperatorCharm, "postgresql", Mock()) as postgresql_mock: # Mock some properties. postgresql_mock.is_tls_enabled = PropertyMock(side_effect=[False, False, False, False]) @@ -1208,7 +1216,7 @@ def test_update_config( # Test without TLS files available. harness.update_config(unset={"profile-limit-memory", "profile_limit_memory"}) with harness.hooks_disabled(): - harness.update_relation_data(self.rel_id, harness.charm.unit.name, {"tls": ""}) + harness.update_relation_data(rel_id, harness.charm.unit.name, {"tls": ""}) _is_tls_enabled.return_value = False harness.charm.update_config() _render_patroni_yml_file.assert_called_once_with( @@ -1221,12 +1229,12 @@ def test_update_config( parameters={"test": "test"}, ) _handle_postgresql_restart_need.assert_called_once_with(False) - assert "tls" not in harness.get_relation_data(self.rel_id, harness.charm.unit.name) + assert "tls" not in harness.get_relation_data(rel_id, harness.charm.unit.name) # Test with TLS files available. _handle_postgresql_restart_need.reset_mock() harness.update_relation_data( - self.rel_id, harness.charm.unit.name, {"tls": ""} + rel_id, harness.charm.unit.name, {"tls": ""} ) # Mock some data in the relation to test that it change. _is_tls_enabled.return_value = True _render_patroni_yml_file.reset_mock() @@ -1241,28 +1249,29 @@ def test_update_config( parameters={"test": "test"}, ) _handle_postgresql_restart_need.assert_called_once() - assert "tls" not in harness.get_relation_data(self.rel_id, harness.charm.unit.name) # The "tls" flag is set in handle_postgresql_restart_need. + assert "tls" not in harness.get_relation_data(rel_id, harness.charm.unit.name) # The "tls" flag is set in handle_postgresql_restart_need. # Test with workload not running yet. harness.update_relation_data( - self.rel_id, harness.charm.unit.name, {"tls": ""} + rel_id, harness.charm.unit.name, {"tls": ""} ) # Mock some data in the relation to test that it change. _handle_postgresql_restart_need.reset_mock() harness.charm.update_config() _handle_postgresql_restart_need.assert_not_called() - assert harness.get_relation_data(self.rel_id, harness.charm.unit.name)["tls"] == "enabled" + assert harness.get_relation_data(rel_id, harness.charm.unit.name)["tls"] == "enabled" # Test with member not started yet. harness.update_relation_data( - self.rel_id, harness.charm.unit.name, {"tls": ""} + rel_id, harness.charm.unit.name, {"tls": ""} ) # Mock some data in the relation to test that it doesn't change. harness.charm.update_config() _handle_postgresql_restart_need.assert_not_called() - assert "tls" not in harness.get_relation_data(self.rel_id, harness.charm.unit.name) + assert "tls" not in harness.get_relation_data(rel_id, harness.charm.unit.name) @patch("charm.PostgresqlOperatorCharm._update_relation_endpoints") @patch("charm.PostgresqlOperatorCharm.primary_endpoint", new_callable=PropertyMock) -def test_on_cluster_topology_change(self, _primary_endpoint, _update_relation_endpoints): +def test_on_cluster_topology_change(harness, _primary_endpoint, _update_relation_endpoints): + rel_id = harness.add_relation(PEER, harness.charm.app.name) # Mock the property value. _primary_endpoint.side_effect = [None, "1.1.1.1"] @@ -1281,8 +1290,9 @@ def test_on_cluster_topology_change(self, _primary_endpoint, _update_relation_en ) @patch("charm.PostgresqlOperatorCharm._update_relation_endpoints") def test_on_cluster_topology_change_keep_blocked( - self, _update_relation_endpoints, _primary_endpoint + harness, _update_relation_endpoints, _primary_endpoint ): + rel_id = harness.add_relation(PEER, harness.charm.app.name) harness.model.unit.status = WaitingStatus(PRIMARY_NOT_REACHABLE_MESSAGE) harness.charm._on_cluster_topology_change(Mock()) @@ -1299,8 +1309,9 @@ def test_on_cluster_topology_change_keep_blocked( ) @patch("charm.PostgresqlOperatorCharm._update_relation_endpoints") def test_on_cluster_topology_change_clear_blocked( - self, _update_relation_endpoints, _primary_endpoint + harness, _update_relation_endpoints, _primary_endpoint ): + rel_id = harness.add_relation(PEER, harness.charm.app.name) harness.model.unit.status = WaitingStatus(PRIMARY_NOT_REACHABLE_MESSAGE) harness.charm._on_cluster_topology_change(Mock()) @@ -1311,7 +1322,8 @@ def test_on_cluster_topology_change_clear_blocked( @patch("charm.PostgresqlOperatorCharm.postgresql", new_callable=PropertyMock) @patch("config.subprocess") -def test_validate_config_options(self, _, _charm_lib): +def test_validate_config_options(harness, _, _charm_lib): + rel_id = harness.add_relation(PEER, harness.charm.app.name) _charm_lib.return_value.get_postgresql_text_search_configs.return_value = [] _charm_lib.return_value.validate_date_style.return_value = [] _charm_lib.return_value.get_postgresql_timezones.return_value = [] @@ -1370,7 +1382,7 @@ def test_validate_config_options(self, _, _charm_lib): @patch("charm.PostgresqlOperatorCharm._reconfigure_cluster") @patch("ops.framework.EventBase.defer") def test_on_peer_relation_changed( - self, + harness, _defer, _reconfigure_cluster, _update_member_ip, @@ -1387,11 +1399,12 @@ def test_on_peer_relation_changed( _update_relation_endpoints, _, ): + rel_id = harness.add_relation(PEER, harness.charm.app.name) # Test an uninitialized cluster. mock_event = Mock() with harness.hooks_disabled(): harness.update_relation_data( - self.rel_id, harness.charm.app.name, {"cluster_initialised": ""} + rel_id, harness.charm.app.name, {"cluster_initialised": ""} ) harness.charm._on_peer_relation_changed(mock_event) mock_event.defer.assert_called_once() @@ -1402,7 +1415,7 @@ def test_on_peer_relation_changed( mock_event.defer.reset_mock() with harness.hooks_disabled(): harness.update_relation_data( - self.rel_id, + rel_id, harness.charm.app.name, {"cluster_initialised": "True", "members_ips": '["1.1.1.1"]'}, ) @@ -1460,7 +1473,7 @@ def test_on_peer_relation_changed( # Test when Patroni has already started but this is a replica with a # huge or unknown lag. - self.relation = harness.model.get_relation(PEER, self.rel_id) + relation = harness.model.get_relation(PEER, rel_id) _member_started.return_value = True for values in itertools.product([True, False], ["0", "1000", "1001", "unknown"]): _defer.reset_mock() @@ -1469,7 +1482,7 @@ def test_on_peer_relation_changed( _is_primary.return_value = values[0] _member_replication_lag.return_value = values[1] harness.charm.unit.status = ActiveStatus() - harness.charm.on.database_peers_relation_changed.emit(self.relation) + harness.charm.on.database_peers_relation_changed.emit(relation) if _is_primary.return_value == values[0] or int(values[1]) <= 1000: _defer.assert_not_called() _check_stanza.assert_called_once() @@ -1482,13 +1495,13 @@ def test_on_peer_relation_changed( assert isinstance(harness.charm.unit.status, MaintenanceStatus) # Test when it was not possible to start the pgBackRest service yet. - self.relation = harness.model.get_relation(PEER, self.rel_id) + relation = harness.model.get_relation(PEER, rel_id) _member_started.return_value = True _defer.reset_mock() _coordinate_stanza_fields.reset_mock() _check_stanza.reset_mock() _start_stop_pgbackrest_service.return_value = False - harness.charm.on.database_peers_relation_changed.emit(self.relation) + harness.charm.on.database_peers_relation_changed.emit(relation) _defer.assert_called_once() _coordinate_stanza_fields.assert_not_called() _check_stanza.assert_not_called() @@ -1497,7 +1510,7 @@ def test_on_peer_relation_changed( # pgBackRest service. _defer.reset_mock() _start_stop_pgbackrest_service.return_value = True - harness.charm.on.database_peers_relation_changed.emit(self.relation) + harness.charm.on.database_peers_relation_changed.emit(relation) _defer.assert_not_called() _coordinate_stanza_fields.assert_called_once() _check_stanza.assert_called_once() @@ -1507,8 +1520,9 @@ def test_on_peer_relation_changed( @patch("charm.PostgresqlOperatorCharm._remove_from_members_ips") @patch("charm.Patroni.remove_raft_member") def test_reconfigure_cluster( - self, _remove_raft_member, _remove_from_members_ips, _add_members + harness, _remove_raft_member, _remove_from_members_ips, _add_members ): + rel_id = harness.add_relation(PEER, harness.charm.app.name) # Test when no change is needed in the member IP. mock_event = Mock() mock_event.unit = harness.charm.unit @@ -1546,7 +1560,7 @@ def test_reconfigure_cluster( mock_event.relation.data = relation_data with harness.hooks_disabled(): harness.update_relation_data( - self.rel_id, harness.charm.app.name, {"members_ips": '["' + ip_to_remove + '"]'} + rel_id, harness.charm.app.name, {"members_ips": '["' + ip_to_remove + '"]'} ) assert (harness.charm._reconfigure_cluster(mock_event)) _remove_raft_member.assert_called_once_with(ip_to_remove) @@ -1555,7 +1569,8 @@ def test_reconfigure_cluster( @patch("charms.postgresql_k8s.v0.postgresql_tls.PostgreSQLTLS._request_certificate") @patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=False) -def test_update_certificate(self, _, _request_certificate): +def test_update_certificate(harness, _, _request_certificate): + rel_id = harness.add_relation(PEER, harness.charm.app.name) # If there is no current TLS files, _request_certificate should be called # only when the certificates relation is established. harness.charm._update_certificate() @@ -1567,7 +1582,7 @@ def test_update_certificate(self, _, _request_certificate): key = private_key = "fake private key" with harness.hooks_disabled(): harness.update_relation_data( - self.rel_id, + rel_id, harness.charm.unit.name, { "ca": ca, @@ -1586,7 +1601,8 @@ def test_update_certificate(self, _, _request_certificate): @patch("charms.postgresql_k8s.v0.postgresql_tls.PostgreSQLTLS._request_certificate") @patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=True) -def test_update_certificate_secrets(self, _, _request_certificate): +def test_update_certificate_secrets(harness, _, _request_certificate): + rel_id = harness.add_relation(PEER, harness.charm.app.name) # If there is no current TLS files, _request_certificate should be called # only when the certificates relation is established. harness.charm._update_certificate() @@ -1612,18 +1628,19 @@ def test_update_certificate_secrets(self, _, _request_certificate): @patch_network_get(private_address="1.1.1.1") @patch("charm.PostgresqlOperatorCharm._update_certificate") @patch("charm.Patroni.stop_patroni") -def test_update_member_ip(self, _stop_patroni, _update_certificate): +def test_update_member_ip(harness, _stop_patroni, _update_certificate): + rel_id = harness.add_relation(PEER, harness.charm.app.name) # Test when the IP address of the unit hasn't changed. with harness.hooks_disabled(): harness.update_relation_data( - self.rel_id, + rel_id, harness.charm.unit.name, { "ip": "1.1.1.1", }, ) assert not (harness.charm._update_member_ip()) - relation_data = harness.get_relation_data(self.rel_id, harness.charm.unit.name) + relation_data = harness.get_relation_data(rel_id, harness.charm.unit.name) assert relation_data.get("ip-to-remove") == None _stop_patroni.assert_not_called() _update_certificate.assert_not_called() @@ -1631,14 +1648,14 @@ def test_update_member_ip(self, _stop_patroni, _update_certificate): # Test when the IP address of the unit has changed. with harness.hooks_disabled(): harness.update_relation_data( - self.rel_id, + rel_id, harness.charm.unit.name, { "ip": "2.2.2.2", }, ) assert (harness.charm._update_member_ip()) - relation_data = harness.get_relation_data(self.rel_id, harness.charm.unit.name) + relation_data = harness.get_relation_data(rel_id, harness.charm.unit.name) assert relation_data.get("ip") == "1.1.1.1" assert relation_data.get("ip-to-remove") == "2.2.2.2" _stop_patroni.assert_called_once() @@ -1648,7 +1665,8 @@ def test_update_member_ip(self, _stop_patroni, _update_certificate): @patch("charm.PostgresqlOperatorCharm.update_config") @patch("charm.Patroni.render_file") @patch("charms.postgresql_k8s.v0.postgresql_tls.PostgreSQLTLS.get_tls_files") -def test_push_tls_files_to_workload(self, _get_tls_files, _render_file, _update_config): +def test_push_tls_files_to_workload(harness, _get_tls_files, _render_file, _update_config): + rel_id = harness.add_relation(PEER, harness.charm.app.name) _get_tls_files.side_effect = [ ("key", "ca", "cert"), ("key", "ca", None), @@ -1668,7 +1686,8 @@ def test_push_tls_files_to_workload(self, _get_tls_files, _render_file, _update_ assert _render_file.call_count == 2 @patch("charm.snap.SnapCache") -def test_is_workload_running(self, _snap_cache): +def test_is_workload_running(harness, _snap_cache): + rel_id = harness.add_relation(PEER, harness.charm.app.name) pg_snap = _snap_cache.return_value[POSTGRESQL_SNAP_NAME] pg_snap.present = False @@ -1677,7 +1696,8 @@ def test_is_workload_running(self, _snap_cache): pg_snap.present = True assert (harness.charm._is_workload_running) -def test_get_available_memory(self): +def test_get_available_memory(harness): + rel_id = harness.add_relation(PEER, harness.charm.app.name) meminfo = ( "MemTotal: 16089488 kB" "MemFree: 799284 kB" @@ -1696,7 +1716,8 @@ def test_get_available_memory(self): @patch("charm.ClusterTopologyObserver") @patch("charm.JujuVersion") -def test_juju_run_exec_divergence(self, _juju_version: Mock, _topology_observer: Mock): +def test_juju_run_exec_divergence(harness, _juju_version: Mock, _topology_observer: Mock): + rel_id = harness.add_relation(PEER, harness.charm.app.name) # Juju 2 _juju_version.from_environ.return_value.major = 2 harness = Harness(PostgresqlOperatorCharm) @@ -1710,7 +1731,8 @@ def test_juju_run_exec_divergence(self, _juju_version: Mock, _topology_observer: harness.begin() _topology_observer.assert_called_once_with(harness.charm, "/usr/bin/juju-exec") -def test_client_relations(self): +def test_client_relations(harness): + rel_id = harness.add_relation(PEER, harness.charm.app.name) # Test when the charm has no relations. assert len(harness.charm.client_relations) == 0 @@ -1727,20 +1749,22 @@ def test_client_relations(self): # Secrets # -def test_scope_obj(self): +def test_scope_obj(harness): + rel_id = harness.add_relation(PEER, harness.charm.app.name) assert harness.charm._scope_obj("app") == harness.charm.framework.model.app assert harness.charm._scope_obj("unit") == harness.charm.framework.model.unit assert harness.charm._scope_obj("test") is None @patch_network_get(private_address="1.1.1.1") @patch("charm.PostgresqlOperatorCharm._on_leader_elected") -def test_get_secret(self, _): +def test_get_secret(harness, _): + rel_id = harness.add_relation(PEER, harness.charm.app.name) # App level changes require leader privileges harness.set_leader() # Test application scope. assert harness.charm.get_secret("app", "password") is None harness.update_relation_data( - self.rel_id, harness.charm.app.name, {"password": "test-password"} + rel_id, harness.charm.app.name, {"password": "test-password"} ) assert harness.charm.get_secret("app", "password") == "test-password" @@ -1749,14 +1773,15 @@ def test_get_secret(self, _): # Test unit scope. assert harness.charm.get_secret("unit", "password") is None harness.update_relation_data( - self.rel_id, harness.charm.unit.name, {"password": "test-password"} + rel_id, harness.charm.unit.name, {"password": "test-password"} ) assert harness.charm.get_secret("unit", "password") == "test-password" @patch_network_get(private_address="1.1.1.1") @patch("charm.PostgresqlOperatorCharm._on_leader_elected") @patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=True) -def test_on_get_password_secrets(self, mock1, mock2): +def test_on_get_password_secrets(harness, mock1, mock2): + rel_id = harness.add_relation(PEER, harness.charm.app.name) # Create a mock event and set passwords in peer relation data. harness.set_leader() mock_event = MagicMock(params={}) @@ -1785,7 +1810,8 @@ def test_on_get_password_secrets(self, mock1, mock2): @patch_network_get(private_address="1.1.1.1") @patch("charm.PostgresqlOperatorCharm._on_leader_elected") @patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=True) -def test_get_secret_secrets(self, scope, _, __): +def test_get_secret_secrets(harness, scope, _): + rel_id = harness.add_relation(PEER, harness.charm.app.name) harness.set_leader() assert harness.charm.get_secret(scope, "operator-password") is None @@ -1794,28 +1820,29 @@ def test_get_secret_secrets(self, scope, _, __): @patch_network_get(private_address="1.1.1.1") @patch("charm.PostgresqlOperatorCharm._on_leader_elected") -def test_set_secret(self, _): +def test_set_secret(harness, _): + rel_id = harness.add_relation(PEER, harness.charm.app.name) harness.set_leader() # Test application scope. - assert "password" not in harness.get_relation_data(self.rel_id, harness.charm.app.name) + assert "password" not in harness.get_relation_data(rel_id, harness.charm.app.name) harness.charm.set_secret("app", "password", "test-password") assert ( - harness.get_relation_data(self.rel_id, harness.charm.app.name)["password"] + harness.get_relation_data(rel_id, harness.charm.app.name)["password"] == "test-password" ) harness.charm.set_secret("app", "password", None) - assert "password" not in harness.get_relation_data(self.rel_id, harness.charm.app.name) + assert "password" not in harness.get_relation_data(rel_id, harness.charm.app.name) # Test unit scope. - assert "password" not in harness.get_relation_data(self.rel_id, harness.charm.unit.name) + assert "password" not in harness.get_relation_data(rel_id, harness.charm.unit.name) harness.charm.set_secret("unit", "password", "test-password") assert ( - harness.get_relation_data(self.rel_id, harness.charm.unit.name)["password"] + harness.get_relation_data(rel_id, harness.charm.unit.name)["password"] == "test-password" ) harness.charm.set_secret("unit", "password", None) - assert "password" not in harness.get_relation_data(self.rel_id, harness.charm.unit.name) + assert "password" not in harness.get_relation_data(rel_id, harness.charm.unit.name) with pytest.raises(RuntimeError): harness.charm.set_secret("test", "password", "test") @@ -1824,7 +1851,8 @@ def test_set_secret(self, _): @patch_network_get(private_address="1.1.1.1") @patch("charm.PostgresqlOperatorCharm._on_leader_elected") @patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=True) -def test_set_reset_new_secret(self, scope, is_leader, _, __): +def test_set_reset_new_secret(harness, scope, is_leader, _): + rel_id = harness.add_relation(PEER, harness.charm.app.name) """NOTE: currently ops.testing seems to allow for non-leader to set secrets too!""" # App has to be leader, unit can be either harness.set_leader(is_leader) @@ -1844,7 +1872,8 @@ def test_set_reset_new_secret(self, scope, is_leader, _, __): @patch_network_get(private_address="1.1.1.1") @patch("charm.PostgresqlOperatorCharm._on_leader_elected") @patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=True) -def test_invalid_secret(self, scope, is_leader, _, __): +def test_invalid_secret(harness, scope, is_leader, _, __): + rel_id = harness.add_relation(PEER, harness.charm.app.name) # App has to be leader, unit can be either harness.set_leader(is_leader) @@ -1854,52 +1883,52 @@ def test_invalid_secret(self, scope, is_leader, _, __): harness.charm.set_secret(scope, "somekey", "") assert harness.charm.get_secret(scope, "somekey") is None -@pytest.mark.usefixtures("use_caplog") @patch_network_get(private_address="1.1.1.1") @patch("charm.PostgresqlOperatorCharm._on_leader_elected") -def test_delete_password(self, _): +def test_delete_password(harness, caplog, _): + rel_id = harness.add_relation(PEER, harness.charm.app.name) """NOTE: currently ops.testing seems to allow for non-leader to remove secrets too!""" harness.set_leader(True) harness.update_relation_data( - self.rel_id, harness.charm.app.name, {"replication": "somepw"} + rel_id, harness.charm.app.name, {"replication": "somepw"} ) harness.charm.remove_secret("app", "replication") assert harness.charm.get_secret("app", "replication") is None harness.set_leader(False) harness.update_relation_data( - self.rel_id, harness.charm.unit.name, {"somekey": "somevalue"} + rel_id, harness.charm.unit.name, {"somekey": "somevalue"} ) harness.charm.remove_secret("unit", "somekey") assert harness.charm.get_secret("unit", "somekey") is None harness.set_leader(True) - with self._caplog.at_level(logging.ERROR): + with caplog.at_level(logging.ERROR): harness.charm.remove_secret("app", "replication") assert ( - "Non-existing field 'replication' was attempted to be removed" in self._caplog.text + "Non-existing field 'replication' was attempted to be removed" in caplog.text ) harness.charm.remove_secret("unit", "somekey") - assert "Non-existing field 'somekey' was attempted to be removed" in self._caplog.text + assert "Non-existing field 'somekey' was attempted to be removed" in caplog.text harness.charm.remove_secret("app", "non-existing-secret") assert ( "Non-existing field 'non-existing-secret' was attempted to be removed" - in self._caplog.text + in caplog.text ) harness.charm.remove_secret("unit", "non-existing-secret") assert ( "Non-existing field 'non-existing-secret' was attempted to be removed" - in self._caplog.text + in caplog.text ) @patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=True) @patch_network_get(private_address="1.1.1.1") @patch("charm.PostgresqlOperatorCharm._on_leader_elected") -@pytest.mark.usefixtures("use_caplog") -def test_delete_existing_password_secrets(self, _, __): +def test_delete_existing_password_secrets(harness, caplog, _, __): + rel_id = harness.add_relation(PEER, harness.charm.app.name) """NOTE: currently ops.testing seems to allow for non-leader to remove secrets too!""" harness.set_leader(True) harness.charm.set_secret("app", "operator-password", "somepw") @@ -1912,43 +1941,44 @@ def test_delete_existing_password_secrets(self, _, __): assert harness.charm.get_secret("unit", "operator-password") is None harness.set_leader(True) - with self._caplog.at_level(logging.ERROR): + with caplog.at_level(logging.ERROR): harness.charm.remove_secret("app", "operator-password") assert ( "Non-existing secret operator-password was attempted to be removed." - in self._caplog.text + in caplog.text ) harness.charm.remove_secret("unit", "operator-password") assert ( "Non-existing secret operator-password was attempted to be removed." - in self._caplog.text + in caplog.text ) harness.charm.remove_secret("app", "non-existing-secret") assert ( "Non-existing field 'non-existing-secret' was attempted to be removed" - in self._caplog.text + in caplog.text ) harness.charm.remove_secret("unit", "non-existing-secret") assert ( "Non-existing field 'non-existing-secret' was attempted to be removed" - in self._caplog.text + in caplog.text ) @parameterized.expand([("app", True), ("unit", True), ("unit", False)]) @patch_network_get(private_address="1.1.1.1") @patch("charm.PostgresqlOperatorCharm._on_leader_elected") @patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=True) -def test_migration_from_databag(self, scope, is_leader, _, __): +def test_migration_from_databag(harness, scope, is_leader, _, __): + rel_id = harness.add_relation(PEER, harness.charm.app.name) """Check if we're moving on to use secrets when live upgrade from databag to Secrets usage.""" # App has to be leader, unit can be either harness.set_leader(is_leader) # Getting current password entity = getattr(harness.charm, scope) - harness.update_relation_data(self.rel_id, entity.name, {"operator-password": "bla"}) + harness.update_relation_data(rel_id, entity.name, {"operator-password": "bla"}) assert harness.charm.get_secret(scope, "operator-password") == "bla" # Reset new secret @@ -1956,14 +1986,15 @@ def test_migration_from_databag(self, scope, is_leader, _, __): assert harness.charm.model.get_secret(label=f"postgresql.{scope}") assert harness.charm.get_secret(scope, "operator-password") == "blablabla" assert "operator-password" not in harness.get_relation_data( - self.rel_id, getattr(harness.charm, scope).name + rel_id, getattr(harness.charm, scope).name ) @parameterized.expand([("app", True), ("unit", True), ("unit", False)]) @patch_network_get(private_address="1.1.1.1") @patch("charm.PostgresqlOperatorCharm._on_leader_elected") @patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=True) -def test_migration_from_single_secret(self, scope, is_leader, _, __): +def test_migration_from_single_secret(harness, scope, is_leader, _, __): + rel_id = harness.add_relation(PEER, harness.charm.app.name) """Check if we're moving on to use secrets when live upgrade from databag to Secrets usage.""" # App has to be leader, unit can be either harness.set_leader(is_leader) @@ -1973,7 +2004,7 @@ def test_migration_from_single_secret(self, scope, is_leader, _, __): # Getting current password entity = getattr(harness.charm, scope) harness.update_relation_data( - self.rel_id, entity.name, {SECRET_INTERNAL_LABEL: secret.id} + rel_id, entity.name, {SECRET_INTERNAL_LABEL: secret.id} ) assert harness.charm.get_secret(scope, "operator-password") == "bla" @@ -1987,7 +2018,7 @@ def test_migration_from_single_secret(self, scope, is_leader, _, __): assert harness.charm.model.get_secret(label=f"postgresql.{scope}") assert harness.charm.get_secret(scope, "operator-password") == "blablabla" assert SECRET_INTERNAL_LABEL not in harness.get_relation_data( - self.rel_id, getattr(harness.charm, scope).name + rel_id, getattr(harness.charm, scope).name ) @patch("charms.rolling_ops.v0.rollingops.RollingOpsManager._on_acquire_lock") @@ -1996,8 +2027,9 @@ def test_migration_from_single_secret(self, scope, is_leader, _, __): @patch("charm.PostgresqlOperatorCharm._unit_ip") @patch("charm.PostgresqlOperatorCharm.is_tls_enabled", new_callable=PropertyMock) def test_handle_postgresql_restart_need( - self, _is_tls_enabled, _, _reload_patroni_configuration, __, _restart + harness, _is_tls_enabled, _, _reload_patroni_configuration, __, _restart ): + rel_id = harness.add_relation(PEER, harness.charm.app.name) with patch.object(PostgresqlOperatorCharm, "postgresql", Mock()) as postgresql_mock: for values in itertools.product( [True, False], [True, False], [True, False], [True, False], [True, False] @@ -2006,10 +2038,10 @@ def test_handle_postgresql_restart_need( _restart.reset_mock() with harness.hooks_disabled(): harness.update_relation_data( - self.rel_id, harness.charm.unit.name, {"tls": ""} + rel_id, harness.charm.unit.name, {"tls": ""} ) harness.update_relation_data( - self.rel_id, + rel_id, harness.charm.unit.name, {"postgresql_restarted": ("True" if values[4] else "")}, ) @@ -2020,20 +2052,19 @@ def test_handle_postgresql_restart_need( harness.charm._handle_postgresql_restart_need(values[0]) _reload_patroni_configuration.assert_called_once() - ( - assert "tls" in harness.get_relation_data(self.rel_id, harness.charm.unit) - if values[0] - else assert "tls" not in harness.get_relation_data(self.rel_id, harness.charm.unit) - ) + if values[0]: + assert "tls" in harness.get_relation_data(rel_id, harness.charm.unit) + else: + assert "tls" not in harness.get_relation_data(rel_id, harness.charm.unit) + if (values[1] != values[2]) or values[3]: - assert "postgresql_restarted" not in harness.get_relation_data(self.rel_id, harness.charm.unit) + assert "postgresql_restarted" not in harness.get_relation_data(rel_id, harness.charm.unit) _restart.assert_called_once() else: - ( - assert "postgresql_restarted" in harness.get_relation_data(self.rel_id, harness.charm.unit) - if values[4] - else assert "postgresql_restarted" not in harness.get_relation_data(self.rel_id, harness.charm.unit) - ) + if values[4]: + assert "postgresql_restarted" in harness.get_relation_data(rel_id, harness.charm.unit) + else: + assert "postgresql_restarted" not in harness.get_relation_data(rel_id, harness.charm.unit) _restart.assert_not_called() @patch("charm.PostgresqlOperatorCharm._update_relation_endpoints") @@ -2047,7 +2078,7 @@ def test_handle_postgresql_restart_need( @patch("charm.PostgresqlOperatorCharm._unit_ip") @patch("charm.Patroni.get_member_ip") def test_on_peer_relation_departed( - self, + harness, _get_member_ip, _unit_ip, _remove_raft_member, @@ -2059,6 +2090,7 @@ def test_on_peer_relation_departed( _primary_endpoint, _update_relation_endpoints, ): + rel_id = harness.add_relation(PEER, harness.charm.app.name) # Test when the current unit is the departing unit. harness.charm.unit.status = ActiveStatus() event = Mock() @@ -2126,7 +2158,7 @@ def test_on_peer_relation_departed( _updated_synchronous_node_count.return_value = False with harness.hooks_disabled(): harness.update_relation_data( - self.rel_id, harness.charm.app.name, {"cluster_initialised": "True"} + rel_id, harness.charm.app.name, {"cluster_initialised": "True"} ) harness.charm._on_peer_relation_departed(event) _remove_raft_member.assert_called_once_with(mock_ip_address) @@ -2142,7 +2174,7 @@ def test_on_peer_relation_departed( _remove_raft_member.reset_mock() event.defer.reset_mock() _updated_synchronous_node_count.reset_mock() - harness.add_relation_unit(self.rel_id, f"{harness.charm.app.name}/2") + harness.add_relation_unit(rel_id, f"{harness.charm.app.name}/2") harness.charm._on_peer_relation_departed(event) _remove_raft_member.assert_called_once_with(mock_ip_address) event.defer.assert_called_once() @@ -2224,7 +2256,8 @@ def test_on_peer_relation_departed( @patch("charm.PostgresqlOperatorCharm._update_relation_endpoints") @patch("charm.PostgresqlOperatorCharm.primary_endpoint", new_callable=PropertyMock) -def test_update_new_unit_status(self, _primary_endpoint, _update_relation_endpoints): +def test_update_new_unit_status(harness, _primary_endpoint, _update_relation_endpoints): + rel_id = harness.add_relation(PEER, harness.charm.app.name) # Test when the charm is blocked. _primary_endpoint.return_value = "endpoint" harness.charm.unit.status = BlockedStatus("fake blocked status") diff --git a/tox.ini b/tox.ini index 18edb1846a..2254138f0b 100644 --- a/tox.ini +++ b/tox.ini @@ -65,7 +65,7 @@ commands_pre = poetry install --only main,charm-libs,unit --no-root commands = poetry run coverage run --source={[vars]src_path} \ - -m pytest -v --tb native -s {posargs} {[vars]tests_path}/unit + -m pytest -v --tb native -s {posargs} {[vars]tests_path}/unit/test_charm.py::test_on_install_snap_failure poetry run coverage report poetry run coverage xml From e31bd76301208e33be3cc1e9161423612e0555a7 Mon Sep 17 00:00:00 2001 From: lucasgameiroborges Date: Wed, 3 Apr 2024 01:49:09 +0000 Subject: [PATCH 03/10] WIP: fix patches for pytest --- tests/unit/test_charm.py | 649 ++++++++++++++++++++------------------- tox.ini | 2 +- 2 files changed, 327 insertions(+), 324 deletions(-) diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py index e391258ace..3cbd0b2385 100644 --- a/tests/unit/test_charm.py +++ b/tests/unit/test_charm.py @@ -147,92 +147,95 @@ def test_patroni_scrape_config_no_tls(harness): ] @patch_network_get(private_address="1.1.1.1") -@patch( - "charm.PostgresqlOperatorCharm.is_tls_enabled", - return_value=True, - new_callable=PropertyMock, -) -def test_patroni_scrape_config_tls(harness, _): - rel_id = harness.add_relation(PEER, harness.charm.app.name) - result = harness.charm.patroni_scrape_config() - - assert result == [ - { - "metrics_path": "/metrics", - "scheme": "https", - "static_configs": [{"targets": ["1.1.1.1:8008"]}], - "tls_config": {"insecure_skip_verify": True}, - }, - ] - -@patch( - "charm.PostgresqlOperatorCharm._units_ips", - new_callable=PropertyMock, - return_value={"1.1.1.1", "1.1.1.2"}, -) -@patch("charm.PostgresqlOperatorCharm._patroni", new_callable=PropertyMock) -def test_primary_endpoint(harness, _patroni, _): - rel_id = harness.add_relation(PEER, harness.charm.app.name) - _patroni.return_value.get_member_ip.return_value = "1.1.1.1" - _patroni.return_value.get_primary.return_value = sentinel.primary - assert harness.charm.primary_endpoint == "1.1.1.1" - - _patroni.return_value.get_member_ip.assert_called_once_with(sentinel.primary) - _patroni.return_value.get_primary.assert_called_once_with() +def test_patroni_scrape_config_tls(harness): + with patch( + "charm.PostgresqlOperatorCharm.is_tls_enabled", + return_value=True, + new_callable=PropertyMock, + ): + rel_id = harness.add_relation(PEER, harness.charm.app.name) + result = harness.charm.patroni_scrape_config() -@patch("charm.PostgresqlOperatorCharm._peers", new_callable=PropertyMock, return_value=None) -@patch( - "charm.PostgresqlOperatorCharm._units_ips", - new_callable=PropertyMock, - return_value={"1.1.1.1", "1.1.1.2"}, -) -@patch("charm.PostgresqlOperatorCharm._patroni", new_callable=PropertyMock) -def test_primary_endpoint_no_peers(harness, _patroni, _, __): - rel_id = harness.add_relation(PEER, harness.charm.app.name) - assert harness.charm.primary_endpoint is None + assert result == [ + { + "metrics_path": "/metrics", + "scheme": "https", + "static_configs": [{"targets": ["1.1.1.1:8008"]}], + "tls_config": {"insecure_skip_verify": True}, + }, + ] + +def test_primary_endpoint(harness): + with patch( + "charm.PostgresqlOperatorCharm._units_ips", + new_callable=PropertyMock, + return_value={"1.1.1.1", "1.1.1.2"}, + ), patch( + "charm.PostgresqlOperatorCharm._patroni", new_callable=PropertyMock + ) as _patroni: + rel_id = harness.add_relation(PEER, harness.charm.app.name) + _patroni.return_value.get_member_ip.return_value = "1.1.1.1" + _patroni.return_value.get_primary.return_value = sentinel.primary + assert harness.charm.primary_endpoint == "1.1.1.1" + + _patroni.return_value.get_member_ip.assert_called_once_with(sentinel.primary) + _patroni.return_value.get_primary.assert_called_once_with() + +def test_primary_endpoint_no_peers(harness): + with patch( + "charm.PostgresqlOperatorCharm._peers", new_callable=PropertyMock, return_value=None + ), patch( + "charm.PostgresqlOperatorCharm._units_ips", + new_callable=PropertyMock, + return_value={"1.1.1.1", "1.1.1.2"}, + ), patch( + "charm.PostgresqlOperatorCharm._patroni", new_callable=PropertyMock + ) as _patroni: + rel_id = harness.add_relation(PEER, harness.charm.app.name) + assert harness.charm.primary_endpoint is None - assert not _patroni.return_value.get_member_ip.called - assert not _patroni.return_value.get_primary.called + assert not _patroni.return_value.get_member_ip.called + assert not _patroni.return_value.get_primary.called -@patch("charm.PostgresqlOperatorCharm._update_relation_endpoints", new_callable=PropertyMock) -@patch( - "charm.PostgresqlOperatorCharm.primary_endpoint", - new_callable=PropertyMock, -) -@patch("charm.PostgresqlOperatorCharm.update_config") @patch_network_get(private_address="1.1.1.1") -def test_on_leader_elected( - harness, _update_config, _primary_endpoint, _update_relation_endpoints -): - rel_id = harness.add_relation(PEER, harness.charm.app.name) - # Assert that there is no password in the peer relation. - assert harness.charm._peers.data[harness.charm.app].get("operator-password", None) == None +def test_on_leader_elected(harness): + with patch( + "charm.PostgresqlOperatorCharm._update_relation_endpoints", new_callable=PropertyMock + ) as _update_relation_endpoints, patch( + "charm.PostgresqlOperatorCharm.primary_endpoint", + new_callable=PropertyMock, + ) as _primary_endpoint, patch( + "charm.PostgresqlOperatorCharm.update_config" + ) as _update_config: + rel_id = harness.add_relation(PEER, harness.charm.app.name) + # Assert that there is no password in the peer relation. + assert harness.charm._peers.data[harness.charm.app].get("operator-password", None) == None - # Check that a new password was generated on leader election. - _primary_endpoint.return_value = "1.1.1.1" - harness.set_leader() - password = harness.charm._peers.data[harness.charm.app].get("operator-password", None) - _update_config.assert_called_once() - _update_relation_endpoints.assert_not_called() - assert password != None + # Check that a new password was generated on leader election. + _primary_endpoint.return_value = "1.1.1.1" + harness.set_leader() + password = harness.charm._peers.data[harness.charm.app].get("operator-password", None) + _update_config.assert_called_once() + _update_relation_endpoints.assert_not_called() + assert password != None - # Mark the cluster as initialised. - harness.charm._peers.data[harness.charm.app].update({"cluster_initialised": "True"}) + # Mark the cluster as initialised. + harness.charm._peers.data[harness.charm.app].update({"cluster_initialised": "True"}) - # Trigger a new leader election and check that the password is still the same - # and also that update_endpoints was called after the cluster was initialised. - harness.set_leader(False) - harness.set_leader() - assert harness.charm._peers.data[harness.charm.app].get("operator-password", None) == password - _update_relation_endpoints.assert_called_once() - assert not (isinstance(harness.model.unit.status, BlockedStatus)) + # Trigger a new leader election and check that the password is still the same + # and also that update_endpoints was called after the cluster was initialised. + harness.set_leader(False) + harness.set_leader() + assert harness.charm._peers.data[harness.charm.app].get("operator-password", None) == password + _update_relation_endpoints.assert_called_once() + assert not (isinstance(harness.model.unit.status, BlockedStatus)) - # Check for a WaitingStatus when the primary is not reachable yet. - _primary_endpoint.return_value = None - harness.set_leader(False) - harness.set_leader() - _update_relation_endpoints.assert_called_once() # Assert it was not called again. - assert (isinstance(harness.model.unit.status, WaitingStatus)) + # Check for a WaitingStatus when the primary is not reachable yet. + _primary_endpoint.return_value = None + harness.set_leader(False) + harness.set_leader() + _update_relation_endpoints.assert_called_once() # Assert it was not called again. + assert (isinstance(harness.model.unit.status, WaitingStatus)) def test_is_cluster_initialised(harness): rel_id = harness.add_relation(PEER, harness.charm.app.name) @@ -246,88 +249,88 @@ def test_is_cluster_initialised(harness): ) assert (harness.charm.is_cluster_initialised) -@patch("charm.PostgresqlOperatorCharm._validate_config_options") -@patch("charm.PostgresqlOperatorCharm.update_config") -@patch("relations.db.DbProvides.set_up_relation") -@patch("charm.PostgresqlOperatorCharm.enable_disable_extensions") -@patch("charm.PostgresqlOperatorCharm.is_cluster_initialised", new_callable=PropertyMock) -def test_on_config_changed( - harness, - _is_cluster_initialised, - _enable_disable_extensions, - _set_up_relation, - _update_config, - _validate_config_options, -): - rel_id = harness.add_relation(PEER, harness.charm.app.name) - # Test when the cluster was not initialised yet. - _is_cluster_initialised.return_value = False - harness.charm.on.config_changed.emit() - _enable_disable_extensions.assert_not_called() - _set_up_relation.assert_not_called() - - # Test when the unit is not the leader. - _is_cluster_initialised.return_value = True - harness.charm.on.config_changed.emit() - _validate_config_options.assert_called_once() - _enable_disable_extensions.assert_not_called() - _set_up_relation.assert_not_called() - - # Test unable to connect to db - _update_config.reset_mock() - _validate_config_options.side_effect = OperationalError - harness.charm.on.config_changed.emit() - assert not _update_config.called - _validate_config_options.side_effect = None - - # Test after the cluster was initialised. - with harness.hooks_disabled(): - harness.set_leader() - harness.charm.on.config_changed.emit() - _enable_disable_extensions.assert_called_once() - _set_up_relation.assert_not_called() - - # Test when the unit is in a blocked state due to extensions request, - # but there are no established legacy relations. - _enable_disable_extensions.reset_mock() - harness.charm.unit.status = BlockedStatus( - "extensions requested through relation, enable them through config options" - ) - harness.charm.on.config_changed.emit() - _enable_disable_extensions.assert_called_once() - _set_up_relation.assert_not_called() - - # Test when the unit is in a blocked state due to extensions request, - # but there are established legacy relations. - _enable_disable_extensions.reset_mock() - _set_up_relation.return_value = False - db_relation_id = harness.add_relation("db", "application") - harness.charm.on.config_changed.emit() - _enable_disable_extensions.assert_called_once() - _set_up_relation.assert_called_once() - harness.remove_relation(db_relation_id) - - _enable_disable_extensions.reset_mock() - _set_up_relation.reset_mock() - harness.add_relation("db-admin", "application") - harness.charm.on.config_changed.emit() - _enable_disable_extensions.assert_called_once() - _set_up_relation.assert_called_once() - - # Test when there are established legacy relations, - # but the charm fails to set up one of them. - _enable_disable_extensions.reset_mock() - _set_up_relation.reset_mock() - _set_up_relation.return_value = False - harness.add_relation("db", "application") - harness.charm.on.config_changed.emit() - _enable_disable_extensions.assert_called_once() - _set_up_relation.assert_called_once() - -@patch("subprocess.check_output", return_value=b"C") -def test_check_extension_dependencies(harness, _): - rel_id = harness.add_relation(PEER, harness.charm.app.name) - with patch.object(PostgresqlOperatorCharm, "postgresql", Mock()) as _: +def test_on_config_changed(harness): + with patch( + "charm.PostgresqlOperatorCharm._validate_config_options" + ) as _validate_config_options, patch( + "charm.PostgresqlOperatorCharm.update_config" + ) as _update_config, patch( + "relations.db.DbProvides.set_up_relation" + ) as _set_up_relation, patch( + "charm.PostgresqlOperatorCharm.enable_disable_extensions" + ) as _enable_disable_extensions, patch( + "charm.PostgresqlOperatorCharm.is_cluster_initialised", new_callable=PropertyMock + ) as _is_cluster_initialised: + rel_id = harness.add_relation(PEER, harness.charm.app.name) + # Test when the cluster was not initialised yet. + _is_cluster_initialised.return_value = False + harness.charm.on.config_changed.emit() + _enable_disable_extensions.assert_not_called() + _set_up_relation.assert_not_called() + + # Test when the unit is not the leader. + _is_cluster_initialised.return_value = True + harness.charm.on.config_changed.emit() + _validate_config_options.assert_called_once() + _enable_disable_extensions.assert_not_called() + _set_up_relation.assert_not_called() + + # Test unable to connect to db + _update_config.reset_mock() + _validate_config_options.side_effect = OperationalError + harness.charm.on.config_changed.emit() + assert not _update_config.called + _validate_config_options.side_effect = None + + # Test after the cluster was initialised. + with harness.hooks_disabled(): + harness.set_leader() + harness.charm.on.config_changed.emit() + _enable_disable_extensions.assert_called_once() + _set_up_relation.assert_not_called() + + # Test when the unit is in a blocked state due to extensions request, + # but there are no established legacy relations. + _enable_disable_extensions.reset_mock() + harness.charm.unit.status = BlockedStatus( + "extensions requested through relation, enable them through config options" + ) + harness.charm.on.config_changed.emit() + _enable_disable_extensions.assert_called_once() + _set_up_relation.assert_not_called() + + # Test when the unit is in a blocked state due to extensions request, + # but there are established legacy relations. + _enable_disable_extensions.reset_mock() + _set_up_relation.return_value = False + db_relation_id = harness.add_relation("db", "application") + harness.charm.on.config_changed.emit() + _enable_disable_extensions.assert_called_once() + _set_up_relation.assert_called_once() + harness.remove_relation(db_relation_id) + + _enable_disable_extensions.reset_mock() + _set_up_relation.reset_mock() + harness.add_relation("db-admin", "application") + harness.charm.on.config_changed.emit() + _enable_disable_extensions.assert_called_once() + _set_up_relation.assert_called_once() + + # Test when there are established legacy relations, + # but the charm fails to set up one of them. + _enable_disable_extensions.reset_mock() + _set_up_relation.reset_mock() + _set_up_relation.return_value = False + harness.add_relation("db", "application") + harness.charm.on.config_changed.emit() + _enable_disable_extensions.assert_called_once() + _set_up_relation.assert_called_once() + +def test_check_extension_dependencies(harness): + with patch("subprocess.check_output", return_value=b"C"), patch.object( + PostgresqlOperatorCharm, "postgresql", Mock() + ): + rel_id = harness.add_relation(PEER, harness.charm.app.name) # Test when plugins dependencies exception is not caused config = { "plugin_address_standardizer_enable": False, @@ -351,10 +354,11 @@ def test_check_extension_dependencies(harness, _): assert (isinstance(harness.model.unit.status, BlockedStatus)) assert harness.model.unit.status.message == EXTENSIONS_DEPENDENCY_MESSAGE -@patch("subprocess.check_output", return_value=b"C") -def test_enable_disable_extensions(harness, caplog, _): - rel_id = harness.add_relation(PEER, harness.charm.app.name) - with patch.object(PostgresqlOperatorCharm, "postgresql", Mock()) as postgresql_mock: +def test_enable_disable_extensions(harness, caplog): + with patch("subprocess.check_output", return_value=b"C"), patch.object( + PostgresqlOperatorCharm, "postgresql", Mock() + ) as postgresql_mock: + rel_id = harness.add_relation(PEER, harness.charm.app.name) # Test when all extensions install/uninstall succeed. postgresql_mock.enable_disable_extension.side_effect = None with caplog.at_level(logging.ERROR): @@ -370,172 +374,171 @@ def test_enable_disable_extensions(harness, caplog, _): with caplog.at_level(logging.ERROR): harness.charm.enable_disable_extensions() assert postgresql_mock.enable_disable_extensions.call_count == 1 - assert "failed to change plugins" in [rec.message for rec in caplog.records] + assert "failed to change plugins: " in caplog.text # Test when one config option should be skipped (because it's not related # to a plugin/extension). postgresql_mock.reset_mock() postgresql_mock.enable_disable_extensions.side_effect = None with caplog.at_level(logging.ERROR): - assert len(caplog.records) == 0 config = """options: -plugin_citext_enable: -default: false -type: boolean -plugin_hstore_enable: -default: false -type: boolean -plugin_pg_trgm_enable: -default: false -type: boolean -plugin_plpython3u_enable: -default: false -type: boolean -plugin_unaccent_enable: -default: false -type: boolean -plugin_debversion_enable: -default: false -type: boolean -plugin_bloom_enable: -default: false -type: boolean -plugin_btree_gin_enable: -default: false -type: boolean -plugin_btree_gist_enable: -default: false -type: boolean -plugin_cube_enable: -default: false -type: boolean -plugin_dict_int_enable: -default: false -type: boolean -plugin_dict_xsyn_enable: -default: false -type: boolean -plugin_earthdistance_enable: -default: false -type: boolean -plugin_fuzzystrmatch_enable: -default: false -type: boolean -plugin_intarray_enable: -default: false -type: boolean -plugin_isn_enable: -default: false -type: boolean -plugin_lo_enable: -default: false -type: boolean -plugin_ltree_enable: -default: false -type: boolean -plugin_old_snapshot_enable: -default: false -type: boolean -plugin_pg_freespacemap_enable: -default: false -type: boolean -plugin_pgrowlocks_enable: -default: false -type: boolean -plugin_pgstattuple_enable: -default: false -type: boolean -plugin_pg_visibility_enable: -default: false -type: boolean -plugin_seg_enable: -default: false -type: boolean -plugin_tablefunc_enable: -default: false -type: boolean -plugin_tcn_enable: -default: false -type: boolean -plugin_tsm_system_rows_enable: -default: false -type: boolean -plugin_tsm_system_time_enable: -default: false -type: boolean -plugin_uuid_ossp_enable: -default: false -type: boolean -plugin_spi_enable: -default: false -type: boolean -plugin_bool_plperl_enable: -default: false -type: boolean -plugin_hll_enable: -default: false -type: boolean -plugin_hypopg_enable: -default: false -type: boolean -plugin_ip4r_enable: -default: false -type: boolean -plugin_plperl_enable: -default: false -type: boolean -plugin_jsonb_plperl_enable: -default: false -type: boolean -plugin_orafce_enable: -default: false -type: boolean -plugin_pg_similarity_enable: -default: false -type: boolean -plugin_prefix_enable: -default: false -type: boolean -plugin_rdkit_enable: -default: false -type: boolean -plugin_tds_fdw_enable: -default: false -type: boolean -plugin_icu_ext_enable: -default: false -type: boolean -plugin_pltcl_enable: -default: false -type: boolean -plugin_postgis_enable: -default: false -type: boolean -plugin_postgis_raster_enable: -default: false -type: boolean -plugin_address_standardizer_enable: -default: false -type: boolean -plugin_address_standardizer_data_us_enable: -default: false -type: boolean -plugin_postgis_tiger_geocoder_enable: -default: false -type: boolean -plugin_postgis_topology_enable: -default: false -type: boolean -plugin_vector_enable: -default: false -type: boolean -profile: -default: production -type: string""" - harness = Harness(PostgresqlOperatorCharm, config=config) - harness.cleanup() - harness.begin() - harness.charm.enable_disable_extensions() + plugin_citext_enable: + default: false + type: boolean + plugin_hstore_enable: + default: false + type: boolean + plugin_pg_trgm_enable: + default: false + type: boolean + plugin_plpython3u_enable: + default: false + type: boolean + plugin_unaccent_enable: + default: false + type: boolean + plugin_debversion_enable: + default: false + type: boolean + plugin_bloom_enable: + default: false + type: boolean + plugin_btree_gin_enable: + default: false + type: boolean + plugin_btree_gist_enable: + default: false + type: boolean + plugin_cube_enable: + default: false + type: boolean + plugin_dict_int_enable: + default: false + type: boolean + plugin_dict_xsyn_enable: + default: false + type: boolean + plugin_earthdistance_enable: + default: false + type: boolean + plugin_fuzzystrmatch_enable: + default: false + type: boolean + plugin_intarray_enable: + default: false + type: boolean + plugin_isn_enable: + default: false + type: boolean + plugin_lo_enable: + default: false + type: boolean + plugin_ltree_enable: + default: false + type: boolean + plugin_old_snapshot_enable: + default: false + type: boolean + plugin_pg_freespacemap_enable: + default: false + type: boolean + plugin_pgrowlocks_enable: + default: false + type: boolean + plugin_pgstattuple_enable: + default: false + type: boolean + plugin_pg_visibility_enable: + default: false + type: boolean + plugin_seg_enable: + default: false + type: boolean + plugin_tablefunc_enable: + default: false + type: boolean + plugin_tcn_enable: + default: false + type: boolean + plugin_tsm_system_rows_enable: + default: false + type: boolean + plugin_tsm_system_time_enable: + default: false + type: boolean + plugin_uuid_ossp_enable: + default: false + type: boolean + plugin_spi_enable: + default: false + type: boolean + plugin_bool_plperl_enable: + default: false + type: boolean + plugin_hll_enable: + default: false + type: boolean + plugin_hypopg_enable: + default: false + type: boolean + plugin_ip4r_enable: + default: false + type: boolean + plugin_plperl_enable: + default: false + type: boolean + plugin_jsonb_plperl_enable: + default: false + type: boolean + plugin_orafce_enable: + default: false + type: boolean + plugin_pg_similarity_enable: + default: false + type: boolean + plugin_prefix_enable: + default: false + type: boolean + plugin_rdkit_enable: + default: false + type: boolean + plugin_tds_fdw_enable: + default: false + type: boolean + plugin_icu_ext_enable: + default: false + type: boolean + plugin_pltcl_enable: + default: false + type: boolean + plugin_postgis_enable: + default: false + type: boolean + plugin_postgis_raster_enable: + default: false + type: boolean + plugin_address_standardizer_enable: + default: false + type: boolean + plugin_address_standardizer_data_us_enable: + default: false + type: boolean + plugin_postgis_tiger_geocoder_enable: + default: false + type: boolean + plugin_postgis_topology_enable: + default: false + type: boolean + plugin_vector_enable: + default: false + type: boolean + profile: + default: production + type: string""" + new_harness = Harness(PostgresqlOperatorCharm, config=config) + new_harness.cleanup() + new_harness.begin() + new_harness.charm.enable_disable_extensions() assert postgresql_mock.enable_disable_extensions.call_count == 1 @patch("charm.PostgresqlOperatorCharm.enable_disable_extensions") diff --git a/tox.ini b/tox.ini index 2254138f0b..7979c7eb3b 100644 --- a/tox.ini +++ b/tox.ini @@ -65,7 +65,7 @@ commands_pre = poetry install --only main,charm-libs,unit --no-root commands = poetry run coverage run --source={[vars]src_path} \ - -m pytest -v --tb native -s {posargs} {[vars]tests_path}/unit/test_charm.py::test_on_install_snap_failure + -m pytest -v --tb native -s {posargs} {[vars]tests_path}/unit/test_charm.py::test_enable_disable_extensions poetry run coverage report poetry run coverage xml From af26860e1503a56d43ea1b5c6630fed791ea6fcb Mon Sep 17 00:00:00 2001 From: lucasgameiroborges Date: Wed, 3 Apr 2024 18:59:53 +0000 Subject: [PATCH 04/10] convert tests from unittest to pytest --- tests/unit/test_charm.py | 2894 +++++++++++++++++++------------------- tox.ini | 2 +- 2 files changed, 1419 insertions(+), 1477 deletions(-) diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py index 3cbd0b2385..148aa0f25c 100644 --- a/tests/unit/test_charm.py +++ b/tests/unit/test_charm.py @@ -541,659 +541,595 @@ def test_enable_disable_extensions(harness, caplog): new_harness.charm.enable_disable_extensions() assert postgresql_mock.enable_disable_extensions.call_count == 1 -@patch("charm.PostgresqlOperatorCharm.enable_disable_extensions") -@patch("charm.snap.SnapCache") -@patch("charm.Patroni.get_postgresql_version") @patch_network_get(private_address="1.1.1.1") -@patch("charm.PostgreSQLProvider.oversee_users") -@patch("charm.PostgresqlOperatorCharm._update_relation_endpoints", new_callable=PropertyMock) -@patch("charm.PostgresqlOperatorCharm.postgresql") -@patch("charm.PostgreSQLProvider.update_endpoints") -@patch("charm.PostgresqlOperatorCharm.update_config") -@patch( - "charm.Patroni.member_started", - new_callable=PropertyMock, -) -@patch("charm.Patroni.bootstrap_cluster") -@patch("charm.PostgresqlOperatorCharm._replication_password") -@patch("charm.PostgresqlOperatorCharm._get_password") -@patch("charm.PostgresqlOperatorCharm._reboot_on_detached_storage") -@patch("upgrade.PostgreSQLUpgrade.idle", return_value=True) -@patch( - "charm.PostgresqlOperatorCharm._is_storage_attached", - side_effect=[False, True, True, True, True], -) -def test_on_start( - harness, - _is_storage_attached, - _idle, - _reboot_on_detached_storage, - _get_password, - _replication_password, - _bootstrap_cluster, - _member_started, - _, - __, - _postgresql, - _update_relation_endpoints, - _oversee_users, - _get_postgresql_version, - _snap_cache, - _enable_disable_extensions, -): - rel_id = harness.add_relation(PEER, harness.charm.app.name) - _get_postgresql_version.return_value = "14.0" - - # Test without storage. - harness.charm.on.start.emit() - _reboot_on_detached_storage.assert_called_once() - - # Test before the passwords are generated. - _member_started.return_value = False - _get_password.return_value = None - harness.charm.on.start.emit() - _bootstrap_cluster.assert_not_called() - assert (isinstance(harness.model.unit.status, WaitingStatus)) - - # Mock the passwords. - _get_password.return_value = "fake-operator-password" - _replication_password.return_value = "fake-replication-password" - - # Mock cluster start and postgres user creation success values. - _bootstrap_cluster.side_effect = [False, True, True] - _postgresql.list_users.side_effect = [[], [], []] - _postgresql.create_user.side_effect = [PostgreSQLCreateUserError, None, None, None] - - # Test for a failed cluster bootstrapping. - # TODO: test replicas start (DPE-494). - harness.set_leader() - harness.charm.on.start.emit() - _bootstrap_cluster.assert_called_once() - _oversee_users.assert_not_called() - assert (isinstance(harness.model.unit.status, BlockedStatus)) - # Set an initial waiting status (like after the install hook was triggered). - harness.model.unit.status = WaitingStatus("fake message") - - # Test the event of an error happening when trying to create the default postgres user. - _member_started.return_value = True - harness.charm.on.start.emit() - _postgresql.create_user.assert_called_once() - _oversee_users.assert_not_called() - assert (isinstance(harness.model.unit.status, BlockedStatus)) - - # Set an initial waiting status again (like after the install hook was triggered). - harness.model.unit.status = WaitingStatus("fake message") - - # Then test the event of a correct cluster bootstrapping. - harness.charm.on.start.emit() - assert _postgresql.create_user.call_count == 4 # Considering the previous failed call. - _oversee_users.assert_called_once() - _enable_disable_extensions.assert_called_once() - assert (isinstance(harness.model.unit.status, ActiveStatus)) - -@patch("charm.snap.SnapCache") -@patch("charm.Patroni.get_postgresql_version") -@patch_network_get(private_address="1.1.1.1") -@patch("charm.Patroni.configure_patroni_on_unit") -@patch( - "charm.Patroni.member_started", - new_callable=PropertyMock, -) -@patch("charm.PostgresqlOperatorCharm._update_relation_endpoints", new_callable=PropertyMock) -@patch.object(EventBase, "defer") -@patch("charm.PostgresqlOperatorCharm._replication_password") -@patch("charm.PostgresqlOperatorCharm._get_password") -@patch("upgrade.PostgreSQLUpgrade.idle", return_value=True) -@patch( - "charm.PostgresqlOperatorCharm._is_storage_attached", - return_value=True, -) -def test_on_start_replica( - harness, - _is_storage_attached, - _idle, - _get_password, - _replication_password, - _defer, - _update_relation_endpoints, - _member_started, - _configure_patroni_on_unit, - _get_postgresql_version, - _snap_cache, -): - rel_id = harness.add_relation(PEER, harness.charm.app.name) - _get_postgresql_version.return_value = "14.0" +def test_on_start(harness): + with ( + patch("charm.PostgresqlOperatorCharm.enable_disable_extensions") as _enable_disable_extensions, + patch("charm.snap.SnapCache") as _snap_cache, + patch("charm.Patroni.get_postgresql_version") as _get_postgresql_version, + patch("charm.PostgreSQLProvider.oversee_users") as _oversee_users, + patch("charm.PostgresqlOperatorCharm._update_relation_endpoints", new_callable=PropertyMock) as _update_relation_endpoints, + patch("charm.PostgresqlOperatorCharm.postgresql") as _postgresql, + patch("charm.PostgreSQLProvider.update_endpoints"), + patch("charm.PostgresqlOperatorCharm.update_config"), + patch( + "charm.Patroni.member_started", + new_callable=PropertyMock, + ) as _member_started, + patch("charm.Patroni.bootstrap_cluster") as _bootstrap_cluster, + patch("charm.PostgresqlOperatorCharm._replication_password") as _replication_password, + patch("charm.PostgresqlOperatorCharm._get_password") as _get_password, + patch("charm.PostgresqlOperatorCharm._reboot_on_detached_storage") as _reboot_on_detached_storage, + patch("upgrade.PostgreSQLUpgrade.idle", return_value=True) as _idle, + patch( + "charm.PostgresqlOperatorCharm._is_storage_attached", + side_effect=[False, True, True, True, True], + ) as _is_storage_attached, + ): + rel_id = harness.add_relation(PEER, harness.charm.app.name) + _get_postgresql_version.return_value = "14.0" - # Set the current unit to be a replica (non leader unit). - harness.set_leader(False) + # Test without storage. + harness.charm.on.start.emit() + _reboot_on_detached_storage.assert_called_once() - # Mock the passwords. - _get_password.return_value = "fake-operator-password" - _replication_password.return_value = "fake-replication-password" + # Test before the passwords are generated. + _member_started.return_value = False + _get_password.return_value = None + harness.charm.on.start.emit() + _bootstrap_cluster.assert_not_called() + assert (isinstance(harness.model.unit.status, WaitingStatus)) - # Test an uninitialized cluster. - harness.charm._peers.data[harness.charm.app].update({"cluster_initialised": ""}) - harness.charm.on.start.emit() - _defer.assert_called_once() + # Mock the passwords. + _get_password.return_value = "fake-operator-password" + _replication_password.return_value = "fake-replication-password" - # Set an initial waiting status again (like after a machine restart). - harness.model.unit.status = WaitingStatus("fake message") + # Mock cluster start and postgres user creation success values. + _bootstrap_cluster.side_effect = [False, True, True] + _postgresql.list_users.side_effect = [[], [], []] + _postgresql.create_user.side_effect = [PostgreSQLCreateUserError, None, None, None] - # Mark the cluster as initialised and with the workload up and running. - harness.charm._peers.data[harness.charm.app].update({"cluster_initialised": "True"}) - _member_started.return_value = True - harness.charm.on.start.emit() - _configure_patroni_on_unit.assert_not_called() - assert (isinstance(harness.model.unit.status, ActiveStatus)) + # Test for a failed cluster bootstrapping. + # TODO: test replicas start (DPE-494). + harness.set_leader() + harness.charm.on.start.emit() + _bootstrap_cluster.assert_called_once() + _oversee_users.assert_not_called() + assert (isinstance(harness.model.unit.status, BlockedStatus)) + # Set an initial waiting status (like after the install hook was triggered). + harness.model.unit.status = WaitingStatus("fake message") + + # Test the event of an error happening when trying to create the default postgres user. + _member_started.return_value = True + harness.charm.on.start.emit() + _postgresql.create_user.assert_called_once() + _oversee_users.assert_not_called() + assert (isinstance(harness.model.unit.status, BlockedStatus)) - # Set an initial waiting status (like after the install hook was triggered). - harness.model.unit.status = WaitingStatus("fake message") + # Set an initial waiting status again (like after the install hook was triggered). + harness.model.unit.status = WaitingStatus("fake message") - # Check that the unit status doesn't change when the workload is not running. - # In that situation only Patroni is configured in the unit (but not started). - _member_started.return_value = False - harness.charm.on.start.emit() - _configure_patroni_on_unit.assert_called_once() - assert (isinstance(harness.model.unit.status, WaitingStatus)) + # Then test the event of a correct cluster bootstrapping. + harness.charm.on.start.emit() + assert _postgresql.create_user.call_count == 4 # Considering the previous failed call. + _oversee_users.assert_called_once() + _enable_disable_extensions.assert_called_once() + assert (isinstance(harness.model.unit.status, ActiveStatus)) @patch_network_get(private_address="1.1.1.1") -@patch("subprocess.check_output", return_value=b"C") -@patch("charm.snap.SnapCache") -@patch("charm.PostgresqlOperatorCharm.postgresql") -@patch("charm.Patroni") -@patch("charm.PostgresqlOperatorCharm._get_password") -@patch("upgrade.PostgreSQLUpgrade.idle", return_value=True) -@patch("charm.PostgresqlOperatorCharm._is_storage_attached", return_value=True) -def test_on_start_no_patroni_member( - harness, - _is_storage_attached, - _idle, - _get_password, - patroni, - _postgresql, - _snap_cache, - _, -): - rel_id = harness.add_relation(PEER, harness.charm.app.name) - # Mock the passwords. - patroni.return_value.member_started = False - _get_password.return_value = "fake-operator-password" - bootstrap_cluster = patroni.return_value.bootstrap_cluster - bootstrap_cluster.return_value = True - - patroni.return_value.get_postgresql_version.return_value = "14.0" - - harness.set_leader() - harness.charm.on.start.emit() - bootstrap_cluster.assert_called_once() - _postgresql.create_user.assert_not_called() - assert (isinstance(harness.model.unit.status, WaitingStatus)) - assert harness.model.unit.status.message == "awaiting for member to start" - -@patch("charm.Patroni.bootstrap_cluster") -@patch("charm.PostgresqlOperatorCharm._replication_password") -@patch("charm.PostgresqlOperatorCharm._get_password") -@patch("charm.PostgresqlOperatorCharm._is_storage_attached", return_value=True) -def test_on_start_after_blocked_state( - harness, _is_storage_attached, _get_password, _replication_password, _bootstrap_cluster -): - rel_id = harness.add_relation(PEER, harness.charm.app.name) - # Set an initial blocked status (like after the install hook was triggered). - initial_status = BlockedStatus("fake message") - harness.model.unit.status = initial_status - - # Test for a failed cluster bootstrapping. - harness.charm.on.start.emit() - _get_password.assert_not_called() - _replication_password.assert_not_called() - _bootstrap_cluster.assert_not_called() - # Assert the status didn't change. - assert harness.model.unit.status == initial_status +def test_on_start_replica(harness): + with ( + patch("charm.snap.SnapCache") as _snap_cache, + patch("charm.Patroni.get_postgresql_version") as _get_postgresql_version, + patch("charm.Patroni.configure_patroni_on_unit") as _configure_patroni_on_unit, + patch( + "charm.Patroni.member_started", + new_callable=PropertyMock, + ) as _member_started, + patch("charm.PostgresqlOperatorCharm._update_relation_endpoints", new_callable=PropertyMock) as _update_relation_endpoints, + patch.object(EventBase, "defer") as _defer, + patch("charm.PostgresqlOperatorCharm._replication_password") as _replication_password, + patch("charm.PostgresqlOperatorCharm._get_password") as _get_password, + patch("upgrade.PostgreSQLUpgrade.idle", return_value=True) as _idle, + patch( + "charm.PostgresqlOperatorCharm._is_storage_attached", + return_value=True, + ) as _is_storage_attached, + ): + rel_id = harness.add_relation(PEER, harness.charm.app.name) + _get_postgresql_version.return_value = "14.0" -@patch_network_get(private_address="1.1.1.1") -@patch("charm.PostgresqlOperatorCharm.update_config") -def test_on_get_password(harness, _): - rel_id = harness.add_relation(PEER, harness.charm.app.name) - # Create a mock event and set passwords in peer relation data. - harness.set_leader(True) - mock_event = MagicMock(params={}) - harness.update_relation_data( - rel_id, - harness.charm.app.name, - { - "operator-password": "test-password", - "replication-password": "replication-test-password", - }, - ) + # Set the current unit to be a replica (non leader unit). + harness.set_leader(False) + + # Mock the passwords. + _get_password.return_value = "fake-operator-password" + _replication_password.return_value = "fake-replication-password" - # Test providing an invalid username. - mock_event.params["username"] = "user" - harness.charm._on_get_password(mock_event) - mock_event.fail.assert_called_once() - mock_event.set_results.assert_not_called() + # Test an uninitialized cluster. + harness.charm._peers.data[harness.charm.app].update({"cluster_initialised": ""}) + harness.charm.on.start.emit() + _defer.assert_called_once() - # Test without providing the username option. - mock_event.reset_mock() - del mock_event.params["username"] - harness.charm._on_get_password(mock_event) - mock_event.set_results.assert_called_once_with({"password": "test-password"}) + # Set an initial waiting status again (like after a machine restart). + harness.model.unit.status = WaitingStatus("fake message") - # Also test providing the username option. - mock_event.reset_mock() - mock_event.params["username"] = "replication" - harness.charm._on_get_password(mock_event) - mock_event.set_results.assert_called_once_with({"password": "replication-test-password"}) + # Mark the cluster as initialised and with the workload up and running. + harness.charm._peers.data[harness.charm.app].update({"cluster_initialised": "True"}) + _member_started.return_value = True + harness.charm.on.start.emit() + _configure_patroni_on_unit.assert_not_called() + assert (isinstance(harness.model.unit.status, ActiveStatus)) + + # Set an initial waiting status (like after the install hook was triggered). + harness.model.unit.status = WaitingStatus("fake message") + + # Check that the unit status doesn't change when the workload is not running. + # In that situation only Patroni is configured in the unit (but not started). + _member_started.return_value = False + harness.charm.on.start.emit() + _configure_patroni_on_unit.assert_called_once() + assert (isinstance(harness.model.unit.status, WaitingStatus)) @patch_network_get(private_address="1.1.1.1") -@patch("charm.PostgresqlOperatorCharm.update_config") -@patch("charm.PostgresqlOperatorCharm.set_secret") -@patch("charm.PostgresqlOperatorCharm.postgresql") -@patch("charm.Patroni.are_all_members_ready") -@patch("charm.PostgresqlOperatorCharm._on_leader_elected") -def test_on_set_password( - harness, - _, - _are_all_members_ready, - _postgresql, - _set_secret, - __, -): - rel_id = harness.add_relation(PEER, harness.charm.app.name) - # Create a mock event. - mock_event = MagicMock(params={}) +def test_on_start_no_patroni_member(harness): + with ( + patch("subprocess.check_output", return_value=b"C"), + patch("charm.snap.SnapCache") as _snap_cache, + patch("charm.PostgresqlOperatorCharm.postgresql") as _postgresql, + patch("charm.Patroni") as patroni, + patch("charm.PostgresqlOperatorCharm._get_password") as _get_password, + patch("upgrade.PostgreSQLUpgrade.idle", return_value=True) as _idle, + patch("charm.PostgresqlOperatorCharm._is_storage_attached", return_value=True) as _is_storage_attached, + ): + rel_id = harness.add_relation(PEER, harness.charm.app.name) + # Mock the passwords. + patroni.return_value.member_started = False + _get_password.return_value = "fake-operator-password" + bootstrap_cluster = patroni.return_value.bootstrap_cluster + bootstrap_cluster.return_value = True - # Set some values for the other mocks. - _are_all_members_ready.side_effect = [False, True, True, True, True] - _postgresql.update_user_password = PropertyMock( - side_effect=[PostgreSQLUpdateUserPasswordError, None, None, None] - ) + patroni.return_value.get_postgresql_version.return_value = "14.0" - # Test trying to set a password through a non leader unit. - harness.charm._on_set_password(mock_event) - mock_event.fail.assert_called_once() - _set_secret.assert_not_called() - - # Test providing an invalid username. - harness.set_leader() - mock_event.reset_mock() - mock_event.params["username"] = "user" - harness.charm._on_set_password(mock_event) - mock_event.fail.assert_called_once() - _set_secret.assert_not_called() - - # Test without providing the username option but without all cluster members ready. - mock_event.reset_mock() - del mock_event.params["username"] - harness.charm._on_set_password(mock_event) - mock_event.fail.assert_called_once() - _set_secret.assert_not_called() - - # Test for an error updating when updating the user password in the database. - mock_event.reset_mock() - harness.charm._on_set_password(mock_event) - mock_event.fail.assert_called_once() - _set_secret.assert_not_called() - - # Test without providing the username option. - harness.charm._on_set_password(mock_event) - assert _set_secret.call_args_list[0][0][1] == "operator-password" - - # Also test providing the username option. - _set_secret.reset_mock() - mock_event.params["username"] = "replication" - harness.charm._on_set_password(mock_event) - assert _set_secret.call_args_list[0][0][1] == "replication-password" - - # And test providing both the username and password options. - _set_secret.reset_mock() - mock_event.params["password"] = "replication-test-password" - harness.charm._on_set_password(mock_event) - _set_secret.assert_called_once_with( - "app", "replication-password", "replication-test-password" - ) + harness.set_leader() + harness.charm.on.start.emit() + bootstrap_cluster.assert_called_once() + _postgresql.create_user.assert_not_called() + assert (isinstance(harness.model.unit.status, WaitingStatus)) + assert harness.model.unit.status.message == "awaiting for member to start" + +def test_on_start_after_blocked_state(harness): + with ( + patch("charm.Patroni.bootstrap_cluster") as _bootstrap_cluster, + patch("charm.PostgresqlOperatorCharm._replication_password") as _replication_password, + patch("charm.PostgresqlOperatorCharm._get_password") as _get_password, + patch("charm.PostgresqlOperatorCharm._is_storage_attached", return_value=True) as _is_storage_attached, + ): + rel_id = harness.add_relation(PEER, harness.charm.app.name) + # Set an initial blocked status (like after the install hook was triggered). + initial_status = BlockedStatus("fake message") + harness.model.unit.status = initial_status + + # Test for a failed cluster bootstrapping. + harness.charm.on.start.emit() + _get_password.assert_not_called() + _replication_password.assert_not_called() + _bootstrap_cluster.assert_not_called() + # Assert the status didn't change. + assert harness.model.unit.status == initial_status @patch_network_get(private_address="1.1.1.1") -@patch("charm.ClusterTopologyObserver.start_observer") -@patch("charm.PostgresqlOperatorCharm._set_primary_status_message") -@patch("charm.Patroni.restart_patroni") -@patch("charm.Patroni.is_member_isolated") -@patch("charm.Patroni.reinitialize_postgresql") -@patch("charm.Patroni.member_replication_lag", new_callable=PropertyMock) -@patch("charm.Patroni.member_started", new_callable=PropertyMock) -@patch("charm.PostgresqlOperatorCharm._update_relation_endpoints") -@patch( - "charm.PostgresqlOperatorCharm.primary_endpoint", - new_callable=PropertyMock(return_value=True), -) -@patch("charm.PostgreSQLProvider.oversee_users") -@patch("upgrade.PostgreSQLUpgrade.idle", return_value=True) -def test_on_update_status( - harness, - _, - _oversee_users, - _primary_endpoint, - _update_relation_endpoints, - _member_started, - _member_replication_lag, - _reinitialize_postgresql, - _is_member_isolated, - _restart_patroni, - _set_primary_status_message, - _start_observer, -): - rel_id = harness.add_relation(PEER, harness.charm.app.name) - # Test before the cluster is initialised. - harness.charm.on.update_status.emit() - _set_primary_status_message.assert_not_called() - - # Test after the cluster was initialised, but with the unit in a blocked state. - with harness.hooks_disabled(): - harness.update_relation_data( - rel_id, harness.charm.app.name, {"cluster_initialised": "True"} - ) - harness.charm.unit.status = BlockedStatus("fake blocked status") - harness.charm.on.update_status.emit() - _set_primary_status_message.assert_not_called() - - # Test with the unit in a status different that blocked. - harness.charm.unit.status = ActiveStatus() - harness.charm.on.update_status.emit() - _set_primary_status_message.assert_called_once() - - # Test the reinitialisation of the replica when its lag is unknown - # after a restart. - _set_primary_status_message.reset_mock() - _member_started.return_value = False - _is_member_isolated.return_value = False - _member_replication_lag.return_value = "unknown" - with harness.hooks_disabled(): +def test_on_get_password(harness): + with patch("charm.PostgresqlOperatorCharm.update_config"): + rel_id = harness.add_relation(PEER, harness.charm.app.name) + # Create a mock event and set passwords in peer relation data. + harness.set_leader(True) + mock_event = MagicMock(params={}) harness.update_relation_data( - rel_id, harness.charm.unit.name, {"postgresql_restarted": "True"} + rel_id, + harness.charm.app.name, + { + "operator-password": "test-password", + "replication-password": "replication-test-password", + }, ) - harness.charm.on.update_status.emit() - _reinitialize_postgresql.assert_called_once() - _restart_patroni.assert_not_called() - _set_primary_status_message.assert_not_called() - # Test call to restart when the member is isolated from the cluster. - _is_member_isolated.return_value = True - with harness.hooks_disabled(): - harness.update_relation_data( - rel_id, harness.charm.unit.name, {"postgresql_restarted": ""} - ) - harness.charm.on.update_status.emit() - _restart_patroni.assert_called_once() - _start_observer.assert_called_once() + # Test providing an invalid username. + mock_event.params["username"] = "user" + harness.charm._on_get_password(mock_event) + mock_event.fail.assert_called_once() + mock_event.set_results.assert_not_called() + + # Test without providing the username option. + mock_event.reset_mock() + del mock_event.params["username"] + harness.charm._on_get_password(mock_event) + mock_event.set_results.assert_called_once_with({"password": "test-password"}) + + # Also test providing the username option. + mock_event.reset_mock() + mock_event.params["username"] = "replication" + harness.charm._on_get_password(mock_event) + mock_event.set_results.assert_called_once_with({"password": "replication-test-password"}) @patch_network_get(private_address="1.1.1.1") -@patch("charm.ClusterTopologyObserver.start_observer") -@patch("charm.PostgresqlOperatorCharm._set_primary_status_message") -@patch("charm.PostgresqlOperatorCharm._handle_workload_failures") -@patch("charm.PostgresqlOperatorCharm._update_relation_endpoints") -@patch( - "charm.PostgresqlOperatorCharm.primary_endpoint", - new_callable=PropertyMock(return_value=True), -) -@patch("charm.PostgreSQLProvider.oversee_users") -@patch("charm.PostgresqlOperatorCharm._handle_processes_failures") -@patch("charm.PostgreSQLBackups.can_use_s3_repository") -@patch("charm.PostgresqlOperatorCharm.update_config") -@patch("charm.Patroni.member_started", new_callable=PropertyMock) -@patch("charm.Patroni.get_member_status") -@patch("upgrade.PostgreSQLUpgrade.idle", return_value=True) -def test_on_update_status_after_restore_operation( - harness, - _, - _get_member_status, - _member_started, - _update_config, - _can_use_s3_repository, - _handle_processes_failures, - _oversee_users, - _primary_endpoint, - _update_relation_endpoints, - _handle_workload_failures, - _set_primary_status_message, - __, -): - rel_id = harness.add_relation(PEER, harness.charm.app.name) - # Test when the restore operation fails. - with harness.hooks_disabled(): - harness.set_leader() - harness.update_relation_data( - rel_id, - harness.charm.app.name, - {"cluster_initialised": "True", "restoring-backup": "2023-01-01T09:00:00Z"}, +def test_on_set_password(harness): + with ( + patch("charm.PostgresqlOperatorCharm.update_config"), + patch("charm.PostgresqlOperatorCharm.set_secret") as _set_secret, + patch("charm.PostgresqlOperatorCharm.postgresql") as _postgresql, + patch("charm.Patroni.are_all_members_ready") as _are_all_members_ready, + patch("charm.PostgresqlOperatorCharm._on_leader_elected"), + ): + rel_id = harness.add_relation(PEER, harness.charm.app.name) + # Create a mock event. + mock_event = MagicMock(params={}) + + # Set some values for the other mocks. + _are_all_members_ready.side_effect = [False, True, True, True, True] + _postgresql.update_user_password = PropertyMock( + side_effect=[PostgreSQLUpdateUserPasswordError, None, None, None] ) - _get_member_status.return_value = "failed" - harness.charm.on.update_status.emit() - _update_config.assert_not_called() - _handle_processes_failures.assert_not_called() - _oversee_users.assert_not_called() - _update_relation_endpoints.assert_not_called() - _handle_workload_failures.assert_not_called() - _set_primary_status_message.assert_not_called() - assert isinstance(harness.charm.unit.status, BlockedStatus) - - # Test when the restore operation hasn't finished yet. - harness.charm.unit.status = ActiveStatus() - _get_member_status.return_value = "running" - _member_started.return_value = False - harness.charm.on.update_status.emit() - _update_config.assert_not_called() - _handle_processes_failures.assert_not_called() - _oversee_users.assert_not_called() - _update_relation_endpoints.assert_not_called() - _handle_workload_failures.assert_not_called() - _set_primary_status_message.assert_not_called() - assert isinstance(harness.charm.unit.status, ActiveStatus) - - # Assert that the backup id is still in the application relation databag. - assert harness.get_relation_data(rel_id, harness.charm.app) == {"cluster_initialised": "True", "restoring-backup": "2023-01-01T09:00:00Z"} - - # Test when the restore operation finished successfully. - _member_started.return_value = True - _can_use_s3_repository.return_value = (True, None) - _handle_processes_failures.return_value = False - _handle_workload_failures.return_value = False - harness.charm.on.update_status.emit() - _update_config.assert_called_once() - _handle_processes_failures.assert_called_once() - _oversee_users.assert_called_once() - _update_relation_endpoints.assert_called_once() - _handle_workload_failures.assert_called_once() - _set_primary_status_message.assert_called_once() - assert isinstance(harness.charm.unit.status, ActiveStatus) - - # Assert that the backup id is not in the application relation databag anymore. - assert harness.get_relation_data(rel_id, harness.charm.app) == {"cluster_initialised": "True"} - - # Test when it's not possible to use the configured S3 repository. - _update_config.reset_mock() - _handle_processes_failures.reset_mock() - _oversee_users.reset_mock() - _update_relation_endpoints.reset_mock() - _handle_workload_failures.reset_mock() - _set_primary_status_message.reset_mock() - with harness.hooks_disabled(): - harness.update_relation_data( - rel_id, - harness.charm.app.name, - {"restoring-backup": "2023-01-01T09:00:00Z"}, + + # Test trying to set a password through a non leader unit. + harness.charm._on_set_password(mock_event) + mock_event.fail.assert_called_once() + _set_secret.assert_not_called() + + # Test providing an invalid username. + harness.set_leader() + mock_event.reset_mock() + mock_event.params["username"] = "user" + harness.charm._on_set_password(mock_event) + mock_event.fail.assert_called_once() + _set_secret.assert_not_called() + + # Test without providing the username option but without all cluster members ready. + mock_event.reset_mock() + del mock_event.params["username"] + harness.charm._on_set_password(mock_event) + mock_event.fail.assert_called_once() + _set_secret.assert_not_called() + + # Test for an error updating when updating the user password in the database. + mock_event.reset_mock() + harness.charm._on_set_password(mock_event) + mock_event.fail.assert_called_once() + _set_secret.assert_not_called() + + # Test without providing the username option. + harness.charm._on_set_password(mock_event) + assert _set_secret.call_args_list[0][0][1] == "operator-password" + + # Also test providing the username option. + _set_secret.reset_mock() + mock_event.params["username"] = "replication" + harness.charm._on_set_password(mock_event) + assert _set_secret.call_args_list[0][0][1] == "replication-password" + + # And test providing both the username and password options. + _set_secret.reset_mock() + mock_event.params["password"] = "replication-test-password" + harness.charm._on_set_password(mock_event) + _set_secret.assert_called_once_with( + "app", "replication-password", "replication-test-password" ) - _can_use_s3_repository.return_value = (False, "fake validation message") - harness.charm.on.update_status.emit() - _update_config.assert_called_once() - _handle_processes_failures.assert_not_called() - _oversee_users.assert_not_called() - _update_relation_endpoints.assert_not_called() - _handle_workload_failures.assert_not_called() - _set_primary_status_message.assert_not_called() - assert isinstance(harness.charm.unit.status, BlockedStatus) - assert harness.charm.unit.status.message == "fake validation message" - - # Assert that the backup id is not in the application relation databag anymore. - assert harness.get_relation_data(rel_id, harness.charm.app) == {"cluster_initialised": "True"} - -@patch("charm.snap.SnapCache") -def test_install_snap_packages(harness, _snap_cache): - rel_id = harness.add_relation(PEER, harness.charm.app.name) - _snap_package = _snap_cache.return_value.__getitem__.return_value - _snap_package.ensure.side_effect = snap.SnapError - _snap_package.present = False - # Test for problem with snap update. - with pytest.raises(snap.SnapError): - harness.charm._install_snap_packages([("postgresql", {"channel": "14/edge"})]) - _snap_cache.return_value.__getitem__.assert_called_once_with("postgresql") - _snap_cache.assert_called_once_with() - _snap_package.ensure.assert_called_once_with(snap.SnapState.Latest, channel="14/edge") - - # Test with a not found package. - _snap_cache.reset_mock() - _snap_package.reset_mock() - _snap_package.ensure.side_effect = snap.SnapNotFoundError - with pytest.raises(snap.SnapNotFoundError): +@patch_network_get(private_address="1.1.1.1") +def test_on_update_status(harness): + with ( + patch("charm.ClusterTopologyObserver.start_observer") as _start_observer, + patch("charm.PostgresqlOperatorCharm._set_primary_status_message") as _set_primary_status_message, + patch("charm.Patroni.restart_patroni") as _restart_patroni, + patch("charm.Patroni.is_member_isolated") as _is_member_isolated, + patch("charm.Patroni.reinitialize_postgresql") as _reinitialize_postgresql, + patch("charm.Patroni.member_replication_lag", new_callable=PropertyMock) as _member_replication_lag, + patch("charm.Patroni.member_started", new_callable=PropertyMock) as _member_started, + patch("charm.PostgresqlOperatorCharm._update_relation_endpoints") as _update_relation_endpoints, + patch( + "charm.PostgresqlOperatorCharm.primary_endpoint", + new_callable=PropertyMock(return_value=True), + ) as _primary_endpoint, + patch("charm.PostgreSQLProvider.oversee_users") as _oversee_users, + patch("upgrade.PostgreSQLUpgrade.idle", return_value=True), + ): + rel_id = harness.add_relation(PEER, harness.charm.app.name) + # Test before the cluster is initialised. + harness.charm.on.update_status.emit() + _set_primary_status_message.assert_not_called() + + # Test after the cluster was initialised, but with the unit in a blocked state. + with harness.hooks_disabled(): + harness.update_relation_data( + rel_id, harness.charm.app.name, {"cluster_initialised": "True"} + ) + harness.charm.unit.status = BlockedStatus("fake blocked status") + harness.charm.on.update_status.emit() + _set_primary_status_message.assert_not_called() + + # Test with the unit in a status different that blocked. + harness.charm.unit.status = ActiveStatus() + harness.charm.on.update_status.emit() + _set_primary_status_message.assert_called_once() + + # Test the reinitialisation of the replica when its lag is unknown + # after a restart. + _set_primary_status_message.reset_mock() + _member_started.return_value = False + _is_member_isolated.return_value = False + _member_replication_lag.return_value = "unknown" + with harness.hooks_disabled(): + harness.update_relation_data( + rel_id, harness.charm.unit.name, {"postgresql_restarted": "True"} + ) + harness.charm.on.update_status.emit() + _reinitialize_postgresql.assert_called_once() + _restart_patroni.assert_not_called() + _set_primary_status_message.assert_not_called() + + # Test call to restart when the member is isolated from the cluster. + _is_member_isolated.return_value = True + with harness.hooks_disabled(): + harness.update_relation_data( + rel_id, harness.charm.unit.name, {"postgresql_restarted": ""} + ) + harness.charm.on.update_status.emit() + _restart_patroni.assert_called_once() + _start_observer.assert_called_once() + +@patch_network_get(private_address="1.1.1.1") +def test_on_update_status_after_restore_operation(harness): + with ( + patch("charm.ClusterTopologyObserver.start_observer"), + patch("charm.PostgresqlOperatorCharm._set_primary_status_message") as _set_primary_status_message, + patch("charm.PostgresqlOperatorCharm._handle_workload_failures") as _handle_workload_failures, + patch("charm.PostgresqlOperatorCharm._update_relation_endpoints") as _update_relation_endpoints, + patch( + "charm.PostgresqlOperatorCharm.primary_endpoint", + new_callable=PropertyMock(return_value=True), + ) as _primary_endpoint, + patch("charm.PostgreSQLProvider.oversee_users") as _oversee_users, + patch("charm.PostgresqlOperatorCharm._handle_processes_failures") as _handle_processes_failures, + patch("charm.PostgreSQLBackups.can_use_s3_repository") as _can_use_s3_repository, + patch("charm.PostgresqlOperatorCharm.update_config") as _update_config, + patch("charm.Patroni.member_started", new_callable=PropertyMock) as _member_started, + patch("charm.Patroni.get_member_status") as _get_member_status, + patch("upgrade.PostgreSQLUpgrade.idle", return_value=True), + ): + rel_id = harness.add_relation(PEER, harness.charm.app.name) + # Test when the restore operation fails. + with harness.hooks_disabled(): + harness.set_leader() + harness.update_relation_data( + rel_id, + harness.charm.app.name, + {"cluster_initialised": "True", "restoring-backup": "2023-01-01T09:00:00Z"}, + ) + _get_member_status.return_value = "failed" + harness.charm.on.update_status.emit() + _update_config.assert_not_called() + _handle_processes_failures.assert_not_called() + _oversee_users.assert_not_called() + _update_relation_endpoints.assert_not_called() + _handle_workload_failures.assert_not_called() + _set_primary_status_message.assert_not_called() + assert isinstance(harness.charm.unit.status, BlockedStatus) + + # Test when the restore operation hasn't finished yet. + harness.charm.unit.status = ActiveStatus() + _get_member_status.return_value = "running" + _member_started.return_value = False + harness.charm.on.update_status.emit() + _update_config.assert_not_called() + _handle_processes_failures.assert_not_called() + _oversee_users.assert_not_called() + _update_relation_endpoints.assert_not_called() + _handle_workload_failures.assert_not_called() + _set_primary_status_message.assert_not_called() + assert isinstance(harness.charm.unit.status, ActiveStatus) + + # Assert that the backup id is still in the application relation databag. + assert harness.get_relation_data(rel_id, harness.charm.app) == {"cluster_initialised": "True", "restoring-backup": "2023-01-01T09:00:00Z"} + + # Test when the restore operation finished successfully. + _member_started.return_value = True + _can_use_s3_repository.return_value = (True, None) + _handle_processes_failures.return_value = False + _handle_workload_failures.return_value = False + harness.charm.on.update_status.emit() + _update_config.assert_called_once() + _handle_processes_failures.assert_called_once() + _oversee_users.assert_called_once() + _update_relation_endpoints.assert_called_once() + _handle_workload_failures.assert_called_once() + _set_primary_status_message.assert_called_once() + assert isinstance(harness.charm.unit.status, ActiveStatus) + + # Assert that the backup id is not in the application relation databag anymore. + assert harness.get_relation_data(rel_id, harness.charm.app) == {"cluster_initialised": "True"} + + # Test when it's not possible to use the configured S3 repository. + _update_config.reset_mock() + _handle_processes_failures.reset_mock() + _oversee_users.reset_mock() + _update_relation_endpoints.reset_mock() + _handle_workload_failures.reset_mock() + _set_primary_status_message.reset_mock() + with harness.hooks_disabled(): + harness.update_relation_data( + rel_id, + harness.charm.app.name, + {"restoring-backup": "2023-01-01T09:00:00Z"}, + ) + _can_use_s3_repository.return_value = (False, "fake validation message") + harness.charm.on.update_status.emit() + _update_config.assert_called_once() + _handle_processes_failures.assert_not_called() + _oversee_users.assert_not_called() + _update_relation_endpoints.assert_not_called() + _handle_workload_failures.assert_not_called() + _set_primary_status_message.assert_not_called() + assert isinstance(harness.charm.unit.status, BlockedStatus) + assert harness.charm.unit.status.message == "fake validation message" + + # Assert that the backup id is not in the application relation databag anymore. + assert harness.get_relation_data(rel_id, harness.charm.app) == {"cluster_initialised": "True"} + +def test_install_snap_packages(harness): + with patch("charm.snap.SnapCache") as _snap_cache: + rel_id = harness.add_relation(PEER, harness.charm.app.name) + _snap_package = _snap_cache.return_value.__getitem__.return_value + _snap_package.ensure.side_effect = snap.SnapError + _snap_package.present = False + + # Test for problem with snap update. + with pytest.raises(snap.SnapError): + harness.charm._install_snap_packages([("postgresql", {"channel": "14/edge"})]) + _snap_cache.return_value.__getitem__.assert_called_once_with("postgresql") + _snap_cache.assert_called_once_with() + _snap_package.ensure.assert_called_once_with(snap.SnapState.Latest, channel="14/edge") + + # Test with a not found package. + _snap_cache.reset_mock() + _snap_package.reset_mock() + _snap_package.ensure.side_effect = snap.SnapNotFoundError + with pytest.raises(snap.SnapNotFoundError): + harness.charm._install_snap_packages([("postgresql", {"channel": "14/edge"})]) + _snap_cache.return_value.__getitem__.assert_called_once_with("postgresql") + _snap_cache.assert_called_once_with() + _snap_package.ensure.assert_called_once_with(snap.SnapState.Latest, channel="14/edge") + + # Then test a valid one. + _snap_cache.reset_mock() + _snap_package.reset_mock() + _snap_package.ensure.side_effect = None harness.charm._install_snap_packages([("postgresql", {"channel": "14/edge"})]) - _snap_cache.return_value.__getitem__.assert_called_once_with("postgresql") - _snap_cache.assert_called_once_with() - _snap_package.ensure.assert_called_once_with(snap.SnapState.Latest, channel="14/edge") - - # Then test a valid one. - _snap_cache.reset_mock() - _snap_package.reset_mock() - _snap_package.ensure.side_effect = None - harness.charm._install_snap_packages([("postgresql", {"channel": "14/edge"})]) - _snap_cache.assert_called_once_with() - _snap_cache.return_value.__getitem__.assert_called_once_with("postgresql") - _snap_package.ensure.assert_called_once_with(snap.SnapState.Latest, channel="14/edge") - _snap_package.hold.assert_not_called() - - # Test revision - _snap_cache.reset_mock() - _snap_package.reset_mock() - _snap_package.ensure.side_effect = None - harness.charm._install_snap_packages([ - ("postgresql", {"revision": {platform.machine(): "42"}}) - ]) - _snap_cache.assert_called_once_with() - _snap_cache.return_value.__getitem__.assert_called_once_with("postgresql") - _snap_package.ensure.assert_called_once_with( - snap.SnapState.Latest, revision="42", channel="" - ) - _snap_package.hold.assert_called_once_with() - - # Test with refresh - _snap_cache.reset_mock() - _snap_package.reset_mock() - _snap_package.present = True - harness.charm._install_snap_packages( - [("postgresql", {"revision": {platform.machine(): "42"}, "channel": "latest/test"})], - refresh=True, - ) - _snap_cache.assert_called_once_with() - _snap_cache.return_value.__getitem__.assert_called_once_with("postgresql") - _snap_package.ensure.assert_called_once_with( - snap.SnapState.Latest, revision="42", channel="latest/test" - ) - _snap_package.hold.assert_called_once_with() - - # Test without refresh - _snap_cache.reset_mock() - _snap_package.reset_mock() - harness.charm._install_snap_packages([ - ("postgresql", {"revision": {platform.machine(): "42"}}) - ]) - _snap_cache.assert_called_once_with() - _snap_cache.return_value.__getitem__.assert_called_once_with("postgresql") - _snap_package.ensure.assert_not_called() - _snap_package.hold.assert_not_called() - - # test missing architecture - _snap_cache.reset_mock() - _snap_package.reset_mock() - _snap_package.present = True - with pytest.raises(KeyError): + _snap_cache.assert_called_once_with() + _snap_cache.return_value.__getitem__.assert_called_once_with("postgresql") + _snap_package.ensure.assert_called_once_with(snap.SnapState.Latest, channel="14/edge") + _snap_package.hold.assert_not_called() + + # Test revision + _snap_cache.reset_mock() + _snap_package.reset_mock() + _snap_package.ensure.side_effect = None + harness.charm._install_snap_packages([ + ("postgresql", {"revision": {platform.machine(): "42"}}) + ]) + _snap_cache.assert_called_once_with() + _snap_cache.return_value.__getitem__.assert_called_once_with("postgresql") + _snap_package.ensure.assert_called_once_with( + snap.SnapState.Latest, revision="42", channel="" + ) + _snap_package.hold.assert_called_once_with() + + # Test with refresh + _snap_cache.reset_mock() + _snap_package.reset_mock() + _snap_package.present = True harness.charm._install_snap_packages( - [("postgresql", {"revision": {"missingarch": "42"}})], + [("postgresql", {"revision": {platform.machine(): "42"}, "channel": "latest/test"})], refresh=True, ) - _snap_cache.assert_called_once_with() - _snap_cache.return_value.__getitem__.assert_called_once_with("postgresql") - assert not _snap_package.ensure.called - assert not _snap_package.hold.called - -@patch( - "subprocess.check_call", - side_effect=[None, subprocess.CalledProcessError(1, "fake command")], -) -def test_is_storage_attached(harness, _check_call): - rel_id = harness.add_relation(PEER, harness.charm.app.name) - # Test with attached storage. - is_storage_attached = harness.charm._is_storage_attached() - _check_call.assert_called_once_with(["mountpoint", "-q", harness.charm._storage_path]) - assert (is_storage_attached) + _snap_cache.assert_called_once_with() + _snap_cache.return_value.__getitem__.assert_called_once_with("postgresql") + _snap_package.ensure.assert_called_once_with( + snap.SnapState.Latest, revision="42", channel="latest/test" + ) + _snap_package.hold.assert_called_once_with() + + # Test without refresh + _snap_cache.reset_mock() + _snap_package.reset_mock() + harness.charm._install_snap_packages([ + ("postgresql", {"revision": {platform.machine(): "42"}}) + ]) + _snap_cache.assert_called_once_with() + _snap_cache.return_value.__getitem__.assert_called_once_with("postgresql") + _snap_package.ensure.assert_not_called() + _snap_package.hold.assert_not_called() + + # test missing architecture + _snap_cache.reset_mock() + _snap_package.reset_mock() + _snap_package.present = True + with pytest.raises(KeyError): + harness.charm._install_snap_packages( + [("postgresql", {"revision": {"missingarch": "42"}})], + refresh=True, + ) + _snap_cache.assert_called_once_with() + _snap_cache.return_value.__getitem__.assert_called_once_with("postgresql") + assert not _snap_package.ensure.called + assert not _snap_package.hold.called + + +def test_is_storage_attached(harness): + with patch( + "subprocess.check_call", + side_effect=[None, subprocess.CalledProcessError(1, "fake command")], + ) as _check_call: + rel_id = harness.add_relation(PEER, harness.charm.app.name) + # Test with attached storage. + is_storage_attached = harness.charm._is_storage_attached() + _check_call.assert_called_once_with(["mountpoint", "-q", harness.charm._storage_path]) + assert (is_storage_attached) - # Test with detached storage. - is_storage_attached = harness.charm._is_storage_attached() - assert not (is_storage_attached) + # Test with detached storage. + is_storage_attached = harness.charm._is_storage_attached() + assert not (is_storage_attached) -@patch("subprocess.check_call") -def test_reboot_on_detached_storage(harness, _check_call): - rel_id = harness.add_relation(PEER, harness.charm.app.name) - mock_event = MagicMock() - harness.charm._reboot_on_detached_storage(mock_event) - mock_event.defer.assert_called_once() - assert (isinstance(harness.charm.unit.status, WaitingStatus)) - _check_call.assert_called_once_with(["systemctl", "reboot"]) + +def test_reboot_on_detached_storage(harness): + with patch("subprocess.check_call") as _check_call: + rel_id = harness.add_relation(PEER, harness.charm.app.name) + mock_event = MagicMock() + harness.charm._reboot_on_detached_storage(mock_event) + mock_event.defer.assert_called_once() + assert (isinstance(harness.charm.unit.status, WaitingStatus)) + _check_call.assert_called_once_with(["systemctl", "reboot"]) @patch_network_get(private_address="1.1.1.1") -@patch("charm.Patroni.restart_postgresql") -@patch("charm.Patroni.are_all_members_ready") -def test_restart(harness, _are_all_members_ready, _restart_postgresql): - rel_id = harness.add_relation(PEER, harness.charm.app.name) - _are_all_members_ready.side_effect = [False, True, True] - - # Test when not all members are ready. - mock_event = MagicMock() - harness.charm._restart(mock_event) - mock_event.defer.assert_called_once() - _restart_postgresql.assert_not_called() - - # Test a successful restart. - mock_event.defer.reset_mock() - harness.charm._restart(mock_event) - assert not (isinstance(harness.charm.unit.status, BlockedStatus)) - mock_event.defer.assert_not_called() - - # Test a failed restart. - _restart_postgresql.side_effect = RetryError(last_attempt=1) - harness.charm._restart(mock_event) - assert (isinstance(harness.charm.unit.status, BlockedStatus)) - mock_event.defer.assert_not_called() +def test_restart(harness): + with ( + patch("charm.Patroni.restart_postgresql") as _restart_postgresql, + patch("charm.Patroni.are_all_members_ready") as _are_all_members_ready, + ): + rel_id = harness.add_relation(PEER, harness.charm.app.name) + _are_all_members_ready.side_effect = [False, True, True] + + # Test when not all members are ready. + mock_event = MagicMock() + harness.charm._restart(mock_event) + mock_event.defer.assert_called_once() + _restart_postgresql.assert_not_called() + + # Test a successful restart. + mock_event.defer.reset_mock() + harness.charm._restart(mock_event) + assert not (isinstance(harness.charm.unit.status, BlockedStatus)) + mock_event.defer.assert_not_called() + + # Test a failed restart. + _restart_postgresql.side_effect = RetryError(last_attempt=1) + harness.charm._restart(mock_event) + assert (isinstance(harness.charm.unit.status, BlockedStatus)) + mock_event.defer.assert_not_called() @patch_network_get(private_address="1.1.1.1") -@patch("subprocess.check_output", return_value=b"C") -@patch("charm.snap.SnapCache") -@patch("charm.PostgresqlOperatorCharm._handle_postgresql_restart_need") -@patch("charm.Patroni.bulk_update_parameters_controller_by_patroni") -@patch("charm.Patroni.member_started", new_callable=PropertyMock) -@patch("charm.PostgresqlOperatorCharm._is_workload_running", new_callable=PropertyMock) -@patch("charm.Patroni.render_patroni_yml_file") -@patch("charm.PostgresqlOperatorCharm.is_tls_enabled", new_callable=PropertyMock) -def test_update_config( - harness, - _is_tls_enabled, - _render_patroni_yml_file, - _is_workload_running, - _member_started, - _, - _handle_postgresql_restart_need, - __, - ___, -): - rel_id = harness.add_relation(PEER, harness.charm.app.name) - with patch.object(PostgresqlOperatorCharm, "postgresql", Mock()) as postgresql_mock: +def test_update_config(harness): + with ( + patch("subprocess.check_output", return_value=b"C"), + patch("charm.snap.SnapCache"), + patch("charm.PostgresqlOperatorCharm._handle_postgresql_restart_need") as _handle_postgresql_restart_need, + patch("charm.Patroni.bulk_update_parameters_controller_by_patroni"), + patch("charm.Patroni.member_started", new_callable=PropertyMock) as _member_started, + patch("charm.PostgresqlOperatorCharm._is_workload_running", new_callable=PropertyMock) as _is_workload_running, + patch("charm.Patroni.render_patroni_yml_file") as _render_patroni_yml_file, + patch("charm.PostgresqlOperatorCharm.is_tls_enabled", new_callable=PropertyMock) as _is_tls_enabled, + patch.object(PostgresqlOperatorCharm, "postgresql", Mock()) as postgresql_mock, + ): + rel_id = harness.add_relation(PEER, harness.charm.app.name) # Mock some properties. postgresql_mock.is_tls_enabled = PropertyMock(side_effect=[False, False, False, False]) _is_workload_running.side_effect = [False, False, True, True, False, True] @@ -1271,433 +1207,431 @@ def test_update_config( _handle_postgresql_restart_need.assert_not_called() assert "tls" not in harness.get_relation_data(rel_id, harness.charm.unit.name) -@patch("charm.PostgresqlOperatorCharm._update_relation_endpoints") -@patch("charm.PostgresqlOperatorCharm.primary_endpoint", new_callable=PropertyMock) -def test_on_cluster_topology_change(harness, _primary_endpoint, _update_relation_endpoints): - rel_id = harness.add_relation(PEER, harness.charm.app.name) - # Mock the property value. - _primary_endpoint.side_effect = [None, "1.1.1.1"] - - # Test without an elected primary. - harness.charm._on_cluster_topology_change(Mock()) - _update_relation_endpoints.assert_not_called() +def test_on_cluster_topology_change(harness): + with ( + patch("charm.PostgresqlOperatorCharm._update_relation_endpoints") as _update_relation_endpoints, + patch("charm.PostgresqlOperatorCharm.primary_endpoint", new_callable=PropertyMock) as _primary_endpoint, + ): + rel_id = harness.add_relation(PEER, harness.charm.app.name) + # Mock the property value. + _primary_endpoint.side_effect = [None, "1.1.1.1"] - # Test with an elected primary. - harness.charm._on_cluster_topology_change(Mock()) - _update_relation_endpoints.assert_called_once() + # Test without an elected primary. + harness.charm._on_cluster_topology_change(Mock()) + _update_relation_endpoints.assert_not_called() -@patch( - "charm.PostgresqlOperatorCharm.primary_endpoint", - new_callable=PropertyMock, - return_value=None, -) -@patch("charm.PostgresqlOperatorCharm._update_relation_endpoints") -def test_on_cluster_topology_change_keep_blocked( - harness, _update_relation_endpoints, _primary_endpoint -): - rel_id = harness.add_relation(PEER, harness.charm.app.name) - harness.model.unit.status = WaitingStatus(PRIMARY_NOT_REACHABLE_MESSAGE) + # Test with an elected primary. + harness.charm._on_cluster_topology_change(Mock()) + _update_relation_endpoints.assert_called_once() - harness.charm._on_cluster_topology_change(Mock()) +def test_on_cluster_topology_change_keep_blocked(harness): + with ( + patch( + "charm.PostgresqlOperatorCharm.primary_endpoint", + new_callable=PropertyMock, + return_value=None, + ) as _primary_endpoint, + patch("charm.PostgresqlOperatorCharm._update_relation_endpoints") as _update_relation_endpoints, + ): + rel_id = harness.add_relation(PEER, harness.charm.app.name) + harness.model.unit.status = WaitingStatus(PRIMARY_NOT_REACHABLE_MESSAGE) - _update_relation_endpoints.assert_not_called() - _primary_endpoint.assert_called_once_with() - assert isinstance(harness.model.unit.status, WaitingStatus) - assert harness.model.unit.status.message == PRIMARY_NOT_REACHABLE_MESSAGE + harness.charm._on_cluster_topology_change(Mock()) -@patch( - "charm.PostgresqlOperatorCharm.primary_endpoint", - new_callable=PropertyMock, - return_value="fake-unit", -) -@patch("charm.PostgresqlOperatorCharm._update_relation_endpoints") -def test_on_cluster_topology_change_clear_blocked( - harness, _update_relation_endpoints, _primary_endpoint -): - rel_id = harness.add_relation(PEER, harness.charm.app.name) - harness.model.unit.status = WaitingStatus(PRIMARY_NOT_REACHABLE_MESSAGE) + _update_relation_endpoints.assert_not_called() + _primary_endpoint.assert_called_once_with() + assert isinstance(harness.model.unit.status, WaitingStatus) + assert harness.model.unit.status.message == PRIMARY_NOT_REACHABLE_MESSAGE + +def test_on_cluster_topology_change_clear_blocked(harness): + with ( + patch( + "charm.PostgresqlOperatorCharm.primary_endpoint", + new_callable=PropertyMock, + return_value="fake-unit", + ) as _primary_endpoint, + patch("charm.PostgresqlOperatorCharm._update_relation_endpoints") as _update_relation_endpoints, + ): + rel_id = harness.add_relation(PEER, harness.charm.app.name) + harness.model.unit.status = WaitingStatus(PRIMARY_NOT_REACHABLE_MESSAGE) - harness.charm._on_cluster_topology_change(Mock()) + harness.charm._on_cluster_topology_change(Mock()) - _update_relation_endpoints.assert_called_once_with() - _primary_endpoint.assert_called_once_with() - assert (isinstance(harness.model.unit.status, ActiveStatus)) + _update_relation_endpoints.assert_called_once_with() + _primary_endpoint.assert_called_once_with() + assert (isinstance(harness.model.unit.status, ActiveStatus)) -@patch("charm.PostgresqlOperatorCharm.postgresql", new_callable=PropertyMock) -@patch("config.subprocess") -def test_validate_config_options(harness, _, _charm_lib): - rel_id = harness.add_relation(PEER, harness.charm.app.name) - _charm_lib.return_value.get_postgresql_text_search_configs.return_value = [] - _charm_lib.return_value.validate_date_style.return_value = [] - _charm_lib.return_value.get_postgresql_timezones.return_value = [] +def test_validate_config_options(harness): + with ( + patch("charm.PostgresqlOperatorCharm.postgresql", new_callable=PropertyMock) as _charm_lib, + patch("config.subprocess"), + ): + rel_id = harness.add_relation(PEER, harness.charm.app.name) + _charm_lib.return_value.get_postgresql_text_search_configs.return_value = [] + _charm_lib.return_value.validate_date_style.return_value = [] + _charm_lib.return_value.get_postgresql_timezones.return_value = [] - # Test instance_default_text_search_config exception - with harness.hooks_disabled(): - harness.update_config({"instance_default_text_search_config": "pg_catalog.test"}) + # Test instance_default_text_search_config exception + with harness.hooks_disabled(): + harness.update_config({"instance_default_text_search_config": "pg_catalog.test"}) - with pytest.raises(ValueError) as e: - harness.charm._validate_config_options() - assert ( - e.msg == "instance_default_text_search_config config option has an invalid value" - ) + with pytest.raises(ValueError) as e: + harness.charm._validate_config_options() + assert ( + e.msg == "instance_default_text_search_config config option has an invalid value" + ) - _charm_lib.return_value.get_postgresql_text_search_configs.assert_called_once_with() - _charm_lib.return_value.get_postgresql_text_search_configs.return_value = [ - "pg_catalog.test" - ] + _charm_lib.return_value.get_postgresql_text_search_configs.assert_called_once_with() + _charm_lib.return_value.get_postgresql_text_search_configs.return_value = [ + "pg_catalog.test" + ] - # Test request_date_style exception - with harness.hooks_disabled(): - harness.update_config({"request_date_style": "ISO, TEST"}) + # Test request_date_style exception + with harness.hooks_disabled(): + harness.update_config({"request_date_style": "ISO, TEST"}) - with pytest.raises(ValueError) as e: - harness.charm._validate_config_options() - assert e.msg == "request_date_style config option has an invalid value" + with pytest.raises(ValueError) as e: + harness.charm._validate_config_options() + assert e.msg == "request_date_style config option has an invalid value" - _charm_lib.return_value.validate_date_style.assert_called_once_with("ISO, TEST") - _charm_lib.return_value.validate_date_style.return_value = ["ISO, TEST"] + _charm_lib.return_value.validate_date_style.assert_called_once_with("ISO, TEST") + _charm_lib.return_value.validate_date_style.return_value = ["ISO, TEST"] - # Test request_time_zone exception - with harness.hooks_disabled(): - harness.update_config({"request_time_zone": "TEST_ZONE"}) + # Test request_time_zone exception + with harness.hooks_disabled(): + harness.update_config({"request_time_zone": "TEST_ZONE"}) - with pytest.raises(ValueError) as e: - harness.charm._validate_config_options() - assert e.msg == "request_time_zone config option has an invalid value" + with pytest.raises(ValueError) as e: + harness.charm._validate_config_options() + assert e.msg == "request_time_zone config option has an invalid value" - _charm_lib.return_value.get_postgresql_timezones.assert_called_once_with() - _charm_lib.return_value.get_postgresql_timezones.return_value = ["TEST_ZONE"] + _charm_lib.return_value.get_postgresql_timezones.assert_called_once_with() + _charm_lib.return_value.get_postgresql_timezones.return_value = ["TEST_ZONE"] @patch_network_get(private_address="1.1.1.1") -@patch("charm.snap.SnapCache") -@patch("charm.PostgresqlOperatorCharm._update_relation_endpoints") -@patch("charm.PostgresqlOperatorCharm.primary_endpoint", new_callable=PropertyMock) -@patch("backups.PostgreSQLBackups.check_stanza") -@patch("backups.PostgreSQLBackups.coordinate_stanza_fields") -@patch("backups.PostgreSQLBackups.start_stop_pgbackrest_service") -@patch("charm.Patroni.reinitialize_postgresql") -@patch("charm.Patroni.member_replication_lag", new_callable=PropertyMock) -@patch("charm.PostgresqlOperatorCharm.is_primary") -@patch("charm.Patroni.member_started", new_callable=PropertyMock) -@patch("charm.Patroni.start_patroni") -@patch("charm.PostgresqlOperatorCharm.update_config") -@patch("charm.PostgresqlOperatorCharm._update_member_ip") -@patch("charm.PostgresqlOperatorCharm._reconfigure_cluster") -@patch("ops.framework.EventBase.defer") -def test_on_peer_relation_changed( - harness, - _defer, - _reconfigure_cluster, - _update_member_ip, - _update_config, - _start_patroni, - _member_started, - _is_primary, - _member_replication_lag, - _reinitialize_postgresql, - _start_stop_pgbackrest_service, - _coordinate_stanza_fields, - _check_stanza, - _primary_endpoint, - _update_relation_endpoints, - _, -): - rel_id = harness.add_relation(PEER, harness.charm.app.name) - # Test an uninitialized cluster. - mock_event = Mock() - with harness.hooks_disabled(): - harness.update_relation_data( - rel_id, harness.charm.app.name, {"cluster_initialised": ""} - ) - harness.charm._on_peer_relation_changed(mock_event) - mock_event.defer.assert_called_once() - _reconfigure_cluster.assert_not_called() +def test_on_peer_relation_changed(harness): + with ( + patch("charm.snap.SnapCache"), + patch("charm.PostgresqlOperatorCharm._update_relation_endpoints") as _update_relation_endpoints, + patch("charm.PostgresqlOperatorCharm.primary_endpoint", new_callable=PropertyMock) as _primary_endpoint, + patch("backups.PostgreSQLBackups.check_stanza") as _check_stanza, + patch("backups.PostgreSQLBackups.coordinate_stanza_fields") as _coordinate_stanza_fields, + patch("backups.PostgreSQLBackups.start_stop_pgbackrest_service") as _start_stop_pgbackrest_service, + patch("charm.Patroni.reinitialize_postgresql") as _reinitialize_postgresql, + patch("charm.Patroni.member_replication_lag", new_callable=PropertyMock) as _member_replication_lag, + patch("charm.PostgresqlOperatorCharm.is_primary") as _is_primary, + patch("charm.Patroni.member_started", new_callable=PropertyMock) as _member_started, + patch("charm.Patroni.start_patroni") as _start_patroni, + patch("charm.PostgresqlOperatorCharm.update_config") as _update_config, + patch("charm.PostgresqlOperatorCharm._update_member_ip") as _update_member_ip, + patch("charm.PostgresqlOperatorCharm._reconfigure_cluster") as _reconfigure_cluster, + patch("ops.framework.EventBase.defer") as _defer, + ): + rel_id = harness.add_relation(PEER, harness.charm.app.name) + # Test an uninitialized cluster. + mock_event = Mock() + with harness.hooks_disabled(): + harness.update_relation_data( + rel_id, harness.charm.app.name, {"cluster_initialised": ""} + ) + harness.charm._on_peer_relation_changed(mock_event) + mock_event.defer.assert_called_once() + _reconfigure_cluster.assert_not_called() + + # Test an initialized cluster and this is the leader unit + # (but it fails to reconfigure the cluster). + mock_event.defer.reset_mock() + with harness.hooks_disabled(): + harness.update_relation_data( + rel_id, + harness.charm.app.name, + {"cluster_initialised": "True", "members_ips": '["1.1.1.1"]'}, + ) + harness.set_leader() + _reconfigure_cluster.return_value = False + harness.charm._on_peer_relation_changed(mock_event) + _reconfigure_cluster.assert_called_once_with(mock_event) + mock_event.defer.assert_called_once() + + # Test when the leader can reconfigure the cluster. + mock_event.defer.reset_mock() + _reconfigure_cluster.reset_mock() + _reconfigure_cluster.return_value = True + _update_member_ip.return_value = False + _member_started.return_value = True + _primary_endpoint.return_value = "1.1.1.1" + harness.model.unit.status = WaitingStatus("awaiting for cluster to start") + harness.charm._on_peer_relation_changed(mock_event) + mock_event.defer.assert_not_called() + _reconfigure_cluster.assert_called_once_with(mock_event) + _update_member_ip.assert_called_once() + _update_config.assert_called_once() + _start_patroni.assert_called_once() + _update_relation_endpoints.assert_called_once() + assert isinstance(harness.model.unit.status, ActiveStatus) - # Test an initialized cluster and this is the leader unit - # (but it fails to reconfigure the cluster). - mock_event.defer.reset_mock() - with harness.hooks_disabled(): - harness.update_relation_data( - rel_id, - harness.charm.app.name, - {"cluster_initialised": "True", "members_ips": '["1.1.1.1"]'}, - ) - harness.set_leader() - _reconfigure_cluster.return_value = False - harness.charm._on_peer_relation_changed(mock_event) - _reconfigure_cluster.assert_called_once_with(mock_event) - mock_event.defer.assert_called_once() - - # Test when the leader can reconfigure the cluster. - mock_event.defer.reset_mock() - _reconfigure_cluster.reset_mock() - _reconfigure_cluster.return_value = True - _update_member_ip.return_value = False - _member_started.return_value = True - _primary_endpoint.return_value = "1.1.1.1" - harness.model.unit.status = WaitingStatus("awaiting for cluster to start") - harness.charm._on_peer_relation_changed(mock_event) - mock_event.defer.assert_not_called() - _reconfigure_cluster.assert_called_once_with(mock_event) - _update_member_ip.assert_called_once() - _update_config.assert_called_once() - _start_patroni.assert_called_once() - _update_relation_endpoints.assert_called_once() - assert isinstance(harness.model.unit.status, ActiveStatus) - - # Test when the cluster member updates its IP. - _update_member_ip.reset_mock() - _update_config.reset_mock() - _start_patroni.reset_mock() - _update_relation_endpoints.reset_mock() - _update_member_ip.return_value = True - harness.charm._on_peer_relation_changed(mock_event) - _update_member_ip.assert_called_once() - _update_config.assert_not_called() - _start_patroni.assert_not_called() - _update_relation_endpoints.assert_not_called() - - # Test when the unit fails to update the Patroni configuration. - _update_member_ip.return_value = False - _update_config.side_effect = RetryError(last_attempt=1) - harness.charm._on_peer_relation_changed(mock_event) - _update_config.assert_called_once() - _start_patroni.assert_not_called() - _update_relation_endpoints.assert_not_called() - assert isinstance(harness.model.unit.status, BlockedStatus) - - # Test when Patroni hasn't started yet in the unit. - _update_config.side_effect = None - _member_started.return_value = False - harness.charm._on_peer_relation_changed(mock_event) - _start_patroni.assert_called_once() - _update_relation_endpoints.assert_not_called() - assert isinstance(harness.model.unit.status, WaitingStatus) - - # Test when Patroni has already started but this is a replica with a - # huge or unknown lag. - relation = harness.model.get_relation(PEER, rel_id) - _member_started.return_value = True - for values in itertools.product([True, False], ["0", "1000", "1001", "unknown"]): + # Test when the cluster member updates its IP. + _update_member_ip.reset_mock() + _update_config.reset_mock() + _start_patroni.reset_mock() + _update_relation_endpoints.reset_mock() + _update_member_ip.return_value = True + harness.charm._on_peer_relation_changed(mock_event) + _update_member_ip.assert_called_once() + _update_config.assert_not_called() + _start_patroni.assert_not_called() + _update_relation_endpoints.assert_not_called() + + # Test when the unit fails to update the Patroni configuration. + _update_member_ip.return_value = False + _update_config.side_effect = RetryError(last_attempt=1) + harness.charm._on_peer_relation_changed(mock_event) + _update_config.assert_called_once() + _start_patroni.assert_not_called() + _update_relation_endpoints.assert_not_called() + assert isinstance(harness.model.unit.status, BlockedStatus) + + # Test when Patroni hasn't started yet in the unit. + _update_config.side_effect = None + _member_started.return_value = False + harness.charm._on_peer_relation_changed(mock_event) + _start_patroni.assert_called_once() + _update_relation_endpoints.assert_not_called() + assert isinstance(harness.model.unit.status, WaitingStatus) + + # Test when Patroni has already started but this is a replica with a + # huge or unknown lag. + relation = harness.model.get_relation(PEER, rel_id) + _member_started.return_value = True + for values in itertools.product([True, False], ["0", "1000", "1001", "unknown"]): + _defer.reset_mock() + _check_stanza.reset_mock() + _start_stop_pgbackrest_service.reset_mock() + _is_primary.return_value = values[0] + _member_replication_lag.return_value = values[1] + harness.charm.unit.status = ActiveStatus() + harness.charm.on.database_peers_relation_changed.emit(relation) + if _is_primary.return_value == values[0] or int(values[1]) <= 1000: + _defer.assert_not_called() + _check_stanza.assert_called_once() + _start_stop_pgbackrest_service.assert_called_once() + assert isinstance(harness.charm.unit.status, ActiveStatus) + else: + _defer.assert_called_once() + _check_stanza.assert_not_called() + _start_stop_pgbackrest_service.assert_not_called() + assert isinstance(harness.charm.unit.status, MaintenanceStatus) + + # Test when it was not possible to start the pgBackRest service yet. + relation = harness.model.get_relation(PEER, rel_id) + _member_started.return_value = True _defer.reset_mock() + _coordinate_stanza_fields.reset_mock() _check_stanza.reset_mock() - _start_stop_pgbackrest_service.reset_mock() - _is_primary.return_value = values[0] - _member_replication_lag.return_value = values[1] - harness.charm.unit.status = ActiveStatus() + _start_stop_pgbackrest_service.return_value = False harness.charm.on.database_peers_relation_changed.emit(relation) - if _is_primary.return_value == values[0] or int(values[1]) <= 1000: - _defer.assert_not_called() - _check_stanza.assert_called_once() - _start_stop_pgbackrest_service.assert_called_once() - assert isinstance(harness.charm.unit.status, ActiveStatus) - else: - _defer.assert_called_once() - _check_stanza.assert_not_called() - _start_stop_pgbackrest_service.assert_not_called() - assert isinstance(harness.charm.unit.status, MaintenanceStatus) - - # Test when it was not possible to start the pgBackRest service yet. - relation = harness.model.get_relation(PEER, rel_id) - _member_started.return_value = True - _defer.reset_mock() - _coordinate_stanza_fields.reset_mock() - _check_stanza.reset_mock() - _start_stop_pgbackrest_service.return_value = False - harness.charm.on.database_peers_relation_changed.emit(relation) - _defer.assert_called_once() - _coordinate_stanza_fields.assert_not_called() - _check_stanza.assert_not_called() - - # Test the last calls been made when it was possible to start the - # pgBackRest service. - _defer.reset_mock() - _start_stop_pgbackrest_service.return_value = True - harness.charm.on.database_peers_relation_changed.emit(relation) - _defer.assert_not_called() - _coordinate_stanza_fields.assert_called_once() - _check_stanza.assert_called_once() + _defer.assert_called_once() + _coordinate_stanza_fields.assert_not_called() + _check_stanza.assert_not_called() -@patch_network_get(private_address="1.1.1.1") -@patch("charm.PostgresqlOperatorCharm._add_members") -@patch("charm.PostgresqlOperatorCharm._remove_from_members_ips") -@patch("charm.Patroni.remove_raft_member") -def test_reconfigure_cluster( - harness, _remove_raft_member, _remove_from_members_ips, _add_members -): - rel_id = harness.add_relation(PEER, harness.charm.app.name) - # Test when no change is needed in the member IP. - mock_event = Mock() - mock_event.unit = harness.charm.unit - mock_event.relation.data = {mock_event.unit: {}} - assert (harness.charm._reconfigure_cluster(mock_event)) - _remove_raft_member.assert_not_called() - _remove_from_members_ips.assert_not_called() - _add_members.assert_called_once_with(mock_event) - - # Test when a change is needed in the member IP, but it fails. - _remove_raft_member.side_effect = RemoveRaftMemberFailedError - _add_members.reset_mock() - ip_to_remove = "1.1.1.1" - relation_data = {mock_event.unit: {"ip-to-remove": ip_to_remove}} - mock_event.relation.data = relation_data - assert not (harness.charm._reconfigure_cluster(mock_event)) - _remove_raft_member.assert_called_once_with(ip_to_remove) - _remove_from_members_ips.assert_not_called() - _add_members.assert_not_called() - - # Test when a change is needed in the member IP, and it succeeds - # (but the old IP was already been removed). - _remove_raft_member.reset_mock() - _remove_raft_member.side_effect = None - _add_members.reset_mock() - mock_event.relation.data = relation_data - assert (harness.charm._reconfigure_cluster(mock_event)) - _remove_raft_member.assert_called_once_with(ip_to_remove) - _remove_from_members_ips.assert_not_called() - _add_members.assert_called_once_with(mock_event) - - # Test when the old IP wasn't removed yet. - _remove_raft_member.reset_mock() - _add_members.reset_mock() - mock_event.relation.data = relation_data - with harness.hooks_disabled(): - harness.update_relation_data( - rel_id, harness.charm.app.name, {"members_ips": '["' + ip_to_remove + '"]'} - ) - assert (harness.charm._reconfigure_cluster(mock_event)) - _remove_raft_member.assert_called_once_with(ip_to_remove) - _remove_from_members_ips.assert_called_once_with(ip_to_remove) - _add_members.assert_called_once_with(mock_event) - -@patch("charms.postgresql_k8s.v0.postgresql_tls.PostgreSQLTLS._request_certificate") -@patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=False) -def test_update_certificate(harness, _, _request_certificate): - rel_id = harness.add_relation(PEER, harness.charm.app.name) - # If there is no current TLS files, _request_certificate should be called - # only when the certificates relation is established. - harness.charm._update_certificate() - _request_certificate.assert_not_called() - - # Test with already present TLS files (when they will be replaced by new ones). - ca = "fake CA" - cert = "fake certificate" - key = private_key = "fake private key" - with harness.hooks_disabled(): - harness.update_relation_data( - rel_id, - harness.charm.unit.name, - { - "ca": ca, - "cert": cert, - "key": key, - "private-key": private_key, - }, - ) - harness.charm._update_certificate() - _request_certificate.assert_called_once_with(private_key) - - harness.charm.get_secret("unit", "ca") == ca - harness.charm.get_secret("unit", "cert") == cert - harness.charm.get_secret("unit", "key") == key - harness.charm.get_secret("unit", "private-key") == private_key - -@patch("charms.postgresql_k8s.v0.postgresql_tls.PostgreSQLTLS._request_certificate") -@patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=True) -def test_update_certificate_secrets(harness, _, _request_certificate): - rel_id = harness.add_relation(PEER, harness.charm.app.name) - # If there is no current TLS files, _request_certificate should be called - # only when the certificates relation is established. - harness.charm._update_certificate() - _request_certificate.assert_not_called() - - # Test with already present TLS files (when they will be replaced by new ones). - ca = "fake CA" - cert = "fake certificate" - key = private_key = "fake private key" - harness.charm.set_secret("unit", "ca", ca) - harness.charm.set_secret("unit", "cert", cert) - harness.charm.set_secret("unit", "key", key) - harness.charm.set_secret("unit", "private-key", private_key) - - harness.charm._update_certificate() - _request_certificate.assert_called_once_with(private_key) - - harness.charm.get_secret("unit", "ca") == ca - harness.charm.get_secret("unit", "cert") == cert - harness.charm.get_secret("unit", "key") == key - harness.charm.get_secret("unit", "private-key") == private_key + # Test the last calls been made when it was possible to start the + # pgBackRest service. + _defer.reset_mock() + _start_stop_pgbackrest_service.return_value = True + harness.charm.on.database_peers_relation_changed.emit(relation) + _defer.assert_not_called() + _coordinate_stanza_fields.assert_called_once() + _check_stanza.assert_called_once() @patch_network_get(private_address="1.1.1.1") -@patch("charm.PostgresqlOperatorCharm._update_certificate") -@patch("charm.Patroni.stop_patroni") -def test_update_member_ip(harness, _stop_patroni, _update_certificate): - rel_id = harness.add_relation(PEER, harness.charm.app.name) - # Test when the IP address of the unit hasn't changed. - with harness.hooks_disabled(): - harness.update_relation_data( - rel_id, - harness.charm.unit.name, - { - "ip": "1.1.1.1", - }, - ) - assert not (harness.charm._update_member_ip()) - relation_data = harness.get_relation_data(rel_id, harness.charm.unit.name) - assert relation_data.get("ip-to-remove") == None - _stop_patroni.assert_not_called() - _update_certificate.assert_not_called() +def test_reconfigure_cluster(harness): + with ( + patch("charm.PostgresqlOperatorCharm._add_members") as _add_members, + patch("charm.PostgresqlOperatorCharm._remove_from_members_ips") as _remove_from_members_ips, + patch("charm.Patroni.remove_raft_member") as _remove_raft_member, + ): + rel_id = harness.add_relation(PEER, harness.charm.app.name) + # Test when no change is needed in the member IP. + mock_event = Mock() + mock_event.unit = harness.charm.unit + mock_event.relation.data = {mock_event.unit: {}} + assert (harness.charm._reconfigure_cluster(mock_event)) + _remove_raft_member.assert_not_called() + _remove_from_members_ips.assert_not_called() + _add_members.assert_called_once_with(mock_event) + + # Test when a change is needed in the member IP, but it fails. + _remove_raft_member.side_effect = RemoveRaftMemberFailedError + _add_members.reset_mock() + ip_to_remove = "1.1.1.1" + relation_data = {mock_event.unit: {"ip-to-remove": ip_to_remove}} + mock_event.relation.data = relation_data + assert not (harness.charm._reconfigure_cluster(mock_event)) + _remove_raft_member.assert_called_once_with(ip_to_remove) + _remove_from_members_ips.assert_not_called() + _add_members.assert_not_called() + + # Test when a change is needed in the member IP, and it succeeds + # (but the old IP was already been removed). + _remove_raft_member.reset_mock() + _remove_raft_member.side_effect = None + _add_members.reset_mock() + mock_event.relation.data = relation_data + assert (harness.charm._reconfigure_cluster(mock_event)) + _remove_raft_member.assert_called_once_with(ip_to_remove) + _remove_from_members_ips.assert_not_called() + _add_members.assert_called_once_with(mock_event) + + # Test when the old IP wasn't removed yet. + _remove_raft_member.reset_mock() + _add_members.reset_mock() + mock_event.relation.data = relation_data + with harness.hooks_disabled(): + harness.update_relation_data( + rel_id, harness.charm.app.name, {"members_ips": '["' + ip_to_remove + '"]'} + ) + assert (harness.charm._reconfigure_cluster(mock_event)) + _remove_raft_member.assert_called_once_with(ip_to_remove) + _remove_from_members_ips.assert_called_once_with(ip_to_remove) + _add_members.assert_called_once_with(mock_event) + +def test_update_certificate(harness): + with ( + patch("charms.postgresql_k8s.v0.postgresql_tls.PostgreSQLTLS._request_certificate") as _request_certificate, + patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=False), + ): + rel_id = harness.add_relation(PEER, harness.charm.app.name) + # If there is no current TLS files, _request_certificate should be called + # only when the certificates relation is established. + harness.charm._update_certificate() + _request_certificate.assert_not_called() + + # Test with already present TLS files (when they will be replaced by new ones). + ca = "fake CA" + cert = "fake certificate" + key = private_key = "fake private key" + with harness.hooks_disabled(): + harness.update_relation_data( + rel_id, + harness.charm.unit.name, + { + "ca": ca, + "cert": cert, + "key": key, + "private-key": private_key, + }, + ) + harness.charm._update_certificate() + _request_certificate.assert_called_once_with(private_key) + + assert harness.charm.get_secret("unit", "ca") == ca + assert harness.charm.get_secret("unit", "cert") == cert + assert harness.charm.get_secret("unit", "key") == key + assert harness.charm.get_secret("unit", "private-key") == private_key + +def test_update_certificate_secrets(harness): + with ( + patch("charms.postgresql_k8s.v0.postgresql_tls.PostgreSQLTLS._request_certificate") as _request_certificate, + patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=True), + ): + rel_id = harness.add_relation(PEER, harness.charm.app.name) + # If there is no current TLS files, _request_certificate should be called + # only when the certificates relation is established. + harness.charm._update_certificate() + _request_certificate.assert_not_called() + + # Test with already present TLS files (when they will be replaced by new ones). + ca = "fake CA" + cert = "fake certificate" + key = private_key = "fake private key" + harness.charm.set_secret("unit", "ca", ca) + harness.charm.set_secret("unit", "cert", cert) + harness.charm.set_secret("unit", "key", key) + harness.charm.set_secret("unit", "private-key", private_key) + + harness.charm._update_certificate() + _request_certificate.assert_called_once_with(private_key) + + assert harness.charm.get_secret("unit", "ca") == ca + assert harness.charm.get_secret("unit", "cert") == cert + assert harness.charm.get_secret("unit", "key") == key + assert harness.charm.get_secret("unit", "private-key") == private_key - # Test when the IP address of the unit has changed. - with harness.hooks_disabled(): - harness.update_relation_data( - rel_id, - harness.charm.unit.name, - { - "ip": "2.2.2.2", - }, - ) - assert (harness.charm._update_member_ip()) - relation_data = harness.get_relation_data(rel_id, harness.charm.unit.name) - assert relation_data.get("ip") == "1.1.1.1" - assert relation_data.get("ip-to-remove") == "2.2.2.2" - _stop_patroni.assert_called_once() - _update_certificate.assert_called_once() +@patch_network_get(private_address="1.1.1.1") +def test_update_member_ip(harness): + with ( + patch("charm.PostgresqlOperatorCharm._update_certificate") as _update_certificate, + patch("charm.Patroni.stop_patroni") as _stop_patroni, + ): + rel_id = harness.add_relation(PEER, harness.charm.app.name) + # Test when the IP address of the unit hasn't changed. + with harness.hooks_disabled(): + harness.update_relation_data( + rel_id, + harness.charm.unit.name, + { + "ip": "1.1.1.1", + }, + ) + assert not (harness.charm._update_member_ip()) + relation_data = harness.get_relation_data(rel_id, harness.charm.unit.name) + assert relation_data.get("ip-to-remove") == None + _stop_patroni.assert_not_called() + _update_certificate.assert_not_called() + + # Test when the IP address of the unit has changed. + with harness.hooks_disabled(): + harness.update_relation_data( + rel_id, + harness.charm.unit.name, + { + "ip": "2.2.2.2", + }, + ) + assert (harness.charm._update_member_ip()) + relation_data = harness.get_relation_data(rel_id, harness.charm.unit.name) + assert relation_data.get("ip") == "1.1.1.1" + assert relation_data.get("ip-to-remove") == "2.2.2.2" + _stop_patroni.assert_called_once() + _update_certificate.assert_called_once() @patch_network_get(private_address="1.1.1.1") -@patch("charm.PostgresqlOperatorCharm.update_config") -@patch("charm.Patroni.render_file") -@patch("charms.postgresql_k8s.v0.postgresql_tls.PostgreSQLTLS.get_tls_files") -def test_push_tls_files_to_workload(harness, _get_tls_files, _render_file, _update_config): - rel_id = harness.add_relation(PEER, harness.charm.app.name) - _get_tls_files.side_effect = [ - ("key", "ca", "cert"), - ("key", "ca", None), - ("key", None, "cert"), - (None, "ca", "cert"), - ] - _update_config.side_effect = [True, False, False, False] +def test_push_tls_files_to_workload(harness): + with ( + patch("charm.PostgresqlOperatorCharm.update_config") as _update_config, + patch("charm.Patroni.render_file") as _render_file, + patch("charms.postgresql_k8s.v0.postgresql_tls.PostgreSQLTLS.get_tls_files") as _get_tls_files, + ): + rel_id = harness.add_relation(PEER, harness.charm.app.name) + _get_tls_files.side_effect = [ + ("key", "ca", "cert"), + ("key", "ca", None), + ("key", None, "cert"), + (None, "ca", "cert"), + ] + _update_config.side_effect = [True, False, False, False] - # Test when all TLS files are available. - assert (harness.charm.push_tls_files_to_workload()) - assert _render_file.call_count == 3 + # Test when all TLS files are available. + assert (harness.charm.push_tls_files_to_workload()) + assert _render_file.call_count == 3 - # Test when not all TLS files are available. - for _ in range(3): - _render_file.reset_mock() - assert not (harness.charm.push_tls_files_to_workload()) - assert _render_file.call_count == 2 + # Test when not all TLS files are available. + for _ in range(3): + _render_file.reset_mock() + assert not (harness.charm.push_tls_files_to_workload()) + assert _render_file.call_count == 2 -@patch("charm.snap.SnapCache") -def test_is_workload_running(harness, _snap_cache): - rel_id = harness.add_relation(PEER, harness.charm.app.name) - pg_snap = _snap_cache.return_value[POSTGRESQL_SNAP_NAME] - pg_snap.present = False - assert not (harness.charm._is_workload_running) +def test_is_workload_running(harness): + with patch("charm.snap.SnapCache") as _snap_cache: + rel_id = harness.add_relation(PEER, harness.charm.app.name) + pg_snap = _snap_cache.return_value[POSTGRESQL_SNAP_NAME] + + pg_snap.present = False + assert not (harness.charm._is_workload_running) - pg_snap.present = True - assert (harness.charm._is_workload_running) + pg_snap.present = True + assert (harness.charm._is_workload_running) def test_get_available_memory(harness): rel_id = harness.add_relation(PEER, harness.charm.app.name) @@ -1717,22 +1651,24 @@ def test_get_available_memory(harness): with patch("builtins.open", mock_open(read_data="")): assert harness.charm.get_available_memory() == 0 -@patch("charm.ClusterTopologyObserver") -@patch("charm.JujuVersion") -def test_juju_run_exec_divergence(harness, _juju_version: Mock, _topology_observer: Mock): - rel_id = harness.add_relation(PEER, harness.charm.app.name) - # Juju 2 - _juju_version.from_environ.return_value.major = 2 - harness = Harness(PostgresqlOperatorCharm) - harness.begin() - _topology_observer.assert_called_once_with(harness.charm, "/usr/bin/juju-run") - _topology_observer.reset_mock() - - # Juju 3 - _juju_version.from_environ.return_value.major = 3 - harness = Harness(PostgresqlOperatorCharm) - harness.begin() - _topology_observer.assert_called_once_with(harness.charm, "/usr/bin/juju-exec") +def test_juju_run_exec_divergence(harness): + with ( + patch("charm.ClusterTopologyObserver") as _topology_observer, + patch("charm.JujuVersion") as _juju_version, + ): + rel_id = harness.add_relation(PEER, harness.charm.app.name) + # Juju 2 + _juju_version.from_environ.return_value.major = 2 + harness = Harness(PostgresqlOperatorCharm) + harness.begin() + _topology_observer.assert_called_once_with(harness.charm, "/usr/bin/juju-run") + _topology_observer.reset_mock() + + # Juju 3 + _juju_version.from_environ.return_value.major = 3 + harness = Harness(PostgresqlOperatorCharm) + harness.begin() + _topology_observer.assert_called_once_with(harness.charm, "/usr/bin/juju-exec") def test_client_relations(harness): rel_id = harness.add_relation(PEER, harness.charm.app.name) @@ -1759,281 +1695,295 @@ def test_scope_obj(harness): assert harness.charm._scope_obj("test") is None @patch_network_get(private_address="1.1.1.1") -@patch("charm.PostgresqlOperatorCharm._on_leader_elected") -def test_get_secret(harness, _): - rel_id = harness.add_relation(PEER, harness.charm.app.name) - # App level changes require leader privileges - harness.set_leader() - # Test application scope. - assert harness.charm.get_secret("app", "password") is None - harness.update_relation_data( - rel_id, harness.charm.app.name, {"password": "test-password"} - ) - assert harness.charm.get_secret("app", "password") == "test-password" - - # Unit level changes don't require leader privileges - harness.set_leader(False) - # Test unit scope. - assert harness.charm.get_secret("unit", "password") is None - harness.update_relation_data( - rel_id, harness.charm.unit.name, {"password": "test-password"} - ) - assert harness.charm.get_secret("unit", "password") == "test-password" +def test_get_secret(harness): + with patch("charm.PostgresqlOperatorCharm._on_leader_elected"): + rel_id = harness.add_relation(PEER, harness.charm.app.name) + # App level changes require leader privileges + harness.set_leader() + # Test application scope. + assert harness.charm.get_secret("app", "password") is None + harness.update_relation_data( + rel_id, harness.charm.app.name, {"password": "test-password"} + ) + assert harness.charm.get_secret("app", "password") == "test-password" + + # Unit level changes don't require leader privileges + harness.set_leader(False) + # Test unit scope. + assert harness.charm.get_secret("unit", "password") is None + harness.update_relation_data( + rel_id, harness.charm.unit.name, {"password": "test-password"} + ) + assert harness.charm.get_secret("unit", "password") == "test-password" @patch_network_get(private_address="1.1.1.1") -@patch("charm.PostgresqlOperatorCharm._on_leader_elected") -@patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=True) -def test_on_get_password_secrets(harness, mock1, mock2): - rel_id = harness.add_relation(PEER, harness.charm.app.name) - # Create a mock event and set passwords in peer relation data. - harness.set_leader() - mock_event = MagicMock(params={}) - harness.charm.set_secret("app", "operator-password", "test-password") - harness.charm.set_secret("app", "replication-password", "replication-test-password") - - # Test providing an invalid username. - mock_event.params["username"] = "user" - harness.charm._on_get_password(mock_event) - mock_event.fail.assert_called_once() - mock_event.set_results.assert_not_called() - - # Test without providing the username option. - mock_event.reset_mock() - del mock_event.params["username"] - harness.charm._on_get_password(mock_event) - mock_event.set_results.assert_called_once_with({"password": "test-password"}) - - # Also test providing the username option. - mock_event.reset_mock() - mock_event.params["username"] = "replication" - harness.charm._on_get_password(mock_event) - mock_event.set_results.assert_called_once_with({"password": "replication-test-password"}) - -@parameterized.expand([("app"), ("unit")]) +def test_on_get_password_secrets(harness): + with ( + patch("charm.PostgresqlOperatorCharm._on_leader_elected"), + patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=True), + ): + rel_id = harness.add_relation(PEER, harness.charm.app.name) + # Create a mock event and set passwords in peer relation data. + harness.set_leader() + mock_event = MagicMock(params={}) + harness.charm.set_secret("app", "operator-password", "test-password") + harness.charm.set_secret("app", "replication-password", "replication-test-password") + + # Test providing an invalid username. + mock_event.params["username"] = "user" + harness.charm._on_get_password(mock_event) + mock_event.fail.assert_called_once() + mock_event.set_results.assert_not_called() + + # Test without providing the username option. + mock_event.reset_mock() + del mock_event.params["username"] + harness.charm._on_get_password(mock_event) + mock_event.set_results.assert_called_once_with({"password": "test-password"}) + + # Also test providing the username option. + mock_event.reset_mock() + mock_event.params["username"] = "replication" + harness.charm._on_get_password(mock_event) + mock_event.set_results.assert_called_once_with({"password": "replication-test-password"}) + +@pytest.mark.parametrize("scope", [("app"), ("unit")]) @patch_network_get(private_address="1.1.1.1") -@patch("charm.PostgresqlOperatorCharm._on_leader_elected") -@patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=True) -def test_get_secret_secrets(harness, scope, _): - rel_id = harness.add_relation(PEER, harness.charm.app.name) - harness.set_leader() +def test_get_secret_secrets(harness, scope): + with ( + patch("charm.PostgresqlOperatorCharm._on_leader_elected"), + patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=True), + ): + rel_id = harness.add_relation(PEER, harness.charm.app.name) + harness.set_leader() - assert harness.charm.get_secret(scope, "operator-password") is None - harness.charm.set_secret(scope, "operator-password", "test-password") - assert harness.charm.get_secret(scope, "operator-password") == "test-password" + assert harness.charm.get_secret(scope, "operator-password") is None + harness.charm.set_secret(scope, "operator-password", "test-password") + assert harness.charm.get_secret(scope, "operator-password") == "test-password" @patch_network_get(private_address="1.1.1.1") -@patch("charm.PostgresqlOperatorCharm._on_leader_elected") -def test_set_secret(harness, _): - rel_id = harness.add_relation(PEER, harness.charm.app.name) - harness.set_leader() - - # Test application scope. - assert "password" not in harness.get_relation_data(rel_id, harness.charm.app.name) - harness.charm.set_secret("app", "password", "test-password") - assert ( - harness.get_relation_data(rel_id, harness.charm.app.name)["password"] - == "test-password" - ) - harness.charm.set_secret("app", "password", None) - assert "password" not in harness.get_relation_data(rel_id, harness.charm.app.name) - - # Test unit scope. - assert "password" not in harness.get_relation_data(rel_id, harness.charm.unit.name) - harness.charm.set_secret("unit", "password", "test-password") - assert ( - harness.get_relation_data(rel_id, harness.charm.unit.name)["password"] - == "test-password" - ) - harness.charm.set_secret("unit", "password", None) - assert "password" not in harness.get_relation_data(rel_id, harness.charm.unit.name) +def test_set_secret(harness): + with patch("charm.PostgresqlOperatorCharm._on_leader_elected"): + rel_id = harness.add_relation(PEER, harness.charm.app.name) + harness.set_leader() - with pytest.raises(RuntimeError): - harness.charm.set_secret("test", "password", "test") + # Test application scope. + assert "password" not in harness.get_relation_data(rel_id, harness.charm.app.name) + harness.charm.set_secret("app", "password", "test-password") + assert ( + harness.get_relation_data(rel_id, harness.charm.app.name)["password"] + == "test-password" + ) + harness.charm.set_secret("app", "password", None) + assert "password" not in harness.get_relation_data(rel_id, harness.charm.app.name) -@parameterized.expand([("app", True), ("unit", True), ("unit", False)]) -@patch_network_get(private_address="1.1.1.1") -@patch("charm.PostgresqlOperatorCharm._on_leader_elected") -@patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=True) -def test_set_reset_new_secret(harness, scope, is_leader, _): - rel_id = harness.add_relation(PEER, harness.charm.app.name) - """NOTE: currently ops.testing seems to allow for non-leader to set secrets too!""" - # App has to be leader, unit can be either - harness.set_leader(is_leader) - # Getting current password - harness.charm.set_secret(scope, "new-secret", "bla") - assert harness.charm.get_secret(scope, "new-secret") == "bla" - - # Reset new secret - harness.charm.set_secret(scope, "new-secret", "blablabla") - assert harness.charm.get_secret(scope, "new-secret") == "blablabla" - - # Set another new secret - harness.charm.set_secret(scope, "new-secret2", "blablabla") - assert harness.charm.get_secret(scope, "new-secret2") == "blablabla" - -@parameterized.expand([("app", True), ("unit", True), ("unit", False)]) + # Test unit scope. + assert "password" not in harness.get_relation_data(rel_id, harness.charm.unit.name) + harness.charm.set_secret("unit", "password", "test-password") + assert ( + harness.get_relation_data(rel_id, harness.charm.unit.name)["password"] + == "test-password" + ) + harness.charm.set_secret("unit", "password", None) + assert "password" not in harness.get_relation_data(rel_id, harness.charm.unit.name) + + with pytest.raises(RuntimeError): + harness.charm.set_secret("test", "password", "test") + +@pytest.mark.parametrize("scope,is_leader", [("app", True), ("unit", True), ("unit", False)]) @patch_network_get(private_address="1.1.1.1") -@patch("charm.PostgresqlOperatorCharm._on_leader_elected") -@patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=True) -def test_invalid_secret(harness, scope, is_leader, _, __): - rel_id = harness.add_relation(PEER, harness.charm.app.name) - # App has to be leader, unit can be either - harness.set_leader(is_leader) +def test_set_reset_new_secret(harness, scope, is_leader): + with ( + patch("charm.PostgresqlOperatorCharm._on_leader_elected"), + patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=True), + ): + rel_id = harness.add_relation(PEER, harness.charm.app.name) + """NOTE: currently ops.testing seems to allow for non-leader to set secrets too!""" + # App has to be leader, unit can be either + harness.set_leader(is_leader) + # Getting current password + harness.charm.set_secret(scope, "new-secret", "bla") + assert harness.charm.get_secret(scope, "new-secret") == "bla" - with pytest.raises(RelationDataTypeError): - harness.charm.set_secret(scope, "somekey", 1) + # Reset new secret + harness.charm.set_secret(scope, "new-secret", "blablabla") + assert harness.charm.get_secret(scope, "new-secret") == "blablabla" - harness.charm.set_secret(scope, "somekey", "") - assert harness.charm.get_secret(scope, "somekey") is None + # Set another new secret + harness.charm.set_secret(scope, "new-secret2", "blablabla") + assert harness.charm.get_secret(scope, "new-secret2") == "blablabla" +@pytest.mark.parametrize("scope,is_leader", [("app", True), ("unit", True), ("unit", False)]) @patch_network_get(private_address="1.1.1.1") -@patch("charm.PostgresqlOperatorCharm._on_leader_elected") -def test_delete_password(harness, caplog, _): - rel_id = harness.add_relation(PEER, harness.charm.app.name) - """NOTE: currently ops.testing seems to allow for non-leader to remove secrets too!""" - harness.set_leader(True) - harness.update_relation_data( - rel_id, harness.charm.app.name, {"replication": "somepw"} - ) - harness.charm.remove_secret("app", "replication") - assert harness.charm.get_secret("app", "replication") is None - - harness.set_leader(False) - harness.update_relation_data( - rel_id, harness.charm.unit.name, {"somekey": "somevalue"} - ) - harness.charm.remove_secret("unit", "somekey") - assert harness.charm.get_secret("unit", "somekey") is None +def test_invalid_secret(harness, scope, is_leader): + with ( + patch("charm.PostgresqlOperatorCharm._on_leader_elected"), + patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=True), + ): + rel_id = harness.add_relation(PEER, harness.charm.app.name) + # App has to be leader, unit can be either + harness.set_leader(is_leader) - harness.set_leader(True) - with caplog.at_level(logging.ERROR): - harness.charm.remove_secret("app", "replication") - assert ( - "Non-existing field 'replication' was attempted to be removed" in caplog.text - ) + with pytest.raises(RelationDataTypeError): + harness.charm.set_secret(scope, "somekey", 1) - harness.charm.remove_secret("unit", "somekey") - assert "Non-existing field 'somekey' was attempted to be removed" in caplog.text + harness.charm.set_secret(scope, "somekey", "") + assert harness.charm.get_secret(scope, "somekey") is None - harness.charm.remove_secret("app", "non-existing-secret") - assert ( - "Non-existing field 'non-existing-secret' was attempted to be removed" - in caplog.text +@patch_network_get(private_address="1.1.1.1") +def test_delete_password(harness, caplog): + with patch("charm.PostgresqlOperatorCharm._on_leader_elected"): + rel_id = harness.add_relation(PEER, harness.charm.app.name) + """NOTE: currently ops.testing seems to allow for non-leader to remove secrets too!""" + harness.set_leader(True) + harness.update_relation_data( + rel_id, harness.charm.app.name, {"replication": "somepw"} ) + harness.charm.remove_secret("app", "replication") + assert harness.charm.get_secret("app", "replication") is None - harness.charm.remove_secret("unit", "non-existing-secret") - assert ( - "Non-existing field 'non-existing-secret' was attempted to be removed" - in caplog.text + harness.set_leader(False) + harness.update_relation_data( + rel_id, harness.charm.unit.name, {"somekey": "somevalue"} ) + harness.charm.remove_secret("unit", "somekey") + assert harness.charm.get_secret("unit", "somekey") is None + + harness.set_leader(True) + with caplog.at_level(logging.ERROR): + harness.charm.remove_secret("app", "replication") + assert ( + "Non-existing field 'replication' was attempted to be removed" in caplog.text + ) + + harness.charm.remove_secret("unit", "somekey") + assert "Non-existing field 'somekey' was attempted to be removed" in caplog.text + + harness.charm.remove_secret("app", "non-existing-secret") + assert ( + "Non-existing field 'non-existing-secret' was attempted to be removed" + in caplog.text + ) + + harness.charm.remove_secret("unit", "non-existing-secret") + assert ( + "Non-existing field 'non-existing-secret' was attempted to be removed" + in caplog.text + ) -@patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=True) @patch_network_get(private_address="1.1.1.1") -@patch("charm.PostgresqlOperatorCharm._on_leader_elected") -def test_delete_existing_password_secrets(harness, caplog, _, __): - rel_id = harness.add_relation(PEER, harness.charm.app.name) - """NOTE: currently ops.testing seems to allow for non-leader to remove secrets too!""" - harness.set_leader(True) - harness.charm.set_secret("app", "operator-password", "somepw") - harness.charm.remove_secret("app", "operator-password") - assert harness.charm.get_secret("app", "operator-password") is None - - harness.set_leader(False) - harness.charm.set_secret("unit", "operator-password", "somesecret") - harness.charm.remove_secret("unit", "operator-password") - assert harness.charm.get_secret("unit", "operator-password") is None - - harness.set_leader(True) - with caplog.at_level(logging.ERROR): +def test_delete_existing_password_secrets(harness, caplog): + with ( + patch("charm.PostgresqlOperatorCharm._on_leader_elected"), + patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=True), + ): + rel_id = harness.add_relation(PEER, harness.charm.app.name) + """NOTE: currently ops.testing seems to allow for non-leader to remove secrets too!""" + harness.set_leader(True) + harness.charm.set_secret("app", "operator-password", "somepw") harness.charm.remove_secret("app", "operator-password") - assert ( - "Non-existing secret operator-password was attempted to be removed." - in caplog.text - ) + assert harness.charm.get_secret("app", "operator-password") is None + harness.set_leader(False) + harness.charm.set_secret("unit", "operator-password", "somesecret") harness.charm.remove_secret("unit", "operator-password") - assert ( - "Non-existing secret operator-password was attempted to be removed." - in caplog.text - ) + assert harness.charm.get_secret("unit", "operator-password") is None - harness.charm.remove_secret("app", "non-existing-secret") - assert ( - "Non-existing field 'non-existing-secret' was attempted to be removed" - in caplog.text - ) + harness.set_leader(True) + with caplog.at_level(logging.ERROR): + harness.charm.remove_secret("app", "operator-password") + assert ( + "Non-existing secret operator-password was attempted to be removed." + in caplog.text + ) + + harness.charm.remove_secret("unit", "operator-password") + assert ( + "Non-existing secret operator-password was attempted to be removed." + in caplog.text + ) + + harness.charm.remove_secret("app", "non-existing-secret") + assert ( + "Non-existing field 'non-existing-secret' was attempted to be removed" + in caplog.text + ) + + harness.charm.remove_secret("unit", "non-existing-secret") + assert ( + "Non-existing field 'non-existing-secret' was attempted to be removed" + in caplog.text + ) + +@pytest.mark.parametrize("scope,is_leader", [("app", True), ("unit", True), ("unit", False)]) +@patch_network_get(private_address="1.1.1.1") +def test_migration_from_databag(harness, scope, is_leader): + with ( + patch("charm.PostgresqlOperatorCharm._on_leader_elected"), + patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=True), + ): + rel_id = harness.add_relation(PEER, harness.charm.app.name) + """Check if we're moving on to use secrets when live upgrade from databag to Secrets usage.""" + # App has to be leader, unit can be either + harness.set_leader(is_leader) - harness.charm.remove_secret("unit", "non-existing-secret") - assert ( - "Non-existing field 'non-existing-secret' was attempted to be removed" - in caplog.text + # Getting current password + entity = getattr(harness.charm, scope) + harness.update_relation_data(rel_id, entity.name, {"operator-password": "bla"}) + assert harness.charm.get_secret(scope, "operator-password") == "bla" + + # Reset new secret + harness.charm.set_secret(scope, "operator-password", "blablabla") + assert harness.charm.model.get_secret(label=f"postgresql.{scope}") + assert harness.charm.get_secret(scope, "operator-password") == "blablabla" + assert "operator-password" not in harness.get_relation_data( + rel_id, getattr(harness.charm, scope).name ) -@parameterized.expand([("app", True), ("unit", True), ("unit", False)]) +@pytest.mark.parametrize("scope,is_leader", [("app", True), ("unit", True), ("unit", False)]) @patch_network_get(private_address="1.1.1.1") -@patch("charm.PostgresqlOperatorCharm._on_leader_elected") -@patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=True) -def test_migration_from_databag(harness, scope, is_leader, _, __): - rel_id = harness.add_relation(PEER, harness.charm.app.name) - """Check if we're moving on to use secrets when live upgrade from databag to Secrets usage.""" - # App has to be leader, unit can be either - harness.set_leader(is_leader) - - # Getting current password - entity = getattr(harness.charm, scope) - harness.update_relation_data(rel_id, entity.name, {"operator-password": "bla"}) - assert harness.charm.get_secret(scope, "operator-password") == "bla" - - # Reset new secret - harness.charm.set_secret(scope, "operator-password", "blablabla") - assert harness.charm.model.get_secret(label=f"postgresql.{scope}") - assert harness.charm.get_secret(scope, "operator-password") == "blablabla" - assert "operator-password" not in harness.get_relation_data( - rel_id, getattr(harness.charm, scope).name - ) - -@parameterized.expand([("app", True), ("unit", True), ("unit", False)]) -@patch_network_get(private_address="1.1.1.1") -@patch("charm.PostgresqlOperatorCharm._on_leader_elected") -@patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=True) -def test_migration_from_single_secret(harness, scope, is_leader, _, __): - rel_id = harness.add_relation(PEER, harness.charm.app.name) - """Check if we're moving on to use secrets when live upgrade from databag to Secrets usage.""" - # App has to be leader, unit can be either - harness.set_leader(is_leader) +def test_migration_from_single_secret(harness, scope, is_leader): + with ( + patch("charm.PostgresqlOperatorCharm._on_leader_elected"), + patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=True), + ): + rel_id = harness.add_relation(PEER, harness.charm.app.name) + """Check if we're moving on to use secrets when live upgrade from databag to Secrets usage.""" + # App has to be leader, unit can be either + harness.set_leader(is_leader) - secret = harness.charm.app.add_secret({"operator-password": "bla"}) + secret = harness.charm.app.add_secret({"operator-password": "bla"}) - # Getting current password - entity = getattr(harness.charm, scope) - harness.update_relation_data( - rel_id, entity.name, {SECRET_INTERNAL_LABEL: secret.id} - ) - assert harness.charm.get_secret(scope, "operator-password") == "bla" + # Getting current password + entity = getattr(harness.charm, scope) + harness.update_relation_data( + rel_id, entity.name, {SECRET_INTERNAL_LABEL: secret.id} + ) + assert harness.charm.get_secret(scope, "operator-password") == "bla" - # Reset new secret - # Only the leader can set app secret content. - with harness.hooks_disabled(): - harness.set_leader(True) - harness.charm.set_secret(scope, "operator-password", "blablabla") - with harness.hooks_disabled(): - harness.set_leader(is_leader) - assert harness.charm.model.get_secret(label=f"postgresql.{scope}") - assert harness.charm.get_secret(scope, "operator-password") == "blablabla" - assert SECRET_INTERNAL_LABEL not in harness.get_relation_data( - rel_id, getattr(harness.charm, scope).name - ) + # Reset new secret + # Only the leader can set app secret content. + with harness.hooks_disabled(): + harness.set_leader(True) + harness.charm.set_secret(scope, "operator-password", "blablabla") + with harness.hooks_disabled(): + harness.set_leader(is_leader) + assert harness.charm.model.get_secret(label=f"postgresql.{scope}") + assert harness.charm.get_secret(scope, "operator-password") == "blablabla" + assert SECRET_INTERNAL_LABEL not in harness.get_relation_data( + rel_id, getattr(harness.charm, scope).name + ) -@patch("charms.rolling_ops.v0.rollingops.RollingOpsManager._on_acquire_lock") -@patch("charm.wait_fixed", return_value=wait_fixed(0)) -@patch("charm.Patroni.reload_patroni_configuration") -@patch("charm.PostgresqlOperatorCharm._unit_ip") -@patch("charm.PostgresqlOperatorCharm.is_tls_enabled", new_callable=PropertyMock) -def test_handle_postgresql_restart_need( - harness, _is_tls_enabled, _, _reload_patroni_configuration, __, _restart -): - rel_id = harness.add_relation(PEER, harness.charm.app.name) - with patch.object(PostgresqlOperatorCharm, "postgresql", Mock()) as postgresql_mock: +def test_handle_postgresql_restart_need(harness): + with ( + patch("charms.rolling_ops.v0.rollingops.RollingOpsManager._on_acquire_lock") as _restart, + patch("charm.wait_fixed", return_value=wait_fixed(0)), + patch("charm.Patroni.reload_patroni_configuration") as _reload_patroni_configuration, + patch("charm.PostgresqlOperatorCharm._unit_ip"), + patch("charm.PostgresqlOperatorCharm.is_tls_enabled", new_callable=PropertyMock) as _is_tls_enabled, + patch.object(PostgresqlOperatorCharm, "postgresql", Mock()) as postgresql_mock, + ): + rel_id = harness.add_relation(PEER, harness.charm.app.name) for values in itertools.product( [True, False], [True, False], [True, False], [True, False], [True, False] ): @@ -2070,214 +2020,206 @@ def test_handle_postgresql_restart_need( assert "postgresql_restarted" not in harness.get_relation_data(rel_id, harness.charm.unit) _restart.assert_not_called() -@patch("charm.PostgresqlOperatorCharm._update_relation_endpoints") -@patch("charm.PostgresqlOperatorCharm.primary_endpoint", new_callable=PropertyMock) -@patch("charm.PostgresqlOperatorCharm.update_config") -@patch("charm.PostgresqlOperatorCharm._remove_from_members_ips") -@patch("charm.Patroni.are_all_members_ready") -@patch("charm.PostgresqlOperatorCharm._get_ips_to_remove") -@patch("charm.PostgresqlOperatorCharm._updated_synchronous_node_count") -@patch("charm.Patroni.remove_raft_member") -@patch("charm.PostgresqlOperatorCharm._unit_ip") -@patch("charm.Patroni.get_member_ip") -def test_on_peer_relation_departed( - harness, - _get_member_ip, - _unit_ip, - _remove_raft_member, - _updated_synchronous_node_count, - _get_ips_to_remove, - _are_all_members_ready, - _remove_from_members_ips, - _update_config, - _primary_endpoint, - _update_relation_endpoints, -): - rel_id = harness.add_relation(PEER, harness.charm.app.name) - # Test when the current unit is the departing unit. - harness.charm.unit.status = ActiveStatus() - event = Mock() - event.departing_unit = harness.charm.unit - harness.charm._on_peer_relation_departed(event) - _remove_raft_member.assert_not_called() - event.defer.assert_not_called() - _updated_synchronous_node_count.assert_not_called() - _get_ips_to_remove.assert_not_called() - _remove_from_members_ips.assert_not_called() - _update_config.assert_not_called() - _update_relation_endpoints.assert_not_called() - assert isinstance(harness.charm.unit.status, ActiveStatus) - - # Test when the current unit is not the departing unit, but removing - # the member from the raft cluster fails. - _remove_raft_member.side_effect = RemoveRaftMemberFailedError - event.departing_unit = Unit( - f"{harness.charm.app.name}/1", None, harness.charm.app._backend, {} - ) - mock_ip_address = "1.1.1.1" - _get_member_ip.return_value = mock_ip_address - harness.charm._on_peer_relation_departed(event) - _remove_raft_member.assert_called_once_with(mock_ip_address) - event.defer.assert_called_once() - _updated_synchronous_node_count.assert_not_called() - _get_ips_to_remove.assert_not_called() - _remove_from_members_ips.assert_not_called() - _update_config.assert_not_called() - _update_relation_endpoints.assert_not_called() - assert isinstance(harness.charm.unit.status, ActiveStatus) - - # Test when the member is successfully removed from the raft cluster, - # but the unit is not the leader. - _remove_raft_member.reset_mock() - event.defer.reset_mock() - _remove_raft_member.side_effect = None - harness.charm._on_peer_relation_departed(event) - _remove_raft_member.assert_called_once_with(mock_ip_address) - event.defer.assert_not_called() - _updated_synchronous_node_count.assert_not_called() - _get_ips_to_remove.assert_not_called() - _remove_from_members_ips.assert_not_called() - _update_config.assert_not_called() - _update_relation_endpoints.assert_not_called() - assert isinstance(harness.charm.unit.status, ActiveStatus) - - # Test when the unit is the leader, but the cluster hasn't initialized yet, - # or it was unable to set synchronous_node_count. - _remove_raft_member.reset_mock() - with harness.hooks_disabled(): - harness.set_leader() - harness.charm._on_peer_relation_departed(event) - _remove_raft_member.assert_called_once_with(mock_ip_address) - event.defer.assert_called_once() - _updated_synchronous_node_count.assert_not_called() - _get_ips_to_remove.assert_not_called() - _remove_from_members_ips.assert_not_called() - _update_config.assert_not_called() - _update_relation_endpoints.assert_not_called() - assert isinstance(harness.charm.unit.status, ActiveStatus) - - _remove_raft_member.reset_mock() - event.defer.reset_mock() - _updated_synchronous_node_count.return_value = False - with harness.hooks_disabled(): - harness.update_relation_data( - rel_id, harness.charm.app.name, {"cluster_initialised": "True"} +def test_on_peer_relation_departed(harness): + with ( + patch("charm.PostgresqlOperatorCharm._update_relation_endpoints") as _update_relation_endpoints, + patch("charm.PostgresqlOperatorCharm.primary_endpoint", new_callable=PropertyMock) as _primary_endpoint, + patch("charm.PostgresqlOperatorCharm.update_config") as _update_config, + patch("charm.PostgresqlOperatorCharm._remove_from_members_ips") as _remove_from_members_ips, + patch("charm.Patroni.are_all_members_ready") as _are_all_members_ready, + patch("charm.PostgresqlOperatorCharm._get_ips_to_remove") as _get_ips_to_remove, + patch("charm.PostgresqlOperatorCharm._updated_synchronous_node_count") as _updated_synchronous_node_count, + patch("charm.Patroni.remove_raft_member") as _remove_raft_member, + patch("charm.PostgresqlOperatorCharm._unit_ip") as _unit_ip, + patch("charm.Patroni.get_member_ip") as _get_member_ip, + ): + rel_id = harness.add_relation(PEER, harness.charm.app.name) + # Test when the current unit is the departing unit. + harness.charm.unit.status = ActiveStatus() + event = Mock() + event.departing_unit = harness.charm.unit + harness.charm._on_peer_relation_departed(event) + _remove_raft_member.assert_not_called() + event.defer.assert_not_called() + _updated_synchronous_node_count.assert_not_called() + _get_ips_to_remove.assert_not_called() + _remove_from_members_ips.assert_not_called() + _update_config.assert_not_called() + _update_relation_endpoints.assert_not_called() + assert isinstance(harness.charm.unit.status, ActiveStatus) + + # Test when the current unit is not the departing unit, but removing + # the member from the raft cluster fails. + _remove_raft_member.side_effect = RemoveRaftMemberFailedError + event.departing_unit = Unit( + f"{harness.charm.app.name}/1", None, harness.charm.app._backend, {} ) - harness.charm._on_peer_relation_departed(event) - _remove_raft_member.assert_called_once_with(mock_ip_address) - event.defer.assert_called_once() - _updated_synchronous_node_count.assert_called_once_with(1) - _get_ips_to_remove.assert_not_called() - _remove_from_members_ips.assert_not_called() - _update_config.assert_not_called() - _update_relation_endpoints.assert_not_called() - assert isinstance(harness.charm.unit.status, ActiveStatus) - - # Test when there is more units in the cluster. - _remove_raft_member.reset_mock() - event.defer.reset_mock() - _updated_synchronous_node_count.reset_mock() - harness.add_relation_unit(rel_id, f"{harness.charm.app.name}/2") - harness.charm._on_peer_relation_departed(event) - _remove_raft_member.assert_called_once_with(mock_ip_address) - event.defer.assert_called_once() - _updated_synchronous_node_count.assert_called_once_with(2) - _get_ips_to_remove.assert_not_called() - _remove_from_members_ips.assert_not_called() - _update_config.assert_not_called() - _update_relation_endpoints.assert_not_called() - assert isinstance(harness.charm.unit.status, ActiveStatus) - - # Test when the cluster is initialised, and it could set synchronous_node_count, - # but there is no IPs to be removed from the members list. - _remove_raft_member.reset_mock() - event.defer.reset_mock() - _updated_synchronous_node_count.reset_mock() - _updated_synchronous_node_count.return_value = True - harness.charm._on_peer_relation_departed(event) - _remove_raft_member.assert_called_once_with(mock_ip_address) - event.defer.assert_not_called() - _updated_synchronous_node_count.assert_called_once_with(2) - _get_ips_to_remove.assert_called_once() - _remove_from_members_ips.assert_not_called() - _update_config.assert_not_called() - _update_relation_endpoints.assert_not_called() - assert isinstance(harness.charm.unit.status, ActiveStatus) - - # Test when there are IPs to be removed from the members list, but not all - # the members are ready yet. - _remove_raft_member.reset_mock() - _updated_synchronous_node_count.reset_mock() - _get_ips_to_remove.reset_mock() - ips_to_remove = ["2.2.2.2", "3.3.3.3"] - _get_ips_to_remove.return_value = ips_to_remove - _are_all_members_ready.return_value = False - harness.charm._on_peer_relation_departed(event) - _remove_raft_member.assert_called_once_with(mock_ip_address) - event.defer.assert_called_once() - _updated_synchronous_node_count.assert_called_once_with(2) - _get_ips_to_remove.assert_called_once() - _remove_from_members_ips.assert_not_called() - _update_config.assert_not_called() - _update_relation_endpoints.assert_not_called() - assert isinstance(harness.charm.unit.status, ActiveStatus) - - # Test when all members are ready. - _remove_raft_member.reset_mock() - event.defer.reset_mock() - _updated_synchronous_node_count.reset_mock() - _get_ips_to_remove.reset_mock() - _are_all_members_ready.return_value = True - harness.charm._on_peer_relation_departed(event) - _remove_raft_member.assert_called_once_with(mock_ip_address) - event.defer.assert_not_called() - _updated_synchronous_node_count.assert_called_once_with(2) - _get_ips_to_remove.assert_called_once() - _remove_from_members_ips.assert_has_calls([call(ips_to_remove[0]), call(ips_to_remove[1])]) - assert _update_config.call_count == 2 - assert _update_relation_endpoints.call_count == 2 - assert isinstance(harness.charm.unit.status, ActiveStatus) - - # Test when the primary is not reachable yet. - _remove_raft_member.reset_mock() - event.defer.reset_mock() - _updated_synchronous_node_count.reset_mock() - _get_ips_to_remove.reset_mock() - _remove_from_members_ips.reset_mock() - _update_config.reset_mock() - _update_relation_endpoints.reset_mock() - _primary_endpoint.return_value = None - harness.charm._on_peer_relation_departed(event) - _remove_raft_member.assert_called_once_with(mock_ip_address) - event.defer.assert_not_called() - _updated_synchronous_node_count.assert_called_once_with(2) - _get_ips_to_remove.assert_called_once() - _remove_from_members_ips.assert_called_once() - _update_config.assert_called_once() - _update_relation_endpoints.assert_not_called() - assert isinstance(harness.charm.unit.status, WaitingStatus) - -@patch("charm.PostgresqlOperatorCharm._update_relation_endpoints") -@patch("charm.PostgresqlOperatorCharm.primary_endpoint", new_callable=PropertyMock) -def test_update_new_unit_status(harness, _primary_endpoint, _update_relation_endpoints): - rel_id = harness.add_relation(PEER, harness.charm.app.name) - # Test when the charm is blocked. - _primary_endpoint.return_value = "endpoint" - harness.charm.unit.status = BlockedStatus("fake blocked status") - harness.charm._update_new_unit_status() - _update_relation_endpoints.assert_called_once() - assert isinstance(harness.charm.unit.status, BlockedStatus) - - # Test when the charm is not blocked. - _update_relation_endpoints.reset_mock() - harness.charm.unit.status = WaitingStatus() - harness.charm._update_new_unit_status() - _update_relation_endpoints.assert_called_once() - assert isinstance(harness.charm.unit.status, ActiveStatus) - - # Test when the primary endpoint is not reachable yet. - _update_relation_endpoints.reset_mock() - _primary_endpoint.return_value = None - harness.charm._update_new_unit_status() - _update_relation_endpoints.assert_not_called() - assert isinstance(harness.charm.unit.status, WaitingStatus) + mock_ip_address = "1.1.1.1" + _get_member_ip.return_value = mock_ip_address + harness.charm._on_peer_relation_departed(event) + _remove_raft_member.assert_called_once_with(mock_ip_address) + event.defer.assert_called_once() + _updated_synchronous_node_count.assert_not_called() + _get_ips_to_remove.assert_not_called() + _remove_from_members_ips.assert_not_called() + _update_config.assert_not_called() + _update_relation_endpoints.assert_not_called() + assert isinstance(harness.charm.unit.status, ActiveStatus) + + # Test when the member is successfully removed from the raft cluster, + # but the unit is not the leader. + _remove_raft_member.reset_mock() + event.defer.reset_mock() + _remove_raft_member.side_effect = None + harness.charm._on_peer_relation_departed(event) + _remove_raft_member.assert_called_once_with(mock_ip_address) + event.defer.assert_not_called() + _updated_synchronous_node_count.assert_not_called() + _get_ips_to_remove.assert_not_called() + _remove_from_members_ips.assert_not_called() + _update_config.assert_not_called() + _update_relation_endpoints.assert_not_called() + assert isinstance(harness.charm.unit.status, ActiveStatus) + + # Test when the unit is the leader, but the cluster hasn't initialized yet, + # or it was unable to set synchronous_node_count. + _remove_raft_member.reset_mock() + with harness.hooks_disabled(): + harness.set_leader() + harness.charm._on_peer_relation_departed(event) + _remove_raft_member.assert_called_once_with(mock_ip_address) + event.defer.assert_called_once() + _updated_synchronous_node_count.assert_not_called() + _get_ips_to_remove.assert_not_called() + _remove_from_members_ips.assert_not_called() + _update_config.assert_not_called() + _update_relation_endpoints.assert_not_called() + assert isinstance(harness.charm.unit.status, ActiveStatus) + + _remove_raft_member.reset_mock() + event.defer.reset_mock() + _updated_synchronous_node_count.return_value = False + with harness.hooks_disabled(): + harness.update_relation_data( + rel_id, harness.charm.app.name, {"cluster_initialised": "True"} + ) + harness.charm._on_peer_relation_departed(event) + _remove_raft_member.assert_called_once_with(mock_ip_address) + event.defer.assert_called_once() + _updated_synchronous_node_count.assert_called_once_with(1) + _get_ips_to_remove.assert_not_called() + _remove_from_members_ips.assert_not_called() + _update_config.assert_not_called() + _update_relation_endpoints.assert_not_called() + assert isinstance(harness.charm.unit.status, ActiveStatus) + + # Test when there is more units in the cluster. + _remove_raft_member.reset_mock() + event.defer.reset_mock() + _updated_synchronous_node_count.reset_mock() + harness.add_relation_unit(rel_id, f"{harness.charm.app.name}/2") + harness.charm._on_peer_relation_departed(event) + _remove_raft_member.assert_called_once_with(mock_ip_address) + event.defer.assert_called_once() + _updated_synchronous_node_count.assert_called_once_with(2) + _get_ips_to_remove.assert_not_called() + _remove_from_members_ips.assert_not_called() + _update_config.assert_not_called() + _update_relation_endpoints.assert_not_called() + assert isinstance(harness.charm.unit.status, ActiveStatus) + + # Test when the cluster is initialised, and it could set synchronous_node_count, + # but there is no IPs to be removed from the members list. + _remove_raft_member.reset_mock() + event.defer.reset_mock() + _updated_synchronous_node_count.reset_mock() + _updated_synchronous_node_count.return_value = True + harness.charm._on_peer_relation_departed(event) + _remove_raft_member.assert_called_once_with(mock_ip_address) + event.defer.assert_not_called() + _updated_synchronous_node_count.assert_called_once_with(2) + _get_ips_to_remove.assert_called_once() + _remove_from_members_ips.assert_not_called() + _update_config.assert_not_called() + _update_relation_endpoints.assert_not_called() + assert isinstance(harness.charm.unit.status, ActiveStatus) + + # Test when there are IPs to be removed from the members list, but not all + # the members are ready yet. + _remove_raft_member.reset_mock() + _updated_synchronous_node_count.reset_mock() + _get_ips_to_remove.reset_mock() + ips_to_remove = ["2.2.2.2", "3.3.3.3"] + _get_ips_to_remove.return_value = ips_to_remove + _are_all_members_ready.return_value = False + harness.charm._on_peer_relation_departed(event) + _remove_raft_member.assert_called_once_with(mock_ip_address) + event.defer.assert_called_once() + _updated_synchronous_node_count.assert_called_once_with(2) + _get_ips_to_remove.assert_called_once() + _remove_from_members_ips.assert_not_called() + _update_config.assert_not_called() + _update_relation_endpoints.assert_not_called() + assert isinstance(harness.charm.unit.status, ActiveStatus) + + # Test when all members are ready. + _remove_raft_member.reset_mock() + event.defer.reset_mock() + _updated_synchronous_node_count.reset_mock() + _get_ips_to_remove.reset_mock() + _are_all_members_ready.return_value = True + harness.charm._on_peer_relation_departed(event) + _remove_raft_member.assert_called_once_with(mock_ip_address) + event.defer.assert_not_called() + _updated_synchronous_node_count.assert_called_once_with(2) + _get_ips_to_remove.assert_called_once() + _remove_from_members_ips.assert_has_calls([call(ips_to_remove[0]), call(ips_to_remove[1])]) + assert _update_config.call_count == 2 + assert _update_relation_endpoints.call_count == 2 + assert isinstance(harness.charm.unit.status, ActiveStatus) + + # Test when the primary is not reachable yet. + _remove_raft_member.reset_mock() + event.defer.reset_mock() + _updated_synchronous_node_count.reset_mock() + _get_ips_to_remove.reset_mock() + _remove_from_members_ips.reset_mock() + _update_config.reset_mock() + _update_relation_endpoints.reset_mock() + _primary_endpoint.return_value = None + harness.charm._on_peer_relation_departed(event) + _remove_raft_member.assert_called_once_with(mock_ip_address) + event.defer.assert_not_called() + _updated_synchronous_node_count.assert_called_once_with(2) + _get_ips_to_remove.assert_called_once() + _remove_from_members_ips.assert_called_once() + _update_config.assert_called_once() + _update_relation_endpoints.assert_not_called() + assert isinstance(harness.charm.unit.status, WaitingStatus) + +def test_update_new_unit_status(harness): + with( + patch("charm.PostgresqlOperatorCharm._update_relation_endpoints") as _update_relation_endpoints, + patch("charm.PostgresqlOperatorCharm.primary_endpoint", new_callable=PropertyMock) as _primary_endpoint, + ): + rel_id = harness.add_relation(PEER, harness.charm.app.name) + # Test when the charm is blocked. + _primary_endpoint.return_value = "endpoint" + harness.charm.unit.status = BlockedStatus("fake blocked status") + harness.charm._update_new_unit_status() + _update_relation_endpoints.assert_called_once() + assert isinstance(harness.charm.unit.status, BlockedStatus) + + # Test when the charm is not blocked. + _update_relation_endpoints.reset_mock() + harness.charm.unit.status = WaitingStatus() + harness.charm._update_new_unit_status() + _update_relation_endpoints.assert_called_once() + assert isinstance(harness.charm.unit.status, ActiveStatus) + + # Test when the primary endpoint is not reachable yet. + _update_relation_endpoints.reset_mock() + _primary_endpoint.return_value = None + harness.charm._update_new_unit_status() + _update_relation_endpoints.assert_not_called() + assert isinstance(harness.charm.unit.status, WaitingStatus) diff --git a/tox.ini b/tox.ini index 7979c7eb3b..18edb1846a 100644 --- a/tox.ini +++ b/tox.ini @@ -65,7 +65,7 @@ commands_pre = poetry install --only main,charm-libs,unit --no-root commands = poetry run coverage run --source={[vars]src_path} \ - -m pytest -v --tb native -s {posargs} {[vars]tests_path}/unit/test_charm.py::test_enable_disable_extensions + -m pytest -v --tb native -s {posargs} {[vars]tests_path}/unit poetry run coverage report poetry run coverage xml From 92d8e8f23f50de4adcae16f9f2baaaceb072959f Mon Sep 17 00:00:00 2001 From: lucasgameiroborges Date: Wed, 3 Apr 2024 19:56:13 +0000 Subject: [PATCH 05/10] fix linting --- tests/unit/test_charm.py | 86 ++++++++++++++++++++-------------------- 1 file changed, 42 insertions(+), 44 deletions(-) diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py index 148aa0f25c..b7839e0659 100644 --- a/tests/unit/test_charm.py +++ b/tests/unit/test_charm.py @@ -4,7 +4,6 @@ import logging import platform import subprocess -import unittest from unittest.mock import MagicMock, Mock, PropertyMock, call, mock_open, patch, sentinel import pytest @@ -24,7 +23,6 @@ WaitingStatus, ) from ops.testing import Harness -from parameterized import parameterized from psycopg2 import OperationalError from tenacity import RetryError, wait_fixed @@ -62,7 +60,7 @@ def test_on_install(harness): "charm.PostgresqlOperatorCharm._is_storage_attached", side_effect=[False, True, True], ) as _is_storage_attached: - rel_id = harness.add_relation(PEER, harness.charm.app.name) + harness.add_relation(PEER, harness.charm.app.name) # Test without storage. harness.charm.on.install.emit() _reboot_on_detached_storage.assert_called_once() @@ -98,7 +96,7 @@ def test_on_install_failed_to_create_home(harness): ) as _is_storage_attached, patch( "charm.logger.exception" ) as _logger_exception: - rel_id = harness.add_relation(PEER, harness.charm.app.name) + harness.add_relation(PEER, harness.charm.app.name) # Test without storage. harness.charm.on.install.emit() _reboot_on_detached_storage.assert_called_once() @@ -123,7 +121,7 @@ def test_on_install_snap_failure(harness): with patch("charm.PostgresqlOperatorCharm._install_snap_packages") as _install_snap_packages, patch( "charm.PostgresqlOperatorCharm._is_storage_attached", return_value=True ) as _is_storage_attached: - rel_id = harness.add_relation(PEER, harness.charm.app.name) + harness.add_relation(PEER, harness.charm.app.name) # Mock the result of the call. _install_snap_packages.side_effect = snap.SnapError # Trigger the hook. @@ -134,7 +132,7 @@ def test_on_install_snap_failure(harness): @patch_network_get(private_address="1.1.1.1") def test_patroni_scrape_config_no_tls(harness): - rel_id = harness.add_relation(PEER, harness.charm.app.name) + harness.add_relation(PEER, harness.charm.app.name) result = harness.charm.patroni_scrape_config() assert result == [ @@ -153,7 +151,7 @@ def test_patroni_scrape_config_tls(harness): return_value=True, new_callable=PropertyMock, ): - rel_id = harness.add_relation(PEER, harness.charm.app.name) + harness.add_relation(PEER, harness.charm.app.name) result = harness.charm.patroni_scrape_config() assert result == [ @@ -173,7 +171,7 @@ def test_primary_endpoint(harness): ), patch( "charm.PostgresqlOperatorCharm._patroni", new_callable=PropertyMock ) as _patroni: - rel_id = harness.add_relation(PEER, harness.charm.app.name) + harness.add_relation(PEER, harness.charm.app.name) _patroni.return_value.get_member_ip.return_value = "1.1.1.1" _patroni.return_value.get_primary.return_value = sentinel.primary assert harness.charm.primary_endpoint == "1.1.1.1" @@ -191,7 +189,7 @@ def test_primary_endpoint_no_peers(harness): ), patch( "charm.PostgresqlOperatorCharm._patroni", new_callable=PropertyMock ) as _patroni: - rel_id = harness.add_relation(PEER, harness.charm.app.name) + harness.add_relation(PEER, harness.charm.app.name) assert harness.charm.primary_endpoint is None assert not _patroni.return_value.get_member_ip.called @@ -207,9 +205,9 @@ def test_on_leader_elected(harness): ) as _primary_endpoint, patch( "charm.PostgresqlOperatorCharm.update_config" ) as _update_config: - rel_id = harness.add_relation(PEER, harness.charm.app.name) + harness.add_relation(PEER, harness.charm.app.name) # Assert that there is no password in the peer relation. - assert harness.charm._peers.data[harness.charm.app].get("operator-password", None) == None + assert harness.charm._peers.data[harness.charm.app].get("operator-password", None) is None # Check that a new password was generated on leader election. _primary_endpoint.return_value = "1.1.1.1" @@ -217,7 +215,7 @@ def test_on_leader_elected(harness): password = harness.charm._peers.data[harness.charm.app].get("operator-password", None) _update_config.assert_called_once() _update_relation_endpoints.assert_not_called() - assert password != None + assert password is not None # Mark the cluster as initialised. harness.charm._peers.data[harness.charm.app].update({"cluster_initialised": "True"}) @@ -261,7 +259,7 @@ def test_on_config_changed(harness): ) as _enable_disable_extensions, patch( "charm.PostgresqlOperatorCharm.is_cluster_initialised", new_callable=PropertyMock ) as _is_cluster_initialised: - rel_id = harness.add_relation(PEER, harness.charm.app.name) + harness.add_relation(PEER, harness.charm.app.name) # Test when the cluster was not initialised yet. _is_cluster_initialised.return_value = False harness.charm.on.config_changed.emit() @@ -330,7 +328,7 @@ def test_check_extension_dependencies(harness): with patch("subprocess.check_output", return_value=b"C"), patch.object( PostgresqlOperatorCharm, "postgresql", Mock() ): - rel_id = harness.add_relation(PEER, harness.charm.app.name) + harness.add_relation(PEER, harness.charm.app.name) # Test when plugins dependencies exception is not caused config = { "plugin_address_standardizer_enable": False, @@ -358,7 +356,7 @@ def test_enable_disable_extensions(harness, caplog): with patch("subprocess.check_output", return_value=b"C"), patch.object( PostgresqlOperatorCharm, "postgresql", Mock() ) as postgresql_mock: - rel_id = harness.add_relation(PEER, harness.charm.app.name) + harness.add_relation(PEER, harness.charm.app.name) # Test when all extensions install/uninstall succeed. postgresql_mock.enable_disable_extension.side_effect = None with caplog.at_level(logging.ERROR): @@ -546,7 +544,7 @@ def test_on_start(harness): with ( patch("charm.PostgresqlOperatorCharm.enable_disable_extensions") as _enable_disable_extensions, patch("charm.snap.SnapCache") as _snap_cache, - patch("charm.Patroni.get_postgresql_version") as _get_postgresql_version, + patch("charm.Patroni.get_postgresql_version") as _get_postgresql_version, patch("charm.PostgreSQLProvider.oversee_users") as _oversee_users, patch("charm.PostgresqlOperatorCharm._update_relation_endpoints", new_callable=PropertyMock) as _update_relation_endpoints, patch("charm.PostgresqlOperatorCharm.postgresql") as _postgresql, @@ -566,7 +564,7 @@ def test_on_start(harness): side_effect=[False, True, True, True, True], ) as _is_storage_attached, ): - rel_id = harness.add_relation(PEER, harness.charm.app.name) + harness.add_relation(PEER, harness.charm.app.name) _get_postgresql_version.return_value = "14.0" # Test without storage. @@ -636,7 +634,7 @@ def test_on_start_replica(harness): return_value=True, ) as _is_storage_attached, ): - rel_id = harness.add_relation(PEER, harness.charm.app.name) + harness.add_relation(PEER, harness.charm.app.name) _get_postgresql_version.return_value = "14.0" # Set the current unit to be a replica (non leader unit). @@ -682,7 +680,7 @@ def test_on_start_no_patroni_member(harness): patch("upgrade.PostgreSQLUpgrade.idle", return_value=True) as _idle, patch("charm.PostgresqlOperatorCharm._is_storage_attached", return_value=True) as _is_storage_attached, ): - rel_id = harness.add_relation(PEER, harness.charm.app.name) + harness.add_relation(PEER, harness.charm.app.name) # Mock the passwords. patroni.return_value.member_started = False _get_password.return_value = "fake-operator-password" @@ -705,7 +703,7 @@ def test_on_start_after_blocked_state(harness): patch("charm.PostgresqlOperatorCharm._get_password") as _get_password, patch("charm.PostgresqlOperatorCharm._is_storage_attached", return_value=True) as _is_storage_attached, ): - rel_id = harness.add_relation(PEER, harness.charm.app.name) + harness.add_relation(PEER, harness.charm.app.name) # Set an initial blocked status (like after the install hook was triggered). initial_status = BlockedStatus("fake message") harness.model.unit.status = initial_status @@ -761,7 +759,7 @@ def test_on_set_password(harness): patch("charm.Patroni.are_all_members_ready") as _are_all_members_ready, patch("charm.PostgresqlOperatorCharm._on_leader_elected"), ): - rel_id = harness.add_relation(PEER, harness.charm.app.name) + harness.add_relation(PEER, harness.charm.app.name) # Create a mock event. mock_event = MagicMock(params={}) @@ -977,7 +975,7 @@ def test_on_update_status_after_restore_operation(harness): def test_install_snap_packages(harness): with patch("charm.snap.SnapCache") as _snap_cache: - rel_id = harness.add_relation(PEER, harness.charm.app.name) + harness.add_relation(PEER, harness.charm.app.name) _snap_package = _snap_cache.return_value.__getitem__.return_value _snap_package.ensure.side_effect = snap.SnapError _snap_package.present = False @@ -1069,7 +1067,7 @@ def test_is_storage_attached(harness): "subprocess.check_call", side_effect=[None, subprocess.CalledProcessError(1, "fake command")], ) as _check_call: - rel_id = harness.add_relation(PEER, harness.charm.app.name) + harness.add_relation(PEER, harness.charm.app.name) # Test with attached storage. is_storage_attached = harness.charm._is_storage_attached() _check_call.assert_called_once_with(["mountpoint", "-q", harness.charm._storage_path]) @@ -1082,7 +1080,7 @@ def test_is_storage_attached(harness): def test_reboot_on_detached_storage(harness): with patch("subprocess.check_call") as _check_call: - rel_id = harness.add_relation(PEER, harness.charm.app.name) + harness.add_relation(PEER, harness.charm.app.name) mock_event = MagicMock() harness.charm._reboot_on_detached_storage(mock_event) mock_event.defer.assert_called_once() @@ -1095,7 +1093,7 @@ def test_restart(harness): patch("charm.Patroni.restart_postgresql") as _restart_postgresql, patch("charm.Patroni.are_all_members_ready") as _are_all_members_ready, ): - rel_id = harness.add_relation(PEER, harness.charm.app.name) + harness.add_relation(PEER, harness.charm.app.name) _are_all_members_ready.side_effect = [False, True, True] # Test when not all members are ready. @@ -1212,7 +1210,7 @@ def test_on_cluster_topology_change(harness): patch("charm.PostgresqlOperatorCharm._update_relation_endpoints") as _update_relation_endpoints, patch("charm.PostgresqlOperatorCharm.primary_endpoint", new_callable=PropertyMock) as _primary_endpoint, ): - rel_id = harness.add_relation(PEER, harness.charm.app.name) + harness.add_relation(PEER, harness.charm.app.name) # Mock the property value. _primary_endpoint.side_effect = [None, "1.1.1.1"] @@ -1233,7 +1231,7 @@ def test_on_cluster_topology_change_keep_blocked(harness): ) as _primary_endpoint, patch("charm.PostgresqlOperatorCharm._update_relation_endpoints") as _update_relation_endpoints, ): - rel_id = harness.add_relation(PEER, harness.charm.app.name) + harness.add_relation(PEER, harness.charm.app.name) harness.model.unit.status = WaitingStatus(PRIMARY_NOT_REACHABLE_MESSAGE) harness.charm._on_cluster_topology_change(Mock()) @@ -1252,7 +1250,7 @@ def test_on_cluster_topology_change_clear_blocked(harness): ) as _primary_endpoint, patch("charm.PostgresqlOperatorCharm._update_relation_endpoints") as _update_relation_endpoints, ): - rel_id = harness.add_relation(PEER, harness.charm.app.name) + harness.add_relation(PEER, harness.charm.app.name) harness.model.unit.status = WaitingStatus(PRIMARY_NOT_REACHABLE_MESSAGE) harness.charm._on_cluster_topology_change(Mock()) @@ -1266,7 +1264,7 @@ def test_validate_config_options(harness): patch("charm.PostgresqlOperatorCharm.postgresql", new_callable=PropertyMock) as _charm_lib, patch("config.subprocess"), ): - rel_id = harness.add_relation(PEER, harness.charm.app.name) + harness.add_relation(PEER, harness.charm.app.name) _charm_lib.return_value.get_postgresql_text_search_configs.return_value = [] _charm_lib.return_value.validate_date_style.return_value = [] _charm_lib.return_value.get_postgresql_timezones.return_value = [] @@ -1534,7 +1532,7 @@ def test_update_certificate_secrets(harness): patch("charms.postgresql_k8s.v0.postgresql_tls.PostgreSQLTLS._request_certificate") as _request_certificate, patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=True), ): - rel_id = harness.add_relation(PEER, harness.charm.app.name) + harness.add_relation(PEER, harness.charm.app.name) # If there is no current TLS files, _request_certificate should be called # only when the certificates relation is established. harness.charm._update_certificate() @@ -1575,7 +1573,7 @@ def test_update_member_ip(harness): ) assert not (harness.charm._update_member_ip()) relation_data = harness.get_relation_data(rel_id, harness.charm.unit.name) - assert relation_data.get("ip-to-remove") == None + assert relation_data.get("ip-to-remove") is None _stop_patroni.assert_not_called() _update_certificate.assert_not_called() @@ -1602,7 +1600,7 @@ def test_push_tls_files_to_workload(harness): patch("charm.Patroni.render_file") as _render_file, patch("charms.postgresql_k8s.v0.postgresql_tls.PostgreSQLTLS.get_tls_files") as _get_tls_files, ): - rel_id = harness.add_relation(PEER, harness.charm.app.name) + harness.add_relation(PEER, harness.charm.app.name) _get_tls_files.side_effect = [ ("key", "ca", "cert"), ("key", "ca", None), @@ -1624,7 +1622,7 @@ def test_push_tls_files_to_workload(harness): def test_is_workload_running(harness): with patch("charm.snap.SnapCache") as _snap_cache: - rel_id = harness.add_relation(PEER, harness.charm.app.name) + harness.add_relation(PEER, harness.charm.app.name) pg_snap = _snap_cache.return_value[POSTGRESQL_SNAP_NAME] pg_snap.present = False @@ -1634,7 +1632,7 @@ def test_is_workload_running(harness): assert (harness.charm._is_workload_running) def test_get_available_memory(harness): - rel_id = harness.add_relation(PEER, harness.charm.app.name) + harness.add_relation(PEER, harness.charm.app.name) meminfo = ( "MemTotal: 16089488 kB" "MemFree: 799284 kB" @@ -1656,7 +1654,7 @@ def test_juju_run_exec_divergence(harness): patch("charm.ClusterTopologyObserver") as _topology_observer, patch("charm.JujuVersion") as _juju_version, ): - rel_id = harness.add_relation(PEER, harness.charm.app.name) + harness.add_relation(PEER, harness.charm.app.name) # Juju 2 _juju_version.from_environ.return_value.major = 2 harness = Harness(PostgresqlOperatorCharm) @@ -1671,7 +1669,7 @@ def test_juju_run_exec_divergence(harness): _topology_observer.assert_called_once_with(harness.charm, "/usr/bin/juju-exec") def test_client_relations(harness): - rel_id = harness.add_relation(PEER, harness.charm.app.name) + harness.add_relation(PEER, harness.charm.app.name) # Test when the charm has no relations. assert len(harness.charm.client_relations) == 0 @@ -1689,7 +1687,7 @@ def test_client_relations(harness): # def test_scope_obj(harness): - rel_id = harness.add_relation(PEER, harness.charm.app.name) + harness.add_relation(PEER, harness.charm.app.name) assert harness.charm._scope_obj("app") == harness.charm.framework.model.app assert harness.charm._scope_obj("unit") == harness.charm.framework.model.unit assert harness.charm._scope_obj("test") is None @@ -1722,7 +1720,7 @@ def test_on_get_password_secrets(harness): patch("charm.PostgresqlOperatorCharm._on_leader_elected"), patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=True), ): - rel_id = harness.add_relation(PEER, harness.charm.app.name) + harness.add_relation(PEER, harness.charm.app.name) # Create a mock event and set passwords in peer relation data. harness.set_leader() mock_event = MagicMock(params={}) @@ -1754,7 +1752,7 @@ def test_get_secret_secrets(harness, scope): patch("charm.PostgresqlOperatorCharm._on_leader_elected"), patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=True), ): - rel_id = harness.add_relation(PEER, harness.charm.app.name) + harness.add_relation(PEER, harness.charm.app.name) harness.set_leader() assert harness.charm.get_secret(scope, "operator-password") is None @@ -1797,7 +1795,7 @@ def test_set_reset_new_secret(harness, scope, is_leader): patch("charm.PostgresqlOperatorCharm._on_leader_elected"), patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=True), ): - rel_id = harness.add_relation(PEER, harness.charm.app.name) + harness.add_relation(PEER, harness.charm.app.name) """NOTE: currently ops.testing seems to allow for non-leader to set secrets too!""" # App has to be leader, unit can be either harness.set_leader(is_leader) @@ -1820,7 +1818,7 @@ def test_invalid_secret(harness, scope, is_leader): patch("charm.PostgresqlOperatorCharm._on_leader_elected"), patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=True), ): - rel_id = harness.add_relation(PEER, harness.charm.app.name) + harness.add_relation(PEER, harness.charm.app.name) # App has to be leader, unit can be either harness.set_leader(is_leader) @@ -1877,7 +1875,7 @@ def test_delete_existing_password_secrets(harness, caplog): patch("charm.PostgresqlOperatorCharm._on_leader_elected"), patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=True), ): - rel_id = harness.add_relation(PEER, harness.charm.app.name) + harness.add_relation(PEER, harness.charm.app.name) """NOTE: currently ops.testing seems to allow for non-leader to remove secrets too!""" harness.set_leader(True) harness.charm.set_secret("app", "operator-password", "somepw") @@ -2009,7 +2007,7 @@ def test_handle_postgresql_restart_need(harness): assert "tls" in harness.get_relation_data(rel_id, harness.charm.unit) else: assert "tls" not in harness.get_relation_data(rel_id, harness.charm.unit) - + if (values[1] != values[2]) or values[3]: assert "postgresql_restarted" not in harness.get_relation_data(rel_id, harness.charm.unit) _restart.assert_called_once() @@ -2202,7 +2200,7 @@ def test_update_new_unit_status(harness): patch("charm.PostgresqlOperatorCharm._update_relation_endpoints") as _update_relation_endpoints, patch("charm.PostgresqlOperatorCharm.primary_endpoint", new_callable=PropertyMock) as _primary_endpoint, ): - rel_id = harness.add_relation(PEER, harness.charm.app.name) + harness.add_relation(PEER, harness.charm.app.name) # Test when the charm is blocked. _primary_endpoint.return_value = "endpoint" harness.charm.unit.status = BlockedStatus("fake blocked status") From 503420f5dc78450c162ef8f4b99472b679bdbf89 Mon Sep 17 00:00:00 2001 From: lucasgameiroborges Date: Wed, 3 Apr 2024 19:59:11 +0000 Subject: [PATCH 06/10] fix lint format --- tests/unit/test_charm.py | 329 ++++++++++++++++++++++++++------------- 1 file changed, 223 insertions(+), 106 deletions(-) diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py index b7839e0659..2d8ea89594 100644 --- a/tests/unit/test_charm.py +++ b/tests/unit/test_charm.py @@ -40,6 +40,7 @@ # @pytest.mark.usefixtures("juju_has_secrets") + @pytest.fixture def harness(): harness = Harness(PostgresqlOperatorCharm) @@ -48,6 +49,7 @@ def harness(): yield harness harness.cleanup() + @patch_network_get(private_address="1.1.1.1") def test_on_install(harness): with patch("charm.subprocess.check_call") as _check_call, patch( @@ -80,7 +82,8 @@ def test_on_install(harness): _check_call.assert_any_call("usermod -d /home/snap_daemon snap_daemon".split()) # Assert the status set by the event handler. - assert (isinstance(harness.model.unit.status, WaitingStatus)) + assert isinstance(harness.model.unit.status, WaitingStatus) + @patch_network_get(private_address="1.1.1.1") def test_on_install_failed_to_create_home(harness): @@ -93,9 +96,7 @@ def test_on_install_failed_to_create_home(harness): ) as _reboot_on_detached_storage, patch( "charm.PostgresqlOperatorCharm._is_storage_attached", side_effect=[False, True, True], - ) as _is_storage_attached, patch( - "charm.logger.exception" - ) as _logger_exception: + ) as _is_storage_attached, patch("charm.logger.exception") as _logger_exception: harness.add_relation(PEER, harness.charm.app.name) # Test without storage. harness.charm.on.install.emit() @@ -114,11 +115,14 @@ def test_on_install_failed_to_create_home(harness): _logger_exception.assert_called_once_with("Unable to create snap_daemon home dir") # Assert the status set by the event handler. - assert (isinstance(harness.model.unit.status, WaitingStatus)) + assert isinstance(harness.model.unit.status, WaitingStatus) + @patch_network_get(private_address="1.1.1.1") def test_on_install_snap_failure(harness): - with patch("charm.PostgresqlOperatorCharm._install_snap_packages") as _install_snap_packages, patch( + with patch( + "charm.PostgresqlOperatorCharm._install_snap_packages" + ) as _install_snap_packages, patch( "charm.PostgresqlOperatorCharm._is_storage_attached", return_value=True ) as _is_storage_attached: harness.add_relation(PEER, harness.charm.app.name) @@ -128,7 +132,8 @@ def test_on_install_snap_failure(harness): harness.charm.on.install.emit() # Assert that the needed calls were made. _install_snap_packages.assert_called_once() - assert (isinstance(harness.model.unit.status, BlockedStatus)) + assert isinstance(harness.model.unit.status, BlockedStatus) + @patch_network_get(private_address="1.1.1.1") def test_patroni_scrape_config_no_tls(harness): @@ -144,6 +149,7 @@ def test_patroni_scrape_config_no_tls(harness): }, ] + @patch_network_get(private_address="1.1.1.1") def test_patroni_scrape_config_tls(harness): with patch( @@ -163,14 +169,13 @@ def test_patroni_scrape_config_tls(harness): }, ] + def test_primary_endpoint(harness): with patch( "charm.PostgresqlOperatorCharm._units_ips", new_callable=PropertyMock, return_value={"1.1.1.1", "1.1.1.2"}, - ), patch( - "charm.PostgresqlOperatorCharm._patroni", new_callable=PropertyMock - ) as _patroni: + ), patch("charm.PostgresqlOperatorCharm._patroni", new_callable=PropertyMock) as _patroni: harness.add_relation(PEER, harness.charm.app.name) _patroni.return_value.get_member_ip.return_value = "1.1.1.1" _patroni.return_value.get_primary.return_value = sentinel.primary @@ -179,6 +184,7 @@ def test_primary_endpoint(harness): _patroni.return_value.get_member_ip.assert_called_once_with(sentinel.primary) _patroni.return_value.get_primary.assert_called_once_with() + def test_primary_endpoint_no_peers(harness): with patch( "charm.PostgresqlOperatorCharm._peers", new_callable=PropertyMock, return_value=None @@ -186,15 +192,14 @@ def test_primary_endpoint_no_peers(harness): "charm.PostgresqlOperatorCharm._units_ips", new_callable=PropertyMock, return_value={"1.1.1.1", "1.1.1.2"}, - ), patch( - "charm.PostgresqlOperatorCharm._patroni", new_callable=PropertyMock - ) as _patroni: + ), patch("charm.PostgresqlOperatorCharm._patroni", new_callable=PropertyMock) as _patroni: harness.add_relation(PEER, harness.charm.app.name) assert harness.charm.primary_endpoint is None assert not _patroni.return_value.get_member_ip.called assert not _patroni.return_value.get_primary.called + @patch_network_get(private_address="1.1.1.1") def test_on_leader_elected(harness): with patch( @@ -202,9 +207,7 @@ def test_on_leader_elected(harness): ) as _update_relation_endpoints, patch( "charm.PostgresqlOperatorCharm.primary_endpoint", new_callable=PropertyMock, - ) as _primary_endpoint, patch( - "charm.PostgresqlOperatorCharm.update_config" - ) as _update_config: + ) as _primary_endpoint, patch("charm.PostgresqlOperatorCharm.update_config") as _update_config: harness.add_relation(PEER, harness.charm.app.name) # Assert that there is no password in the peer relation. assert harness.charm._peers.data[harness.charm.app].get("operator-password", None) is None @@ -224,7 +227,9 @@ def test_on_leader_elected(harness): # and also that update_endpoints was called after the cluster was initialised. harness.set_leader(False) harness.set_leader() - assert harness.charm._peers.data[harness.charm.app].get("operator-password", None) == password + assert ( + harness.charm._peers.data[harness.charm.app].get("operator-password", None) == password + ) _update_relation_endpoints.assert_called_once() assert not (isinstance(harness.model.unit.status, BlockedStatus)) @@ -233,7 +238,8 @@ def test_on_leader_elected(harness): harness.set_leader(False) harness.set_leader() _update_relation_endpoints.assert_called_once() # Assert it was not called again. - assert (isinstance(harness.model.unit.status, WaitingStatus)) + assert isinstance(harness.model.unit.status, WaitingStatus) + def test_is_cluster_initialised(harness): rel_id = harness.add_relation(PEER, harness.charm.app.name) @@ -245,7 +251,8 @@ def test_is_cluster_initialised(harness): harness.update_relation_data( rel_id, harness.charm.app.name, {"cluster_initialised": "True"} ) - assert (harness.charm.is_cluster_initialised) + assert harness.charm.is_cluster_initialised + def test_on_config_changed(harness): with patch( @@ -324,6 +331,7 @@ def test_on_config_changed(harness): _enable_disable_extensions.assert_called_once() _set_up_relation.assert_called_once() + def test_check_extension_dependencies(harness): with patch("subprocess.check_output", return_value=b"C"), patch.object( PostgresqlOperatorCharm, "postgresql", Mock() @@ -349,9 +357,10 @@ def test_check_extension_dependencies(harness): config["plugin_address_standardizer_enable"] = True harness.update_config(config) harness.charm.enable_disable_extensions() - assert (isinstance(harness.model.unit.status, BlockedStatus)) + assert isinstance(harness.model.unit.status, BlockedStatus) assert harness.model.unit.status.message == EXTENSIONS_DEPENDENCY_MESSAGE + def test_enable_disable_extensions(harness, caplog): with patch("subprocess.check_output", return_value=b"C"), patch.object( PostgresqlOperatorCharm, "postgresql", Mock() @@ -539,14 +548,19 @@ def test_enable_disable_extensions(harness, caplog): new_harness.charm.enable_disable_extensions() assert postgresql_mock.enable_disable_extensions.call_count == 1 + @patch_network_get(private_address="1.1.1.1") def test_on_start(harness): with ( - patch("charm.PostgresqlOperatorCharm.enable_disable_extensions") as _enable_disable_extensions, + patch( + "charm.PostgresqlOperatorCharm.enable_disable_extensions" + ) as _enable_disable_extensions, patch("charm.snap.SnapCache") as _snap_cache, patch("charm.Patroni.get_postgresql_version") as _get_postgresql_version, patch("charm.PostgreSQLProvider.oversee_users") as _oversee_users, - patch("charm.PostgresqlOperatorCharm._update_relation_endpoints", new_callable=PropertyMock) as _update_relation_endpoints, + patch( + "charm.PostgresqlOperatorCharm._update_relation_endpoints", new_callable=PropertyMock + ) as _update_relation_endpoints, patch("charm.PostgresqlOperatorCharm.postgresql") as _postgresql, patch("charm.PostgreSQLProvider.update_endpoints"), patch("charm.PostgresqlOperatorCharm.update_config"), @@ -557,7 +571,9 @@ def test_on_start(harness): patch("charm.Patroni.bootstrap_cluster") as _bootstrap_cluster, patch("charm.PostgresqlOperatorCharm._replication_password") as _replication_password, patch("charm.PostgresqlOperatorCharm._get_password") as _get_password, - patch("charm.PostgresqlOperatorCharm._reboot_on_detached_storage") as _reboot_on_detached_storage, + patch( + "charm.PostgresqlOperatorCharm._reboot_on_detached_storage" + ) as _reboot_on_detached_storage, patch("upgrade.PostgreSQLUpgrade.idle", return_value=True) as _idle, patch( "charm.PostgresqlOperatorCharm._is_storage_attached", @@ -576,7 +592,7 @@ def test_on_start(harness): _get_password.return_value = None harness.charm.on.start.emit() _bootstrap_cluster.assert_not_called() - assert (isinstance(harness.model.unit.status, WaitingStatus)) + assert isinstance(harness.model.unit.status, WaitingStatus) # Mock the passwords. _get_password.return_value = "fake-operator-password" @@ -593,7 +609,7 @@ def test_on_start(harness): harness.charm.on.start.emit() _bootstrap_cluster.assert_called_once() _oversee_users.assert_not_called() - assert (isinstance(harness.model.unit.status, BlockedStatus)) + assert isinstance(harness.model.unit.status, BlockedStatus) # Set an initial waiting status (like after the install hook was triggered). harness.model.unit.status = WaitingStatus("fake message") @@ -602,17 +618,18 @@ def test_on_start(harness): harness.charm.on.start.emit() _postgresql.create_user.assert_called_once() _oversee_users.assert_not_called() - assert (isinstance(harness.model.unit.status, BlockedStatus)) + assert isinstance(harness.model.unit.status, BlockedStatus) # Set an initial waiting status again (like after the install hook was triggered). harness.model.unit.status = WaitingStatus("fake message") # Then test the event of a correct cluster bootstrapping. harness.charm.on.start.emit() - assert _postgresql.create_user.call_count == 4 # Considering the previous failed call. + assert _postgresql.create_user.call_count == 4 # Considering the previous failed call. _oversee_users.assert_called_once() _enable_disable_extensions.assert_called_once() - assert (isinstance(harness.model.unit.status, ActiveStatus)) + assert isinstance(harness.model.unit.status, ActiveStatus) + @patch_network_get(private_address="1.1.1.1") def test_on_start_replica(harness): @@ -624,7 +641,9 @@ def test_on_start_replica(harness): "charm.Patroni.member_started", new_callable=PropertyMock, ) as _member_started, - patch("charm.PostgresqlOperatorCharm._update_relation_endpoints", new_callable=PropertyMock) as _update_relation_endpoints, + patch( + "charm.PostgresqlOperatorCharm._update_relation_endpoints", new_callable=PropertyMock + ) as _update_relation_endpoints, patch.object(EventBase, "defer") as _defer, patch("charm.PostgresqlOperatorCharm._replication_password") as _replication_password, patch("charm.PostgresqlOperatorCharm._get_password") as _get_password, @@ -657,7 +676,7 @@ def test_on_start_replica(harness): _member_started.return_value = True harness.charm.on.start.emit() _configure_patroni_on_unit.assert_not_called() - assert (isinstance(harness.model.unit.status, ActiveStatus)) + assert isinstance(harness.model.unit.status, ActiveStatus) # Set an initial waiting status (like after the install hook was triggered). harness.model.unit.status = WaitingStatus("fake message") @@ -667,7 +686,8 @@ def test_on_start_replica(harness): _member_started.return_value = False harness.charm.on.start.emit() _configure_patroni_on_unit.assert_called_once() - assert (isinstance(harness.model.unit.status, WaitingStatus)) + assert isinstance(harness.model.unit.status, WaitingStatus) + @patch_network_get(private_address="1.1.1.1") def test_on_start_no_patroni_member(harness): @@ -678,7 +698,9 @@ def test_on_start_no_patroni_member(harness): patch("charm.Patroni") as patroni, patch("charm.PostgresqlOperatorCharm._get_password") as _get_password, patch("upgrade.PostgreSQLUpgrade.idle", return_value=True) as _idle, - patch("charm.PostgresqlOperatorCharm._is_storage_attached", return_value=True) as _is_storage_attached, + patch( + "charm.PostgresqlOperatorCharm._is_storage_attached", return_value=True + ) as _is_storage_attached, ): harness.add_relation(PEER, harness.charm.app.name) # Mock the passwords. @@ -693,15 +715,18 @@ def test_on_start_no_patroni_member(harness): harness.charm.on.start.emit() bootstrap_cluster.assert_called_once() _postgresql.create_user.assert_not_called() - assert (isinstance(harness.model.unit.status, WaitingStatus)) + assert isinstance(harness.model.unit.status, WaitingStatus) assert harness.model.unit.status.message == "awaiting for member to start" + def test_on_start_after_blocked_state(harness): with ( patch("charm.Patroni.bootstrap_cluster") as _bootstrap_cluster, patch("charm.PostgresqlOperatorCharm._replication_password") as _replication_password, patch("charm.PostgresqlOperatorCharm._get_password") as _get_password, - patch("charm.PostgresqlOperatorCharm._is_storage_attached", return_value=True) as _is_storage_attached, + patch( + "charm.PostgresqlOperatorCharm._is_storage_attached", return_value=True + ) as _is_storage_attached, ): harness.add_relation(PEER, harness.charm.app.name) # Set an initial blocked status (like after the install hook was triggered). @@ -716,6 +741,7 @@ def test_on_start_after_blocked_state(harness): # Assert the status didn't change. assert harness.model.unit.status == initial_status + @patch_network_get(private_address="1.1.1.1") def test_on_get_password(harness): with patch("charm.PostgresqlOperatorCharm.update_config"): @@ -750,6 +776,7 @@ def test_on_get_password(harness): harness.charm._on_get_password(mock_event) mock_event.set_results.assert_called_once_with({"password": "replication-test-password"}) + @patch_network_get(private_address="1.1.1.1") def test_on_set_password(harness): with ( @@ -813,17 +840,24 @@ def test_on_set_password(harness): "app", "replication-password", "replication-test-password" ) + @patch_network_get(private_address="1.1.1.1") def test_on_update_status(harness): with ( patch("charm.ClusterTopologyObserver.start_observer") as _start_observer, - patch("charm.PostgresqlOperatorCharm._set_primary_status_message") as _set_primary_status_message, + patch( + "charm.PostgresqlOperatorCharm._set_primary_status_message" + ) as _set_primary_status_message, patch("charm.Patroni.restart_patroni") as _restart_patroni, patch("charm.Patroni.is_member_isolated") as _is_member_isolated, patch("charm.Patroni.reinitialize_postgresql") as _reinitialize_postgresql, - patch("charm.Patroni.member_replication_lag", new_callable=PropertyMock) as _member_replication_lag, + patch( + "charm.Patroni.member_replication_lag", new_callable=PropertyMock + ) as _member_replication_lag, patch("charm.Patroni.member_started", new_callable=PropertyMock) as _member_started, - patch("charm.PostgresqlOperatorCharm._update_relation_endpoints") as _update_relation_endpoints, + patch( + "charm.PostgresqlOperatorCharm._update_relation_endpoints" + ) as _update_relation_endpoints, patch( "charm.PostgresqlOperatorCharm.primary_endpoint", new_callable=PropertyMock(return_value=True), @@ -875,19 +909,28 @@ def test_on_update_status(harness): _restart_patroni.assert_called_once() _start_observer.assert_called_once() + @patch_network_get(private_address="1.1.1.1") def test_on_update_status_after_restore_operation(harness): with ( patch("charm.ClusterTopologyObserver.start_observer"), - patch("charm.PostgresqlOperatorCharm._set_primary_status_message") as _set_primary_status_message, - patch("charm.PostgresqlOperatorCharm._handle_workload_failures") as _handle_workload_failures, - patch("charm.PostgresqlOperatorCharm._update_relation_endpoints") as _update_relation_endpoints, + patch( + "charm.PostgresqlOperatorCharm._set_primary_status_message" + ) as _set_primary_status_message, + patch( + "charm.PostgresqlOperatorCharm._handle_workload_failures" + ) as _handle_workload_failures, + patch( + "charm.PostgresqlOperatorCharm._update_relation_endpoints" + ) as _update_relation_endpoints, patch( "charm.PostgresqlOperatorCharm.primary_endpoint", new_callable=PropertyMock(return_value=True), ) as _primary_endpoint, patch("charm.PostgreSQLProvider.oversee_users") as _oversee_users, - patch("charm.PostgresqlOperatorCharm._handle_processes_failures") as _handle_processes_failures, + patch( + "charm.PostgresqlOperatorCharm._handle_processes_failures" + ) as _handle_processes_failures, patch("charm.PostgreSQLBackups.can_use_s3_repository") as _can_use_s3_repository, patch("charm.PostgresqlOperatorCharm.update_config") as _update_config, patch("charm.Patroni.member_started", new_callable=PropertyMock) as _member_started, @@ -927,7 +970,10 @@ def test_on_update_status_after_restore_operation(harness): assert isinstance(harness.charm.unit.status, ActiveStatus) # Assert that the backup id is still in the application relation databag. - assert harness.get_relation_data(rel_id, harness.charm.app) == {"cluster_initialised": "True", "restoring-backup": "2023-01-01T09:00:00Z"} + assert harness.get_relation_data(rel_id, harness.charm.app) == { + "cluster_initialised": "True", + "restoring-backup": "2023-01-01T09:00:00Z", + } # Test when the restore operation finished successfully. _member_started.return_value = True @@ -944,7 +990,9 @@ def test_on_update_status_after_restore_operation(harness): assert isinstance(harness.charm.unit.status, ActiveStatus) # Assert that the backup id is not in the application relation databag anymore. - assert harness.get_relation_data(rel_id, harness.charm.app) == {"cluster_initialised": "True"} + assert harness.get_relation_data(rel_id, harness.charm.app) == { + "cluster_initialised": "True" + } # Test when it's not possible to use the configured S3 repository. _update_config.reset_mock() @@ -971,7 +1019,10 @@ def test_on_update_status_after_restore_operation(harness): assert harness.charm.unit.status.message == "fake validation message" # Assert that the backup id is not in the application relation databag anymore. - assert harness.get_relation_data(rel_id, harness.charm.app) == {"cluster_initialised": "True"} + assert harness.get_relation_data(rel_id, harness.charm.app) == { + "cluster_initialised": "True" + } + def test_install_snap_packages(harness): with patch("charm.snap.SnapCache") as _snap_cache: @@ -1071,7 +1122,7 @@ def test_is_storage_attached(harness): # Test with attached storage. is_storage_attached = harness.charm._is_storage_attached() _check_call.assert_called_once_with(["mountpoint", "-q", harness.charm._storage_path]) - assert (is_storage_attached) + assert is_storage_attached # Test with detached storage. is_storage_attached = harness.charm._is_storage_attached() @@ -1084,9 +1135,10 @@ def test_reboot_on_detached_storage(harness): mock_event = MagicMock() harness.charm._reboot_on_detached_storage(mock_event) mock_event.defer.assert_called_once() - assert (isinstance(harness.charm.unit.status, WaitingStatus)) + assert isinstance(harness.charm.unit.status, WaitingStatus) _check_call.assert_called_once_with(["systemctl", "reboot"]) + @patch_network_get(private_address="1.1.1.1") def test_restart(harness): with ( @@ -1111,20 +1163,27 @@ def test_restart(harness): # Test a failed restart. _restart_postgresql.side_effect = RetryError(last_attempt=1) harness.charm._restart(mock_event) - assert (isinstance(harness.charm.unit.status, BlockedStatus)) + assert isinstance(harness.charm.unit.status, BlockedStatus) mock_event.defer.assert_not_called() + @patch_network_get(private_address="1.1.1.1") def test_update_config(harness): with ( patch("subprocess.check_output", return_value=b"C"), patch("charm.snap.SnapCache"), - patch("charm.PostgresqlOperatorCharm._handle_postgresql_restart_need") as _handle_postgresql_restart_need, + patch( + "charm.PostgresqlOperatorCharm._handle_postgresql_restart_need" + ) as _handle_postgresql_restart_need, patch("charm.Patroni.bulk_update_parameters_controller_by_patroni"), patch("charm.Patroni.member_started", new_callable=PropertyMock) as _member_started, - patch("charm.PostgresqlOperatorCharm._is_workload_running", new_callable=PropertyMock) as _is_workload_running, + patch( + "charm.PostgresqlOperatorCharm._is_workload_running", new_callable=PropertyMock + ) as _is_workload_running, patch("charm.Patroni.render_patroni_yml_file") as _render_patroni_yml_file, - patch("charm.PostgresqlOperatorCharm.is_tls_enabled", new_callable=PropertyMock) as _is_tls_enabled, + patch( + "charm.PostgresqlOperatorCharm.is_tls_enabled", new_callable=PropertyMock + ) as _is_tls_enabled, patch.object(PostgresqlOperatorCharm, "postgresql", Mock()) as postgresql_mock, ): rel_id = harness.add_relation(PEER, harness.charm.app.name) @@ -1139,9 +1198,7 @@ def test_update_config(harness): harness.charm.update_config() # Test when only one of the two config options for profile limit memory is set. - harness.update_config( - {"profile_limit_memory": 1000}, unset={"profile-limit-memory"} - ) + harness.update_config({"profile_limit_memory": 1000}, unset={"profile-limit-memory"}) harness.charm.update_config() # Test when the two config options for profile limit memory are set at the same time. @@ -1186,7 +1243,9 @@ def test_update_config(harness): parameters={"test": "test"}, ) _handle_postgresql_restart_need.assert_called_once() - assert "tls" not in harness.get_relation_data(rel_id, harness.charm.unit.name) # The "tls" flag is set in handle_postgresql_restart_need. + assert "tls" not in harness.get_relation_data( + rel_id, harness.charm.unit.name + ) # The "tls" flag is set in handle_postgresql_restart_need. # Test with workload not running yet. harness.update_relation_data( @@ -1205,10 +1264,15 @@ def test_update_config(harness): _handle_postgresql_restart_need.assert_not_called() assert "tls" not in harness.get_relation_data(rel_id, harness.charm.unit.name) + def test_on_cluster_topology_change(harness): with ( - patch("charm.PostgresqlOperatorCharm._update_relation_endpoints") as _update_relation_endpoints, - patch("charm.PostgresqlOperatorCharm.primary_endpoint", new_callable=PropertyMock) as _primary_endpoint, + patch( + "charm.PostgresqlOperatorCharm._update_relation_endpoints" + ) as _update_relation_endpoints, + patch( + "charm.PostgresqlOperatorCharm.primary_endpoint", new_callable=PropertyMock + ) as _primary_endpoint, ): harness.add_relation(PEER, harness.charm.app.name) # Mock the property value. @@ -1222,6 +1286,7 @@ def test_on_cluster_topology_change(harness): harness.charm._on_cluster_topology_change(Mock()) _update_relation_endpoints.assert_called_once() + def test_on_cluster_topology_change_keep_blocked(harness): with ( patch( @@ -1229,7 +1294,9 @@ def test_on_cluster_topology_change_keep_blocked(harness): new_callable=PropertyMock, return_value=None, ) as _primary_endpoint, - patch("charm.PostgresqlOperatorCharm._update_relation_endpoints") as _update_relation_endpoints, + patch( + "charm.PostgresqlOperatorCharm._update_relation_endpoints" + ) as _update_relation_endpoints, ): harness.add_relation(PEER, harness.charm.app.name) harness.model.unit.status = WaitingStatus(PRIMARY_NOT_REACHABLE_MESSAGE) @@ -1241,6 +1308,7 @@ def test_on_cluster_topology_change_keep_blocked(harness): assert isinstance(harness.model.unit.status, WaitingStatus) assert harness.model.unit.status.message == PRIMARY_NOT_REACHABLE_MESSAGE + def test_on_cluster_topology_change_clear_blocked(harness): with ( patch( @@ -1248,7 +1316,9 @@ def test_on_cluster_topology_change_clear_blocked(harness): new_callable=PropertyMock, return_value="fake-unit", ) as _primary_endpoint, - patch("charm.PostgresqlOperatorCharm._update_relation_endpoints") as _update_relation_endpoints, + patch( + "charm.PostgresqlOperatorCharm._update_relation_endpoints" + ) as _update_relation_endpoints, ): harness.add_relation(PEER, harness.charm.app.name) harness.model.unit.status = WaitingStatus(PRIMARY_NOT_REACHABLE_MESSAGE) @@ -1257,7 +1327,8 @@ def test_on_cluster_topology_change_clear_blocked(harness): _update_relation_endpoints.assert_called_once_with() _primary_endpoint.assert_called_once_with() - assert (isinstance(harness.model.unit.status, ActiveStatus)) + assert isinstance(harness.model.unit.status, ActiveStatus) + def test_validate_config_options(harness): with ( @@ -1306,17 +1377,26 @@ def test_validate_config_options(harness): _charm_lib.return_value.get_postgresql_timezones.assert_called_once_with() _charm_lib.return_value.get_postgresql_timezones.return_value = ["TEST_ZONE"] + @patch_network_get(private_address="1.1.1.1") def test_on_peer_relation_changed(harness): with ( patch("charm.snap.SnapCache"), - patch("charm.PostgresqlOperatorCharm._update_relation_endpoints") as _update_relation_endpoints, - patch("charm.PostgresqlOperatorCharm.primary_endpoint", new_callable=PropertyMock) as _primary_endpoint, + patch( + "charm.PostgresqlOperatorCharm._update_relation_endpoints" + ) as _update_relation_endpoints, + patch( + "charm.PostgresqlOperatorCharm.primary_endpoint", new_callable=PropertyMock + ) as _primary_endpoint, patch("backups.PostgreSQLBackups.check_stanza") as _check_stanza, patch("backups.PostgreSQLBackups.coordinate_stanza_fields") as _coordinate_stanza_fields, - patch("backups.PostgreSQLBackups.start_stop_pgbackrest_service") as _start_stop_pgbackrest_service, + patch( + "backups.PostgreSQLBackups.start_stop_pgbackrest_service" + ) as _start_stop_pgbackrest_service, patch("charm.Patroni.reinitialize_postgresql") as _reinitialize_postgresql, - patch("charm.Patroni.member_replication_lag", new_callable=PropertyMock) as _member_replication_lag, + patch( + "charm.Patroni.member_replication_lag", new_callable=PropertyMock + ) as _member_replication_lag, patch("charm.PostgresqlOperatorCharm.is_primary") as _is_primary, patch("charm.Patroni.member_started", new_callable=PropertyMock) as _member_started, patch("charm.Patroni.start_patroni") as _start_patroni, @@ -1441,11 +1521,14 @@ def test_on_peer_relation_changed(harness): _coordinate_stanza_fields.assert_called_once() _check_stanza.assert_called_once() + @patch_network_get(private_address="1.1.1.1") def test_reconfigure_cluster(harness): with ( patch("charm.PostgresqlOperatorCharm._add_members") as _add_members, - patch("charm.PostgresqlOperatorCharm._remove_from_members_ips") as _remove_from_members_ips, + patch( + "charm.PostgresqlOperatorCharm._remove_from_members_ips" + ) as _remove_from_members_ips, patch("charm.Patroni.remove_raft_member") as _remove_raft_member, ): rel_id = harness.add_relation(PEER, harness.charm.app.name) @@ -1453,7 +1536,7 @@ def test_reconfigure_cluster(harness): mock_event = Mock() mock_event.unit = harness.charm.unit mock_event.relation.data = {mock_event.unit: {}} - assert (harness.charm._reconfigure_cluster(mock_event)) + assert harness.charm._reconfigure_cluster(mock_event) _remove_raft_member.assert_not_called() _remove_from_members_ips.assert_not_called() _add_members.assert_called_once_with(mock_event) @@ -1475,7 +1558,7 @@ def test_reconfigure_cluster(harness): _remove_raft_member.side_effect = None _add_members.reset_mock() mock_event.relation.data = relation_data - assert (harness.charm._reconfigure_cluster(mock_event)) + assert harness.charm._reconfigure_cluster(mock_event) _remove_raft_member.assert_called_once_with(ip_to_remove) _remove_from_members_ips.assert_not_called() _add_members.assert_called_once_with(mock_event) @@ -1488,14 +1571,17 @@ def test_reconfigure_cluster(harness): harness.update_relation_data( rel_id, harness.charm.app.name, {"members_ips": '["' + ip_to_remove + '"]'} ) - assert (harness.charm._reconfigure_cluster(mock_event)) + assert harness.charm._reconfigure_cluster(mock_event) _remove_raft_member.assert_called_once_with(ip_to_remove) _remove_from_members_ips.assert_called_once_with(ip_to_remove) _add_members.assert_called_once_with(mock_event) + def test_update_certificate(harness): with ( - patch("charms.postgresql_k8s.v0.postgresql_tls.PostgreSQLTLS._request_certificate") as _request_certificate, + patch( + "charms.postgresql_k8s.v0.postgresql_tls.PostgreSQLTLS._request_certificate" + ) as _request_certificate, patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=False), ): rel_id = harness.add_relation(PEER, harness.charm.app.name) @@ -1527,9 +1613,12 @@ def test_update_certificate(harness): assert harness.charm.get_secret("unit", "key") == key assert harness.charm.get_secret("unit", "private-key") == private_key + def test_update_certificate_secrets(harness): with ( - patch("charms.postgresql_k8s.v0.postgresql_tls.PostgreSQLTLS._request_certificate") as _request_certificate, + patch( + "charms.postgresql_k8s.v0.postgresql_tls.PostgreSQLTLS._request_certificate" + ) as _request_certificate, patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=True), ): harness.add_relation(PEER, harness.charm.app.name) @@ -1555,6 +1644,7 @@ def test_update_certificate_secrets(harness): assert harness.charm.get_secret("unit", "key") == key assert harness.charm.get_secret("unit", "private-key") == private_key + @patch_network_get(private_address="1.1.1.1") def test_update_member_ip(harness): with ( @@ -1586,19 +1676,22 @@ def test_update_member_ip(harness): "ip": "2.2.2.2", }, ) - assert (harness.charm._update_member_ip()) + assert harness.charm._update_member_ip() relation_data = harness.get_relation_data(rel_id, harness.charm.unit.name) assert relation_data.get("ip") == "1.1.1.1" assert relation_data.get("ip-to-remove") == "2.2.2.2" _stop_patroni.assert_called_once() _update_certificate.assert_called_once() + @patch_network_get(private_address="1.1.1.1") def test_push_tls_files_to_workload(harness): with ( patch("charm.PostgresqlOperatorCharm.update_config") as _update_config, patch("charm.Patroni.render_file") as _render_file, - patch("charms.postgresql_k8s.v0.postgresql_tls.PostgreSQLTLS.get_tls_files") as _get_tls_files, + patch( + "charms.postgresql_k8s.v0.postgresql_tls.PostgreSQLTLS.get_tls_files" + ) as _get_tls_files, ): harness.add_relation(PEER, harness.charm.app.name) _get_tls_files.side_effect = [ @@ -1610,7 +1703,7 @@ def test_push_tls_files_to_workload(harness): _update_config.side_effect = [True, False, False, False] # Test when all TLS files are available. - assert (harness.charm.push_tls_files_to_workload()) + assert harness.charm.push_tls_files_to_workload() assert _render_file.call_count == 3 # Test when not all TLS files are available. @@ -1629,7 +1722,8 @@ def test_is_workload_running(harness): assert not (harness.charm._is_workload_running) pg_snap.present = True - assert (harness.charm._is_workload_running) + assert harness.charm._is_workload_running + def test_get_available_memory(harness): harness.add_relation(PEER, harness.charm.app.name) @@ -1649,6 +1743,7 @@ def test_get_available_memory(harness): with patch("builtins.open", mock_open(read_data="")): assert harness.charm.get_available_memory() == 0 + def test_juju_run_exec_divergence(harness): with ( patch("charm.ClusterTopologyObserver") as _topology_observer, @@ -1668,6 +1763,7 @@ def test_juju_run_exec_divergence(harness): harness.begin() _topology_observer.assert_called_once_with(harness.charm, "/usr/bin/juju-exec") + def test_client_relations(harness): harness.add_relation(PEER, harness.charm.app.name) # Test when the charm has no relations. @@ -1682,16 +1778,19 @@ def test_client_relations(harness): db_admin_relation = harness.model.get_relation("db-admin") assert harness.charm.client_relations == [database_relation, db_relation, db_admin_relation] + # # Secrets # + def test_scope_obj(harness): harness.add_relation(PEER, harness.charm.app.name) assert harness.charm._scope_obj("app") == harness.charm.framework.model.app assert harness.charm._scope_obj("unit") == harness.charm.framework.model.unit assert harness.charm._scope_obj("test") is None + @patch_network_get(private_address="1.1.1.1") def test_get_secret(harness): with patch("charm.PostgresqlOperatorCharm._on_leader_elected"): @@ -1700,9 +1799,7 @@ def test_get_secret(harness): harness.set_leader() # Test application scope. assert harness.charm.get_secret("app", "password") is None - harness.update_relation_data( - rel_id, harness.charm.app.name, {"password": "test-password"} - ) + harness.update_relation_data(rel_id, harness.charm.app.name, {"password": "test-password"}) assert harness.charm.get_secret("app", "password") == "test-password" # Unit level changes don't require leader privileges @@ -1714,6 +1811,7 @@ def test_get_secret(harness): ) assert harness.charm.get_secret("unit", "password") == "test-password" + @patch_network_get(private_address="1.1.1.1") def test_on_get_password_secrets(harness): with ( @@ -1745,6 +1843,7 @@ def test_on_get_password_secrets(harness): harness.charm._on_get_password(mock_event) mock_event.set_results.assert_called_once_with({"password": "replication-test-password"}) + @pytest.mark.parametrize("scope", [("app"), ("unit")]) @patch_network_get(private_address="1.1.1.1") def test_get_secret_secrets(harness, scope): @@ -1759,6 +1858,7 @@ def test_get_secret_secrets(harness, scope): harness.charm.set_secret(scope, "operator-password", "test-password") assert harness.charm.get_secret(scope, "operator-password") == "test-password" + @patch_network_get(private_address="1.1.1.1") def test_set_secret(harness): with patch("charm.PostgresqlOperatorCharm._on_leader_elected"): @@ -1788,6 +1888,7 @@ def test_set_secret(harness): with pytest.raises(RuntimeError): harness.charm.set_secret("test", "password", "test") + @pytest.mark.parametrize("scope,is_leader", [("app", True), ("unit", True), ("unit", False)]) @patch_network_get(private_address="1.1.1.1") def test_set_reset_new_secret(harness, scope, is_leader): @@ -1811,6 +1912,7 @@ def test_set_reset_new_secret(harness, scope, is_leader): harness.charm.set_secret(scope, "new-secret2", "blablabla") assert harness.charm.get_secret(scope, "new-secret2") == "blablabla" + @pytest.mark.parametrize("scope,is_leader", [("app", True), ("unit", True), ("unit", False)]) @patch_network_get(private_address="1.1.1.1") def test_invalid_secret(harness, scope, is_leader): @@ -1828,31 +1930,26 @@ def test_invalid_secret(harness, scope, is_leader): harness.charm.set_secret(scope, "somekey", "") assert harness.charm.get_secret(scope, "somekey") is None + @patch_network_get(private_address="1.1.1.1") def test_delete_password(harness, caplog): with patch("charm.PostgresqlOperatorCharm._on_leader_elected"): rel_id = harness.add_relation(PEER, harness.charm.app.name) """NOTE: currently ops.testing seems to allow for non-leader to remove secrets too!""" harness.set_leader(True) - harness.update_relation_data( - rel_id, harness.charm.app.name, {"replication": "somepw"} - ) + harness.update_relation_data(rel_id, harness.charm.app.name, {"replication": "somepw"}) harness.charm.remove_secret("app", "replication") assert harness.charm.get_secret("app", "replication") is None harness.set_leader(False) - harness.update_relation_data( - rel_id, harness.charm.unit.name, {"somekey": "somevalue"} - ) + harness.update_relation_data(rel_id, harness.charm.unit.name, {"somekey": "somevalue"}) harness.charm.remove_secret("unit", "somekey") assert harness.charm.get_secret("unit", "somekey") is None harness.set_leader(True) with caplog.at_level(logging.ERROR): harness.charm.remove_secret("app", "replication") - assert ( - "Non-existing field 'replication' was attempted to be removed" in caplog.text - ) + assert "Non-existing field 'replication' was attempted to be removed" in caplog.text harness.charm.remove_secret("unit", "somekey") assert "Non-existing field 'somekey' was attempted to be removed" in caplog.text @@ -1869,6 +1966,7 @@ def test_delete_password(harness, caplog): in caplog.text ) + @patch_network_get(private_address="1.1.1.1") def test_delete_existing_password_secrets(harness, caplog): with ( @@ -1891,14 +1989,12 @@ def test_delete_existing_password_secrets(harness, caplog): with caplog.at_level(logging.ERROR): harness.charm.remove_secret("app", "operator-password") assert ( - "Non-existing secret operator-password was attempted to be removed." - in caplog.text + "Non-existing secret operator-password was attempted to be removed." in caplog.text ) harness.charm.remove_secret("unit", "operator-password") assert ( - "Non-existing secret operator-password was attempted to be removed." - in caplog.text + "Non-existing secret operator-password was attempted to be removed." in caplog.text ) harness.charm.remove_secret("app", "non-existing-secret") @@ -1913,6 +2009,7 @@ def test_delete_existing_password_secrets(harness, caplog): in caplog.text ) + @pytest.mark.parametrize("scope,is_leader", [("app", True), ("unit", True), ("unit", False)]) @patch_network_get(private_address="1.1.1.1") def test_migration_from_databag(harness, scope, is_leader): @@ -1938,6 +2035,7 @@ def test_migration_from_databag(harness, scope, is_leader): rel_id, getattr(harness.charm, scope).name ) + @pytest.mark.parametrize("scope,is_leader", [("app", True), ("unit", True), ("unit", False)]) @patch_network_get(private_address="1.1.1.1") def test_migration_from_single_secret(harness, scope, is_leader): @@ -1954,9 +2052,7 @@ def test_migration_from_single_secret(harness, scope, is_leader): # Getting current password entity = getattr(harness.charm, scope) - harness.update_relation_data( - rel_id, entity.name, {SECRET_INTERNAL_LABEL: secret.id} - ) + harness.update_relation_data(rel_id, entity.name, {SECRET_INTERNAL_LABEL: secret.id}) assert harness.charm.get_secret(scope, "operator-password") == "bla" # Reset new secret @@ -1972,13 +2068,16 @@ def test_migration_from_single_secret(harness, scope, is_leader): rel_id, getattr(harness.charm, scope).name ) + def test_handle_postgresql_restart_need(harness): with ( patch("charms.rolling_ops.v0.rollingops.RollingOpsManager._on_acquire_lock") as _restart, patch("charm.wait_fixed", return_value=wait_fixed(0)), patch("charm.Patroni.reload_patroni_configuration") as _reload_patroni_configuration, patch("charm.PostgresqlOperatorCharm._unit_ip"), - patch("charm.PostgresqlOperatorCharm.is_tls_enabled", new_callable=PropertyMock) as _is_tls_enabled, + patch( + "charm.PostgresqlOperatorCharm.is_tls_enabled", new_callable=PropertyMock + ) as _is_tls_enabled, patch.object(PostgresqlOperatorCharm, "postgresql", Mock()) as postgresql_mock, ): rel_id = harness.add_relation(PEER, harness.charm.app.name) @@ -1988,9 +2087,7 @@ def test_handle_postgresql_restart_need(harness): _reload_patroni_configuration.reset_mock() _restart.reset_mock() with harness.hooks_disabled(): - harness.update_relation_data( - rel_id, harness.charm.unit.name, {"tls": ""} - ) + harness.update_relation_data(rel_id, harness.charm.unit.name, {"tls": ""}) harness.update_relation_data( rel_id, harness.charm.unit.name, @@ -2009,24 +2106,39 @@ def test_handle_postgresql_restart_need(harness): assert "tls" not in harness.get_relation_data(rel_id, harness.charm.unit) if (values[1] != values[2]) or values[3]: - assert "postgresql_restarted" not in harness.get_relation_data(rel_id, harness.charm.unit) + assert "postgresql_restarted" not in harness.get_relation_data( + rel_id, harness.charm.unit + ) _restart.assert_called_once() else: if values[4]: - assert "postgresql_restarted" in harness.get_relation_data(rel_id, harness.charm.unit) + assert "postgresql_restarted" in harness.get_relation_data( + rel_id, harness.charm.unit + ) else: - assert "postgresql_restarted" not in harness.get_relation_data(rel_id, harness.charm.unit) + assert "postgresql_restarted" not in harness.get_relation_data( + rel_id, harness.charm.unit + ) _restart.assert_not_called() + def test_on_peer_relation_departed(harness): with ( - patch("charm.PostgresqlOperatorCharm._update_relation_endpoints") as _update_relation_endpoints, - patch("charm.PostgresqlOperatorCharm.primary_endpoint", new_callable=PropertyMock) as _primary_endpoint, + patch( + "charm.PostgresqlOperatorCharm._update_relation_endpoints" + ) as _update_relation_endpoints, + patch( + "charm.PostgresqlOperatorCharm.primary_endpoint", new_callable=PropertyMock + ) as _primary_endpoint, patch("charm.PostgresqlOperatorCharm.update_config") as _update_config, - patch("charm.PostgresqlOperatorCharm._remove_from_members_ips") as _remove_from_members_ips, + patch( + "charm.PostgresqlOperatorCharm._remove_from_members_ips" + ) as _remove_from_members_ips, patch("charm.Patroni.are_all_members_ready") as _are_all_members_ready, patch("charm.PostgresqlOperatorCharm._get_ips_to_remove") as _get_ips_to_remove, - patch("charm.PostgresqlOperatorCharm._updated_synchronous_node_count") as _updated_synchronous_node_count, + patch( + "charm.PostgresqlOperatorCharm._updated_synchronous_node_count" + ) as _updated_synchronous_node_count, patch("charm.Patroni.remove_raft_member") as _remove_raft_member, patch("charm.PostgresqlOperatorCharm._unit_ip") as _unit_ip, patch("charm.Patroni.get_member_ip") as _get_member_ip, @@ -2195,10 +2307,15 @@ def test_on_peer_relation_departed(harness): _update_relation_endpoints.assert_not_called() assert isinstance(harness.charm.unit.status, WaitingStatus) + def test_update_new_unit_status(harness): - with( - patch("charm.PostgresqlOperatorCharm._update_relation_endpoints") as _update_relation_endpoints, - patch("charm.PostgresqlOperatorCharm.primary_endpoint", new_callable=PropertyMock) as _primary_endpoint, + with ( + patch( + "charm.PostgresqlOperatorCharm._update_relation_endpoints" + ) as _update_relation_endpoints, + patch( + "charm.PostgresqlOperatorCharm.primary_endpoint", new_callable=PropertyMock + ) as _primary_endpoint, ): harness.add_relation(PEER, harness.charm.app.name) # Test when the charm is blocked. From 37f6b763ea6186781e5cdbfe79907fa41f240251 Mon Sep 17 00:00:00 2001 From: lucasgameiroborges Date: Thu, 4 Apr 2024 20:16:51 +0000 Subject: [PATCH 07/10] add secrets pytest fixture parameter --- tests/unit/test_charm.py | 135 +++++++++++---------------------------- 1 file changed, 37 insertions(+), 98 deletions(-) diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py index 2d8ea89594..45d5d3ea2b 100644 --- a/tests/unit/test_charm.py +++ b/tests/unit/test_charm.py @@ -38,10 +38,7 @@ CREATE_CLUSTER_CONF_PATH = "/etc/postgresql-common/createcluster.d/pgcharm.conf" -# @pytest.mark.usefixtures("juju_has_secrets") - - -@pytest.fixture +@pytest.fixture(autouse=True) def harness(): harness = Harness(PostgresqlOperatorCharm) harness.begin() @@ -50,6 +47,14 @@ def harness(): harness.cleanup() +# This causes every test defined in this file will run 2 times, with +# the patch of charm.JujuVersion.has_secrets set as True and as False +@pytest.fixture(params=[True, False], autouse=True) +def _has_secrets(request, monkeypatch): + monkeypatch.setattr("charm.JujuVersion.has_secrets", PropertyMock(return_value=request.param)) + return request.param + + @patch_network_get(private_address="1.1.1.1") def test_on_install(harness): with patch("charm.subprocess.check_call") as _check_call, patch( @@ -215,7 +220,7 @@ def test_on_leader_elected(harness): # Check that a new password was generated on leader election. _primary_endpoint.return_value = "1.1.1.1" harness.set_leader() - password = harness.charm._peers.data[harness.charm.app].get("operator-password", None) + password = harness.charm.get_secret("app", "operator-password") _update_config.assert_called_once() _update_relation_endpoints.assert_not_called() assert password is not None @@ -227,9 +232,7 @@ def test_on_leader_elected(harness): # and also that update_endpoints was called after the cluster was initialised. harness.set_leader(False) harness.set_leader() - assert ( - harness.charm._peers.data[harness.charm.app].get("operator-password", None) == password - ) + assert harness.charm.get_secret("app", "operator-password") == password _update_relation_endpoints.assert_called_once() assert not (isinstance(harness.model.unit.status, BlockedStatus)) @@ -1577,49 +1580,11 @@ def test_reconfigure_cluster(harness): _add_members.assert_called_once_with(mock_event) -def test_update_certificate(harness): - with ( - patch( - "charms.postgresql_k8s.v0.postgresql_tls.PostgreSQLTLS._request_certificate" - ) as _request_certificate, - patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=False), - ): - rel_id = harness.add_relation(PEER, harness.charm.app.name) - # If there is no current TLS files, _request_certificate should be called - # only when the certificates relation is established. - harness.charm._update_certificate() - _request_certificate.assert_not_called() - - # Test with already present TLS files (when they will be replaced by new ones). - ca = "fake CA" - cert = "fake certificate" - key = private_key = "fake private key" - with harness.hooks_disabled(): - harness.update_relation_data( - rel_id, - harness.charm.unit.name, - { - "ca": ca, - "cert": cert, - "key": key, - "private-key": private_key, - }, - ) - harness.charm._update_certificate() - _request_certificate.assert_called_once_with(private_key) - - assert harness.charm.get_secret("unit", "ca") == ca - assert harness.charm.get_secret("unit", "cert") == cert - assert harness.charm.get_secret("unit", "key") == key - assert harness.charm.get_secret("unit", "private-key") == private_key - - def test_update_certificate_secrets(harness): with ( patch( "charms.postgresql_k8s.v0.postgresql_tls.PostgreSQLTLS._request_certificate" ) as _request_certificate, - patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=True), ): harness.add_relation(PEER, harness.charm.app.name) # If there is no current TLS files, _request_certificate should be called @@ -1816,7 +1781,6 @@ def test_get_secret(harness): def test_on_get_password_secrets(harness): with ( patch("charm.PostgresqlOperatorCharm._on_leader_elected"), - patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=True), ): harness.add_relation(PEER, harness.charm.app.name) # Create a mock event and set passwords in peer relation data. @@ -1849,7 +1813,6 @@ def test_on_get_password_secrets(harness): def test_get_secret_secrets(harness, scope): with ( patch("charm.PostgresqlOperatorCharm._on_leader_elected"), - patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=True), ): harness.add_relation(PEER, harness.charm.app.name) harness.set_leader() @@ -1894,7 +1857,6 @@ def test_set_secret(harness): def test_set_reset_new_secret(harness, scope, is_leader): with ( patch("charm.PostgresqlOperatorCharm._on_leader_elected"), - patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=True), ): harness.add_relation(PEER, harness.charm.app.name) """NOTE: currently ops.testing seems to allow for non-leader to set secrets too!""" @@ -1918,7 +1880,6 @@ def test_set_reset_new_secret(harness, scope, is_leader): def test_invalid_secret(harness, scope, is_leader): with ( patch("charm.PostgresqlOperatorCharm._on_leader_elected"), - patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=True), ): harness.add_relation(PEER, harness.charm.app.name) # App has to be leader, unit can be either @@ -1932,46 +1893,9 @@ def test_invalid_secret(harness, scope, is_leader): @patch_network_get(private_address="1.1.1.1") -def test_delete_password(harness, caplog): - with patch("charm.PostgresqlOperatorCharm._on_leader_elected"): - rel_id = harness.add_relation(PEER, harness.charm.app.name) - """NOTE: currently ops.testing seems to allow for non-leader to remove secrets too!""" - harness.set_leader(True) - harness.update_relation_data(rel_id, harness.charm.app.name, {"replication": "somepw"}) - harness.charm.remove_secret("app", "replication") - assert harness.charm.get_secret("app", "replication") is None - - harness.set_leader(False) - harness.update_relation_data(rel_id, harness.charm.unit.name, {"somekey": "somevalue"}) - harness.charm.remove_secret("unit", "somekey") - assert harness.charm.get_secret("unit", "somekey") is None - - harness.set_leader(True) - with caplog.at_level(logging.ERROR): - harness.charm.remove_secret("app", "replication") - assert "Non-existing field 'replication' was attempted to be removed" in caplog.text - - harness.charm.remove_secret("unit", "somekey") - assert "Non-existing field 'somekey' was attempted to be removed" in caplog.text - - harness.charm.remove_secret("app", "non-existing-secret") - assert ( - "Non-existing field 'non-existing-secret' was attempted to be removed" - in caplog.text - ) - - harness.charm.remove_secret("unit", "non-existing-secret") - assert ( - "Non-existing field 'non-existing-secret' was attempted to be removed" - in caplog.text - ) - - -@patch_network_get(private_address="1.1.1.1") -def test_delete_existing_password_secrets(harness, caplog): +def test_delete_existing_password_secrets(harness, _has_secrets, caplog): with ( patch("charm.PostgresqlOperatorCharm._on_leader_elected"), - patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=True), ): harness.add_relation(PEER, harness.charm.app.name) """NOTE: currently ops.testing seems to allow for non-leader to remove secrets too!""" @@ -1987,15 +1911,20 @@ def test_delete_existing_password_secrets(harness, caplog): harness.set_leader(True) with caplog.at_level(logging.ERROR): + if _has_secrets: + error_message = ( + "Non-existing secret operator-password was attempted to be removed." + ) + else: + error_message = ( + "Non-existing field 'operator-password' was attempted to be removed" + ) + harness.charm.remove_secret("app", "operator-password") - assert ( - "Non-existing secret operator-password was attempted to be removed." in caplog.text - ) + assert error_message in caplog.text harness.charm.remove_secret("unit", "operator-password") - assert ( - "Non-existing secret operator-password was attempted to be removed." in caplog.text - ) + assert error_message in caplog.text harness.charm.remove_secret("app", "non-existing-secret") assert ( @@ -2012,13 +1941,18 @@ def test_delete_existing_password_secrets(harness, caplog): @pytest.mark.parametrize("scope,is_leader", [("app", True), ("unit", True), ("unit", False)]) @patch_network_get(private_address="1.1.1.1") -def test_migration_from_databag(harness, scope, is_leader): +def test_migration_from_databag(harness, _has_secrets, scope, is_leader): with ( patch("charm.PostgresqlOperatorCharm._on_leader_elected"), - patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=True), ): rel_id = harness.add_relation(PEER, harness.charm.app.name) """Check if we're moving on to use secrets when live upgrade from databag to Secrets usage.""" + + # as this test checks for a migration from databag to secrets, + # there's no need for this test when secrets are not enabled. + if not _has_secrets: + return + # App has to be leader, unit can be either harness.set_leader(is_leader) @@ -2038,13 +1972,18 @@ def test_migration_from_databag(harness, scope, is_leader): @pytest.mark.parametrize("scope,is_leader", [("app", True), ("unit", True), ("unit", False)]) @patch_network_get(private_address="1.1.1.1") -def test_migration_from_single_secret(harness, scope, is_leader): +def test_migration_from_single_secret(harness, _has_secrets, scope, is_leader): with ( patch("charm.PostgresqlOperatorCharm._on_leader_elected"), - patch("charm.JujuVersion.has_secrets", new_callable=PropertyMock, return_value=True), ): rel_id = harness.add_relation(PEER, harness.charm.app.name) """Check if we're moving on to use secrets when live upgrade from databag to Secrets usage.""" + + # as this test checks for a migration from databag to secrets, + # there's no need for this test when secrets are not enabled. + if not _has_secrets: + return + # App has to be leader, unit can be either harness.set_leader(is_leader) From c33a1b34283e44077839245558c109a318f6abb5 Mon Sep 17 00:00:00 2001 From: lucasgameiroborges Date: Thu, 4 Apr 2024 20:20:20 +0000 Subject: [PATCH 08/10] fix typo --- tests/unit/test_charm.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py index 45d5d3ea2b..cdf10ba465 100644 --- a/tests/unit/test_charm.py +++ b/tests/unit/test_charm.py @@ -47,8 +47,8 @@ def harness(): harness.cleanup() -# This causes every test defined in this file will run 2 times, with -# the patch of charm.JujuVersion.has_secrets set as True and as False +# This causes every test defined in this file to run 2 times, each with +# charm.JujuVersion.has_secrets set as True or as False @pytest.fixture(params=[True, False], autouse=True) def _has_secrets(request, monkeypatch): monkeypatch.setattr("charm.JujuVersion.has_secrets", PropertyMock(return_value=request.param)) From b32bbe12a16f487f81c0e01ddf933359af3f8378 Mon Sep 17 00:00:00 2001 From: lucasgameiroborges Date: Fri, 5 Apr 2024 15:52:12 +0000 Subject: [PATCH 09/10] test renaming + refactor --- tests/unit/test_charm.py | 77 +++++++++++----------------------------- 1 file changed, 20 insertions(+), 57 deletions(-) diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py index cdf10ba465..07d11ae441 100644 --- a/tests/unit/test_charm.py +++ b/tests/unit/test_charm.py @@ -43,6 +43,7 @@ def harness(): harness = Harness(PostgresqlOperatorCharm) harness.begin() harness.add_relation("upgrade", harness.charm.app.name) + harness.add_relation(PEER, harness.charm.app.name) yield harness harness.cleanup() @@ -67,7 +68,6 @@ def test_on_install(harness): "charm.PostgresqlOperatorCharm._is_storage_attached", side_effect=[False, True, True], ) as _is_storage_attached: - harness.add_relation(PEER, harness.charm.app.name) # Test without storage. harness.charm.on.install.emit() _reboot_on_detached_storage.assert_called_once() @@ -102,7 +102,6 @@ def test_on_install_failed_to_create_home(harness): "charm.PostgresqlOperatorCharm._is_storage_attached", side_effect=[False, True, True], ) as _is_storage_attached, patch("charm.logger.exception") as _logger_exception: - harness.add_relation(PEER, harness.charm.app.name) # Test without storage. harness.charm.on.install.emit() _reboot_on_detached_storage.assert_called_once() @@ -130,7 +129,6 @@ def test_on_install_snap_failure(harness): ) as _install_snap_packages, patch( "charm.PostgresqlOperatorCharm._is_storage_attached", return_value=True ) as _is_storage_attached: - harness.add_relation(PEER, harness.charm.app.name) # Mock the result of the call. _install_snap_packages.side_effect = snap.SnapError # Trigger the hook. @@ -142,7 +140,6 @@ def test_on_install_snap_failure(harness): @patch_network_get(private_address="1.1.1.1") def test_patroni_scrape_config_no_tls(harness): - harness.add_relation(PEER, harness.charm.app.name) result = harness.charm.patroni_scrape_config() assert result == [ @@ -162,7 +159,6 @@ def test_patroni_scrape_config_tls(harness): return_value=True, new_callable=PropertyMock, ): - harness.add_relation(PEER, harness.charm.app.name) result = harness.charm.patroni_scrape_config() assert result == [ @@ -181,7 +177,6 @@ def test_primary_endpoint(harness): new_callable=PropertyMock, return_value={"1.1.1.1", "1.1.1.2"}, ), patch("charm.PostgresqlOperatorCharm._patroni", new_callable=PropertyMock) as _patroni: - harness.add_relation(PEER, harness.charm.app.name) _patroni.return_value.get_member_ip.return_value = "1.1.1.1" _patroni.return_value.get_primary.return_value = sentinel.primary assert harness.charm.primary_endpoint == "1.1.1.1" @@ -198,7 +193,6 @@ def test_primary_endpoint_no_peers(harness): new_callable=PropertyMock, return_value={"1.1.1.1", "1.1.1.2"}, ), patch("charm.PostgresqlOperatorCharm._patroni", new_callable=PropertyMock) as _patroni: - harness.add_relation(PEER, harness.charm.app.name) assert harness.charm.primary_endpoint is None assert not _patroni.return_value.get_member_ip.called @@ -213,7 +207,6 @@ def test_on_leader_elected(harness): "charm.PostgresqlOperatorCharm.primary_endpoint", new_callable=PropertyMock, ) as _primary_endpoint, patch("charm.PostgresqlOperatorCharm.update_config") as _update_config: - harness.add_relation(PEER, harness.charm.app.name) # Assert that there is no password in the peer relation. assert harness.charm._peers.data[harness.charm.app].get("operator-password", None) is None @@ -245,7 +238,7 @@ def test_on_leader_elected(harness): def test_is_cluster_initialised(harness): - rel_id = harness.add_relation(PEER, harness.charm.app.name) + rel_id = harness.model.get_relation(PEER).id # Test when the cluster was not initialised yet. assert not (harness.charm.is_cluster_initialised) @@ -269,7 +262,6 @@ def test_on_config_changed(harness): ) as _enable_disable_extensions, patch( "charm.PostgresqlOperatorCharm.is_cluster_initialised", new_callable=PropertyMock ) as _is_cluster_initialised: - harness.add_relation(PEER, harness.charm.app.name) # Test when the cluster was not initialised yet. _is_cluster_initialised.return_value = False harness.charm.on.config_changed.emit() @@ -339,7 +331,6 @@ def test_check_extension_dependencies(harness): with patch("subprocess.check_output", return_value=b"C"), patch.object( PostgresqlOperatorCharm, "postgresql", Mock() ): - harness.add_relation(PEER, harness.charm.app.name) # Test when plugins dependencies exception is not caused config = { "plugin_address_standardizer_enable": False, @@ -368,7 +359,6 @@ def test_enable_disable_extensions(harness, caplog): with patch("subprocess.check_output", return_value=b"C"), patch.object( PostgresqlOperatorCharm, "postgresql", Mock() ) as postgresql_mock: - harness.add_relation(PEER, harness.charm.app.name) # Test when all extensions install/uninstall succeed. postgresql_mock.enable_disable_extension.side_effect = None with caplog.at_level(logging.ERROR): @@ -583,7 +573,6 @@ def test_on_start(harness): side_effect=[False, True, True, True, True], ) as _is_storage_attached, ): - harness.add_relation(PEER, harness.charm.app.name) _get_postgresql_version.return_value = "14.0" # Test without storage. @@ -656,7 +645,6 @@ def test_on_start_replica(harness): return_value=True, ) as _is_storage_attached, ): - harness.add_relation(PEER, harness.charm.app.name) _get_postgresql_version.return_value = "14.0" # Set the current unit to be a replica (non leader unit). @@ -705,7 +693,6 @@ def test_on_start_no_patroni_member(harness): "charm.PostgresqlOperatorCharm._is_storage_attached", return_value=True ) as _is_storage_attached, ): - harness.add_relation(PEER, harness.charm.app.name) # Mock the passwords. patroni.return_value.member_started = False _get_password.return_value = "fake-operator-password" @@ -731,7 +718,6 @@ def test_on_start_after_blocked_state(harness): "charm.PostgresqlOperatorCharm._is_storage_attached", return_value=True ) as _is_storage_attached, ): - harness.add_relation(PEER, harness.charm.app.name) # Set an initial blocked status (like after the install hook was triggered). initial_status = BlockedStatus("fake message") harness.model.unit.status = initial_status @@ -748,7 +734,7 @@ def test_on_start_after_blocked_state(harness): @patch_network_get(private_address="1.1.1.1") def test_on_get_password(harness): with patch("charm.PostgresqlOperatorCharm.update_config"): - rel_id = harness.add_relation(PEER, harness.charm.app.name) + rel_id = harness.model.get_relation(PEER).id # Create a mock event and set passwords in peer relation data. harness.set_leader(True) mock_event = MagicMock(params={}) @@ -789,7 +775,6 @@ def test_on_set_password(harness): patch("charm.Patroni.are_all_members_ready") as _are_all_members_ready, patch("charm.PostgresqlOperatorCharm._on_leader_elected"), ): - harness.add_relation(PEER, harness.charm.app.name) # Create a mock event. mock_event = MagicMock(params={}) @@ -868,7 +853,7 @@ def test_on_update_status(harness): patch("charm.PostgreSQLProvider.oversee_users") as _oversee_users, patch("upgrade.PostgreSQLUpgrade.idle", return_value=True), ): - rel_id = harness.add_relation(PEER, harness.charm.app.name) + rel_id = harness.model.get_relation(PEER).id # Test before the cluster is initialised. harness.charm.on.update_status.emit() _set_primary_status_message.assert_not_called() @@ -940,7 +925,7 @@ def test_on_update_status_after_restore_operation(harness): patch("charm.Patroni.get_member_status") as _get_member_status, patch("upgrade.PostgreSQLUpgrade.idle", return_value=True), ): - rel_id = harness.add_relation(PEER, harness.charm.app.name) + rel_id = harness.model.get_relation(PEER).id # Test when the restore operation fails. with harness.hooks_disabled(): harness.set_leader() @@ -1029,7 +1014,6 @@ def test_on_update_status_after_restore_operation(harness): def test_install_snap_packages(harness): with patch("charm.snap.SnapCache") as _snap_cache: - harness.add_relation(PEER, harness.charm.app.name) _snap_package = _snap_cache.return_value.__getitem__.return_value _snap_package.ensure.side_effect = snap.SnapError _snap_package.present = False @@ -1121,7 +1105,6 @@ def test_is_storage_attached(harness): "subprocess.check_call", side_effect=[None, subprocess.CalledProcessError(1, "fake command")], ) as _check_call: - harness.add_relation(PEER, harness.charm.app.name) # Test with attached storage. is_storage_attached = harness.charm._is_storage_attached() _check_call.assert_called_once_with(["mountpoint", "-q", harness.charm._storage_path]) @@ -1134,7 +1117,6 @@ def test_is_storage_attached(harness): def test_reboot_on_detached_storage(harness): with patch("subprocess.check_call") as _check_call: - harness.add_relation(PEER, harness.charm.app.name) mock_event = MagicMock() harness.charm._reboot_on_detached_storage(mock_event) mock_event.defer.assert_called_once() @@ -1148,7 +1130,6 @@ def test_restart(harness): patch("charm.Patroni.restart_postgresql") as _restart_postgresql, patch("charm.Patroni.are_all_members_ready") as _are_all_members_ready, ): - harness.add_relation(PEER, harness.charm.app.name) _are_all_members_ready.side_effect = [False, True, True] # Test when not all members are ready. @@ -1189,7 +1170,7 @@ def test_update_config(harness): ) as _is_tls_enabled, patch.object(PostgresqlOperatorCharm, "postgresql", Mock()) as postgresql_mock, ): - rel_id = harness.add_relation(PEER, harness.charm.app.name) + rel_id = harness.model.get_relation(PEER).id # Mock some properties. postgresql_mock.is_tls_enabled = PropertyMock(side_effect=[False, False, False, False]) _is_workload_running.side_effect = [False, False, True, True, False, True] @@ -1277,7 +1258,6 @@ def test_on_cluster_topology_change(harness): "charm.PostgresqlOperatorCharm.primary_endpoint", new_callable=PropertyMock ) as _primary_endpoint, ): - harness.add_relation(PEER, harness.charm.app.name) # Mock the property value. _primary_endpoint.side_effect = [None, "1.1.1.1"] @@ -1301,7 +1281,6 @@ def test_on_cluster_topology_change_keep_blocked(harness): "charm.PostgresqlOperatorCharm._update_relation_endpoints" ) as _update_relation_endpoints, ): - harness.add_relation(PEER, harness.charm.app.name) harness.model.unit.status = WaitingStatus(PRIMARY_NOT_REACHABLE_MESSAGE) harness.charm._on_cluster_topology_change(Mock()) @@ -1323,7 +1302,6 @@ def test_on_cluster_topology_change_clear_blocked(harness): "charm.PostgresqlOperatorCharm._update_relation_endpoints" ) as _update_relation_endpoints, ): - harness.add_relation(PEER, harness.charm.app.name) harness.model.unit.status = WaitingStatus(PRIMARY_NOT_REACHABLE_MESSAGE) harness.charm._on_cluster_topology_change(Mock()) @@ -1338,7 +1316,6 @@ def test_validate_config_options(harness): patch("charm.PostgresqlOperatorCharm.postgresql", new_callable=PropertyMock) as _charm_lib, patch("config.subprocess"), ): - harness.add_relation(PEER, harness.charm.app.name) _charm_lib.return_value.get_postgresql_text_search_configs.return_value = [] _charm_lib.return_value.validate_date_style.return_value = [] _charm_lib.return_value.get_postgresql_timezones.return_value = [] @@ -1408,7 +1385,7 @@ def test_on_peer_relation_changed(harness): patch("charm.PostgresqlOperatorCharm._reconfigure_cluster") as _reconfigure_cluster, patch("ops.framework.EventBase.defer") as _defer, ): - rel_id = harness.add_relation(PEER, harness.charm.app.name) + rel_id = harness.model.get_relation(PEER).id # Test an uninitialized cluster. mock_event = Mock() with harness.hooks_disabled(): @@ -1534,7 +1511,7 @@ def test_reconfigure_cluster(harness): ) as _remove_from_members_ips, patch("charm.Patroni.remove_raft_member") as _remove_raft_member, ): - rel_id = harness.add_relation(PEER, harness.charm.app.name) + rel_id = harness.model.get_relation(PEER).id # Test when no change is needed in the member IP. mock_event = Mock() mock_event.unit = harness.charm.unit @@ -1580,13 +1557,12 @@ def test_reconfigure_cluster(harness): _add_members.assert_called_once_with(mock_event) -def test_update_certificate_secrets(harness): +def test_update_certificate(harness): with ( patch( "charms.postgresql_k8s.v0.postgresql_tls.PostgreSQLTLS._request_certificate" ) as _request_certificate, ): - harness.add_relation(PEER, harness.charm.app.name) # If there is no current TLS files, _request_certificate should be called # only when the certificates relation is established. harness.charm._update_certificate() @@ -1616,7 +1592,7 @@ def test_update_member_ip(harness): patch("charm.PostgresqlOperatorCharm._update_certificate") as _update_certificate, patch("charm.Patroni.stop_patroni") as _stop_patroni, ): - rel_id = harness.add_relation(PEER, harness.charm.app.name) + rel_id = harness.model.get_relation(PEER).id # Test when the IP address of the unit hasn't changed. with harness.hooks_disabled(): harness.update_relation_data( @@ -1658,7 +1634,6 @@ def test_push_tls_files_to_workload(harness): "charms.postgresql_k8s.v0.postgresql_tls.PostgreSQLTLS.get_tls_files" ) as _get_tls_files, ): - harness.add_relation(PEER, harness.charm.app.name) _get_tls_files.side_effect = [ ("key", "ca", "cert"), ("key", "ca", None), @@ -1680,7 +1655,6 @@ def test_push_tls_files_to_workload(harness): def test_is_workload_running(harness): with patch("charm.snap.SnapCache") as _snap_cache: - harness.add_relation(PEER, harness.charm.app.name) pg_snap = _snap_cache.return_value[POSTGRESQL_SNAP_NAME] pg_snap.present = False @@ -1691,7 +1665,6 @@ def test_is_workload_running(harness): def test_get_available_memory(harness): - harness.add_relation(PEER, harness.charm.app.name) meminfo = ( "MemTotal: 16089488 kB" "MemFree: 799284 kB" @@ -1714,7 +1687,6 @@ def test_juju_run_exec_divergence(harness): patch("charm.ClusterTopologyObserver") as _topology_observer, patch("charm.JujuVersion") as _juju_version, ): - harness.add_relation(PEER, harness.charm.app.name) # Juju 2 _juju_version.from_environ.return_value.major = 2 harness = Harness(PostgresqlOperatorCharm) @@ -1730,7 +1702,6 @@ def test_juju_run_exec_divergence(harness): def test_client_relations(harness): - harness.add_relation(PEER, harness.charm.app.name) # Test when the charm has no relations. assert len(harness.charm.client_relations) == 0 @@ -1750,7 +1721,6 @@ def test_client_relations(harness): def test_scope_obj(harness): - harness.add_relation(PEER, harness.charm.app.name) assert harness.charm._scope_obj("app") == harness.charm.framework.model.app assert harness.charm._scope_obj("unit") == harness.charm.framework.model.unit assert harness.charm._scope_obj("test") is None @@ -1759,7 +1729,7 @@ def test_scope_obj(harness): @patch_network_get(private_address="1.1.1.1") def test_get_secret(harness): with patch("charm.PostgresqlOperatorCharm._on_leader_elected"): - rel_id = harness.add_relation(PEER, harness.charm.app.name) + rel_id = harness.model.get_relation(PEER).id # App level changes require leader privileges harness.set_leader() # Test application scope. @@ -1782,7 +1752,6 @@ def test_on_get_password_secrets(harness): with ( patch("charm.PostgresqlOperatorCharm._on_leader_elected"), ): - harness.add_relation(PEER, harness.charm.app.name) # Create a mock event and set passwords in peer relation data. harness.set_leader() mock_event = MagicMock(params={}) @@ -1814,7 +1783,6 @@ def test_get_secret_secrets(harness, scope): with ( patch("charm.PostgresqlOperatorCharm._on_leader_elected"), ): - harness.add_relation(PEER, harness.charm.app.name) harness.set_leader() assert harness.charm.get_secret(scope, "operator-password") is None @@ -1825,7 +1793,7 @@ def test_get_secret_secrets(harness, scope): @patch_network_get(private_address="1.1.1.1") def test_set_secret(harness): with patch("charm.PostgresqlOperatorCharm._on_leader_elected"): - rel_id = harness.add_relation(PEER, harness.charm.app.name) + rel_id = harness.model.get_relation(PEER).id harness.set_leader() # Test application scope. @@ -1858,7 +1826,6 @@ def test_set_reset_new_secret(harness, scope, is_leader): with ( patch("charm.PostgresqlOperatorCharm._on_leader_elected"), ): - harness.add_relation(PEER, harness.charm.app.name) """NOTE: currently ops.testing seems to allow for non-leader to set secrets too!""" # App has to be leader, unit can be either harness.set_leader(is_leader) @@ -1881,7 +1848,6 @@ def test_invalid_secret(harness, scope, is_leader): with ( patch("charm.PostgresqlOperatorCharm._on_leader_elected"), ): - harness.add_relation(PEER, harness.charm.app.name) # App has to be leader, unit can be either harness.set_leader(is_leader) @@ -1893,11 +1859,10 @@ def test_invalid_secret(harness, scope, is_leader): @patch_network_get(private_address="1.1.1.1") -def test_delete_existing_password_secrets(harness, _has_secrets, caplog): +def test_delete_password(harness, _has_secrets, caplog): with ( patch("charm.PostgresqlOperatorCharm._on_leader_elected"), ): - harness.add_relation(PEER, harness.charm.app.name) """NOTE: currently ops.testing seems to allow for non-leader to remove secrets too!""" harness.set_leader(True) harness.charm.set_secret("app", "operator-password", "somepw") @@ -1942,17 +1907,16 @@ def test_delete_existing_password_secrets(harness, _has_secrets, caplog): @pytest.mark.parametrize("scope,is_leader", [("app", True), ("unit", True), ("unit", False)]) @patch_network_get(private_address="1.1.1.1") def test_migration_from_databag(harness, _has_secrets, scope, is_leader): + """Check if we're moving on to use secrets when live upgrade from databag to Secrets usage.""" with ( patch("charm.PostgresqlOperatorCharm._on_leader_elected"), ): - rel_id = harness.add_relation(PEER, harness.charm.app.name) - """Check if we're moving on to use secrets when live upgrade from databag to Secrets usage.""" - # as this test checks for a migration from databag to secrets, # there's no need for this test when secrets are not enabled. if not _has_secrets: return + rel_id = harness.model.get_relation(PEER).id # App has to be leader, unit can be either harness.set_leader(is_leader) @@ -1973,17 +1937,17 @@ def test_migration_from_databag(harness, _has_secrets, scope, is_leader): @pytest.mark.parametrize("scope,is_leader", [("app", True), ("unit", True), ("unit", False)]) @patch_network_get(private_address="1.1.1.1") def test_migration_from_single_secret(harness, _has_secrets, scope, is_leader): + """Check if we're moving on to use secrets when live upgrade from databag to Secrets usage.""" with ( patch("charm.PostgresqlOperatorCharm._on_leader_elected"), ): - rel_id = harness.add_relation(PEER, harness.charm.app.name) - """Check if we're moving on to use secrets when live upgrade from databag to Secrets usage.""" - # as this test checks for a migration from databag to secrets, # there's no need for this test when secrets are not enabled. if not _has_secrets: return + rel_id = harness.model.get_relation(PEER).id + # App has to be leader, unit can be either harness.set_leader(is_leader) @@ -2019,7 +1983,7 @@ def test_handle_postgresql_restart_need(harness): ) as _is_tls_enabled, patch.object(PostgresqlOperatorCharm, "postgresql", Mock()) as postgresql_mock, ): - rel_id = harness.add_relation(PEER, harness.charm.app.name) + rel_id = harness.model.get_relation(PEER).id for values in itertools.product( [True, False], [True, False], [True, False], [True, False], [True, False] ): @@ -2082,7 +2046,7 @@ def test_on_peer_relation_departed(harness): patch("charm.PostgresqlOperatorCharm._unit_ip") as _unit_ip, patch("charm.Patroni.get_member_ip") as _get_member_ip, ): - rel_id = harness.add_relation(PEER, harness.charm.app.name) + rel_id = harness.model.get_relation(PEER).id # Test when the current unit is the departing unit. harness.charm.unit.status = ActiveStatus() event = Mock() @@ -2256,7 +2220,6 @@ def test_update_new_unit_status(harness): "charm.PostgresqlOperatorCharm.primary_endpoint", new_callable=PropertyMock ) as _primary_endpoint, ): - harness.add_relation(PEER, harness.charm.app.name) # Test when the charm is blocked. _primary_endpoint.return_value = "endpoint" harness.charm.unit.status = BlockedStatus("fake blocked status") From cf05809793bd3556b98ca9201fa7996128f77b9e Mon Sep 17 00:00:00 2001 From: lucasgameiroborges Date: Fri, 5 Apr 2024 17:53:08 +0000 Subject: [PATCH 10/10] Trigger CLA check with company email