1- from unittest .mock import MagicMock , patch
2-
3- import requests
4-
1+ import asyncio
2+ from unittest .mock import AsyncMock , patch , MagicMock
53from bsv .fee_models .live_policy import LivePolicy
64
7-
5+ # Reset the singleton instance before each test
86def setup_function (_ ):
97 LivePolicy ._instance = None
108
11-
9+ # Reset the singleton instance after each test
1210def teardown_function (_ ):
1311 LivePolicy ._instance = None
1412
15-
16- def _mock_response (payload ):
17- response = MagicMock ()
18- response .raise_for_status .return_value = None
19- response .json .return_value = payload
20- return response
21-
22-
23- @patch ("bsv.fee_models.live_policy.requests.get" )
24- def test_parses_mining_fee (mock_get ):
25- payload = {
26- "policy" : {
27- "fees" : {
28- "miningFee" : {"satoshis" : 5 , "bytes" : 250 }
13+ @patch ("bsv.fee_models.live_policy.default_http_client" , autospec = True )
14+ def test_parses_mining_fee (mock_http_client_factory ):
15+ # Prepare the mocked DefaultHttpClient instance
16+ mock_http_client = AsyncMock ()
17+ mock_http_client_factory .return_value = mock_http_client
18+
19+ # Set up a mock response
20+ mock_http_client .get .return_value .json_data = {
21+ "data" : {
22+ "policy" : {
23+ "fees" : {
24+ "miningFee" : {"satoshis" : 5 , "bytes" : 250 }
25+ }
2926 }
3027 }
3128 }
32- mock_get .return_value = _mock_response (payload )
33-
34- policy = LivePolicy (cache_ttl_ms = 60000 , fallback_sat_per_kb = 1 )
35-
36- assert policy .current_rate_sat_per_kb () == 20
3729
30+ # Create the test instance
31+ policy = LivePolicy (
32+ cache_ttl_ms = 60000 ,
33+ fallback_sat_per_kb = 1 ,
34+ arc_policy_url = "https://arc.mock/policy"
35+ )
36+
37+ # Execute and verify the result
38+ rate = asyncio .run (policy .current_rate_sat_per_kb ())
39+ assert rate == 20
40+ mock_http_client .get .assert_called_once ()
41+
42+
43+ @patch ("bsv.fee_models.live_policy.default_http_client" , autospec = True )
44+ def test_cache_reused_when_valid (mock_http_client_factory ):
45+ # Prepare the mocked DefaultHttpClient instance
46+ mock_http_client = AsyncMock ()
47+ mock_http_client_factory .return_value = mock_http_client
48+
49+ # Set up a mock response
50+ mock_http_client .get .return_value .json_data = {
51+ "data" : {
52+ "policy" : {"satPerKb" : 50 }
53+ }
54+ }
3855
39- @patch ("bsv.fee_models.live_policy.requests.get" )
40- def test_cache_reused_when_valid (mock_get ):
41- payload = {"policy" : {"satPerKb" : 50 }}
42- mock_get .return_value = _mock_response (payload )
43-
44- policy = LivePolicy (cache_ttl_ms = 60000 , fallback_sat_per_kb = 1 )
56+ policy = LivePolicy (
57+ cache_ttl_ms = 60000 ,
58+ fallback_sat_per_kb = 1 ,
59+ arc_policy_url = "https://arc.mock/policy"
60+ )
4561
46- first = policy .current_rate_sat_per_kb ()
47- second = policy .current_rate_sat_per_kb ()
62+ # Call multiple times within the cache validity period
63+ first_rate = asyncio .run (policy .current_rate_sat_per_kb ())
64+ second_rate = asyncio .run (policy .current_rate_sat_per_kb ())
4865
49- assert first == 50
50- assert second == 50
51- mock_get .assert_called_once ()
66+ # Verify the results
67+ assert first_rate == 50
68+ assert second_rate == 50
69+ mock_http_client .get .assert_called_once ()
5270
5371
54- @patch ("bsv.fee_models.live_policy.requests.get" )
72+ @patch ("bsv.fee_models.live_policy.default_http_client" , autospec = True )
5573@patch ("bsv.fee_models.live_policy.logger.warning" )
56- def test_uses_cached_value_when_fetch_fails (mock_log , mock_get ):
57- payload = {"policy" : {"satPerKb" : 75 }}
58- mock_get .side_effect = [
59- _mock_response (payload ),
60- requests .RequestException ("Network down" ),
74+ def test_uses_cached_value_when_fetch_fails (mock_log , mock_http_client_factory ):
75+ # Prepare the mocked DefaultHttpClient instance
76+ mock_http_client = AsyncMock ()
77+ mock_http_client_factory .return_value = mock_http_client
78+
79+ # Set up mock responses (success first, then failure)
80+ mock_http_client .get .side_effect = [
81+ AsyncMock (json_data = {"data" : {"policy" : {"satPerKb" : 75 }}}),
82+ Exception ("Network down" )
6183 ]
6284
63- policy = LivePolicy (cache_ttl_ms = 1 , fallback_sat_per_kb = 5 )
85+ policy = LivePolicy (
86+ cache_ttl_ms = 1 ,
87+ fallback_sat_per_kb = 5 ,
88+ arc_policy_url = "https://arc.mock/policy"
89+ )
6490
65- first = policy .current_rate_sat_per_kb ()
66- assert first == 75
91+ # The first execution succeeds
92+ first_rate = asyncio .run (policy .current_rate_sat_per_kb ())
93+ assert first_rate == 75
6794
68- # Expire cache manually
95+ # Force invalidation of the cache
6996 with policy ._cache_lock :
7097 policy ._cache .fetched_at_ms -= 10
7198
72- second = policy .current_rate_sat_per_kb ()
73- assert second == 75
99+ # The second execution uses the cache
100+ second_rate = asyncio .run (policy .current_rate_sat_per_kb ())
101+ assert second_rate == 75
74102
103+ # Verify that a log is recorded for cache usage
75104 assert mock_log .call_count == 1
76105 args , _ = mock_log .call_args
77106 assert args [0 ] == "Failed to fetch live fee rate, using cached value: %s"
78- assert isinstance (args [1 ], requests .RequestException )
79- assert str (args [1 ]) == "Network down"
107+ mock_http_client .get .assert_called ()
80108
81109
82- @patch ("bsv.fee_models.live_policy.requests.get " , side_effect = requests . RequestException ( "boom" ) )
110+ @patch ("bsv.fee_models.live_policy.default_http_client " , autospec = True )
83111@patch ("bsv.fee_models.live_policy.logger.warning" )
84- def test_falls_back_to_default_when_no_cache (mock_log , _mock_get ):
85- policy = LivePolicy (cache_ttl_ms = 60000 , fallback_sat_per_kb = 9 )
112+ def test_falls_back_to_default_when_no_cache (mock_log , mock_http_client_factory ):
113+ # Prepare the mocked DefaultHttpClient instance
114+ mock_http_client = AsyncMock ()
115+ mock_http_client_factory .return_value = mock_http_client
86116
87- assert policy .current_rate_sat_per_kb () == 9
117+ # Set up a mock response (always failing)
118+ mock_http_client .get .side_effect = Exception ("Network failure" )
88119
120+ policy = LivePolicy (
121+ cache_ttl_ms = 60000 ,
122+ fallback_sat_per_kb = 9 ,
123+ arc_policy_url = "https://arc.mock/policy"
124+ )
125+
126+ # Fallback value is returned during execution
127+ rate = asyncio .run (policy .current_rate_sat_per_kb ())
128+ assert rate == 9
129+
130+ # Verify that a log is recorded
89131 assert mock_log .call_count == 1
90132 args , _ = mock_log .call_args
91133 assert args [0 ] == "Failed to fetch live fee rate, using fallback %d sat/kB: %s"
92134 assert args [1 ] == 9
93- assert isinstance (args [2 ], requests .RequestException )
94- assert str (args [2 ]) == "boom"
135+ mock_http_client .get .assert_called ()
95136
96137
97- @patch ("bsv.fee_models.live_policy.requests.get" )
138+ @patch ("bsv.fee_models.live_policy.default_http_client" , autospec = True )
98139@patch ("bsv.fee_models.live_policy.logger.warning" )
99- def test_invalid_response_triggers_fallback (mock_log , mock_get ):
100- mock_get .return_value = _mock_response ({"policy" : {"invalid" : True }})
140+ def test_invalid_response_triggers_fallback (mock_log , mock_http_client_factory ):
141+ # Prepare the mocked DefaultHttpClient instance
142+ mock_http_client = AsyncMock ()
143+ mock_http_client_factory .return_value = mock_http_client
144+
145+ # Set up an invalid response
146+ mock_http_client .get .return_value .json_data = {
147+ "data" : {"policy" : {"invalid" : True }}
148+ }
101149
102- policy = LivePolicy (cache_ttl_ms = 60000 , fallback_sat_per_kb = 3 )
150+ policy = LivePolicy (
151+ cache_ttl_ms = 60000 ,
152+ fallback_sat_per_kb = 3 ,
153+ arc_policy_url = "https://arc.mock/policy"
154+ )
103155
104- assert policy .current_rate_sat_per_kb () == 3
156+ # Fallback value is returned due to the invalid response
157+ rate = asyncio .run (policy .current_rate_sat_per_kb ())
158+ assert rate == 3
105159
160+ # Verify that a log is recorded
106161 assert mock_log .call_count == 1
107162 args , _ = mock_log .call_args
108163 assert args [0 ] == "Failed to fetch live fee rate, using fallback %d sat/kB: %s"
109164 assert args [1 ] == 3
110- assert isinstance (args [2 ], ValueError )
111- assert str (args [2 ]) == "Invalid policy response format"
112-
113-
114- def test_singleton_returns_same_instance ():
115- first = LivePolicy .get_instance (cache_ttl_ms = 10000 )
116- second = LivePolicy .get_instance (cache_ttl_ms = 20000 )
117-
118- assert first is second
119- assert first .cache_ttl_ms == 10000
120-
121-
122- def test_custom_instance_uses_provided_ttl ():
123- policy = LivePolicy (cache_ttl_ms = 30000 )
124- assert policy .cache_ttl_ms == 30000
125-
126-
127- @patch ("bsv.fee_models.live_policy.requests.get" )
128- def test_singleton_cache_shared (mock_get ):
129- payload = {"policy" : {"satPerKb" : 25 }}
130- mock_get .return_value = _mock_response (payload )
131-
132- policy1 = LivePolicy .get_instance ()
133- policy2 = LivePolicy .get_instance ()
134-
135- assert policy1 is policy2
136- assert policy1 .current_rate_sat_per_kb () == 25
137- assert policy2 .current_rate_sat_per_kb () == 25
138- mock_get .assert_called_once ()
165+ mock_http_client .get .assert_called ()
0 commit comments