From 209efff9e7fa8f8a4f139579de72abafb68c3fe2 Mon Sep 17 00:00:00 2001 From: Gere_X Date: Sat, 5 Jul 2025 16:32:29 +0200 Subject: [PATCH 01/35] def test_can_promote_cluster(): --- tests/unit/test_async_replication.py | 80 ++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 tests/unit/test_async_replication.py diff --git a/tests/unit/test_async_replication.py b/tests/unit/test_async_replication.py new file mode 100644 index 0000000000..66a5cfaefc --- /dev/null +++ b/tests/unit/test_async_replication.py @@ -0,0 +1,80 @@ +from unittest.mock import MagicMock, PropertyMock, patch +from src.relations.async_replication import PostgreSQLAsyncReplication +from src.relations.async_replication import READ_ONLY_MODE_BLOCKING_MESSAGE +from src.relations.async_replication import StandbyClusterAlreadyPromotedError + +def test_can_promote_cluster(): + """Tests all conditions in _can_promote_cluster""" + + # 1. Test when cluster is not initialized + mock_charm = MagicMock() + mock_event = MagicMock() + type(mock_charm).is_cluster_initialised = PropertyMock(return_value=False) + + with patch.object(PostgreSQLAsyncReplication, '_get_primary_cluster') as mock_get_primary: + relation = PostgreSQLAsyncReplication(mock_charm) + mock_get_primary.return_value = (MagicMock(), "0") + + assert relation._can_promote_cluster(mock_event) is False + mock_event.fail.assert_called_with("Cluster not initialised yet.") + + # 2. Test when cluster is initialized but no relation exists + mock_charm = MagicMock() + mock_event = MagicMock() + type(mock_charm).is_cluster_initialised = PropertyMock(return_value=True) + + # Create fresh mocks for this test case + mock_peers_data = MagicMock() + mock_peers_data.update = MagicMock() + + with patch.multiple(PostgreSQLAsyncReplication, + _relation=None, + _get_primary_cluster=MagicMock(), + _set_app_status=MagicMock(), + _handle_forceful_promotion=MagicMock(return_value=False)): + + # Setup test-specific conditions + mock_charm._patroni = MagicMock() + mock_charm._patroni.get_standby_leader.return_value = "standby-leader" + mock_charm._patroni.promote_standby_cluster.return_value = True + mock_charm.app.status.message = READ_ONLY_MODE_BLOCKING_MESSAGE + mock_charm._peers = MagicMock() + mock_charm._peers.data = {mock_charm.app: mock_peers_data} + mock_charm._set_primary_status_message = MagicMock() + + relation = PostgreSQLAsyncReplication(mock_charm) + assert relation._can_promote_cluster(mock_event) is False + + # Verify only the expected calls for this test case + mock_peers_data.update.assert_called_once_with({ + "promoted-cluster-counter": "" + }) + relation._set_app_status.assert_called_once() + mock_charm._set_primary_status_message.assert_called_once() + + # 2b. Test when standby leader exists but promotion fails + mock_charm._patroni.promote_standby_cluster.side_effect = StandbyClusterAlreadyPromotedError("Already promoted") + relation = PostgreSQLAsyncReplication(mock_charm) + assert relation._can_promote_cluster(mock_event) is False + mock_event.fail.assert_called_with("Already promoted") + + # 2c. Test when no standby leader exists + mock_charm._patroni.get_standby_leader.return_value = None + relation = PostgreSQLAsyncReplication(mock_charm) + assert relation._can_promote_cluster(mock_event) is False + mock_event.fail.assert_called_with("No relation and no standby leader found.") + + # 3. Test normal case with relation exists + mock_charm = MagicMock() + mock_event = MagicMock() + type(mock_charm).is_cluster_initialised = PropertyMock(return_value=True) + + with patch.object(PostgreSQLAsyncReplication, '_get_primary_cluster') as mock_get_primary: + # Mock that relation exists + with patch.object(PostgreSQLAsyncReplication, '_relation', new_callable=PropertyMock) as mock_relation: + mock_relation.return_value = MagicMock() # Simulate existing relation + + mock_get_primary.return_value = (MagicMock(), "1") + with patch.object(PostgreSQLAsyncReplication, '_handle_forceful_promotion', return_value=True): + relation = PostgreSQLAsyncReplication(mock_charm) + assert relation._can_promote_cluster(mock_event) is True From 358ad607cccb1ce1098bb4e11ae7665432f0a1d3 Mon Sep 17 00:00:00 2001 From: Gere_X Date: Sat, 5 Jul 2025 17:56:19 +0200 Subject: [PATCH 02/35] test_handle_database_start --- tests/unit/test_async_replication.py | 144 ++++++++++++++++++++++++++- 1 file changed, 140 insertions(+), 4 deletions(-) diff --git a/tests/unit/test_async_replication.py b/tests/unit/test_async_replication.py index 66a5cfaefc..a0b9025a2b 100644 --- a/tests/unit/test_async_replication.py +++ b/tests/unit/test_async_replication.py @@ -1,8 +1,13 @@ +import contextlib from unittest.mock import MagicMock, PropertyMock, patch -from src.relations.async_replication import PostgreSQLAsyncReplication -from src.relations.async_replication import READ_ONLY_MODE_BLOCKING_MESSAGE -from src.relations.async_replication import StandbyClusterAlreadyPromotedError - +import pytest +from ops.model import WaitingStatus +from src.relations.async_replication import ( + PostgreSQLAsyncReplication, + NotReadyError, + READ_ONLY_MODE_BLOCKING_MESSAGE, + StandbyClusterAlreadyPromotedError +) def test_can_promote_cluster(): """Tests all conditions in _can_promote_cluster""" @@ -78,3 +83,134 @@ def test_can_promote_cluster(): with patch.object(PostgreSQLAsyncReplication, '_handle_forceful_promotion', return_value=True): relation = PostgreSQLAsyncReplication(mock_charm) assert relation._can_promote_cluster(mock_event) is True + +def test_handle_database_start(): + """Tests all conditions in _handle_database_start""" + + def create_mock_unit(): + unit = MagicMock() + unit.name = f"unit-{id(unit)}" + return unit + + # 1. Test when database is started (member_started = True) and all units ready + mock_charm = MagicMock() + mock_event = MagicMock() + mock_charm._patroni.member_started = True + mock_charm.unit.is_leader.return_value = True + + # Create mock units + mock_unit1 = create_mock_unit() + mock_unit2 = create_mock_unit() + mock_charm.unit = create_mock_unit() + mock_charm.app = MagicMock() + + # Setup peers data structure with proper keys + mock_peers_data = { + mock_charm.unit: MagicMock(), + mock_unit1: MagicMock(), + mock_unit2: MagicMock(), + mock_charm.app: MagicMock() + } + mock_charm._peers = MagicMock() + mock_charm._peers.data = mock_peers_data + mock_charm._peers.units = [mock_unit1, mock_unit2] + + with patch.object(PostgreSQLAsyncReplication, '_get_highest_promoted_cluster_counter_value', return_value="1"), \ + patch.object(PostgreSQLAsyncReplication, '_is_following_promoted_cluster', return_value=False): + + # Configure all units to have matching counter values + for unit in [mock_unit1, mock_unit2, mock_charm.unit]: + mock_peers_data[unit].get.return_value = "1" + + relation = PostgreSQLAsyncReplication(mock_charm) + relation._handle_database_start(mock_event) + + # Verify updates when all units are ready + mock_peers_data[mock_charm.unit].update.assert_any_call({"stopped": ""}) + mock_peers_data[mock_charm.unit].update.assert_any_call({ + "unit-promoted-cluster-counter": "1" + }) + mock_charm.update_config.assert_called_once() + mock_peers_data[mock_charm.app].update.assert_called_once_with({ + "cluster_initialised": "True" + }) + mock_charm._set_primary_status_message.assert_called_once() + + # 2. Test when not all units are ready (leader case) + mock_charm = MagicMock() + mock_event = MagicMock() + mock_charm._patroni.member_started = True + mock_charm.unit.is_leader.return_value = True + + mock_unit1 = create_mock_unit() + mock_unit2 = create_mock_unit() + mock_charm.unit = create_mock_unit() + mock_charm.app = MagicMock() + + mock_peers_data = { + mock_charm.unit: MagicMock(), + mock_unit1: MagicMock(), + mock_unit2: MagicMock(), + mock_charm.app: MagicMock() + } + mock_charm._peers = MagicMock() + mock_charm._peers.data = mock_peers_data + mock_charm._peers.units = [mock_unit1, mock_unit2] + + with patch.object(PostgreSQLAsyncReplication, '_get_highest_promoted_cluster_counter_value', return_value="1"), \ + patch.object(PostgreSQLAsyncReplication, '_is_following_promoted_cluster', return_value=True): + + # Configure some units to have mismatched counter values + mock_peers_data[mock_charm.unit].get.return_value = "1" + mock_peers_data[mock_unit1].get.return_value = "1" + mock_peers_data[mock_unit2].get.return_value = "0" # Different value + + relation = PostgreSQLAsyncReplication(mock_charm) + relation._handle_database_start(mock_event) + + # Verify waiting status and deferral + assert isinstance(mock_charm.unit.status, WaitingStatus) + mock_event.defer.assert_called_once() + + # 3. Test when database is not started (non-leader case) + mock_charm = MagicMock() + mock_event = MagicMock() + mock_charm._patroni.member_started = False + mock_charm.unit.is_leader.return_value = False + + with patch.object(PostgreSQLAsyncReplication, '_get_highest_promoted_cluster_counter_value'), \ + patch('src.relations.async_replication.contextlib.suppress') as mock_suppress: + + mock_suppress.return_value.__enter__.return_value = None + mock_charm._patroni.reload_patroni_configuration.side_effect = NotReadyError() + + relation = PostgreSQLAsyncReplication(mock_charm) + relation._handle_database_start(mock_event) + + # Verify retry and deferral + mock_charm._patroni.reload_patroni_configuration.assert_called_once() + assert isinstance(mock_charm.unit.status, WaitingStatus) + mock_event.defer.assert_called_once() + + # 4. Test when database is starting (leader case) + mock_charm = MagicMock() + mock_event = MagicMock() + mock_charm._patroni.member_started = False + mock_charm.unit.is_leader.return_value = True + + relation = PostgreSQLAsyncReplication(mock_charm) + relation._handle_database_start(mock_event) + + # Verify waiting status and deferral + assert isinstance(mock_charm.unit.status, WaitingStatus) + mock_event.defer.assert_called_once() + + +def test__on_async_relation_changed(): + pass + +def test_on_secret_changed(): + pass + +def test_stop_database(): + pass \ No newline at end of file From afb09a33dbeb3bba36e5ad159b183f0178e86ce0 Mon Sep 17 00:00:00 2001 From: Gere_X Date: Sat, 5 Jul 2025 18:32:31 +0200 Subject: [PATCH 03/35] _on_async_relation_changed --- tests/unit/test_async_replication.py | 91 +++++++++++++++++++++++++--- 1 file changed, 84 insertions(+), 7 deletions(-) diff --git a/tests/unit/test_async_replication.py b/tests/unit/test_async_replication.py index a0b9025a2b..6733a7bf3c 100644 --- a/tests/unit/test_async_replication.py +++ b/tests/unit/test_async_replication.py @@ -8,6 +8,13 @@ READ_ONLY_MODE_BLOCKING_MESSAGE, StandbyClusterAlreadyPromotedError ) + +def create_mock_unit(name="unit"): + unit = MagicMock() + unit.name = name + return unit + + def test_can_promote_cluster(): """Tests all conditions in _can_promote_cluster""" @@ -86,11 +93,6 @@ def test_can_promote_cluster(): def test_handle_database_start(): """Tests all conditions in _handle_database_start""" - - def create_mock_unit(): - unit = MagicMock() - unit.name = f"unit-{id(unit)}" - return unit # 1. Test when database is started (member_started = True) and all units ready mock_charm = MagicMock() @@ -206,8 +208,83 @@ def create_mock_unit(): mock_event.defer.assert_called_once() -def test__on_async_relation_changed(): - pass +def test_on_async_relation_changed(): + """Tests all conditions in _on_async_relation_changed""" + + mock_charm = MagicMock() + mock_event = MagicMock() + mock_charm.unit.is_leader.return_value = True + mock_charm.unit = create_mock_unit("leader") + mock_charm.app = MagicMock() + mock_unit1 = create_mock_unit("unit1") + mock_unit2 = create_mock_unit("unit2") + mock_charm._peers.units = [mock_unit1, mock_unit2] + mock_charm._peers.data = { + mock_charm.unit: {"stopped": "1"}, + mock_unit1: {"unit-promoted-cluster-counter": "5"}, + mock_unit2: {"unit-promoted-cluster-counter": "5"}, + mock_charm.app: {"promoted-cluster-counter": "5"}, + } + mock_charm.is_unit_stopped = True + + relation = PostgreSQLAsyncReplication(mock_charm) + + + with patch.object(relation, "_get_primary_cluster", return_value=None), \ + patch.object(relation, "_set_app_status") as mock_status: + relation._on_async_relation_changed(mock_event) + mock_status.assert_called_once() + mock_event.defer.assert_not_called() + + + with patch.object(relation, "_get_primary_cluster", return_value="clusterX"), \ + patch.object(relation, "_configure_primary_cluster", return_value=True): + relation._on_async_relation_changed(mock_event) + mock_event.defer.assert_not_called() + + + mock_charm.unit.is_leader.return_value = False + with patch.object(relation, "_get_primary_cluster", return_value="clusterX"), \ + patch.object(relation, "_configure_primary_cluster", return_value=False), \ + patch.object(relation, "_is_following_promoted_cluster", return_value=True): + relation._on_async_relation_changed(mock_event) + mock_event.defer.assert_not_called() + + + mock_charm.unit.is_leader.return_value = True + mock_charm.is_unit_stopped = False + with patch.object(relation, "_get_primary_cluster", return_value="clusterX"), \ + patch.object(relation, "_configure_primary_cluster", return_value=False), \ + patch.object(relation, "_is_following_promoted_cluster", return_value=False), \ + patch.object(relation, "_stop_database", return_value=True), \ + patch.object(relation, "_get_highest_promoted_cluster_counter_value", return_value="5"): + relation._on_async_relation_changed(mock_event) + assert isinstance(mock_charm.unit.status, WaitingStatus) + mock_event.defer.assert_called() + + + mock_charm.is_unit_stopped = True + with patch.object(relation, "_get_primary_cluster", return_value="clusterX"), \ + patch.object(relation, "_configure_primary_cluster", return_value=False), \ + patch.object(relation, "_is_following_promoted_cluster", return_value=False), \ + patch.object(relation, "_stop_database", return_value=True), \ + patch.object(relation, "_get_highest_promoted_cluster_counter_value", return_value="5"), \ + patch.object(relation, "_wait_for_standby_leader", return_value=True): + relation._on_async_relation_changed(mock_event) + + mock_charm._patroni.start_patroni.assert_not_called() + + with patch.object(relation, "_get_primary_cluster", return_value="clusterX"), \ + patch.object(relation, "_configure_primary_cluster", return_value=False), \ + patch.object(relation, "_is_following_promoted_cluster", return_value=False), \ + patch.object(relation, "_stop_database", return_value=True), \ + patch.object(relation, "_get_highest_promoted_cluster_counter_value", return_value="5"), \ + patch.object(relation, "_wait_for_standby_leader", return_value=False), \ + patch.object(mock_charm._patroni, "start_patroni", return_value=True), \ + patch.object(relation, "_handle_database_start") as mock_handle_start: + relation._on_async_relation_changed(mock_event) + mock_charm.update_config.assert_called_once() + mock_handle_start.assert_called_once_with(mock_event) def test_on_secret_changed(): pass From 7c83eaa1247cdcf7645f054c0e993bd9a493f06e Mon Sep 17 00:00:00 2001 From: Gere_X Date: Sun, 6 Jul 2025 11:31:10 +0200 Subject: [PATCH 04/35] test_on_secret_changed first if --- tests/unit/test_async_replication.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_async_replication.py b/tests/unit/test_async_replication.py index 6733a7bf3c..c1c57dbf3a 100644 --- a/tests/unit/test_async_replication.py +++ b/tests/unit/test_async_replication.py @@ -9,6 +9,11 @@ StandbyClusterAlreadyPromotedError ) +REPLICATION_OFFER_RELATION = "replication-offer" +REPLICATION_CONSUMER_RELATION = "replication-consumer" +PEER = "peer" +SECRET_LABEL = "secret-label" + def create_mock_unit(name="unit"): unit = MagicMock() unit.name = name @@ -287,7 +292,24 @@ def test_on_async_relation_changed(): mock_handle_start.assert_called_once_with(mock_event) def test_on_secret_changed(): - pass + """Test _on_secret_changed""" + # 1. relation is None + mock_charm = MagicMock() + mock_event = MagicMock() + + relation = PostgreSQLAsyncReplication(mock_charm) + + with patch.object( + PostgreSQLAsyncReplication, + '_relation', + new_callable=PropertyMock, + return_value=None + ): + with patch('logging.Logger.debug') as mock_debug: + relation._on_secret_changed(mock_event) + + mock_debug.assert_called_once_with("Early exit on_secret_changed: No relation found.") + mock_event.defer.assert_not_called() def test_stop_database(): pass \ No newline at end of file From 8da4eee1c796a3be80adb0a79dd8e74068f8c386 Mon Sep 17 00:00:00 2001 From: Gere_X Date: Sun, 6 Jul 2025 14:45:19 +0200 Subject: [PATCH 05/35] change a lit bit --- tests/unit/test_async_replication.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/tests/unit/test_async_replication.py b/tests/unit/test_async_replication.py index c1c57dbf3a..4753f4f017 100644 --- a/tests/unit/test_async_replication.py +++ b/tests/unit/test_async_replication.py @@ -28,12 +28,10 @@ def test_can_promote_cluster(): mock_event = MagicMock() type(mock_charm).is_cluster_initialised = PropertyMock(return_value=False) - with patch.object(PostgreSQLAsyncReplication, '_get_primary_cluster') as mock_get_primary: - relation = PostgreSQLAsyncReplication(mock_charm) - mock_get_primary.return_value = (MagicMock(), "0") - - assert relation._can_promote_cluster(mock_event) is False - mock_event.fail.assert_called_with("Cluster not initialised yet.") + relation = PostgreSQLAsyncReplication(mock_charm) + + assert relation._can_promote_cluster(mock_event) is False + mock_event.fail.assert_called_with("Cluster not initialised yet.") # 2. Test when cluster is initialized but no relation exists mock_charm = MagicMock() From 04593c8ce0844d5e1fcbfca50e8cc6f779993330 Mon Sep 17 00:00:00 2001 From: Gere_X Date: Mon, 7 Jul 2025 13:26:16 +0200 Subject: [PATCH 06/35] add tests for stop database function --- tests/unit/test_async_replication.py | 106 ++++++++++++++++++++++++++- 1 file changed, 105 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_async_replication.py b/tests/unit/test_async_replication.py index 4753f4f017..8971670bf2 100644 --- a/tests/unit/test_async_replication.py +++ b/tests/unit/test_async_replication.py @@ -1,6 +1,7 @@ import contextlib from unittest.mock import MagicMock, PropertyMock, patch import pytest +import os from ops.model import WaitingStatus from src.relations.async_replication import ( PostgreSQLAsyncReplication, @@ -13,6 +14,10 @@ REPLICATION_CONSUMER_RELATION = "replication-consumer" PEER = "peer" SECRET_LABEL = "secret-label" +SNAP_CURRENT_PATH = "/var/snap/charmed-postgresql/current" + +SNAP_CONF_PATH = f"{SNAP_CURRENT_PATH}/etc" +PATRONI_CONF_PATH = f"{SNAP_CONF_PATH}/patroni" def create_mock_unit(name="unit"): unit = MagicMock() @@ -310,4 +315,103 @@ def test_on_secret_changed(): mock_event.defer.assert_not_called() def test_stop_database(): - pass \ No newline at end of file + """Test _stop_database""" + # Setup mock objects + mock_charm = MagicMock() + mock_event = MagicMock() + mock_charm.is_unit_stopped = False + mock_charm.unit.is_leader.return_value = False + mock_charm._patroni.stop_patroni.return_value = True + + # Properly setup peers data structure + mock_unit = MagicMock() + mock_app = MagicMock() + mock_charm.unit = mock_unit + mock_charm.app = mock_app + mock_charm._peers.data = { + mock_app: {}, + mock_unit: {} + } + + relation = PostgreSQLAsyncReplication(mock_charm) + + # 1. Test early exit when following promoted cluster + with patch.object( + PostgreSQLAsyncReplication, + '_is_following_promoted_cluster', + return_value=True + ), patch('os.path.exists', return_value=True): + result = relation._stop_database(mock_event) + assert result is True + mock_charm._patroni.stop_patroni.assert_not_called() + + # # 2. Test successful stop sequence for non-leader unit with data path + # with patch.object( + # PostgreSQLAsyncReplication, + # '_is_following_promoted_cluster', + # return_value=False + # ), patch('os.path.exists', return_value=True), \ + # patch.object(PostgreSQLAsyncReplication, '_configure_standby_cluster', return_value=True), \ + # patch.object(PostgreSQLAsyncReplication, '_reinitialise_pgdata'), \ + # patch('shutil.rmtree'), \ + # patch('pathlib.Path') as mock_path: + + # # Mock the constants + # with patch('relations.async_replication.PATRONI_CONF_PATH', '/mock/patroni/conf'): + # # Setup mock for the raft directory check + # mock_path_instance = MagicMock() + # mock_path.return_value = mock_path_instance + # mock_path_instance.exists.return_value = True + # mock_path_instance.is_dir.return_value = True + + # result = relation._stop_database(mock_event) + # assert result is True + # mock_charm._patroni.stop_patroni.assert_called_once() + # mock_path.assert_called_once_with('/mock/patroni/conf/raft') + # assert mock_charm._peers.data[mock_unit].get("stopped") == "True" + + # 3. Test deferral when patroni fails to stop + # with patch.object( + # PostgreSQLAsyncReplication, + # '_is_following_promoted_cluster', + # return_value=False + # ), patch('os.path.exists', return_value=True): + # mock_charm._patroni.stop_patroni.return_value = False + # result = relation._stop_database(mock_event) + # assert result is False + # mock_event.defer.assert_called_once() + + # 4. Test non-leader with no data path + with patch.object( + PostgreSQLAsyncReplication, + '_is_following_promoted_cluster', + return_value=False + ), patch('os.path.exists', return_value=False): + mock_charm.unit.is_leader.return_value = False + result = relation._stop_database(mock_event) + assert result is False + mock_charm._patroni.stop_patroni.assert_not_called() + + # 5. Test leader unit behavior + with patch.object( + PostgreSQLAsyncReplication, + '_is_following_promoted_cluster', + return_value=False + ), patch('os.path.exists', return_value=True), \ + patch.object(PostgreSQLAsyncReplication, '_configure_standby_cluster', return_value=True), \ + patch.object(PostgreSQLAsyncReplication, '_reinitialise_pgdata'), \ + patch('shutil.rmtree'), \ + patch('pathlib.Path') as mock_path: + + # Setup mock for the raft directory check + mock_path_instance = MagicMock() + mock_path.return_value = mock_path_instance + mock_path_instance.exists.return_value = True + mock_path_instance.is_dir.return_value = True + + mock_charm.unit.is_leader.return_value = True + result = relation._stop_database(mock_event) + assert result is True + mock_charm._patroni.stop_patroni.assert_called_once() + assert mock_charm._peers.data[mock_app].get("cluster_initialised") == "" + assert mock_charm._peers.data[mock_unit].get("stopped") == "True" \ No newline at end of file From 7229cd1dbfaa5b2b2b6550d6e6d2fe9d8dbbcc43 Mon Sep 17 00:00:00 2001 From: Gere_X Date: Mon, 7 Jul 2025 13:59:39 +0200 Subject: [PATCH 07/35] still need some upgrades --- tests/unit/test_async_replication.py | 41 +++++++++++++--------------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/tests/unit/test_async_replication.py b/tests/unit/test_async_replication.py index 8971670bf2..5abbf8ca12 100644 --- a/tests/unit/test_async_replication.py +++ b/tests/unit/test_async_replication.py @@ -2,6 +2,7 @@ from unittest.mock import MagicMock, PropertyMock, patch import pytest import os +from tenacity import RetryError from ops.model import WaitingStatus from src.relations.async_replication import ( PostgreSQLAsyncReplication, @@ -346,29 +347,24 @@ def test_stop_database(): mock_charm._patroni.stop_patroni.assert_not_called() # # 2. Test successful stop sequence for non-leader unit with data path - # with patch.object( - # PostgreSQLAsyncReplication, - # '_is_following_promoted_cluster', - # return_value=False - # ), patch('os.path.exists', return_value=True), \ - # patch.object(PostgreSQLAsyncReplication, '_configure_standby_cluster', return_value=True), \ - # patch.object(PostgreSQLAsyncReplication, '_reinitialise_pgdata'), \ - # patch('shutil.rmtree'), \ - # patch('pathlib.Path') as mock_path: + # mock_retry = MagicMock() + # mock_retry.side_effect = RetryError(MagicMock()) + # mock_charm._patroni.stop_patroni.return_value = False + + # with patch('tenacity.Retrying', mock_retry), \ + # patch('pwd.getpwnam') as mock_getpwnam, \ + # patch.object(PostgreSQLAsyncReplication, '_is_following_promoted_cluster', return_value=False), \ + # patch('os.path.exists', return_value=True): - # # Mock the constants - # with patch('relations.async_replication.PATRONI_CONF_PATH', '/mock/patroni/conf'): - # # Setup mock for the raft directory check - # mock_path_instance = MagicMock() - # mock_path.return_value = mock_path_instance - # mock_path_instance.exists.return_value = True - # mock_path_instance.is_dir.return_value = True - - # result = relation._stop_database(mock_event) - # assert result is True - # mock_charm._patroni.stop_patroni.assert_called_once() - # mock_path.assert_called_once_with('/mock/patroni/conf/raft') - # assert mock_charm._peers.data[mock_unit].get("stopped") == "True" + # # Mock the system user response + # mock_getpwnam.return_value = MagicMock(pw_uid=1000, pw_gid=1000) + + # relation = PostgreSQLAsyncReplication(mock_charm) + # result = relation._stop_database(mock_event) + + # assert result is False + # mock_event.defer.assert_called_once() + # 3. Test deferral when patroni fails to stop # with patch.object( @@ -382,6 +378,7 @@ def test_stop_database(): # mock_event.defer.assert_called_once() # 4. Test non-leader with no data path + mock_charm._patroni.stop_patroni.return_value = True with patch.object( PostgreSQLAsyncReplication, '_is_following_promoted_cluster', From 488b0e4da24223fb09abd0cd69f4177fd267ca70 Mon Sep 17 00:00:00 2001 From: Gere_X Date: Wed, 9 Jul 2025 14:16:19 +0200 Subject: [PATCH 08/35] add this --- tests/unit/test_async_replication.py | 70 +++++++++++++++++++--------- 1 file changed, 49 insertions(+), 21 deletions(-) diff --git a/tests/unit/test_async_replication.py b/tests/unit/test_async_replication.py index 5abbf8ca12..e9913c75d9 100644 --- a/tests/unit/test_async_replication.py +++ b/tests/unit/test_async_replication.py @@ -346,26 +346,6 @@ def test_stop_database(): assert result is True mock_charm._patroni.stop_patroni.assert_not_called() - # # 2. Test successful stop sequence for non-leader unit with data path - # mock_retry = MagicMock() - # mock_retry.side_effect = RetryError(MagicMock()) - # mock_charm._patroni.stop_patroni.return_value = False - - # with patch('tenacity.Retrying', mock_retry), \ - # patch('pwd.getpwnam') as mock_getpwnam, \ - # patch.object(PostgreSQLAsyncReplication, '_is_following_promoted_cluster', return_value=False), \ - # patch('os.path.exists', return_value=True): - - # # Mock the system user response - # mock_getpwnam.return_value = MagicMock(pw_uid=1000, pw_gid=1000) - - # relation = PostgreSQLAsyncReplication(mock_charm) - # result = relation._stop_database(mock_event) - - # assert result is False - # mock_event.defer.assert_called_once() - - # 3. Test deferral when patroni fails to stop # with patch.object( # PostgreSQLAsyncReplication, @@ -411,4 +391,52 @@ def test_stop_database(): assert result is True mock_charm._patroni.stop_patroni.assert_called_once() assert mock_charm._peers.data[mock_app].get("cluster_initialised") == "" - assert mock_charm._peers.data[mock_unit].get("stopped") == "True" \ No newline at end of file + assert mock_charm._peers.data[mock_unit].get("stopped") == "True" + + +# def test_stop_database_non_leader_no_data_path(): +# """Test _stop_database when non-leader unit has no data path""" +# # Setup mock objects +# mock_charm = MagicMock() +# mock_event = MagicMock() + +# # Set up conditions +# mock_charm.is_unit_stopped = False +# mock_charm.unit.is_leader.return_value = False + +# # Properly mock _patroni +# mock_charm._patroni = MagicMock() +# mock_charm._patroni.stop_patroni.return_value = (True, "Success") + +# # Mock system-level operations +# with patch('subprocess.Popen') as mock_popen, \ +# patch('subprocess.run') as mock_run: + +# mock_popen.return_value.communicate.return_value = (b"output", b"") +# mock_run.return_value = MagicMock(stdout=b"output", stderr=b"", returncode=0) + +# # Setup COMPLETE peers data structure with counters +# mock_unit = MagicMock() +# mock_app = MagicMock() +# mock_charm.unit = mock_unit +# mock_charm.app = mock_app +# mock_charm._peers = MagicMock() +# mock_charm._peers.data = { +# mock_app: { +# 'async_replication': { +# 'status': 'active', +# 'promoted_cluster_counter': '42' # Must be string or int based on real code +# } +# }, +# mock_unit: { +# 'unit_id': 'postgresql/0', +# 'relation_promoted_cluster_counter': '45' # Must match real data format +# } +# } + +# relation = PostgreSQLAsyncReplication(mock_charm) +# result = relation._stop_database(mock_event) + +# assert result is False +# mock_charm._patroni.stop_patroni.assert_not_called() +# mock_event.defer.assert_not_called() From c5c2edfcb6fd495761ff9c9e43e7a3756805883f Mon Sep 17 00:00:00 2001 From: Gere_X Date: Wed, 9 Jul 2025 18:09:03 +0200 Subject: [PATCH 09/35] add learning --- tests/unit/test_async_replication.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/unit/test_async_replication.py b/tests/unit/test_async_replication.py index e9913c75d9..1c07debffb 100644 --- a/tests/unit/test_async_replication.py +++ b/tests/unit/test_async_replication.py @@ -1,4 +1,5 @@ import contextlib +from email.mime import application from unittest.mock import MagicMock, PropertyMock, patch import pytest import os @@ -100,6 +101,21 @@ def test_can_promote_cluster(): relation = PostgreSQLAsyncReplication(mock_charm) assert relation._can_promote_cluster(mock_event) is True + + # 4. + mock_app = MagicMock(spec=application) + mock_charm.app = mock_app + + with patch.object(PostgreSQLAsyncReplication, '_get_primary_cluster') as mock_get_primary: + mock_get_primary.return_value = mock_app + + relation = PostgreSQLAsyncReplication(mock_charm) + result = relation._can_promote_cluster(mock_event) + + assert result is False + mock_event.fail.assert_called_with("This cluster is already the primary cluster.") + + def test_handle_database_start(): """Tests all conditions in _handle_database_start""" From 3d26cba5d7df629ffd845ac870b84ab14e16ef82 Mon Sep 17 00:00:00 2001 From: Gere_X Date: Thu, 10 Jul 2025 12:55:41 +0200 Subject: [PATCH 10/35] test__configure_primary_cluster --- tests/unit/test_async_replication.py | 65 ++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/tests/unit/test_async_replication.py b/tests/unit/test_async_replication.py index 1c07debffb..b11c2f2bda 100644 --- a/tests/unit/test_async_replication.py +++ b/tests/unit/test_async_replication.py @@ -409,6 +409,71 @@ def test_stop_database(): assert mock_charm._peers.data[mock_app].get("cluster_initialised") == "" assert mock_charm._peers.data[mock_unit].get("stopped") == "True" +def test__configure_primary_cluster(): + mock_charm = MagicMock() + mock_event = MagicMock() + mock_charm.app = MagicMock() + + relation = PostgreSQLAsyncReplication(mock_charm) + + result = relation._configure_primary_cluster(None,mock_event) + assert result is False + + # 2. + mock_charm = MagicMock() + mock_event = MagicMock() + mock_charm.app = MagicMock() + mock_charm.unit.is_leader.return_value = False + mock_charm.update_config = MagicMock() + + relation = PostgreSQLAsyncReplication(mock_charm) + relation.is_primary_cluster = MagicMock(return_value=False) + result = relation._configure_primary_cluster(mock_charm.app,mock_event) + mock_charm.update_config.assert_called_once() + assert result is True + + # 3. + mock_charm = MagicMock() + mock_event = MagicMock() + mock_charm.app = MagicMock() + mock_charm.unit.is_leader.return_value = True + mock_charm.update_config = MagicMock() + #mock_charm._update_primary_cluster_data = MagicMock() + mock_charm._patroni.get_standby_leader.return_value = True + mock_charm._patroni.promote_standby_cluster = MagicMock() + + relation = PostgreSQLAsyncReplication(mock_charm) + relation.is_primary_cluster = MagicMock(return_value=True) + + relation._update_primary_cluster_data = MagicMock() + + result = relation._configure_primary_cluster(mock_charm.app,mock_event) + + mock_charm.update_config.assert_called_once() + relation._update_primary_cluster_data.assert_called_once() + mock_charm._patroni.promote_standby_cluster() + assert result is True + + # 4. + mock_charm = MagicMock() + mock_event = MagicMock() + mock_charm.app = MagicMock() + mock_charm.unit.is_leader.return_value = True + mock_charm.update_config = MagicMock() + mock_charm._patroni.get_standby_leader.return_value = None + + relation = PostgreSQLAsyncReplication(mock_charm) + relation.is_primary_cluster = MagicMock(return_value=True) + + relation._update_primary_cluster_data = MagicMock() + + result = relation._configure_primary_cluster(mock_charm.app,mock_event) + + mock_charm.update_config.assert_called_once() + relation._update_primary_cluster_data.assert_called_once() + assert result is True + + # def test_stop_database_non_leader_no_data_path(): # """Test _stop_database when non-leader unit has no data path""" From 2bc7acde4165cb6358d0dd0055e656e9490075f7 Mon Sep 17 00:00:00 2001 From: Gere_X Date: Thu, 10 Jul 2025 13:25:30 +0200 Subject: [PATCH 11/35] clean up --- tests/unit/test_async_replication.py | 96 ++-------------------------- 1 file changed, 6 insertions(+), 90 deletions(-) diff --git a/tests/unit/test_async_replication.py b/tests/unit/test_async_replication.py index b11c2f2bda..ff77c7fc01 100644 --- a/tests/unit/test_async_replication.py +++ b/tests/unit/test_async_replication.py @@ -1,9 +1,5 @@ -import contextlib from email.mime import application from unittest.mock import MagicMock, PropertyMock, patch -import pytest -import os -from tenacity import RetryError from ops.model import WaitingStatus from src.relations.async_replication import ( PostgreSQLAsyncReplication, @@ -12,14 +8,7 @@ StandbyClusterAlreadyPromotedError ) -REPLICATION_OFFER_RELATION = "replication-offer" -REPLICATION_CONSUMER_RELATION = "replication-consumer" PEER = "peer" -SECRET_LABEL = "secret-label" -SNAP_CURRENT_PATH = "/var/snap/charmed-postgresql/current" - -SNAP_CONF_PATH = f"{SNAP_CURRENT_PATH}/etc" -PATRONI_CONF_PATH = f"{SNAP_CONF_PATH}/patroni" def create_mock_unit(name="unit"): unit = MagicMock() @@ -45,7 +34,6 @@ def test_can_promote_cluster(): mock_event = MagicMock() type(mock_charm).is_cluster_initialised = PropertyMock(return_value=True) - # Create fresh mocks for this test case mock_peers_data = MagicMock() mock_peers_data.update = MagicMock() @@ -54,8 +42,7 @@ def test_can_promote_cluster(): _get_primary_cluster=MagicMock(), _set_app_status=MagicMock(), _handle_forceful_promotion=MagicMock(return_value=False)): - - # Setup test-specific conditions + mock_charm._patroni = MagicMock() mock_charm._patroni.get_standby_leader.return_value = "standby-leader" mock_charm._patroni.promote_standby_cluster.return_value = True @@ -67,7 +54,6 @@ def test_can_promote_cluster(): relation = PostgreSQLAsyncReplication(mock_charm) assert relation._can_promote_cluster(mock_event) is False - # Verify only the expected calls for this test case mock_peers_data.update.assert_called_once_with({ "promoted-cluster-counter": "" }) @@ -92,9 +78,8 @@ def test_can_promote_cluster(): type(mock_charm).is_cluster_initialised = PropertyMock(return_value=True) with patch.object(PostgreSQLAsyncReplication, '_get_primary_cluster') as mock_get_primary: - # Mock that relation exists with patch.object(PostgreSQLAsyncReplication, '_relation', new_callable=PropertyMock) as mock_relation: - mock_relation.return_value = MagicMock() # Simulate existing relation + mock_relation.return_value = MagicMock() mock_get_primary.return_value = (MagicMock(), "1") with patch.object(PostgreSQLAsyncReplication, '_handle_forceful_promotion', return_value=True): @@ -125,13 +110,11 @@ def test_handle_database_start(): mock_charm._patroni.member_started = True mock_charm.unit.is_leader.return_value = True - # Create mock units mock_unit1 = create_mock_unit() mock_unit2 = create_mock_unit() mock_charm.unit = create_mock_unit() mock_charm.app = MagicMock() - # Setup peers data structure with proper keys mock_peers_data = { mock_charm.unit: MagicMock(), mock_unit1: MagicMock(), @@ -145,14 +128,12 @@ def test_handle_database_start(): with patch.object(PostgreSQLAsyncReplication, '_get_highest_promoted_cluster_counter_value', return_value="1"), \ patch.object(PostgreSQLAsyncReplication, '_is_following_promoted_cluster', return_value=False): - # Configure all units to have matching counter values for unit in [mock_unit1, mock_unit2, mock_charm.unit]: mock_peers_data[unit].get.return_value = "1" relation = PostgreSQLAsyncReplication(mock_charm) relation._handle_database_start(mock_event) - # Verify updates when all units are ready mock_peers_data[mock_charm.unit].update.assert_any_call({"stopped": ""}) mock_peers_data[mock_charm.unit].update.assert_any_call({ "unit-promoted-cluster-counter": "1" @@ -187,15 +168,13 @@ def test_handle_database_start(): with patch.object(PostgreSQLAsyncReplication, '_get_highest_promoted_cluster_counter_value', return_value="1"), \ patch.object(PostgreSQLAsyncReplication, '_is_following_promoted_cluster', return_value=True): - # Configure some units to have mismatched counter values mock_peers_data[mock_charm.unit].get.return_value = "1" mock_peers_data[mock_unit1].get.return_value = "1" - mock_peers_data[mock_unit2].get.return_value = "0" # Different value + mock_peers_data[mock_unit2].get.return_value = "0" relation = PostgreSQLAsyncReplication(mock_charm) relation._handle_database_start(mock_event) - # Verify waiting status and deferral assert isinstance(mock_charm.unit.status, WaitingStatus) mock_event.defer.assert_called_once() @@ -214,7 +193,6 @@ def test_handle_database_start(): relation = PostgreSQLAsyncReplication(mock_charm) relation._handle_database_start(mock_event) - # Verify retry and deferral mock_charm._patroni.reload_patroni_configuration.assert_called_once() assert isinstance(mock_charm.unit.status, WaitingStatus) mock_event.defer.assert_called_once() @@ -333,14 +311,12 @@ def test_on_secret_changed(): def test_stop_database(): """Test _stop_database""" - # Setup mock objects mock_charm = MagicMock() mock_event = MagicMock() mock_charm.is_unit_stopped = False mock_charm.unit.is_leader.return_value = False mock_charm._patroni.stop_patroni.return_value = True - # Properly setup peers data structure mock_unit = MagicMock() mock_app = MagicMock() mock_charm.unit = mock_unit @@ -362,18 +338,7 @@ def test_stop_database(): assert result is True mock_charm._patroni.stop_patroni.assert_not_called() - # 3. Test deferral when patroni fails to stop - # with patch.object( - # PostgreSQLAsyncReplication, - # '_is_following_promoted_cluster', - # return_value=False - # ), patch('os.path.exists', return_value=True): - # mock_charm._patroni.stop_patroni.return_value = False - # result = relation._stop_database(mock_event) - # assert result is False - # mock_event.defer.assert_called_once() - - # 4. Test non-leader with no data path + # 2. Test non-leader with no data path mock_charm._patroni.stop_patroni.return_value = True with patch.object( PostgreSQLAsyncReplication, @@ -385,7 +350,7 @@ def test_stop_database(): assert result is False mock_charm._patroni.stop_patroni.assert_not_called() - # 5. Test leader unit behavior + # 3. Test leader unit behavior with patch.object( PostgreSQLAsyncReplication, '_is_following_promoted_cluster', @@ -410,6 +375,7 @@ def test_stop_database(): assert mock_charm._peers.data[mock_unit].get("stopped") == "True" def test__configure_primary_cluster(): + # 1. mock_charm = MagicMock() mock_event = MagicMock() mock_charm.app = MagicMock() @@ -438,7 +404,6 @@ def test__configure_primary_cluster(): mock_charm.app = MagicMock() mock_charm.unit.is_leader.return_value = True mock_charm.update_config = MagicMock() - #mock_charm._update_primary_cluster_data = MagicMock() mock_charm._patroni.get_standby_leader.return_value = True mock_charm._patroni.promote_standby_cluster = MagicMock() @@ -472,52 +437,3 @@ def test__configure_primary_cluster(): mock_charm.update_config.assert_called_once() relation._update_primary_cluster_data.assert_called_once() assert result is True - - - -# def test_stop_database_non_leader_no_data_path(): -# """Test _stop_database when non-leader unit has no data path""" -# # Setup mock objects -# mock_charm = MagicMock() -# mock_event = MagicMock() - -# # Set up conditions -# mock_charm.is_unit_stopped = False -# mock_charm.unit.is_leader.return_value = False - -# # Properly mock _patroni -# mock_charm._patroni = MagicMock() -# mock_charm._patroni.stop_patroni.return_value = (True, "Success") - -# # Mock system-level operations -# with patch('subprocess.Popen') as mock_popen, \ -# patch('subprocess.run') as mock_run: - -# mock_popen.return_value.communicate.return_value = (b"output", b"") -# mock_run.return_value = MagicMock(stdout=b"output", stderr=b"", returncode=0) - -# # Setup COMPLETE peers data structure with counters -# mock_unit = MagicMock() -# mock_app = MagicMock() -# mock_charm.unit = mock_unit -# mock_charm.app = mock_app -# mock_charm._peers = MagicMock() -# mock_charm._peers.data = { -# mock_app: { -# 'async_replication': { -# 'status': 'active', -# 'promoted_cluster_counter': '42' # Must be string or int based on real code -# } -# }, -# mock_unit: { -# 'unit_id': 'postgresql/0', -# 'relation_promoted_cluster_counter': '45' # Must match real data format -# } -# } - -# relation = PostgreSQLAsyncReplication(mock_charm) -# result = relation._stop_database(mock_event) - -# assert result is False -# mock_charm._patroni.stop_patroni.assert_not_called() -# mock_event.defer.assert_not_called() From 883102e8ae7760bbcb3f4b6471b3be95a5a099df Mon Sep 17 00:00:00 2001 From: Gere_X Date: Thu, 10 Jul 2025 14:07:04 +0200 Subject: [PATCH 12/35] we don't need import application --- tests/unit/test_async_replication.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/unit/test_async_replication.py b/tests/unit/test_async_replication.py index ff77c7fc01..d9a6d22df7 100644 --- a/tests/unit/test_async_replication.py +++ b/tests/unit/test_async_replication.py @@ -1,4 +1,3 @@ -from email.mime import application from unittest.mock import MagicMock, PropertyMock, patch from ops.model import WaitingStatus from src.relations.async_replication import ( @@ -88,7 +87,7 @@ def test_can_promote_cluster(): # 4. - mock_app = MagicMock(spec=application) + mock_app = MagicMock() mock_charm.app = mock_app with patch.object(PostgreSQLAsyncReplication, '_get_primary_cluster') as mock_get_primary: From 46ea650e5f50f77249e83f2800812f7b40dd14bb Mon Sep 17 00:00:00 2001 From: Gere_X Date: Fri, 11 Jul 2025 13:58:33 +0200 Subject: [PATCH 13/35] start test__on_async_relation_departed --- tests/unit/test_async_replication.py | 118 +++++++++++++-------------- 1 file changed, 59 insertions(+), 59 deletions(-) diff --git a/tests/unit/test_async_replication.py b/tests/unit/test_async_replication.py index d9a6d22df7..3d6486604d 100644 --- a/tests/unit/test_async_replication.py +++ b/tests/unit/test_async_replication.py @@ -1,10 +1,12 @@ from unittest.mock import MagicMock, PropertyMock, patch + from ops.model import WaitingStatus + from src.relations.async_replication import ( - PostgreSQLAsyncReplication, - NotReadyError, READ_ONLY_MODE_BLOCKING_MESSAGE, - StandbyClusterAlreadyPromotedError + NotReadyError, + PostgreSQLAsyncReplication, + StandbyClusterAlreadyPromotedError, ) PEER = "peer" @@ -16,13 +18,12 @@ def create_mock_unit(name="unit"): def test_can_promote_cluster(): - """Tests all conditions in _can_promote_cluster""" - + # 1. Test when cluster is not initialized mock_charm = MagicMock() mock_event = MagicMock() type(mock_charm).is_cluster_initialised = PropertyMock(return_value=False) - + relation = PostgreSQLAsyncReplication(mock_charm) assert relation._can_promote_cluster(mock_event) is False @@ -32,10 +33,10 @@ def test_can_promote_cluster(): mock_charm = MagicMock() mock_event = MagicMock() type(mock_charm).is_cluster_initialised = PropertyMock(return_value=True) - + mock_peers_data = MagicMock() mock_peers_data.update = MagicMock() - + with patch.multiple(PostgreSQLAsyncReplication, _relation=None, _get_primary_cluster=MagicMock(), @@ -49,10 +50,10 @@ def test_can_promote_cluster(): mock_charm._peers = MagicMock() mock_charm._peers.data = {mock_charm.app: mock_peers_data} mock_charm._set_primary_status_message = MagicMock() - + relation = PostgreSQLAsyncReplication(mock_charm) assert relation._can_promote_cluster(mock_event) is False - + mock_peers_data.update.assert_called_once_with({ "promoted-cluster-counter": "" }) @@ -75,45 +76,44 @@ def test_can_promote_cluster(): mock_charm = MagicMock() mock_event = MagicMock() type(mock_charm).is_cluster_initialised = PropertyMock(return_value=True) - + with patch.object(PostgreSQLAsyncReplication, '_get_primary_cluster') as mock_get_primary: with patch.object(PostgreSQLAsyncReplication, '_relation', new_callable=PropertyMock) as mock_relation: - mock_relation.return_value = MagicMock() - + mock_relation.return_value = MagicMock() + mock_get_primary.return_value = (MagicMock(), "1") with patch.object(PostgreSQLAsyncReplication, '_handle_forceful_promotion', return_value=True): relation = PostgreSQLAsyncReplication(mock_charm) assert relation._can_promote_cluster(mock_event) is True - - # 4. + + # 4. mock_app = MagicMock() mock_charm.app = mock_app with patch.object(PostgreSQLAsyncReplication, '_get_primary_cluster') as mock_get_primary: - mock_get_primary.return_value = mock_app - + mock_get_primary.return_value = mock_app + relation = PostgreSQLAsyncReplication(mock_charm) result = relation._can_promote_cluster(mock_event) - + assert result is False mock_event.fail.assert_called_with("This cluster is already the primary cluster.") def test_handle_database_start(): - """Tests all conditions in _handle_database_start""" # 1. Test when database is started (member_started = True) and all units ready mock_charm = MagicMock() mock_event = MagicMock() mock_charm._patroni.member_started = True mock_charm.unit.is_leader.return_value = True - + mock_unit1 = create_mock_unit() mock_unit2 = create_mock_unit() mock_charm.unit = create_mock_unit() mock_charm.app = MagicMock() - + mock_peers_data = { mock_charm.unit: MagicMock(), mock_unit1: MagicMock(), @@ -123,16 +123,16 @@ def test_handle_database_start(): mock_charm._peers = MagicMock() mock_charm._peers.data = mock_peers_data mock_charm._peers.units = [mock_unit1, mock_unit2] - + with patch.object(PostgreSQLAsyncReplication, '_get_highest_promoted_cluster_counter_value', return_value="1"), \ patch.object(PostgreSQLAsyncReplication, '_is_following_promoted_cluster', return_value=False): - + for unit in [mock_unit1, mock_unit2, mock_charm.unit]: mock_peers_data[unit].get.return_value = "1" - + relation = PostgreSQLAsyncReplication(mock_charm) relation._handle_database_start(mock_event) - + mock_peers_data[mock_charm.unit].update.assert_any_call({"stopped": ""}) mock_peers_data[mock_charm.unit].update.assert_any_call({ "unit-promoted-cluster-counter": "1" @@ -148,12 +148,12 @@ def test_handle_database_start(): mock_event = MagicMock() mock_charm._patroni.member_started = True mock_charm.unit.is_leader.return_value = True - + mock_unit1 = create_mock_unit() mock_unit2 = create_mock_unit() mock_charm.unit = create_mock_unit() mock_charm.app = MagicMock() - + mock_peers_data = { mock_charm.unit: MagicMock(), mock_unit1: MagicMock(), @@ -163,17 +163,17 @@ def test_handle_database_start(): mock_charm._peers = MagicMock() mock_charm._peers.data = mock_peers_data mock_charm._peers.units = [mock_unit1, mock_unit2] - + with patch.object(PostgreSQLAsyncReplication, '_get_highest_promoted_cluster_counter_value', return_value="1"), \ patch.object(PostgreSQLAsyncReplication, '_is_following_promoted_cluster', return_value=True): - + mock_peers_data[mock_charm.unit].get.return_value = "1" mock_peers_data[mock_unit1].get.return_value = "1" - mock_peers_data[mock_unit2].get.return_value = "0" - + mock_peers_data[mock_unit2].get.return_value = "0" + relation = PostgreSQLAsyncReplication(mock_charm) relation._handle_database_start(mock_event) - + assert isinstance(mock_charm.unit.status, WaitingStatus) mock_event.defer.assert_called_once() @@ -182,16 +182,16 @@ def test_handle_database_start(): mock_event = MagicMock() mock_charm._patroni.member_started = False mock_charm.unit.is_leader.return_value = False - + with patch.object(PostgreSQLAsyncReplication, '_get_highest_promoted_cluster_counter_value'), \ patch('src.relations.async_replication.contextlib.suppress') as mock_suppress: - + mock_suppress.return_value.__enter__.return_value = None mock_charm._patroni.reload_patroni_configuration.side_effect = NotReadyError() - + relation = PostgreSQLAsyncReplication(mock_charm) relation._handle_database_start(mock_event) - + mock_charm._patroni.reload_patroni_configuration.assert_called_once() assert isinstance(mock_charm.unit.status, WaitingStatus) mock_event.defer.assert_called_once() @@ -201,23 +201,22 @@ def test_handle_database_start(): mock_event = MagicMock() mock_charm._patroni.member_started = False mock_charm.unit.is_leader.return_value = True - + relation = PostgreSQLAsyncReplication(mock_charm) relation._handle_database_start(mock_event) - + # Verify waiting status and deferral assert isinstance(mock_charm.unit.status, WaitingStatus) mock_event.defer.assert_called_once() def test_on_async_relation_changed(): - """Tests all conditions in _on_async_relation_changed""" mock_charm = MagicMock() mock_event = MagicMock() mock_charm.unit.is_leader.return_value = True mock_charm.unit = create_mock_unit("leader") - mock_charm.app = MagicMock() + mock_charm.app = MagicMock() mock_unit1 = create_mock_unit("unit1") mock_unit2 = create_mock_unit("unit2") mock_charm._peers.units = [mock_unit1, mock_unit2] @@ -236,7 +235,7 @@ def test_on_async_relation_changed(): patch.object(relation, "_set_app_status") as mock_status: relation._on_async_relation_changed(mock_event) mock_status.assert_called_once() - mock_event.defer.assert_not_called() + mock_event.defer.assert_not_called() with patch.object(relation, "_get_primary_cluster", return_value="clusterX"), \ @@ -254,7 +253,7 @@ def test_on_async_relation_changed(): mock_charm.unit.is_leader.return_value = True - mock_charm.is_unit_stopped = False + mock_charm.is_unit_stopped = False with patch.object(relation, "_get_primary_cluster", return_value="clusterX"), \ patch.object(relation, "_configure_primary_cluster", return_value=False), \ patch.object(relation, "_is_following_promoted_cluster", return_value=False), \ @@ -289,33 +288,32 @@ def test_on_async_relation_changed(): mock_handle_start.assert_called_once_with(mock_event) def test_on_secret_changed(): - """Test _on_secret_changed""" + # 1. relation is None mock_charm = MagicMock() mock_event = MagicMock() - + relation = PostgreSQLAsyncReplication(mock_charm) - + with patch.object( PostgreSQLAsyncReplication, '_relation', new_callable=PropertyMock, return_value=None - ): - with patch('logging.Logger.debug') as mock_debug: - relation._on_secret_changed(mock_event) - - mock_debug.assert_called_once_with("Early exit on_secret_changed: No relation found.") - mock_event.defer.assert_not_called() + ), patch('logging.Logger.debug') as mock_debug: + relation._on_secret_changed(mock_event) + + mock_debug.assert_called_once_with("Early exit on_secret_changed: No relation found.") + mock_event.defer.assert_not_called() def test_stop_database(): - """Test _stop_database""" + mock_charm = MagicMock() mock_event = MagicMock() mock_charm.is_unit_stopped = False mock_charm.unit.is_leader.return_value = False mock_charm._patroni.stop_patroni.return_value = True - + mock_unit = MagicMock() mock_app = MagicMock() mock_charm.unit = mock_unit @@ -359,13 +357,12 @@ def test_stop_database(): patch.object(PostgreSQLAsyncReplication, '_reinitialise_pgdata'), \ patch('shutil.rmtree'), \ patch('pathlib.Path') as mock_path: - - # Setup mock for the raft directory check + mock_path_instance = MagicMock() mock_path.return_value = mock_path_instance mock_path_instance.exists.return_value = True mock_path_instance.is_dir.return_value = True - + mock_charm.unit.is_leader.return_value = True result = relation._stop_database(mock_event) assert result is True @@ -380,7 +377,7 @@ def test__configure_primary_cluster(): mock_charm.app = MagicMock() relation = PostgreSQLAsyncReplication(mock_charm) - + result = relation._configure_primary_cluster(None,mock_event) assert result is False @@ -397,7 +394,7 @@ def test__configure_primary_cluster(): mock_charm.update_config.assert_called_once() assert result is True - # 3. + # 3. mock_charm = MagicMock() mock_event = MagicMock() mock_charm.app = MagicMock() @@ -418,7 +415,7 @@ def test__configure_primary_cluster(): mock_charm._patroni.promote_standby_cluster() assert result is True - # 4. + # 4. mock_charm = MagicMock() mock_event = MagicMock() mock_charm.app = MagicMock() @@ -436,3 +433,6 @@ def test__configure_primary_cluster(): mock_charm.update_config.assert_called_once() relation._update_primary_cluster_data.assert_called_once() assert result is True + +def test__on_async_relation_departed(): + pass From 5cec1c8bcf7c4aeb7f2c6d3a987c144d3dde9548 Mon Sep 17 00:00:00 2001 From: Gere_X Date: Fri, 11 Jul 2025 14:03:28 +0200 Subject: [PATCH 14/35] format --- tests/unit/test_async_replication.py | 226 +++++++++++++++------------ 1 file changed, 125 insertions(+), 101 deletions(-) diff --git a/tests/unit/test_async_replication.py b/tests/unit/test_async_replication.py index 3d6486604d..c1b4299c3c 100644 --- a/tests/unit/test_async_replication.py +++ b/tests/unit/test_async_replication.py @@ -11,6 +11,7 @@ PEER = "peer" + def create_mock_unit(name="unit"): unit = MagicMock() unit.name = name @@ -18,7 +19,6 @@ def create_mock_unit(name="unit"): def test_can_promote_cluster(): - # 1. Test when cluster is not initialized mock_charm = MagicMock() mock_event = MagicMock() @@ -37,12 +37,13 @@ def test_can_promote_cluster(): mock_peers_data = MagicMock() mock_peers_data.update = MagicMock() - with patch.multiple(PostgreSQLAsyncReplication, - _relation=None, - _get_primary_cluster=MagicMock(), - _set_app_status=MagicMock(), - _handle_forceful_promotion=MagicMock(return_value=False)): - + with patch.multiple( + PostgreSQLAsyncReplication, + _relation=None, + _get_primary_cluster=MagicMock(), + _set_app_status=MagicMock(), + _handle_forceful_promotion=MagicMock(return_value=False), + ): mock_charm._patroni = MagicMock() mock_charm._patroni.get_standby_leader.return_value = "standby-leader" mock_charm._patroni.promote_standby_cluster.return_value = True @@ -54,14 +55,14 @@ def test_can_promote_cluster(): relation = PostgreSQLAsyncReplication(mock_charm) assert relation._can_promote_cluster(mock_event) is False - mock_peers_data.update.assert_called_once_with({ - "promoted-cluster-counter": "" - }) + mock_peers_data.update.assert_called_once_with({"promoted-cluster-counter": ""}) relation._set_app_status.assert_called_once() mock_charm._set_primary_status_message.assert_called_once() # 2b. Test when standby leader exists but promotion fails - mock_charm._patroni.promote_standby_cluster.side_effect = StandbyClusterAlreadyPromotedError("Already promoted") + mock_charm._patroni.promote_standby_cluster.side_effect = ( + StandbyClusterAlreadyPromotedError("Already promoted") + ) relation = PostgreSQLAsyncReplication(mock_charm) assert relation._can_promote_cluster(mock_event) is False mock_event.fail.assert_called_with("Already promoted") @@ -77,21 +78,23 @@ def test_can_promote_cluster(): mock_event = MagicMock() type(mock_charm).is_cluster_initialised = PropertyMock(return_value=True) - with patch.object(PostgreSQLAsyncReplication, '_get_primary_cluster') as mock_get_primary: - with patch.object(PostgreSQLAsyncReplication, '_relation', new_callable=PropertyMock) as mock_relation: - mock_relation.return_value = MagicMock() - - mock_get_primary.return_value = (MagicMock(), "1") - with patch.object(PostgreSQLAsyncReplication, '_handle_forceful_promotion', return_value=True): - relation = PostgreSQLAsyncReplication(mock_charm) - assert relation._can_promote_cluster(mock_event) is True - + with ( + patch.object(PostgreSQLAsyncReplication, "_get_primary_cluster") as mock_get_primary, + patch.object( + PostgreSQLAsyncReplication, "_relation", new_callable=PropertyMock + ) as mock_relation, + patch.object(PostgreSQLAsyncReplication, "_handle_forceful_promotion", return_value=True), + ): + mock_relation.return_value = MagicMock() + mock_get_primary.return_value = (MagicMock(), "1") + relation = PostgreSQLAsyncReplication(mock_charm) + assert relation._can_promote_cluster(mock_event) is True # 4. mock_app = MagicMock() mock_charm.app = mock_app - with patch.object(PostgreSQLAsyncReplication, '_get_primary_cluster') as mock_get_primary: + with patch.object(PostgreSQLAsyncReplication, "_get_primary_cluster") as mock_get_primary: mock_get_primary.return_value = mock_app relation = PostgreSQLAsyncReplication(mock_charm) @@ -102,7 +105,6 @@ def test_can_promote_cluster(): def test_handle_database_start(): - # 1. Test when database is started (member_started = True) and all units ready mock_charm = MagicMock() mock_event = MagicMock() @@ -118,15 +120,22 @@ def test_handle_database_start(): mock_charm.unit: MagicMock(), mock_unit1: MagicMock(), mock_unit2: MagicMock(), - mock_charm.app: MagicMock() + mock_charm.app: MagicMock(), } mock_charm._peers = MagicMock() mock_charm._peers.data = mock_peers_data mock_charm._peers.units = [mock_unit1, mock_unit2] - with patch.object(PostgreSQLAsyncReplication, '_get_highest_promoted_cluster_counter_value', return_value="1"), \ - patch.object(PostgreSQLAsyncReplication, '_is_following_promoted_cluster', return_value=False): - + with ( + patch.object( + PostgreSQLAsyncReplication, + "_get_highest_promoted_cluster_counter_value", + return_value="1", + ), + patch.object( + PostgreSQLAsyncReplication, "_is_following_promoted_cluster", return_value=False + ), + ): for unit in [mock_unit1, mock_unit2, mock_charm.unit]: mock_peers_data[unit].get.return_value = "1" @@ -158,15 +167,22 @@ def test_handle_database_start(): mock_charm.unit: MagicMock(), mock_unit1: MagicMock(), mock_unit2: MagicMock(), - mock_charm.app: MagicMock() + mock_charm.app: MagicMock(), } mock_charm._peers = MagicMock() mock_charm._peers.data = mock_peers_data mock_charm._peers.units = [mock_unit1, mock_unit2] - with patch.object(PostgreSQLAsyncReplication, '_get_highest_promoted_cluster_counter_value', return_value="1"), \ - patch.object(PostgreSQLAsyncReplication, '_is_following_promoted_cluster', return_value=True): - + with ( + patch.object( + PostgreSQLAsyncReplication, + "_get_highest_promoted_cluster_counter_value", + return_value="1", + ), + patch.object( + PostgreSQLAsyncReplication, "_is_following_promoted_cluster", return_value=True + ), + ): mock_peers_data[mock_charm.unit].get.return_value = "1" mock_peers_data[mock_unit1].get.return_value = "1" mock_peers_data[mock_unit2].get.return_value = "0" @@ -183,9 +199,10 @@ def test_handle_database_start(): mock_charm._patroni.member_started = False mock_charm.unit.is_leader.return_value = False - with patch.object(PostgreSQLAsyncReplication, '_get_highest_promoted_cluster_counter_value'), \ - patch('src.relations.async_replication.contextlib.suppress') as mock_suppress: - + with ( + patch.object(PostgreSQLAsyncReplication, "_get_highest_promoted_cluster_counter_value"), + patch("src.relations.async_replication.contextlib.suppress") as mock_suppress, + ): mock_suppress.return_value.__enter__.return_value = None mock_charm._patroni.reload_patroni_configuration.side_effect = NotReadyError() @@ -211,7 +228,6 @@ def test_handle_database_start(): def test_on_async_relation_changed(): - mock_charm = MagicMock() mock_event = MagicMock() mock_charm.unit.is_leader.return_value = True @@ -230,84 +246,91 @@ def test_on_async_relation_changed(): relation = PostgreSQLAsyncReplication(mock_charm) - - with patch.object(relation, "_get_primary_cluster", return_value=None), \ - patch.object(relation, "_set_app_status") as mock_status: + with ( + patch.object(relation, "_get_primary_cluster", return_value=None), + patch.object(relation, "_set_app_status") as mock_status, + ): relation._on_async_relation_changed(mock_event) mock_status.assert_called_once() mock_event.defer.assert_not_called() - - with patch.object(relation, "_get_primary_cluster", return_value="clusterX"), \ - patch.object(relation, "_configure_primary_cluster", return_value=True): + with ( + patch.object(relation, "_get_primary_cluster", return_value="clusterX"), + patch.object(relation, "_configure_primary_cluster", return_value=True), + ): relation._on_async_relation_changed(mock_event) mock_event.defer.assert_not_called() - mock_charm.unit.is_leader.return_value = False - with patch.object(relation, "_get_primary_cluster", return_value="clusterX"), \ - patch.object(relation, "_configure_primary_cluster", return_value=False), \ - patch.object(relation, "_is_following_promoted_cluster", return_value=True): + with ( + patch.object(relation, "_get_primary_cluster", return_value="clusterX"), + patch.object(relation, "_configure_primary_cluster", return_value=False), + patch.object(relation, "_is_following_promoted_cluster", return_value=True), + ): relation._on_async_relation_changed(mock_event) mock_event.defer.assert_not_called() - mock_charm.unit.is_leader.return_value = True mock_charm.is_unit_stopped = False - with patch.object(relation, "_get_primary_cluster", return_value="clusterX"), \ - patch.object(relation, "_configure_primary_cluster", return_value=False), \ - patch.object(relation, "_is_following_promoted_cluster", return_value=False), \ - patch.object(relation, "_stop_database", return_value=True), \ - patch.object(relation, "_get_highest_promoted_cluster_counter_value", return_value="5"): + with ( + patch.object(relation, "_get_primary_cluster", return_value="clusterX"), + patch.object(relation, "_configure_primary_cluster", return_value=False), + patch.object(relation, "_is_following_promoted_cluster", return_value=False), + patch.object(relation, "_stop_database", return_value=True), + patch.object(relation, "_get_highest_promoted_cluster_counter_value", return_value="5"), + ): relation._on_async_relation_changed(mock_event) assert isinstance(mock_charm.unit.status, WaitingStatus) mock_event.defer.assert_called() - mock_charm.is_unit_stopped = True - with patch.object(relation, "_get_primary_cluster", return_value="clusterX"), \ - patch.object(relation, "_configure_primary_cluster", return_value=False), \ - patch.object(relation, "_is_following_promoted_cluster", return_value=False), \ - patch.object(relation, "_stop_database", return_value=True), \ - patch.object(relation, "_get_highest_promoted_cluster_counter_value", return_value="5"), \ - patch.object(relation, "_wait_for_standby_leader", return_value=True): + with ( + patch.object(relation, "_get_primary_cluster", return_value="clusterX"), + patch.object(relation, "_configure_primary_cluster", return_value=False), + patch.object(relation, "_is_following_promoted_cluster", return_value=False), + patch.object(relation, "_stop_database", return_value=True), + patch.object(relation, "_get_highest_promoted_cluster_counter_value", return_value="5"), + patch.object(relation, "_wait_for_standby_leader", return_value=True), + ): relation._on_async_relation_changed(mock_event) mock_charm._patroni.start_patroni.assert_not_called() - with patch.object(relation, "_get_primary_cluster", return_value="clusterX"), \ - patch.object(relation, "_configure_primary_cluster", return_value=False), \ - patch.object(relation, "_is_following_promoted_cluster", return_value=False), \ - patch.object(relation, "_stop_database", return_value=True), \ - patch.object(relation, "_get_highest_promoted_cluster_counter_value", return_value="5"), \ - patch.object(relation, "_wait_for_standby_leader", return_value=False), \ - patch.object(mock_charm._patroni, "start_patroni", return_value=True), \ - patch.object(relation, "_handle_database_start") as mock_handle_start: + with ( + patch.object(relation, "_get_primary_cluster", return_value="clusterX"), + patch.object(relation, "_configure_primary_cluster", return_value=False), + patch.object(relation, "_is_following_promoted_cluster", return_value=False), + patch.object(relation, "_stop_database", return_value=True), + patch.object(relation, "_get_highest_promoted_cluster_counter_value", return_value="5"), + patch.object(relation, "_wait_for_standby_leader", return_value=False), + patch.object(mock_charm._patroni, "start_patroni", return_value=True), + patch.object(relation, "_handle_database_start") as mock_handle_start, + ): relation._on_async_relation_changed(mock_event) mock_charm.update_config.assert_called_once() mock_handle_start.assert_called_once_with(mock_event) -def test_on_secret_changed(): +def test_on_secret_changed(): # 1. relation is None mock_charm = MagicMock() mock_event = MagicMock() relation = PostgreSQLAsyncReplication(mock_charm) - with patch.object( - PostgreSQLAsyncReplication, - '_relation', - new_callable=PropertyMock, - return_value=None - ), patch('logging.Logger.debug') as mock_debug: + with ( + patch.object( + PostgreSQLAsyncReplication, "_relation", new_callable=PropertyMock, return_value=None + ), + patch("logging.Logger.debug") as mock_debug, + ): relation._on_secret_changed(mock_event) mock_debug.assert_called_once_with("Early exit on_secret_changed: No relation found.") mock_event.defer.assert_not_called() -def test_stop_database(): +def test_stop_database(): mock_charm = MagicMock() mock_event = MagicMock() mock_charm.is_unit_stopped = False @@ -318,46 +341,45 @@ def test_stop_database(): mock_app = MagicMock() mock_charm.unit = mock_unit mock_charm.app = mock_app - mock_charm._peers.data = { - mock_app: {}, - mock_unit: {} - } + mock_charm._peers.data = {mock_app: {}, mock_unit: {}} relation = PostgreSQLAsyncReplication(mock_charm) # 1. Test early exit when following promoted cluster - with patch.object( - PostgreSQLAsyncReplication, - '_is_following_promoted_cluster', - return_value=True - ), patch('os.path.exists', return_value=True): + with ( + patch.object( + PostgreSQLAsyncReplication, "_is_following_promoted_cluster", return_value=True + ), + patch("os.path.exists", return_value=True), + ): result = relation._stop_database(mock_event) assert result is True mock_charm._patroni.stop_patroni.assert_not_called() # 2. Test non-leader with no data path mock_charm._patroni.stop_patroni.return_value = True - with patch.object( - PostgreSQLAsyncReplication, - '_is_following_promoted_cluster', - return_value=False - ), patch('os.path.exists', return_value=False): + with ( + patch.object( + PostgreSQLAsyncReplication, "_is_following_promoted_cluster", return_value=False + ), + patch("os.path.exists", return_value=False), + ): mock_charm.unit.is_leader.return_value = False result = relation._stop_database(mock_event) assert result is False mock_charm._patroni.stop_patroni.assert_not_called() # 3. Test leader unit behavior - with patch.object( - PostgreSQLAsyncReplication, - '_is_following_promoted_cluster', - return_value=False - ), patch('os.path.exists', return_value=True), \ - patch.object(PostgreSQLAsyncReplication, '_configure_standby_cluster', return_value=True), \ - patch.object(PostgreSQLAsyncReplication, '_reinitialise_pgdata'), \ - patch('shutil.rmtree'), \ - patch('pathlib.Path') as mock_path: - + with ( + patch.object( + PostgreSQLAsyncReplication, "_is_following_promoted_cluster", return_value=False + ), + patch("os.path.exists", return_value=True), + patch.object(PostgreSQLAsyncReplication, "_configure_standby_cluster", return_value=True), + patch.object(PostgreSQLAsyncReplication, "_reinitialise_pgdata"), + patch("shutil.rmtree"), + patch("pathlib.Path") as mock_path, + ): mock_path_instance = MagicMock() mock_path.return_value = mock_path_instance mock_path_instance.exists.return_value = True @@ -370,6 +392,7 @@ def test_stop_database(): assert mock_charm._peers.data[mock_app].get("cluster_initialised") == "" assert mock_charm._peers.data[mock_unit].get("stopped") == "True" + def test__configure_primary_cluster(): # 1. mock_charm = MagicMock() @@ -378,7 +401,7 @@ def test__configure_primary_cluster(): relation = PostgreSQLAsyncReplication(mock_charm) - result = relation._configure_primary_cluster(None,mock_event) + result = relation._configure_primary_cluster(None, mock_event) assert result is False # 2. @@ -390,7 +413,7 @@ def test__configure_primary_cluster(): relation = PostgreSQLAsyncReplication(mock_charm) relation.is_primary_cluster = MagicMock(return_value=False) - result = relation._configure_primary_cluster(mock_charm.app,mock_event) + result = relation._configure_primary_cluster(mock_charm.app, mock_event) mock_charm.update_config.assert_called_once() assert result is True @@ -408,7 +431,7 @@ def test__configure_primary_cluster(): relation._update_primary_cluster_data = MagicMock() - result = relation._configure_primary_cluster(mock_charm.app,mock_event) + result = relation._configure_primary_cluster(mock_charm.app, mock_event) mock_charm.update_config.assert_called_once() relation._update_primary_cluster_data.assert_called_once() @@ -428,11 +451,12 @@ def test__configure_primary_cluster(): relation._update_primary_cluster_data = MagicMock() - result = relation._configure_primary_cluster(mock_charm.app,mock_event) + result = relation._configure_primary_cluster(mock_charm.app, mock_event) mock_charm.update_config.assert_called_once() relation._update_primary_cluster_data.assert_called_once() assert result is True + def test__on_async_relation_departed(): pass From ff9045867482b2ddc64f2aa1811780766cd7bb3c Mon Sep 17 00:00:00 2001 From: Gere_X Date: Fri, 11 Jul 2025 14:16:18 +0200 Subject: [PATCH 15/35] done test__on_async_relation_departed --- tests/unit/test_async_replication.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_async_replication.py b/tests/unit/test_async_replication.py index c1b4299c3c..0fb19c2956 100644 --- a/tests/unit/test_async_replication.py +++ b/tests/unit/test_async_replication.py @@ -459,4 +459,15 @@ def test__configure_primary_cluster(): def test__on_async_relation_departed(): - pass + mock_charm = MagicMock() + mock_event = MagicMock() + mock_peers = MagicMock() + + mock_event.departing_unit = MagicMock() + mock_charm.unit = mock_event.departing_unit + mock_charm._peers = mock_peers + + relation = PostgreSQLAsyncReplication(mock_charm) + + result = relation._on_async_relation_departed(mock_event) + assert result is None From 7f51371c5c3f95c96797d4da9fe6c4f2840dc9ab Mon Sep 17 00:00:00 2001 From: Gere_X Date: Fri, 11 Jul 2025 14:49:15 +0200 Subject: [PATCH 16/35] test_on_async_relation_joined --- tests/unit/test_async_replication.py | 30 +++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_async_replication.py b/tests/unit/test_async_replication.py index 0fb19c2956..c813861484 100644 --- a/tests/unit/test_async_replication.py +++ b/tests/unit/test_async_replication.py @@ -462,12 +462,40 @@ def test__on_async_relation_departed(): mock_charm = MagicMock() mock_event = MagicMock() mock_peers = MagicMock() - + mock_unit_data = {} mock_event.departing_unit = MagicMock() mock_charm.unit = mock_event.departing_unit mock_charm._peers = mock_peers + mock_peers.data = {mock_charm.unit: mock_unit_data} relation = PostgreSQLAsyncReplication(mock_charm) result = relation._on_async_relation_departed(mock_event) assert result is None + assert mock_unit_data == {"departing": "True"} + +def test_on_async_relation_joined(): + + mock_charm = MagicMock() + mock_event = MagicMock() + mock_peers = MagicMock() + mock_unit_data = {} + + mock_charm._unit_ip = "10.0.0.1" + mock_charm._peers = mock_peers + mock_peers.data = {mock_charm.unit: mock_unit_data} + + relation = PostgreSQLAsyncReplication(mock_charm) + + relation._get_highest_promoted_cluster_counter_value = MagicMock(return_value="1") + + result = relation._on_async_relation_joined(mock_event) + + assert result is None + + assert mock_unit_data == { + "unit-promoted-cluster-counter": "1" + } + + relation._get_highest_promoted_cluster_counter_value.assert_called_once() + From 49941c0126771d213f6792e5951d71b891fd84a0 Mon Sep 17 00:00:00 2001 From: Gere_X Date: Fri, 11 Jul 2025 15:20:04 +0200 Subject: [PATCH 17/35] test_on_create_replication --- tests/unit/test_async_replication.py | 70 ++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/tests/unit/test_async_replication.py b/tests/unit/test_async_replication.py index c813861484..d4f19d6bd6 100644 --- a/tests/unit/test_async_replication.py +++ b/tests/unit/test_async_replication.py @@ -7,6 +7,7 @@ NotReadyError, PostgreSQLAsyncReplication, StandbyClusterAlreadyPromotedError, + REPLICATION_CONSUMER_RELATION, ) PEER = "peer" @@ -498,4 +499,73 @@ def test_on_async_relation_joined(): } relation._get_highest_promoted_cluster_counter_value.assert_called_once() + +def test_on_create_replication(): + # 1. + mock_charm = MagicMock() + mock_event = MagicMock() + relation = PostgreSQLAsyncReplication(mock_charm) + + relation._get_primary_cluster = MagicMock(return_value=True) + + result = relation._on_create_replication(mock_event) + + assert result is None + mock_event.fail.assert_called_once_with( + "There is already a replication set up." + ) + + # 2. + mock_charm = MagicMock() + mock_event = MagicMock() + + relation = PostgreSQLAsyncReplication(mock_charm) + + relation._get_primary_cluster = MagicMock(return_value=None) + + mock_relation = MagicMock() + mock_relation.name = REPLICATION_CONSUMER_RELATION + type(relation)._relation = PropertyMock(return_value=mock_relation) + + result = relation._on_create_replication(mock_event) + + assert result is None + mock_event.fail.assert_called_once_with( + "This action must be run in the cluster where the offer was created." + ) + # 3. + mock_charm = MagicMock() + mock_event = MagicMock() + + relation = PostgreSQLAsyncReplication(mock_charm) + + relation._get_primary_cluster = MagicMock(return_value=None) + + relation._handle_replication_change = MagicMock(return_value=True) + + mock_relation = MagicMock() + mock_relation.name = "Something" + type(relation)._relation = PropertyMock(return_value=mock_relation) + + result = relation._on_create_replication(mock_event) + + assert result is None + + # 4. + mock_charm = MagicMock() + mock_event = MagicMock() + + relation = PostgreSQLAsyncReplication(mock_charm) + + relation._get_primary_cluster = MagicMock(return_value=None) + + relation._handle_replication_change = MagicMock(return_value=False) + + mock_relation = MagicMock() + mock_relation.name = "Something" + type(relation)._relation = PropertyMock(return_value=mock_relation) + + result = relation._on_create_replication(mock_event) + + assert result is None From 105d5ff3557283fcaa5857e30f503a40a5d8cd98 Mon Sep 17 00:00:00 2001 From: Gere_X Date: Fri, 11 Jul 2025 16:11:53 +0200 Subject: [PATCH 18/35] test_promote_to_primary --- tests/unit/test_async_replication.py | 53 ++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/tests/unit/test_async_replication.py b/tests/unit/test_async_replication.py index d4f19d6bd6..012b4df84e 100644 --- a/tests/unit/test_async_replication.py +++ b/tests/unit/test_async_replication.py @@ -569,3 +569,56 @@ def test_on_create_replication(): assert result is None +def test_promote_to_primary(): + + # 1. + mock_charm = MagicMock() + mock_event = MagicMock() + mock_relation = MagicMock() + mock_relation.status = MagicMock() + mock_relation.status.message = "Something" + + relation = PostgreSQLAsyncReplication(mock_charm) + relation._get_primary_cluster = MagicMock(return_value = None) + + type(relation).app = PropertyMock(return_value=mock_relation) + result = relation.promote_to_primary(mock_event) + + assert result is None + mock_event.fail.assert_called_once_with( + "No primary cluster found. Run `create-replication` action in the cluster where the offer was created." + ) + + # 2. + mock_charm = MagicMock() + mock_event = MagicMock() + mock_relation = MagicMock() + mock_relation.status = MagicMock() + mock_relation.status.message = READ_ONLY_MODE_BLOCKING_MESSAGE + + relation = PostgreSQLAsyncReplication(mock_charm) + relation._get_primary_cluster = MagicMock(return_value = None) + + type(relation).app = PropertyMock(return_value=mock_relation) + relation._handle_replication_change = MagicMock(return_value=False) + + result = relation.promote_to_primary(mock_event) + + assert result is None + + # 3. + # mock_charm = MagicMock() + # mock_event = MagicMock() + # mock_relation = MagicMock() + # mock_relation.status = MagicMock() + # mock_relation.status.message = READ_ONLY_MODE_BLOCKING_MESSAGE + + # relation = PostgreSQLAsyncReplication(mock_charm) + # relation._get_primary_cluster = MagicMock(return_value = None) + + # type(relation).app = PropertyMock(return_value=mock_relation) + # relation._handle_replication_change = MagicMock(return_value=True) + + # result = relation.promote_to_primary(mock_event) + + # assert result is None From 54de455a0773798d3cc57775cacce10183605f8e Mon Sep 17 00:00:00 2001 From: Gere_X Date: Fri, 11 Jul 2025 17:49:46 +0200 Subject: [PATCH 19/35] add test --- tests/unit/test_async_replication.py | 59 ++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/tests/unit/test_async_replication.py b/tests/unit/test_async_replication.py index 012b4df84e..99ce2194e3 100644 --- a/tests/unit/test_async_replication.py +++ b/tests/unit/test_async_replication.py @@ -1,6 +1,7 @@ from unittest.mock import MagicMock, PropertyMock, patch from ops.model import WaitingStatus +import pytest from src.relations.async_replication import ( READ_ONLY_MODE_BLOCKING_MESSAGE, @@ -622,3 +623,61 @@ def test_promote_to_primary(): # result = relation.promote_to_primary(mock_event) # assert result is None + +def test__configure_standby_cluster(): + mock_charm = MagicMock() + mock_event = MagicMock() + + relation = PostgreSQLAsyncReplication(mock_charm) + relation._relation = MagicMock() + relation._relation.name = REPLICATION_CONSUMER_RELATION + relation._update_internal_secret = MagicMock(return_value=False) + + result = relation._configure_standby_cluster(mock_event) + + + assert result is False + + mock_event.defer.assert_called_once() + + # 2. + + mock_charm = MagicMock() + mock_event = MagicMock() + + relation = PostgreSQLAsyncReplication(mock_charm) + relation._relation = MagicMock() + relation._relation.name = "something_else" + relation._update_internal_secret = MagicMock(return_value=True) + relation.get_system_identifier = MagicMock(return_value=(None, 2)) + + + with pytest.raises(Exception) as exc_info: + relation._configure_standby_cluster(mock_event) + + assert str(exc_info.value) == "2" + + # 3. + + mock_charm = MagicMock() + mock_event = MagicMock() + + relation = PostgreSQLAsyncReplication(mock_charm) + relation._relation = MagicMock() + relation._relation.name = "some_relation" + relation._relation.app = "remote-app" + relation._relation.data = {relation._relation.app: {"system-id": "123"}} + + + relation._update_internal_secret = MagicMock(return_value=True) + relation.get_system_identifier = MagicMock(return_value=("456", None)) + relation.charm = MagicMock() + relation.charm.app_peer_data = {} + + + with patch("subprocess.check_call") as mock_check_call: + + result = relation._configure_standby_cluster(mock_event) + + assert result is True + mock_check_call.assert_called_once() \ No newline at end of file From f64c4389026af789b042649226a2931713b2aaa3 Mon Sep 17 00:00:00 2001 From: Gere_X Date: Mon, 14 Jul 2025 12:39:05 +0200 Subject: [PATCH 20/35] test_wait_for_standby_leader --- tests/unit/test_async_replication.py | 65 ++++++++++++++++++++-------- 1 file changed, 47 insertions(+), 18 deletions(-) diff --git a/tests/unit/test_async_replication.py b/tests/unit/test_async_replication.py index 99ce2194e3..44eda7f3ff 100644 --- a/tests/unit/test_async_replication.py +++ b/tests/unit/test_async_replication.py @@ -607,23 +607,6 @@ def test_promote_to_primary(): assert result is None - # 3. - # mock_charm = MagicMock() - # mock_event = MagicMock() - # mock_relation = MagicMock() - # mock_relation.status = MagicMock() - # mock_relation.status.message = READ_ONLY_MODE_BLOCKING_MESSAGE - - # relation = PostgreSQLAsyncReplication(mock_charm) - # relation._get_primary_cluster = MagicMock(return_value = None) - - # type(relation).app = PropertyMock(return_value=mock_relation) - # relation._handle_replication_change = MagicMock(return_value=True) - - # result = relation.promote_to_primary(mock_event) - - # assert result is None - def test__configure_standby_cluster(): mock_charm = MagicMock() mock_event = MagicMock() @@ -680,4 +663,50 @@ def test__configure_standby_cluster(): result = relation._configure_standby_cluster(mock_event) assert result is True - mock_check_call.assert_called_once() \ No newline at end of file + mock_check_call.assert_called_once() + +def test_wait_for_standby_leader(): + # 1. + mock_charm = MagicMock() + mock_event = MagicMock() + + relation = PostgreSQLAsyncReplication(mock_charm) + + mock_charm._patroni.get_standby_leader.return_value = None + mock_charm.unit.is_leader.return_value = False + mock_charm._patroni.is_member_isolated = True + mock_charm._patroni.restart_patroni = MagicMock() + + result = relation._wait_for_standby_leader(mock_event) + assert result is True + mock_charm._patroni.restart_patroni.assert_called_once() + mock_event.defer.assert_called_once() + + # 2. + mock_charm = MagicMock() + mock_event = MagicMock() + + relation = PostgreSQLAsyncReplication(mock_charm) + + mock_charm._patroni.get_standby_leader.return_value = None + mock_charm.unit.is_leader.return_value = False + mock_charm._patroni.is_member_isolated = False + + result = relation._wait_for_standby_leader(mock_event) + assert result is True + mock_event.defer.assert_called_once() + + # 3. + mock_charm = MagicMock() + mock_event = MagicMock() + + relation = PostgreSQLAsyncReplication(mock_charm) + + mock_charm._patroni.get_standby_leader.return_value = None + mock_charm.unit.is_leader.return_value = True + + result = relation._wait_for_standby_leader(mock_event) + assert result is False + +def test_update_primary_cluster_data(): + pass \ No newline at end of file From 9e93d786c52c0dbab1de1ea5a40dccfb6ef8bc13 Mon Sep 17 00:00:00 2001 From: Gere_X Date: Mon, 14 Jul 2025 13:07:41 +0200 Subject: [PATCH 21/35] formatize --- tests/unit/test_async_replication.py | 123 +++++++++++++-------------- 1 file changed, 58 insertions(+), 65 deletions(-) diff --git a/tests/unit/test_async_replication.py b/tests/unit/test_async_replication.py index 44eda7f3ff..05b0624a4b 100644 --- a/tests/unit/test_async_replication.py +++ b/tests/unit/test_async_replication.py @@ -1,19 +1,21 @@ +# Copyright 2024 Canonical Ltd. +# See LICENSE file for licensing details. + from unittest.mock import MagicMock, PropertyMock, patch -from ops.model import WaitingStatus import pytest +from ops.model import WaitingStatus from src.relations.async_replication import ( READ_ONLY_MODE_BLOCKING_MESSAGE, + REPLICATION_CONSUMER_RELATION, NotReadyError, PostgreSQLAsyncReplication, StandbyClusterAlreadyPromotedError, - REPLICATION_CONSUMER_RELATION, ) PEER = "peer" - def create_mock_unit(name="unit"): unit = MagicMock() unit.name = name @@ -459,7 +461,6 @@ def test__configure_primary_cluster(): relation._update_primary_cluster_data.assert_called_once() assert result is True - def test__on_async_relation_departed(): mock_charm = MagicMock() mock_event = MagicMock() @@ -473,7 +474,7 @@ def test__on_async_relation_departed(): relation = PostgreSQLAsyncReplication(mock_charm) result = relation._on_async_relation_departed(mock_event) - assert result is None + assert result is None assert mock_unit_data == {"departing": "True"} def test_on_async_relation_joined(): @@ -481,24 +482,24 @@ def test_on_async_relation_joined(): mock_charm = MagicMock() mock_event = MagicMock() mock_peers = MagicMock() - mock_unit_data = {} - + mock_unit_data = {} + mock_charm._unit_ip = "10.0.0.1" mock_charm._peers = mock_peers mock_peers.data = {mock_charm.unit: mock_unit_data} - + relation = PostgreSQLAsyncReplication(mock_charm) - + relation._get_highest_promoted_cluster_counter_value = MagicMock(return_value="1") - + result = relation._on_async_relation_joined(mock_event) - + assert result is None - + assert mock_unit_data == { "unit-promoted-cluster-counter": "1" } - + relation._get_highest_promoted_cluster_counter_value.assert_called_once() def test_on_create_replication(): @@ -506,11 +507,11 @@ def test_on_create_replication(): mock_charm = MagicMock() mock_event = MagicMock() relation = PostgreSQLAsyncReplication(mock_charm) - + relation._get_primary_cluster = MagicMock(return_value=True) - + result = relation._on_create_replication(mock_event) - + assert result is None mock_event.fail.assert_called_once_with( "There is already a replication set up." @@ -519,17 +520,17 @@ def test_on_create_replication(): # 2. mock_charm = MagicMock() mock_event = MagicMock() - + relation = PostgreSQLAsyncReplication(mock_charm) - + relation._get_primary_cluster = MagicMock(return_value=None) - + mock_relation = MagicMock() mock_relation.name = REPLICATION_CONSUMER_RELATION type(relation)._relation = PropertyMock(return_value=mock_relation) - + result = relation._on_create_replication(mock_event) - + assert result is None mock_event.fail.assert_called_once_with( "This action must be run in the cluster where the offer was created." @@ -537,39 +538,39 @@ def test_on_create_replication(): # 3. mock_charm = MagicMock() mock_event = MagicMock() - + relation = PostgreSQLAsyncReplication(mock_charm) - + relation._get_primary_cluster = MagicMock(return_value=None) relation._handle_replication_change = MagicMock(return_value=True) - + mock_relation = MagicMock() mock_relation.name = "Something" type(relation)._relation = PropertyMock(return_value=mock_relation) - + result = relation._on_create_replication(mock_event) - + assert result is None # 4. mock_charm = MagicMock() mock_event = MagicMock() - + relation = PostgreSQLAsyncReplication(mock_charm) - + relation._get_primary_cluster = MagicMock(return_value=None) relation._handle_replication_change = MagicMock(return_value=False) - + mock_relation = MagicMock() mock_relation.name = "Something" type(relation)._relation = PropertyMock(return_value=mock_relation) - + result = relation._on_create_replication(mock_event) - + assert result is None - + def test_promote_to_primary(): # 1. @@ -578,25 +579,24 @@ def test_promote_to_primary(): mock_relation = MagicMock() mock_relation.status = MagicMock() mock_relation.status.message = "Something" - + relation = PostgreSQLAsyncReplication(mock_charm) relation._get_primary_cluster = MagicMock(return_value = None) type(relation).app = PropertyMock(return_value=mock_relation) result = relation.promote_to_primary(mock_event) - assert result is None mock_event.fail.assert_called_once_with( "No primary cluster found. Run `create-replication` action in the cluster where the offer was created." ) - + # 2. mock_charm = MagicMock() mock_event = MagicMock() mock_relation = MagicMock() mock_relation.status = MagicMock() mock_relation.status.message = READ_ONLY_MODE_BLOCKING_MESSAGE - + relation = PostgreSQLAsyncReplication(mock_charm) relation._get_primary_cluster = MagicMock(return_value = None) @@ -610,58 +610,52 @@ def test_promote_to_primary(): def test__configure_standby_cluster(): mock_charm = MagicMock() mock_event = MagicMock() - + relation = PostgreSQLAsyncReplication(mock_charm) relation._relation = MagicMock() - relation._relation.name = REPLICATION_CONSUMER_RELATION + relation._relation.name = REPLICATION_CONSUMER_RELATION relation._update_internal_secret = MagicMock(return_value=False) result = relation._configure_standby_cluster(mock_event) - assert result is False mock_event.defer.assert_called_once() - # 2. - + # 2. mock_charm = MagicMock() mock_event = MagicMock() - + relation = PostgreSQLAsyncReplication(mock_charm) relation._relation = MagicMock() relation._relation.name = "something_else" relation._update_internal_secret = MagicMock(return_value=True) - relation.get_system_identifier = MagicMock(return_value=(None, 2)) - + relation.get_system_identifier = MagicMock(return_value=(None, 2)) with pytest.raises(Exception) as exc_info: relation._configure_standby_cluster(mock_event) - - assert str(exc_info.value) == "2" - # 3. - + assert str(exc_info.value) == "2" + + # 3. mock_charm = MagicMock() mock_event = MagicMock() - + relation = PostgreSQLAsyncReplication(mock_charm) relation._relation = MagicMock() - relation._relation.name = "some_relation" + relation._relation.name = "some_relation" relation._relation.app = "remote-app" - relation._relation.data = {relation._relation.app: {"system-id": "123"}} - + relation._relation.data = {relation._relation.app: {"system-id": "123"}} relation._update_internal_secret = MagicMock(return_value=True) - relation.get_system_identifier = MagicMock(return_value=("456", None)) + relation.get_system_identifier = MagicMock(return_value=("456", None)) relation.charm = MagicMock() relation.charm.app_peer_data = {} - with patch("subprocess.check_call") as mock_check_call: - + result = relation._configure_standby_cluster(mock_event) - + assert result is True mock_check_call.assert_called_once() @@ -669,8 +663,8 @@ def test_wait_for_standby_leader(): # 1. mock_charm = MagicMock() mock_event = MagicMock() - - relation = PostgreSQLAsyncReplication(mock_charm) + + relation = PostgreSQLAsyncReplication(mock_charm) mock_charm._patroni.get_standby_leader.return_value = None mock_charm.unit.is_leader.return_value = False @@ -685,9 +679,9 @@ def test_wait_for_standby_leader(): # 2. mock_charm = MagicMock() mock_event = MagicMock() - - relation = PostgreSQLAsyncReplication(mock_charm) - + + relation = PostgreSQLAsyncReplication(mock_charm) + mock_charm._patroni.get_standby_leader.return_value = None mock_charm.unit.is_leader.return_value = False mock_charm._patroni.is_member_isolated = False @@ -699,9 +693,8 @@ def test_wait_for_standby_leader(): # 3. mock_charm = MagicMock() mock_event = MagicMock() - - relation = PostgreSQLAsyncReplication(mock_charm) - + + relation = PostgreSQLAsyncReplication(mock_charm) mock_charm._patroni.get_standby_leader.return_value = None mock_charm.unit.is_leader.return_value = True @@ -709,4 +702,4 @@ def test_wait_for_standby_leader(): assert result is False def test_update_primary_cluster_data(): - pass \ No newline at end of file + pass From 350c579b5825667662357d44852ecacdd028c321 Mon Sep 17 00:00:00 2001 From: Gere_X Date: Mon, 14 Jul 2025 13:22:52 +0200 Subject: [PATCH 22/35] formatize 2.0 --- tests/unit/test_async_replication.py | 36 +++++++++++++++------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/tests/unit/test_async_replication.py b/tests/unit/test_async_replication.py index 05b0624a4b..9a9d52724d 100644 --- a/tests/unit/test_async_replication.py +++ b/tests/unit/test_async_replication.py @@ -16,6 +16,7 @@ PEER = "peer" + def create_mock_unit(name="unit"): unit = MagicMock() unit.name = name @@ -461,6 +462,7 @@ def test__configure_primary_cluster(): relation._update_primary_cluster_data.assert_called_once() assert result is True + def test__on_async_relation_departed(): mock_charm = MagicMock() mock_event = MagicMock() @@ -477,8 +479,8 @@ def test__on_async_relation_departed(): assert result is None assert mock_unit_data == {"departing": "True"} -def test_on_async_relation_joined(): +def test_on_async_relation_joined(): mock_charm = MagicMock() mock_event = MagicMock() mock_peers = MagicMock() @@ -496,12 +498,11 @@ def test_on_async_relation_joined(): assert result is None - assert mock_unit_data == { - "unit-promoted-cluster-counter": "1" - } + assert mock_unit_data == {"unit-promoted-cluster-counter": "1"} relation._get_highest_promoted_cluster_counter_value.assert_called_once() + def test_on_create_replication(): # 1. mock_charm = MagicMock() @@ -513,9 +514,7 @@ def test_on_create_replication(): result = relation._on_create_replication(mock_event) assert result is None - mock_event.fail.assert_called_once_with( - "There is already a replication set up." - ) + mock_event.fail.assert_called_once_with("There is already a replication set up.") # 2. mock_charm = MagicMock() @@ -533,8 +532,8 @@ def test_on_create_replication(): assert result is None mock_event.fail.assert_called_once_with( - "This action must be run in the cluster where the offer was created." - ) + "This action must be run in the cluster where the offer was created." + ) # 3. mock_charm = MagicMock() mock_event = MagicMock() @@ -571,34 +570,35 @@ def test_on_create_replication(): assert result is None -def test_promote_to_primary(): +def test_promote_to_primary(): # 1. mock_charm = MagicMock() mock_event = MagicMock() mock_relation = MagicMock() mock_relation.status = MagicMock() - mock_relation.status.message = "Something" + mock_relation.status.message = "Something" relation = PostgreSQLAsyncReplication(mock_charm) - relation._get_primary_cluster = MagicMock(return_value = None) + relation._get_primary_cluster = MagicMock(return_value=None) type(relation).app = PropertyMock(return_value=mock_relation) result = relation.promote_to_primary(mock_event) assert result is None + mock_event.fail.assert_called_once_with( - "No primary cluster found. Run `create-replication` action in the cluster where the offer was created." - ) + "No primary cluster found. Run `create-replication` action in the cluster where the offer was created." + ) # 2. mock_charm = MagicMock() mock_event = MagicMock() mock_relation = MagicMock() mock_relation.status = MagicMock() - mock_relation.status.message = READ_ONLY_MODE_BLOCKING_MESSAGE + mock_relation.status.message = READ_ONLY_MODE_BLOCKING_MESSAGE relation = PostgreSQLAsyncReplication(mock_charm) - relation._get_primary_cluster = MagicMock(return_value = None) + relation._get_primary_cluster = MagicMock(return_value=None) type(relation).app = PropertyMock(return_value=mock_relation) relation._handle_replication_change = MagicMock(return_value=False) @@ -607,6 +607,7 @@ def test_promote_to_primary(): assert result is None + def test__configure_standby_cluster(): mock_charm = MagicMock() mock_event = MagicMock() @@ -653,12 +654,12 @@ def test__configure_standby_cluster(): relation.charm.app_peer_data = {} with patch("subprocess.check_call") as mock_check_call: - result = relation._configure_standby_cluster(mock_event) assert result is True mock_check_call.assert_called_once() + def test_wait_for_standby_leader(): # 1. mock_charm = MagicMock() @@ -701,5 +702,6 @@ def test_wait_for_standby_leader(): result = relation._wait_for_standby_leader(mock_event) assert result is False + def test_update_primary_cluster_data(): pass From b26a18575e31439b9c08c34d8a5d00f0d5d6f291 Mon Sep 17 00:00:00 2001 From: Gere_X Date: Mon, 14 Jul 2025 14:53:41 +0200 Subject: [PATCH 23/35] test_get_partner_addresses --- tests/unit/test_async_replication.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_async_replication.py b/tests/unit/test_async_replication.py index 9a9d52724d..9b725956a4 100644 --- a/tests/unit/test_async_replication.py +++ b/tests/unit/test_async_replication.py @@ -703,5 +703,24 @@ def test_wait_for_standby_leader(): assert result is False -def test_update_primary_cluster_data(): +def test_get_partner_addresses(): + mock_charm = MagicMock() + + mock_charm._peer_members_ips = ["str"] + mock_charm.app = MagicMock() + mock_charm.unit = MagicMock() + mock_charm.unit.is_leader.return_value = True + mock_charm._peers = MagicMock() + mock_charm._peers.data = {mock_charm.unit: {}} + + relation = PostgreSQLAsyncReplication(mock_charm) + relation._get_primary_cluster = MagicMock(return_value=None) + relation._get_highest_promoted_cluster_counter_value = MagicMock(return_value=None) + + result = relation.get_partner_addresses() + + assert result == ["str"] + + +def test_get_primary_cluster(): pass From 5baadc4b9a9449c57c87302ce35623ab9fb4365b Mon Sep 17 00:00:00 2001 From: Gere_X Date: Mon, 14 Jul 2025 15:41:26 +0200 Subject: [PATCH 24/35] test_handle_replication_change --- tests/unit/test_async_replication.py | 50 ++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_async_replication.py b/tests/unit/test_async_replication.py index 9b725956a4..184355ae5a 100644 --- a/tests/unit/test_async_replication.py +++ b/tests/unit/test_async_replication.py @@ -722,5 +722,51 @@ def test_get_partner_addresses(): assert result == ["str"] -def test_get_primary_cluster(): - pass +def test_handle_replication_change(): + # 1. + mock_charm = MagicMock() + mock_event = MagicMock() + relation = PostgreSQLAsyncReplication(mock_charm) + relation._can_promote_cluster = MagicMock(return_value=False) + result = relation._handle_replication_change(mock_event) + assert result is False + # 2. + mock_charm = MagicMock() + mock_event = MagicMock() + relation = PostgreSQLAsyncReplication(mock_charm) + relation._can_promote_cluster = MagicMock(return_value=True) + relation.get_system_identifier = MagicMock(return_value=(12345, "some error")) + result = relation._handle_replication_change(mock_event) + assert result is False + + # 3. + mock_charm = MagicMock() + mock_event = MagicMock() + mock_relation = MagicMock() + + mock_unit1 = MagicMock() + mock_unit2 = MagicMock() + mock_relation.units = [mock_unit1, mock_unit2] + mock_relation.data = { + mock_unit1: {"unit-address": "10.0.0.1"}, + mock_unit2: {"unit-address": "10.0.0.2"}, + mock_charm.app: {}, + } + + relation = PostgreSQLAsyncReplication(mock_charm) + relation._relation = mock_relation + relation._can_promote_cluster = MagicMock(return_value=True) + relation.get_system_identifier = MagicMock(return_value=(12345, None)) + relation._get_highest_promoted_cluster_counter_value = MagicMock(return_value="1") + relation._update_primary_cluster_data = MagicMock() + relation._re_emit_async_relation_changed_event = MagicMock() + + result = relation._handle_replication_change(mock_event) + + assert result is True + relation._can_promote_cluster.assert_called_once_with(mock_event) + relation.get_system_identifier.assert_called_once() + relation._get_highest_promoted_cluster_counter_value.assert_called_once() + relation._update_primary_cluster_data.assert_called_once_with(2, 12345) + relation._re_emit_async_relation_changed_event.assert_called_once() + mock_event.fail.assert_not_called() From 6de0a6b7f34d9753d96eccbfcfc447fbd4d630e6 Mon Sep 17 00:00:00 2001 From: Gere_X Date: Mon, 14 Jul 2025 18:18:06 +0200 Subject: [PATCH 25/35] test_handle_forceful_promotion --- tests/unit/test_async_replication.py | 35 ++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/unit/test_async_replication.py b/tests/unit/test_async_replication.py index 184355ae5a..cab371ba83 100644 --- a/tests/unit/test_async_replication.py +++ b/tests/unit/test_async_replication.py @@ -5,6 +5,7 @@ import pytest from ops.model import WaitingStatus +from tenacity import RetryError from src.relations.async_replication import ( READ_ONLY_MODE_BLOCKING_MESSAGE, @@ -770,3 +771,37 @@ def test_handle_replication_change(): relation._update_primary_cluster_data.assert_called_once_with(2, 12345) relation._re_emit_async_relation_changed_event.assert_called_once() mock_event.fail.assert_not_called() + + +def test_handle_forceful_promotion(): + # 1. + mock_charm = MagicMock() + mock_event = MagicMock() + + mock_event.params.get.return_value = True + relation = PostgreSQLAsyncReplication(mock_charm) + result = relation._handle_forceful_promotion(mock_event) + + assert result is True + # 2. + mock_charm = MagicMock() + mock_event = MagicMock() + + mock_event.params.get.return_value = False + + relation = PostgreSQLAsyncReplication(mock_charm) + + relation._relation = MagicMock() + relation._relation.app = MagicMock() + relation._relation.app.name = "test-app" + + relation.get_all_primary_cluster_endpoints = MagicMock(return_value=[1, 2, 3]) + + mock_charm._patroni.get_primary.side_effect = RetryError("timeout") + + result = relation._handle_forceful_promotion(mock_event) + + mock_event.fail.assert_called_once_with( + "test-app isn't reachable. Pass `force=true` to promote anyway." + ) + assert result is False From 0d40c54fad8ac4974bc659ef49dd6ab872f76e19 Mon Sep 17 00:00:00 2001 From: Gere_X Date: Tue, 15 Jul 2025 09:02:03 +0200 Subject: [PATCH 26/35] add tests --- tests/unit/test_async_replication.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/unit/test_async_replication.py b/tests/unit/test_async_replication.py index cab371ba83..ebdbc05656 100644 --- a/tests/unit/test_async_replication.py +++ b/tests/unit/test_async_replication.py @@ -805,3 +805,22 @@ def test_handle_forceful_promotion(): "test-app isn't reachable. Pass `force=true` to promote anyway." ) assert result is False + # 3. + mock_charm = MagicMock() + mock_event = MagicMock() + + mock_event.params.get.return_value = False + + relation = PostgreSQLAsyncReplication(mock_charm) + + relation._relation = MagicMock() + relation._relation.app = MagicMock() + relation._relation.app.name = "test-app" + + relation.get_all_primary_cluster_endpoints = MagicMock(return_value=[1, 2, 3]) + + mock_charm._patroni.get_primary.side_effect = None + + result = relation._handle_forceful_promotion(mock_event) + + assert result is True From 5fc6790b3629b9f6294463b278ae9427453200b4 Mon Sep 17 00:00:00 2001 From: Gere_X Date: Tue, 15 Jul 2025 09:22:31 +0200 Subject: [PATCH 27/35] test_on_async_relation_broken --- tests/unit/test_async_replication.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/unit/test_async_replication.py b/tests/unit/test_async_replication.py index ebdbc05656..5025a44a48 100644 --- a/tests/unit/test_async_replication.py +++ b/tests/unit/test_async_replication.py @@ -824,3 +824,29 @@ def test_handle_forceful_promotion(): result = relation._handle_forceful_promotion(mock_event) assert result is True + + +def test_on_async_relation_broken(): + # 1. + mock_charm = MagicMock() + mock_event = MagicMock() + mock_charm._peers = True + + relation = PostgreSQLAsyncReplication(mock_charm) + + result = relation._on_async_relation_broken(mock_event) + + assert result is None + # 2. + mock_charm = MagicMock() + mock_charm._peers = MagicMock() + mock_charm.is_unit_departing = False + mock_charm._patroni.get_standby_leader.return_value = None + mock_charm.unit.is_leader.return_value = True + mock_event = MagicMock() + + relation = PostgreSQLAsyncReplication(mock_charm) + relation._on_async_relation_broken(mock_event) + + assert mock_charm.update_config.called + From b1dba430963a5b8902801d97204c261665ee0785 Mon Sep 17 00:00:00 2001 From: Gere_X Date: Tue, 15 Jul 2025 09:24:45 +0200 Subject: [PATCH 28/35] formatize --- tests/unit/test_async_replication.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/unit/test_async_replication.py b/tests/unit/test_async_replication.py index 5025a44a48..aa416b0d73 100644 --- a/tests/unit/test_async_replication.py +++ b/tests/unit/test_async_replication.py @@ -849,4 +849,3 @@ def test_on_async_relation_broken(): relation._on_async_relation_broken(mock_event) assert mock_charm.update_config.called - From 68e2b6667ba8f9f44c0f19ebb57e08260199117c Mon Sep 17 00:00:00 2001 From: Gere_X Date: Tue, 15 Jul 2025 15:53:29 +0200 Subject: [PATCH 29/35] finishing --- tests/unit/test_async_replication.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_async_replication.py b/tests/unit/test_async_replication.py index aa416b0d73..dee6175c02 100644 --- a/tests/unit/test_async_replication.py +++ b/tests/unit/test_async_replication.py @@ -15,8 +15,6 @@ StandbyClusterAlreadyPromotedError, ) -PEER = "peer" - def create_mock_unit(name="unit"): unit = MagicMock() @@ -824,6 +822,25 @@ def test_handle_forceful_promotion(): result = relation._handle_forceful_promotion(mock_event) assert result is True + # 4. + mock_charm = MagicMock() + mock_event = MagicMock() + + mock_event.params.get.return_value = False + + relation = PostgreSQLAsyncReplication(mock_charm) + + relation._relation = MagicMock() + relation._relation.app = MagicMock() + relation._relation.app.name = "test-app" + + relation.get_all_primary_cluster_endpoints = MagicMock(return_value=[]) + + mock_charm._patroni.get_primary.side_effect = None + + result = relation._handle_forceful_promotion(mock_event) + + assert result is True def test_on_async_relation_broken(): From 5047dec8c144bf41384edfc0b63617d30054ba57 Mon Sep 17 00:00:00 2001 From: Gere_X Date: Wed, 16 Jul 2025 12:44:16 +0200 Subject: [PATCH 30/35] need some help --- tests/unit/test_async_replication.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/unit/test_async_replication.py b/tests/unit/test_async_replication.py index dee6175c02..0d01ac4d5c 100644 --- a/tests/unit/test_async_replication.py +++ b/tests/unit/test_async_replication.py @@ -1,6 +1,7 @@ # Copyright 2024 Canonical Ltd. # See LICENSE file for licensing details. +# import json from unittest.mock import MagicMock, PropertyMock, patch import pytest @@ -10,6 +11,7 @@ from src.relations.async_replication import ( READ_ONLY_MODE_BLOCKING_MESSAGE, REPLICATION_CONSUMER_RELATION, + # REPLICATION_OFFER_RELATION, NotReadyError, PostgreSQLAsyncReplication, StandbyClusterAlreadyPromotedError, @@ -866,3 +868,13 @@ def test_on_async_relation_broken(): relation._on_async_relation_broken(mock_event) assert mock_charm.update_config.called + + +# def test_update_primary_cluster_data(): +# mock_charm = MagicMock() +# relation = PostgreSQLAsyncReplication(mock_charm) +# #relation._relation.data = json() +# relation._relation.name = REPLICATION_OFFER_RELATION + +# result = relation._update_primary_cluster_data(None, "Something") +# assert result is None From 80ecf4885a8897669d669a5feafcac373ffc5a90 Mon Sep 17 00:00:00 2001 From: Gere_X Date: Thu, 31 Jul 2025 14:21:01 +0200 Subject: [PATCH 31/35] date --- tests/unit/test_async_replication.py | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/tests/unit/test_async_replication.py b/tests/unit/test_async_replication.py index 0d01ac4d5c..40fe20b6df 100644 --- a/tests/unit/test_async_replication.py +++ b/tests/unit/test_async_replication.py @@ -1,7 +1,6 @@ -# Copyright 2024 Canonical Ltd. +# Copyright 2025 Canonical Ltd. # See LICENSE file for licensing details. -# import json from unittest.mock import MagicMock, PropertyMock, patch import pytest @@ -11,7 +10,6 @@ from src.relations.async_replication import ( READ_ONLY_MODE_BLOCKING_MESSAGE, REPLICATION_CONSUMER_RELATION, - # REPLICATION_OFFER_RELATION, NotReadyError, PostgreSQLAsyncReplication, StandbyClusterAlreadyPromotedError, @@ -868,13 +866,3 @@ def test_on_async_relation_broken(): relation._on_async_relation_broken(mock_event) assert mock_charm.update_config.called - - -# def test_update_primary_cluster_data(): -# mock_charm = MagicMock() -# relation = PostgreSQLAsyncReplication(mock_charm) -# #relation._relation.data = json() -# relation._relation.name = REPLICATION_OFFER_RELATION - -# result = relation._update_primary_cluster_data(None, "Something") -# assert result is None From 76868c6b1a0d30a14f962cc2a9589aa82edb9ae3 Mon Sep 17 00:00:00 2001 From: Gere_X Date: Thu, 31 Jul 2025 14:37:38 +0200 Subject: [PATCH 32/35] add aplications --- tests/unit/test_async_replication.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/unit/test_async_replication.py b/tests/unit/test_async_replication.py index 40fe20b6df..9d5995adba 100644 --- a/tests/unit/test_async_replication.py +++ b/tests/unit/test_async_replication.py @@ -6,6 +6,7 @@ import pytest from ops.model import WaitingStatus from tenacity import RetryError +from ops import Application from src.relations.async_replication import ( READ_ONLY_MODE_BLOCKING_MESSAGE, @@ -49,7 +50,7 @@ def test_can_promote_cluster(): _handle_forceful_promotion=MagicMock(return_value=False), ): mock_charm._patroni = MagicMock() - mock_charm._patroni.get_standby_leader.return_value = "standby-leader" + mock_charm._patroni.get_standby_leader.return_value = "unit-name" mock_charm._patroni.promote_standby_cluster.return_value = True mock_charm.app.status.message = READ_ONLY_MODE_BLOCKING_MESSAGE mock_charm._peers = MagicMock() @@ -508,7 +509,8 @@ def test_on_create_replication(): mock_event = MagicMock() relation = PostgreSQLAsyncReplication(mock_charm) - relation._get_primary_cluster = MagicMock(return_value=True) + mock_application = MagicMock(spec=Application) + relation._get_primary_cluster = MagicMock(return_value=mock_application) result = relation._on_create_replication(mock_event) @@ -593,13 +595,14 @@ def test_promote_to_primary(): mock_charm = MagicMock() mock_event = MagicMock() mock_relation = MagicMock() + mock_app = MagicMock(spec=Application) mock_relation.status = MagicMock() mock_relation.status.message = READ_ONLY_MODE_BLOCKING_MESSAGE relation = PostgreSQLAsyncReplication(mock_charm) relation._get_primary_cluster = MagicMock(return_value=None) - type(relation).app = PropertyMock(return_value=mock_relation) + type(relation).app = PropertyMock(return_value=mock_app) relation._handle_replication_change = MagicMock(return_value=False) result = relation.promote_to_primary(mock_event) From 5f7f434650544dca5865f755f1310e6fa1100dca Mon Sep 17 00:00:00 2001 From: Gere_X Date: Thu, 31 Jul 2025 14:40:00 +0200 Subject: [PATCH 33/35] format --- tests/unit/test_async_replication.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_async_replication.py b/tests/unit/test_async_replication.py index 9d5995adba..b32d9d5113 100644 --- a/tests/unit/test_async_replication.py +++ b/tests/unit/test_async_replication.py @@ -4,9 +4,9 @@ from unittest.mock import MagicMock, PropertyMock, patch import pytest +from ops import Application from ops.model import WaitingStatus from tenacity import RetryError -from ops import Application from src.relations.async_replication import ( READ_ONLY_MODE_BLOCKING_MESSAGE, From 5715edf88ad33efbcde80bd3fcc04247ed2567c8 Mon Sep 17 00:00:00 2001 From: Gere_X Date: Thu, 31 Jul 2025 14:48:49 +0200 Subject: [PATCH 34/35] promote_standby_cluster is none --- tests/unit/test_async_replication.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_async_replication.py b/tests/unit/test_async_replication.py index b32d9d5113..6af4e6d577 100644 --- a/tests/unit/test_async_replication.py +++ b/tests/unit/test_async_replication.py @@ -51,7 +51,7 @@ def test_can_promote_cluster(): ): mock_charm._patroni = MagicMock() mock_charm._patroni.get_standby_leader.return_value = "unit-name" - mock_charm._patroni.promote_standby_cluster.return_value = True + mock_charm._patroni.promote_standby_cluster.return_value = None mock_charm.app.status.message = READ_ONLY_MODE_BLOCKING_MESSAGE mock_charm._peers = MagicMock() mock_charm._peers.data = {mock_charm.app: mock_peers_data} From e1feb84047cec8bd53a1667fa7ba25d3003f0e0a Mon Sep 17 00:00:00 2001 From: Gere_X Date: Thu, 31 Jul 2025 14:50:50 +0200 Subject: [PATCH 35/35] add unit name --- tests/unit/test_async_replication.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_async_replication.py b/tests/unit/test_async_replication.py index 6af4e6d577..0174a0080d 100644 --- a/tests/unit/test_async_replication.py +++ b/tests/unit/test_async_replication.py @@ -50,7 +50,7 @@ def test_can_promote_cluster(): _handle_forceful_promotion=MagicMock(return_value=False), ): mock_charm._patroni = MagicMock() - mock_charm._patroni.get_standby_leader.return_value = "unit-name" + mock_charm._patroni.get_standby_leader.return_value = "postgresql/1" mock_charm._patroni.promote_standby_cluster.return_value = None mock_charm.app.status.message = READ_ONLY_MODE_BLOCKING_MESSAGE mock_charm._peers = MagicMock()