From 93dbf8be4613ba86a070ca06c0af358b8da5535a Mon Sep 17 00:00:00 2001 From: Lucas Gameiro Borges Date: Mon, 22 Apr 2024 01:56:06 +0000 Subject: [PATCH 1/8] first effort --- tests/unit/test_backups.py | 1323 +++++++++--------- tests/unit/test_cluster.py | 368 ++--- tests/unit/test_cluster_topology_observer.py | 67 +- tests/unit/test_postgresql_provider.py | 456 +++--- tests/unit/test_upgrade.py | 166 ++- 5 files changed, 1197 insertions(+), 1183 deletions(-) diff --git a/tests/unit/test_backups.py b/tests/unit/test_backups.py index 4c04fb6fdf..658f7f7a8f 100644 --- a/tests/unit/test_backups.py +++ b/tests/unit/test_backups.py @@ -1,10 +1,11 @@ # Copyright 2023 Canonical Ltd. # See LICENSE file for licensing details. -import unittest +import pytest from pathlib import PosixPath from subprocess import PIPE, CompletedProcess, TimeoutExpired from typing import OrderedDict from unittest.mock import ANY, MagicMock, PropertyMock, call, mock_open, patch +from unittest import TestCase as tc import botocore as botocore from boto3.exceptions import S3UploadFailedError @@ -27,187 +28,177 @@ S3_PARAMETERS_RELATION = "s3-parameters" -class TestPostgreSQLBackups(unittest.TestCase): - def setUp(self): - self.harness = Harness(PostgresqlOperatorCharm) - self.addCleanup(self.harness.cleanup) +@pytest.fixture(autouse=True) +def harness(): + harness = Harness(PostgresqlOperatorCharm) - # Set up the initial relation and hooks. - self.peer_rel_id = self.harness.add_relation(PEER, "postgresql") - self.harness.add_relation_unit(self.peer_rel_id, "postgresql/0") - self.harness.begin() - self.charm = self.harness.charm + # Set up the initial relation and hooks. + peer_rel_id = harness.add_relation(PEER, "postgresql") + harness.add_relation_unit(peer_rel_id, "postgresql/0") + harness.begin() + yield harness + harness.cleanup() - def relate_to_s3_integrator(self): - self.s3_rel_id = self.harness.add_relation(S3_PARAMETERS_RELATION, "s3-integrator") +def test_stanza_name(harness): + tc().assertEqual( + harness.charm.backup.stanza_name, f"{harness.charm.model.name}.{harness.charm.cluster_name}" + ) - def remove_relation_from_s3_integrator(self): - self.harness.remove_relation(S3_PARAMETERS_RELATION, "s3-integrator") - self.s3_rel_id = None +def test_are_backup_settings_ok(harness): + # Test without S3 relation. + tc().assertEqual( + harness.charm.backup._are_backup_settings_ok(), + (False, "Relation with s3-integrator charm missing, cannot create/restore backup."), + ) - def test_stanza_name(self): - self.assertEqual( - self.charm.backup.stanza_name, f"{self.charm.model.name}.{self.charm.cluster_name}" - ) + # Test when there are missing S3 parameters. + harness.add_relation(S3_PARAMETERS_RELATION, "s3-integrator") + tc().assertEqual( + harness.charm.backup._are_backup_settings_ok(), + (False, "Missing S3 parameters: ['bucket', 'access-key', 'secret-key']"), + ) - def test_are_backup_settings_ok(self): - # Test without S3 relation. - self.assertEqual( - self.charm.backup._are_backup_settings_ok(), - (False, "Relation with s3-integrator charm missing, cannot create/restore backup."), + # Test when all required parameters are provided. + with patch("charm.PostgreSQLBackups._retrieve_s3_parameters") as _retrieve_s3_parameters: + _retrieve_s3_parameters.return_value = ["bucket", "access-key", "secret-key"], [] + tc().assertEqual( + harness.charm.backup._are_backup_settings_ok(), + (True, None), ) - # Test when there are missing S3 parameters. - self.relate_to_s3_integrator() - self.assertEqual( - self.charm.backup._are_backup_settings_ok(), - (False, "Missing S3 parameters: ['bucket', 'access-key', 'secret-key']"), - ) - - # Test when all required parameters are provided. - with patch("charm.PostgreSQLBackups._retrieve_s3_parameters") as _retrieve_s3_parameters: - _retrieve_s3_parameters.return_value = ["bucket", "access-key", "secret-key"], [] - self.assertEqual( - self.charm.backup._are_backup_settings_ok(), - (True, None), - ) - - @patch_network_get(private_address="1.1.1.1") - @patch("charm.PostgreSQLBackups._are_backup_settings_ok") - @patch("charm.Patroni.member_started", new_callable=PropertyMock) - @patch("ops.model.Application.planned_units") - @patch("charm.PostgresqlOperatorCharm.is_primary", new_callable=PropertyMock) - def test_can_unit_perform_backup( - self, _is_primary, _planned_units, _member_started, _are_backup_settings_ok +@patch_network_get(private_address="1.1.1.1") +def test_can_unit_perform_backup(harness): + with ( + patch("charm.PostgreSQLBackups._are_backup_settings_ok") as _are_backup_settings_ok, + patch("charm.Patroni.member_started", new_callable=PropertyMock) as _member_started, + patch("ops.model.Application.planned_units") as _planned_units, + patch("charm.PostgresqlOperatorCharm.is_primary", new_callable=PropertyMock) as _is_primary, ): + peer_rel_id = harness.model.get_relation(PEER).id # Test when the charm fails to retrieve the primary. _is_primary.side_effect = RetryError(last_attempt=1) - self.assertEqual( - self.charm.backup._can_unit_perform_backup(), + tc().assertEqual( + harness.charm.backup._can_unit_perform_backup(), (False, "Unit cannot perform backups as the database seems to be offline"), ) # Test when the unit is in a blocked state. _is_primary.side_effect = None _is_primary.return_value = True - self.charm.unit.status = BlockedStatus("fake blocked state") - self.assertEqual( - self.charm.backup._can_unit_perform_backup(), + harness.charm.unit.status = BlockedStatus("fake blocked state") + tc().assertEqual( + harness.charm.backup._can_unit_perform_backup(), (False, "Unit is in a blocking state"), ) # Test when running the check in the primary, there are replicas and TLS is enabled. - self.charm.unit.status = ActiveStatus() + harness.charm.unit.status = ActiveStatus() _planned_units.return_value = 2 - with self.harness.hooks_disabled(): - self.harness.update_relation_data( - self.peer_rel_id, - self.charm.unit.name, + with harness.hooks_disabled(): + harness.update_relation_data( + peer_rel_id, + harness.charm.unit.name, {"tls": "True"}, ) - self.assertEqual( - self.charm.backup._can_unit_perform_backup(), + tc().assertEqual( + harness.charm.backup._can_unit_perform_backup(), (False, "Unit cannot perform backups as it is the cluster primary"), ) # Test when running the check in a replica and TLS is disabled. _is_primary.return_value = False - with self.harness.hooks_disabled(): - self.harness.update_relation_data( - self.peer_rel_id, - self.charm.unit.name, + with harness.hooks_disabled(): + harness.update_relation_data( + peer_rel_id, + harness.charm.unit.name, {"tls": ""}, ) - self.assertEqual( - self.charm.backup._can_unit_perform_backup(), + tc().assertEqual( + harness.charm.backup._can_unit_perform_backup(), (False, "Unit cannot perform backups as TLS is not enabled"), ) # Test when Patroni or PostgreSQL hasn't started yet. _is_primary.return_value = True _member_started.return_value = False - self.assertEqual( - self.charm.backup._can_unit_perform_backup(), + tc().assertEqual( + harness.charm.backup._can_unit_perform_backup(), (False, "Unit cannot perform backups as it's not in running state"), ) # Test when the stanza was not initialised yet. _member_started.return_value = True - self.assertEqual( - self.charm.backup._can_unit_perform_backup(), + tc().assertEqual( + harness.charm.backup._can_unit_perform_backup(), (False, "Stanza was not initialised"), ) # Test when S3 parameters are not ok. - with self.harness.hooks_disabled(): - self.harness.update_relation_data( - self.peer_rel_id, - self.charm.app.name, - {"stanza": self.charm.backup.stanza_name}, + with harness.hooks_disabled(): + harness.update_relation_data( + peer_rel_id, + harness.charm.app.name, + {"stanza": harness.charm.backup.stanza_name}, ) _are_backup_settings_ok.return_value = (False, "fake error message") - self.assertEqual( - self.charm.backup._can_unit_perform_backup(), + tc().assertEqual( + harness.charm.backup._can_unit_perform_backup(), (False, "fake error message"), ) # Test when everything is ok to run a backup. _are_backup_settings_ok.return_value = (True, None) - self.assertEqual( - self.charm.backup._can_unit_perform_backup(), + tc().assertEqual( + harness.charm.backup._can_unit_perform_backup(), (True, None), ) - @patch_network_get(private_address="1.1.1.1") - @patch("charm.Patroni.reload_patroni_configuration") - @patch("charm.Patroni.member_started", new_callable=PropertyMock) - @patch("charm.PostgresqlOperatorCharm.update_config") - @patch("charm.Patroni.get_postgresql_version", return_value="14.10") - @patch("charm.PostgreSQLBackups._execute_command") - def test_can_use_s3_repository( - self, - _execute_command, - _get_postgresql_version, - _update_config, - _member_started, - _reload_patroni_configuration, +@patch_network_get(private_address="1.1.1.1") +def test_can_use_s3_repository(harness): + with ( + patch("charm.Patroni.reload_patroni_configuration") as _reload_patroni_configuration, + patch("charm.PostgreSQLBackups._execute_command") as _execute_command, + patch("charm.Patroni.member_started", new_callable=PropertyMock) as _member_started, + patch("charm.PostgresqlOperatorCharm.update_config") as _update_config, + patch("charm.Patroni.get_postgresql_version", return_value="14.10") as _get_postgresql_version, ): + peer_rel_id = harness.model.get_relation(PEER).id # Define the stanza name inside the unit relation data. - with self.harness.hooks_disabled(): - self.harness.update_relation_data( - self.peer_rel_id, - self.charm.app.name, - {"stanza": self.charm.backup.stanza_name}, + with harness.hooks_disabled(): + harness.update_relation_data( + peer_rel_id, + harness.charm.app.name, + {"stanza": harness.charm.backup.stanza_name}, ) # Test when nothing is returned from the pgBackRest info command. _execute_command.side_effect = TimeoutExpired(cmd="fake command", timeout=30) - with self.assertRaises(TimeoutError): - self.charm.backup.can_use_s3_repository() + with tc().assertRaises(TimeoutError): + harness.charm.backup.can_use_s3_repository() _execute_command.side_effect = None _execute_command.return_value = (1, "", "") - self.assertEqual( - self.charm.backup.can_use_s3_repository(), + tc().assertEqual( + harness.charm.backup.can_use_s3_repository(), (False, FAILED_TO_INITIALIZE_STANZA_ERROR_MESSAGE), ) # Test when the unit is a replica. pgbackrest_info_same_cluster_backup_output = ( 0, - f'[{{"db": [{{"system-id": "12345"}}], "name": "{self.charm.backup.stanza_name}"}}]', + f'[{{"db": [{{"system-id": "12345"}}], "name": "{harness.charm.backup.stanza_name}"}}]', "", ) _execute_command.return_value = pgbackrest_info_same_cluster_backup_output - self.assertEqual( - self.charm.backup.can_use_s3_repository(), + tc().assertEqual( + harness.charm.backup.can_use_s3_repository(), (True, None), ) # Assert that the stanza name is still in the unit relation data. - self.assertEqual( - self.harness.get_relation_data(self.peer_rel_id, self.charm.app), - {"stanza": self.charm.backup.stanza_name}, + tc().assertEqual( + harness.get_relation_data(peer_rel_id, harness.charm.app), + {"stanza": harness.charm.backup.stanza_name}, ) # Test when the unit is the leader and the workload is running, @@ -217,11 +208,11 @@ def test_can_use_s3_repository( pgbackrest_info_same_cluster_backup_output, (1, "", "fake error"), ] - with self.harness.hooks_disabled(): - self.harness.set_leader() - with self.assertRaises(Exception): - self.assertEqual( - self.charm.backup.can_use_s3_repository(), + with harness.hooks_disabled(): + harness.set_leader() + with tc().assertRaises(Exception): + tc().assertEqual( + harness.charm.backup.can_use_s3_repository(), (False, ANOTHER_CLUSTER_REPOSITORY_ERROR_MESSAGE), ) _update_config.assert_not_called() @@ -231,7 +222,7 @@ def test_can_use_s3_repository( # Test when the cluster system id can be retrieved, but it's different from the stanza system id. pgbackrest_info_other_cluster_system_id_backup_output = ( 0, - f'[{{"db": [{{"system-id": "12345"}}], "name": "{self.charm.backup.stanza_name}"}}]', + f'[{{"db": [{{"system-id": "12345"}}], "name": "{harness.charm.backup.stanza_name}"}}]', "", ) other_instance_system_identifier_output = ( @@ -243,14 +234,14 @@ def test_can_use_s3_repository( pgbackrest_info_other_cluster_system_id_backup_output, other_instance_system_identifier_output, ] - with self.harness.hooks_disabled(): - self.harness.update_relation_data( - self.peer_rel_id, - self.charm.app.name, - {"stanza": self.charm.backup.stanza_name}, + with harness.hooks_disabled(): + harness.update_relation_data( + peer_rel_id, + harness.charm.app.name, + {"stanza": harness.charm.backup.stanza_name}, ) - self.assertEqual( - self.charm.backup.can_use_s3_repository(), + tc().assertEqual( + harness.charm.backup.can_use_s3_repository(), (False, ANOTHER_CLUSTER_REPOSITORY_ERROR_MESSAGE), ) _update_config.assert_called_once() @@ -258,7 +249,7 @@ def test_can_use_s3_repository( _reload_patroni_configuration.assert_called_once() # Assert that the stanza name is not present in the unit relation data anymore. - self.assertEqual(self.harness.get_relation_data(self.peer_rel_id, self.charm.app), {}) + tc().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.app), {}) # Test when the cluster system id can be retrieved, but it's different from the stanza system id. _update_config.reset_mock() @@ -266,7 +257,7 @@ def test_can_use_s3_repository( _reload_patroni_configuration.reset_mock() pgbackrest_info_other_cluster_name_backup_output = ( 0, - f'[{{"db": [{{"system-id": "12345"}}], "name": "another-model.{self.charm.cluster_name}"}}]', + f'[{{"db": [{{"system-id": "12345"}}], "name": "another-model.{harness.charm.cluster_name}"}}]', "", ) same_instance_system_identifier_output = ( @@ -278,14 +269,14 @@ def test_can_use_s3_repository( pgbackrest_info_other_cluster_name_backup_output, same_instance_system_identifier_output, ] - with self.harness.hooks_disabled(): - self.harness.update_relation_data( - self.peer_rel_id, - self.charm.app.name, - {"stanza": self.charm.backup.stanza_name}, + with harness.hooks_disabled(): + harness.update_relation_data( + peer_rel_id, + harness.charm.app.name, + {"stanza": harness.charm.backup.stanza_name}, ) - self.assertEqual( - self.charm.backup.can_use_s3_repository(), + tc().assertEqual( + harness.charm.backup.can_use_s3_repository(), (False, ANOTHER_CLUSTER_REPOSITORY_ERROR_MESSAGE), ) _update_config.assert_called_once() @@ -293,25 +284,25 @@ def test_can_use_s3_repository( _reload_patroni_configuration.assert_called_once() # Assert that the stanza name is not present in the unit relation data anymore. - self.assertEqual(self.harness.get_relation_data(self.peer_rel_id, self.charm.app), {}) + tc().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.app), {}) # Test when the workload is not running. _update_config.reset_mock() _member_started.reset_mock() _reload_patroni_configuration.reset_mock() _member_started.return_value = False - with self.harness.hooks_disabled(): - self.harness.update_relation_data( - self.peer_rel_id, - self.charm.app.name, - {"stanza": self.charm.backup.stanza_name}, + with harness.hooks_disabled(): + harness.update_relation_data( + peer_rel_id, + harness.charm.app.name, + {"stanza": harness.charm.backup.stanza_name}, ) _execute_command.side_effect = [ pgbackrest_info_same_cluster_backup_output, other_instance_system_identifier_output, ] - self.assertEqual( - self.charm.backup.can_use_s3_repository(), + tc().assertEqual( + harness.charm.backup.can_use_s3_repository(), (False, ANOTHER_CLUSTER_REPOSITORY_ERROR_MESSAGE), ) _update_config.assert_called_once() @@ -319,56 +310,58 @@ def test_can_use_s3_repository( _reload_patroni_configuration.assert_not_called() # Assert that the stanza name is not present in the unit relation data anymore. - self.assertEqual(self.harness.get_relation_data(self.peer_rel_id, self.charm.app), {}) + tc().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.app), {}) # Test when there is no backup from another cluster in the S3 repository. - with self.harness.hooks_disabled(): - self.harness.update_relation_data( - self.peer_rel_id, - self.charm.app.name, - {"stanza": self.charm.backup.stanza_name}, + with harness.hooks_disabled(): + harness.update_relation_data( + peer_rel_id, + harness.charm.app.name, + {"stanza": harness.charm.backup.stanza_name}, ) _execute_command.side_effect = [ pgbackrest_info_same_cluster_backup_output, same_instance_system_identifier_output, ] - self.assertEqual( - self.charm.backup.can_use_s3_repository(), + tc().assertEqual( + harness.charm.backup.can_use_s3_repository(), (True, None), ) # Assert that the stanza name is still in the unit relation data. - self.assertEqual( - self.harness.get_relation_data(self.peer_rel_id, self.charm.app), - {"stanza": self.charm.backup.stanza_name}, + tc().assertEqual( + harness.get_relation_data(peer_rel_id, harness.charm.app), + {"stanza": harness.charm.backup.stanza_name}, ) - def test_construct_endpoint(self): - # Test with an AWS endpoint without region. - s3_parameters = {"endpoint": "https://s3.amazonaws.com", "region": ""} - self.assertEqual( - self.charm.backup._construct_endpoint(s3_parameters), "https://s3.amazonaws.com" - ) +def test_construct_endpoint(harness): + # Test with an AWS endpoint without region. + s3_parameters = {"endpoint": "https://s3.amazonaws.com", "region": ""} + tc().assertEqual( + harness.charm.backup._construct_endpoint(s3_parameters), "https://s3.amazonaws.com" + ) - # Test with an AWS endpoint with region. - s3_parameters["region"] = "us-east-1" - self.assertEqual( - self.charm.backup._construct_endpoint(s3_parameters), - "https://s3.us-east-1.amazonaws.com", - ) + # Test with an AWS endpoint with region. + s3_parameters["region"] = "us-east-1" + tc().assertEqual( + harness.charm.backup._construct_endpoint(s3_parameters), + "https://s3.us-east-1.amazonaws.com", + ) - # Test with another cloud endpoint. - s3_parameters["endpoint"] = "https://storage.googleapis.com" - self.assertEqual( - self.charm.backup._construct_endpoint(s3_parameters), "https://storage.googleapis.com" - ) + # Test with another cloud endpoint. + s3_parameters["endpoint"] = "https://storage.googleapis.com" + tc().assertEqual( + harness.charm.backup._construct_endpoint(s3_parameters), "https://storage.googleapis.com" + ) - @patch("boto3.session.Session.resource") - @patch("charm.PostgreSQLBackups._retrieve_s3_parameters") - def test_create_bucket_if_not_exists(self, _retrieve_s3_parameters, _resource): +def test_create_bucket_if_not_exists(harness): + with ( + patch("boto3.session.Session.resource") as _resource, + patch("charm.PostgreSQLBackups._retrieve_s3_parameters") as _retrieve_s3_parameters, + ): # Test when there are missing S3 parameters. _retrieve_s3_parameters.return_value = ([], ["bucket", "access-key", "secret-key"]) - self.charm.backup._create_bucket_if_not_exists() + harness.charm.backup._create_bucket_if_not_exists() _resource.assert_not_called() # Test when the charm fails to create a boto3 session. @@ -383,15 +376,15 @@ def test_create_bucket_if_not_exists(self, _retrieve_s3_parameters, _resource): [], ) _resource.side_effect = ValueError - with self.assertRaises(ValueError): - self.charm.backup._create_bucket_if_not_exists() + with tc().assertRaises(ValueError): + harness.charm.backup._create_bucket_if_not_exists() # Test when the bucket already exists. _resource.side_effect = None head_bucket = _resource.return_value.Bucket.return_value.meta.client.head_bucket create = _resource.return_value.Bucket.return_value.create wait_until_exists = _resource.return_value.Bucket.return_value.wait_until_exists - self.charm.backup._create_bucket_if_not_exists() + harness.charm.backup._create_bucket_if_not_exists() head_bucket.assert_called_once() create.assert_not_called() wait_until_exists.assert_not_called() @@ -402,7 +395,7 @@ def test_create_bucket_if_not_exists(self, _retrieve_s3_parameters, _resource): error_response={"Error": {"Code": 1, "message": "fake error"}}, operation_name="fake operation name", ) - self.charm.backup._create_bucket_if_not_exists() + harness.charm.backup._create_bucket_if_not_exists() head_bucket.assert_called_once() create.assert_called_once() wait_until_exists.assert_called_once() @@ -415,8 +408,8 @@ def test_create_bucket_if_not_exists(self, _retrieve_s3_parameters, _resource): error_response={"Error": {"Code": 1, "message": "fake error"}}, operation_name="fake operation name", ) - with self.assertRaises(ClientError): - self.charm.backup._create_bucket_if_not_exists() + with tc().assertRaises(ClientError): + harness.charm.backup._create_bucket_if_not_exists() head_bucket.assert_called_once() create.assert_called_once() wait_until_exists.assert_not_called() @@ -427,19 +420,22 @@ def test_create_bucket_if_not_exists(self, _retrieve_s3_parameters, _resource): head_bucket.side_effect = botocore.exceptions.ConnectTimeoutError( endpoint_url="fake endpoint URL" ) - with self.assertRaises(botocore.exceptions.ConnectTimeoutError): - self.charm.backup._create_bucket_if_not_exists() + with tc().assertRaises(botocore.exceptions.ConnectTimeoutError): + harness.charm.backup._create_bucket_if_not_exists() head_bucket.assert_called_once() create.assert_not_called() wait_until_exists.assert_not_called() - @patch("shutil.rmtree") - @patch("pathlib.Path.is_dir") - @patch("pathlib.Path.exists") - def test_empty_data_files(self, _exists, _is_dir, _rmtree): + +def test_empty_data_files(harness): + with ( + patch("shutil.rmtree") as _rmtree, + patch("pathlib.Path.is_dir") as _is_dir, + patch("pathlib.Path.exists") as _exists, + ): # Test when the data directory doesn't exist. _exists.return_value = False - self.assertTrue(self.charm.backup._empty_data_files()) + tc().assertTrue(harness.charm.backup._empty_data_files()) _rmtree.assert_not_called() # Test when the removal of the data files fails. @@ -447,49 +443,52 @@ def test_empty_data_files(self, _exists, _is_dir, _rmtree): _exists.return_value = True _is_dir.return_value = True _rmtree.side_effect = OSError - self.assertFalse(self.charm.backup._empty_data_files()) + tc().assertFalse(harness.charm.backup._empty_data_files()) _rmtree.assert_called_once_with(path) # Test when data files are successfully removed. _rmtree.reset_mock() _rmtree.side_effect = None - self.assertTrue(self.charm.backup._empty_data_files()) + tc().assertTrue(harness.charm.backup._empty_data_files()) _rmtree.assert_called_once_with(path) - @patch("charm.PostgresqlOperatorCharm.update_config") - def test_change_connectivity_to_database(self, _update_config): +def test_change_connectivity_to_database(harness): + with patch("charm.PostgresqlOperatorCharm.update_config") as _update_config: + peer_rel_id = harness.model.get_relation(PEER).id # Ensure that there is no connectivity info in the unit relation databag. - with self.harness.hooks_disabled(): - self.harness.update_relation_data( - self.peer_rel_id, - self.charm.unit.name, + with harness.hooks_disabled(): + harness.update_relation_data( + peer_rel_id, + harness.charm.unit.name, {"connectivity": ""}, ) # Test when connectivity should be turned on. - self.charm.backup._change_connectivity_to_database(True) - self.assertEqual( - self.harness.get_relation_data(self.peer_rel_id, self.charm.unit), + harness.charm.backup._change_connectivity_to_database(True) + tc().assertEqual( + harness.get_relation_data(peer_rel_id, harness.charm.unit), {"connectivity": "on"}, ) _update_config.assert_called_once() # Test when connectivity should be turned off. _update_config.reset_mock() - self.charm.backup._change_connectivity_to_database(False) - self.assertEqual( - self.harness.get_relation_data(self.peer_rel_id, self.charm.unit), + harness.charm.backup._change_connectivity_to_database(False) + tc().assertEqual( + harness.get_relation_data(peer_rel_id, harness.charm.unit), {"connectivity": "off"}, ) _update_config.assert_called_once() - @patch("pwd.getpwnam") - @patch("backups.run") - def test_execute_command(self, _run, _getpwnam): +def test_execute_command(harness): + with ( + patch("backups.run") as _run, + patch("pwd.getpwnam") as _getpwnam, + ): # Test when the command fails. command = "rm -r /var/lib/postgresql/data/pgdata".split() _run.return_value = CompletedProcess(command, 1, b"", b"fake stderr") - self.assertEqual(self.charm.backup._execute_command(command), (1, "", "fake stderr")) + tc().assertEqual(harness.charm.backup._execute_command(command), (1, "", "fake stderr")) _run.assert_called_once_with( command, input=None, stdout=PIPE, stderr=PIPE, preexec_fn=ANY, timeout=None ) @@ -500,8 +499,8 @@ def test_execute_command(self, _run, _getpwnam): _getpwnam.reset_mock() _run.side_effect = None _run.return_value = CompletedProcess(command, 0, b"fake stdout", b"") - self.assertEqual( - self.charm.backup._execute_command(command, command_input=b"fake input", timeout=5), + tc().assertEqual( + harness.charm.backup._execute_command(command, command_input=b"fake input", timeout=5), (0, "fake stdout", ""), ) _run.assert_called_once_with( @@ -509,33 +508,33 @@ def test_execute_command(self, _run, _getpwnam): ) _getpwnam.assert_called_once_with("snap_daemon") - def test_format_backup_list(self): - # Test when there are no backups. - self.assertEqual( - self.charm.backup._format_backup_list([]), - """backup-id | backup-type | backup-status +def test_format_backup_list(harness): + # Test when there are no backups. + tc().assertEqual( + harness.charm.backup._format_backup_list([]), + """backup-id | backup-type | backup-status ----------------------------------------------------""", - ) + ) - # Test when there are backups. - backup_list = [ - ("2023-01-01T09:00:00Z", "physical", "failed: fake error"), - ("2023-01-01T10:00:00Z", "physical", "finished"), - ] - self.assertEqual( - self.charm.backup._format_backup_list(backup_list), - """backup-id | backup-type | backup-status + # Test when there are backups. + backup_list = [ + ("2023-01-01T09:00:00Z", "physical", "failed: fake error"), + ("2023-01-01T10:00:00Z", "physical", "finished"), + ] + tc().assertEqual( + harness.charm.backup._format_backup_list(backup_list), + """backup-id | backup-type | backup-status ---------------------------------------------------- 2023-01-01T09:00:00Z | physical | failed: fake error 2023-01-01T10:00:00Z | physical | finished""", - ) + ) - @patch("charm.PostgreSQLBackups._execute_command") - def test_generate_backup_list_output(self, _execute_command): +def test_generate_backup_list_output(harness): + with patch("charm.PostgreSQLBackups._execute_command") as _execute_command: # Test when no backups are returned. _execute_command.return_value = (0, '[{"backup":[]}]', "") - self.assertEqual( - self.charm.backup._generate_backup_list_output(), + tc().assertEqual( + harness.charm.backup._generate_backup_list_output(), """backup-id | backup-type | backup-status ----------------------------------------------------""", ) @@ -546,27 +545,27 @@ def test_generate_backup_list_output(self, _execute_command): '[{"backup":[{"label":"20230101-090000F","error":"fake error"},{"label":"20230101-100000F","error":null}]}]', "", ) - self.assertEqual( - self.charm.backup._generate_backup_list_output(), + tc().assertEqual( + harness.charm.backup._generate_backup_list_output(), """backup-id | backup-type | backup-status ---------------------------------------------------- 2023-01-01T09:00:00Z | physical | failed: fake error 2023-01-01T10:00:00Z | physical | finished""", ) - @patch("charm.PostgreSQLBackups._execute_command") - def test_list_backups(self, _execute_command): +def test_list_backups(harness): + with patch("charm.PostgreSQLBackups._execute_command") as _execute_command: # Test when the command that list the backups fails. _execute_command.return_value = (1, "", "fake stderr") - with self.assertRaises(ListBackupsError): - self.assertEqual( - self.charm.backup._list_backups(show_failed=True), OrderedDict[str, str]() + with tc().assertRaises(ListBackupsError): + tc().assertEqual( + harness.charm.backup._list_backups(show_failed=True), OrderedDict[str, str]() ) # Test when no backups are available. _execute_command.return_value = (0, "[]", "") - self.assertEqual( - self.charm.backup._list_backups(show_failed=True), OrderedDict[str, str]() + tc().assertEqual( + harness.charm.backup._list_backups(show_failed=True), OrderedDict[str, str]() ) # Test when some backups are available. @@ -575,8 +574,8 @@ def test_list_backups(self, _execute_command): '[{"backup":[{"label":"20230101-090000F","error":"fake error"},{"label":"20230101-100000F","error":null}],"name":"test-stanza"}]', "", ) - self.assertEqual( - self.charm.backup._list_backups(show_failed=True), + tc().assertEqual( + harness.charm.backup._list_backups(show_failed=True), OrderedDict[str, str]([ ("2023-01-01T09:00:00Z", "test-stanza"), ("2023-01-01T10:00:00Z", "test-stanza"), @@ -584,27 +583,22 @@ def test_list_backups(self, _execute_command): ) # Test when some backups are available, but it's not desired to list failed backups. - self.assertEqual( - self.charm.backup._list_backups(show_failed=False), + tc().assertEqual( + harness.charm.backup._list_backups(show_failed=False), OrderedDict[str, str]([("2023-01-01T10:00:00Z", "test-stanza")]), ) - @patch_network_get(private_address="1.1.1.1") - @patch("charm.Patroni.reload_patroni_configuration") - @patch("charm.Patroni.member_started", new_callable=PropertyMock) - @patch("backups.wait_fixed", return_value=wait_fixed(0)) - @patch("charm.PostgresqlOperatorCharm.update_config") - @patch("charm.PostgreSQLBackups._execute_command") - @patch("charm.PostgresqlOperatorCharm.is_primary", new_callable=PropertyMock) - def test_initialise_stanza( - self, - _is_primary, - _execute_command, - _update_config, - _, - _member_started, - _reload_patroni_configuration, +@patch_network_get(private_address="1.1.1.1") +def test_initialise_stanza(harness): + with ( + patch("charm.Patroni.reload_patroni_configuration") as _reload_patroni_configuration, + patch("charm.Patroni.member_started", new_callable=PropertyMock) as _member_started, + patch("backups.wait_fixed", return_value=wait_fixed(0)), + patch("charm.PostgresqlOperatorCharm.update_config") as _update_config, + patch("charm.PostgreSQLBackups._execute_command") as _execute_command, + patch("charm.PostgresqlOperatorCharm.is_primary", new_callable=PropertyMock) as _is_primary, ): + peer_rel_id = harness.model.get_relation(PEER).id # Test when the unit is not the primary. _is_primary.return_value = False _execute_command.assert_not_called() @@ -612,8 +606,8 @@ def test_initialise_stanza( # Test when the unit is the primary, but it's in a blocked state # other than the ones can be solved by new S3 settings. _is_primary.return_value = True - self.charm.unit.status = BlockedStatus("fake blocked state") - self.charm.backup._initialise_stanza() + harness.charm.unit.status = BlockedStatus("fake blocked state") + harness.charm.backup._initialise_stanza() _execute_command.assert_not_called() # Test when the blocked state is any of the blocked stated that can be solved @@ -621,7 +615,7 @@ def test_initialise_stanza( stanza_creation_command = [ "charmed-postgresql.pgbackrest", "--config=/var/snap/charmed-postgresql/current/etc/pgbackrest/pgbackrest.conf", - f"--stanza={self.charm.backup.stanza_name}", + f"--stanza={harness.charm.backup.stanza_name}", "stanza-create", ] _execute_command.return_value = (1, "", "fake stderr") @@ -631,87 +625,82 @@ def test_initialise_stanza( FAILED_TO_INITIALIZE_STANZA_ERROR_MESSAGE, ]: _execute_command.reset_mock() - self.charm.unit.status = BlockedStatus(blocked_state) - self.charm.backup._initialise_stanza() + harness.charm.unit.status = BlockedStatus(blocked_state) + harness.charm.backup._initialise_stanza() _execute_command.assert_called_once_with(stanza_creation_command) - self.assertIsInstance(self.charm.unit.status, BlockedStatus) - self.assertEqual( - self.charm.unit.status.message, FAILED_TO_INITIALIZE_STANZA_ERROR_MESSAGE + tc().assertIsInstance(harness.charm.unit.status, BlockedStatus) + tc().assertEqual( + harness.charm.unit.status.message, FAILED_TO_INITIALIZE_STANZA_ERROR_MESSAGE ) # Assert there is no stanza name in the application relation databag. - self.assertEqual(self.harness.get_relation_data(self.peer_rel_id, self.charm.app), {}) + tc().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.app), {}) # Test when the failure in the stanza creation is due to a timeout. _execute_command.reset_mock() _execute_command.return_value = (49, "", "fake stderr") - with self.assertRaises(TimeoutError): - self.charm.backup._initialise_stanza() + with tc().assertRaises(TimeoutError): + harness.charm.backup._initialise_stanza() # Test when the archiving is working correctly (pgBackRest check command succeeds) # and the unit is not the leader. _execute_command.reset_mock() _execute_command.return_value = (0, "fake stdout", "") _member_started.return_value = True - self.charm.backup._initialise_stanza() - self.assertEqual(self.harness.get_relation_data(self.peer_rel_id, self.charm.app), {}) - self.assertEqual( - self.harness.get_relation_data(self.peer_rel_id, self.charm.unit), + harness.charm.backup._initialise_stanza() + tc().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.app), {}) + tc().assertEqual( + harness.get_relation_data(peer_rel_id, harness.charm.unit), { - "stanza": f"{self.charm.model.name}.postgresql", + "stanza": f"{harness.charm.model.name}.postgresql", "init-pgbackrest": "True", }, ) - self.assertIsInstance(self.charm.unit.status, MaintenanceStatus) + tc().assertIsInstance(harness.charm.unit.status, MaintenanceStatus) # Test when the unit is the leader. - with self.harness.hooks_disabled(): - self.harness.set_leader() - self.harness.update_relation_data( - self.peer_rel_id, self.charm.unit.name, {"stanza": "", "init-pgbackrest": ""} + with harness.hooks_disabled(): + harness.set_leader() + harness.update_relation_data( + peer_rel_id, harness.charm.unit.name, {"stanza": "", "init-pgbackrest": ""} ) - self.charm.backup._initialise_stanza() + harness.charm.backup._initialise_stanza() _update_config.assert_not_called() - self.assertEqual( - self.harness.get_relation_data(self.peer_rel_id, self.charm.app), + tc().assertEqual( + harness.get_relation_data(peer_rel_id, harness.charm.app), {"stanza": "None.postgresql", "init-pgbackrest": "True"}, ) _member_started.assert_not_called() _reload_patroni_configuration.assert_not_called() - self.assertIsInstance(self.charm.unit.status, MaintenanceStatus) - - @patch_network_get(private_address="1.1.1.1") - @patch("charm.Patroni.reload_patroni_configuration") - @patch("charm.Patroni.member_started", new_callable=PropertyMock) - @patch("backups.wait_fixed", return_value=wait_fixed(0)) - @patch("charm.PostgresqlOperatorCharm.update_config") - @patch("charm.PostgreSQLBackups._execute_command") - @patch("charm.PostgresqlOperatorCharm.is_primary", new_callable=PropertyMock) - def test_check_stanza( - self, - _is_primary, - _execute_command, - _update_config, - _, - _member_started, - _reload_patroni_configuration, + tc().assertIsInstance(harness.charm.unit.status, MaintenanceStatus) + +@patch_network_get(private_address="1.1.1.1") +def test_check_stanza(harness): + with ( + patch("charm.Patroni.reload_patroni_configuration") as _reload_patroni_configuration, + patch("charm.Patroni.member_started", new_callable=PropertyMock) as _member_started, + patch("backups.wait_fixed", return_value=wait_fixed(0)), + patch("charm.PostgresqlOperatorCharm.update_config") as _update_config, + patch("charm.PostgreSQLBackups._execute_command") as _execute_command, + patch("charm.PostgresqlOperatorCharm.is_primary", new_callable=PropertyMock) as _is_primary, ): + peer_rel_id = harness.model.get_relation(PEER).id # Set peer data flag - with self.harness.hooks_disabled(): - self.harness.update_relation_data( - self.peer_rel_id, - self.charm.app.name, + with harness.hooks_disabled(): + harness.update_relation_data( + peer_rel_id, + harness.charm.app.name, {"stanza": "test-stanza", "init-pgbackrest": "True"}, ) - self.harness.update_relation_data( - self.peer_rel_id, - self.charm.unit.name, + harness.update_relation_data( + peer_rel_id, + harness.charm.unit.name, {"stanza": "test-stanza", "init-pgbackrest": "True"}, ) # Test when the unit is not the primary. _is_primary.return_value = False - self.charm.backup.check_stanza() + harness.charm.backup.check_stanza() _execute_command.assert_not_called() # Set the unit as primary. @@ -720,27 +709,27 @@ def test_check_stanza( # Test when the archiving is not working correctly (pgBackRest check command fails). _execute_command.return_value = (49, "", "fake stderr") _member_started.return_value = True - self.charm.backup.check_stanza() - self.assertEqual(_update_config.call_count, 2) - self.assertEqual(self.harness.get_relation_data(self.peer_rel_id, self.charm.app), {}) - self.assertEqual(_member_started.call_count, 5) - self.assertEqual(_reload_patroni_configuration.call_count, 5) - self.assertEqual(self.harness.get_relation_data(self.peer_rel_id, self.charm.app), {}) - self.assertEqual(self.harness.get_relation_data(self.peer_rel_id, self.charm.unit), {}) - self.assertIsInstance(self.charm.unit.status, BlockedStatus) - self.assertEqual(self.charm.unit.status.message, FAILED_TO_INITIALIZE_STANZA_ERROR_MESSAGE) + harness.charm.backup.check_stanza() + tc().assertEqual(_update_config.call_count, 2) + tc().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.app), {}) + tc().assertEqual(_member_started.call_count, 5) + tc().assertEqual(_reload_patroni_configuration.call_count, 5) + tc().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.app), {}) + tc().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.unit), {}) + tc().assertIsInstance(harness.charm.unit.status, BlockedStatus) + tc().assertEqual(harness.charm.unit.status.message, FAILED_TO_INITIALIZE_STANZA_ERROR_MESSAGE) # Test when the archiving is working correctly (pgBackRest check command succeeds) # and the unit is not the leader. - with self.harness.hooks_disabled(): - self.harness.update_relation_data( - self.peer_rel_id, - self.charm.app.name, + with harness.hooks_disabled(): + harness.update_relation_data( + peer_rel_id, + harness.charm.app.name, {"stanza": "test-stanza", "init-pgbackrest": "True"}, ) - self.harness.update_relation_data( - self.peer_rel_id, - self.charm.unit.name, + harness.update_relation_data( + peer_rel_id, + harness.charm.unit.name, {"stanza": "test-stanza", "init-pgbackrest": "True"}, ) _execute_command.reset_mock() @@ -749,182 +738,178 @@ def test_check_stanza( _reload_patroni_configuration.reset_mock() _execute_command.side_effect = None _execute_command.return_value = (0, "fake stdout", "") - self.charm.backup.check_stanza() + harness.charm.backup.check_stanza() _update_config.assert_called_once() _member_started.assert_called_once() _reload_patroni_configuration.assert_called_once() - self.assertEqual( - self.harness.get_relation_data(self.peer_rel_id, self.charm.app), + tc().assertEqual( + harness.get_relation_data(peer_rel_id, harness.charm.app), {"stanza": "test-stanza", "init-pgbackrest": "True"}, ) - self.assertEqual( - self.harness.get_relation_data(self.peer_rel_id, self.charm.unit), + tc().assertEqual( + harness.get_relation_data(peer_rel_id, harness.charm.unit), {"stanza": "test-stanza"}, ) - self.assertIsInstance(self.charm.unit.status, ActiveStatus) + tc().assertIsInstance(harness.charm.unit.status, ActiveStatus) # Test when the unit is the leader. - self.charm.unit.status = BlockedStatus("fake blocked state") - with self.harness.hooks_disabled(): - self.harness.set_leader() - self.harness.update_relation_data( - self.peer_rel_id, - self.charm.app.name, + harness.charm.unit.status = BlockedStatus("fake blocked state") + with harness.hooks_disabled(): + harness.set_leader() + harness.update_relation_data( + peer_rel_id, + harness.charm.app.name, {"init-pgbackrest": "True"}, ) - self.harness.update_relation_data( - self.peer_rel_id, - self.charm.unit.name, + harness.update_relation_data( + peer_rel_id, + harness.charm.unit.name, {"init-pgbackrest": "True"}, ) _update_config.reset_mock() _member_started.reset_mock() _reload_patroni_configuration.reset_mock() - self.charm.backup.check_stanza() + harness.charm.backup.check_stanza() _update_config.assert_called_once() _member_started.assert_called_once() _reload_patroni_configuration.assert_called_once() - self.assertEqual( - self.harness.get_relation_data(self.peer_rel_id, self.charm.app), + tc().assertEqual( + harness.get_relation_data(peer_rel_id, harness.charm.app), {"stanza": "test-stanza"}, ) - self.assertEqual( - self.harness.get_relation_data(self.peer_rel_id, self.charm.unit), + tc().assertEqual( + harness.get_relation_data(peer_rel_id, harness.charm.unit), {"stanza": "test-stanza"}, ) - self.assertIsInstance(self.charm.unit.status, ActiveStatus) - - def test_coordinate_stanza_fields(self): - # Add a new unit to the relation. - new_unit_name = "postgresql-k8s/1" - new_unit = Unit(new_unit_name, None, self.harness.charm.app._backend, {}) - self.harness.add_relation_unit(self.peer_rel_id, new_unit_name) - - # Test when the stanza name is neither in the application relation databag nor in the unit relation databag. - self.charm.backup.coordinate_stanza_fields() - self.assertEqual(self.harness.get_relation_data(self.peer_rel_id, self.charm.app), {}) - self.assertEqual(self.harness.get_relation_data(self.peer_rel_id, self.charm.unit), {}) - self.assertEqual(self.harness.get_relation_data(self.peer_rel_id, new_unit), {}) - - # Test when the stanza name is in the unit relation databag but the unit is not the leader. - stanza_name = f"{self.charm.model.name}.{self.charm.app.name}" - with self.harness.hooks_disabled(): - self.harness.update_relation_data( - self.peer_rel_id, new_unit_name, {"stanza": stanza_name, "init-pgbackrest": "True"} - ) - self.charm.backup.coordinate_stanza_fields() - self.assertEqual(self.harness.get_relation_data(self.peer_rel_id, self.charm.app), {}) - self.assertEqual(self.harness.get_relation_data(self.peer_rel_id, self.charm.unit), {}) - self.assertEqual( - self.harness.get_relation_data(self.peer_rel_id, new_unit), - {"stanza": stanza_name, "init-pgbackrest": "True"}, - ) + tc().assertIsInstance(harness.charm.unit.status, ActiveStatus) + +def test_coordinate_stanza_fields(harness): + peer_rel_id = harness.model.get_relation(PEER).id + # Add a new unit to the relation. + new_unit_name = "postgresql-k8s/1" + new_unit = Unit(new_unit_name, None, harness.charm.app._backend, {}) + harness.add_relation_unit(peer_rel_id, new_unit_name) + + # Test when the stanza name is neither in the application relation databag nor in the unit relation databag. + harness.charm.backup.coordinate_stanza_fields() + tc().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.app), {}) + tc().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.unit), {}) + tc().assertEqual(harness.get_relation_data(peer_rel_id, new_unit), {}) + + # Test when the stanza name is in the unit relation databag but the unit is not the leader. + stanza_name = f"{harness.charm.model.name}.{harness.charm.app.name}" + with harness.hooks_disabled(): + harness.update_relation_data( + peer_rel_id, new_unit_name, {"stanza": stanza_name, "init-pgbackrest": "True"} + ) + harness.charm.backup.coordinate_stanza_fields() + tc().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.app), {}) + tc().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.unit), {}) + tc().assertEqual( + harness.get_relation_data(peer_rel_id, new_unit), + {"stanza": stanza_name, "init-pgbackrest": "True"}, + ) - # Test when the unit is the leader. - with self.harness.hooks_disabled(): - self.harness.set_leader() - self.charm.backup.coordinate_stanza_fields() - self.assertEqual( - self.harness.get_relation_data(self.peer_rel_id, self.charm.app), - {"stanza": stanza_name, "init-pgbackrest": "True"}, - ) - self.assertEqual(self.harness.get_relation_data(self.peer_rel_id, self.charm.unit), {}) - self.assertEqual( - self.harness.get_relation_data(self.peer_rel_id, new_unit), - {"stanza": stanza_name, "init-pgbackrest": "True"}, - ) - - # Test when the stanza was already checked in the primary non-leader unit. - with self.harness.hooks_disabled(): - self.harness.update_relation_data( - self.peer_rel_id, new_unit_name, {"init-pgbackrest": ""} - ) - self.charm.backup.coordinate_stanza_fields() - self.assertEqual( - self.harness.get_relation_data(self.peer_rel_id, self.charm.app), - {"stanza": stanza_name}, - ) - self.assertEqual(self.harness.get_relation_data(self.peer_rel_id, self.charm.unit), {}) - self.assertEqual( - self.harness.get_relation_data(self.peer_rel_id, new_unit), {"stanza": stanza_name} - ) + # Test when the unit is the leader. + with harness.hooks_disabled(): + harness.set_leader() + harness.charm.backup.coordinate_stanza_fields() + tc().assertEqual( + harness.get_relation_data(peer_rel_id, harness.charm.app), + {"stanza": stanza_name, "init-pgbackrest": "True"}, + ) + tc().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.unit), {}) + tc().assertEqual( + harness.get_relation_data(peer_rel_id, new_unit), + {"stanza": stanza_name, "init-pgbackrest": "True"}, + ) - # Test when the "init-pgbackrest" flag was removed from the application relation databag - # and this is the unit that has the stanza name in the unit relation databag. - with self.harness.hooks_disabled(): - self.harness.update_relation_data( - self.peer_rel_id, self.charm.unit.name, {"stanza": stanza_name} - ) - self.charm.backup.coordinate_stanza_fields() - self.assertEqual( - self.harness.get_relation_data(self.peer_rel_id, self.charm.app), - {"stanza": stanza_name}, - ) - self.assertEqual(self.harness.get_relation_data(self.peer_rel_id, self.charm.unit), {}) - self.assertEqual( - self.harness.get_relation_data(self.peer_rel_id, new_unit), {"stanza": stanza_name} + # Test when the stanza was already checked in the primary non-leader unit. + with harness.hooks_disabled(): + harness.update_relation_data( + peer_rel_id, new_unit_name, {"init-pgbackrest": ""} ) + harness.charm.backup.coordinate_stanza_fields() + tc().assertEqual( + harness.get_relation_data(peer_rel_id, harness.charm.app), + {"stanza": stanza_name}, + ) + tc().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.unit), {}) + tc().assertEqual( + harness.get_relation_data(peer_rel_id, new_unit), {"stanza": stanza_name} + ) - # Test when the unit is not the leader. - with self.harness.hooks_disabled(): - self.harness.set_leader(False) - self.harness.update_relation_data( - self.peer_rel_id, self.charm.unit.name, {"stanza": stanza_name} - ) - self.charm.backup.coordinate_stanza_fields() - self.assertEqual( - self.harness.get_relation_data(self.peer_rel_id, self.charm.app), - {"stanza": stanza_name}, + # Test when the "init-pgbackrest" flag was removed from the application relation databag + # and this is the unit that has the stanza name in the unit relation databag. + with harness.hooks_disabled(): + harness.update_relation_data( + peer_rel_id, harness.charm.unit.name, {"stanza": stanza_name} ) - self.assertEqual(self.harness.get_relation_data(self.peer_rel_id, self.charm.unit), {}) - self.assertEqual( - self.harness.get_relation_data(self.peer_rel_id, new_unit), {"stanza": stanza_name} + harness.charm.backup.coordinate_stanza_fields() + tc().assertEqual( + harness.get_relation_data(peer_rel_id, harness.charm.app), + {"stanza": stanza_name}, + ) + tc().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.unit), {}) + tc().assertEqual( + harness.get_relation_data(peer_rel_id, new_unit), {"stanza": stanza_name} + ) + + # Test when the unit is not the leader. + with harness.hooks_disabled(): + harness.set_leader(False) + harness.update_relation_data( + peer_rel_id, harness.charm.unit.name, {"stanza": stanza_name} ) + harness.charm.backup.coordinate_stanza_fields() + tc().assertEqual( + harness.get_relation_data(peer_rel_id, harness.charm.app), + {"stanza": stanza_name}, + ) + tc().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.unit), {}) + tc().assertEqual( + harness.get_relation_data(peer_rel_id, new_unit), {"stanza": stanza_name} + ) - @patch("charm.PostgreSQLBackups._execute_command") - @patch("charm.PostgresqlOperatorCharm.primary_endpoint", new_callable=PropertyMock) - @patch("charm.Patroni.get_primary") - def test_is_primary_pgbackrest_service_running( - self, _get_primary, _primary_endpoint, _execute_command +def test_is_primary_pgbackrest_service_running(harness): + with ( + patch("charm.PostgreSQLBackups._execute_command") as _execute_command, + patch("charm.PostgresqlOperatorCharm.primary_endpoint", new_callable=PropertyMock) as _primary_endpoint, + patch("charm.Patroni.get_primary") as _get_primary, ): # Test when the pgBackRest fails to contact the primary server. _get_primary.side_effect = None _execute_command.return_value = (1, "", "fake stderr") - self.assertFalse(self.charm.backup._is_primary_pgbackrest_service_running) + tc().assertFalse(harness.charm.backup._is_primary_pgbackrest_service_running) _execute_command.assert_called_once() # Test when the endpoint is not generated. _execute_command.reset_mock() _primary_endpoint.return_value = None - self.assertFalse(self.charm.backup._is_primary_pgbackrest_service_running) + tc().assertFalse(harness.charm.backup._is_primary_pgbackrest_service_running) _execute_command.assert_not_called() # Test when the pgBackRest succeeds on contacting the primary server. _execute_command.reset_mock() _execute_command.return_value = (0, "fake stdout", "") _primary_endpoint.return_value = "fake_endpoint" - self.assertTrue(self.charm.backup._is_primary_pgbackrest_service_running) + tc().assertTrue(harness.charm.backup._is_primary_pgbackrest_service_running) _execute_command.assert_called_once() - @patch("charm.PostgreSQLBackups._initialise_stanza") - @patch("charm.PostgreSQLBackups.can_use_s3_repository") - @patch("charm.PostgreSQLBackups._create_bucket_if_not_exists") - @patch("charm.PostgresqlOperatorCharm.is_primary", new_callable=PropertyMock) - @patch("charm.PostgreSQLBackups._render_pgbackrest_conf_file") - @patch("ops.framework.EventBase.defer") - def test_on_s3_credential_changed( - self, - _defer, - _render_pgbackrest_conf_file, - _is_primary, - _create_bucket_if_not_exists, - _can_use_s3_repository, - _initialise_stanza, +def test_on_s3_credential_changed(harness): + with ( + patch("charm.PostgreSQLBackups._initialise_stanza") as _initialise_stanza, + patch("charm.PostgreSQLBackups.can_use_s3_repository") as _can_use_s3_repository, + patch("charm.PostgreSQLBackups._create_bucket_if_not_exists") as _create_bucket_if_not_exists, + patch("charm.PostgresqlOperatorCharm.is_primary", new_callable=PropertyMock) as _is_primary, + patch("charm.PostgreSQLBackups._render_pgbackrest_conf_file") as _render_pgbackrest_conf_file, + patch("ops.framework.EventBase.defer") as _defer, ): + peer_rel_id = harness.model.get_relation(PEER).id # Test when the cluster was not initialised yet. - self.relate_to_s3_integrator() - self.charm.backup.s3_client.on.credentials_changed.emit( - relation=self.harness.model.get_relation(S3_PARAMETERS_RELATION, self.s3_rel_id) + s3_rel_id = harness.add_relation(S3_PARAMETERS_RELATION, "s3-integrator") + harness.charm.backup.s3_client.on.credentials_changed.emit( + relation=harness.model.get_relation(S3_PARAMETERS_RELATION, s3_rel_id) ) _defer.assert_called_once() _render_pgbackrest_conf_file.assert_not_called() @@ -935,16 +920,16 @@ def test_on_s3_credential_changed( # Test when the cluster is already initialised, but the charm fails to render # the pgBackRest configuration file due to missing S3 parameters. _defer.reset_mock() - with self.harness.hooks_disabled(): - self.harness.update_relation_data( - self.peer_rel_id, - self.charm.app.name, + with harness.hooks_disabled(): + harness.update_relation_data( + peer_rel_id, + harness.charm.app.name, {"cluster_initialised": "True"}, ) _render_pgbackrest_conf_file.return_value = False _is_primary.return_value = False - self.charm.backup.s3_client.on.credentials_changed.emit( - relation=self.harness.model.get_relation(S3_PARAMETERS_RELATION, self.s3_rel_id) + harness.charm.backup.s3_client.on.credentials_changed.emit( + relation=harness.model.get_relation(S3_PARAMETERS_RELATION, s3_rel_id) ) _defer.assert_not_called() _render_pgbackrest_conf_file.assert_called_once() @@ -953,22 +938,22 @@ def test_on_s3_credential_changed( _initialise_stanza.assert_not_called() # Test that followers will not initialise the bucket - self.charm.unit.status = ActiveStatus() + harness.charm.unit.status = ActiveStatus() _render_pgbackrest_conf_file.reset_mock() - with self.harness.hooks_disabled(): - self.harness.update_relation_data( - self.peer_rel_id, - self.charm.app.name, + with harness.hooks_disabled(): + harness.update_relation_data( + peer_rel_id, + harness.charm.app.name, {"cluster_initialised": "True"}, ) _render_pgbackrest_conf_file.return_value = True - self.charm.backup.s3_client.on.credentials_changed.emit( - relation=self.harness.model.get_relation(S3_PARAMETERS_RELATION, self.s3_rel_id) + harness.charm.backup.s3_client.on.credentials_changed.emit( + relation=harness.model.get_relation(S3_PARAMETERS_RELATION, s3_rel_id) ) _render_pgbackrest_conf_file.assert_called_once() _create_bucket_if_not_exists.assert_not_called() - self.assertIsInstance(self.charm.unit.status, ActiveStatus) + tc().assertIsInstance(harness.charm.unit.status, ActiveStatus) _can_use_s3_repository.assert_not_called() _initialise_stanza.assert_not_called() @@ -985,14 +970,14 @@ def test_on_s3_credential_changed( _render_pgbackrest_conf_file.reset_mock() _create_bucket_if_not_exists.reset_mock() _create_bucket_if_not_exists.side_effect = error - self.charm.backup.s3_client.on.credentials_changed.emit( - relation=self.harness.model.get_relation(S3_PARAMETERS_RELATION, self.s3_rel_id) + harness.charm.backup.s3_client.on.credentials_changed.emit( + relation=harness.model.get_relation(S3_PARAMETERS_RELATION, s3_rel_id) ) _render_pgbackrest_conf_file.assert_called_once() _create_bucket_if_not_exists.assert_called_once() - self.assertIsInstance(self.charm.unit.status, BlockedStatus) - self.assertEqual( - self.charm.unit.status.message, FAILED_TO_ACCESS_CREATE_BUCKET_ERROR_MESSAGE + tc().assertIsInstance(harness.charm.unit.status, BlockedStatus) + tc().assertEqual( + harness.charm.unit.status.message, FAILED_TO_ACCESS_CREATE_BUCKET_ERROR_MESSAGE ) _can_use_s3_repository.assert_not_called() _initialise_stanza.assert_not_called() @@ -1001,11 +986,11 @@ def test_on_s3_credential_changed( _create_bucket_if_not_exists.reset_mock() _create_bucket_if_not_exists.side_effect = None _can_use_s3_repository.return_value = (False, "fake validation message") - self.charm.backup.s3_client.on.credentials_changed.emit( - relation=self.harness.model.get_relation(S3_PARAMETERS_RELATION, self.s3_rel_id) + harness.charm.backup.s3_client.on.credentials_changed.emit( + relation=harness.model.get_relation(S3_PARAMETERS_RELATION, s3_rel_id) ) - self.assertIsInstance(self.charm.unit.status, BlockedStatus) - self.assertEqual(self.charm.unit.status.message, "fake validation message") + tc().assertIsInstance(harness.charm.unit.status, BlockedStatus) + tc().assertEqual(harness.charm.unit.status.message, "fake validation message") _create_bucket_if_not_exists.assert_called_once() _can_use_s3_repository.assert_called_once() _initialise_stanza.assert_not_called() @@ -1013,81 +998,72 @@ def test_on_s3_credential_changed( # Test when the stanza can be initialised and the pgBackRest service can start. _can_use_s3_repository.reset_mock() _can_use_s3_repository.return_value = (True, None) - self.charm.backup.s3_client.on.credentials_changed.emit( - relation=self.harness.model.get_relation(S3_PARAMETERS_RELATION, self.s3_rel_id) + harness.charm.backup.s3_client.on.credentials_changed.emit( + relation=harness.model.get_relation(S3_PARAMETERS_RELATION, s3_rel_id) ) _can_use_s3_repository.assert_called_once() _initialise_stanza.assert_called_once() - def test_on_s3_credential_gone(self): - # Test that unrelated blocks will remain - self.charm.unit.status = BlockedStatus("test block") - self.charm.backup._on_s3_credential_gone(None) - self.assertIsInstance(self.charm.unit.status, BlockedStatus) - - # Test that s3 related blocks will be cleared - self.charm.unit.status = BlockedStatus(ANOTHER_CLUSTER_REPOSITORY_ERROR_MESSAGE) - self.charm.backup._on_s3_credential_gone(None) - self.assertIsInstance(self.charm.unit.status, ActiveStatus) - - # Test removal of relation data when the unit is not the leader. - with self.harness.hooks_disabled(): - self.harness.update_relation_data( - self.peer_rel_id, - self.charm.app.name, - {"stanza": "test-stanza", "init-pgbackrest": "True"}, - ) - self.harness.update_relation_data( - self.peer_rel_id, - self.charm.app.name, - {"stanza": "test-stanza", "init-pgbackrest": "True"}, - ) - self.charm.backup._on_s3_credential_gone(None) - self.assertEqual( - self.harness.get_relation_data(self.peer_rel_id, self.charm.app), +def test_on_s3_credential_gone(harness): + peer_rel_id = harness.model.get_relation(PEER).id + # Test that unrelated blocks will remain + harness.charm.unit.status = BlockedStatus("test block") + harness.charm.backup._on_s3_credential_gone(None) + tc().assertIsInstance(harness.charm.unit.status, BlockedStatus) + + # Test that s3 related blocks will be cleared + harness.charm.unit.status = BlockedStatus(ANOTHER_CLUSTER_REPOSITORY_ERROR_MESSAGE) + harness.charm.backup._on_s3_credential_gone(None) + tc().assertIsInstance(harness.charm.unit.status, ActiveStatus) + + # Test removal of relation data when the unit is not the leader. + with harness.hooks_disabled(): + harness.update_relation_data( + peer_rel_id, + harness.charm.app.name, {"stanza": "test-stanza", "init-pgbackrest": "True"}, ) - self.assertEqual(self.harness.get_relation_data(self.peer_rel_id, self.charm.unit), {}) - - # Test removal of relation data when the unit is the leader. - with self.harness.hooks_disabled(): - self.harness.set_leader() - self.harness.update_relation_data( - self.peer_rel_id, - self.charm.unit.name, - {"stanza": "test-stanza", "init-pgbackrest": "True"}, - ) - self.charm.backup._on_s3_credential_gone(None) - self.assertEqual(self.harness.get_relation_data(self.peer_rel_id, self.charm.app), {}) - self.assertEqual(self.harness.get_relation_data(self.peer_rel_id, self.charm.unit), {}) - - @patch("charm.PostgresqlOperatorCharm.update_config") - @patch("charm.PostgreSQLBackups._change_connectivity_to_database") - @patch("charm.PostgreSQLBackups._list_backups") - @patch("charm.PostgreSQLBackups._execute_command") - @patch("charm.PostgresqlOperatorCharm.is_primary", new_callable=PropertyMock) - @patch("charm.PostgreSQLBackups._upload_content_to_s3") - @patch("backups.datetime") - @patch("ops.JujuVersion.from_environ") - @patch("charm.PostgreSQLBackups._retrieve_s3_parameters") - @patch("charm.PostgreSQLBackups._can_unit_perform_backup") - def test_on_create_backup_action( - self, - _can_unit_perform_backup, - _retrieve_s3_parameters, - _from_environ, - _datetime, - _upload_content_to_s3, - _is_primary, - _execute_command, - _list_backups, - _change_connectivity_to_database, - _update_config, + harness.update_relation_data( + peer_rel_id, + harness.charm.app.name, + {"stanza": "test-stanza", "init-pgbackrest": "True"}, + ) + harness.charm.backup._on_s3_credential_gone(None) + tc().assertEqual( + harness.get_relation_data(peer_rel_id, harness.charm.app), + {"stanza": "test-stanza", "init-pgbackrest": "True"}, + ) + tc().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.unit), {}) + + # Test removal of relation data when the unit is the leader. + with harness.hooks_disabled(): + harness.set_leader() + harness.update_relation_data( + peer_rel_id, + harness.charm.unit.name, + {"stanza": "test-stanza", "init-pgbackrest": "True"}, + ) + harness.charm.backup._on_s3_credential_gone(None) + tc().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.app), {}) + tc().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.unit), {}) + +def test_on_create_backup_action(harness): + with ( + patch("charm.PostgresqlOperatorCharm.update_config") as _update_config, + patch("charm.PostgreSQLBackups._change_connectivity_to_database") as _change_connectivity_to_database, + patch("charm.PostgreSQLBackups._list_backups") as _list_backups, + patch("charm.PostgreSQLBackups._execute_command") as _execute_command, + patch("charm.PostgresqlOperatorCharm.is_primary", new_callable=PropertyMock) as _is_primary, + patch("charm.PostgreSQLBackups._upload_content_to_s3") as _upload_content_to_s3, + patch("backups.datetime") as _datetime, + patch("ops.JujuVersion.from_environ") as _from_environ, + patch("charm.PostgreSQLBackups._retrieve_s3_parameters") as _retrieve_s3_parameters, + patch("charm.PostgreSQLBackups._can_unit_perform_backup") as _can_unit_perform_backup, ): # Test when the unit cannot perform a backup. mock_event = MagicMock() _can_unit_perform_backup.return_value = (False, "fake validation message") - self.charm.backup._on_create_backup_action(mock_event) + harness.charm.backup._on_create_backup_action(mock_event) mock_event.fail.assert_called_once() mock_event.set_results.assert_not_called() @@ -1110,15 +1086,15 @@ def test_on_create_backup_action( _from_environ.return_value = "test-juju-version" _upload_content_to_s3.return_value = False expected_metadata = f"""Date Backup Requested: 2023-01-01T09:00:00Z -Model Name: {self.charm.model.name} -Application Name: {self.charm.model.app.name} -Unit Name: {self.charm.unit.name} +Model Name: {harness.charm.model.name} +Application Name: {harness.charm.model.app.name} +Unit Name: {harness.charm.unit.name} Juju Version: test-juju-version """ - self.charm.backup._on_create_backup_action(mock_event) + harness.charm.backup._on_create_backup_action(mock_event) _upload_content_to_s3.assert_called_once_with( expected_metadata, - f"test-path/backup/{self.charm.model.name}.{self.charm.cluster_name}/latest", + f"test-path/backup/{harness.charm.model.name}.{harness.charm.cluster_name}/latest", mock_s3_parameters, ) mock_event.fail.assert_called_once() @@ -1129,7 +1105,7 @@ def test_on_create_backup_action( _upload_content_to_s3.return_value = True _is_primary.return_value = True _execute_command.return_value = (1, "", "fake error") - self.charm.backup._on_create_backup_action(mock_event) + harness.charm.backup._on_create_backup_action(mock_event) update_config_calls = [ call(is_creating_backup=True), call(is_creating_backup=False), @@ -1144,18 +1120,18 @@ def test_on_create_backup_action( _upload_content_to_s3.side_effect = [True, False] _execute_command.side_effect = None _execute_command.return_value = (0, "fake stdout", "fake stderr") - _list_backups.return_value = {"2023-01-01T09:00:00Z": self.charm.backup.stanza_name} + _list_backups.return_value = {"2023-01-01T09:00:00Z": harness.charm.backup.stanza_name} _update_config.reset_mock() - self.charm.backup._on_create_backup_action(mock_event) + harness.charm.backup._on_create_backup_action(mock_event) _upload_content_to_s3.assert_has_calls([ call( expected_metadata, - f"test-path/backup/{self.charm.model.name}.{self.charm.cluster_name}/latest", + f"test-path/backup/{harness.charm.model.name}.{harness.charm.cluster_name}/latest", mock_s3_parameters, ), call( "Stdout:\nfake stdout\n\nStderr:\nfake stderr\n", - f"test-path/backup/{self.charm.model.name}.{self.charm.cluster_name}/2023-01-01T09:00:00Z/backup.log", + f"test-path/backup/{harness.charm.model.name}.{harness.charm.cluster_name}/2023-01-01T09:00:00Z/backup.log", mock_s3_parameters, ), ]) @@ -1169,16 +1145,16 @@ def test_on_create_backup_action( _upload_content_to_s3.side_effect = None _upload_content_to_s3.return_value = True _update_config.reset_mock() - self.charm.backup._on_create_backup_action(mock_event) + harness.charm.backup._on_create_backup_action(mock_event) _upload_content_to_s3.assert_has_calls([ call( expected_metadata, - f"test-path/backup/{self.charm.model.name}.{self.charm.cluster_name}/latest", + f"test-path/backup/{harness.charm.model.name}.{harness.charm.cluster_name}/latest", mock_s3_parameters, ), call( "Stdout:\nfake stdout\n\nStderr:\nfake stderr\n", - f"test-path/backup/{self.charm.model.name}.{self.charm.cluster_name}/2023-01-01T09:00:00Z/backup.log", + f"test-path/backup/{harness.charm.model.name}.{harness.charm.cluster_name}/2023-01-01T09:00:00Z/backup.log", mock_s3_parameters, ), ]) @@ -1191,30 +1167,32 @@ def test_on_create_backup_action( mock_event.reset_mock() _upload_content_to_s3.reset_mock() _is_primary.return_value = False - self.charm.backup._on_create_backup_action(mock_event) + harness.charm.backup._on_create_backup_action(mock_event) _upload_content_to_s3.assert_has_calls([ call( expected_metadata, - f"test-path/backup/{self.charm.model.name}.{self.charm.cluster_name}/latest", + f"test-path/backup/{harness.charm.model.name}.{harness.charm.cluster_name}/latest", mock_s3_parameters, ), call( "Stdout:\nfake stdout\n\nStderr:\nfake stderr\n", - f"test-path/backup/{self.charm.model.name}.{self.charm.cluster_name}/2023-01-01T09:00:00Z/backup.log", + f"test-path/backup/{harness.charm.model.name}.{harness.charm.cluster_name}/2023-01-01T09:00:00Z/backup.log", mock_s3_parameters, ), ]) - self.assertEqual(_change_connectivity_to_database.call_count, 2) + tc().assertEqual(_change_connectivity_to_database.call_count, 2) mock_event.fail.assert_not_called() mock_event.set_results.assert_called_once_with({"backup-status": "backup created"}) - @patch("charm.PostgreSQLBackups._generate_backup_list_output") - @patch("charm.PostgreSQLBackups._are_backup_settings_ok") - def test_on_list_backups_action(self, _are_backup_settings_ok, _generate_backup_list_output): +def test_on_list_backups_action(harness): + with ( + patch("charm.PostgreSQLBackups._generate_backup_list_output") as _generate_backup_list_output, + patch("charm.PostgreSQLBackups._are_backup_settings_ok") as _are_backup_settings_ok, + ): # Test when not all backup settings are ok. mock_event = MagicMock() _are_backup_settings_ok.return_value = (False, "fake validation message") - self.charm.backup._on_list_backups_action(mock_event) + harness.charm.backup._on_list_backups_action(mock_event) mock_event.fail.assert_called_once() _generate_backup_list_output.assert_not_called() mock_event.set_results.assert_not_called() @@ -1223,7 +1201,7 @@ def test_on_list_backups_action(self, _are_backup_settings_ok, _generate_backup_ mock_event.reset_mock() _are_backup_settings_ok.return_value = (True, None) _generate_backup_list_output.side_effect = ListBackupsError - self.charm.backup._on_list_backups_action(mock_event) + harness.charm.backup._on_list_backups_action(mock_event) _generate_backup_list_output.assert_called_once() mock_event.fail.assert_called_once() mock_event.set_results.assert_not_called() @@ -1234,44 +1212,37 @@ def test_on_list_backups_action(self, _are_backup_settings_ok, _generate_backup_ _are_backup_settings_ok.return_value = (True, None) _generate_backup_list_output.side_effect = None _generate_backup_list_output.return_value = """backup-id | backup-type | backup-status ----------------------------------------------------- -2023-01-01T09:00:00Z | physical | failed: fake error -2023-01-01T10:00:00Z | physical | finished""" - self.charm.backup._on_list_backups_action(mock_event) + ---------------------------------------------------- + 2023-01-01T09:00:00Z | physical | failed: fake error + 2023-01-01T10:00:00Z | physical | finished""" + harness.charm.backup._on_list_backups_action(mock_event) _generate_backup_list_output.assert_called_once() mock_event.set_results.assert_called_once_with({ "backups": """backup-id | backup-type | backup-status ----------------------------------------------------- -2023-01-01T09:00:00Z | physical | failed: fake error -2023-01-01T10:00:00Z | physical | finished""" + ---------------------------------------------------- + 2023-01-01T09:00:00Z | physical | failed: fake error + 2023-01-01T10:00:00Z | physical | finished""" }) mock_event.fail.assert_not_called() - @patch_network_get(private_address="1.1.1.1") - @patch("charm.Patroni.start_patroni") - @patch("charm.PostgresqlOperatorCharm.update_config") - @patch("charm.PostgreSQLBackups._empty_data_files") - @patch("charm.PostgreSQLBackups._restart_database") - @patch("charm.PostgreSQLBackups._execute_command") - @patch("charm.Patroni.stop_patroni") - @patch("charm.PostgreSQLBackups._list_backups") - @patch("charm.PostgreSQLBackups._pre_restore_checks") - def test_on_restore_action( - self, - _pre_restore_checks, - _list_backups, - _stop_patroni, - _execute_command, - _restart_database, - _empty_data_files, - _update_config, - _start_patroni, +@patch_network_get(private_address="1.1.1.1") +def test_on_restore_action(harness): + with ( + patch("charm.Patroni.start_patroni") as _start_patroni, + patch("charm.PostgresqlOperatorCharm.update_config") as _update_config, + patch("charm.PostgreSQLBackups._empty_data_files") as _empty_data_files, + patch("charm.PostgreSQLBackups._restart_database") as _restart_database, + patch("charm.PostgreSQLBackups._execute_command") as _execute_command, + patch("charm.Patroni.stop_patroni") as _stop_patroni, + patch("charm.PostgreSQLBackups._list_backups") as _list_backups, + patch("charm.PostgreSQLBackups._pre_restore_checks") as _pre_restore_checks, ): + peer_rel_id = harness.model.get_relation(PEER).id # Test when pre restore checks fail. mock_event = MagicMock() _pre_restore_checks.return_value = False - self.charm.unit.status = ActiveStatus() - self.charm.backup._on_restore_action(mock_event) + harness.charm.unit.status = ActiveStatus() + harness.charm.backup._on_restore_action(mock_event) _list_backups.assert_not_called() _stop_patroni.assert_not_called() _execute_command.assert_not_called() @@ -1281,14 +1252,14 @@ def test_on_restore_action( _start_patroni.assert_not_called() mock_event.fail.assert_not_called() mock_event.set_results.assert_not_called() - self.assertNotIsInstance(self.charm.unit.status, MaintenanceStatus) + tc().assertNotIsInstance(harness.charm.unit.status, MaintenanceStatus) # Test when the user provides an invalid backup id. mock_event.params = {"backup-id": "2023-01-01T10:00:00Z"} _pre_restore_checks.return_value = True - _list_backups.return_value = {"2023-01-01T09:00:00Z": self.charm.backup.stanza_name} - self.charm.unit.status = ActiveStatus() - self.charm.backup._on_restore_action(mock_event) + _list_backups.return_value = {"2023-01-01T09:00:00Z": harness.charm.backup.stanza_name} + harness.charm.unit.status = ActiveStatus() + harness.charm.backup._on_restore_action(mock_event) _list_backups.assert_called_once_with(show_failed=False) mock_event.fail.assert_called_once() _stop_patroni.assert_not_called() @@ -1298,13 +1269,13 @@ def test_on_restore_action( _update_config.assert_not_called() _start_patroni.assert_not_called() mock_event.set_results.assert_not_called() - self.assertNotIsInstance(self.charm.unit.status, MaintenanceStatus) + tc().assertNotIsInstance(harness.charm.unit.status, MaintenanceStatus) # Test when the charm fails to stop the workload. mock_event.reset_mock() mock_event.params = {"backup-id": "2023-01-01T09:00:00Z"} _stop_patroni.return_value = False - self.charm.backup._on_restore_action(mock_event) + harness.charm.backup._on_restore_action(mock_event) _stop_patroni.assert_called_once() mock_event.fail.assert_called_once() _execute_command.assert_not_called() @@ -1319,7 +1290,7 @@ def test_on_restore_action( mock_event.params = {"backup-id": "2023-01-01T09:00:00Z"} _stop_patroni.return_value = True _empty_data_files.return_value = False - self.charm.backup._on_restore_action(mock_event) + harness.charm.backup._on_restore_action(mock_event) _empty_data_files.assert_called_once() mock_event.fail.assert_called_once() _restart_database.assert_called_once() @@ -1332,13 +1303,13 @@ def test_on_restore_action( _restart_database.reset_mock() _empty_data_files.return_value = True _execute_command.return_value = (1, "", "fake stderr") - self.assertEqual(self.harness.get_relation_data(self.peer_rel_id, self.charm.app), {}) - self.charm.backup._on_restore_action(mock_event) - self.assertEqual( - self.harness.get_relation_data(self.peer_rel_id, self.charm.app), + tc().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.app), {}) + harness.charm.backup._on_restore_action(mock_event) + tc().assertEqual( + harness.get_relation_data(peer_rel_id, harness.charm.app), { "restoring-backup": "20230101-090000F", - "restore-stanza": f"{self.charm.model.name}.{self.charm.cluster_name}", + "restore-stanza": f"{harness.charm.model.name}.{harness.charm.cluster_name}", }, ) _execute_command.assert_called_once_with( @@ -1347,7 +1318,7 @@ def test_on_restore_action( "-c", "/var/snap/charmed-postgresql/current/etc/patroni/patroni.yaml", "remove", - self.charm.app.name, + harness.charm.app.name, ], command_input=b"postgresql\nYes I am aware", timeout=10, @@ -1362,59 +1333,63 @@ def test_on_restore_action( mock_event.reset_mock() _restart_database.reset_mock() _execute_command.return_value = (0, "fake stdout", "") - self.charm.backup._on_restore_action(mock_event) + harness.charm.backup._on_restore_action(mock_event) _restart_database.assert_not_called() mock_event.fail.assert_not_called() mock_event.set_results.assert_called_once_with({"restore-status": "restore started"}) - @patch("ops.model.Application.planned_units") - @patch("charm.PostgreSQLBackups._are_backup_settings_ok") - def test_pre_restore_checks(self, _are_backup_settings_ok, _planned_units): +def test_pre_restore_checks(harness): + with ( + patch("ops.model.Application.planned_units") as _planned_units, + patch("charm.PostgreSQLBackups._are_backup_settings_ok") as _are_backup_settings_ok, + ): # Test when S3 parameters are not ok. mock_event = MagicMock(params={}) _are_backup_settings_ok.return_value = (False, "fake error message") - self.assertEqual(self.charm.backup._pre_restore_checks(mock_event), False) + tc().assertEqual(harness.charm.backup._pre_restore_checks(mock_event), False) mock_event.fail.assert_called_once() # Test when no backup id is provided. mock_event.reset_mock() _are_backup_settings_ok.return_value = (True, None) - self.assertEqual(self.charm.backup._pre_restore_checks(mock_event), False) + tc().assertEqual(harness.charm.backup._pre_restore_checks(mock_event), False) mock_event.fail.assert_called_once() # Test when the unit is in a blocked state that is not recoverable by changing # S3 parameters. mock_event.reset_mock() mock_event.params = {"backup-id": "2023-01-01T09:00:00Z"} - self.charm.unit.status = BlockedStatus("fake blocked state") - self.assertEqual(self.charm.backup._pre_restore_checks(mock_event), False) + harness.charm.unit.status = BlockedStatus("fake blocked state") + tc().assertEqual(harness.charm.backup._pre_restore_checks(mock_event), False) mock_event.fail.assert_called_once() # Test when the unit is in a blocked state that is recoverable by changing S3 parameters, # but the cluster has more than one unit. mock_event.reset_mock() - self.charm.unit.status = BlockedStatus(ANOTHER_CLUSTER_REPOSITORY_ERROR_MESSAGE) + harness.charm.unit.status = BlockedStatus(ANOTHER_CLUSTER_REPOSITORY_ERROR_MESSAGE) _planned_units.return_value = 2 - self.assertEqual(self.charm.backup._pre_restore_checks(mock_event), False) + tc().assertEqual(harness.charm.backup._pre_restore_checks(mock_event), False) mock_event.fail.assert_called_once() # Test when the cluster has only one unit, but it's not the leader yet. mock_event.reset_mock() _planned_units.return_value = 1 - self.assertEqual(self.charm.backup._pre_restore_checks(mock_event), False) + tc().assertEqual(harness.charm.backup._pre_restore_checks(mock_event), False) mock_event.fail.assert_called_once() # Test when everything is ok to run a restore. mock_event.reset_mock() - with self.harness.hooks_disabled(): - self.harness.set_leader() - self.assertEqual(self.charm.backup._pre_restore_checks(mock_event), True) + with harness.hooks_disabled(): + harness.set_leader() + tc().assertEqual(harness.charm.backup._pre_restore_checks(mock_event), True) mock_event.fail.assert_not_called() - @patch_network_get(private_address="1.1.1.1") - @patch("charm.Patroni.render_file") - @patch("charm.PostgreSQLBackups._retrieve_s3_parameters") - def test_render_pgbackrest_conf_file(self, _retrieve_s3_parameters, _render_file): +@patch_network_get(private_address="1.1.1.1") +def test_render_pgbackrest_conf_file(harness): + with ( + patch("charm.Patroni.render_file") as _render_file, + patch("charm.PostgreSQLBackups._retrieve_s3_parameters") as _retrieve_s3_parameters, + ): # Set up a mock for the `open` method, set returned data to postgresql.conf template. with open("templates/pgbackrest.conf.j2", "r") as f: mock = mock_open(read_data=f.read()) @@ -1425,7 +1400,7 @@ def test_render_pgbackrest_conf_file(self, _retrieve_s3_parameters, _render_file # Patch the `open` method with our mock. with patch("builtins.open", mock, create=True): # Call the method - self.charm.backup._render_pgbackrest_conf_file() + harness.charm.backup._render_pgbackrest_conf_file() mock.assert_not_called() _render_file.assert_not_called() @@ -1448,8 +1423,8 @@ def test_render_pgbackrest_conf_file(self, _retrieve_s3_parameters, _render_file with open("templates/pgbackrest.conf.j2") as file: template = Template(file.read()) expected_content = template.render( - enable_tls=self.charm.is_tls_enabled and len(self.charm.peer_members_endpoints) > 0, - peer_endpoints=self.charm._peer_members_ips, + enable_tls=harness.charm.is_tls_enabled and len(harness.charm.peer_members_endpoints) > 0, + peer_endpoints=harness.charm._peer_members_ips, path="test-path/", data_path="/var/snap/charmed-postgresql/common/var/lib/postgresql", log_path="/var/snap/charmed-postgresql/common/var/log/pgbackrest", @@ -1459,18 +1434,18 @@ def test_render_pgbackrest_conf_file(self, _retrieve_s3_parameters, _render_file s3_uri_style="path", access_key="test-access-key", secret_key="test-secret-key", - stanza=self.charm.backup.stanza_name, - storage_path=self.charm._storage_path, + stanza=harness.charm.backup.stanza_name, + storage_path=harness.charm._storage_path, user="backup", ) # Patch the `open` method with our mock. with patch("builtins.open", mock, create=True): # Call the method - self.charm.backup._render_pgbackrest_conf_file() + harness.charm.backup._render_pgbackrest_conf_file() # Check the template is opened read-only in the call to open. - self.assertEqual(mock.call_args_list[0][0], ("templates/pgbackrest.conf.j2", "r")) + tc().assertEqual(mock.call_args_list[0][0], ("templates/pgbackrest.conf.j2", "r")) # Ensure the correct rendered template is sent to _render_file method. _render_file.assert_called_once_with( @@ -1479,30 +1454,33 @@ def test_render_pgbackrest_conf_file(self, _retrieve_s3_parameters, _render_file 0o644, ) - @patch_network_get(private_address="1.1.1.1") - @patch("charm.Patroni.start_patroni") - @patch("charm.PostgresqlOperatorCharm.update_config") - def test_restart_database(self, _update_config, _start_patroni): - with self.harness.hooks_disabled(): - self.harness.update_relation_data( - self.peer_rel_id, - self.charm.unit.name, +@patch_network_get(private_address="1.1.1.1") +def test_restart_database(harness): + with ( + patch("charm.Patroni.start_patroni") as _start_patroni, + patch("charm.PostgresqlOperatorCharm.update_config") as _update_config, + ): + peer_rel_id = harness.model.get_relation(PEER).id + with harness.hooks_disabled(): + harness.update_relation_data( + peer_rel_id, + harness.charm.unit.name, {"restoring-backup": "2023-01-01T09:00:00Z"}, ) - self.charm.backup._restart_database() + harness.charm.backup._restart_database() # Assert that the backup id is not in the application relation databag anymore. - self.assertEqual(self.harness.get_relation_data(self.peer_rel_id, self.charm.app), {}) + tc().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.app), {}) _update_config.assert_called_once() _start_patroni.assert_called_once() - @patch("charms.data_platform_libs.v0.s3.S3Requirer.get_s3_connection_info") - def test_retrieve_s3_parameters(self, _get_s3_connection_info): +def test_retrieve_s3_parameters(harness): + with patch("charms.data_platform_libs.v0.s3.S3Requirer.get_s3_connection_info") as _get_s3_connection_info: # Test when there are missing S3 parameters. _get_s3_connection_info.return_value = {} - self.assertEqual( - self.charm.backup._retrieve_s3_parameters(), + tc().assertEqual( + harness.charm.backup._retrieve_s3_parameters(), ({}, ["bucket", "access-key", "secret-key"]), ) @@ -1512,8 +1490,8 @@ def test_retrieve_s3_parameters(self, _get_s3_connection_info): "access-key": "test-access-key", "secret-key": "test-secret-key", } - self.assertEqual( - self.charm.backup._retrieve_s3_parameters(), + tc().assertEqual( + harness.charm.backup._retrieve_s3_parameters(), ( { "access-key": "test-access-key", @@ -1538,8 +1516,8 @@ def test_retrieve_s3_parameters(self, _get_s3_connection_info): "region": " us-east-1 ", "s3-uri-style": " path ", } - self.assertEqual( - self.charm.backup._retrieve_s3_parameters(), + tc().assertEqual( + harness.charm.backup._retrieve_s3_parameters(), ( { "access-key": "test-access-key", @@ -1554,32 +1532,25 @@ def test_retrieve_s3_parameters(self, _get_s3_connection_info): ), ) - @patch( - "charm.PostgreSQLBackups._is_primary_pgbackrest_service_running", new_callable=PropertyMock - ) - @patch("charm.PostgresqlOperatorCharm.is_primary", new_callable=PropertyMock) - @patch("backups.snap.SnapCache") - @patch("charm.PostgresqlOperatorCharm._peer_members_ips", new_callable=PropertyMock) - @patch("charm.PostgresqlOperatorCharm.is_tls_enabled", new_callable=PropertyMock) - @patch("charm.PostgreSQLBackups._render_pgbackrest_conf_file") - @patch("charm.PostgreSQLBackups._are_backup_settings_ok") - def test_start_stop_pgbackrest_service( - self, - _are_backup_settings_ok, - _render_pgbackrest_conf_file, - _is_tls_enabled, - _peer_members_ips, - _snap_cache, - _is_primary, - _is_primary_pgbackrest_service_running, +def test_start_stop_pgbackrest_service(harness): + with ( + patch( + "charm.PostgreSQLBackups._is_primary_pgbackrest_service_running", new_callable=PropertyMock + ) as _is_primary_pgbackrest_service_running, + patch("charm.PostgresqlOperatorCharm.is_primary", new_callable=PropertyMock) as _is_primary, + patch("backups.snap.SnapCache") as _snap_cache, + patch("charm.PostgresqlOperatorCharm._peer_members_ips", new_callable=PropertyMock) as _peer_members_ips, + patch("charm.PostgresqlOperatorCharm.is_tls_enabled", new_callable=PropertyMock) as _is_tls_enabled, + patch("charm.PostgreSQLBackups._render_pgbackrest_conf_file") as _render_pgbackrest_conf_file, + patch("charm.PostgreSQLBackups._are_backup_settings_ok") as _are_backup_settings_ok, ): # Test when S3 parameters are not ok (no operation, but returns success). _are_backup_settings_ok.return_value = (False, "fake error message") restart = MagicMock() stop = MagicMock() _snap_cache.return_value = {"charmed-postgresql": MagicMock(restart=restart, stop=stop)} - self.assertEqual( - self.charm.backup.start_stop_pgbackrest_service(), + tc().assertEqual( + harness.charm.backup.start_stop_pgbackrest_service(), True, ) stop.assert_not_called() @@ -1588,8 +1559,8 @@ def test_start_stop_pgbackrest_service( # Test when it was not possible to render the pgBackRest configuration file. _are_backup_settings_ok.return_value = (True, None) _render_pgbackrest_conf_file.return_value = False - self.assertEqual( - self.charm.backup.start_stop_pgbackrest_service(), + tc().assertEqual( + harness.charm.backup.start_stop_pgbackrest_service(), False, ) stop.assert_not_called() @@ -1598,8 +1569,8 @@ def test_start_stop_pgbackrest_service( # Test when TLS is not enabled (should stop the service). _render_pgbackrest_conf_file.return_value = True _is_tls_enabled.return_value = False - self.assertEqual( - self.charm.backup.start_stop_pgbackrest_service(), + tc().assertEqual( + harness.charm.backup.start_stop_pgbackrest_service(), True, ) stop.assert_called_once() @@ -1609,8 +1580,8 @@ def test_start_stop_pgbackrest_service( stop.reset_mock() _is_tls_enabled.return_value = True _peer_members_ips.return_value = [] - self.assertEqual( - self.charm.backup.start_stop_pgbackrest_service(), + tc().assertEqual( + harness.charm.backup.start_stop_pgbackrest_service(), True, ) stop.assert_called_once() @@ -1621,8 +1592,8 @@ def test_start_stop_pgbackrest_service( _peer_members_ips.return_value = ["1.1.1.1"] _is_primary.return_value = False _is_primary_pgbackrest_service_running.return_value = False - self.assertEqual( - self.charm.backup.start_stop_pgbackrest_service(), + tc().assertEqual( + harness.charm.backup.start_stop_pgbackrest_service(), False, ) stop.assert_not_called() @@ -1630,8 +1601,8 @@ def test_start_stop_pgbackrest_service( # Test when the service has already started in the primary. _is_primary_pgbackrest_service_running.return_value = True - self.assertEqual( - self.charm.backup.start_stop_pgbackrest_service(), + tc().assertEqual( + harness.charm.backup.start_stop_pgbackrest_service(), True, ) stop.assert_not_called() @@ -1641,17 +1612,19 @@ def test_start_stop_pgbackrest_service( restart.reset_mock() _is_primary.return_value = True _is_primary_pgbackrest_service_running.return_value = False - self.assertEqual( - self.charm.backup.start_stop_pgbackrest_service(), + tc().assertEqual( + harness.charm.backup.start_stop_pgbackrest_service(), True, ) stop.assert_not_called() restart.assert_called_once() - @patch("tempfile.NamedTemporaryFile") - @patch("charm.PostgreSQLBackups._construct_endpoint") - @patch("boto3.session.Session.resource") - def test_upload_content_to_s3(self, _resource, _construct_endpoint, _named_temporary_file): +def test_upload_content_to_s3(harness): + with ( + patch("tempfile.NamedTemporaryFile") as _named_temporary_file, + patch("charm.PostgreSQLBackups._construct_endpoint") as _construct_endpoint, + patch("boto3.session.Session.resource") as _resource, + ): # Set some parameters. content = "test-content" s3_path = "test-file." @@ -1669,8 +1642,8 @@ def test_upload_content_to_s3(self, _resource, _construct_endpoint, _named_tempo _resource.side_effect = ValueError _construct_endpoint.return_value = "https://s3.us-east-1.amazonaws.com" _named_temporary_file.return_value.__enter__.return_value.name = "/tmp/test-file" - self.assertEqual( - self.charm.backup._upload_content_to_s3(content, s3_path, s3_parameters), + tc().assertEqual( + harness.charm.backup._upload_content_to_s3(content, s3_path, s3_parameters), False, ) _resource.assert_called_once_with("s3", endpoint_url="https://s3.us-east-1.amazonaws.com") @@ -1680,8 +1653,8 @@ def test_upload_content_to_s3(self, _resource, _construct_endpoint, _named_tempo _resource.reset_mock() _resource.side_effect = None upload_file.side_effect = S3UploadFailedError - self.assertEqual( - self.charm.backup._upload_content_to_s3(content, s3_path, s3_parameters), + tc().assertEqual( + harness.charm.backup._upload_content_to_s3(content, s3_path, s3_parameters), False, ) _resource.assert_called_once_with("s3", endpoint_url="https://s3.us-east-1.amazonaws.com") @@ -1693,8 +1666,8 @@ def test_upload_content_to_s3(self, _resource, _construct_endpoint, _named_tempo _named_temporary_file.reset_mock() upload_file.reset_mock() upload_file.side_effect = None - self.assertEqual( - self.charm.backup._upload_content_to_s3(content, s3_path, s3_parameters), + tc().assertEqual( + harness.charm.backup._upload_content_to_s3(content, s3_path, s3_parameters), True, ) _resource.assert_called_once_with("s3", endpoint_url="https://s3.us-east-1.amazonaws.com") diff --git a/tests/unit/test_cluster.py b/tests/unit/test_cluster.py index 24afe390af..1cefccd45a 100644 --- a/tests/unit/test_cluster.py +++ b/tests/unit/test_cluster.py @@ -1,8 +1,9 @@ # Copyright 2021 Canonical Ltd. # See LICENSE file for licensing details. -import unittest +import pytest from unittest.mock import MagicMock, Mock, PropertyMock, mock_open, patch, sentinel +from unittest import TestCase as tc import requests as requests import tenacity as tenacity @@ -44,48 +45,53 @@ def json(self): raise requests.exceptions.Timeout() -class TestCluster(unittest.TestCase): - def setUp(self): - # Setup a cluster. - self.peers_ips = {"2.2.2.2", "3.3.3.3"} - - self.patroni = Patroni( - "1.1.1.1", - "postgresql", - "postgresql-0", - 1, - self.peers_ips, - "fake-superuser-password", - "fake-replication-password", - "fake-rewind-password", - False, - ) - - def test_get_alternative_patroni_url(self): - # Mock tenacity attempt. - retry = tenacity.Retrying() - retry_state = tenacity.RetryCallState(retry, None, None, None) - attempt = tenacity.AttemptManager(retry_state) - - # Test the first URL that is returned (it should have the current unit IP). - url = self.patroni._get_alternative_patroni_url(attempt) - self.assertEqual(url, f"http://{self.patroni.unit_ip}:8008") - - # Test returning the other servers URLs. - for attempt_number in range( - attempt.retry_state.attempt_number + 1, len(self.peers_ips) + 2 - ): - attempt.retry_state.attempt_number = attempt_number - url = self.patroni._get_alternative_patroni_url(attempt) - self.assertIn(url.split("http://")[1].split(":8008")[0], self.peers_ips) - - @patch("requests.get", side_effect=mocked_requests_get) - @patch("charm.Patroni._get_alternative_patroni_url") - def test_get_member_ip(self, _get_alternative_patroni_url, _get): +@pytest.fixture(autouse=True) +def peers_ips(): + peers_ips = {"2.2.2.2", "3.3.3.3"} + yield peers_ips + +@pytest.fixture(autouse=True) +def patroni(peers_ips): + patroni = Patroni( + "1.1.1.1", + "postgresql", + "postgresql-0", + 1, + peers_ips, + "fake-superuser-password", + "fake-replication-password", + "fake-rewind-password", + False, + ) + yield patroni + +def test_get_alternative_patroni_url(peers_ips, patroni): + # Mock tenacity attempt. + retry = tenacity.Retrying() + retry_state = tenacity.RetryCallState(retry, None, None, None) + attempt = tenacity.AttemptManager(retry_state) + + # Test the first URL that is returned (it should have the current unit IP). + url = patroni._get_alternative_patroni_url(attempt) + tc().assertEqual(url, f"http://{patroni.unit_ip}:8008") + + # Test returning the other servers URLs. + for attempt_number in range( + attempt.retry_state.attempt_number + 1, len(peers_ips) + 2 + ): + attempt.retry_state.attempt_number = attempt_number + url = patroni._get_alternative_patroni_url(attempt) + tc().assertIn(url.split("http://")[1].split(":8008")[0], peers_ips) + +def test_get_member_ip(peers_ips, patroni): + with ( + patch("requests.get", side_effect=mocked_requests_get) as _get, + patch("charm.Patroni._get_alternative_patroni_url") as _get_alternative_patroni_url, + ): # Test error on trying to get the member IP. _get_alternative_patroni_url.side_effect = "http://server2" - with self.assertRaises(tenacity.RetryError): - self.patroni.get_member_ip(self.patroni.member_name) + with tc().assertRaises(tenacity.RetryError): + patroni.get_member_ip(patroni.member_name) # Test using an alternative Patroni URL. _get_alternative_patroni_url.side_effect = [ @@ -93,40 +99,42 @@ def test_get_member_ip(self, _get_alternative_patroni_url, _get): "http://server2", "http://server1", ] - ip = self.patroni.get_member_ip(self.patroni.member_name) - self.assertEqual(ip, "1.1.1.1") + ip = patroni.get_member_ip(patroni.member_name) + tc().assertEqual(ip, "1.1.1.1") # Test using the current Patroni URL. _get_alternative_patroni_url.side_effect = ["http://server1"] - ip = self.patroni.get_member_ip(self.patroni.member_name) - self.assertEqual(ip, "1.1.1.1") + ip = patroni.get_member_ip(patroni.member_name) + tc().assertEqual(ip, "1.1.1.1") # Test when not having that specific member in the cluster. _get_alternative_patroni_url.side_effect = ["http://server1"] - ip = self.patroni.get_member_ip("other-member-name") - self.assertIsNone(ip) + ip = patroni.get_member_ip("other-member-name") + tc().assertIsNone(ip) - @patch("charm.snap.SnapClient") - def test_get_postgresql_version(self, _snap_client): +def test_get_postgresql_version(peers_ips, patroni): + with patch("charm.snap.SnapClient") as _snap_client: # TODO test a real implementation _get_installed_snaps = _snap_client.return_value.get_installed_snaps _get_installed_snaps.return_value = [ {"name": "something"}, {"name": "charmed-postgresql", "version": "14.0"}, ] - version = self.patroni.get_postgresql_version() + version = patroni.get_postgresql_version() - self.assertEqual(version, "14.0") + tc().assertEqual(version, "14.0") _snap_client.assert_called_once_with() _get_installed_snaps.assert_called_once_with() - @patch("requests.get", side_effect=mocked_requests_get) - @patch("charm.Patroni._get_alternative_patroni_url") - def test_get_primary(self, _get_alternative_patroni_url, _get): +def test_get_primary(peers_ips, patroni): + with ( + patch("requests.get", side_effect=mocked_requests_get) as _get, + patch("charm.Patroni._get_alternative_patroni_url") as _get_alternative_patroni_url, + ): # Test error on trying to get the member IP. _get_alternative_patroni_url.side_effect = "http://server2" - with self.assertRaises(tenacity.RetryError): - self.patroni.get_primary(self.patroni.member_name) + with tc().assertRaises(tenacity.RetryError): + patroni.get_primary(patroni.member_name) # Test using an alternative Patroni URL. _get_alternative_patroni_url.side_effect = [ @@ -134,21 +142,21 @@ def test_get_primary(self, _get_alternative_patroni_url, _get): "http://server2", "http://server1", ] - primary = self.patroni.get_primary() - self.assertEqual(primary, "postgresql-0") + primary = patroni.get_primary() + tc().assertEqual(primary, "postgresql-0") # Test using the current Patroni URL. _get_alternative_patroni_url.side_effect = ["http://server1"] - primary = self.patroni.get_primary() - self.assertEqual(primary, "postgresql-0") + primary = patroni.get_primary() + tc().assertEqual(primary, "postgresql-0") # Test requesting the primary in the unit name pattern. _get_alternative_patroni_url.side_effect = ["http://server1"] - primary = self.patroni.get_primary(unit_name_pattern=True) - self.assertEqual(primary, "postgresql/0") + primary = patroni.get_primary(unit_name_pattern=True) + tc().assertEqual(primary, "postgresql/0") - @patch("requests.get") - def test_is_creating_backup(self, _get): +def test_is_creating_backup(peers_ips, patroni): + with patch("requests.get") as _get: # Test when one member is creating a backup. response = _get.return_value response.json.return_value = { @@ -157,21 +165,23 @@ def test_is_creating_backup(self, _get): {"name": "postgresql-1", "tags": {"is_creating_backup": True}}, ] } - self.assertTrue(self.patroni.is_creating_backup) + tc().assertTrue(patroni.is_creating_backup) # Test when no member is creating a backup. response.json.return_value = { "members": [{"name": "postgresql-0"}, {"name": "postgresql-1"}] } - self.assertFalse(self.patroni.is_creating_backup) + tc().assertFalse(patroni.is_creating_backup) - @patch("requests.get") - @patch("charm.Patroni.get_primary") - @patch("cluster.stop_after_delay", return_value=stop_after_delay(0)) - def test_is_replication_healthy(self, _, __, _get): +def test_is_replication_healthy(peers_ips, patroni): + with ( + patch("requests.get") as _get, + patch("charm.Patroni.get_primary"), + patch("cluster.stop_after_delay", return_value=stop_after_delay(0)), + ): # Test when replication is healthy. _get.return_value.status_code = 200 - self.assertTrue(self.patroni.is_replication_healthy) + tc().assertTrue(patroni.is_replication_healthy) # Test when replication is not healthy. _get.side_effect = [ @@ -179,30 +189,34 @@ def test_is_replication_healthy(self, _, __, _get): MagicMock(status_code=200), MagicMock(status_code=503), ] - self.assertFalse(self.patroni.is_replication_healthy) - - @patch("cluster.stop_after_delay", return_value=tenacity.stop_after_delay(0)) - @patch("cluster.wait_fixed", return_value=tenacity.wait_fixed(0)) - @patch("requests.get", side_effect=mocked_requests_get) - @patch("charm.Patroni._patroni_url", new_callable=PropertyMock) - def test_is_member_isolated(self, _patroni_url, _get, _, __): + tc().assertFalse(patroni.is_replication_healthy) + +def test_is_member_isolated(peers_ips, patroni): + with ( + patch("cluster.stop_after_delay", return_value=tenacity.stop_after_delay(0)), + patch("cluster.wait_fixed", return_value=tenacity.wait_fixed(0)), + patch("requests.get", side_effect=mocked_requests_get) as _get, + patch("charm.Patroni._patroni_url", new_callable=PropertyMock) as _patroni_url, + ): # Test when it wasn't possible to connect to the Patroni API. _patroni_url.return_value = "http://server3" - self.assertFalse(self.patroni.is_member_isolated) + tc().assertFalse(patroni.is_member_isolated) # Test when the member isn't isolated from the cluster. _patroni_url.return_value = "http://server1" - self.assertFalse(self.patroni.is_member_isolated) + tc().assertFalse(patroni.is_member_isolated) # Test when the member is isolated from the cluster. _patroni_url.return_value = "http://server4" - self.assertTrue(self.patroni.is_member_isolated) - - @patch("os.chmod") - @patch("os.chown") - @patch("pwd.getpwnam") - @patch("tempfile.NamedTemporaryFile") - def test_render_file(self, _temp_file, _pwnam, _chown, _chmod): + tc().assertTrue(patroni.is_member_isolated) + +def test_render_file(peers_ips, patroni): + with ( + patch("os.chmod") as _chmod, + patch("os.chown") as _chown, + patch("pwd.getpwnam") as _pwnam, + patch("tempfile.NamedTemporaryFile") as _temp_file, + ): # Set a mocked temporary filename. filename = "/tmp/temporaryfilename" _temp_file.return_value.name = filename @@ -214,10 +228,10 @@ def test_render_file(self, _temp_file, _pwnam, _chown, _chmod): _pwnam.return_value.pw_uid = 35 _pwnam.return_value.pw_gid = 35 # Call the method using a temporary configuration file. - self.patroni.render_file(filename, "rendered-content", 0o640) + patroni.render_file(filename, "rendered-content", 0o640) # Check the rendered file is opened with "w+" mode. - self.assertEqual(mock.call_args_list[0][0], (filename, "w+")) + tc().assertEqual(mock.call_args_list[0][0], (filename, "w+")) # Ensure that the correct user is lookup up. _pwnam.assert_called_with("snap_daemon") # Ensure the file is chmod'd correctly. @@ -225,10 +239,12 @@ def test_render_file(self, _temp_file, _pwnam, _chown, _chmod): # Ensure the file is chown'd correctly. _chown.assert_called_with(filename, uid=35, gid=35) - @patch("charm.Patroni.get_postgresql_version") - @patch("charm.Patroni.render_file") - @patch("charm.Patroni._create_directory") - def test_render_patroni_yml_file(self, _, _render_file, _get_postgresql_version): +def test_render_patroni_yml_file(peers_ips, patroni): + with ( + patch("charm.Patroni.get_postgresql_version") as _get_postgresql_version, + patch("charm.Patroni.render_file") as _render_file, + patch("charm.Patroni._create_directory"), + ): _get_postgresql_version.return_value = "14.7" # Define variables to render in the template. @@ -248,16 +264,16 @@ def test_render_patroni_yml_file(self, _, _render_file, _get_postgresql_version) log_path=PATRONI_LOGS_PATH, postgresql_log_path=POSTGRESQL_LOGS_PATH, member_name=member_name, - peers_ips=self.peers_ips, + peers_ips=peers_ips, scope=scope, - self_ip=self.patroni.unit_ip, + self_ip=patroni.unit_ip, superuser="operator", superuser_password=superuser_password, replication_password=replication_password, rewind_user=REWIND_USER, rewind_password=rewind_password, version=postgresql_version, - minority_count=self.patroni.planned_units // 2, + minority_count=patroni.planned_units // 2, ) # Setup a mock for the `open` method, set returned data to patroni.yml template. @@ -267,10 +283,10 @@ def test_render_patroni_yml_file(self, _, _render_file, _get_postgresql_version) # Patch the `open` method with our mock. with patch("builtins.open", mock, create=True): # Call the method. - self.patroni.render_patroni_yml_file() + patroni.render_patroni_yml_file() # Check the template is opened read-only in the call to open. - self.assertEqual(mock.call_args_list[0][0], ("templates/patroni.yml.j2", "r")) + tc().assertEqual(mock.call_args_list[0][0], ("templates/patroni.yml.j2", "r")) # Ensure the correct rendered template is sent to _render_file method. _render_file.assert_called_once_with( "/var/snap/charmed-postgresql/current/etc/patroni/patroni.yaml", @@ -278,31 +294,35 @@ def test_render_patroni_yml_file(self, _, _render_file, _get_postgresql_version) 0o600, ) - @patch("charm.snap.SnapCache") - @patch("charm.Patroni._create_directory") - def test_start_patroni(self, _create_directory, _snap_cache): +def test_start_patroni(peers_ips, patroni): + with ( + patch("charm.snap.SnapCache") as _snap_cache, + patch("charm.Patroni._create_directory") as _create_directory, + ): _cache = _snap_cache.return_value _selected_snap = _cache.__getitem__.return_value _selected_snap.start.side_effect = [None, snap.SnapError] # Test a success scenario. - assert self.patroni.start_patroni() + assert patroni.start_patroni() _cache.__getitem__.assert_called_once_with("charmed-postgresql") _selected_snap.start.assert_called_once_with(services=[PATRONI_SERVICE]) # Test a fail scenario. - assert not self.patroni.start_patroni() + assert not patroni.start_patroni() - @patch("charm.snap.SnapCache") - @patch("charm.Patroni._create_directory") - def test_stop_patroni(self, _create_directory, _snap_cache): +def test_stop_patroni(peers_ips, patroni): + with ( + patch("charm.snap.SnapCache") as _snap_cache, + patch("charm.Patroni._create_directory") as _create_directory, + ): _cache = _snap_cache.return_value _selected_snap = _cache.__getitem__.return_value _selected_snap.stop.side_effect = [None, snap.SnapError] _selected_snap.services.__getitem__.return_value.__getitem__.return_value = False # Test a success scenario. - assert self.patroni.stop_patroni() + assert patroni.stop_patroni() _cache.__getitem__.assert_called_once_with("charmed-postgresql") _selected_snap.stop.assert_called_once_with(services=[PATRONI_SERVICE]) _selected_snap.services.__getitem__.return_value.__getitem__.assert_called_once_with( @@ -310,72 +330,74 @@ def test_stop_patroni(self, _create_directory, _snap_cache): ) # Test a fail scenario. - assert not self.patroni.stop_patroni() + assert not patroni.stop_patroni() - @patch("requests.get", side_effect=mocked_requests_get) - @patch("charm.Patroni._patroni_url", new_callable=PropertyMock) - def test_member_replication_lag(self, _patroni_url, _get): +def test_member_replication_lag(peers_ips, patroni): + with ( + patch("requests.get", side_effect=mocked_requests_get) as _get, + patch("charm.Patroni._patroni_url", new_callable=PropertyMock) as _patroni_url, + ): # Test when the cluster member has a value for the lag field. _patroni_url.return_value = "http://server1" - lag = self.patroni.member_replication_lag + lag = patroni.member_replication_lag assert lag == "1" # Test when the cluster member doesn't have a value for the lag field. - self.patroni.member_name = "postgresql-1" - lag = self.patroni.member_replication_lag + patroni.member_name = "postgresql-1" + lag = patroni.member_replication_lag assert lag == "unknown" # Test when the API call fails. _patroni_url.return_value = "http://server2" with patch.object(tenacity.Retrying, "iter", Mock(side_effect=tenacity.RetryError(None))): - lag = self.patroni.member_replication_lag + lag = patroni.member_replication_lag assert lag == "unknown" - @patch("requests.post") - def test_reinitialize_postgresql(self, _post): - self.patroni.reinitialize_postgresql() + +def test_reinitialize_postgresql(peers_ips, patroni): + with patch("requests.post") as _post: + patroni.reinitialize_postgresql() _post.assert_called_once_with( - f"http://{self.patroni.unit_ip}:8008/reinitialize", verify=True + f"http://{patroni.unit_ip}:8008/reinitialize", verify=True ) - @patch("requests.post") - @patch("cluster.Patroni.get_primary", return_value="primary") - def test_switchover(self, _, _post): +def test_switchover(peers_ips, patroni): + with ( + patch("requests.post") as _post, + patch("cluster.Patroni.get_primary", return_value="primary"), + ): response = _post.return_value response.status_code = 200 - self.patroni.switchover() + patroni.switchover() _post.assert_called_once_with( "http://1.1.1.1:8008/switchover", json={"leader": "primary"}, verify=True ) - @patch("requests.patch") - def test_update_synchronous_node_count(self, _patch): +def test_update_synchronous_node_count(peers_ips, patroni): + with patch("requests.patch") as _patch: response = _patch.return_value response.status_code = 200 - self.patroni.update_synchronous_node_count() + patroni.update_synchronous_node_count() _patch.assert_called_once_with( "http://1.1.1.1:8008/config", json={"synchronous_node_count": 0}, verify=True ) - @patch("os.chmod") - @patch("builtins.open") - @patch("os.chown") - @patch("pwd.getpwnam") - def test_configure_patroni_on_unit( - self, - _getpwnam, - _chown, - _open, - _chmod, + +def test_configure_patroni_on_unit(peers_ips, patroni): + with ( + patch("os.chmod") as _chmod, + patch("builtins.open") as _open, + patch("os.chown") as _chown, + patch("pwd.getpwnam") as _getpwnam, ): _getpwnam.return_value.pw_uid = sentinel.uid _getpwnam.return_value.pw_gid = sentinel.gid - self.patroni.configure_patroni_on_unit() + patroni.configure_patroni_on_unit() _getpwnam.assert_called_once_with("snap_daemon") @@ -390,62 +412,74 @@ def test_configure_patroni_on_unit( "/var/snap/charmed-postgresql/common/var/lib/postgresql", 488 ) - @patch("cluster.requests.get") - @patch("cluster.stop_after_delay", return_value=tenacity.stop_after_delay(0)) - @patch("cluster.wait_fixed", return_value=tenacity.wait_fixed(0)) - def test_member_started_true(self, _, __, _get): +def test_member_started_true(peers_ips, patroni): + with ( + patch("cluster.requests.get") as _get, + patch("cluster.stop_after_delay", return_value=tenacity.stop_after_delay(0)), + patch("cluster.wait_fixed", return_value=tenacity.wait_fixed(0)), + ): _get.return_value.json.return_value = {"state": "running"} - assert self.patroni.member_started + assert patroni.member_started _get.assert_called_once_with("http://1.1.1.1:8008/health", verify=True, timeout=5) - @patch("cluster.requests.get") - @patch("cluster.stop_after_delay", return_value=tenacity.stop_after_delay(0)) - @patch("cluster.wait_fixed", return_value=tenacity.wait_fixed(0)) - def test_member_started_false(self, _, __, _get): +def test_member_started_false(peers_ips, patroni): + with ( + patch("cluster.requests.get") as _get, + patch("cluster.stop_after_delay", return_value=tenacity.stop_after_delay(0)), + patch("cluster.wait_fixed", return_value=tenacity.wait_fixed(0)), + ): _get.return_value.json.return_value = {"state": "stopped"} - assert not self.patroni.member_started + assert not patroni.member_started _get.assert_called_once_with("http://1.1.1.1:8008/health", verify=True, timeout=5) - @patch("cluster.requests.get") - @patch("cluster.stop_after_delay", return_value=tenacity.stop_after_delay(0)) - @patch("cluster.wait_fixed", return_value=tenacity.wait_fixed(0)) - def test_member_started_error(self, _, __, _get): +def test_member_started_error(peers_ips, patroni): + with ( + patch("cluster.requests.get") as _get, + patch("cluster.stop_after_delay", return_value=tenacity.stop_after_delay(0)), + patch("cluster.wait_fixed", return_value=tenacity.wait_fixed(0)), + ): _get.side_effect = Exception - assert not self.patroni.member_started + assert not patroni.member_started _get.assert_called_once_with("http://1.1.1.1:8008/health", verify=True, timeout=5) - @patch("cluster.requests.get") - @patch("cluster.stop_after_delay", return_value=tenacity.stop_after_delay(0)) - @patch("cluster.wait_fixed", return_value=tenacity.wait_fixed(0)) - def test_member_inactive_true(self, _, __, _get): +def test_member_inactive_true(peers_ips, patroni): + with ( + patch("cluster.requests.get") as _get, + patch("cluster.stop_after_delay", return_value=tenacity.stop_after_delay(0)), + patch("cluster.wait_fixed", return_value=tenacity.wait_fixed(0)), + ): _get.return_value.json.return_value = {"state": "stopped"} - assert self.patroni.member_inactive + assert patroni.member_inactive _get.assert_called_once_with("http://1.1.1.1:8008/health", verify=True, timeout=5) - @patch("cluster.requests.get") - @patch("cluster.stop_after_delay", return_value=tenacity.stop_after_delay(0)) - @patch("cluster.wait_fixed", return_value=tenacity.wait_fixed(0)) - def test_member_inactive_false(self, _, __, _get): +def test_member_inactive_false(peers_ips, patroni): + with ( + patch("cluster.requests.get") as _get, + patch("cluster.stop_after_delay", return_value=tenacity.stop_after_delay(0)), + patch("cluster.wait_fixed", return_value=tenacity.wait_fixed(0)), + ): _get.return_value.json.return_value = {"state": "starting"} - assert not self.patroni.member_inactive + assert not patroni.member_inactive _get.assert_called_once_with("http://1.1.1.1:8008/health", verify=True, timeout=5) - @patch("cluster.requests.get") - @patch("cluster.stop_after_delay", return_value=tenacity.stop_after_delay(0)) - @patch("cluster.wait_fixed", return_value=tenacity.wait_fixed(0)) - def test_member_inactive_error(self, _, __, _get): +def test_member_inactive_error(peers_ips, patroni): + with ( + patch("cluster.requests.get") as _get, + patch("cluster.stop_after_delay", return_value=tenacity.stop_after_delay(0)), + patch("cluster.wait_fixed", return_value=tenacity.wait_fixed(0)), + ): _get.side_effect = Exception - assert self.patroni.member_inactive + assert patroni.member_inactive _get.assert_called_once_with("http://1.1.1.1:8008/health", verify=True, timeout=5) diff --git a/tests/unit/test_cluster_topology_observer.py b/tests/unit/test_cluster_topology_observer.py index f9403f821b..304b7c9f6a 100644 --- a/tests/unit/test_cluster_topology_observer.py +++ b/tests/unit/test_cluster_topology_observer.py @@ -1,7 +1,7 @@ # Copyright 2023 Canonical Ltd. # See LICENSE file for licensing details. import signal -import unittest +import pytest from typing import Optional from unittest.mock import Mock, PropertyMock, patch @@ -56,63 +56,68 @@ def _peers(self) -> Optional[Relation]: return None -class TestClusterTopologyChange(unittest.TestCase): - def setUp(self) -> None: - self.harness = Harness(MockCharm, meta="name: test-charm") - self.harness.begin() - self.charm = self.harness.charm +@pytest.fixture(autouse=True) +def harness(): + harness = Harness(MockCharm, meta="name: test-charm") + harness.begin() + yield harness + harness.cleanup() - @patch("builtins.open") - @patch("subprocess.Popen") - @patch.object(MockCharm, "_peers", new_callable=PropertyMock) - def test_start_observer(self, _peers, _popen, _open): +def test_start_observer(harness): + with ( + patch("builtins.open") as _open, + patch("subprocess.Popen") as _popen, + patch.object(MockCharm, "_peers", new_callable=PropertyMock) as _peers, + ): # Test that nothing is done if there is already a running process. - _peers.return_value = Mock(data={self.charm.unit: {"observer-pid": "1"}}) - self.charm.observer.start_observer() + _peers.return_value = Mock(data={harness.charm.unit: {"observer-pid": "1"}}) + harness.charm.observer.start_observer() _popen.assert_not_called() # Test that nothing is done if the charm is not in an active status. - self.charm.unit.status = WaitingStatus() - _peers.return_value = Mock(data={self.charm.unit: {}}) - self.charm.observer.start_observer() + harness.charm.unit.status = WaitingStatus() + _peers.return_value = Mock(data={harness.charm.unit: {}}) + harness.charm.observer.start_observer() _popen.assert_not_called() # Test that nothing is done if peer relation is not available yet. - self.charm.unit.status = ActiveStatus() + harness.charm.unit.status = ActiveStatus() _peers.return_value = None - self.charm.observer.start_observer() + harness.charm.observer.start_observer() _popen.assert_not_called() # Test that nothing is done if there is already a running process. - _peers.return_value = Mock(data={self.charm.unit: {}}) + _peers.return_value = Mock(data={harness.charm.unit: {}}) _popen.return_value = Mock(pid=1) - self.charm.observer.start_observer() + harness.charm.observer.start_observer() _popen.assert_called_once() - @patch("os.kill") - @patch.object(MockCharm, "_peers", new_callable=PropertyMock) - def test_stop_observer(self, _peers, _kill): +def test_stop_observer(harness): + with ( + patch("os.kill") as _kill, + patch.object(MockCharm, "_peers", new_callable=PropertyMock) as _peers, + ): # Test that nothing is done if there is no process running. - self.charm.observer.stop_observer() + harness.charm.observer.stop_observer() _kill.assert_not_called() - _peers.return_value = Mock(data={self.charm.unit: {}}) - self.charm.observer.stop_observer() + _peers.return_value = Mock(data={harness.charm.unit: {}}) + harness.charm.observer.stop_observer() _kill.assert_not_called() # Test that the process is killed. - _peers.return_value = Mock(data={self.charm.unit: {"observer-pid": "1"}}) - self.charm.observer.stop_observer() + _peers.return_value = Mock(data={harness.charm.unit: {"observer-pid": "1"}}) + harness.charm.observer.stop_observer() _kill.assert_called_once_with(1, signal.SIGINT) - @patch("subprocess.run") - def test_dispatch(self, _run): +def test_dispatch(harness): + with patch("subprocess.run") as _run: command = "test-command" charm_dir = "/path" - dispatch(command, self.charm.unit.name, charm_dir) + dispatch(command, harness.charm.unit.name, charm_dir) _run.assert_called_once_with([ command, "-u", - self.charm.unit.name, + harness.charm.unit.name, f"JUJU_DISPATCH_PATH=hooks/cluster_topology_change {charm_dir}/dispatch", ]) diff --git a/tests/unit/test_postgresql_provider.py b/tests/unit/test_postgresql_provider.py index b8ae4b590d..c30da20750 100644 --- a/tests/unit/test_postgresql_provider.py +++ b/tests/unit/test_postgresql_provider.py @@ -1,8 +1,9 @@ # Copyright 2022 Canonical Ltd. # See LICENSE file for licensing details. -import unittest +import pytest from unittest.mock import Mock, PropertyMock, patch +from unittest import TestCase as tc from charms.postgresql_k8s.v0.postgresql import ( PostgreSQLCreateDatabaseError, @@ -24,278 +25,287 @@ POSTGRESQL_VERSION = "12" +@pytest.fixture(autouse=True) +def harness(): + harness = Harness(PostgresqlOperatorCharm) + + # Set up the initial relation and hooks. + harness.set_leader(True) + harness.begin() + + # Define some relations. + rel_id = harness.add_relation(RELATION_NAME, "application") + harness.add_relation_unit(rel_id, "application/0") + peer_rel_id = harness.add_relation(PEER, harness.charm.app.name) + harness.add_relation_unit(peer_rel_id, harness.charm.unit.name) + harness.update_relation_data( + peer_rel_id, + harness.charm.app.name, + {"cluster_initialised": "True"}, + ) + yield harness + harness.cleanup() + +def request_database(_harness): + # Reset the charm status. + rel_id = _harness.model.get_relation(RELATION_NAME).id + _harness.model.unit.status = ActiveStatus() + + # Reset the application databag. + _harness.update_relation_data( + rel_id, + "application", + {"database": "", "extra-user-roles": ""}, + ) + + # Reset the database databag. + _harness.update_relation_data( + rel_id, + _harness.charm.app.name, + {"data": "", "username": "", "password": "", "version": "", "database": ""}, + ) + + # Simulate the request of a new database plus extra user roles. + _harness.update_relation_data( + rel_id, + "application", + {"database": DATABASE, "extra-user-roles": EXTRA_USER_ROLES}, + ) + @patch_network_get(private_address="1.1.1.1") -class TestPostgreSQLProvider(unittest.TestCase): - def setUp(self): - self.harness = Harness(PostgresqlOperatorCharm) - self.addCleanup(self.harness.cleanup) - - # Set up the initial relation and hooks. - self.harness.set_leader(True) - self.harness.begin() - self.app = self.harness.charm.app.name - self.unit = self.harness.charm.unit.name - - # Define some relations. - self.rel_id = self.harness.add_relation(RELATION_NAME, "application") - self.harness.add_relation_unit(self.rel_id, "application/0") - self.peer_rel_id = self.harness.add_relation(PEER, self.app) - self.harness.add_relation_unit(self.peer_rel_id, self.unit) - self.harness.update_relation_data( - self.peer_rel_id, - self.app, - {"cluster_initialised": "True"}, +def test_on_database_requested(harness): + with ( + patch.object(PostgresqlOperatorCharm, "postgresql", Mock()) as postgresql_mock, + patch("subprocess.check_output", return_value=b"C"), + patch("charm.PostgreSQLProvider.update_endpoints") as _update_endpoints, + patch("relations.postgresql_provider.new_password", return_value="test-password") as _new_password, + patch.object(EventBase, "defer") as _defer, + patch( + "charm.PostgresqlOperatorCharm.primary_endpoint", + new_callable=PropertyMock, + ) as _primary_endpoint, + patch("charm.Patroni.member_started", new_callable=PropertyMock) as _member_started, + ): + rel_id = harness.model.get_relation(RELATION_NAME).id + # Set some side effects to test multiple situations. + _member_started.side_effect = [False, True, True, True, True, True] + _primary_endpoint.side_effect = [ + None, + {"1.1.1.1"}, + {"1.1.1.1"}, + {"1.1.1.1"}, + {"1.1.1.1"}, + ] + postgresql_mock.create_user = PropertyMock( + side_effect=[None, PostgreSQLCreateUserError, None, None] ) - self.provider = self.harness.charm.postgresql_client_relation + postgresql_mock.create_database = PropertyMock( + side_effect=[None, PostgreSQLCreateDatabaseError, None] + ) + postgresql_mock.get_postgresql_version = PropertyMock( + side_effect=[ + POSTGRESQL_VERSION, + PostgreSQLGetPostgreSQLVersionError, + ] + ) + + # Request a database before the database is ready. + request_database(harness) + _defer.assert_called_once() + + # Request a database before primary endpoint is available. + request_database(harness) + tc().assertEqual(_defer.call_count, 2) - def request_database(self): - # Reset the charm status. - self.harness.model.unit.status = ActiveStatus() + # Request it again when the database is ready. + request_database(harness) - # Reset the application databag. - self.harness.update_relation_data( - self.rel_id, - "application", - {"database": "", "extra-user-roles": ""}, + # Assert that the correct calls were made. + user = f"relation-{rel_id}" + postgresql_mock.create_user.assert_called_once_with( + user, "test-password", extra_user_roles=EXTRA_USER_ROLES + ) + database_relation = harness.model.get_relation(RELATION_NAME) + client_relations = [database_relation] + postgresql_mock.create_database.assert_called_once_with( + DATABASE, user, plugins=[], client_relations=client_relations + ) + postgresql_mock.get_postgresql_version.assert_called_once() + _update_endpoints.assert_called_once() + + # Assert that the relation data was updated correctly. + tc().assertEqual( + harness.get_relation_data(rel_id, harness.charm.app.name), + { + "data": f'{{"database": "{DATABASE}", "extra-user-roles": "{EXTRA_USER_ROLES}"}}', + "username": user, + "password": "test-password", + "version": POSTGRESQL_VERSION, + "database": f"{DATABASE}", + }, ) - # Reset the database databag. - self.harness.update_relation_data( - self.rel_id, - self.app, - {"data": "", "username": "", "password": "", "version": "", "database": ""}, + # Assert no BlockedStatus was set. + tc().assertFalse(isinstance(harness.model.unit.status, BlockedStatus)) + + # BlockedStatus due to a PostgreSQLCreateUserError. + request_database(harness) + tc().assertTrue(isinstance(harness.model.unit.status, BlockedStatus)) + # No data is set in the databag by the database. + tc().assertEqual( + harness.get_relation_data(rel_id, harness.charm.app.name), + { + "data": f'{{"database": "{DATABASE}", "extra-user-roles": "{EXTRA_USER_ROLES}"}}', + }, ) - # Simulate the request of a new database plus extra user roles. - self.harness.update_relation_data( - self.rel_id, - "application", - {"database": DATABASE, "extra-user-roles": EXTRA_USER_ROLES}, + # BlockedStatus due to a PostgreSQLCreateDatabaseError. + request_database(harness) + tc().assertTrue(isinstance(harness.model.unit.status, BlockedStatus)) + # No data is set in the databag by the database. + tc().assertEqual( + harness.get_relation_data(rel_id, harness.charm.app.name), + { + "data": f'{{"database": "{DATABASE}", "extra-user-roles": "{EXTRA_USER_ROLES}"}}', + }, ) - @patch("subprocess.check_output", return_value=b"C") - @patch("charm.PostgreSQLProvider.update_endpoints") - @patch("relations.postgresql_provider.new_password", return_value="test-password") - @patch.object(EventBase, "defer") - @patch( - "charm.PostgresqlOperatorCharm.primary_endpoint", - new_callable=PropertyMock, - ) - @patch("charm.Patroni.member_started", new_callable=PropertyMock) - def test_on_database_requested( - self, _member_started, _primary_endpoint, _defer, _new_password, _update_endpoints, _ - ): - with patch.object(PostgresqlOperatorCharm, "postgresql", Mock()) as postgresql_mock: - # Set some side effects to test multiple situations. - _member_started.side_effect = [False, True, True, True, True, True] - _primary_endpoint.side_effect = [ - None, - {"1.1.1.1"}, - {"1.1.1.1"}, - {"1.1.1.1"}, - {"1.1.1.1"}, + # BlockedStatus due to a PostgreSQLGetPostgreSQLVersionError. + request_database(harness) + tc().assertTrue(isinstance(harness.model.unit.status, BlockedStatus)) + +@patch_network_get(private_address="1.1.1.1") +def test_oversee_users(harness): + with patch.object(PostgresqlOperatorCharm, "postgresql", Mock()) as postgresql_mock: + # Create two relations and add the username in their databags. + rel_id = harness.add_relation(RELATION_NAME, "application") + harness.update_relation_data( + rel_id, + harness.charm.app.name, + {"username": f"relation-{rel_id}"}, + ) + another_rel_id = harness.add_relation(RELATION_NAME, "application") + harness.update_relation_data( + another_rel_id, + harness.charm.app.name, + {"username": f"relation-{another_rel_id}"}, + ) + + # Mock some database calls. + postgresql_mock.list_users = PropertyMock( + side_effect=[ + {f"relation-{rel_id}", f"relation-{another_rel_id}", "postgres"}, + {f"relation-{rel_id}", f"relation-{another_rel_id}", "postgres"}, + PostgreSQLListUsersError, ] - postgresql_mock.create_user = PropertyMock( - side_effect=[None, PostgreSQLCreateUserError, None, None] - ) - postgresql_mock.create_database = PropertyMock( - side_effect=[None, PostgreSQLCreateDatabaseError, None] - ) - postgresql_mock.get_postgresql_version = PropertyMock( - side_effect=[ - POSTGRESQL_VERSION, - PostgreSQLGetPostgreSQLVersionError, - ] - ) - - # Request a database before the database is ready. - self.request_database() - _defer.assert_called_once() - - # Request a database before primary endpoint is available. - self.request_database() - self.assertEqual(_defer.call_count, 2) - - # Request it again when the database is ready. - self.request_database() - - # Assert that the correct calls were made. - user = f"relation-{self.rel_id}" - postgresql_mock.create_user.assert_called_once_with( - user, "test-password", extra_user_roles=EXTRA_USER_ROLES - ) - database_relation = self.harness.model.get_relation(RELATION_NAME) - client_relations = [database_relation] - postgresql_mock.create_database.assert_called_once_with( - DATABASE, user, plugins=[], client_relations=client_relations - ) - postgresql_mock.get_postgresql_version.assert_called_once() - _update_endpoints.assert_called_once() - - # Assert that the relation data was updated correctly. - self.assertEqual( - self.harness.get_relation_data(self.rel_id, self.app), - { - "data": f'{{"database": "{DATABASE}", "extra-user-roles": "{EXTRA_USER_ROLES}"}}', - "username": user, - "password": "test-password", - "version": POSTGRESQL_VERSION, - "database": f"{DATABASE}", - }, - ) - - # Assert no BlockedStatus was set. - self.assertFalse(isinstance(self.harness.model.unit.status, BlockedStatus)) - - # BlockedStatus due to a PostgreSQLCreateUserError. - self.request_database() - self.assertTrue(isinstance(self.harness.model.unit.status, BlockedStatus)) - # No data is set in the databag by the database. - self.assertEqual( - self.harness.get_relation_data(self.rel_id, self.app), - { - "data": f'{{"database": "{DATABASE}", "extra-user-roles": "{EXTRA_USER_ROLES}"}}', - }, - ) - - # BlockedStatus due to a PostgreSQLCreateDatabaseError. - self.request_database() - self.assertTrue(isinstance(self.harness.model.unit.status, BlockedStatus)) - # No data is set in the databag by the database. - self.assertEqual( - self.harness.get_relation_data(self.rel_id, self.app), - { - "data": f'{{"database": "{DATABASE}", "extra-user-roles": "{EXTRA_USER_ROLES}"}}', - }, - ) - - # BlockedStatus due to a PostgreSQLGetPostgreSQLVersionError. - self.request_database() - self.assertTrue(isinstance(self.harness.model.unit.status, BlockedStatus)) - - def test_oversee_users(self): - with patch.object(PostgresqlOperatorCharm, "postgresql", Mock()) as postgresql_mock: - # Create two relations and add the username in their databags. - rel_id = self.harness.add_relation(RELATION_NAME, "application") - self.harness.update_relation_data( - rel_id, - self.harness.charm.app.name, - {"username": f"relation-{rel_id}"}, - ) - another_rel_id = self.harness.add_relation(RELATION_NAME, "application") - self.harness.update_relation_data( - another_rel_id, - self.harness.charm.app.name, - {"username": f"relation-{another_rel_id}"}, - ) - - # Mock some database calls. - postgresql_mock.list_users = PropertyMock( - side_effect=[ - {f"relation-{rel_id}", f"relation-{another_rel_id}", "postgres"}, - {f"relation-{rel_id}", f"relation-{another_rel_id}", "postgres"}, - PostgreSQLListUsersError, - ] - ) - - # Call the method and check that no users were deleted. - self.provider.oversee_users() - postgresql_mock.delete_user.assert_not_called() - - # Test again (but removing the relation before calling the method). - self.harness.remove_relation(rel_id) - self.provider.oversee_users() - postgresql_mock.delete_user.assert_called_once_with(f"relation-{rel_id}") - - # And test that no delete call is made if the users list couldn't be retrieved. - self.provider.oversee_users() - postgresql_mock.delete_user.assert_called_once() # Only the previous call. - - @patch( - "charm.PostgresqlOperatorCharm.primary_endpoint", - new_callable=PropertyMock(return_value="1.1.1.1"), - ) - @patch( - "charm.PostgresqlOperatorCharm.members_ips", - new_callable=PropertyMock, - ) - @patch("charm.Patroni.get_primary", new_callable=PropertyMock) - def test_update_endpoints_with_event(self, _get_primary, _members_ips, _primary_endpoint): + ) + + # Call the method and check that no users were deleted. + harness.charm.postgresql_client_relation.oversee_users() + postgresql_mock.delete_user.assert_not_called() + + # Test again (but removing the relation before calling the method). + harness.remove_relation(rel_id) + harness.charm.postgresql_client_relation.oversee_users() + postgresql_mock.delete_user.assert_called_once_with(f"relation-{rel_id}") + + # And test that no delete call is made if the users list couldn't be retrieved. + harness.charm.postgresql_client_relation.oversee_users() + postgresql_mock.delete_user.assert_called_once() # Only the previous call. + +@patch_network_get(private_address="1.1.1.1") +def test_update_endpoints_with_event(harness): + with ( + patch( + "charm.PostgresqlOperatorCharm.primary_endpoint", + new_callable=PropertyMock(return_value="1.1.1.1"), + ) as _primary_endpoint, + patch( + "charm.PostgresqlOperatorCharm.members_ips", + new_callable=PropertyMock, + ) as _members_ips, + patch("charm.Patroni.get_primary", new_callable=PropertyMock) as _get_primary, + ): # Mock the members_ips list to simulate different scenarios # (with and without a replica). + rel_id = harness.model.get_relation(RELATION_NAME).id _members_ips.side_effect = [{"1.1.1.1", "2.2.2.2"}, {"1.1.1.1"}] # Add two different relations. - self.rel_id = self.harness.add_relation(RELATION_NAME, "application") - self.another_rel_id = self.harness.add_relation(RELATION_NAME, "application") + rel_id = harness.add_relation(RELATION_NAME, "application") + another_rel_id = harness.add_relation(RELATION_NAME, "application") # Define a mock relation changed event to be used in the subsequent update endpoints calls. mock_event = Mock() # Set the app, id and the initial data for the relation. - mock_event.app = self.harness.charm.model.get_app("application") - mock_event.relation.id = self.rel_id + mock_event.app = harness.charm.model.get_app("application") + mock_event.relation.id = rel_id # Test with both a primary and a replica. # Update the endpoints with the event and check that it updated # only the right relation databag (the one from the event). - self.provider.update_endpoints(mock_event) - self.assertEqual( - self.harness.get_relation_data(self.rel_id, self.app), + harness.charm.postgresql_client_relation.update_endpoints(mock_event) + tc().assertEqual( + harness.get_relation_data(rel_id, harness.charm.app.name), {"endpoints": "1.1.1.1:5432", "read-only-endpoints": "2.2.2.2:5432"}, ) - self.assertEqual( - self.harness.get_relation_data(self.another_rel_id, self.app), + tc().assertEqual( + harness.get_relation_data(another_rel_id, harness.charm.app.name), {}, ) # Also test with only a primary instance. - self.provider.update_endpoints(mock_event) - self.assertEqual( - self.harness.get_relation_data(self.rel_id, self.app), + harness.charm.postgresql_client_relation.update_endpoints(mock_event) + tc().assertEqual( + harness.get_relation_data(rel_id, harness.charm.app.name), {"endpoints": "1.1.1.1:5432"}, ) - self.assertEqual( - self.harness.get_relation_data(self.another_rel_id, self.app), + tc().assertEqual( + harness.get_relation_data(another_rel_id, harness.charm.app.name), {}, ) - @patch( - "charm.PostgresqlOperatorCharm.primary_endpoint", - new_callable=PropertyMock(return_value="1.1.1.1"), - ) - @patch( - "charm.PostgresqlOperatorCharm.members_ips", - new_callable=PropertyMock, - ) - @patch("charm.Patroni.get_primary", new_callable=PropertyMock) - def test_update_endpoints_without_event(self, _get_primary, _members_ips, _primary_endpoint): +@patch_network_get(private_address="1.1.1.1") +def test_update_endpoints_without_event(harness): + with ( + patch( + "charm.PostgresqlOperatorCharm.primary_endpoint", + new_callable=PropertyMock(return_value="1.1.1.1"), + ) as _primary_endpoint, + patch( + "charm.PostgresqlOperatorCharm.members_ips", + new_callable=PropertyMock, + ) as _members_ips, + patch("charm.Patroni.get_primary", new_callable=PropertyMock) as _get_primary, + ): # Mock the members_ips list to simulate different scenarios # (with and without a replica). _members_ips.side_effect = [{"1.1.1.1", "2.2.2.2"}, {"1.1.1.1"}] + rel_id = harness.model.get_relation(RELATION_NAME).id # Add two different relations. - self.rel_id = self.harness.add_relation(RELATION_NAME, "application") - self.another_rel_id = self.harness.add_relation(RELATION_NAME, "application") + rel_id = harness.add_relation(RELATION_NAME, "application") + another_rel_id = harness.add_relation(RELATION_NAME, "application") # Test with both a primary and a replica. # Update the endpoints and check that all relations' databags are updated. - self.provider.update_endpoints() - self.assertEqual( - self.harness.get_relation_data(self.rel_id, self.app), + harness.charm.postgresql_client_relation.update_endpoints() + tc().assertEqual( + harness.get_relation_data(rel_id, harness.charm.app.name), {"endpoints": "1.1.1.1:5432", "read-only-endpoints": "2.2.2.2:5432"}, ) - self.assertEqual( - self.harness.get_relation_data(self.another_rel_id, self.app), + tc().assertEqual( + harness.get_relation_data(another_rel_id, harness.charm.app.name), {"endpoints": "1.1.1.1:5432", "read-only-endpoints": "2.2.2.2:5432"}, ) # Also test with only a primary instance. - self.provider.update_endpoints() - self.assertEqual( - self.harness.get_relation_data(self.rel_id, self.app), + harness.charm.postgresql_client_relation.update_endpoints() + tc().assertEqual( + harness.get_relation_data(rel_id, harness.charm.app.name), {"endpoints": "1.1.1.1:5432"}, ) - self.assertEqual( - self.harness.get_relation_data(self.another_rel_id, self.app), + tc().assertEqual( + harness.get_relation_data(another_rel_id, harness.charm.app.name), {"endpoints": "1.1.1.1:5432"}, ) diff --git a/tests/unit/test_upgrade.py b/tests/unit/test_upgrade.py index d04c195cd9..adcb928735 100644 --- a/tests/unit/test_upgrade.py +++ b/tests/unit/test_upgrade.py @@ -1,7 +1,8 @@ # Copyright 2023 Canonical Ltd. # See LICENSE file for licensing details. -import unittest +import pytest from unittest.mock import MagicMock, PropertyMock, patch +from unittest import TestCase as tc import tenacity from charms.data_platform_libs.v0.upgrade import ClusterNotReadyError @@ -12,78 +13,71 @@ from tests.helpers import patch_network_get -class TestUpgrade(unittest.TestCase): - """Test the upgrade class.""" - - def setUp(self): - """Set up the test.""" - self.harness = Harness(PostgresqlOperatorCharm) - self.harness.begin() - self.upgrade_relation_id = self.harness.add_relation("upgrade", "postgresql") - self.peer_relation_id = self.harness.add_relation("database-peers", "postgresql") - for rel_id in (self.upgrade_relation_id, self.peer_relation_id): - self.harness.add_relation_unit(rel_id, "postgresql/1") - with self.harness.hooks_disabled(): - self.harness.update_relation_data( - self.upgrade_relation_id, "postgresql/1", {"state": "idle"} - ) - self.charm = self.harness.charm - - @patch_network_get(private_address="1.1.1.1") - @patch("charm.Patroni.get_sync_standby_names") - @patch("charm.Patroni.get_primary") - def test_build_upgrade_stack(self, _get_primary, _get_sync_standby_names): + +@pytest.fixture(autouse=True) +def harness(): + """Set up the test.""" + harness = Harness(PostgresqlOperatorCharm) + harness.begin() + upgrade_relation_id = harness.add_relation("upgrade", "postgresql") + peer_relation_id = harness.add_relation("database-peers", "postgresql") + for rel_id in (upgrade_relation_id, peer_relation_id): + harness.add_relation_unit(rel_id, "postgresql/1") + with harness.hooks_disabled(): + harness.update_relation_data( + upgrade_relation_id, "postgresql/1", {"state": "idle"} + ) + yield harness + harness.cleanup() + +@patch_network_get(private_address="1.1.1.1") +def test_build_upgrade_stack(harness): + with ( + patch("charm.Patroni.get_sync_standby_names") as _get_sync_standby_names, + patch("charm.Patroni.get_primary") as _get_primary, + ): # Set some side effects to test multiple situations. _get_primary.side_effect = ["postgresql/0", "postgresql/1"] _get_sync_standby_names.side_effect = [["postgresql/1"], ["postgresql/2"]] - for rel_id in (self.upgrade_relation_id, self.peer_relation_id): - self.harness.add_relation_unit(rel_id, "postgresql/2") - - self.assertEqual(self.charm.upgrade.build_upgrade_stack(), [0, 1, 2]) - self.assertEqual(self.charm.upgrade.build_upgrade_stack(), [1, 2, 0]) - - @patch("charm.PostgresqlOperatorCharm.update_config") - @patch("upgrade.logger.info") - def test_log_rollback(self, mock_logging, _update_config): - self.charm.upgrade.log_rollback_instructions() + upgrade_relation_id = harness.model.get_relation("upgrade").id + peer_relation_id = harness.model.get_relation("database-peers").id + for rel_id in (upgrade_relation_id, peer_relation_id): + harness.add_relation_unit(rel_id, "postgresql/2") + + tc().assertEqual(harness.charm.upgrade.build_upgrade_stack(), [0, 1, 2]) + tc().assertEqual(harness.charm.upgrade.build_upgrade_stack(), [1, 2, 0]) + +def test_log_rollback(harness): + with ( + patch("charm.PostgresqlOperatorCharm.update_config") as _update_config, + patch("upgrade.logger.info") as mock_logging, + ): + harness.charm.upgrade.log_rollback_instructions() mock_logging.assert_any_call( "Run `juju refresh --revision postgresql` to initiate the rollback" ) - @patch_network_get(private_address="1.1.1.1") - @patch("charm.Patroni.get_postgresql_version") - @patch("charms.data_platform_libs.v0.upgrade.DataUpgrade.on_upgrade_changed") - @patch("charms.data_platform_libs.v0.upgrade.DataUpgrade.set_unit_failed") - @patch("charms.data_platform_libs.v0.upgrade.DataUpgrade.set_unit_completed") - @patch("charm.Patroni.is_replication_healthy", new_callable=PropertyMock) - @patch("charm.Patroni.cluster_members", new_callable=PropertyMock) - @patch("charm.Patroni.member_started", new_callable=PropertyMock) - @patch("upgrade.wait_fixed", return_value=tenacity.wait_fixed(0)) - @patch("charm.PostgreSQLBackups.start_stop_pgbackrest_service") - @patch("charm.PostgresqlOperatorCharm._setup_exporter") - @patch("charm.Patroni.start_patroni") - @patch("charm.PostgresqlOperatorCharm._install_snap_packages") - @patch("charm.PostgresqlOperatorCharm.update_config") - def test_on_upgrade_granted( - self, - _update_config, - _install_snap_packages, - _start_patroni, - _setup_exporter, - _start_stop_pgbackrest_service, - _, - _member_started, - _cluster_members, - _is_replication_healthy, - _set_unit_completed, - _set_unit_failed, - _on_upgrade_changed, - __, +@patch_network_get(private_address="1.1.1.1") +def test_on_upgrade_granted(harness): + with ( + patch("charm.Patroni.get_postgresql_version"), + patch("charms.data_platform_libs.v0.upgrade.DataUpgrade.on_upgrade_changed") as _on_upgrade_changed, + patch("charms.data_platform_libs.v0.upgrade.DataUpgrade.set_unit_failed") as _set_unit_failed, + patch("charms.data_platform_libs.v0.upgrade.DataUpgrade.set_unit_completed") as _set_unit_completed, + patch("charm.Patroni.is_replication_healthy", new_callable=PropertyMock) as _is_replication_healthy, + patch("charm.Patroni.cluster_members", new_callable=PropertyMock) as _cluster_members, + patch("charm.Patroni.member_started", new_callable=PropertyMock) as _member_started, + patch("upgrade.wait_fixed", return_value=tenacity.wait_fixed(0)), + patch("charm.PostgreSQLBackups.start_stop_pgbackrest_service") as _start_stop_pgbackrest_service, + patch("charm.PostgresqlOperatorCharm._setup_exporter") as _setup_exporter, + patch("charm.Patroni.start_patroni") as _start_patroni, + patch("charm.PostgresqlOperatorCharm._install_snap_packages") as _install_snap_packages, + patch("charm.PostgresqlOperatorCharm.update_config") as _update_config, ): # Test when the charm fails to start Patroni. mock_event = MagicMock() _start_patroni.return_value = False - self.charm.upgrade._on_upgrade_granted(mock_event) + harness.charm.upgrade._on_upgrade_granted(mock_event) _update_config.assert_called_once() _install_snap_packages.assert_called_once_with(packages=SNAP_PACKAGES, refresh=True) _member_started.assert_not_called() @@ -96,8 +90,8 @@ def test_on_upgrade_granted( _set_unit_failed.reset_mock() _start_patroni.return_value = True _member_started.return_value = False - self.charm.upgrade._on_upgrade_granted(mock_event) - self.assertEqual(_member_started.call_count, 6) + harness.charm.upgrade._on_upgrade_granted(mock_event) + tc().assertEqual(_member_started.call_count, 6) _cluster_members.assert_not_called() mock_event.defer.assert_called_once() _set_unit_completed.assert_not_called() @@ -109,9 +103,9 @@ def test_on_upgrade_granted( mock_event.defer.reset_mock() _member_started.return_value = True _cluster_members.return_value = ["postgresql-1"] - self.charm.upgrade._on_upgrade_granted(mock_event) - self.assertEqual(_member_started.call_count, 6) - self.assertEqual(_cluster_members.call_count, 6) + harness.charm.upgrade._on_upgrade_granted(mock_event) + tc().assertEqual(_member_started.call_count, 6) + tc().assertEqual(_cluster_members.call_count, 6) mock_event.defer.assert_called_once() _set_unit_completed.assert_not_called() _set_unit_failed.assert_not_called() @@ -123,10 +117,10 @@ def test_on_upgrade_granted( _set_unit_failed.reset_mock() mock_event.defer.reset_mock() _cluster_members.return_value = [ - self.charm.unit.name.replace("/", "-"), + harness.charm.unit.name.replace("/", "-"), "postgresql-1", ] - self.charm.upgrade._on_upgrade_granted(mock_event) + harness.charm.upgrade._on_upgrade_granted(mock_event) _member_started.assert_called_once() _cluster_members.assert_called_once() mock_event.defer.assert_not_called() @@ -138,7 +132,7 @@ def test_on_upgrade_granted( # is not healthy yet. _set_unit_completed.reset_mock() _is_replication_healthy.return_value = False - self.charm.upgrade._on_upgrade_granted(mock_event) + harness.charm.upgrade._on_upgrade_granted(mock_event) mock_event.defer.assert_called_once() _set_unit_completed.assert_not_called() _set_unit_failed.assert_not_called() @@ -148,9 +142,9 @@ def test_on_upgrade_granted( _cluster_members.reset_mock() mock_event.defer.reset_mock() _is_replication_healthy.return_value = True - with self.harness.hooks_disabled(): - self.harness.set_leader(True) - self.charm.upgrade._on_upgrade_granted(mock_event) + with harness.hooks_disabled(): + harness.set_leader(True) + harness.charm.upgrade._on_upgrade_granted(mock_event) _member_started.assert_called_once() _cluster_members.assert_called_once() mock_event.defer.assert_not_called() @@ -158,28 +152,26 @@ def test_on_upgrade_granted( _set_unit_failed.assert_not_called() _on_upgrade_changed.assert_called_once() - @patch_network_get(private_address="1.1.1.1") - @patch("charm.Patroni.is_creating_backup", new_callable=PropertyMock) - @patch("charm.Patroni.are_all_members_ready") - def test_pre_upgrade_check( - self, - _are_all_members_ready, - _is_creating_backup, +@patch_network_get(private_address="1.1.1.1") +def test_pre_upgrade_check(harness): + with ( + patch("charm.Patroni.is_creating_backup", new_callable=PropertyMock) as _is_creating_backup, + patch("charm.Patroni.are_all_members_ready") as _are_all_members_ready, ): - with self.harness.hooks_disabled(): - self.harness.set_leader(True) + with harness.hooks_disabled(): + harness.set_leader(True) # Set some side effects to test multiple situations. _are_all_members_ready.side_effect = [False, True, True] _is_creating_backup.side_effect = [True, False, False] # Test when not all members are ready. - with self.assertRaises(ClusterNotReadyError): - self.charm.upgrade.pre_upgrade_check() + with tc().assertRaises(ClusterNotReadyError): + harness.charm.upgrade.pre_upgrade_check() # Test when a backup is being created. - with self.assertRaises(ClusterNotReadyError): - self.charm.upgrade.pre_upgrade_check() + with tc().assertRaises(ClusterNotReadyError): + harness.charm.upgrade.pre_upgrade_check() # Test when everything is ok to start the upgrade. - self.charm.upgrade.pre_upgrade_check() + harness.charm.upgrade.pre_upgrade_check() From c41acb39d8d3386e35b456f8836988a143c0441a Mon Sep 17 00:00:00 2001 From: Lucas Gameiro Borges Date: Mon, 22 Apr 2024 11:01:58 +0000 Subject: [PATCH 2/8] finish migration to pytest --- tests/unit/test_db.py | 1110 +++++++++++++++++++------------------- tests/unit/test_utils.py | 21 +- 2 files changed, 570 insertions(+), 561 deletions(-) diff --git a/tests/unit/test_db.py b/tests/unit/test_db.py index bec3f771fd..a54a3929f0 100644 --- a/tests/unit/test_db.py +++ b/tests/unit/test_db.py @@ -1,8 +1,9 @@ # Copyright 2022 Canonical Ltd. # See LICENSE file for licensing details. -import unittest +import pytest from unittest.mock import Mock, PropertyMock, patch +from unittest import TestCase as tc from charms.postgresql_k8s.v0.postgresql import ( PostgreSQLCreateDatabaseError, @@ -22,78 +23,73 @@ POSTGRESQL_VERSION = "12" -@patch_network_get(private_address="1.1.1.1") -class TestDbProvides(unittest.TestCase): - def setUp(self): - self.harness = Harness(PostgresqlOperatorCharm) - self.addCleanup(self.harness.cleanup) - - # Set up the initial relation and hooks. - self.harness.set_leader(True) - self.harness.begin() - self.app = self.harness.charm.app.name - self.unit = self.harness.charm.unit.name - - # Define some relations. - self.rel_id = self.harness.add_relation(RELATION_NAME, "application") - self.harness.add_relation_unit(self.rel_id, "application/0") - self.peer_rel_id = self.harness.add_relation(PEER, self.app) - self.harness.add_relation_unit(self.peer_rel_id, f"{self.app}/1") - self.harness.add_relation_unit(self.peer_rel_id, self.unit) - self.harness.update_relation_data( - self.peer_rel_id, - self.app, - {"cluster_initialised": "True"}, +@pytest.fixture(autouse=True) +def harness(): + harness = Harness(PostgresqlOperatorCharm) + + # Set up the initial relation and hooks. + harness.set_leader(True) + harness.begin() + + # Define some relations. + rel_id = harness.add_relation(RELATION_NAME, "application") + harness.add_relation_unit(rel_id, "application/0") + peer_rel_id = harness.add_relation(PEER, harness.charm.app.name) + harness.add_relation_unit(peer_rel_id, f"{harness.charm.app.name}/1") + harness.add_relation_unit(peer_rel_id, harness.charm.unit.name) + harness.update_relation_data( + peer_rel_id, + harness.charm.app.name, + {"cluster_initialised": "True"}, + ) + yield harness + harness.cleanup() + +def request_database(_harness): + # Reset the charm status. + _harness.model.unit.status = ActiveStatus() + rel_id = _harness.model.get_relation(RELATION_NAME).id + + with _harness.hooks_disabled(): + # Reset the application databag. + _harness.update_relation_data( + rel_id, + "application/0", + {"database": ""}, ) - self.legacy_db_relation = self.harness.charm.legacy_db_relation - - def request_database(self): - # Reset the charm status. - self.harness.model.unit.status = ActiveStatus() - - with self.harness.hooks_disabled(): - # Reset the application databag. - self.harness.update_relation_data( - self.rel_id, - "application/0", - {"database": ""}, - ) - # Reset the database databag. - self.harness.update_relation_data( - self.rel_id, - self.app, - { - "allowed-subnets": "", - "allowed-units": "", - "port": "", - "version": "", - "user": "", - "password": "", - "database": "", - }, - ) - - # Simulate the request of a new database. - self.harness.update_relation_data( - self.rel_id, - "application/0", - {"database": DATABASE}, + # Reset the database databag. + _harness.update_relation_data( + rel_id, + _harness.charm.app.name, + { + "allowed-subnets": "", + "allowed-units": "", + "port": "", + "version": "", + "user": "", + "password": "", + "database": "", + }, ) - @patch("charm.DbProvides.set_up_relation") - @patch.object(EventBase, "defer") - @patch( - "charm.PostgresqlOperatorCharm.primary_endpoint", - new_callable=PropertyMock, + # Simulate the request of a new database. + _harness.update_relation_data( + rel_id, + "application/0", + {"database": DATABASE}, ) - @patch("charm.Patroni.member_started", new_callable=PropertyMock) - def test_on_relation_changed( - self, - _member_started, - _primary_endpoint, - _defer, - _set_up_relation, + +@patch_network_get(private_address="1.1.1.1") +def test_on_relation_changed(harness): + with ( + patch("charm.DbProvides.set_up_relation") as _set_up_relation, + patch.object(EventBase, "defer") as _defer, + patch( + "charm.PostgresqlOperatorCharm.primary_endpoint", + new_callable=PropertyMock, + ) as _primary_endpoint, + patch("charm.Patroni.member_started", new_callable=PropertyMock) as _member_started, ): # Set some side effects to test multiple situations. _member_started.side_effect = [False, True, True, True, True, True] @@ -105,552 +101,566 @@ def test_on_relation_changed( {"1.1.1.1"}, ] # Request a database to a non leader unit. - with self.harness.hooks_disabled(): - self.harness.set_leader(False) - self.request_database() + with harness.hooks_disabled(): + harness.set_leader(False) + request_database(harness) _defer.assert_not_called() _set_up_relation.assert_not_called() # Request a database before the database is ready. - with self.harness.hooks_disabled(): - self.harness.set_leader() - self.request_database() + with harness.hooks_disabled(): + harness.set_leader() + request_database(harness) _defer.assert_called_once() _set_up_relation.assert_not_called() # Request a database before primary endpoint is available. - self.request_database() - self.assertEqual(_defer.call_count, 2) + request_database(harness) + tc().assertEqual(_defer.call_count, 2) _set_up_relation.assert_not_called() # Request it again when the database is ready. _defer.reset_mock() - self.request_database() + request_database(harness) _defer.assert_not_called() _set_up_relation.assert_called_once() - def test_get_extensions(self): - # Test when there are no extensions in the relation databags. - relation = self.harness.model.get_relation(RELATION_NAME, self.rel_id) - self.assertEqual( - self.harness.charm.legacy_db_relation._get_extensions(relation), ([], set()) +@patch_network_get(private_address="1.1.1.1") +def test_get_extensions(harness): + # Test when there are no extensions in the relation databags. + rel_id = harness.model.get_relation(RELATION_NAME).id + relation = harness.model.get_relation(RELATION_NAME, rel_id) + tc().assertEqual( + harness.charm.legacy_db_relation._get_extensions(relation), ([], set()) + ) + + # Test when there are extensions in the application relation databag. + extensions = ["", "citext:public", "debversion"] + with harness.hooks_disabled(): + harness.update_relation_data( + rel_id, + "application", + {"extensions": ",".join(extensions)}, + ) + tc().assertEqual( + harness.charm.legacy_db_relation._get_extensions(relation), + ([extensions[1], extensions[2]], {extensions[1].split(":")[0], extensions[2]}), + ) + + # Test when there are extensions in the unit relation databag. + with harness.hooks_disabled(): + harness.update_relation_data( + rel_id, + "application", + {"extensions": ""}, + ) + harness.update_relation_data( + rel_id, + "application/0", + {"extensions": ",".join(extensions)}, + ) + tc().assertEqual( + harness.charm.legacy_db_relation._get_extensions(relation), + ([extensions[1], extensions[2]], {extensions[1].split(":")[0], extensions[2]}), + ) + + # Test when one of the plugins/extensions is enabled. + config = """options: + plugin_citext_enable: + default: true + type: boolean + plugin_debversion_enable: + default: false + type: boolean""" + harness = Harness(PostgresqlOperatorCharm, config=config) + harness.cleanup() + harness.begin() + tc().assertEqual( + harness.charm.legacy_db_relation._get_extensions(relation), + ([extensions[1], extensions[2]], {extensions[2]}), + ) + +@patch_network_get(private_address="1.1.1.1") +def test_set_up_relation(harness): + with ( + patch.object(PostgresqlOperatorCharm, "postgresql", Mock()) as postgresql_mock, + patch("subprocess.check_output", return_value=b"C"), + patch("relations.db.DbProvides._update_unit_status") as _update_unit_status, + patch("relations.db.DbProvides.update_endpoints") as _update_endpoints, + patch("relations.db.new_password", return_value="test-password") as _new_password, + patch("relations.db.DbProvides._get_extensions") as _get_extensions, + ): + rel_id = harness.model.get_relation(RELATION_NAME).id + # Define some mocks' side effects. + extensions = ["citext:public", "debversion"] + _get_extensions.side_effect = [ + (extensions, {"debversion"}), + (extensions, set()), + (extensions, set()), + (extensions, set()), + (extensions, set()), + (extensions, set()), + (extensions, set()), + ] + postgresql_mock.create_user = PropertyMock( + side_effect=[None, None, None, PostgreSQLCreateUserError, None, None] + ) + postgresql_mock.create_database = PropertyMock( + side_effect=[None, None, None, PostgreSQLCreateDatabaseError, None] ) - # Test when there are extensions in the application relation databag. - extensions = ["", "citext:public", "debversion"] - with self.harness.hooks_disabled(): - self.harness.update_relation_data( - self.rel_id, + # Assert no operation is done when at least one of the requested extensions + # is disabled. + relation = harness.model.get_relation(RELATION_NAME, rel_id) + tc().assertFalse(harness.charm.legacy_db_relation.set_up_relation(relation)) + postgresql_mock.create_user.assert_not_called() + postgresql_mock.create_database.assert_not_called() + postgresql_mock.get_postgresql_version.assert_not_called() + _update_endpoints.assert_not_called() + _update_unit_status.assert_not_called() + + # Assert that the correct calls were made in a successful setup. + harness.charm.unit.status = ActiveStatus() + with harness.hooks_disabled(): + harness.update_relation_data( + rel_id, "application", - {"extensions": ",".join(extensions)}, + {"database": DATABASE}, ) - self.assertEqual( - self.harness.charm.legacy_db_relation._get_extensions(relation), - ([extensions[1], extensions[2]], {extensions[1].split(":")[0], extensions[2]}), + tc().assertTrue(harness.charm.legacy_db_relation.set_up_relation(relation)) + user = f"relation-{rel_id}" + postgresql_mock.create_user.assert_called_once_with(user, "test-password", False) + postgresql_mock.create_database.assert_called_once_with( + DATABASE, user, plugins=[], client_relations=[relation] ) - - # Test when there are extensions in the unit relation databag. - with self.harness.hooks_disabled(): - self.harness.update_relation_data( - self.rel_id, + _update_endpoints.assert_called_once() + _update_unit_status.assert_called_once() + tc().assertNotIsInstance(harness.model.unit.status, BlockedStatus) + + # Assert that the correct calls were made when the database name is not + # provided in both application and unit databags. + postgresql_mock.create_user.reset_mock() + postgresql_mock.create_database.reset_mock() + postgresql_mock.get_postgresql_version.reset_mock() + _update_endpoints.reset_mock() + _update_unit_status.reset_mock() + with harness.hooks_disabled(): + harness.update_relation_data( + rel_id, "application", - {"extensions": ""}, + {"database": ""}, ) - self.harness.update_relation_data( - self.rel_id, + harness.update_relation_data( + rel_id, "application/0", - {"extensions": ",".join(extensions)}, + {"database": DATABASE}, ) - self.assertEqual( - self.harness.charm.legacy_db_relation._get_extensions(relation), - ([extensions[1], extensions[2]], {extensions[1].split(":")[0], extensions[2]}), + tc().assertTrue(harness.charm.legacy_db_relation.set_up_relation(relation)) + postgresql_mock.create_user.assert_called_once_with(user, "test-password", False) + postgresql_mock.create_database.assert_called_once_with( + DATABASE, user, plugins=[], client_relations=[relation] ) - - # Test when one of the plugins/extensions is enabled. - config = """options: - plugin_citext_enable: - default: true - type: boolean - plugin_debversion_enable: - default: false - type: boolean""" - harness = Harness(PostgresqlOperatorCharm, config=config) - self.addCleanup(harness.cleanup) - harness.begin() - self.assertEqual( - harness.charm.legacy_db_relation._get_extensions(relation), - ([extensions[1], extensions[2]], {extensions[2]}), + _update_endpoints.assert_called_once() + _update_unit_status.assert_called_once() + tc().assertNotIsInstance(harness.model.unit.status, BlockedStatus) + + # Assert that the correct calls were made when the database name is not provided. + postgresql_mock.create_user.reset_mock() + postgresql_mock.create_database.reset_mock() + postgresql_mock.get_postgresql_version.reset_mock() + _update_endpoints.reset_mock() + _update_unit_status.reset_mock() + with harness.hooks_disabled(): + harness.update_relation_data( + rel_id, + "application/0", + {"database": ""}, + ) + tc().assertTrue(harness.charm.legacy_db_relation.set_up_relation(relation)) + postgresql_mock.create_user.assert_called_once_with(user, "test-password", False) + postgresql_mock.create_database.assert_called_once_with( + "application", user, plugins=[], client_relations=[relation] ) + _update_endpoints.assert_called_once() + _update_unit_status.assert_called_once() + tc().assertNotIsInstance(harness.model.unit.status, BlockedStatus) + + # BlockedStatus due to a PostgreSQLCreateUserError. + postgresql_mock.create_database.reset_mock() + postgresql_mock.get_postgresql_version.reset_mock() + _update_endpoints.reset_mock() + _update_unit_status.reset_mock() + tc().assertFalse(harness.charm.legacy_db_relation.set_up_relation(relation)) + postgresql_mock.create_database.assert_not_called() + _update_endpoints.assert_not_called() + _update_unit_status.assert_not_called() + tc().assertIsInstance(harness.model.unit.status, BlockedStatus) + + # BlockedStatus due to a PostgreSQLCreateDatabaseError. + harness.charm.unit.status = ActiveStatus() + tc().assertFalse(harness.charm.legacy_db_relation.set_up_relation(relation)) + _update_endpoints.assert_not_called() + _update_unit_status.assert_not_called() + tc().assertIsInstance(harness.model.unit.status, BlockedStatus) - @patch("subprocess.check_output", return_value=b"C") - @patch("relations.db.DbProvides._update_unit_status") - @patch("relations.db.DbProvides.update_endpoints") - @patch("relations.db.new_password", return_value="test-password") - @patch("relations.db.DbProvides._get_extensions") - def test_set_up_relation( - self, - _get_extensions, - _new_password, - _update_endpoints, - _update_unit_status, - _, +@patch_network_get(private_address="1.1.1.1") +def test_update_unit_status(harness): + with ( + patch("relations.db.DbProvides._check_for_blocking_relations") as _check_for_blocking_relations, + patch("charm.PostgresqlOperatorCharm.is_blocked", new_callable=PropertyMock) as _is_blocked, ): - with patch.object(PostgresqlOperatorCharm, "postgresql", Mock()) as postgresql_mock: - # Define some mocks' side effects. - extensions = ["citext:public", "debversion"] - _get_extensions.side_effect = [ - (extensions, {"debversion"}), - (extensions, set()), - (extensions, set()), - (extensions, set()), - (extensions, set()), - (extensions, set()), - (extensions, set()), - ] - postgresql_mock.create_user = PropertyMock( - side_effect=[None, None, None, PostgreSQLCreateUserError, None, None] - ) - postgresql_mock.create_database = PropertyMock( - side_effect=[None, None, None, PostgreSQLCreateDatabaseError, None] - ) - - # Assert no operation is done when at least one of the requested extensions - # is disabled. - relation = self.harness.model.get_relation(RELATION_NAME, self.rel_id) - self.assertFalse(self.harness.charm.legacy_db_relation.set_up_relation(relation)) - postgresql_mock.create_user.assert_not_called() - postgresql_mock.create_database.assert_not_called() - postgresql_mock.get_postgresql_version.assert_not_called() - _update_endpoints.assert_not_called() - _update_unit_status.assert_not_called() - - # Assert that the correct calls were made in a successful setup. - self.harness.charm.unit.status = ActiveStatus() - with self.harness.hooks_disabled(): - self.harness.update_relation_data( - self.rel_id, - "application", - {"database": DATABASE}, - ) - self.assertTrue(self.harness.charm.legacy_db_relation.set_up_relation(relation)) - user = f"relation-{self.rel_id}" - postgresql_mock.create_user.assert_called_once_with(user, "test-password", False) - postgresql_mock.create_database.assert_called_once_with( - DATABASE, user, plugins=[], client_relations=[relation] - ) - _update_endpoints.assert_called_once() - _update_unit_status.assert_called_once() - self.assertNotIsInstance(self.harness.model.unit.status, BlockedStatus) - - # Assert that the correct calls were made when the database name is not - # provided in both application and unit databags. - postgresql_mock.create_user.reset_mock() - postgresql_mock.create_database.reset_mock() - postgresql_mock.get_postgresql_version.reset_mock() - _update_endpoints.reset_mock() - _update_unit_status.reset_mock() - with self.harness.hooks_disabled(): - self.harness.update_relation_data( - self.rel_id, - "application", - {"database": ""}, - ) - self.harness.update_relation_data( - self.rel_id, - "application/0", - {"database": DATABASE}, - ) - self.assertTrue(self.harness.charm.legacy_db_relation.set_up_relation(relation)) - postgresql_mock.create_user.assert_called_once_with(user, "test-password", False) - postgresql_mock.create_database.assert_called_once_with( - DATABASE, user, plugins=[], client_relations=[relation] - ) - _update_endpoints.assert_called_once() - _update_unit_status.assert_called_once() - self.assertNotIsInstance(self.harness.model.unit.status, BlockedStatus) - - # Assert that the correct calls were made when the database name is not provided. - postgresql_mock.create_user.reset_mock() - postgresql_mock.create_database.reset_mock() - postgresql_mock.get_postgresql_version.reset_mock() - _update_endpoints.reset_mock() - _update_unit_status.reset_mock() - with self.harness.hooks_disabled(): - self.harness.update_relation_data( - self.rel_id, - "application/0", - {"database": ""}, - ) - self.assertTrue(self.harness.charm.legacy_db_relation.set_up_relation(relation)) - postgresql_mock.create_user.assert_called_once_with(user, "test-password", False) - postgresql_mock.create_database.assert_called_once_with( - "application", user, plugins=[], client_relations=[relation] - ) - _update_endpoints.assert_called_once() - _update_unit_status.assert_called_once() - self.assertNotIsInstance(self.harness.model.unit.status, BlockedStatus) - - # BlockedStatus due to a PostgreSQLCreateUserError. - postgresql_mock.create_database.reset_mock() - postgresql_mock.get_postgresql_version.reset_mock() - _update_endpoints.reset_mock() - _update_unit_status.reset_mock() - self.assertFalse(self.harness.charm.legacy_db_relation.set_up_relation(relation)) - postgresql_mock.create_database.assert_not_called() - _update_endpoints.assert_not_called() - _update_unit_status.assert_not_called() - self.assertIsInstance(self.harness.model.unit.status, BlockedStatus) - - # BlockedStatus due to a PostgreSQLCreateDatabaseError. - self.harness.charm.unit.status = ActiveStatus() - self.assertFalse(self.harness.charm.legacy_db_relation.set_up_relation(relation)) - _update_endpoints.assert_not_called() - _update_unit_status.assert_not_called() - self.assertIsInstance(self.harness.model.unit.status, BlockedStatus) - - @patch("relations.db.DbProvides._check_for_blocking_relations") - @patch("charm.PostgresqlOperatorCharm.is_blocked", new_callable=PropertyMock) - def test_update_unit_status(self, _is_blocked, _check_for_blocking_relations): # Test when the charm is not blocked. - relation = self.harness.model.get_relation(RELATION_NAME, self.rel_id) + rel_id = harness.model.get_relation(RELATION_NAME).id + relation = harness.model.get_relation(RELATION_NAME, rel_id) _is_blocked.return_value = False - self.harness.charm.legacy_db_relation._update_unit_status(relation) + harness.charm.legacy_db_relation._update_unit_status(relation) _check_for_blocking_relations.assert_not_called() - self.assertNotIsInstance(self.harness.charm.unit.status, ActiveStatus) + tc().assertNotIsInstance(harness.charm.unit.status, ActiveStatus) # Test when the charm is blocked but not due to extensions request. _is_blocked.return_value = True - self.harness.charm.unit.status = BlockedStatus("fake message") - self.harness.charm.legacy_db_relation._update_unit_status(relation) + harness.charm.unit.status = BlockedStatus("fake message") + harness.charm.legacy_db_relation._update_unit_status(relation) _check_for_blocking_relations.assert_not_called() - self.assertNotIsInstance(self.harness.charm.unit.status, ActiveStatus) + tc().assertNotIsInstance(harness.charm.unit.status, ActiveStatus) # Test when there are relations causing the blocked status. - self.harness.charm.unit.status = BlockedStatus( + harness.charm.unit.status = BlockedStatus( "extensions requested through relation, enable them through config options" ) _check_for_blocking_relations.return_value = True - self.harness.charm.legacy_db_relation._update_unit_status(relation) + harness.charm.legacy_db_relation._update_unit_status(relation) _check_for_blocking_relations.assert_called_once_with(relation.id) - self.assertNotIsInstance(self.harness.charm.unit.status, ActiveStatus) + tc().assertNotIsInstance(harness.charm.unit.status, ActiveStatus) # Test when there are no relations causing the blocked status anymore. _check_for_blocking_relations.reset_mock() _check_for_blocking_relations.return_value = False - self.harness.charm.legacy_db_relation._update_unit_status(relation) + harness.charm.legacy_db_relation._update_unit_status(relation) _check_for_blocking_relations.assert_called_once_with(relation.id) - self.assertIsInstance(self.harness.charm.unit.status, ActiveStatus) + tc().assertIsInstance(harness.charm.unit.status, ActiveStatus) - @patch( - "charm.PostgresqlOperatorCharm.primary_endpoint", - new_callable=PropertyMock, - ) - @patch("charm.PostgresqlOperatorCharm.is_blocked", new_callable=PropertyMock) - @patch("charm.Patroni.member_started", new_callable=PropertyMock) - @patch("charm.DbProvides._on_relation_departed") - def test_on_relation_broken_extensions_unblock( - self, _on_relation_departed, _member_started, _primary_endpoint, is_blocked +@patch_network_get(private_address="1.1.1.1") +def test_on_relation_broken_extensions_unblock(harness): + with ( + patch.object(PostgresqlOperatorCharm, "postgresql", Mock()) as postgresql_mock, + patch( + "charm.PostgresqlOperatorCharm.primary_endpoint", + new_callable=PropertyMock, + ) as _primary_endpoint, + patch("charm.PostgresqlOperatorCharm.is_blocked", new_callable=PropertyMock) as is_blocked, + patch("charm.Patroni.member_started", new_callable=PropertyMock) as _member_started, + patch("charm.DbProvides._on_relation_departed") as _on_relation_departed, ): - with patch.object(PostgresqlOperatorCharm, "postgresql", Mock()) as postgresql_mock: - # Set some side effects to test multiple situations. - is_blocked.return_value = True - _member_started.return_value = True - _primary_endpoint.return_value = {"1.1.1.1"} - postgresql_mock.delete_user = PropertyMock(return_value=None) - self.harness.model.unit.status = BlockedStatus( - "extensions requested through relation, enable them through config options" + # Set some side effects to test multiple situations. + rel_id = harness.model.get_relation(RELATION_NAME).id + is_blocked.return_value = True + _member_started.return_value = True + _primary_endpoint.return_value = {"1.1.1.1"} + postgresql_mock.delete_user = PropertyMock(return_value=None) + harness.model.unit.status = BlockedStatus( + "extensions requested through relation, enable them through config options" + ) + with harness.hooks_disabled(): + harness.update_relation_data( + rel_id, + "application", + {"database": DATABASE, "extensions": "test"}, ) - with self.harness.hooks_disabled(): - self.harness.update_relation_data( - self.rel_id, - "application", - {"database": DATABASE, "extensions": "test"}, - ) - - # Break the relation that blocked the charm. - self.harness.remove_relation(self.rel_id) - self.assertTrue(isinstance(self.harness.model.unit.status, ActiveStatus)) - - @patch( - "charm.PostgresqlOperatorCharm.primary_endpoint", - new_callable=PropertyMock, - ) - @patch("charm.PostgresqlOperatorCharm.is_blocked", new_callable=PropertyMock) - @patch("charm.Patroni.member_started", new_callable=PropertyMock) - @patch("charm.DbProvides._on_relation_departed") - def test_on_relation_broken_extensions_keep_block( - self, _on_relation_departed, _member_started, _primary_endpoint, is_blocked + + # Break the relation that blocked the charm. + harness.remove_relation(rel_id) + tc().assertTrue(isinstance(harness.model.unit.status, ActiveStatus)) + +@patch_network_get(private_address="1.1.1.1") +def test_on_relation_broken_extensions_keep_block(harness): + with ( + patch.object(PostgresqlOperatorCharm, "postgresql", Mock()) as postgresql_mock, + patch("charm.DbProvides._on_relation_departed") as _on_relation_departed, + patch("charm.Patroni.member_started", new_callable=PropertyMock) as _member_started, + patch( + "charm.PostgresqlOperatorCharm.primary_endpoint", + new_callable=PropertyMock, + ) as _primary_endpoint, + patch("charm.PostgresqlOperatorCharm.is_blocked", new_callable=PropertyMock) as is_blocked, ): - with patch.object(PostgresqlOperatorCharm, "postgresql", Mock()) as postgresql_mock: - # Set some side effects to test multiple situations. - is_blocked.return_value = True - _member_started.return_value = True - _primary_endpoint.return_value = {"1.1.1.1"} - postgresql_mock.delete_user = PropertyMock(return_value=None) - self.harness.model.unit.status = BlockedStatus( - "extensions requested through relation, enable them through config options" + # Set some side effects to test multiple situations. + rel_id = harness.model.get_relation(RELATION_NAME).id + is_blocked.return_value = True + _member_started.return_value = True + _primary_endpoint.return_value = {"1.1.1.1"} + postgresql_mock.delete_user = PropertyMock(return_value=None) + harness.model.unit.status = BlockedStatus( + "extensions requested through relation, enable them through config options" + ) + with harness.hooks_disabled(): + first_rel_id = harness.add_relation(RELATION_NAME, "application1") + harness.update_relation_data( + first_rel_id, + "application1", + {"database": DATABASE, "extensions": "test"}, ) - with self.harness.hooks_disabled(): - first_rel_id = self.harness.add_relation(RELATION_NAME, "application1") - self.harness.update_relation_data( - first_rel_id, - "application1", - {"database": DATABASE, "extensions": "test"}, - ) - second_rel_id = self.harness.add_relation(RELATION_NAME, "application2") - self.harness.update_relation_data( - second_rel_id, - "application2", - {"database": DATABASE, "extensions": "test"}, - ) - - event = Mock() - event.relation.id = first_rel_id - # Break one of the relations that block the charm. - self.harness.charm.legacy_db_relation._on_relation_broken(event) - self.assertTrue(isinstance(self.harness.model.unit.status, BlockedStatus)) - - @patch( - "charm.DbProvides._get_state", - side_effect="postgresql/0", - ) - @patch( - "charm.PostgresqlOperatorCharm.primary_endpoint", - new_callable=PropertyMock(return_value="1.1.1.1"), - ) - @patch( - "charm.PostgresqlOperatorCharm.members_ips", - new_callable=PropertyMock, - ) - @patch("charm.Patroni.get_primary", return_value="postgresql/0") - def test_update_endpoints_with_relation( - self, _get_primary, _members_ips, _primary_endpoint, _get_state - ): - with patch.object(PostgresqlOperatorCharm, "postgresql", Mock()) as postgresql_mock: - # Set some side effects to test multiple situations. - postgresql_mock.get_postgresql_version = PropertyMock( - side_effect=[ - PostgreSQLGetPostgreSQLVersionError, - POSTGRESQL_VERSION, - POSTGRESQL_VERSION, - ] + second_rel_id = harness.add_relation(RELATION_NAME, "application2") + harness.update_relation_data( + second_rel_id, + "application2", + {"database": DATABASE, "extensions": "test"}, ) - # Mock the members_ips list to simulate different scenarios - # (with and without a replica). - _members_ips.side_effect = [ - {"1.1.1.1", "2.2.2.2"}, - {"1.1.1.1", "2.2.2.2"}, - {"1.1.1.1"}, - {"1.1.1.1"}, + event = Mock() + event.relation.id = first_rel_id + # Break one of the relations that block the charm. + harness.charm.legacy_db_relation._on_relation_broken(event) + tc().assertTrue(isinstance(harness.model.unit.status, BlockedStatus)) + +@patch_network_get(private_address="1.1.1.1") +def test_update_endpoints_with_relation(harness): + with ( + patch.object(PostgresqlOperatorCharm, "postgresql", Mock()) as postgresql_mock, + patch("charm.Patroni.get_primary") as _get_primary, + patch( + "charm.PostgresqlOperatorCharm.members_ips", + new_callable=PropertyMock, + ) as _members_ips, + patch( + "charm.PostgresqlOperatorCharm.primary_endpoint", + new_callable=PropertyMock(return_value="1.1.1.1"), + ) as _primary_endpoint, + patch( + "charm.DbProvides._get_state", + side_effect="postgresql/0", + ) as _get_state, + ): + peer_rel_id = harness.model.get_relation(PEER).id + # Set some side effects to test multiple situations. + postgresql_mock.get_postgresql_version = PropertyMock( + side_effect=[ + PostgreSQLGetPostgreSQLVersionError, + POSTGRESQL_VERSION, + POSTGRESQL_VERSION, ] + ) - # Add two different relations. - self.rel_id = self.harness.add_relation(RELATION_NAME, "application") - self.another_rel_id = self.harness.add_relation(RELATION_NAME, "application") + # Mock the members_ips list to simulate different scenarios + # (with and without a replica). + _members_ips.side_effect = [ + {"1.1.1.1", "2.2.2.2"}, + {"1.1.1.1", "2.2.2.2"}, + {"1.1.1.1"}, + {"1.1.1.1"}, + ] + + # Add two different relations. + rel_id = harness.add_relation(RELATION_NAME, "application") + another_rel_id = harness.add_relation(RELATION_NAME, "application") - # Get the relation to be used in the subsequent update endpoints calls. - relation = self.harness.model.get_relation(RELATION_NAME, self.rel_id) + # Get the relation to be used in the subsequent update endpoints calls. + relation = harness.model.get_relation(RELATION_NAME, rel_id) - # Set some data to be used and compared in the relations. - password = "test-password" - master = ( - f"dbname={DATABASE} host=1.1.1.1 password={password} port={DATABASE_PORT} user=" + # Set some data to be used and compared in the relations. + password = "test-password" + master = ( + f"dbname={DATABASE} host=1.1.1.1 password={password} port={DATABASE_PORT} user=" + ) + standbys = ( + f"dbname={DATABASE} host=2.2.2.2 password={password} port={DATABASE_PORT} user=" + ) + + # Set some required data before update_endpoints is called. + for rel in [rel_id, another_rel_id]: + user = f"relation-{rel}" + harness.update_relation_data( + rel, + harness.charm.app.name, + { + "user": user, + "password": password, + "database": DATABASE, + }, ) - standbys = ( - f"dbname={DATABASE} host=2.2.2.2 password={password} port={DATABASE_PORT} user=" + harness.update_relation_data( + peer_rel_id, + harness.charm.app.name, + { + user: password, + f"{user}-database": DATABASE, + }, ) - # Set some required data before update_endpoints is called. - for rel_id in [self.rel_id, self.another_rel_id]: - user = f"relation-{rel_id}" - self.harness.update_relation_data( - rel_id, - self.app, - { - "user": user, - "password": password, - "database": DATABASE, - }, - ) - self.harness.update_relation_data( - self.peer_rel_id, - self.app, - { - user: password, - f"{user}-database": DATABASE, - }, - ) - - # BlockedStatus due to a PostgreSQLGetPostgreSQLVersionError. - self.legacy_db_relation.update_endpoints(relation) - self.assertIsInstance(self.harness.model.unit.status, BlockedStatus) - self.assertEqual(self.harness.get_relation_data(rel_id, self.unit), {}) - - # Test with both a primary and a replica. - # Update the endpoints with the event and check that it updated only - # the right relation databags (the app and unit databags from the event). - self.legacy_db_relation.update_endpoints(relation) - for rel_id in [self.rel_id, self.another_rel_id]: - # Set the expected username based on the relation id. - user = f"relation-{rel_id}" - - # Set the assert function based on each relation (whether it should have data). - assert_based_on_relation = ( - self.assertTrue if rel_id == self.rel_id else self.assertFalse - ) - - # Check that the unit relation databag contains (or not) the endpoints. - unit_relation_data = self.harness.get_relation_data(rel_id, self.unit) - print(f"unit_relation_data: {unit_relation_data}") - assert_based_on_relation( - "master" in unit_relation_data - and master + user == unit_relation_data["master"] - ) - assert_based_on_relation( - "standbys" in unit_relation_data - and standbys + user == unit_relation_data["standbys"] - ) - - # Also test with only a primary instance. - self.legacy_db_relation.update_endpoints(relation) - for rel_id in [self.rel_id, self.another_rel_id]: - # Set the expected username based on the relation id. - user = f"relation-{rel_id}" - - # Set the assert function based on each relation (whether it should have data). - assert_based_on_relation = ( - self.assertTrue if rel_id == self.rel_id else self.assertFalse - ) - - # Check that the unit relation databag contains the endpoints. - unit_relation_data = self.harness.get_relation_data(rel_id, self.unit) - assert_based_on_relation( - "master" in unit_relation_data - and master + user == unit_relation_data["master"] - ) - assert_based_on_relation( - "standbys" in unit_relation_data - and standbys + user == unit_relation_data["standbys"] - ) - - @patch( - "charm.DbProvides._get_state", - side_effect="postgresql/0", - ) - @patch( - "charm.PostgresqlOperatorCharm.primary_endpoint", - new_callable=PropertyMock(return_value="1.1.1.1"), - ) - @patch( - "charm.PostgresqlOperatorCharm.members_ips", - new_callable=PropertyMock, - ) - @patch("charm.Patroni.get_primary") - def test_update_endpoints_without_relation( - self, _get_primary, _members_ips, _primary_endpoint, _get_state - ): - with patch.object(PostgresqlOperatorCharm, "postgresql", Mock()) as postgresql_mock: - # Set some side effects to test multiple situations. - postgresql_mock.get_postgresql_version = PropertyMock( - side_effect=[ - PostgreSQLGetPostgreSQLVersionError, - POSTGRESQL_VERSION, - POSTGRESQL_VERSION, - ] + # BlockedStatus due to a PostgreSQLGetPostgreSQLVersionError. + harness.charm.legacy_db_relation.update_endpoints(relation) + tc().assertIsInstance(harness.model.unit.status, BlockedStatus) + tc().assertEqual(harness.get_relation_data(rel_id, harness.charm.unit.name), {}) + + # Test with both a primary and a replica. + # Update the endpoints with the event and check that it updated only + # the right relation databags (the app and unit databags from the event). + harness.charm.legacy_db_relation.update_endpoints(relation) + for rel in [rel_id, another_rel_id]: + # Set the expected username based on the relation id. + user = f"relation-{rel}" + + # Set the assert function based on each relation (whether it should have data). + assert_based_on_relation = ( + tc().assertTrue if rel == rel_id else tc().assertFalse + ) + + # Check that the unit relation databag contains (or not) the endpoints. + unit_relation_data = harness.get_relation_data(rel, harness.charm.unit.name) + print(f"unit_relation_data: {unit_relation_data}") + assert_based_on_relation( + "master" in unit_relation_data + and master + user == unit_relation_data["master"] + ) + assert_based_on_relation( + "standbys" in unit_relation_data + and standbys + user == unit_relation_data["standbys"] ) - _get_primary.return_value = self.unit - # Mock the members_ips list to simulate different scenarios - # (with and without a replica). - _members_ips.side_effect = [ - {"1.1.1.1", "2.2.2.2"}, - {"1.1.1.1", "2.2.2.2"}, - {"1.1.1.1"}, - {"1.1.1.1"}, - ] - # Add two different relations. - self.rel_id = self.harness.add_relation(RELATION_NAME, "application") - self.another_rel_id = self.harness.add_relation(RELATION_NAME, "application") + # Also test with only a primary instance. + harness.charm.legacy_db_relation.update_endpoints(relation) + for rel in [rel_id, another_rel_id]: + # Set the expected username based on the relation id. + user = f"relation-{rel}" - # Set some data to be used and compared in the relations. - password = "test-password" - master = ( - f"dbname={DATABASE} host=1.1.1.1 password={password} port={DATABASE_PORT} user=" + # Set the assert function based on each relation (whether it should have data). + assert_based_on_relation = ( + tc().assertTrue if rel == rel_id else tc().assertFalse ) - standbys = ( - f"dbname={DATABASE} host=2.2.2.2 password={password} port={DATABASE_PORT} user=" + + # Check that the unit relation databag contains the endpoints. + unit_relation_data = harness.get_relation_data(rel, harness.charm.unit.name) + assert_based_on_relation( + "master" in unit_relation_data + and master + user == unit_relation_data["master"] + ) + assert_based_on_relation( + "standbys" in unit_relation_data + and standbys + user == unit_relation_data["standbys"] ) - # Set some required data before update_endpoints is called. - for rel_id in [self.rel_id, self.another_rel_id]: - user = f"relation-{rel_id}" - self.harness.update_relation_data( - rel_id, - self.app, - { - "user": user, - "password": password, - "database": DATABASE, - }, - ) - self.harness.update_relation_data( - self.peer_rel_id, - self.app, - { - user: password, - f"{user}-database": DATABASE, - }, - ) - - # BlockedStatus due to a PostgreSQLGetPostgreSQLVersionError. - self.legacy_db_relation.update_endpoints() - self.assertIsInstance(self.harness.model.unit.status, BlockedStatus) - self.assertEqual(self.harness.get_relation_data(rel_id, self.unit), {}) - - # Test with both a primary and a replica. - # Update the endpoints and check that all relations' databags are updated. - self.legacy_db_relation.update_endpoints() - for rel_id in [self.rel_id, self.another_rel_id]: - # Set the expected username based on the relation id. - user = f"relation-{rel_id}" - - # Check that the unit relation databag contains the endpoints. - unit_relation_data = self.harness.get_relation_data(rel_id, self.unit) - self.assertTrue( - "master" in unit_relation_data - and master + user == unit_relation_data["master"] - ) - self.assertTrue( - "standbys" in unit_relation_data - and standbys + user == unit_relation_data["standbys"] - ) - - # Also test with only a primary instance. - self.legacy_db_relation.update_endpoints() - for rel_id in [self.rel_id, self.another_rel_id]: - # Set the expected username based on the relation id. - user = f"relation-{rel_id}" - - # Check that the unit relation databag contains the endpoints. - unit_relation_data = self.harness.get_relation_data(rel_id, self.unit) - self.assertTrue( - "master" in unit_relation_data - and master + user == unit_relation_data["master"] - ) - self.assertTrue( - "standbys" in unit_relation_data - and standbys + user == unit_relation_data["standbys"] - ) - - def test_get_allowed_units(self): - # No allowed units from the current database application. - peer_relation = self.harness.model.get_relation(PEER, self.peer_rel_id) - self.assertEqual(self.legacy_db_relation._get_allowed_units(peer_relation), "") - - # List of space separated allowed units from the other application. - self.harness.add_relation_unit(self.rel_id, "application/1") - db_relation = self.harness.model.get_relation(RELATION_NAME, self.rel_id) - self.assertEqual( - self.legacy_db_relation._get_allowed_units(db_relation), "application/0 application/1" +@patch_network_get(private_address="1.1.1.1") +def test_update_endpoints_without_relation(harness): + with ( + patch.object(PostgresqlOperatorCharm, "postgresql", Mock()) as postgresql_mock, + patch("charm.Patroni.get_primary") as _get_primary, + patch( + "charm.PostgresqlOperatorCharm.members_ips", + new_callable=PropertyMock, + ) as _members_ips, + patch( + "charm.PostgresqlOperatorCharm.primary_endpoint", + new_callable=PropertyMock(return_value="1.1.1.1"), + ) as _primary_endpoint, + patch( + "charm.DbProvides._get_state", + side_effect="postgresql/0", + ) as _get_state, + ): + # Set some side effects to test multiple situations. + peer_rel_id = harness.model.get_relation(PEER).id + postgresql_mock.get_postgresql_version = PropertyMock( + side_effect=[ + PostgreSQLGetPostgreSQLVersionError, + POSTGRESQL_VERSION, + POSTGRESQL_VERSION, + ] ) + _get_primary.return_value = harness.charm.unit.name + # Mock the members_ips list to simulate different scenarios + # (with and without a replica). + _members_ips.side_effect = [ + {"1.1.1.1", "2.2.2.2"}, + {"1.1.1.1", "2.2.2.2"}, + {"1.1.1.1"}, + {"1.1.1.1"}, + ] + + # Add two different relations. + rel_id = harness.add_relation(RELATION_NAME, "application") + another_rel_id = harness.add_relation(RELATION_NAME, "application") + + # Set some data to be used and compared in the relations. + password = "test-password" + master = ( + f"dbname={DATABASE} host=1.1.1.1 password={password} port={DATABASE_PORT} user=" + ) + standbys = ( + f"dbname={DATABASE} host=2.2.2.2 password={password} port={DATABASE_PORT} user=" + ) + + # Set some required data before update_endpoints is called. + for rel in [rel_id, another_rel_id]: + user = f"relation-{rel}" + harness.update_relation_data( + rel, + harness.charm.app.name, + { + "user": user, + "password": password, + "database": DATABASE, + }, + ) + harness.update_relation_data( + peer_rel_id, + harness.charm.app.name, + { + user: password, + f"{user}-database": DATABASE, + }, + ) + + # BlockedStatus due to a PostgreSQLGetPostgreSQLVersionError. + harness.charm.legacy_db_relation.update_endpoints() + tc().assertIsInstance(harness.model.unit.status, BlockedStatus) + tc().assertEqual(harness.get_relation_data(rel_id, harness.charm.unit.name), {}) + + # Test with both a primary and a replica. + # Update the endpoints and check that all relations' databags are updated. + harness.charm.legacy_db_relation.update_endpoints() + for rel in [rel_id, another_rel_id]: + # Set the expected username based on the relation id. + user = f"relation-{rel}" + + # Check that the unit relation databag contains the endpoints. + unit_relation_data = harness.get_relation_data(rel, harness.charm.unit.name) + tc().assertTrue( + "master" in unit_relation_data + and master + user == unit_relation_data["master"] + ) + tc().assertTrue( + "standbys" in unit_relation_data + and standbys + user == unit_relation_data["standbys"] + ) + + # Also test with only a primary instance. + harness.charm.legacy_db_relation.update_endpoints() + for rel in [rel_id, another_rel_id]: + # Set the expected username based on the relation id. + user = f"relation-{rel}" + + # Check that the unit relation databag contains the endpoints. + unit_relation_data = harness.get_relation_data(rel, harness.charm.unit.name) + tc().assertTrue( + "master" in unit_relation_data + and master + user == unit_relation_data["master"] + ) + tc().assertTrue( + "standbys" in unit_relation_data + and standbys + user == unit_relation_data["standbys"] + ) + +@patch_network_get(private_address="1.1.1.1") +def test_get_allowed_units(harness): + # No allowed units from the current database application. + peer_rel_id = harness.model.get_relation(PEER).id + rel_id = harness.model.get_relation(RELATION_NAME).id + peer_relation = harness.model.get_relation(PEER, peer_rel_id) + tc().assertEqual(harness.charm.legacy_db_relation._get_allowed_units(peer_relation), "") + + # List of space separated allowed units from the other application. + harness.add_relation_unit(rel_id, "application/1") + db_relation = harness.model.get_relation(RELATION_NAME, rel_id) + tc().assertEqual( + harness.charm.legacy_db_relation._get_allowed_units(db_relation), "application/0 application/1" + ) diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index de4812ce5d..77f1e018c8 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -2,19 +2,18 @@ # See LICENSE file for licensing details. import re -import unittest +from unittest import TestCase as tc from utils import new_password -class TestUtils(unittest.TestCase): - def test_new_password(self): - # Test the password generation twice in order to check if we get different passwords and - # that they meet the required criteria. - first_password = new_password() - self.assertEqual(len(first_password), 16) - self.assertIsNotNone(re.fullmatch("[a-zA-Z0-9\b]{16}$", first_password)) +def test_new_password(): + # Test the password generation twice in order to check if we get different passwords and + # that they meet the required criteria. + first_password = new_password() + tc().assertEqual(len(first_password), 16) + tc().assertIsNotNone(re.fullmatch("[a-zA-Z0-9\b]{16}$", first_password)) - second_password = new_password() - self.assertIsNotNone(re.fullmatch("[a-zA-Z0-9\b]{16}$", second_password)) - self.assertNotEqual(second_password, first_password) + second_password = new_password() + tc().assertIsNotNone(re.fullmatch("[a-zA-Z0-9\b]{16}$", second_password)) + tc().assertNotEqual(second_password, first_password) From c27f6b1572cd1d29fabd57482543c0eaac8494a2 Mon Sep 17 00:00:00 2001 From: Lucas Gameiro Borges Date: Mon, 22 Apr 2024 11:03:23 +0000 Subject: [PATCH 3/8] remove print statement --- tests/unit/test_db.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/unit/test_db.py b/tests/unit/test_db.py index a54a3929f0..b147731255 100644 --- a/tests/unit/test_db.py +++ b/tests/unit/test_db.py @@ -507,7 +507,6 @@ def test_update_endpoints_with_relation(harness): # Check that the unit relation databag contains (or not) the endpoints. unit_relation_data = harness.get_relation_data(rel, harness.charm.unit.name) - print(f"unit_relation_data: {unit_relation_data}") assert_based_on_relation( "master" in unit_relation_data and master + user == unit_relation_data["master"] From 6ed4250d31bc4760f2c1f46fc658c504f679580a Mon Sep 17 00:00:00 2001 From: Lucas Gameiro Borges Date: Mon, 22 Apr 2024 11:10:58 +0000 Subject: [PATCH 4/8] add conftest with has_secrets fixture --- tests/unit/conftest.py | 12 ++++++++++++ tests/unit/test_charm.py | 8 -------- 2 files changed, 12 insertions(+), 8 deletions(-) create mode 100644 tests/unit/conftest.py diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py new file mode 100644 index 0000000000..dd66e3a910 --- /dev/null +++ b/tests/unit/conftest.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python3 +# Copyright 2022 Canonical Ltd. +# See LICENSE file for licensing details. +import pytest +from unittest.mock import PropertyMock + +# 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)) + return request.param diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py index 07d11ae441..a4b5bb649b 100644 --- a/tests/unit/test_charm.py +++ b/tests/unit/test_charm.py @@ -48,14 +48,6 @@ def harness(): harness.cleanup() -# 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)) - 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( From ca90f65c63fb3cd7ced68be98006f2d19d83d560 Mon Sep 17 00:00:00 2001 From: Lucas Gameiro Borges Date: Mon, 22 Apr 2024 11:16:32 +0000 Subject: [PATCH 5/8] fix commit email From e092b8a4528b8c966a79a4100e5b5ecf2f215898 Mon Sep 17 00:00:00 2001 From: Lucas Gameiro Borges Date: Mon, 22 Apr 2024 11:23:26 +0000 Subject: [PATCH 6/8] fix lint --- tests/unit/conftest.py | 4 +- tests/unit/test_backups.py | 377 +++++++++++-------- tests/unit/test_cluster.py | 73 ++-- tests/unit/test_cluster_topology_observer.py | 5 +- tests/unit/test_db.py | 124 +++--- tests/unit/test_postgresql_provider.py | 45 ++- tests/unit/test_upgrade.py | 51 ++- tests/unit/test_utils.py | 10 +- 8 files changed, 395 insertions(+), 294 deletions(-) diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index dd66e3a910..e32583ed9b 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -1,9 +1,11 @@ #!/usr/bin/env python3 # Copyright 2022 Canonical Ltd. # See LICENSE file for licensing details. -import pytest from unittest.mock import PropertyMock +import pytest + + # 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) diff --git a/tests/unit/test_backups.py b/tests/unit/test_backups.py index 658f7f7a8f..50d60487c0 100644 --- a/tests/unit/test_backups.py +++ b/tests/unit/test_backups.py @@ -1,13 +1,13 @@ # Copyright 2023 Canonical Ltd. # See LICENSE file for licensing details. -import pytest from pathlib import PosixPath from subprocess import PIPE, CompletedProcess, TimeoutExpired from typing import OrderedDict +from unittest import TestCase from unittest.mock import ANY, MagicMock, PropertyMock, call, mock_open, patch -from unittest import TestCase as tc import botocore as botocore +import pytest from boto3.exceptions import S3UploadFailedError from botocore.exceptions import ClientError from jinja2 import Template @@ -39,21 +39,24 @@ def harness(): yield harness harness.cleanup() + def test_stanza_name(harness): - tc().assertEqual( - harness.charm.backup.stanza_name, f"{harness.charm.model.name}.{harness.charm.cluster_name}" + TestCase().assertEqual( + harness.charm.backup.stanza_name, + f"{harness.charm.model.name}.{harness.charm.cluster_name}", ) + def test_are_backup_settings_ok(harness): # Test without S3 relation. - tc().assertEqual( + TestCase().assertEqual( harness.charm.backup._are_backup_settings_ok(), (False, "Relation with s3-integrator charm missing, cannot create/restore backup."), ) # Test when there are missing S3 parameters. harness.add_relation(S3_PARAMETERS_RELATION, "s3-integrator") - tc().assertEqual( + TestCase().assertEqual( harness.charm.backup._are_backup_settings_ok(), (False, "Missing S3 parameters: ['bucket', 'access-key', 'secret-key']"), ) @@ -61,23 +64,26 @@ def test_are_backup_settings_ok(harness): # Test when all required parameters are provided. with patch("charm.PostgreSQLBackups._retrieve_s3_parameters") as _retrieve_s3_parameters: _retrieve_s3_parameters.return_value = ["bucket", "access-key", "secret-key"], [] - tc().assertEqual( + TestCase().assertEqual( harness.charm.backup._are_backup_settings_ok(), (True, None), ) + @patch_network_get(private_address="1.1.1.1") def test_can_unit_perform_backup(harness): with ( patch("charm.PostgreSQLBackups._are_backup_settings_ok") as _are_backup_settings_ok, patch("charm.Patroni.member_started", new_callable=PropertyMock) as _member_started, patch("ops.model.Application.planned_units") as _planned_units, - patch("charm.PostgresqlOperatorCharm.is_primary", new_callable=PropertyMock) as _is_primary, + patch( + "charm.PostgresqlOperatorCharm.is_primary", new_callable=PropertyMock + ) as _is_primary, ): peer_rel_id = harness.model.get_relation(PEER).id # Test when the charm fails to retrieve the primary. _is_primary.side_effect = RetryError(last_attempt=1) - tc().assertEqual( + TestCase().assertEqual( harness.charm.backup._can_unit_perform_backup(), (False, "Unit cannot perform backups as the database seems to be offline"), ) @@ -86,7 +92,7 @@ def test_can_unit_perform_backup(harness): _is_primary.side_effect = None _is_primary.return_value = True harness.charm.unit.status = BlockedStatus("fake blocked state") - tc().assertEqual( + TestCase().assertEqual( harness.charm.backup._can_unit_perform_backup(), (False, "Unit is in a blocking state"), ) @@ -100,7 +106,7 @@ def test_can_unit_perform_backup(harness): harness.charm.unit.name, {"tls": "True"}, ) - tc().assertEqual( + TestCase().assertEqual( harness.charm.backup._can_unit_perform_backup(), (False, "Unit cannot perform backups as it is the cluster primary"), ) @@ -113,7 +119,7 @@ def test_can_unit_perform_backup(harness): harness.charm.unit.name, {"tls": ""}, ) - tc().assertEqual( + TestCase().assertEqual( harness.charm.backup._can_unit_perform_backup(), (False, "Unit cannot perform backups as TLS is not enabled"), ) @@ -121,14 +127,14 @@ def test_can_unit_perform_backup(harness): # Test when Patroni or PostgreSQL hasn't started yet. _is_primary.return_value = True _member_started.return_value = False - tc().assertEqual( + TestCase().assertEqual( harness.charm.backup._can_unit_perform_backup(), (False, "Unit cannot perform backups as it's not in running state"), ) # Test when the stanza was not initialised yet. _member_started.return_value = True - tc().assertEqual( + TestCase().assertEqual( harness.charm.backup._can_unit_perform_backup(), (False, "Stanza was not initialised"), ) @@ -141,18 +147,19 @@ def test_can_unit_perform_backup(harness): {"stanza": harness.charm.backup.stanza_name}, ) _are_backup_settings_ok.return_value = (False, "fake error message") - tc().assertEqual( + TestCase().assertEqual( harness.charm.backup._can_unit_perform_backup(), (False, "fake error message"), ) # Test when everything is ok to run a backup. _are_backup_settings_ok.return_value = (True, None) - tc().assertEqual( + TestCase().assertEqual( harness.charm.backup._can_unit_perform_backup(), (True, None), ) + @patch_network_get(private_address="1.1.1.1") def test_can_use_s3_repository(harness): with ( @@ -160,7 +167,9 @@ def test_can_use_s3_repository(harness): patch("charm.PostgreSQLBackups._execute_command") as _execute_command, patch("charm.Patroni.member_started", new_callable=PropertyMock) as _member_started, patch("charm.PostgresqlOperatorCharm.update_config") as _update_config, - patch("charm.Patroni.get_postgresql_version", return_value="14.10") as _get_postgresql_version, + patch( + "charm.Patroni.get_postgresql_version", return_value="14.10" + ) as _get_postgresql_version, ): peer_rel_id = harness.model.get_relation(PEER).id # Define the stanza name inside the unit relation data. @@ -173,12 +182,12 @@ def test_can_use_s3_repository(harness): # Test when nothing is returned from the pgBackRest info command. _execute_command.side_effect = TimeoutExpired(cmd="fake command", timeout=30) - with tc().assertRaises(TimeoutError): + with TestCase().assertRaises(TimeoutError): harness.charm.backup.can_use_s3_repository() _execute_command.side_effect = None _execute_command.return_value = (1, "", "") - tc().assertEqual( + TestCase().assertEqual( harness.charm.backup.can_use_s3_repository(), (False, FAILED_TO_INITIALIZE_STANZA_ERROR_MESSAGE), ) @@ -190,13 +199,13 @@ def test_can_use_s3_repository(harness): "", ) _execute_command.return_value = pgbackrest_info_same_cluster_backup_output - tc().assertEqual( + TestCase().assertEqual( harness.charm.backup.can_use_s3_repository(), (True, None), ) # Assert that the stanza name is still in the unit relation data. - tc().assertEqual( + TestCase().assertEqual( harness.get_relation_data(peer_rel_id, harness.charm.app), {"stanza": harness.charm.backup.stanza_name}, ) @@ -210,8 +219,8 @@ def test_can_use_s3_repository(harness): ] with harness.hooks_disabled(): harness.set_leader() - with tc().assertRaises(Exception): - tc().assertEqual( + with TestCase().assertRaises(Exception): + TestCase().assertEqual( harness.charm.backup.can_use_s3_repository(), (False, ANOTHER_CLUSTER_REPOSITORY_ERROR_MESSAGE), ) @@ -240,7 +249,7 @@ def test_can_use_s3_repository(harness): harness.charm.app.name, {"stanza": harness.charm.backup.stanza_name}, ) - tc().assertEqual( + TestCase().assertEqual( harness.charm.backup.can_use_s3_repository(), (False, ANOTHER_CLUSTER_REPOSITORY_ERROR_MESSAGE), ) @@ -249,7 +258,7 @@ def test_can_use_s3_repository(harness): _reload_patroni_configuration.assert_called_once() # Assert that the stanza name is not present in the unit relation data anymore. - tc().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.app), {}) + TestCase().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.app), {}) # Test when the cluster system id can be retrieved, but it's different from the stanza system id. _update_config.reset_mock() @@ -275,7 +284,7 @@ def test_can_use_s3_repository(harness): harness.charm.app.name, {"stanza": harness.charm.backup.stanza_name}, ) - tc().assertEqual( + TestCase().assertEqual( harness.charm.backup.can_use_s3_repository(), (False, ANOTHER_CLUSTER_REPOSITORY_ERROR_MESSAGE), ) @@ -284,7 +293,7 @@ def test_can_use_s3_repository(harness): _reload_patroni_configuration.assert_called_once() # Assert that the stanza name is not present in the unit relation data anymore. - tc().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.app), {}) + TestCase().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.app), {}) # Test when the workload is not running. _update_config.reset_mock() @@ -301,7 +310,7 @@ def test_can_use_s3_repository(harness): pgbackrest_info_same_cluster_backup_output, other_instance_system_identifier_output, ] - tc().assertEqual( + TestCase().assertEqual( harness.charm.backup.can_use_s3_repository(), (False, ANOTHER_CLUSTER_REPOSITORY_ERROR_MESSAGE), ) @@ -310,7 +319,7 @@ def test_can_use_s3_repository(harness): _reload_patroni_configuration.assert_not_called() # Assert that the stanza name is not present in the unit relation data anymore. - tc().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.app), {}) + TestCase().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.app), {}) # Test when there is no backup from another cluster in the S3 repository. with harness.hooks_disabled(): @@ -323,37 +332,39 @@ def test_can_use_s3_repository(harness): pgbackrest_info_same_cluster_backup_output, same_instance_system_identifier_output, ] - tc().assertEqual( + TestCase().assertEqual( harness.charm.backup.can_use_s3_repository(), (True, None), ) # Assert that the stanza name is still in the unit relation data. - tc().assertEqual( + TestCase().assertEqual( harness.get_relation_data(peer_rel_id, harness.charm.app), {"stanza": harness.charm.backup.stanza_name}, ) + def test_construct_endpoint(harness): # Test with an AWS endpoint without region. s3_parameters = {"endpoint": "https://s3.amazonaws.com", "region": ""} - tc().assertEqual( + TestCase().assertEqual( harness.charm.backup._construct_endpoint(s3_parameters), "https://s3.amazonaws.com" ) # Test with an AWS endpoint with region. s3_parameters["region"] = "us-east-1" - tc().assertEqual( + TestCase().assertEqual( harness.charm.backup._construct_endpoint(s3_parameters), "https://s3.us-east-1.amazonaws.com", ) # Test with another cloud endpoint. s3_parameters["endpoint"] = "https://storage.googleapis.com" - tc().assertEqual( + TestCase().assertEqual( harness.charm.backup._construct_endpoint(s3_parameters), "https://storage.googleapis.com" ) + def test_create_bucket_if_not_exists(harness): with ( patch("boto3.session.Session.resource") as _resource, @@ -376,7 +387,7 @@ def test_create_bucket_if_not_exists(harness): [], ) _resource.side_effect = ValueError - with tc().assertRaises(ValueError): + with TestCase().assertRaises(ValueError): harness.charm.backup._create_bucket_if_not_exists() # Test when the bucket already exists. @@ -408,7 +419,7 @@ def test_create_bucket_if_not_exists(harness): error_response={"Error": {"Code": 1, "message": "fake error"}}, operation_name="fake operation name", ) - with tc().assertRaises(ClientError): + with TestCase().assertRaises(ClientError): harness.charm.backup._create_bucket_if_not_exists() head_bucket.assert_called_once() create.assert_called_once() @@ -420,7 +431,7 @@ def test_create_bucket_if_not_exists(harness): head_bucket.side_effect = botocore.exceptions.ConnectTimeoutError( endpoint_url="fake endpoint URL" ) - with tc().assertRaises(botocore.exceptions.ConnectTimeoutError): + with TestCase().assertRaises(botocore.exceptions.ConnectTimeoutError): harness.charm.backup._create_bucket_if_not_exists() head_bucket.assert_called_once() create.assert_not_called() @@ -435,7 +446,7 @@ def test_empty_data_files(harness): ): # Test when the data directory doesn't exist. _exists.return_value = False - tc().assertTrue(harness.charm.backup._empty_data_files()) + TestCase().assertTrue(harness.charm.backup._empty_data_files()) _rmtree.assert_not_called() # Test when the removal of the data files fails. @@ -443,15 +454,16 @@ def test_empty_data_files(harness): _exists.return_value = True _is_dir.return_value = True _rmtree.side_effect = OSError - tc().assertFalse(harness.charm.backup._empty_data_files()) + TestCase().assertFalse(harness.charm.backup._empty_data_files()) _rmtree.assert_called_once_with(path) # Test when data files are successfully removed. _rmtree.reset_mock() _rmtree.side_effect = None - tc().assertTrue(harness.charm.backup._empty_data_files()) + TestCase().assertTrue(harness.charm.backup._empty_data_files()) _rmtree.assert_called_once_with(path) + def test_change_connectivity_to_database(harness): with patch("charm.PostgresqlOperatorCharm.update_config") as _update_config: peer_rel_id = harness.model.get_relation(PEER).id @@ -465,7 +477,7 @@ def test_change_connectivity_to_database(harness): # Test when connectivity should be turned on. harness.charm.backup._change_connectivity_to_database(True) - tc().assertEqual( + TestCase().assertEqual( harness.get_relation_data(peer_rel_id, harness.charm.unit), {"connectivity": "on"}, ) @@ -474,12 +486,13 @@ def test_change_connectivity_to_database(harness): # Test when connectivity should be turned off. _update_config.reset_mock() harness.charm.backup._change_connectivity_to_database(False) - tc().assertEqual( + TestCase().assertEqual( harness.get_relation_data(peer_rel_id, harness.charm.unit), {"connectivity": "off"}, ) _update_config.assert_called_once() + def test_execute_command(harness): with ( patch("backups.run") as _run, @@ -488,7 +501,9 @@ def test_execute_command(harness): # Test when the command fails. command = "rm -r /var/lib/postgresql/data/pgdata".split() _run.return_value = CompletedProcess(command, 1, b"", b"fake stderr") - tc().assertEqual(harness.charm.backup._execute_command(command), (1, "", "fake stderr")) + TestCase().assertEqual( + harness.charm.backup._execute_command(command), (1, "", "fake stderr") + ) _run.assert_called_once_with( command, input=None, stdout=PIPE, stderr=PIPE, preexec_fn=ANY, timeout=None ) @@ -499,7 +514,7 @@ def test_execute_command(harness): _getpwnam.reset_mock() _run.side_effect = None _run.return_value = CompletedProcess(command, 0, b"fake stdout", b"") - tc().assertEqual( + TestCase().assertEqual( harness.charm.backup._execute_command(command, command_input=b"fake input", timeout=5), (0, "fake stdout", ""), ) @@ -508,9 +523,10 @@ def test_execute_command(harness): ) _getpwnam.assert_called_once_with("snap_daemon") + def test_format_backup_list(harness): # Test when there are no backups. - tc().assertEqual( + TestCase().assertEqual( harness.charm.backup._format_backup_list([]), """backup-id | backup-type | backup-status ----------------------------------------------------""", @@ -521,7 +537,7 @@ def test_format_backup_list(harness): ("2023-01-01T09:00:00Z", "physical", "failed: fake error"), ("2023-01-01T10:00:00Z", "physical", "finished"), ] - tc().assertEqual( + TestCase().assertEqual( harness.charm.backup._format_backup_list(backup_list), """backup-id | backup-type | backup-status ---------------------------------------------------- @@ -529,11 +545,12 @@ def test_format_backup_list(harness): 2023-01-01T10:00:00Z | physical | finished""", ) + def test_generate_backup_list_output(harness): with patch("charm.PostgreSQLBackups._execute_command") as _execute_command: # Test when no backups are returned. _execute_command.return_value = (0, '[{"backup":[]}]', "") - tc().assertEqual( + TestCase().assertEqual( harness.charm.backup._generate_backup_list_output(), """backup-id | backup-type | backup-status ----------------------------------------------------""", @@ -545,7 +562,7 @@ def test_generate_backup_list_output(harness): '[{"backup":[{"label":"20230101-090000F","error":"fake error"},{"label":"20230101-100000F","error":null}]}]', "", ) - tc().assertEqual( + TestCase().assertEqual( harness.charm.backup._generate_backup_list_output(), """backup-id | backup-type | backup-status ---------------------------------------------------- @@ -553,18 +570,19 @@ def test_generate_backup_list_output(harness): 2023-01-01T10:00:00Z | physical | finished""", ) + def test_list_backups(harness): with patch("charm.PostgreSQLBackups._execute_command") as _execute_command: # Test when the command that list the backups fails. _execute_command.return_value = (1, "", "fake stderr") - with tc().assertRaises(ListBackupsError): - tc().assertEqual( + with TestCase().assertRaises(ListBackupsError): + TestCase().assertEqual( harness.charm.backup._list_backups(show_failed=True), OrderedDict[str, str]() ) # Test when no backups are available. _execute_command.return_value = (0, "[]", "") - tc().assertEqual( + TestCase().assertEqual( harness.charm.backup._list_backups(show_failed=True), OrderedDict[str, str]() ) @@ -574,7 +592,7 @@ def test_list_backups(harness): '[{"backup":[{"label":"20230101-090000F","error":"fake error"},{"label":"20230101-100000F","error":null}],"name":"test-stanza"}]', "", ) - tc().assertEqual( + TestCase().assertEqual( harness.charm.backup._list_backups(show_failed=True), OrderedDict[str, str]([ ("2023-01-01T09:00:00Z", "test-stanza"), @@ -583,11 +601,12 @@ def test_list_backups(harness): ) # Test when some backups are available, but it's not desired to list failed backups. - tc().assertEqual( + TestCase().assertEqual( harness.charm.backup._list_backups(show_failed=False), OrderedDict[str, str]([("2023-01-01T10:00:00Z", "test-stanza")]), ) + @patch_network_get(private_address="1.1.1.1") def test_initialise_stanza(harness): with ( @@ -596,7 +615,9 @@ def test_initialise_stanza(harness): patch("backups.wait_fixed", return_value=wait_fixed(0)), patch("charm.PostgresqlOperatorCharm.update_config") as _update_config, patch("charm.PostgreSQLBackups._execute_command") as _execute_command, - patch("charm.PostgresqlOperatorCharm.is_primary", new_callable=PropertyMock) as _is_primary, + patch( + "charm.PostgresqlOperatorCharm.is_primary", new_callable=PropertyMock + ) as _is_primary, ): peer_rel_id = harness.model.get_relation(PEER).id # Test when the unit is not the primary. @@ -628,18 +649,18 @@ def test_initialise_stanza(harness): harness.charm.unit.status = BlockedStatus(blocked_state) harness.charm.backup._initialise_stanza() _execute_command.assert_called_once_with(stanza_creation_command) - tc().assertIsInstance(harness.charm.unit.status, BlockedStatus) - tc().assertEqual( + TestCase().assertIsInstance(harness.charm.unit.status, BlockedStatus) + TestCase().assertEqual( harness.charm.unit.status.message, FAILED_TO_INITIALIZE_STANZA_ERROR_MESSAGE ) # Assert there is no stanza name in the application relation databag. - tc().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.app), {}) + TestCase().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.app), {}) # Test when the failure in the stanza creation is due to a timeout. _execute_command.reset_mock() _execute_command.return_value = (49, "", "fake stderr") - with tc().assertRaises(TimeoutError): + with TestCase().assertRaises(TimeoutError): harness.charm.backup._initialise_stanza() # Test when the archiving is working correctly (pgBackRest check command succeeds) @@ -648,15 +669,15 @@ def test_initialise_stanza(harness): _execute_command.return_value = (0, "fake stdout", "") _member_started.return_value = True harness.charm.backup._initialise_stanza() - tc().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.app), {}) - tc().assertEqual( + TestCase().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.app), {}) + TestCase().assertEqual( harness.get_relation_data(peer_rel_id, harness.charm.unit), { "stanza": f"{harness.charm.model.name}.postgresql", "init-pgbackrest": "True", }, ) - tc().assertIsInstance(harness.charm.unit.status, MaintenanceStatus) + TestCase().assertIsInstance(harness.charm.unit.status, MaintenanceStatus) # Test when the unit is the leader. with harness.hooks_disabled(): @@ -666,13 +687,14 @@ def test_initialise_stanza(harness): ) harness.charm.backup._initialise_stanza() _update_config.assert_not_called() - tc().assertEqual( + TestCase().assertEqual( harness.get_relation_data(peer_rel_id, harness.charm.app), {"stanza": "None.postgresql", "init-pgbackrest": "True"}, ) _member_started.assert_not_called() _reload_patroni_configuration.assert_not_called() - tc().assertIsInstance(harness.charm.unit.status, MaintenanceStatus) + TestCase().assertIsInstance(harness.charm.unit.status, MaintenanceStatus) + @patch_network_get(private_address="1.1.1.1") def test_check_stanza(harness): @@ -682,7 +704,9 @@ def test_check_stanza(harness): patch("backups.wait_fixed", return_value=wait_fixed(0)), patch("charm.PostgresqlOperatorCharm.update_config") as _update_config, patch("charm.PostgreSQLBackups._execute_command") as _execute_command, - patch("charm.PostgresqlOperatorCharm.is_primary", new_callable=PropertyMock) as _is_primary, + patch( + "charm.PostgresqlOperatorCharm.is_primary", new_callable=PropertyMock + ) as _is_primary, ): peer_rel_id = harness.model.get_relation(PEER).id # Set peer data flag @@ -710,14 +734,16 @@ def test_check_stanza(harness): _execute_command.return_value = (49, "", "fake stderr") _member_started.return_value = True harness.charm.backup.check_stanza() - tc().assertEqual(_update_config.call_count, 2) - tc().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.app), {}) - tc().assertEqual(_member_started.call_count, 5) - tc().assertEqual(_reload_patroni_configuration.call_count, 5) - tc().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.app), {}) - tc().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.unit), {}) - tc().assertIsInstance(harness.charm.unit.status, BlockedStatus) - tc().assertEqual(harness.charm.unit.status.message, FAILED_TO_INITIALIZE_STANZA_ERROR_MESSAGE) + TestCase().assertEqual(_update_config.call_count, 2) + TestCase().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.app), {}) + TestCase().assertEqual(_member_started.call_count, 5) + TestCase().assertEqual(_reload_patroni_configuration.call_count, 5) + TestCase().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.app), {}) + TestCase().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.unit), {}) + TestCase().assertIsInstance(harness.charm.unit.status, BlockedStatus) + TestCase().assertEqual( + harness.charm.unit.status.message, FAILED_TO_INITIALIZE_STANZA_ERROR_MESSAGE + ) # Test when the archiving is working correctly (pgBackRest check command succeeds) # and the unit is not the leader. @@ -742,15 +768,15 @@ def test_check_stanza(harness): _update_config.assert_called_once() _member_started.assert_called_once() _reload_patroni_configuration.assert_called_once() - tc().assertEqual( + TestCase().assertEqual( harness.get_relation_data(peer_rel_id, harness.charm.app), {"stanza": "test-stanza", "init-pgbackrest": "True"}, ) - tc().assertEqual( + TestCase().assertEqual( harness.get_relation_data(peer_rel_id, harness.charm.unit), {"stanza": "test-stanza"}, ) - tc().assertIsInstance(harness.charm.unit.status, ActiveStatus) + TestCase().assertIsInstance(harness.charm.unit.status, ActiveStatus) # Test when the unit is the leader. harness.charm.unit.status = BlockedStatus("fake blocked state") @@ -773,15 +799,16 @@ def test_check_stanza(harness): _update_config.assert_called_once() _member_started.assert_called_once() _reload_patroni_configuration.assert_called_once() - tc().assertEqual( + TestCase().assertEqual( harness.get_relation_data(peer_rel_id, harness.charm.app), {"stanza": "test-stanza"}, ) - tc().assertEqual( + TestCase().assertEqual( harness.get_relation_data(peer_rel_id, harness.charm.unit), {"stanza": "test-stanza"}, ) - tc().assertIsInstance(harness.charm.unit.status, ActiveStatus) + TestCase().assertIsInstance(harness.charm.unit.status, ActiveStatus) + def test_coordinate_stanza_fields(harness): peer_rel_id = harness.model.get_relation(PEER).id @@ -792,9 +819,9 @@ def test_coordinate_stanza_fields(harness): # Test when the stanza name is neither in the application relation databag nor in the unit relation databag. harness.charm.backup.coordinate_stanza_fields() - tc().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.app), {}) - tc().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.unit), {}) - tc().assertEqual(harness.get_relation_data(peer_rel_id, new_unit), {}) + TestCase().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.app), {}) + TestCase().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.unit), {}) + TestCase().assertEqual(harness.get_relation_data(peer_rel_id, new_unit), {}) # Test when the stanza name is in the unit relation databag but the unit is not the leader. stanza_name = f"{harness.charm.model.name}.{harness.charm.app.name}" @@ -803,9 +830,9 @@ def test_coordinate_stanza_fields(harness): peer_rel_id, new_unit_name, {"stanza": stanza_name, "init-pgbackrest": "True"} ) harness.charm.backup.coordinate_stanza_fields() - tc().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.app), {}) - tc().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.unit), {}) - tc().assertEqual( + TestCase().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.app), {}) + TestCase().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.unit), {}) + TestCase().assertEqual( harness.get_relation_data(peer_rel_id, new_unit), {"stanza": stanza_name, "init-pgbackrest": "True"}, ) @@ -814,95 +841,99 @@ def test_coordinate_stanza_fields(harness): with harness.hooks_disabled(): harness.set_leader() harness.charm.backup.coordinate_stanza_fields() - tc().assertEqual( + TestCase().assertEqual( harness.get_relation_data(peer_rel_id, harness.charm.app), {"stanza": stanza_name, "init-pgbackrest": "True"}, ) - tc().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.unit), {}) - tc().assertEqual( + TestCase().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.unit), {}) + TestCase().assertEqual( harness.get_relation_data(peer_rel_id, new_unit), {"stanza": stanza_name, "init-pgbackrest": "True"}, ) # Test when the stanza was already checked in the primary non-leader unit. with harness.hooks_disabled(): - harness.update_relation_data( - peer_rel_id, new_unit_name, {"init-pgbackrest": ""} - ) + harness.update_relation_data(peer_rel_id, new_unit_name, {"init-pgbackrest": ""}) harness.charm.backup.coordinate_stanza_fields() - tc().assertEqual( + TestCase().assertEqual( harness.get_relation_data(peer_rel_id, harness.charm.app), {"stanza": stanza_name}, ) - tc().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.unit), {}) - tc().assertEqual( + TestCase().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.unit), {}) + TestCase().assertEqual( harness.get_relation_data(peer_rel_id, new_unit), {"stanza": stanza_name} ) # Test when the "init-pgbackrest" flag was removed from the application relation databag # and this is the unit that has the stanza name in the unit relation databag. with harness.hooks_disabled(): - harness.update_relation_data( - peer_rel_id, harness.charm.unit.name, {"stanza": stanza_name} - ) + harness.update_relation_data(peer_rel_id, harness.charm.unit.name, {"stanza": stanza_name}) harness.charm.backup.coordinate_stanza_fields() - tc().assertEqual( + TestCase().assertEqual( harness.get_relation_data(peer_rel_id, harness.charm.app), {"stanza": stanza_name}, ) - tc().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.unit), {}) - tc().assertEqual( + TestCase().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.unit), {}) + TestCase().assertEqual( harness.get_relation_data(peer_rel_id, new_unit), {"stanza": stanza_name} ) # Test when the unit is not the leader. with harness.hooks_disabled(): harness.set_leader(False) - harness.update_relation_data( - peer_rel_id, harness.charm.unit.name, {"stanza": stanza_name} - ) + harness.update_relation_data(peer_rel_id, harness.charm.unit.name, {"stanza": stanza_name}) harness.charm.backup.coordinate_stanza_fields() - tc().assertEqual( + TestCase().assertEqual( harness.get_relation_data(peer_rel_id, harness.charm.app), {"stanza": stanza_name}, ) - tc().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.unit), {}) - tc().assertEqual( + TestCase().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.unit), {}) + TestCase().assertEqual( harness.get_relation_data(peer_rel_id, new_unit), {"stanza": stanza_name} ) + def test_is_primary_pgbackrest_service_running(harness): with ( patch("charm.PostgreSQLBackups._execute_command") as _execute_command, - patch("charm.PostgresqlOperatorCharm.primary_endpoint", new_callable=PropertyMock) as _primary_endpoint, + patch( + "charm.PostgresqlOperatorCharm.primary_endpoint", new_callable=PropertyMock + ) as _primary_endpoint, patch("charm.Patroni.get_primary") as _get_primary, ): # Test when the pgBackRest fails to contact the primary server. _get_primary.side_effect = None _execute_command.return_value = (1, "", "fake stderr") - tc().assertFalse(harness.charm.backup._is_primary_pgbackrest_service_running) + TestCase().assertFalse(harness.charm.backup._is_primary_pgbackrest_service_running) _execute_command.assert_called_once() # Test when the endpoint is not generated. _execute_command.reset_mock() _primary_endpoint.return_value = None - tc().assertFalse(harness.charm.backup._is_primary_pgbackrest_service_running) + TestCase().assertFalse(harness.charm.backup._is_primary_pgbackrest_service_running) _execute_command.assert_not_called() # Test when the pgBackRest succeeds on contacting the primary server. _execute_command.reset_mock() _execute_command.return_value = (0, "fake stdout", "") _primary_endpoint.return_value = "fake_endpoint" - tc().assertTrue(harness.charm.backup._is_primary_pgbackrest_service_running) + TestCase().assertTrue(harness.charm.backup._is_primary_pgbackrest_service_running) _execute_command.assert_called_once() + def test_on_s3_credential_changed(harness): with ( patch("charm.PostgreSQLBackups._initialise_stanza") as _initialise_stanza, patch("charm.PostgreSQLBackups.can_use_s3_repository") as _can_use_s3_repository, - patch("charm.PostgreSQLBackups._create_bucket_if_not_exists") as _create_bucket_if_not_exists, - patch("charm.PostgresqlOperatorCharm.is_primary", new_callable=PropertyMock) as _is_primary, - patch("charm.PostgreSQLBackups._render_pgbackrest_conf_file") as _render_pgbackrest_conf_file, + patch( + "charm.PostgreSQLBackups._create_bucket_if_not_exists" + ) as _create_bucket_if_not_exists, + patch( + "charm.PostgresqlOperatorCharm.is_primary", new_callable=PropertyMock + ) as _is_primary, + patch( + "charm.PostgreSQLBackups._render_pgbackrest_conf_file" + ) as _render_pgbackrest_conf_file, patch("ops.framework.EventBase.defer") as _defer, ): peer_rel_id = harness.model.get_relation(PEER).id @@ -953,7 +984,7 @@ def test_on_s3_credential_changed(harness): ) _render_pgbackrest_conf_file.assert_called_once() _create_bucket_if_not_exists.assert_not_called() - tc().assertIsInstance(harness.charm.unit.status, ActiveStatus) + TestCase().assertIsInstance(harness.charm.unit.status, ActiveStatus) _can_use_s3_repository.assert_not_called() _initialise_stanza.assert_not_called() @@ -975,8 +1006,8 @@ def test_on_s3_credential_changed(harness): ) _render_pgbackrest_conf_file.assert_called_once() _create_bucket_if_not_exists.assert_called_once() - tc().assertIsInstance(harness.charm.unit.status, BlockedStatus) - tc().assertEqual( + TestCase().assertIsInstance(harness.charm.unit.status, BlockedStatus) + TestCase().assertEqual( harness.charm.unit.status.message, FAILED_TO_ACCESS_CREATE_BUCKET_ERROR_MESSAGE ) _can_use_s3_repository.assert_not_called() @@ -989,8 +1020,8 @@ def test_on_s3_credential_changed(harness): harness.charm.backup.s3_client.on.credentials_changed.emit( relation=harness.model.get_relation(S3_PARAMETERS_RELATION, s3_rel_id) ) - tc().assertIsInstance(harness.charm.unit.status, BlockedStatus) - tc().assertEqual(harness.charm.unit.status.message, "fake validation message") + TestCase().assertIsInstance(harness.charm.unit.status, BlockedStatus) + TestCase().assertEqual(harness.charm.unit.status.message, "fake validation message") _create_bucket_if_not_exists.assert_called_once() _can_use_s3_repository.assert_called_once() _initialise_stanza.assert_not_called() @@ -1004,17 +1035,18 @@ def test_on_s3_credential_changed(harness): _can_use_s3_repository.assert_called_once() _initialise_stanza.assert_called_once() + def test_on_s3_credential_gone(harness): peer_rel_id = harness.model.get_relation(PEER).id # Test that unrelated blocks will remain harness.charm.unit.status = BlockedStatus("test block") harness.charm.backup._on_s3_credential_gone(None) - tc().assertIsInstance(harness.charm.unit.status, BlockedStatus) + TestCase().assertIsInstance(harness.charm.unit.status, BlockedStatus) # Test that s3 related blocks will be cleared harness.charm.unit.status = BlockedStatus(ANOTHER_CLUSTER_REPOSITORY_ERROR_MESSAGE) harness.charm.backup._on_s3_credential_gone(None) - tc().assertIsInstance(harness.charm.unit.status, ActiveStatus) + TestCase().assertIsInstance(harness.charm.unit.status, ActiveStatus) # Test removal of relation data when the unit is not the leader. with harness.hooks_disabled(): @@ -1029,11 +1061,11 @@ def test_on_s3_credential_gone(harness): {"stanza": "test-stanza", "init-pgbackrest": "True"}, ) harness.charm.backup._on_s3_credential_gone(None) - tc().assertEqual( + TestCase().assertEqual( harness.get_relation_data(peer_rel_id, harness.charm.app), {"stanza": "test-stanza", "init-pgbackrest": "True"}, ) - tc().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.unit), {}) + TestCase().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.unit), {}) # Test removal of relation data when the unit is the leader. with harness.hooks_disabled(): @@ -1044,16 +1076,21 @@ def test_on_s3_credential_gone(harness): {"stanza": "test-stanza", "init-pgbackrest": "True"}, ) harness.charm.backup._on_s3_credential_gone(None) - tc().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.app), {}) - tc().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.unit), {}) + TestCase().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.app), {}) + TestCase().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.unit), {}) + def test_on_create_backup_action(harness): with ( patch("charm.PostgresqlOperatorCharm.update_config") as _update_config, - patch("charm.PostgreSQLBackups._change_connectivity_to_database") as _change_connectivity_to_database, + patch( + "charm.PostgreSQLBackups._change_connectivity_to_database" + ) as _change_connectivity_to_database, patch("charm.PostgreSQLBackups._list_backups") as _list_backups, patch("charm.PostgreSQLBackups._execute_command") as _execute_command, - patch("charm.PostgresqlOperatorCharm.is_primary", new_callable=PropertyMock) as _is_primary, + patch( + "charm.PostgresqlOperatorCharm.is_primary", new_callable=PropertyMock + ) as _is_primary, patch("charm.PostgreSQLBackups._upload_content_to_s3") as _upload_content_to_s3, patch("backups.datetime") as _datetime, patch("ops.JujuVersion.from_environ") as _from_environ, @@ -1180,13 +1217,16 @@ def test_on_create_backup_action(harness): mock_s3_parameters, ), ]) - tc().assertEqual(_change_connectivity_to_database.call_count, 2) + TestCase().assertEqual(_change_connectivity_to_database.call_count, 2) mock_event.fail.assert_not_called() mock_event.set_results.assert_called_once_with({"backup-status": "backup created"}) + def test_on_list_backups_action(harness): with ( - patch("charm.PostgreSQLBackups._generate_backup_list_output") as _generate_backup_list_output, + patch( + "charm.PostgreSQLBackups._generate_backup_list_output" + ) as _generate_backup_list_output, patch("charm.PostgreSQLBackups._are_backup_settings_ok") as _are_backup_settings_ok, ): # Test when not all backup settings are ok. @@ -1225,6 +1265,7 @@ def test_on_list_backups_action(harness): }) mock_event.fail.assert_not_called() + @patch_network_get(private_address="1.1.1.1") def test_on_restore_action(harness): with ( @@ -1252,7 +1293,7 @@ def test_on_restore_action(harness): _start_patroni.assert_not_called() mock_event.fail.assert_not_called() mock_event.set_results.assert_not_called() - tc().assertNotIsInstance(harness.charm.unit.status, MaintenanceStatus) + TestCase().assertNotIsInstance(harness.charm.unit.status, MaintenanceStatus) # Test when the user provides an invalid backup id. mock_event.params = {"backup-id": "2023-01-01T10:00:00Z"} @@ -1269,7 +1310,7 @@ def test_on_restore_action(harness): _update_config.assert_not_called() _start_patroni.assert_not_called() mock_event.set_results.assert_not_called() - tc().assertNotIsInstance(harness.charm.unit.status, MaintenanceStatus) + TestCase().assertNotIsInstance(harness.charm.unit.status, MaintenanceStatus) # Test when the charm fails to stop the workload. mock_event.reset_mock() @@ -1303,9 +1344,9 @@ def test_on_restore_action(harness): _restart_database.reset_mock() _empty_data_files.return_value = True _execute_command.return_value = (1, "", "fake stderr") - tc().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.app), {}) + TestCase().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.app), {}) harness.charm.backup._on_restore_action(mock_event) - tc().assertEqual( + TestCase().assertEqual( harness.get_relation_data(peer_rel_id, harness.charm.app), { "restoring-backup": "20230101-090000F", @@ -1338,6 +1379,7 @@ def test_on_restore_action(harness): mock_event.fail.assert_not_called() mock_event.set_results.assert_called_once_with({"restore-status": "restore started"}) + def test_pre_restore_checks(harness): with ( patch("ops.model.Application.planned_units") as _planned_units, @@ -1346,13 +1388,13 @@ def test_pre_restore_checks(harness): # Test when S3 parameters are not ok. mock_event = MagicMock(params={}) _are_backup_settings_ok.return_value = (False, "fake error message") - tc().assertEqual(harness.charm.backup._pre_restore_checks(mock_event), False) + TestCase().assertEqual(harness.charm.backup._pre_restore_checks(mock_event), False) mock_event.fail.assert_called_once() # Test when no backup id is provided. mock_event.reset_mock() _are_backup_settings_ok.return_value = (True, None) - tc().assertEqual(harness.charm.backup._pre_restore_checks(mock_event), False) + TestCase().assertEqual(harness.charm.backup._pre_restore_checks(mock_event), False) mock_event.fail.assert_called_once() # Test when the unit is in a blocked state that is not recoverable by changing @@ -1360,7 +1402,7 @@ def test_pre_restore_checks(harness): mock_event.reset_mock() mock_event.params = {"backup-id": "2023-01-01T09:00:00Z"} harness.charm.unit.status = BlockedStatus("fake blocked state") - tc().assertEqual(harness.charm.backup._pre_restore_checks(mock_event), False) + TestCase().assertEqual(harness.charm.backup._pre_restore_checks(mock_event), False) mock_event.fail.assert_called_once() # Test when the unit is in a blocked state that is recoverable by changing S3 parameters, @@ -1368,22 +1410,23 @@ def test_pre_restore_checks(harness): mock_event.reset_mock() harness.charm.unit.status = BlockedStatus(ANOTHER_CLUSTER_REPOSITORY_ERROR_MESSAGE) _planned_units.return_value = 2 - tc().assertEqual(harness.charm.backup._pre_restore_checks(mock_event), False) + TestCase().assertEqual(harness.charm.backup._pre_restore_checks(mock_event), False) mock_event.fail.assert_called_once() # Test when the cluster has only one unit, but it's not the leader yet. mock_event.reset_mock() _planned_units.return_value = 1 - tc().assertEqual(harness.charm.backup._pre_restore_checks(mock_event), False) + TestCase().assertEqual(harness.charm.backup._pre_restore_checks(mock_event), False) mock_event.fail.assert_called_once() # Test when everything is ok to run a restore. mock_event.reset_mock() with harness.hooks_disabled(): harness.set_leader() - tc().assertEqual(harness.charm.backup._pre_restore_checks(mock_event), True) + TestCase().assertEqual(harness.charm.backup._pre_restore_checks(mock_event), True) mock_event.fail.assert_not_called() + @patch_network_get(private_address="1.1.1.1") def test_render_pgbackrest_conf_file(harness): with ( @@ -1423,7 +1466,8 @@ def test_render_pgbackrest_conf_file(harness): with open("templates/pgbackrest.conf.j2") as file: template = Template(file.read()) expected_content = template.render( - enable_tls=harness.charm.is_tls_enabled and len(harness.charm.peer_members_endpoints) > 0, + enable_tls=harness.charm.is_tls_enabled + and len(harness.charm.peer_members_endpoints) > 0, peer_endpoints=harness.charm._peer_members_ips, path="test-path/", data_path="/var/snap/charmed-postgresql/common/var/lib/postgresql", @@ -1445,7 +1489,7 @@ def test_render_pgbackrest_conf_file(harness): harness.charm.backup._render_pgbackrest_conf_file() # Check the template is opened read-only in the call to open. - tc().assertEqual(mock.call_args_list[0][0], ("templates/pgbackrest.conf.j2", "r")) + TestCase().assertEqual(mock.call_args_list[0][0], ("templates/pgbackrest.conf.j2", "r")) # Ensure the correct rendered template is sent to _render_file method. _render_file.assert_called_once_with( @@ -1454,6 +1498,7 @@ def test_render_pgbackrest_conf_file(harness): 0o644, ) + @patch_network_get(private_address="1.1.1.1") def test_restart_database(harness): with ( @@ -1470,16 +1515,19 @@ def test_restart_database(harness): harness.charm.backup._restart_database() # Assert that the backup id is not in the application relation databag anymore. - tc().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.app), {}) + TestCase().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.app), {}) _update_config.assert_called_once() _start_patroni.assert_called_once() + def test_retrieve_s3_parameters(harness): - with patch("charms.data_platform_libs.v0.s3.S3Requirer.get_s3_connection_info") as _get_s3_connection_info: + with patch( + "charms.data_platform_libs.v0.s3.S3Requirer.get_s3_connection_info" + ) as _get_s3_connection_info: # Test when there are missing S3 parameters. _get_s3_connection_info.return_value = {} - tc().assertEqual( + TestCase().assertEqual( harness.charm.backup._retrieve_s3_parameters(), ({}, ["bucket", "access-key", "secret-key"]), ) @@ -1490,7 +1538,7 @@ def test_retrieve_s3_parameters(harness): "access-key": "test-access-key", "secret-key": "test-secret-key", } - tc().assertEqual( + TestCase().assertEqual( harness.charm.backup._retrieve_s3_parameters(), ( { @@ -1516,7 +1564,7 @@ def test_retrieve_s3_parameters(harness): "region": " us-east-1 ", "s3-uri-style": " path ", } - tc().assertEqual( + TestCase().assertEqual( harness.charm.backup._retrieve_s3_parameters(), ( { @@ -1532,16 +1580,26 @@ def test_retrieve_s3_parameters(harness): ), ) + def test_start_stop_pgbackrest_service(harness): with ( patch( - "charm.PostgreSQLBackups._is_primary_pgbackrest_service_running", new_callable=PropertyMock + "charm.PostgreSQLBackups._is_primary_pgbackrest_service_running", + new_callable=PropertyMock, ) as _is_primary_pgbackrest_service_running, - patch("charm.PostgresqlOperatorCharm.is_primary", new_callable=PropertyMock) as _is_primary, + patch( + "charm.PostgresqlOperatorCharm.is_primary", new_callable=PropertyMock + ) as _is_primary, patch("backups.snap.SnapCache") as _snap_cache, - patch("charm.PostgresqlOperatorCharm._peer_members_ips", new_callable=PropertyMock) as _peer_members_ips, - patch("charm.PostgresqlOperatorCharm.is_tls_enabled", new_callable=PropertyMock) as _is_tls_enabled, - patch("charm.PostgreSQLBackups._render_pgbackrest_conf_file") as _render_pgbackrest_conf_file, + patch( + "charm.PostgresqlOperatorCharm._peer_members_ips", new_callable=PropertyMock + ) as _peer_members_ips, + patch( + "charm.PostgresqlOperatorCharm.is_tls_enabled", new_callable=PropertyMock + ) as _is_tls_enabled, + patch( + "charm.PostgreSQLBackups._render_pgbackrest_conf_file" + ) as _render_pgbackrest_conf_file, patch("charm.PostgreSQLBackups._are_backup_settings_ok") as _are_backup_settings_ok, ): # Test when S3 parameters are not ok (no operation, but returns success). @@ -1549,7 +1607,7 @@ def test_start_stop_pgbackrest_service(harness): restart = MagicMock() stop = MagicMock() _snap_cache.return_value = {"charmed-postgresql": MagicMock(restart=restart, stop=stop)} - tc().assertEqual( + TestCase().assertEqual( harness.charm.backup.start_stop_pgbackrest_service(), True, ) @@ -1559,7 +1617,7 @@ def test_start_stop_pgbackrest_service(harness): # Test when it was not possible to render the pgBackRest configuration file. _are_backup_settings_ok.return_value = (True, None) _render_pgbackrest_conf_file.return_value = False - tc().assertEqual( + TestCase().assertEqual( harness.charm.backup.start_stop_pgbackrest_service(), False, ) @@ -1569,7 +1627,7 @@ def test_start_stop_pgbackrest_service(harness): # Test when TLS is not enabled (should stop the service). _render_pgbackrest_conf_file.return_value = True _is_tls_enabled.return_value = False - tc().assertEqual( + TestCase().assertEqual( harness.charm.backup.start_stop_pgbackrest_service(), True, ) @@ -1580,7 +1638,7 @@ def test_start_stop_pgbackrest_service(harness): stop.reset_mock() _is_tls_enabled.return_value = True _peer_members_ips.return_value = [] - tc().assertEqual( + TestCase().assertEqual( harness.charm.backup.start_stop_pgbackrest_service(), True, ) @@ -1592,7 +1650,7 @@ def test_start_stop_pgbackrest_service(harness): _peer_members_ips.return_value = ["1.1.1.1"] _is_primary.return_value = False _is_primary_pgbackrest_service_running.return_value = False - tc().assertEqual( + TestCase().assertEqual( harness.charm.backup.start_stop_pgbackrest_service(), False, ) @@ -1601,7 +1659,7 @@ def test_start_stop_pgbackrest_service(harness): # Test when the service has already started in the primary. _is_primary_pgbackrest_service_running.return_value = True - tc().assertEqual( + TestCase().assertEqual( harness.charm.backup.start_stop_pgbackrest_service(), True, ) @@ -1612,13 +1670,14 @@ def test_start_stop_pgbackrest_service(harness): restart.reset_mock() _is_primary.return_value = True _is_primary_pgbackrest_service_running.return_value = False - tc().assertEqual( + TestCase().assertEqual( harness.charm.backup.start_stop_pgbackrest_service(), True, ) stop.assert_not_called() restart.assert_called_once() + def test_upload_content_to_s3(harness): with ( patch("tempfile.NamedTemporaryFile") as _named_temporary_file, @@ -1642,7 +1701,7 @@ def test_upload_content_to_s3(harness): _resource.side_effect = ValueError _construct_endpoint.return_value = "https://s3.us-east-1.amazonaws.com" _named_temporary_file.return_value.__enter__.return_value.name = "/tmp/test-file" - tc().assertEqual( + TestCase().assertEqual( harness.charm.backup._upload_content_to_s3(content, s3_path, s3_parameters), False, ) @@ -1653,7 +1712,7 @@ def test_upload_content_to_s3(harness): _resource.reset_mock() _resource.side_effect = None upload_file.side_effect = S3UploadFailedError - tc().assertEqual( + TestCase().assertEqual( harness.charm.backup._upload_content_to_s3(content, s3_path, s3_parameters), False, ) @@ -1666,7 +1725,7 @@ def test_upload_content_to_s3(harness): _named_temporary_file.reset_mock() upload_file.reset_mock() upload_file.side_effect = None - tc().assertEqual( + TestCase().assertEqual( harness.charm.backup._upload_content_to_s3(content, s3_path, s3_parameters), True, ) diff --git a/tests/unit/test_cluster.py b/tests/unit/test_cluster.py index 1cefccd45a..dca853f6b5 100644 --- a/tests/unit/test_cluster.py +++ b/tests/unit/test_cluster.py @@ -1,10 +1,10 @@ # Copyright 2021 Canonical Ltd. # See LICENSE file for licensing details. -import pytest +from unittest import TestCase from unittest.mock import MagicMock, Mock, PropertyMock, mock_open, patch, sentinel -from unittest import TestCase as tc +import pytest import requests as requests import tenacity as tenacity from charms.operator_libs_linux.v2 import snap @@ -50,6 +50,7 @@ def peers_ips(): peers_ips = {"2.2.2.2", "3.3.3.3"} yield peers_ips + @pytest.fixture(autouse=True) def patroni(peers_ips): patroni = Patroni( @@ -65,6 +66,7 @@ def patroni(peers_ips): ) yield patroni + def test_get_alternative_patroni_url(peers_ips, patroni): # Mock tenacity attempt. retry = tenacity.Retrying() @@ -73,15 +75,14 @@ def test_get_alternative_patroni_url(peers_ips, patroni): # Test the first URL that is returned (it should have the current unit IP). url = patroni._get_alternative_patroni_url(attempt) - tc().assertEqual(url, f"http://{patroni.unit_ip}:8008") + TestCase().assertEqual(url, f"http://{patroni.unit_ip}:8008") # Test returning the other servers URLs. - for attempt_number in range( - attempt.retry_state.attempt_number + 1, len(peers_ips) + 2 - ): + for attempt_number in range(attempt.retry_state.attempt_number + 1, len(peers_ips) + 2): attempt.retry_state.attempt_number = attempt_number url = patroni._get_alternative_patroni_url(attempt) - tc().assertIn(url.split("http://")[1].split(":8008")[0], peers_ips) + TestCase().assertIn(url.split("http://")[1].split(":8008")[0], peers_ips) + def test_get_member_ip(peers_ips, patroni): with ( @@ -90,7 +91,7 @@ def test_get_member_ip(peers_ips, patroni): ): # Test error on trying to get the member IP. _get_alternative_patroni_url.side_effect = "http://server2" - with tc().assertRaises(tenacity.RetryError): + with TestCase().assertRaises(tenacity.RetryError): patroni.get_member_ip(patroni.member_name) # Test using an alternative Patroni URL. @@ -100,17 +101,18 @@ def test_get_member_ip(peers_ips, patroni): "http://server1", ] ip = patroni.get_member_ip(patroni.member_name) - tc().assertEqual(ip, "1.1.1.1") + TestCase().assertEqual(ip, "1.1.1.1") # Test using the current Patroni URL. _get_alternative_patroni_url.side_effect = ["http://server1"] ip = patroni.get_member_ip(patroni.member_name) - tc().assertEqual(ip, "1.1.1.1") + TestCase().assertEqual(ip, "1.1.1.1") # Test when not having that specific member in the cluster. _get_alternative_patroni_url.side_effect = ["http://server1"] ip = patroni.get_member_ip("other-member-name") - tc().assertIsNone(ip) + TestCase().assertIsNone(ip) + def test_get_postgresql_version(peers_ips, patroni): with patch("charm.snap.SnapClient") as _snap_client: @@ -122,10 +124,11 @@ def test_get_postgresql_version(peers_ips, patroni): ] version = patroni.get_postgresql_version() - tc().assertEqual(version, "14.0") + TestCase().assertEqual(version, "14.0") _snap_client.assert_called_once_with() _get_installed_snaps.assert_called_once_with() + def test_get_primary(peers_ips, patroni): with ( patch("requests.get", side_effect=mocked_requests_get) as _get, @@ -133,7 +136,7 @@ def test_get_primary(peers_ips, patroni): ): # Test error on trying to get the member IP. _get_alternative_patroni_url.side_effect = "http://server2" - with tc().assertRaises(tenacity.RetryError): + with TestCase().assertRaises(tenacity.RetryError): patroni.get_primary(patroni.member_name) # Test using an alternative Patroni URL. @@ -143,17 +146,18 @@ def test_get_primary(peers_ips, patroni): "http://server1", ] primary = patroni.get_primary() - tc().assertEqual(primary, "postgresql-0") + TestCase().assertEqual(primary, "postgresql-0") # Test using the current Patroni URL. _get_alternative_patroni_url.side_effect = ["http://server1"] primary = patroni.get_primary() - tc().assertEqual(primary, "postgresql-0") + TestCase().assertEqual(primary, "postgresql-0") # Test requesting the primary in the unit name pattern. _get_alternative_patroni_url.side_effect = ["http://server1"] primary = patroni.get_primary(unit_name_pattern=True) - tc().assertEqual(primary, "postgresql/0") + TestCase().assertEqual(primary, "postgresql/0") + def test_is_creating_backup(peers_ips, patroni): with patch("requests.get") as _get: @@ -165,13 +169,14 @@ def test_is_creating_backup(peers_ips, patroni): {"name": "postgresql-1", "tags": {"is_creating_backup": True}}, ] } - tc().assertTrue(patroni.is_creating_backup) + TestCase().assertTrue(patroni.is_creating_backup) # Test when no member is creating a backup. response.json.return_value = { "members": [{"name": "postgresql-0"}, {"name": "postgresql-1"}] } - tc().assertFalse(patroni.is_creating_backup) + TestCase().assertFalse(patroni.is_creating_backup) + def test_is_replication_healthy(peers_ips, patroni): with ( @@ -181,7 +186,7 @@ def test_is_replication_healthy(peers_ips, patroni): ): # Test when replication is healthy. _get.return_value.status_code = 200 - tc().assertTrue(patroni.is_replication_healthy) + TestCase().assertTrue(patroni.is_replication_healthy) # Test when replication is not healthy. _get.side_effect = [ @@ -189,7 +194,8 @@ def test_is_replication_healthy(peers_ips, patroni): MagicMock(status_code=200), MagicMock(status_code=503), ] - tc().assertFalse(patroni.is_replication_healthy) + TestCase().assertFalse(patroni.is_replication_healthy) + def test_is_member_isolated(peers_ips, patroni): with ( @@ -200,15 +206,16 @@ def test_is_member_isolated(peers_ips, patroni): ): # Test when it wasn't possible to connect to the Patroni API. _patroni_url.return_value = "http://server3" - tc().assertFalse(patroni.is_member_isolated) + TestCase().assertFalse(patroni.is_member_isolated) # Test when the member isn't isolated from the cluster. _patroni_url.return_value = "http://server1" - tc().assertFalse(patroni.is_member_isolated) + TestCase().assertFalse(patroni.is_member_isolated) # Test when the member is isolated from the cluster. _patroni_url.return_value = "http://server4" - tc().assertTrue(patroni.is_member_isolated) + TestCase().assertTrue(patroni.is_member_isolated) + def test_render_file(peers_ips, patroni): with ( @@ -231,7 +238,7 @@ def test_render_file(peers_ips, patroni): patroni.render_file(filename, "rendered-content", 0o640) # Check the rendered file is opened with "w+" mode. - tc().assertEqual(mock.call_args_list[0][0], (filename, "w+")) + TestCase().assertEqual(mock.call_args_list[0][0], (filename, "w+")) # Ensure that the correct user is lookup up. _pwnam.assert_called_with("snap_daemon") # Ensure the file is chmod'd correctly. @@ -239,6 +246,7 @@ def test_render_file(peers_ips, patroni): # Ensure the file is chown'd correctly. _chown.assert_called_with(filename, uid=35, gid=35) + def test_render_patroni_yml_file(peers_ips, patroni): with ( patch("charm.Patroni.get_postgresql_version") as _get_postgresql_version, @@ -286,7 +294,7 @@ def test_render_patroni_yml_file(peers_ips, patroni): patroni.render_patroni_yml_file() # Check the template is opened read-only in the call to open. - tc().assertEqual(mock.call_args_list[0][0], ("templates/patroni.yml.j2", "r")) + TestCase().assertEqual(mock.call_args_list[0][0], ("templates/patroni.yml.j2", "r")) # Ensure the correct rendered template is sent to _render_file method. _render_file.assert_called_once_with( "/var/snap/charmed-postgresql/current/etc/patroni/patroni.yaml", @@ -294,6 +302,7 @@ def test_render_patroni_yml_file(peers_ips, patroni): 0o600, ) + def test_start_patroni(peers_ips, patroni): with ( patch("charm.snap.SnapCache") as _snap_cache, @@ -311,6 +320,7 @@ def test_start_patroni(peers_ips, patroni): # Test a fail scenario. assert not patroni.start_patroni() + def test_stop_patroni(peers_ips, patroni): with ( patch("charm.snap.SnapCache") as _snap_cache, @@ -332,6 +342,7 @@ def test_stop_patroni(peers_ips, patroni): # Test a fail scenario. assert not patroni.stop_patroni() + def test_member_replication_lag(peers_ips, patroni): with ( patch("requests.get", side_effect=mocked_requests_get) as _get, @@ -357,9 +368,8 @@ def test_member_replication_lag(peers_ips, patroni): def test_reinitialize_postgresql(peers_ips, patroni): with patch("requests.post") as _post: patroni.reinitialize_postgresql() - _post.assert_called_once_with( - f"http://{patroni.unit_ip}:8008/reinitialize", verify=True - ) + _post.assert_called_once_with(f"http://{patroni.unit_ip}:8008/reinitialize", verify=True) + def test_switchover(peers_ips, patroni): with ( @@ -375,6 +385,7 @@ def test_switchover(peers_ips, patroni): "http://1.1.1.1:8008/switchover", json={"leader": "primary"}, verify=True ) + def test_update_synchronous_node_count(peers_ips, patroni): with patch("requests.patch") as _patch: response = _patch.return_value @@ -412,6 +423,7 @@ def test_configure_patroni_on_unit(peers_ips, patroni): "/var/snap/charmed-postgresql/common/var/lib/postgresql", 488 ) + def test_member_started_true(peers_ips, patroni): with ( patch("cluster.requests.get") as _get, @@ -424,6 +436,7 @@ def test_member_started_true(peers_ips, patroni): _get.assert_called_once_with("http://1.1.1.1:8008/health", verify=True, timeout=5) + def test_member_started_false(peers_ips, patroni): with ( patch("cluster.requests.get") as _get, @@ -436,6 +449,7 @@ def test_member_started_false(peers_ips, patroni): _get.assert_called_once_with("http://1.1.1.1:8008/health", verify=True, timeout=5) + def test_member_started_error(peers_ips, patroni): with ( patch("cluster.requests.get") as _get, @@ -448,6 +462,7 @@ def test_member_started_error(peers_ips, patroni): _get.assert_called_once_with("http://1.1.1.1:8008/health", verify=True, timeout=5) + def test_member_inactive_true(peers_ips, patroni): with ( patch("cluster.requests.get") as _get, @@ -460,6 +475,7 @@ def test_member_inactive_true(peers_ips, patroni): _get.assert_called_once_with("http://1.1.1.1:8008/health", verify=True, timeout=5) + def test_member_inactive_false(peers_ips, patroni): with ( patch("cluster.requests.get") as _get, @@ -472,6 +488,7 @@ def test_member_inactive_false(peers_ips, patroni): _get.assert_called_once_with("http://1.1.1.1:8008/health", verify=True, timeout=5) + def test_member_inactive_error(peers_ips, patroni): with ( patch("cluster.requests.get") as _get, diff --git a/tests/unit/test_cluster_topology_observer.py b/tests/unit/test_cluster_topology_observer.py index 304b7c9f6a..507db774b9 100644 --- a/tests/unit/test_cluster_topology_observer.py +++ b/tests/unit/test_cluster_topology_observer.py @@ -1,10 +1,10 @@ # Copyright 2023 Canonical Ltd. # See LICENSE file for licensing details. import signal -import pytest from typing import Optional from unittest.mock import Mock, PropertyMock, patch +import pytest from ops.charm import CharmBase from ops.model import ActiveStatus, Relation, WaitingStatus from ops.testing import Harness @@ -63,6 +63,7 @@ def harness(): yield harness harness.cleanup() + def test_start_observer(harness): with ( patch("builtins.open") as _open, @@ -92,6 +93,7 @@ def test_start_observer(harness): harness.charm.observer.start_observer() _popen.assert_called_once() + def test_stop_observer(harness): with ( patch("os.kill") as _kill, @@ -110,6 +112,7 @@ def test_stop_observer(harness): harness.charm.observer.stop_observer() _kill.assert_called_once_with(1, signal.SIGINT) + def test_dispatch(harness): with patch("subprocess.run") as _run: command = "test-command" diff --git a/tests/unit/test_db.py b/tests/unit/test_db.py index b147731255..364bd12b30 100644 --- a/tests/unit/test_db.py +++ b/tests/unit/test_db.py @@ -1,10 +1,10 @@ # Copyright 2022 Canonical Ltd. # See LICENSE file for licensing details. -import pytest +from unittest import TestCase from unittest.mock import Mock, PropertyMock, patch -from unittest import TestCase as tc +import pytest from charms.postgresql_k8s.v0.postgresql import ( PostgreSQLCreateDatabaseError, PostgreSQLCreateUserError, @@ -45,6 +45,7 @@ def harness(): yield harness harness.cleanup() + def request_database(_harness): # Reset the charm status. _harness.model.unit.status = ActiveStatus() @@ -80,6 +81,7 @@ def request_database(_harness): {"database": DATABASE}, ) + @patch_network_get(private_address="1.1.1.1") def test_on_relation_changed(harness): with ( @@ -116,7 +118,7 @@ def test_on_relation_changed(harness): # Request a database before primary endpoint is available. request_database(harness) - tc().assertEqual(_defer.call_count, 2) + TestCase().assertEqual(_defer.call_count, 2) _set_up_relation.assert_not_called() # Request it again when the database is ready. @@ -125,14 +127,13 @@ def test_on_relation_changed(harness): _defer.assert_not_called() _set_up_relation.assert_called_once() + @patch_network_get(private_address="1.1.1.1") def test_get_extensions(harness): # Test when there are no extensions in the relation databags. rel_id = harness.model.get_relation(RELATION_NAME).id relation = harness.model.get_relation(RELATION_NAME, rel_id) - tc().assertEqual( - harness.charm.legacy_db_relation._get_extensions(relation), ([], set()) - ) + TestCase().assertEqual(harness.charm.legacy_db_relation._get_extensions(relation), ([], set())) # Test when there are extensions in the application relation databag. extensions = ["", "citext:public", "debversion"] @@ -142,7 +143,7 @@ def test_get_extensions(harness): "application", {"extensions": ",".join(extensions)}, ) - tc().assertEqual( + TestCase().assertEqual( harness.charm.legacy_db_relation._get_extensions(relation), ([extensions[1], extensions[2]], {extensions[1].split(":")[0], extensions[2]}), ) @@ -159,7 +160,7 @@ def test_get_extensions(harness): "application/0", {"extensions": ",".join(extensions)}, ) - tc().assertEqual( + TestCase().assertEqual( harness.charm.legacy_db_relation._get_extensions(relation), ([extensions[1], extensions[2]], {extensions[1].split(":")[0], extensions[2]}), ) @@ -175,11 +176,12 @@ def test_get_extensions(harness): harness = Harness(PostgresqlOperatorCharm, config=config) harness.cleanup() harness.begin() - tc().assertEqual( + TestCase().assertEqual( harness.charm.legacy_db_relation._get_extensions(relation), ([extensions[1], extensions[2]], {extensions[2]}), ) + @patch_network_get(private_address="1.1.1.1") def test_set_up_relation(harness): with ( @@ -212,7 +214,7 @@ def test_set_up_relation(harness): # Assert no operation is done when at least one of the requested extensions # is disabled. relation = harness.model.get_relation(RELATION_NAME, rel_id) - tc().assertFalse(harness.charm.legacy_db_relation.set_up_relation(relation)) + TestCase().assertFalse(harness.charm.legacy_db_relation.set_up_relation(relation)) postgresql_mock.create_user.assert_not_called() postgresql_mock.create_database.assert_not_called() postgresql_mock.get_postgresql_version.assert_not_called() @@ -227,7 +229,7 @@ def test_set_up_relation(harness): "application", {"database": DATABASE}, ) - tc().assertTrue(harness.charm.legacy_db_relation.set_up_relation(relation)) + TestCase().assertTrue(harness.charm.legacy_db_relation.set_up_relation(relation)) user = f"relation-{rel_id}" postgresql_mock.create_user.assert_called_once_with(user, "test-password", False) postgresql_mock.create_database.assert_called_once_with( @@ -235,7 +237,7 @@ def test_set_up_relation(harness): ) _update_endpoints.assert_called_once() _update_unit_status.assert_called_once() - tc().assertNotIsInstance(harness.model.unit.status, BlockedStatus) + TestCase().assertNotIsInstance(harness.model.unit.status, BlockedStatus) # Assert that the correct calls were made when the database name is not # provided in both application and unit databags. @@ -255,14 +257,14 @@ def test_set_up_relation(harness): "application/0", {"database": DATABASE}, ) - tc().assertTrue(harness.charm.legacy_db_relation.set_up_relation(relation)) + TestCase().assertTrue(harness.charm.legacy_db_relation.set_up_relation(relation)) postgresql_mock.create_user.assert_called_once_with(user, "test-password", False) postgresql_mock.create_database.assert_called_once_with( DATABASE, user, plugins=[], client_relations=[relation] ) _update_endpoints.assert_called_once() _update_unit_status.assert_called_once() - tc().assertNotIsInstance(harness.model.unit.status, BlockedStatus) + TestCase().assertNotIsInstance(harness.model.unit.status, BlockedStatus) # Assert that the correct calls were made when the database name is not provided. postgresql_mock.create_user.reset_mock() @@ -276,38 +278,43 @@ def test_set_up_relation(harness): "application/0", {"database": ""}, ) - tc().assertTrue(harness.charm.legacy_db_relation.set_up_relation(relation)) + TestCase().assertTrue(harness.charm.legacy_db_relation.set_up_relation(relation)) postgresql_mock.create_user.assert_called_once_with(user, "test-password", False) postgresql_mock.create_database.assert_called_once_with( "application", user, plugins=[], client_relations=[relation] ) _update_endpoints.assert_called_once() _update_unit_status.assert_called_once() - tc().assertNotIsInstance(harness.model.unit.status, BlockedStatus) + TestCase().assertNotIsInstance(harness.model.unit.status, BlockedStatus) # BlockedStatus due to a PostgreSQLCreateUserError. postgresql_mock.create_database.reset_mock() postgresql_mock.get_postgresql_version.reset_mock() _update_endpoints.reset_mock() _update_unit_status.reset_mock() - tc().assertFalse(harness.charm.legacy_db_relation.set_up_relation(relation)) + TestCase().assertFalse(harness.charm.legacy_db_relation.set_up_relation(relation)) postgresql_mock.create_database.assert_not_called() _update_endpoints.assert_not_called() _update_unit_status.assert_not_called() - tc().assertIsInstance(harness.model.unit.status, BlockedStatus) + TestCase().assertIsInstance(harness.model.unit.status, BlockedStatus) # BlockedStatus due to a PostgreSQLCreateDatabaseError. harness.charm.unit.status = ActiveStatus() - tc().assertFalse(harness.charm.legacy_db_relation.set_up_relation(relation)) + TestCase().assertFalse(harness.charm.legacy_db_relation.set_up_relation(relation)) _update_endpoints.assert_not_called() _update_unit_status.assert_not_called() - tc().assertIsInstance(harness.model.unit.status, BlockedStatus) + TestCase().assertIsInstance(harness.model.unit.status, BlockedStatus) + @patch_network_get(private_address="1.1.1.1") def test_update_unit_status(harness): with ( - patch("relations.db.DbProvides._check_for_blocking_relations") as _check_for_blocking_relations, - patch("charm.PostgresqlOperatorCharm.is_blocked", new_callable=PropertyMock) as _is_blocked, + patch( + "relations.db.DbProvides._check_for_blocking_relations" + ) as _check_for_blocking_relations, + patch( + "charm.PostgresqlOperatorCharm.is_blocked", new_callable=PropertyMock + ) as _is_blocked, ): # Test when the charm is not blocked. rel_id = harness.model.get_relation(RELATION_NAME).id @@ -315,14 +322,14 @@ def test_update_unit_status(harness): _is_blocked.return_value = False harness.charm.legacy_db_relation._update_unit_status(relation) _check_for_blocking_relations.assert_not_called() - tc().assertNotIsInstance(harness.charm.unit.status, ActiveStatus) + TestCase().assertNotIsInstance(harness.charm.unit.status, ActiveStatus) # Test when the charm is blocked but not due to extensions request. _is_blocked.return_value = True harness.charm.unit.status = BlockedStatus("fake message") harness.charm.legacy_db_relation._update_unit_status(relation) _check_for_blocking_relations.assert_not_called() - tc().assertNotIsInstance(harness.charm.unit.status, ActiveStatus) + TestCase().assertNotIsInstance(harness.charm.unit.status, ActiveStatus) # Test when there are relations causing the blocked status. harness.charm.unit.status = BlockedStatus( @@ -331,14 +338,15 @@ def test_update_unit_status(harness): _check_for_blocking_relations.return_value = True harness.charm.legacy_db_relation._update_unit_status(relation) _check_for_blocking_relations.assert_called_once_with(relation.id) - tc().assertNotIsInstance(harness.charm.unit.status, ActiveStatus) + TestCase().assertNotIsInstance(harness.charm.unit.status, ActiveStatus) # Test when there are no relations causing the blocked status anymore. _check_for_blocking_relations.reset_mock() _check_for_blocking_relations.return_value = False harness.charm.legacy_db_relation._update_unit_status(relation) _check_for_blocking_relations.assert_called_once_with(relation.id) - tc().assertIsInstance(harness.charm.unit.status, ActiveStatus) + TestCase().assertIsInstance(harness.charm.unit.status, ActiveStatus) + @patch_network_get(private_address="1.1.1.1") def test_on_relation_broken_extensions_unblock(harness): @@ -370,7 +378,8 @@ def test_on_relation_broken_extensions_unblock(harness): # Break the relation that blocked the charm. harness.remove_relation(rel_id) - tc().assertTrue(isinstance(harness.model.unit.status, ActiveStatus)) + TestCase().assertTrue(isinstance(harness.model.unit.status, ActiveStatus)) + @patch_network_get(private_address="1.1.1.1") def test_on_relation_broken_extensions_keep_block(harness): @@ -385,7 +394,6 @@ def test_on_relation_broken_extensions_keep_block(harness): patch("charm.PostgresqlOperatorCharm.is_blocked", new_callable=PropertyMock) as is_blocked, ): # Set some side effects to test multiple situations. - rel_id = harness.model.get_relation(RELATION_NAME).id is_blocked.return_value = True _member_started.return_value = True _primary_endpoint.return_value = {"1.1.1.1"} @@ -411,7 +419,8 @@ def test_on_relation_broken_extensions_keep_block(harness): event.relation.id = first_rel_id # Break one of the relations that block the charm. harness.charm.legacy_db_relation._on_relation_broken(event) - tc().assertTrue(isinstance(harness.model.unit.status, BlockedStatus)) + TestCase().assertTrue(isinstance(harness.model.unit.status, BlockedStatus)) + @patch_network_get(private_address="1.1.1.1") def test_update_endpoints_with_relation(harness): @@ -459,12 +468,8 @@ def test_update_endpoints_with_relation(harness): # Set some data to be used and compared in the relations. password = "test-password" - master = ( - f"dbname={DATABASE} host=1.1.1.1 password={password} port={DATABASE_PORT} user=" - ) - standbys = ( - f"dbname={DATABASE} host=2.2.2.2 password={password} port={DATABASE_PORT} user=" - ) + master = f"dbname={DATABASE} host=1.1.1.1 password={password} port={DATABASE_PORT} user=" + standbys = f"dbname={DATABASE} host=2.2.2.2 password={password} port={DATABASE_PORT} user=" # Set some required data before update_endpoints is called. for rel in [rel_id, another_rel_id]: @@ -489,8 +494,8 @@ def test_update_endpoints_with_relation(harness): # BlockedStatus due to a PostgreSQLGetPostgreSQLVersionError. harness.charm.legacy_db_relation.update_endpoints(relation) - tc().assertIsInstance(harness.model.unit.status, BlockedStatus) - tc().assertEqual(harness.get_relation_data(rel_id, harness.charm.unit.name), {}) + TestCase().assertIsInstance(harness.model.unit.status, BlockedStatus) + TestCase().assertEqual(harness.get_relation_data(rel_id, harness.charm.unit.name), {}) # Test with both a primary and a replica. # Update the endpoints with the event and check that it updated only @@ -502,14 +507,13 @@ def test_update_endpoints_with_relation(harness): # Set the assert function based on each relation (whether it should have data). assert_based_on_relation = ( - tc().assertTrue if rel == rel_id else tc().assertFalse + TestCase().assertTrue if rel == rel_id else TestCase().assertFalse ) # Check that the unit relation databag contains (or not) the endpoints. unit_relation_data = harness.get_relation_data(rel, harness.charm.unit.name) assert_based_on_relation( - "master" in unit_relation_data - and master + user == unit_relation_data["master"] + "master" in unit_relation_data and master + user == unit_relation_data["master"] ) assert_based_on_relation( "standbys" in unit_relation_data @@ -524,20 +528,20 @@ def test_update_endpoints_with_relation(harness): # Set the assert function based on each relation (whether it should have data). assert_based_on_relation = ( - tc().assertTrue if rel == rel_id else tc().assertFalse + TestCase().assertTrue if rel == rel_id else TestCase().assertFalse ) # Check that the unit relation databag contains the endpoints. unit_relation_data = harness.get_relation_data(rel, harness.charm.unit.name) assert_based_on_relation( - "master" in unit_relation_data - and master + user == unit_relation_data["master"] + "master" in unit_relation_data and master + user == unit_relation_data["master"] ) assert_based_on_relation( "standbys" in unit_relation_data and standbys + user == unit_relation_data["standbys"] ) + @patch_network_get(private_address="1.1.1.1") def test_update_endpoints_without_relation(harness): with ( @@ -581,12 +585,8 @@ def test_update_endpoints_without_relation(harness): # Set some data to be used and compared in the relations. password = "test-password" - master = ( - f"dbname={DATABASE} host=1.1.1.1 password={password} port={DATABASE_PORT} user=" - ) - standbys = ( - f"dbname={DATABASE} host=2.2.2.2 password={password} port={DATABASE_PORT} user=" - ) + master = f"dbname={DATABASE} host=1.1.1.1 password={password} port={DATABASE_PORT} user=" + standbys = f"dbname={DATABASE} host=2.2.2.2 password={password} port={DATABASE_PORT} user=" # Set some required data before update_endpoints is called. for rel in [rel_id, another_rel_id]: @@ -611,8 +611,8 @@ def test_update_endpoints_without_relation(harness): # BlockedStatus due to a PostgreSQLGetPostgreSQLVersionError. harness.charm.legacy_db_relation.update_endpoints() - tc().assertIsInstance(harness.model.unit.status, BlockedStatus) - tc().assertEqual(harness.get_relation_data(rel_id, harness.charm.unit.name), {}) + TestCase().assertIsInstance(harness.model.unit.status, BlockedStatus) + TestCase().assertEqual(harness.get_relation_data(rel_id, harness.charm.unit.name), {}) # Test with both a primary and a replica. # Update the endpoints and check that all relations' databags are updated. @@ -623,11 +623,10 @@ def test_update_endpoints_without_relation(harness): # Check that the unit relation databag contains the endpoints. unit_relation_data = harness.get_relation_data(rel, harness.charm.unit.name) - tc().assertTrue( - "master" in unit_relation_data - and master + user == unit_relation_data["master"] + TestCase().assertTrue( + "master" in unit_relation_data and master + user == unit_relation_data["master"] ) - tc().assertTrue( + TestCase().assertTrue( "standbys" in unit_relation_data and standbys + user == unit_relation_data["standbys"] ) @@ -640,26 +639,27 @@ def test_update_endpoints_without_relation(harness): # Check that the unit relation databag contains the endpoints. unit_relation_data = harness.get_relation_data(rel, harness.charm.unit.name) - tc().assertTrue( - "master" in unit_relation_data - and master + user == unit_relation_data["master"] + TestCase().assertTrue( + "master" in unit_relation_data and master + user == unit_relation_data["master"] ) - tc().assertTrue( + TestCase().assertTrue( "standbys" in unit_relation_data and standbys + user == unit_relation_data["standbys"] ) + @patch_network_get(private_address="1.1.1.1") def test_get_allowed_units(harness): # No allowed units from the current database application. peer_rel_id = harness.model.get_relation(PEER).id rel_id = harness.model.get_relation(RELATION_NAME).id peer_relation = harness.model.get_relation(PEER, peer_rel_id) - tc().assertEqual(harness.charm.legacy_db_relation._get_allowed_units(peer_relation), "") + TestCase().assertEqual(harness.charm.legacy_db_relation._get_allowed_units(peer_relation), "") # List of space separated allowed units from the other application. harness.add_relation_unit(rel_id, "application/1") db_relation = harness.model.get_relation(RELATION_NAME, rel_id) - tc().assertEqual( - harness.charm.legacy_db_relation._get_allowed_units(db_relation), "application/0 application/1" + TestCase().assertEqual( + harness.charm.legacy_db_relation._get_allowed_units(db_relation), + "application/0 application/1", ) diff --git a/tests/unit/test_postgresql_provider.py b/tests/unit/test_postgresql_provider.py index c30da20750..2edb9b039c 100644 --- a/tests/unit/test_postgresql_provider.py +++ b/tests/unit/test_postgresql_provider.py @@ -1,10 +1,10 @@ # Copyright 2022 Canonical Ltd. # See LICENSE file for licensing details. -import pytest +from unittest import TestCase from unittest.mock import Mock, PropertyMock, patch -from unittest import TestCase as tc +import pytest from charms.postgresql_k8s.v0.postgresql import ( PostgreSQLCreateDatabaseError, PostgreSQLCreateUserError, @@ -46,6 +46,7 @@ def harness(): yield harness harness.cleanup() + def request_database(_harness): # Reset the charm status. rel_id = _harness.model.get_relation(RELATION_NAME).id @@ -72,13 +73,16 @@ def request_database(_harness): {"database": DATABASE, "extra-user-roles": EXTRA_USER_ROLES}, ) + @patch_network_get(private_address="1.1.1.1") def test_on_database_requested(harness): with ( patch.object(PostgresqlOperatorCharm, "postgresql", Mock()) as postgresql_mock, patch("subprocess.check_output", return_value=b"C"), patch("charm.PostgreSQLProvider.update_endpoints") as _update_endpoints, - patch("relations.postgresql_provider.new_password", return_value="test-password") as _new_password, + patch( + "relations.postgresql_provider.new_password", return_value="test-password" + ) as _new_password, patch.object(EventBase, "defer") as _defer, patch( "charm.PostgresqlOperatorCharm.primary_endpoint", @@ -115,7 +119,7 @@ def test_on_database_requested(harness): # Request a database before primary endpoint is available. request_database(harness) - tc().assertEqual(_defer.call_count, 2) + TestCase().assertEqual(_defer.call_count, 2) # Request it again when the database is ready. request_database(harness) @@ -134,7 +138,7 @@ def test_on_database_requested(harness): _update_endpoints.assert_called_once() # Assert that the relation data was updated correctly. - tc().assertEqual( + TestCase().assertEqual( harness.get_relation_data(rel_id, harness.charm.app.name), { "data": f'{{"database": "{DATABASE}", "extra-user-roles": "{EXTRA_USER_ROLES}"}}', @@ -146,13 +150,13 @@ def test_on_database_requested(harness): ) # Assert no BlockedStatus was set. - tc().assertFalse(isinstance(harness.model.unit.status, BlockedStatus)) + TestCase().assertFalse(isinstance(harness.model.unit.status, BlockedStatus)) # BlockedStatus due to a PostgreSQLCreateUserError. request_database(harness) - tc().assertTrue(isinstance(harness.model.unit.status, BlockedStatus)) + TestCase().assertTrue(isinstance(harness.model.unit.status, BlockedStatus)) # No data is set in the databag by the database. - tc().assertEqual( + TestCase().assertEqual( harness.get_relation_data(rel_id, harness.charm.app.name), { "data": f'{{"database": "{DATABASE}", "extra-user-roles": "{EXTRA_USER_ROLES}"}}', @@ -161,9 +165,9 @@ def test_on_database_requested(harness): # BlockedStatus due to a PostgreSQLCreateDatabaseError. request_database(harness) - tc().assertTrue(isinstance(harness.model.unit.status, BlockedStatus)) + TestCase().assertTrue(isinstance(harness.model.unit.status, BlockedStatus)) # No data is set in the databag by the database. - tc().assertEqual( + TestCase().assertEqual( harness.get_relation_data(rel_id, harness.charm.app.name), { "data": f'{{"database": "{DATABASE}", "extra-user-roles": "{EXTRA_USER_ROLES}"}}', @@ -172,7 +176,8 @@ def test_on_database_requested(harness): # BlockedStatus due to a PostgreSQLGetPostgreSQLVersionError. request_database(harness) - tc().assertTrue(isinstance(harness.model.unit.status, BlockedStatus)) + TestCase().assertTrue(isinstance(harness.model.unit.status, BlockedStatus)) + @patch_network_get(private_address="1.1.1.1") def test_oversee_users(harness): @@ -213,6 +218,7 @@ def test_oversee_users(harness): harness.charm.postgresql_client_relation.oversee_users() postgresql_mock.delete_user.assert_called_once() # Only the previous call. + @patch_network_get(private_address="1.1.1.1") def test_update_endpoints_with_event(harness): with ( @@ -245,26 +251,27 @@ def test_update_endpoints_with_event(harness): # Update the endpoints with the event and check that it updated # only the right relation databag (the one from the event). harness.charm.postgresql_client_relation.update_endpoints(mock_event) - tc().assertEqual( + TestCase().assertEqual( harness.get_relation_data(rel_id, harness.charm.app.name), {"endpoints": "1.1.1.1:5432", "read-only-endpoints": "2.2.2.2:5432"}, ) - tc().assertEqual( + TestCase().assertEqual( harness.get_relation_data(another_rel_id, harness.charm.app.name), {}, ) # Also test with only a primary instance. harness.charm.postgresql_client_relation.update_endpoints(mock_event) - tc().assertEqual( + TestCase().assertEqual( harness.get_relation_data(rel_id, harness.charm.app.name), {"endpoints": "1.1.1.1:5432"}, ) - tc().assertEqual( + TestCase().assertEqual( harness.get_relation_data(another_rel_id, harness.charm.app.name), {}, ) + @patch_network_get(private_address="1.1.1.1") def test_update_endpoints_without_event(harness): with ( @@ -290,22 +297,22 @@ def test_update_endpoints_without_event(harness): # Test with both a primary and a replica. # Update the endpoints and check that all relations' databags are updated. harness.charm.postgresql_client_relation.update_endpoints() - tc().assertEqual( + TestCase().assertEqual( harness.get_relation_data(rel_id, harness.charm.app.name), {"endpoints": "1.1.1.1:5432", "read-only-endpoints": "2.2.2.2:5432"}, ) - tc().assertEqual( + TestCase().assertEqual( harness.get_relation_data(another_rel_id, harness.charm.app.name), {"endpoints": "1.1.1.1:5432", "read-only-endpoints": "2.2.2.2:5432"}, ) # Also test with only a primary instance. harness.charm.postgresql_client_relation.update_endpoints() - tc().assertEqual( + TestCase().assertEqual( harness.get_relation_data(rel_id, harness.charm.app.name), {"endpoints": "1.1.1.1:5432"}, ) - tc().assertEqual( + TestCase().assertEqual( harness.get_relation_data(another_rel_id, harness.charm.app.name), {"endpoints": "1.1.1.1:5432"}, ) diff --git a/tests/unit/test_upgrade.py b/tests/unit/test_upgrade.py index adcb928735..0a82d4b00b 100644 --- a/tests/unit/test_upgrade.py +++ b/tests/unit/test_upgrade.py @@ -1,9 +1,9 @@ # Copyright 2023 Canonical Ltd. # See LICENSE file for licensing details. -import pytest +from unittest import TestCase from unittest.mock import MagicMock, PropertyMock, patch -from unittest import TestCase as tc +import pytest import tenacity from charms.data_platform_libs.v0.upgrade import ClusterNotReadyError from ops.testing import Harness @@ -13,7 +13,6 @@ from tests.helpers import patch_network_get - @pytest.fixture(autouse=True) def harness(): """Set up the test.""" @@ -24,12 +23,11 @@ def harness(): for rel_id in (upgrade_relation_id, peer_relation_id): harness.add_relation_unit(rel_id, "postgresql/1") with harness.hooks_disabled(): - harness.update_relation_data( - upgrade_relation_id, "postgresql/1", {"state": "idle"} - ) + harness.update_relation_data(upgrade_relation_id, "postgresql/1", {"state": "idle"}) yield harness harness.cleanup() + @patch_network_get(private_address="1.1.1.1") def test_build_upgrade_stack(harness): with ( @@ -44,8 +42,9 @@ def test_build_upgrade_stack(harness): for rel_id in (upgrade_relation_id, peer_relation_id): harness.add_relation_unit(rel_id, "postgresql/2") - tc().assertEqual(harness.charm.upgrade.build_upgrade_stack(), [0, 1, 2]) - tc().assertEqual(harness.charm.upgrade.build_upgrade_stack(), [1, 2, 0]) + TestCase().assertEqual(harness.charm.upgrade.build_upgrade_stack(), [0, 1, 2]) + TestCase().assertEqual(harness.charm.upgrade.build_upgrade_stack(), [1, 2, 0]) + def test_log_rollback(harness): with ( @@ -57,18 +56,29 @@ def test_log_rollback(harness): "Run `juju refresh --revision postgresql` to initiate the rollback" ) + @patch_network_get(private_address="1.1.1.1") def test_on_upgrade_granted(harness): with ( patch("charm.Patroni.get_postgresql_version"), - patch("charms.data_platform_libs.v0.upgrade.DataUpgrade.on_upgrade_changed") as _on_upgrade_changed, - patch("charms.data_platform_libs.v0.upgrade.DataUpgrade.set_unit_failed") as _set_unit_failed, - patch("charms.data_platform_libs.v0.upgrade.DataUpgrade.set_unit_completed") as _set_unit_completed, - patch("charm.Patroni.is_replication_healthy", new_callable=PropertyMock) as _is_replication_healthy, + patch( + "charms.data_platform_libs.v0.upgrade.DataUpgrade.on_upgrade_changed" + ) as _on_upgrade_changed, + patch( + "charms.data_platform_libs.v0.upgrade.DataUpgrade.set_unit_failed" + ) as _set_unit_failed, + patch( + "charms.data_platform_libs.v0.upgrade.DataUpgrade.set_unit_completed" + ) as _set_unit_completed, + patch( + "charm.Patroni.is_replication_healthy", new_callable=PropertyMock + ) as _is_replication_healthy, patch("charm.Patroni.cluster_members", new_callable=PropertyMock) as _cluster_members, patch("charm.Patroni.member_started", new_callable=PropertyMock) as _member_started, patch("upgrade.wait_fixed", return_value=tenacity.wait_fixed(0)), - patch("charm.PostgreSQLBackups.start_stop_pgbackrest_service") as _start_stop_pgbackrest_service, + patch( + "charm.PostgreSQLBackups.start_stop_pgbackrest_service" + ) as _start_stop_pgbackrest_service, patch("charm.PostgresqlOperatorCharm._setup_exporter") as _setup_exporter, patch("charm.Patroni.start_patroni") as _start_patroni, patch("charm.PostgresqlOperatorCharm._install_snap_packages") as _install_snap_packages, @@ -91,7 +101,7 @@ def test_on_upgrade_granted(harness): _start_patroni.return_value = True _member_started.return_value = False harness.charm.upgrade._on_upgrade_granted(mock_event) - tc().assertEqual(_member_started.call_count, 6) + TestCase().assertEqual(_member_started.call_count, 6) _cluster_members.assert_not_called() mock_event.defer.assert_called_once() _set_unit_completed.assert_not_called() @@ -104,8 +114,8 @@ def test_on_upgrade_granted(harness): _member_started.return_value = True _cluster_members.return_value = ["postgresql-1"] harness.charm.upgrade._on_upgrade_granted(mock_event) - tc().assertEqual(_member_started.call_count, 6) - tc().assertEqual(_cluster_members.call_count, 6) + TestCase().assertEqual(_member_started.call_count, 6) + TestCase().assertEqual(_cluster_members.call_count, 6) mock_event.defer.assert_called_once() _set_unit_completed.assert_not_called() _set_unit_failed.assert_not_called() @@ -152,10 +162,13 @@ def test_on_upgrade_granted(harness): _set_unit_failed.assert_not_called() _on_upgrade_changed.assert_called_once() + @patch_network_get(private_address="1.1.1.1") def test_pre_upgrade_check(harness): with ( - patch("charm.Patroni.is_creating_backup", new_callable=PropertyMock) as _is_creating_backup, + patch( + "charm.Patroni.is_creating_backup", new_callable=PropertyMock + ) as _is_creating_backup, patch("charm.Patroni.are_all_members_ready") as _are_all_members_ready, ): with harness.hooks_disabled(): @@ -166,11 +179,11 @@ def test_pre_upgrade_check(harness): _is_creating_backup.side_effect = [True, False, False] # Test when not all members are ready. - with tc().assertRaises(ClusterNotReadyError): + with TestCase().assertRaises(ClusterNotReadyError): harness.charm.upgrade.pre_upgrade_check() # Test when a backup is being created. - with tc().assertRaises(ClusterNotReadyError): + with TestCase().assertRaises(ClusterNotReadyError): harness.charm.upgrade.pre_upgrade_check() # Test when everything is ok to start the upgrade. diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 77f1e018c8..3e2f26b183 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -2,7 +2,7 @@ # See LICENSE file for licensing details. import re -from unittest import TestCase as tc +from unittest import TestCase from utils import new_password @@ -11,9 +11,9 @@ def test_new_password(): # Test the password generation twice in order to check if we get different passwords and # that they meet the required criteria. first_password = new_password() - tc().assertEqual(len(first_password), 16) - tc().assertIsNotNone(re.fullmatch("[a-zA-Z0-9\b]{16}$", first_password)) + TestCase().assertEqual(len(first_password), 16) + TestCase().assertIsNotNone(re.fullmatch("[a-zA-Z0-9\b]{16}$", first_password)) second_password = new_password() - tc().assertIsNotNone(re.fullmatch("[a-zA-Z0-9\b]{16}$", second_password)) - tc().assertNotEqual(second_password, first_password) + TestCase().assertIsNotNone(re.fullmatch("[a-zA-Z0-9\b]{16}$", second_password)) + TestCase().assertNotEqual(second_password, first_password) From eb7bd38155a9c75c1656b71261832a73f1244e9e Mon Sep 17 00:00:00 2001 From: Lucas Gameiro Borges Date: Mon, 22 Apr 2024 11:27:07 +0000 Subject: [PATCH 7/8] fix copyright year --- tests/unit/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index e32583ed9b..057868fc71 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright 2022 Canonical Ltd. +# Copyright 2024 Canonical Ltd. # See LICENSE file for licensing details. from unittest.mock import PropertyMock From 6c0fdd70a7fc4db1c13ccf9013461709770d668d Mon Sep 17 00:00:00 2001 From: Lucas Gameiro Borges Date: Mon, 22 Apr 2024 22:59:49 +0000 Subject: [PATCH 8/8] refactor asserts --- tests/unit/test_backups.py | 269 ++++++++++++------------- tests/unit/test_cluster.py | 43 ++-- tests/unit/test_db.py | 75 ++++--- tests/unit/test_postgresql_provider.py | 35 ++-- tests/unit/test_upgrade.py | 17 +- tests/unit/test_utils.py | 11 +- 6 files changed, 228 insertions(+), 222 deletions(-) diff --git a/tests/unit/test_backups.py b/tests/unit/test_backups.py index 50d60487c0..cc1a0fc25d 100644 --- a/tests/unit/test_backups.py +++ b/tests/unit/test_backups.py @@ -27,6 +27,9 @@ FAILED_TO_INITIALIZE_STANZA_ERROR_MESSAGE = "failed to initialize stanza, check your S3 settings" S3_PARAMETERS_RELATION = "s3-parameters" +# used for assert functions +tc = TestCase() + @pytest.fixture(autouse=True) def harness(): @@ -41,7 +44,7 @@ def harness(): def test_stanza_name(harness): - TestCase().assertEqual( + tc.assertEqual( harness.charm.backup.stanza_name, f"{harness.charm.model.name}.{harness.charm.cluster_name}", ) @@ -49,14 +52,14 @@ def test_stanza_name(harness): def test_are_backup_settings_ok(harness): # Test without S3 relation. - TestCase().assertEqual( + tc.assertEqual( harness.charm.backup._are_backup_settings_ok(), (False, "Relation with s3-integrator charm missing, cannot create/restore backup."), ) # Test when there are missing S3 parameters. harness.add_relation(S3_PARAMETERS_RELATION, "s3-integrator") - TestCase().assertEqual( + tc.assertEqual( harness.charm.backup._are_backup_settings_ok(), (False, "Missing S3 parameters: ['bucket', 'access-key', 'secret-key']"), ) @@ -64,7 +67,7 @@ def test_are_backup_settings_ok(harness): # Test when all required parameters are provided. with patch("charm.PostgreSQLBackups._retrieve_s3_parameters") as _retrieve_s3_parameters: _retrieve_s3_parameters.return_value = ["bucket", "access-key", "secret-key"], [] - TestCase().assertEqual( + tc.assertEqual( harness.charm.backup._are_backup_settings_ok(), (True, None), ) @@ -83,7 +86,7 @@ def test_can_unit_perform_backup(harness): peer_rel_id = harness.model.get_relation(PEER).id # Test when the charm fails to retrieve the primary. _is_primary.side_effect = RetryError(last_attempt=1) - TestCase().assertEqual( + tc.assertEqual( harness.charm.backup._can_unit_perform_backup(), (False, "Unit cannot perform backups as the database seems to be offline"), ) @@ -92,7 +95,7 @@ def test_can_unit_perform_backup(harness): _is_primary.side_effect = None _is_primary.return_value = True harness.charm.unit.status = BlockedStatus("fake blocked state") - TestCase().assertEqual( + tc.assertEqual( harness.charm.backup._can_unit_perform_backup(), (False, "Unit is in a blocking state"), ) @@ -106,7 +109,7 @@ def test_can_unit_perform_backup(harness): harness.charm.unit.name, {"tls": "True"}, ) - TestCase().assertEqual( + tc.assertEqual( harness.charm.backup._can_unit_perform_backup(), (False, "Unit cannot perform backups as it is the cluster primary"), ) @@ -119,7 +122,7 @@ def test_can_unit_perform_backup(harness): harness.charm.unit.name, {"tls": ""}, ) - TestCase().assertEqual( + tc.assertEqual( harness.charm.backup._can_unit_perform_backup(), (False, "Unit cannot perform backups as TLS is not enabled"), ) @@ -127,14 +130,14 @@ def test_can_unit_perform_backup(harness): # Test when Patroni or PostgreSQL hasn't started yet. _is_primary.return_value = True _member_started.return_value = False - TestCase().assertEqual( + tc.assertEqual( harness.charm.backup._can_unit_perform_backup(), (False, "Unit cannot perform backups as it's not in running state"), ) # Test when the stanza was not initialised yet. _member_started.return_value = True - TestCase().assertEqual( + tc.assertEqual( harness.charm.backup._can_unit_perform_backup(), (False, "Stanza was not initialised"), ) @@ -147,14 +150,14 @@ def test_can_unit_perform_backup(harness): {"stanza": harness.charm.backup.stanza_name}, ) _are_backup_settings_ok.return_value = (False, "fake error message") - TestCase().assertEqual( + tc.assertEqual( harness.charm.backup._can_unit_perform_backup(), (False, "fake error message"), ) # Test when everything is ok to run a backup. _are_backup_settings_ok.return_value = (True, None) - TestCase().assertEqual( + tc.assertEqual( harness.charm.backup._can_unit_perform_backup(), (True, None), ) @@ -182,12 +185,12 @@ def test_can_use_s3_repository(harness): # Test when nothing is returned from the pgBackRest info command. _execute_command.side_effect = TimeoutExpired(cmd="fake command", timeout=30) - with TestCase().assertRaises(TimeoutError): + with tc.assertRaises(TimeoutError): harness.charm.backup.can_use_s3_repository() _execute_command.side_effect = None _execute_command.return_value = (1, "", "") - TestCase().assertEqual( + tc.assertEqual( harness.charm.backup.can_use_s3_repository(), (False, FAILED_TO_INITIALIZE_STANZA_ERROR_MESSAGE), ) @@ -199,13 +202,13 @@ def test_can_use_s3_repository(harness): "", ) _execute_command.return_value = pgbackrest_info_same_cluster_backup_output - TestCase().assertEqual( + tc.assertEqual( harness.charm.backup.can_use_s3_repository(), (True, None), ) # Assert that the stanza name is still in the unit relation data. - TestCase().assertEqual( + tc.assertEqual( harness.get_relation_data(peer_rel_id, harness.charm.app), {"stanza": harness.charm.backup.stanza_name}, ) @@ -219,8 +222,8 @@ def test_can_use_s3_repository(harness): ] with harness.hooks_disabled(): harness.set_leader() - with TestCase().assertRaises(Exception): - TestCase().assertEqual( + with tc.assertRaises(Exception): + tc.assertEqual( harness.charm.backup.can_use_s3_repository(), (False, ANOTHER_CLUSTER_REPOSITORY_ERROR_MESSAGE), ) @@ -249,7 +252,7 @@ def test_can_use_s3_repository(harness): harness.charm.app.name, {"stanza": harness.charm.backup.stanza_name}, ) - TestCase().assertEqual( + tc.assertEqual( harness.charm.backup.can_use_s3_repository(), (False, ANOTHER_CLUSTER_REPOSITORY_ERROR_MESSAGE), ) @@ -258,7 +261,7 @@ def test_can_use_s3_repository(harness): _reload_patroni_configuration.assert_called_once() # Assert that the stanza name is not present in the unit relation data anymore. - TestCase().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.app), {}) + tc.assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.app), {}) # Test when the cluster system id can be retrieved, but it's different from the stanza system id. _update_config.reset_mock() @@ -284,7 +287,7 @@ def test_can_use_s3_repository(harness): harness.charm.app.name, {"stanza": harness.charm.backup.stanza_name}, ) - TestCase().assertEqual( + tc.assertEqual( harness.charm.backup.can_use_s3_repository(), (False, ANOTHER_CLUSTER_REPOSITORY_ERROR_MESSAGE), ) @@ -293,7 +296,7 @@ def test_can_use_s3_repository(harness): _reload_patroni_configuration.assert_called_once() # Assert that the stanza name is not present in the unit relation data anymore. - TestCase().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.app), {}) + tc.assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.app), {}) # Test when the workload is not running. _update_config.reset_mock() @@ -310,7 +313,7 @@ def test_can_use_s3_repository(harness): pgbackrest_info_same_cluster_backup_output, other_instance_system_identifier_output, ] - TestCase().assertEqual( + tc.assertEqual( harness.charm.backup.can_use_s3_repository(), (False, ANOTHER_CLUSTER_REPOSITORY_ERROR_MESSAGE), ) @@ -319,7 +322,7 @@ def test_can_use_s3_repository(harness): _reload_patroni_configuration.assert_not_called() # Assert that the stanza name is not present in the unit relation data anymore. - TestCase().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.app), {}) + tc.assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.app), {}) # Test when there is no backup from another cluster in the S3 repository. with harness.hooks_disabled(): @@ -332,13 +335,13 @@ def test_can_use_s3_repository(harness): pgbackrest_info_same_cluster_backup_output, same_instance_system_identifier_output, ] - TestCase().assertEqual( + tc.assertEqual( harness.charm.backup.can_use_s3_repository(), (True, None), ) # Assert that the stanza name is still in the unit relation data. - TestCase().assertEqual( + tc.assertEqual( harness.get_relation_data(peer_rel_id, harness.charm.app), {"stanza": harness.charm.backup.stanza_name}, ) @@ -347,20 +350,20 @@ def test_can_use_s3_repository(harness): def test_construct_endpoint(harness): # Test with an AWS endpoint without region. s3_parameters = {"endpoint": "https://s3.amazonaws.com", "region": ""} - TestCase().assertEqual( + tc.assertEqual( harness.charm.backup._construct_endpoint(s3_parameters), "https://s3.amazonaws.com" ) # Test with an AWS endpoint with region. s3_parameters["region"] = "us-east-1" - TestCase().assertEqual( + tc.assertEqual( harness.charm.backup._construct_endpoint(s3_parameters), "https://s3.us-east-1.amazonaws.com", ) # Test with another cloud endpoint. s3_parameters["endpoint"] = "https://storage.googleapis.com" - TestCase().assertEqual( + tc.assertEqual( harness.charm.backup._construct_endpoint(s3_parameters), "https://storage.googleapis.com" ) @@ -387,7 +390,7 @@ def test_create_bucket_if_not_exists(harness): [], ) _resource.side_effect = ValueError - with TestCase().assertRaises(ValueError): + with tc.assertRaises(ValueError): harness.charm.backup._create_bucket_if_not_exists() # Test when the bucket already exists. @@ -419,7 +422,7 @@ def test_create_bucket_if_not_exists(harness): error_response={"Error": {"Code": 1, "message": "fake error"}}, operation_name="fake operation name", ) - with TestCase().assertRaises(ClientError): + with tc.assertRaises(ClientError): harness.charm.backup._create_bucket_if_not_exists() head_bucket.assert_called_once() create.assert_called_once() @@ -431,7 +434,7 @@ def test_create_bucket_if_not_exists(harness): head_bucket.side_effect = botocore.exceptions.ConnectTimeoutError( endpoint_url="fake endpoint URL" ) - with TestCase().assertRaises(botocore.exceptions.ConnectTimeoutError): + with tc.assertRaises(botocore.exceptions.ConnectTimeoutError): harness.charm.backup._create_bucket_if_not_exists() head_bucket.assert_called_once() create.assert_not_called() @@ -446,7 +449,7 @@ def test_empty_data_files(harness): ): # Test when the data directory doesn't exist. _exists.return_value = False - TestCase().assertTrue(harness.charm.backup._empty_data_files()) + tc.assertTrue(harness.charm.backup._empty_data_files()) _rmtree.assert_not_called() # Test when the removal of the data files fails. @@ -454,13 +457,13 @@ def test_empty_data_files(harness): _exists.return_value = True _is_dir.return_value = True _rmtree.side_effect = OSError - TestCase().assertFalse(harness.charm.backup._empty_data_files()) + tc.assertFalse(harness.charm.backup._empty_data_files()) _rmtree.assert_called_once_with(path) # Test when data files are successfully removed. _rmtree.reset_mock() _rmtree.side_effect = None - TestCase().assertTrue(harness.charm.backup._empty_data_files()) + tc.assertTrue(harness.charm.backup._empty_data_files()) _rmtree.assert_called_once_with(path) @@ -477,7 +480,7 @@ def test_change_connectivity_to_database(harness): # Test when connectivity should be turned on. harness.charm.backup._change_connectivity_to_database(True) - TestCase().assertEqual( + tc.assertEqual( harness.get_relation_data(peer_rel_id, harness.charm.unit), {"connectivity": "on"}, ) @@ -486,7 +489,7 @@ def test_change_connectivity_to_database(harness): # Test when connectivity should be turned off. _update_config.reset_mock() harness.charm.backup._change_connectivity_to_database(False) - TestCase().assertEqual( + tc.assertEqual( harness.get_relation_data(peer_rel_id, harness.charm.unit), {"connectivity": "off"}, ) @@ -501,9 +504,7 @@ def test_execute_command(harness): # Test when the command fails. command = "rm -r /var/lib/postgresql/data/pgdata".split() _run.return_value = CompletedProcess(command, 1, b"", b"fake stderr") - TestCase().assertEqual( - harness.charm.backup._execute_command(command), (1, "", "fake stderr") - ) + tc.assertEqual(harness.charm.backup._execute_command(command), (1, "", "fake stderr")) _run.assert_called_once_with( command, input=None, stdout=PIPE, stderr=PIPE, preexec_fn=ANY, timeout=None ) @@ -514,7 +515,7 @@ def test_execute_command(harness): _getpwnam.reset_mock() _run.side_effect = None _run.return_value = CompletedProcess(command, 0, b"fake stdout", b"") - TestCase().assertEqual( + tc.assertEqual( harness.charm.backup._execute_command(command, command_input=b"fake input", timeout=5), (0, "fake stdout", ""), ) @@ -526,7 +527,7 @@ def test_execute_command(harness): def test_format_backup_list(harness): # Test when there are no backups. - TestCase().assertEqual( + tc.assertEqual( harness.charm.backup._format_backup_list([]), """backup-id | backup-type | backup-status ----------------------------------------------------""", @@ -537,7 +538,7 @@ def test_format_backup_list(harness): ("2023-01-01T09:00:00Z", "physical", "failed: fake error"), ("2023-01-01T10:00:00Z", "physical", "finished"), ] - TestCase().assertEqual( + tc.assertEqual( harness.charm.backup._format_backup_list(backup_list), """backup-id | backup-type | backup-status ---------------------------------------------------- @@ -550,7 +551,7 @@ def test_generate_backup_list_output(harness): with patch("charm.PostgreSQLBackups._execute_command") as _execute_command: # Test when no backups are returned. _execute_command.return_value = (0, '[{"backup":[]}]', "") - TestCase().assertEqual( + tc.assertEqual( harness.charm.backup._generate_backup_list_output(), """backup-id | backup-type | backup-status ----------------------------------------------------""", @@ -562,7 +563,7 @@ def test_generate_backup_list_output(harness): '[{"backup":[{"label":"20230101-090000F","error":"fake error"},{"label":"20230101-100000F","error":null}]}]', "", ) - TestCase().assertEqual( + tc.assertEqual( harness.charm.backup._generate_backup_list_output(), """backup-id | backup-type | backup-status ---------------------------------------------------- @@ -575,14 +576,14 @@ def test_list_backups(harness): with patch("charm.PostgreSQLBackups._execute_command") as _execute_command: # Test when the command that list the backups fails. _execute_command.return_value = (1, "", "fake stderr") - with TestCase().assertRaises(ListBackupsError): - TestCase().assertEqual( + with tc.assertRaises(ListBackupsError): + tc.assertEqual( harness.charm.backup._list_backups(show_failed=True), OrderedDict[str, str]() ) # Test when no backups are available. _execute_command.return_value = (0, "[]", "") - TestCase().assertEqual( + tc.assertEqual( harness.charm.backup._list_backups(show_failed=True), OrderedDict[str, str]() ) @@ -592,7 +593,7 @@ def test_list_backups(harness): '[{"backup":[{"label":"20230101-090000F","error":"fake error"},{"label":"20230101-100000F","error":null}],"name":"test-stanza"}]', "", ) - TestCase().assertEqual( + tc.assertEqual( harness.charm.backup._list_backups(show_failed=True), OrderedDict[str, str]([ ("2023-01-01T09:00:00Z", "test-stanza"), @@ -601,7 +602,7 @@ def test_list_backups(harness): ) # Test when some backups are available, but it's not desired to list failed backups. - TestCase().assertEqual( + tc.assertEqual( harness.charm.backup._list_backups(show_failed=False), OrderedDict[str, str]([("2023-01-01T10:00:00Z", "test-stanza")]), ) @@ -649,18 +650,18 @@ def test_initialise_stanza(harness): harness.charm.unit.status = BlockedStatus(blocked_state) harness.charm.backup._initialise_stanza() _execute_command.assert_called_once_with(stanza_creation_command) - TestCase().assertIsInstance(harness.charm.unit.status, BlockedStatus) - TestCase().assertEqual( + tc.assertIsInstance(harness.charm.unit.status, BlockedStatus) + tc.assertEqual( harness.charm.unit.status.message, FAILED_TO_INITIALIZE_STANZA_ERROR_MESSAGE ) # Assert there is no stanza name in the application relation databag. - TestCase().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.app), {}) + tc.assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.app), {}) # Test when the failure in the stanza creation is due to a timeout. _execute_command.reset_mock() _execute_command.return_value = (49, "", "fake stderr") - with TestCase().assertRaises(TimeoutError): + with tc.assertRaises(TimeoutError): harness.charm.backup._initialise_stanza() # Test when the archiving is working correctly (pgBackRest check command succeeds) @@ -669,15 +670,15 @@ def test_initialise_stanza(harness): _execute_command.return_value = (0, "fake stdout", "") _member_started.return_value = True harness.charm.backup._initialise_stanza() - TestCase().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.app), {}) - TestCase().assertEqual( + tc.assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.app), {}) + tc.assertEqual( harness.get_relation_data(peer_rel_id, harness.charm.unit), { "stanza": f"{harness.charm.model.name}.postgresql", "init-pgbackrest": "True", }, ) - TestCase().assertIsInstance(harness.charm.unit.status, MaintenanceStatus) + tc.assertIsInstance(harness.charm.unit.status, MaintenanceStatus) # Test when the unit is the leader. with harness.hooks_disabled(): @@ -687,13 +688,13 @@ def test_initialise_stanza(harness): ) harness.charm.backup._initialise_stanza() _update_config.assert_not_called() - TestCase().assertEqual( + tc.assertEqual( harness.get_relation_data(peer_rel_id, harness.charm.app), {"stanza": "None.postgresql", "init-pgbackrest": "True"}, ) _member_started.assert_not_called() _reload_patroni_configuration.assert_not_called() - TestCase().assertIsInstance(harness.charm.unit.status, MaintenanceStatus) + tc.assertIsInstance(harness.charm.unit.status, MaintenanceStatus) @patch_network_get(private_address="1.1.1.1") @@ -734,14 +735,14 @@ def test_check_stanza(harness): _execute_command.return_value = (49, "", "fake stderr") _member_started.return_value = True harness.charm.backup.check_stanza() - TestCase().assertEqual(_update_config.call_count, 2) - TestCase().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.app), {}) - TestCase().assertEqual(_member_started.call_count, 5) - TestCase().assertEqual(_reload_patroni_configuration.call_count, 5) - TestCase().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.app), {}) - TestCase().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.unit), {}) - TestCase().assertIsInstance(harness.charm.unit.status, BlockedStatus) - TestCase().assertEqual( + tc.assertEqual(_update_config.call_count, 2) + tc.assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.app), {}) + tc.assertEqual(_member_started.call_count, 5) + tc.assertEqual(_reload_patroni_configuration.call_count, 5) + tc.assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.app), {}) + tc.assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.unit), {}) + tc.assertIsInstance(harness.charm.unit.status, BlockedStatus) + tc.assertEqual( harness.charm.unit.status.message, FAILED_TO_INITIALIZE_STANZA_ERROR_MESSAGE ) @@ -768,15 +769,15 @@ def test_check_stanza(harness): _update_config.assert_called_once() _member_started.assert_called_once() _reload_patroni_configuration.assert_called_once() - TestCase().assertEqual( + tc.assertEqual( harness.get_relation_data(peer_rel_id, harness.charm.app), {"stanza": "test-stanza", "init-pgbackrest": "True"}, ) - TestCase().assertEqual( + tc.assertEqual( harness.get_relation_data(peer_rel_id, harness.charm.unit), {"stanza": "test-stanza"}, ) - TestCase().assertIsInstance(harness.charm.unit.status, ActiveStatus) + tc.assertIsInstance(harness.charm.unit.status, ActiveStatus) # Test when the unit is the leader. harness.charm.unit.status = BlockedStatus("fake blocked state") @@ -799,15 +800,15 @@ def test_check_stanza(harness): _update_config.assert_called_once() _member_started.assert_called_once() _reload_patroni_configuration.assert_called_once() - TestCase().assertEqual( + tc.assertEqual( harness.get_relation_data(peer_rel_id, harness.charm.app), {"stanza": "test-stanza"}, ) - TestCase().assertEqual( + tc.assertEqual( harness.get_relation_data(peer_rel_id, harness.charm.unit), {"stanza": "test-stanza"}, ) - TestCase().assertIsInstance(harness.charm.unit.status, ActiveStatus) + tc.assertIsInstance(harness.charm.unit.status, ActiveStatus) def test_coordinate_stanza_fields(harness): @@ -819,9 +820,9 @@ def test_coordinate_stanza_fields(harness): # Test when the stanza name is neither in the application relation databag nor in the unit relation databag. harness.charm.backup.coordinate_stanza_fields() - TestCase().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.app), {}) - TestCase().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.unit), {}) - TestCase().assertEqual(harness.get_relation_data(peer_rel_id, new_unit), {}) + tc.assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.app), {}) + tc.assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.unit), {}) + tc.assertEqual(harness.get_relation_data(peer_rel_id, new_unit), {}) # Test when the stanza name is in the unit relation databag but the unit is not the leader. stanza_name = f"{harness.charm.model.name}.{harness.charm.app.name}" @@ -830,9 +831,9 @@ def test_coordinate_stanza_fields(harness): peer_rel_id, new_unit_name, {"stanza": stanza_name, "init-pgbackrest": "True"} ) harness.charm.backup.coordinate_stanza_fields() - TestCase().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.app), {}) - TestCase().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.unit), {}) - TestCase().assertEqual( + tc.assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.app), {}) + tc.assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.unit), {}) + tc.assertEqual( harness.get_relation_data(peer_rel_id, new_unit), {"stanza": stanza_name, "init-pgbackrest": "True"}, ) @@ -841,12 +842,12 @@ def test_coordinate_stanza_fields(harness): with harness.hooks_disabled(): harness.set_leader() harness.charm.backup.coordinate_stanza_fields() - TestCase().assertEqual( + tc.assertEqual( harness.get_relation_data(peer_rel_id, harness.charm.app), {"stanza": stanza_name, "init-pgbackrest": "True"}, ) - TestCase().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.unit), {}) - TestCase().assertEqual( + tc.assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.unit), {}) + tc.assertEqual( harness.get_relation_data(peer_rel_id, new_unit), {"stanza": stanza_name, "init-pgbackrest": "True"}, ) @@ -855,42 +856,36 @@ def test_coordinate_stanza_fields(harness): with harness.hooks_disabled(): harness.update_relation_data(peer_rel_id, new_unit_name, {"init-pgbackrest": ""}) harness.charm.backup.coordinate_stanza_fields() - TestCase().assertEqual( + tc.assertEqual( harness.get_relation_data(peer_rel_id, harness.charm.app), {"stanza": stanza_name}, ) - TestCase().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.unit), {}) - TestCase().assertEqual( - harness.get_relation_data(peer_rel_id, new_unit), {"stanza": stanza_name} - ) + tc.assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.unit), {}) + tc.assertEqual(harness.get_relation_data(peer_rel_id, new_unit), {"stanza": stanza_name}) # Test when the "init-pgbackrest" flag was removed from the application relation databag # and this is the unit that has the stanza name in the unit relation databag. with harness.hooks_disabled(): harness.update_relation_data(peer_rel_id, harness.charm.unit.name, {"stanza": stanza_name}) harness.charm.backup.coordinate_stanza_fields() - TestCase().assertEqual( + tc.assertEqual( harness.get_relation_data(peer_rel_id, harness.charm.app), {"stanza": stanza_name}, ) - TestCase().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.unit), {}) - TestCase().assertEqual( - harness.get_relation_data(peer_rel_id, new_unit), {"stanza": stanza_name} - ) + tc.assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.unit), {}) + tc.assertEqual(harness.get_relation_data(peer_rel_id, new_unit), {"stanza": stanza_name}) # Test when the unit is not the leader. with harness.hooks_disabled(): harness.set_leader(False) harness.update_relation_data(peer_rel_id, harness.charm.unit.name, {"stanza": stanza_name}) harness.charm.backup.coordinate_stanza_fields() - TestCase().assertEqual( + tc.assertEqual( harness.get_relation_data(peer_rel_id, harness.charm.app), {"stanza": stanza_name}, ) - TestCase().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.unit), {}) - TestCase().assertEqual( - harness.get_relation_data(peer_rel_id, new_unit), {"stanza": stanza_name} - ) + tc.assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.unit), {}) + tc.assertEqual(harness.get_relation_data(peer_rel_id, new_unit), {"stanza": stanza_name}) def test_is_primary_pgbackrest_service_running(harness): @@ -904,20 +899,20 @@ def test_is_primary_pgbackrest_service_running(harness): # Test when the pgBackRest fails to contact the primary server. _get_primary.side_effect = None _execute_command.return_value = (1, "", "fake stderr") - TestCase().assertFalse(harness.charm.backup._is_primary_pgbackrest_service_running) + tc.assertFalse(harness.charm.backup._is_primary_pgbackrest_service_running) _execute_command.assert_called_once() # Test when the endpoint is not generated. _execute_command.reset_mock() _primary_endpoint.return_value = None - TestCase().assertFalse(harness.charm.backup._is_primary_pgbackrest_service_running) + tc.assertFalse(harness.charm.backup._is_primary_pgbackrest_service_running) _execute_command.assert_not_called() # Test when the pgBackRest succeeds on contacting the primary server. _execute_command.reset_mock() _execute_command.return_value = (0, "fake stdout", "") _primary_endpoint.return_value = "fake_endpoint" - TestCase().assertTrue(harness.charm.backup._is_primary_pgbackrest_service_running) + tc.assertTrue(harness.charm.backup._is_primary_pgbackrest_service_running) _execute_command.assert_called_once() @@ -984,7 +979,7 @@ def test_on_s3_credential_changed(harness): ) _render_pgbackrest_conf_file.assert_called_once() _create_bucket_if_not_exists.assert_not_called() - TestCase().assertIsInstance(harness.charm.unit.status, ActiveStatus) + tc.assertIsInstance(harness.charm.unit.status, ActiveStatus) _can_use_s3_repository.assert_not_called() _initialise_stanza.assert_not_called() @@ -1006,8 +1001,8 @@ def test_on_s3_credential_changed(harness): ) _render_pgbackrest_conf_file.assert_called_once() _create_bucket_if_not_exists.assert_called_once() - TestCase().assertIsInstance(harness.charm.unit.status, BlockedStatus) - TestCase().assertEqual( + tc.assertIsInstance(harness.charm.unit.status, BlockedStatus) + tc.assertEqual( harness.charm.unit.status.message, FAILED_TO_ACCESS_CREATE_BUCKET_ERROR_MESSAGE ) _can_use_s3_repository.assert_not_called() @@ -1020,8 +1015,8 @@ def test_on_s3_credential_changed(harness): harness.charm.backup.s3_client.on.credentials_changed.emit( relation=harness.model.get_relation(S3_PARAMETERS_RELATION, s3_rel_id) ) - TestCase().assertIsInstance(harness.charm.unit.status, BlockedStatus) - TestCase().assertEqual(harness.charm.unit.status.message, "fake validation message") + tc.assertIsInstance(harness.charm.unit.status, BlockedStatus) + tc.assertEqual(harness.charm.unit.status.message, "fake validation message") _create_bucket_if_not_exists.assert_called_once() _can_use_s3_repository.assert_called_once() _initialise_stanza.assert_not_called() @@ -1041,12 +1036,12 @@ def test_on_s3_credential_gone(harness): # Test that unrelated blocks will remain harness.charm.unit.status = BlockedStatus("test block") harness.charm.backup._on_s3_credential_gone(None) - TestCase().assertIsInstance(harness.charm.unit.status, BlockedStatus) + tc.assertIsInstance(harness.charm.unit.status, BlockedStatus) # Test that s3 related blocks will be cleared harness.charm.unit.status = BlockedStatus(ANOTHER_CLUSTER_REPOSITORY_ERROR_MESSAGE) harness.charm.backup._on_s3_credential_gone(None) - TestCase().assertIsInstance(harness.charm.unit.status, ActiveStatus) + tc.assertIsInstance(harness.charm.unit.status, ActiveStatus) # Test removal of relation data when the unit is not the leader. with harness.hooks_disabled(): @@ -1061,11 +1056,11 @@ def test_on_s3_credential_gone(harness): {"stanza": "test-stanza", "init-pgbackrest": "True"}, ) harness.charm.backup._on_s3_credential_gone(None) - TestCase().assertEqual( + tc.assertEqual( harness.get_relation_data(peer_rel_id, harness.charm.app), {"stanza": "test-stanza", "init-pgbackrest": "True"}, ) - TestCase().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.unit), {}) + tc.assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.unit), {}) # Test removal of relation data when the unit is the leader. with harness.hooks_disabled(): @@ -1076,8 +1071,8 @@ def test_on_s3_credential_gone(harness): {"stanza": "test-stanza", "init-pgbackrest": "True"}, ) harness.charm.backup._on_s3_credential_gone(None) - TestCase().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.app), {}) - TestCase().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.unit), {}) + tc.assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.app), {}) + tc.assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.unit), {}) def test_on_create_backup_action(harness): @@ -1217,7 +1212,7 @@ def test_on_create_backup_action(harness): mock_s3_parameters, ), ]) - TestCase().assertEqual(_change_connectivity_to_database.call_count, 2) + tc.assertEqual(_change_connectivity_to_database.call_count, 2) mock_event.fail.assert_not_called() mock_event.set_results.assert_called_once_with({"backup-status": "backup created"}) @@ -1293,7 +1288,7 @@ def test_on_restore_action(harness): _start_patroni.assert_not_called() mock_event.fail.assert_not_called() mock_event.set_results.assert_not_called() - TestCase().assertNotIsInstance(harness.charm.unit.status, MaintenanceStatus) + tc.assertNotIsInstance(harness.charm.unit.status, MaintenanceStatus) # Test when the user provides an invalid backup id. mock_event.params = {"backup-id": "2023-01-01T10:00:00Z"} @@ -1310,7 +1305,7 @@ def test_on_restore_action(harness): _update_config.assert_not_called() _start_patroni.assert_not_called() mock_event.set_results.assert_not_called() - TestCase().assertNotIsInstance(harness.charm.unit.status, MaintenanceStatus) + tc.assertNotIsInstance(harness.charm.unit.status, MaintenanceStatus) # Test when the charm fails to stop the workload. mock_event.reset_mock() @@ -1344,9 +1339,9 @@ def test_on_restore_action(harness): _restart_database.reset_mock() _empty_data_files.return_value = True _execute_command.return_value = (1, "", "fake stderr") - TestCase().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.app), {}) + tc.assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.app), {}) harness.charm.backup._on_restore_action(mock_event) - TestCase().assertEqual( + tc.assertEqual( harness.get_relation_data(peer_rel_id, harness.charm.app), { "restoring-backup": "20230101-090000F", @@ -1388,13 +1383,13 @@ def test_pre_restore_checks(harness): # Test when S3 parameters are not ok. mock_event = MagicMock(params={}) _are_backup_settings_ok.return_value = (False, "fake error message") - TestCase().assertEqual(harness.charm.backup._pre_restore_checks(mock_event), False) + tc.assertEqual(harness.charm.backup._pre_restore_checks(mock_event), False) mock_event.fail.assert_called_once() # Test when no backup id is provided. mock_event.reset_mock() _are_backup_settings_ok.return_value = (True, None) - TestCase().assertEqual(harness.charm.backup._pre_restore_checks(mock_event), False) + tc.assertEqual(harness.charm.backup._pre_restore_checks(mock_event), False) mock_event.fail.assert_called_once() # Test when the unit is in a blocked state that is not recoverable by changing @@ -1402,7 +1397,7 @@ def test_pre_restore_checks(harness): mock_event.reset_mock() mock_event.params = {"backup-id": "2023-01-01T09:00:00Z"} harness.charm.unit.status = BlockedStatus("fake blocked state") - TestCase().assertEqual(harness.charm.backup._pre_restore_checks(mock_event), False) + tc.assertEqual(harness.charm.backup._pre_restore_checks(mock_event), False) mock_event.fail.assert_called_once() # Test when the unit is in a blocked state that is recoverable by changing S3 parameters, @@ -1410,20 +1405,20 @@ def test_pre_restore_checks(harness): mock_event.reset_mock() harness.charm.unit.status = BlockedStatus(ANOTHER_CLUSTER_REPOSITORY_ERROR_MESSAGE) _planned_units.return_value = 2 - TestCase().assertEqual(harness.charm.backup._pre_restore_checks(mock_event), False) + tc.assertEqual(harness.charm.backup._pre_restore_checks(mock_event), False) mock_event.fail.assert_called_once() # Test when the cluster has only one unit, but it's not the leader yet. mock_event.reset_mock() _planned_units.return_value = 1 - TestCase().assertEqual(harness.charm.backup._pre_restore_checks(mock_event), False) + tc.assertEqual(harness.charm.backup._pre_restore_checks(mock_event), False) mock_event.fail.assert_called_once() # Test when everything is ok to run a restore. mock_event.reset_mock() with harness.hooks_disabled(): harness.set_leader() - TestCase().assertEqual(harness.charm.backup._pre_restore_checks(mock_event), True) + tc.assertEqual(harness.charm.backup._pre_restore_checks(mock_event), True) mock_event.fail.assert_not_called() @@ -1489,7 +1484,7 @@ def test_render_pgbackrest_conf_file(harness): harness.charm.backup._render_pgbackrest_conf_file() # Check the template is opened read-only in the call to open. - TestCase().assertEqual(mock.call_args_list[0][0], ("templates/pgbackrest.conf.j2", "r")) + tc.assertEqual(mock.call_args_list[0][0], ("templates/pgbackrest.conf.j2", "r")) # Ensure the correct rendered template is sent to _render_file method. _render_file.assert_called_once_with( @@ -1515,7 +1510,7 @@ def test_restart_database(harness): harness.charm.backup._restart_database() # Assert that the backup id is not in the application relation databag anymore. - TestCase().assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.app), {}) + tc.assertEqual(harness.get_relation_data(peer_rel_id, harness.charm.app), {}) _update_config.assert_called_once() _start_patroni.assert_called_once() @@ -1527,7 +1522,7 @@ def test_retrieve_s3_parameters(harness): ) as _get_s3_connection_info: # Test when there are missing S3 parameters. _get_s3_connection_info.return_value = {} - TestCase().assertEqual( + tc.assertEqual( harness.charm.backup._retrieve_s3_parameters(), ({}, ["bucket", "access-key", "secret-key"]), ) @@ -1538,7 +1533,7 @@ def test_retrieve_s3_parameters(harness): "access-key": "test-access-key", "secret-key": "test-secret-key", } - TestCase().assertEqual( + tc.assertEqual( harness.charm.backup._retrieve_s3_parameters(), ( { @@ -1564,7 +1559,7 @@ def test_retrieve_s3_parameters(harness): "region": " us-east-1 ", "s3-uri-style": " path ", } - TestCase().assertEqual( + tc.assertEqual( harness.charm.backup._retrieve_s3_parameters(), ( { @@ -1607,7 +1602,7 @@ def test_start_stop_pgbackrest_service(harness): restart = MagicMock() stop = MagicMock() _snap_cache.return_value = {"charmed-postgresql": MagicMock(restart=restart, stop=stop)} - TestCase().assertEqual( + tc.assertEqual( harness.charm.backup.start_stop_pgbackrest_service(), True, ) @@ -1617,7 +1612,7 @@ def test_start_stop_pgbackrest_service(harness): # Test when it was not possible to render the pgBackRest configuration file. _are_backup_settings_ok.return_value = (True, None) _render_pgbackrest_conf_file.return_value = False - TestCase().assertEqual( + tc.assertEqual( harness.charm.backup.start_stop_pgbackrest_service(), False, ) @@ -1627,7 +1622,7 @@ def test_start_stop_pgbackrest_service(harness): # Test when TLS is not enabled (should stop the service). _render_pgbackrest_conf_file.return_value = True _is_tls_enabled.return_value = False - TestCase().assertEqual( + tc.assertEqual( harness.charm.backup.start_stop_pgbackrest_service(), True, ) @@ -1638,7 +1633,7 @@ def test_start_stop_pgbackrest_service(harness): stop.reset_mock() _is_tls_enabled.return_value = True _peer_members_ips.return_value = [] - TestCase().assertEqual( + tc.assertEqual( harness.charm.backup.start_stop_pgbackrest_service(), True, ) @@ -1650,7 +1645,7 @@ def test_start_stop_pgbackrest_service(harness): _peer_members_ips.return_value = ["1.1.1.1"] _is_primary.return_value = False _is_primary_pgbackrest_service_running.return_value = False - TestCase().assertEqual( + tc.assertEqual( harness.charm.backup.start_stop_pgbackrest_service(), False, ) @@ -1659,7 +1654,7 @@ def test_start_stop_pgbackrest_service(harness): # Test when the service has already started in the primary. _is_primary_pgbackrest_service_running.return_value = True - TestCase().assertEqual( + tc.assertEqual( harness.charm.backup.start_stop_pgbackrest_service(), True, ) @@ -1670,7 +1665,7 @@ def test_start_stop_pgbackrest_service(harness): restart.reset_mock() _is_primary.return_value = True _is_primary_pgbackrest_service_running.return_value = False - TestCase().assertEqual( + tc.assertEqual( harness.charm.backup.start_stop_pgbackrest_service(), True, ) @@ -1701,7 +1696,7 @@ def test_upload_content_to_s3(harness): _resource.side_effect = ValueError _construct_endpoint.return_value = "https://s3.us-east-1.amazonaws.com" _named_temporary_file.return_value.__enter__.return_value.name = "/tmp/test-file" - TestCase().assertEqual( + tc.assertEqual( harness.charm.backup._upload_content_to_s3(content, s3_path, s3_parameters), False, ) @@ -1712,7 +1707,7 @@ def test_upload_content_to_s3(harness): _resource.reset_mock() _resource.side_effect = None upload_file.side_effect = S3UploadFailedError - TestCase().assertEqual( + tc.assertEqual( harness.charm.backup._upload_content_to_s3(content, s3_path, s3_parameters), False, ) @@ -1725,7 +1720,7 @@ def test_upload_content_to_s3(harness): _named_temporary_file.reset_mock() upload_file.reset_mock() upload_file.side_effect = None - TestCase().assertEqual( + tc.assertEqual( harness.charm.backup._upload_content_to_s3(content, s3_path, s3_parameters), True, ) diff --git a/tests/unit/test_cluster.py b/tests/unit/test_cluster.py index dca853f6b5..6c5f227af6 100644 --- a/tests/unit/test_cluster.py +++ b/tests/unit/test_cluster.py @@ -23,6 +23,9 @@ PATRONI_SERVICE = "patroni" CREATE_CLUSTER_CONF_PATH = "/var/snap/charmed-postgresql/current/etc/postgresql/postgresql.conf" +# used for assert functions +tc = TestCase() + # This method will be used by the mock to replace requests.get def mocked_requests_get(*args, **kwargs): @@ -75,13 +78,13 @@ def test_get_alternative_patroni_url(peers_ips, patroni): # Test the first URL that is returned (it should have the current unit IP). url = patroni._get_alternative_patroni_url(attempt) - TestCase().assertEqual(url, f"http://{patroni.unit_ip}:8008") + tc.assertEqual(url, f"http://{patroni.unit_ip}:8008") # Test returning the other servers URLs. for attempt_number in range(attempt.retry_state.attempt_number + 1, len(peers_ips) + 2): attempt.retry_state.attempt_number = attempt_number url = patroni._get_alternative_patroni_url(attempt) - TestCase().assertIn(url.split("http://")[1].split(":8008")[0], peers_ips) + tc.assertIn(url.split("http://")[1].split(":8008")[0], peers_ips) def test_get_member_ip(peers_ips, patroni): @@ -91,7 +94,7 @@ def test_get_member_ip(peers_ips, patroni): ): # Test error on trying to get the member IP. _get_alternative_patroni_url.side_effect = "http://server2" - with TestCase().assertRaises(tenacity.RetryError): + with tc.assertRaises(tenacity.RetryError): patroni.get_member_ip(patroni.member_name) # Test using an alternative Patroni URL. @@ -101,17 +104,17 @@ def test_get_member_ip(peers_ips, patroni): "http://server1", ] ip = patroni.get_member_ip(patroni.member_name) - TestCase().assertEqual(ip, "1.1.1.1") + tc.assertEqual(ip, "1.1.1.1") # Test using the current Patroni URL. _get_alternative_patroni_url.side_effect = ["http://server1"] ip = patroni.get_member_ip(patroni.member_name) - TestCase().assertEqual(ip, "1.1.1.1") + tc.assertEqual(ip, "1.1.1.1") # Test when not having that specific member in the cluster. _get_alternative_patroni_url.side_effect = ["http://server1"] ip = patroni.get_member_ip("other-member-name") - TestCase().assertIsNone(ip) + tc.assertIsNone(ip) def test_get_postgresql_version(peers_ips, patroni): @@ -124,7 +127,7 @@ def test_get_postgresql_version(peers_ips, patroni): ] version = patroni.get_postgresql_version() - TestCase().assertEqual(version, "14.0") + tc.assertEqual(version, "14.0") _snap_client.assert_called_once_with() _get_installed_snaps.assert_called_once_with() @@ -136,7 +139,7 @@ def test_get_primary(peers_ips, patroni): ): # Test error on trying to get the member IP. _get_alternative_patroni_url.side_effect = "http://server2" - with TestCase().assertRaises(tenacity.RetryError): + with tc.assertRaises(tenacity.RetryError): patroni.get_primary(patroni.member_name) # Test using an alternative Patroni URL. @@ -146,17 +149,17 @@ def test_get_primary(peers_ips, patroni): "http://server1", ] primary = patroni.get_primary() - TestCase().assertEqual(primary, "postgresql-0") + tc.assertEqual(primary, "postgresql-0") # Test using the current Patroni URL. _get_alternative_patroni_url.side_effect = ["http://server1"] primary = patroni.get_primary() - TestCase().assertEqual(primary, "postgresql-0") + tc.assertEqual(primary, "postgresql-0") # Test requesting the primary in the unit name pattern. _get_alternative_patroni_url.side_effect = ["http://server1"] primary = patroni.get_primary(unit_name_pattern=True) - TestCase().assertEqual(primary, "postgresql/0") + tc.assertEqual(primary, "postgresql/0") def test_is_creating_backup(peers_ips, patroni): @@ -169,13 +172,13 @@ def test_is_creating_backup(peers_ips, patroni): {"name": "postgresql-1", "tags": {"is_creating_backup": True}}, ] } - TestCase().assertTrue(patroni.is_creating_backup) + tc.assertTrue(patroni.is_creating_backup) # Test when no member is creating a backup. response.json.return_value = { "members": [{"name": "postgresql-0"}, {"name": "postgresql-1"}] } - TestCase().assertFalse(patroni.is_creating_backup) + tc.assertFalse(patroni.is_creating_backup) def test_is_replication_healthy(peers_ips, patroni): @@ -186,7 +189,7 @@ def test_is_replication_healthy(peers_ips, patroni): ): # Test when replication is healthy. _get.return_value.status_code = 200 - TestCase().assertTrue(patroni.is_replication_healthy) + tc.assertTrue(patroni.is_replication_healthy) # Test when replication is not healthy. _get.side_effect = [ @@ -194,7 +197,7 @@ def test_is_replication_healthy(peers_ips, patroni): MagicMock(status_code=200), MagicMock(status_code=503), ] - TestCase().assertFalse(patroni.is_replication_healthy) + tc.assertFalse(patroni.is_replication_healthy) def test_is_member_isolated(peers_ips, patroni): @@ -206,15 +209,15 @@ def test_is_member_isolated(peers_ips, patroni): ): # Test when it wasn't possible to connect to the Patroni API. _patroni_url.return_value = "http://server3" - TestCase().assertFalse(patroni.is_member_isolated) + tc.assertFalse(patroni.is_member_isolated) # Test when the member isn't isolated from the cluster. _patroni_url.return_value = "http://server1" - TestCase().assertFalse(patroni.is_member_isolated) + tc.assertFalse(patroni.is_member_isolated) # Test when the member is isolated from the cluster. _patroni_url.return_value = "http://server4" - TestCase().assertTrue(patroni.is_member_isolated) + tc.assertTrue(patroni.is_member_isolated) def test_render_file(peers_ips, patroni): @@ -238,7 +241,7 @@ def test_render_file(peers_ips, patroni): patroni.render_file(filename, "rendered-content", 0o640) # Check the rendered file is opened with "w+" mode. - TestCase().assertEqual(mock.call_args_list[0][0], (filename, "w+")) + tc.assertEqual(mock.call_args_list[0][0], (filename, "w+")) # Ensure that the correct user is lookup up. _pwnam.assert_called_with("snap_daemon") # Ensure the file is chmod'd correctly. @@ -294,7 +297,7 @@ def test_render_patroni_yml_file(peers_ips, patroni): patroni.render_patroni_yml_file() # Check the template is opened read-only in the call to open. - TestCase().assertEqual(mock.call_args_list[0][0], ("templates/patroni.yml.j2", "r")) + tc.assertEqual(mock.call_args_list[0][0], ("templates/patroni.yml.j2", "r")) # Ensure the correct rendered template is sent to _render_file method. _render_file.assert_called_once_with( "/var/snap/charmed-postgresql/current/etc/patroni/patroni.yaml", diff --git a/tests/unit/test_db.py b/tests/unit/test_db.py index 364bd12b30..b914db6841 100644 --- a/tests/unit/test_db.py +++ b/tests/unit/test_db.py @@ -22,6 +22,9 @@ RELATION_NAME = "db" POSTGRESQL_VERSION = "12" +# used for assert functions +tc = TestCase() + @pytest.fixture(autouse=True) def harness(): @@ -118,7 +121,7 @@ def test_on_relation_changed(harness): # Request a database before primary endpoint is available. request_database(harness) - TestCase().assertEqual(_defer.call_count, 2) + tc.assertEqual(_defer.call_count, 2) _set_up_relation.assert_not_called() # Request it again when the database is ready. @@ -133,7 +136,7 @@ def test_get_extensions(harness): # Test when there are no extensions in the relation databags. rel_id = harness.model.get_relation(RELATION_NAME).id relation = harness.model.get_relation(RELATION_NAME, rel_id) - TestCase().assertEqual(harness.charm.legacy_db_relation._get_extensions(relation), ([], set())) + tc.assertEqual(harness.charm.legacy_db_relation._get_extensions(relation), ([], set())) # Test when there are extensions in the application relation databag. extensions = ["", "citext:public", "debversion"] @@ -143,7 +146,7 @@ def test_get_extensions(harness): "application", {"extensions": ",".join(extensions)}, ) - TestCase().assertEqual( + tc.assertEqual( harness.charm.legacy_db_relation._get_extensions(relation), ([extensions[1], extensions[2]], {extensions[1].split(":")[0], extensions[2]}), ) @@ -160,7 +163,7 @@ def test_get_extensions(harness): "application/0", {"extensions": ",".join(extensions)}, ) - TestCase().assertEqual( + tc.assertEqual( harness.charm.legacy_db_relation._get_extensions(relation), ([extensions[1], extensions[2]], {extensions[1].split(":")[0], extensions[2]}), ) @@ -176,7 +179,7 @@ def test_get_extensions(harness): harness = Harness(PostgresqlOperatorCharm, config=config) harness.cleanup() harness.begin() - TestCase().assertEqual( + tc.assertEqual( harness.charm.legacy_db_relation._get_extensions(relation), ([extensions[1], extensions[2]], {extensions[2]}), ) @@ -214,7 +217,7 @@ def test_set_up_relation(harness): # Assert no operation is done when at least one of the requested extensions # is disabled. relation = harness.model.get_relation(RELATION_NAME, rel_id) - TestCase().assertFalse(harness.charm.legacy_db_relation.set_up_relation(relation)) + tc.assertFalse(harness.charm.legacy_db_relation.set_up_relation(relation)) postgresql_mock.create_user.assert_not_called() postgresql_mock.create_database.assert_not_called() postgresql_mock.get_postgresql_version.assert_not_called() @@ -229,7 +232,7 @@ def test_set_up_relation(harness): "application", {"database": DATABASE}, ) - TestCase().assertTrue(harness.charm.legacy_db_relation.set_up_relation(relation)) + tc.assertTrue(harness.charm.legacy_db_relation.set_up_relation(relation)) user = f"relation-{rel_id}" postgresql_mock.create_user.assert_called_once_with(user, "test-password", False) postgresql_mock.create_database.assert_called_once_with( @@ -237,7 +240,7 @@ def test_set_up_relation(harness): ) _update_endpoints.assert_called_once() _update_unit_status.assert_called_once() - TestCase().assertNotIsInstance(harness.model.unit.status, BlockedStatus) + tc.assertNotIsInstance(harness.model.unit.status, BlockedStatus) # Assert that the correct calls were made when the database name is not # provided in both application and unit databags. @@ -257,14 +260,14 @@ def test_set_up_relation(harness): "application/0", {"database": DATABASE}, ) - TestCase().assertTrue(harness.charm.legacy_db_relation.set_up_relation(relation)) + tc.assertTrue(harness.charm.legacy_db_relation.set_up_relation(relation)) postgresql_mock.create_user.assert_called_once_with(user, "test-password", False) postgresql_mock.create_database.assert_called_once_with( DATABASE, user, plugins=[], client_relations=[relation] ) _update_endpoints.assert_called_once() _update_unit_status.assert_called_once() - TestCase().assertNotIsInstance(harness.model.unit.status, BlockedStatus) + tc.assertNotIsInstance(harness.model.unit.status, BlockedStatus) # Assert that the correct calls were made when the database name is not provided. postgresql_mock.create_user.reset_mock() @@ -278,32 +281,32 @@ def test_set_up_relation(harness): "application/0", {"database": ""}, ) - TestCase().assertTrue(harness.charm.legacy_db_relation.set_up_relation(relation)) + tc.assertTrue(harness.charm.legacy_db_relation.set_up_relation(relation)) postgresql_mock.create_user.assert_called_once_with(user, "test-password", False) postgresql_mock.create_database.assert_called_once_with( "application", user, plugins=[], client_relations=[relation] ) _update_endpoints.assert_called_once() _update_unit_status.assert_called_once() - TestCase().assertNotIsInstance(harness.model.unit.status, BlockedStatus) + tc.assertNotIsInstance(harness.model.unit.status, BlockedStatus) # BlockedStatus due to a PostgreSQLCreateUserError. postgresql_mock.create_database.reset_mock() postgresql_mock.get_postgresql_version.reset_mock() _update_endpoints.reset_mock() _update_unit_status.reset_mock() - TestCase().assertFalse(harness.charm.legacy_db_relation.set_up_relation(relation)) + tc.assertFalse(harness.charm.legacy_db_relation.set_up_relation(relation)) postgresql_mock.create_database.assert_not_called() _update_endpoints.assert_not_called() _update_unit_status.assert_not_called() - TestCase().assertIsInstance(harness.model.unit.status, BlockedStatus) + tc.assertIsInstance(harness.model.unit.status, BlockedStatus) # BlockedStatus due to a PostgreSQLCreateDatabaseError. harness.charm.unit.status = ActiveStatus() - TestCase().assertFalse(harness.charm.legacy_db_relation.set_up_relation(relation)) + tc.assertFalse(harness.charm.legacy_db_relation.set_up_relation(relation)) _update_endpoints.assert_not_called() _update_unit_status.assert_not_called() - TestCase().assertIsInstance(harness.model.unit.status, BlockedStatus) + tc.assertIsInstance(harness.model.unit.status, BlockedStatus) @patch_network_get(private_address="1.1.1.1") @@ -322,14 +325,14 @@ def test_update_unit_status(harness): _is_blocked.return_value = False harness.charm.legacy_db_relation._update_unit_status(relation) _check_for_blocking_relations.assert_not_called() - TestCase().assertNotIsInstance(harness.charm.unit.status, ActiveStatus) + tc.assertNotIsInstance(harness.charm.unit.status, ActiveStatus) # Test when the charm is blocked but not due to extensions request. _is_blocked.return_value = True harness.charm.unit.status = BlockedStatus("fake message") harness.charm.legacy_db_relation._update_unit_status(relation) _check_for_blocking_relations.assert_not_called() - TestCase().assertNotIsInstance(harness.charm.unit.status, ActiveStatus) + tc.assertNotIsInstance(harness.charm.unit.status, ActiveStatus) # Test when there are relations causing the blocked status. harness.charm.unit.status = BlockedStatus( @@ -338,14 +341,14 @@ def test_update_unit_status(harness): _check_for_blocking_relations.return_value = True harness.charm.legacy_db_relation._update_unit_status(relation) _check_for_blocking_relations.assert_called_once_with(relation.id) - TestCase().assertNotIsInstance(harness.charm.unit.status, ActiveStatus) + tc.assertNotIsInstance(harness.charm.unit.status, ActiveStatus) # Test when there are no relations causing the blocked status anymore. _check_for_blocking_relations.reset_mock() _check_for_blocking_relations.return_value = False harness.charm.legacy_db_relation._update_unit_status(relation) _check_for_blocking_relations.assert_called_once_with(relation.id) - TestCase().assertIsInstance(harness.charm.unit.status, ActiveStatus) + tc.assertIsInstance(harness.charm.unit.status, ActiveStatus) @patch_network_get(private_address="1.1.1.1") @@ -378,7 +381,7 @@ def test_on_relation_broken_extensions_unblock(harness): # Break the relation that blocked the charm. harness.remove_relation(rel_id) - TestCase().assertTrue(isinstance(harness.model.unit.status, ActiveStatus)) + tc.assertTrue(isinstance(harness.model.unit.status, ActiveStatus)) @patch_network_get(private_address="1.1.1.1") @@ -419,7 +422,7 @@ def test_on_relation_broken_extensions_keep_block(harness): event.relation.id = first_rel_id # Break one of the relations that block the charm. harness.charm.legacy_db_relation._on_relation_broken(event) - TestCase().assertTrue(isinstance(harness.model.unit.status, BlockedStatus)) + tc.assertTrue(isinstance(harness.model.unit.status, BlockedStatus)) @patch_network_get(private_address="1.1.1.1") @@ -494,8 +497,8 @@ def test_update_endpoints_with_relation(harness): # BlockedStatus due to a PostgreSQLGetPostgreSQLVersionError. harness.charm.legacy_db_relation.update_endpoints(relation) - TestCase().assertIsInstance(harness.model.unit.status, BlockedStatus) - TestCase().assertEqual(harness.get_relation_data(rel_id, harness.charm.unit.name), {}) + tc.assertIsInstance(harness.model.unit.status, BlockedStatus) + tc.assertEqual(harness.get_relation_data(rel_id, harness.charm.unit.name), {}) # Test with both a primary and a replica. # Update the endpoints with the event and check that it updated only @@ -506,9 +509,7 @@ def test_update_endpoints_with_relation(harness): user = f"relation-{rel}" # Set the assert function based on each relation (whether it should have data). - assert_based_on_relation = ( - TestCase().assertTrue if rel == rel_id else TestCase().assertFalse - ) + assert_based_on_relation = tc.assertTrue if rel == rel_id else tc.assertFalse # Check that the unit relation databag contains (or not) the endpoints. unit_relation_data = harness.get_relation_data(rel, harness.charm.unit.name) @@ -527,9 +528,7 @@ def test_update_endpoints_with_relation(harness): user = f"relation-{rel}" # Set the assert function based on each relation (whether it should have data). - assert_based_on_relation = ( - TestCase().assertTrue if rel == rel_id else TestCase().assertFalse - ) + assert_based_on_relation = tc.assertTrue if rel == rel_id else tc.assertFalse # Check that the unit relation databag contains the endpoints. unit_relation_data = harness.get_relation_data(rel, harness.charm.unit.name) @@ -611,8 +610,8 @@ def test_update_endpoints_without_relation(harness): # BlockedStatus due to a PostgreSQLGetPostgreSQLVersionError. harness.charm.legacy_db_relation.update_endpoints() - TestCase().assertIsInstance(harness.model.unit.status, BlockedStatus) - TestCase().assertEqual(harness.get_relation_data(rel_id, harness.charm.unit.name), {}) + tc.assertIsInstance(harness.model.unit.status, BlockedStatus) + tc.assertEqual(harness.get_relation_data(rel_id, harness.charm.unit.name), {}) # Test with both a primary and a replica. # Update the endpoints and check that all relations' databags are updated. @@ -623,10 +622,10 @@ def test_update_endpoints_without_relation(harness): # Check that the unit relation databag contains the endpoints. unit_relation_data = harness.get_relation_data(rel, harness.charm.unit.name) - TestCase().assertTrue( + tc.assertTrue( "master" in unit_relation_data and master + user == unit_relation_data["master"] ) - TestCase().assertTrue( + tc.assertTrue( "standbys" in unit_relation_data and standbys + user == unit_relation_data["standbys"] ) @@ -639,10 +638,10 @@ def test_update_endpoints_without_relation(harness): # Check that the unit relation databag contains the endpoints. unit_relation_data = harness.get_relation_data(rel, harness.charm.unit.name) - TestCase().assertTrue( + tc.assertTrue( "master" in unit_relation_data and master + user == unit_relation_data["master"] ) - TestCase().assertTrue( + tc.assertTrue( "standbys" in unit_relation_data and standbys + user == unit_relation_data["standbys"] ) @@ -654,12 +653,12 @@ def test_get_allowed_units(harness): peer_rel_id = harness.model.get_relation(PEER).id rel_id = harness.model.get_relation(RELATION_NAME).id peer_relation = harness.model.get_relation(PEER, peer_rel_id) - TestCase().assertEqual(harness.charm.legacy_db_relation._get_allowed_units(peer_relation), "") + tc.assertEqual(harness.charm.legacy_db_relation._get_allowed_units(peer_relation), "") # List of space separated allowed units from the other application. harness.add_relation_unit(rel_id, "application/1") db_relation = harness.model.get_relation(RELATION_NAME, rel_id) - TestCase().assertEqual( + tc.assertEqual( harness.charm.legacy_db_relation._get_allowed_units(db_relation), "application/0 application/1", ) diff --git a/tests/unit/test_postgresql_provider.py b/tests/unit/test_postgresql_provider.py index 2edb9b039c..bd8fcec787 100644 --- a/tests/unit/test_postgresql_provider.py +++ b/tests/unit/test_postgresql_provider.py @@ -24,6 +24,9 @@ RELATION_NAME = "database" POSTGRESQL_VERSION = "12" +# used for assert functions +tc = TestCase() + @pytest.fixture(autouse=True) def harness(): @@ -119,7 +122,7 @@ def test_on_database_requested(harness): # Request a database before primary endpoint is available. request_database(harness) - TestCase().assertEqual(_defer.call_count, 2) + tc.assertEqual(_defer.call_count, 2) # Request it again when the database is ready. request_database(harness) @@ -138,7 +141,7 @@ def test_on_database_requested(harness): _update_endpoints.assert_called_once() # Assert that the relation data was updated correctly. - TestCase().assertEqual( + tc.assertEqual( harness.get_relation_data(rel_id, harness.charm.app.name), { "data": f'{{"database": "{DATABASE}", "extra-user-roles": "{EXTRA_USER_ROLES}"}}', @@ -150,13 +153,13 @@ def test_on_database_requested(harness): ) # Assert no BlockedStatus was set. - TestCase().assertFalse(isinstance(harness.model.unit.status, BlockedStatus)) + tc.assertFalse(isinstance(harness.model.unit.status, BlockedStatus)) # BlockedStatus due to a PostgreSQLCreateUserError. request_database(harness) - TestCase().assertTrue(isinstance(harness.model.unit.status, BlockedStatus)) + tc.assertTrue(isinstance(harness.model.unit.status, BlockedStatus)) # No data is set in the databag by the database. - TestCase().assertEqual( + tc.assertEqual( harness.get_relation_data(rel_id, harness.charm.app.name), { "data": f'{{"database": "{DATABASE}", "extra-user-roles": "{EXTRA_USER_ROLES}"}}', @@ -165,9 +168,9 @@ def test_on_database_requested(harness): # BlockedStatus due to a PostgreSQLCreateDatabaseError. request_database(harness) - TestCase().assertTrue(isinstance(harness.model.unit.status, BlockedStatus)) + tc.assertTrue(isinstance(harness.model.unit.status, BlockedStatus)) # No data is set in the databag by the database. - TestCase().assertEqual( + tc.assertEqual( harness.get_relation_data(rel_id, harness.charm.app.name), { "data": f'{{"database": "{DATABASE}", "extra-user-roles": "{EXTRA_USER_ROLES}"}}', @@ -176,7 +179,7 @@ def test_on_database_requested(harness): # BlockedStatus due to a PostgreSQLGetPostgreSQLVersionError. request_database(harness) - TestCase().assertTrue(isinstance(harness.model.unit.status, BlockedStatus)) + tc.assertTrue(isinstance(harness.model.unit.status, BlockedStatus)) @patch_network_get(private_address="1.1.1.1") @@ -251,22 +254,22 @@ def test_update_endpoints_with_event(harness): # Update the endpoints with the event and check that it updated # only the right relation databag (the one from the event). harness.charm.postgresql_client_relation.update_endpoints(mock_event) - TestCase().assertEqual( + tc.assertEqual( harness.get_relation_data(rel_id, harness.charm.app.name), {"endpoints": "1.1.1.1:5432", "read-only-endpoints": "2.2.2.2:5432"}, ) - TestCase().assertEqual( + tc.assertEqual( harness.get_relation_data(another_rel_id, harness.charm.app.name), {}, ) # Also test with only a primary instance. harness.charm.postgresql_client_relation.update_endpoints(mock_event) - TestCase().assertEqual( + tc.assertEqual( harness.get_relation_data(rel_id, harness.charm.app.name), {"endpoints": "1.1.1.1:5432"}, ) - TestCase().assertEqual( + tc.assertEqual( harness.get_relation_data(another_rel_id, harness.charm.app.name), {}, ) @@ -297,22 +300,22 @@ def test_update_endpoints_without_event(harness): # Test with both a primary and a replica. # Update the endpoints and check that all relations' databags are updated. harness.charm.postgresql_client_relation.update_endpoints() - TestCase().assertEqual( + tc.assertEqual( harness.get_relation_data(rel_id, harness.charm.app.name), {"endpoints": "1.1.1.1:5432", "read-only-endpoints": "2.2.2.2:5432"}, ) - TestCase().assertEqual( + tc.assertEqual( harness.get_relation_data(another_rel_id, harness.charm.app.name), {"endpoints": "1.1.1.1:5432", "read-only-endpoints": "2.2.2.2:5432"}, ) # Also test with only a primary instance. harness.charm.postgresql_client_relation.update_endpoints() - TestCase().assertEqual( + tc.assertEqual( harness.get_relation_data(rel_id, harness.charm.app.name), {"endpoints": "1.1.1.1:5432"}, ) - TestCase().assertEqual( + tc.assertEqual( harness.get_relation_data(another_rel_id, harness.charm.app.name), {"endpoints": "1.1.1.1:5432"}, ) diff --git a/tests/unit/test_upgrade.py b/tests/unit/test_upgrade.py index 0a82d4b00b..b6489e29f2 100644 --- a/tests/unit/test_upgrade.py +++ b/tests/unit/test_upgrade.py @@ -12,6 +12,9 @@ from constants import SNAP_PACKAGES from tests.helpers import patch_network_get +# used for assert functions +tc = TestCase() + @pytest.fixture(autouse=True) def harness(): @@ -42,8 +45,8 @@ def test_build_upgrade_stack(harness): for rel_id in (upgrade_relation_id, peer_relation_id): harness.add_relation_unit(rel_id, "postgresql/2") - TestCase().assertEqual(harness.charm.upgrade.build_upgrade_stack(), [0, 1, 2]) - TestCase().assertEqual(harness.charm.upgrade.build_upgrade_stack(), [1, 2, 0]) + tc.assertEqual(harness.charm.upgrade.build_upgrade_stack(), [0, 1, 2]) + tc.assertEqual(harness.charm.upgrade.build_upgrade_stack(), [1, 2, 0]) def test_log_rollback(harness): @@ -101,7 +104,7 @@ def test_on_upgrade_granted(harness): _start_patroni.return_value = True _member_started.return_value = False harness.charm.upgrade._on_upgrade_granted(mock_event) - TestCase().assertEqual(_member_started.call_count, 6) + tc.assertEqual(_member_started.call_count, 6) _cluster_members.assert_not_called() mock_event.defer.assert_called_once() _set_unit_completed.assert_not_called() @@ -114,8 +117,8 @@ def test_on_upgrade_granted(harness): _member_started.return_value = True _cluster_members.return_value = ["postgresql-1"] harness.charm.upgrade._on_upgrade_granted(mock_event) - TestCase().assertEqual(_member_started.call_count, 6) - TestCase().assertEqual(_cluster_members.call_count, 6) + tc.assertEqual(_member_started.call_count, 6) + tc.assertEqual(_cluster_members.call_count, 6) mock_event.defer.assert_called_once() _set_unit_completed.assert_not_called() _set_unit_failed.assert_not_called() @@ -179,11 +182,11 @@ def test_pre_upgrade_check(harness): _is_creating_backup.side_effect = [True, False, False] # Test when not all members are ready. - with TestCase().assertRaises(ClusterNotReadyError): + with tc.assertRaises(ClusterNotReadyError): harness.charm.upgrade.pre_upgrade_check() # Test when a backup is being created. - with TestCase().assertRaises(ClusterNotReadyError): + with tc.assertRaises(ClusterNotReadyError): harness.charm.upgrade.pre_upgrade_check() # Test when everything is ok to start the upgrade. diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 3e2f26b183..1080e659e6 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -6,14 +6,17 @@ from utils import new_password +# used for assert functions +tc = TestCase() + def test_new_password(): # Test the password generation twice in order to check if we get different passwords and # that they meet the required criteria. first_password = new_password() - TestCase().assertEqual(len(first_password), 16) - TestCase().assertIsNotNone(re.fullmatch("[a-zA-Z0-9\b]{16}$", first_password)) + tc.assertEqual(len(first_password), 16) + tc.assertIsNotNone(re.fullmatch("[a-zA-Z0-9\b]{16}$", first_password)) second_password = new_password() - TestCase().assertIsNotNone(re.fullmatch("[a-zA-Z0-9\b]{16}$", second_password)) - TestCase().assertNotEqual(second_password, first_password) + tc.assertIsNotNone(re.fullmatch("[a-zA-Z0-9\b]{16}$", second_password)) + tc.assertNotEqual(second_password, first_password)