11# Copyright 2023 Canonical Ltd.
22# See LICENSE file for licensing details.
33import signal
4- from unittest .mock import Mock , PropertyMock , patch
4+ import sys
5+ from json import dumps
6+ from unittest .mock import Mock , PropertyMock , call , patch , sentinel
57
68import pytest
79from ops .charm import CharmBase
1315 ClusterTopologyChangeCharmEvents ,
1416 ClusterTopologyObserver ,
1517)
16- from scripts .cluster_topology_observer import dispatch
17-
18-
19- # This method will be used by the mock to replace requests.get
20- def mocked_requests_get (* args , ** kwargs ):
21- class MockResponse :
22- def __init__ (self , json_data ):
23- self .json_data = json_data
24-
25- def json (self ):
26- return self .json_data
27-
28- data = {
29- "http://server1/cluster" : {
30- "members" : [{"name" : "postgresql-0" , "host" : "1.1.1.1" , "role" : "leader" , "lag" : "1" }]
31- }
32- }
33- if args [0 ] in data :
34- return MockResponse (data [args [0 ]])
18+ from scripts .cluster_topology_observer import UnreachableUnitsError , dispatch , main
3519
3620
3721class MockCharm (CharmBase ):
@@ -48,7 +32,7 @@ def _on_cluster_topology_change(self, _) -> None:
4832
4933 @property
5034 def _patroni (self ) -> Patroni :
51- return Mock (_patroni_url = "http://1.1.1.1:8008/" , verify = True )
35+ return Mock (_patroni_url = "http://1.1.1.1:8008/" , peers_ips = {}, verify = True )
5236
5337 @property
5438 def _peers (self ) -> Relation | None :
@@ -153,3 +137,54 @@ def test_dispatch(harness):
153137 harness .charm .unit .name ,
154138 f"JUJU_DISPATCH_PATH=hooks/cluster_topology_change { charm_dir } /dispatch" ,
155139 ])
140+
141+
142+ def test_main ():
143+ with (
144+ patch .object (
145+ sys ,
146+ "argv" ,
147+ ["cmd" , "http://server1:8008,http://server2:8008" , "run_cmd" , "unit/0" , "charm_dir" ],
148+ ),
149+ patch ("scripts.cluster_topology_observer.sleep" , return_value = None ),
150+ patch ("scripts.cluster_topology_observer.urlopen" ) as _urlopen ,
151+ patch ("scripts.cluster_topology_observer.subprocess" ) as _subprocess ,
152+ patch (
153+ "scripts.cluster_topology_observer.create_default_context" ,
154+ return_value = sentinel .sslcontext ,
155+ ),
156+ ):
157+ response1 = {
158+ "members" : [
159+ {"name" : "unit-2" , "api_url" : "http://server3:8008/patroni" , "role" : "standby" },
160+ {"name" : "unit-0" , "api_url" : "http://server1:8008/patroni" , "role" : "leader" },
161+ ]
162+ }
163+ mock1 = Mock ()
164+ mock1 .read .return_value = dumps (response1 )
165+ response2 = {
166+ "members" : [
167+ {"name" : "unit-2" , "api_url" : "https://server3:8008/patroni" , "role" : "leader" },
168+ ]
169+ }
170+ mock2 = Mock ()
171+ mock2 .read .return_value = dumps (response2 )
172+ _urlopen .side_effect = [mock1 , Exception , mock2 ]
173+ with pytest .raises (UnreachableUnitsError ):
174+ main ()
175+ assert _urlopen .call_args_list == [
176+ # Iteration 1. server2 is not called
177+ call ("http://server1:8008/cluster" , timeout = 5 , context = sentinel .sslcontext ),
178+ # Iteration 2 local unit server1 is called first
179+ call ("http://server1:8008/cluster" , timeout = 5 , context = sentinel .sslcontext ),
180+ call ("http://server3:8008/cluster" , timeout = 5 , context = sentinel .sslcontext ),
181+ # Iteration 3 Last known member is server3
182+ call ("https://server3:8008/cluster" , timeout = 5 , context = sentinel .sslcontext ),
183+ ]
184+
185+ _subprocess .run .assert_called_once_with ([
186+ "run_cmd" ,
187+ "-u" ,
188+ "unit/0" ,
189+ "JUJU_DISPATCH_PATH=hooks/cluster_topology_change charm_dir/dispatch" ,
190+ ])
0 commit comments