|
| 1 | +import json |
| 2 | +import os |
| 3 | +import time |
| 4 | +import unittest |
| 5 | + |
| 6 | +from unittest.mock import patch |
| 7 | +from network_stub import NetworkStub |
| 8 | +from statsig import StatsigOptions, statsig, StatsigUser |
| 9 | +from statsig.evaluation_details import EvaluationDetails, EvaluationReason |
| 10 | +from statsig.http_worker import HttpWorker |
| 11 | + |
| 12 | +_network_stub = NetworkStub("http://test-sync-config-fallback", mock_statsig_api=True) |
| 13 | +with open(os.path.join(os.path.abspath(os.path.dirname(__file__)), '../testdata/download_config_specs.json')) as r: |
| 14 | + CONFIG_SPECS_RESPONSE = r.read() |
| 15 | + |
| 16 | +class TestSyncConfigFallback(unittest.TestCase): |
| 17 | + @classmethod |
| 18 | + @patch('requests.request', side_effect=_network_stub.mock) |
| 19 | + def setUpClass(cls, mock_proxy): |
| 20 | + cls.dcs_hit = 0 |
| 21 | + _network_stub.reset() |
| 22 | + def dcs_proxy_callback(url: str, **kwargs): |
| 23 | + cls.dcs_hit += 1 |
| 24 | + return json.loads(CONFIG_SPECS_RESPONSE) |
| 25 | + |
| 26 | + _network_stub.stub_request_with_function( |
| 27 | + "download_config_specs/.*", 200, dcs_proxy_callback) |
| 28 | + |
| 29 | + cls. test_user = StatsigUser( "123", email="[email protected]") |
| 30 | + |
| 31 | + def tearDown(self): |
| 32 | + self.dcs_hit = 0 |
| 33 | + statsig.shutdown() |
| 34 | + |
| 35 | + @patch('requests.request', side_effect=_network_stub.mock) |
| 36 | + @patch.object(HttpWorker, 'get_dcs_fallback') |
| 37 | + def test_default_behavior(self, fallback_mock, request_mock): |
| 38 | + # default behavior is no fallback if is out of sync |
| 39 | + options = StatsigOptions(api=_network_stub.host, fallback_to_statsig_api=True, rulesets_sync_interval=1) |
| 40 | + statsig.initialize("secret-key", options) |
| 41 | + gate = statsig.get_feature_gate(self.test_user, "always_on_gate") |
| 42 | + eval_detail: EvaluationDetails = gate.get_evaluation_details() |
| 43 | + self.assertEqual(eval_detail.reason, EvaluationReason.network) |
| 44 | + self.assertEqual(eval_detail.config_sync_time, 1631638014811) |
| 45 | + time.sleep(1.1) |
| 46 | + |
| 47 | + fallback_mock.assert_not_called() |
| 48 | + |
| 49 | + @patch('requests.request', side_effect=_network_stub.mock) |
| 50 | + @patch.object(HttpWorker, 'get_dcs_fallback') |
| 51 | + def test_fallback_when_out_of_sync(self, fallback_mock, request_mock): |
| 52 | + # default behavior is no fallback if is out of sync |
| 53 | + options = StatsigOptions(api_for_download_config_specs=_network_stub.host, fallback_to_statsig_api=True, rulesets_sync_interval=1, out_of_sync_threshold_in_s=0.5) |
| 54 | + statsig.initialize("secret-key", options) |
| 55 | + gate = statsig.get_feature_gate(self.test_user, "always_on_gate") |
| 56 | + eval_detail: EvaluationDetails = gate.get_evaluation_details() |
| 57 | + self.assertEqual(eval_detail.reason, EvaluationReason.network) |
| 58 | + self.assertEqual(eval_detail.config_sync_time, 1631638014811) |
| 59 | + time.sleep(1.1) |
| 60 | + #ensure it falls back |
| 61 | + fallback_mock.assert_called_once() |
| 62 | + |
| 63 | + @patch('requests.request', side_effect=_network_stub.mock) |
| 64 | + @patch.object(HttpWorker, 'get_dcs_fallback') |
| 65 | + def test_behavior_when_not_out_of_sync(self, fallback_mock, request_mock): |
| 66 | + # default behavior is no fallback if is out of sync |
| 67 | + options = StatsigOptions(api_for_download_config_specs=_network_stub.host, fallback_to_statsig_api=True, rulesets_sync_interval=1, out_of_sync_threshold_in_s=4e10) |
| 68 | + statsig.initialize("secret-key", options) |
| 69 | + gate = statsig.get_feature_gate(self.test_user, "always_on_gate") |
| 70 | + eval_detail: EvaluationDetails = gate.get_evaluation_details() |
| 71 | + self.assertEqual(eval_detail.reason, EvaluationReason.network) |
| 72 | + self.assertEqual(eval_detail.config_sync_time, 1631638014811) |
| 73 | + time.sleep(1.1) |
| 74 | + #ensure no fallback |
| 75 | + fallback_mock.assert_not_called() |
0 commit comments