File: test_snapshots.py

package info (click to toggle)
python-azure 20251118%2Bgit-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 783,356 kB
  • sloc: python: 6,474,533; ansic: 804; javascript: 287; sh: 205; makefile: 198; xml: 109
file content (314 lines) | stat: -rw-r--r-- 15,029 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
# -------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for
# license information.
# --------------------------------------------------------------------------

import pytest
import time
from azure.appconfiguration.provider._models import SettingSelector
from azure.appconfiguration.provider._constants import NULL_CHAR
from azure.appconfiguration.provider import load, WatchKey
from azure.appconfiguration import (
    ConfigurationSetting,
    ConfigurationSettingsFilter,
    SnapshotComposition,
    SnapshotStatus,
)
from azure.core.exceptions import ResourceNotFoundError
from devtools_testutils import recorded_by_proxy, is_live
from preparers import app_config_decorator
from testcase import AppConfigTestCase


class TestSnapshotSupport:
    """Tests for snapshot functionality in SettingSelector."""

    def test_setting_selector_with_snapshot_name(self):
        """Test SettingSelector with snapshot_name parameter."""
        selector = SettingSelector(snapshot_name="my-snapshot")
        assert selector.snapshot_name == "my-snapshot"
        assert selector.key_filter is None
        assert selector.label_filter == NULL_CHAR
        assert selector.tag_filters is None

    def test_setting_selector_snapshot_name_with_key_filter_raises_error(self):
        """Test that SettingSelector raises ValueError when both snapshot_name and key_filter are provided."""
        with pytest.raises(
            ValueError,
            match=r"Cannot specify both snapshot_name and key_filter\. "
            r"When using snapshots, all other filtering parameters are ignored\.",
        ):
            SettingSelector(snapshot_name="my-snapshot", key_filter="*")

    def test_setting_selector_snapshot_name_with_label_filter_raises_error(self):
        """Test that SettingSelector raises ValueError when both snapshot_name and label_filter are provided."""
        with pytest.raises(
            ValueError,
            match=r"Cannot specify both snapshot_name and label_filter\. "
            r"When using snapshots, all other filtering parameters are ignored\.",
        ):
            SettingSelector(snapshot_name="my-snapshot", label_filter="prod")

    def test_setting_selector_snapshot_name_with_tag_filters_raises_error(self):
        """Test that SettingSelector raises ValueError when both snapshot_name and tag_filters are provided."""
        with pytest.raises(
            ValueError,
            match=r"Cannot specify both snapshot_name and tag_filters\. "
            r"When using snapshots, all other filtering parameters are ignored\.",
        ):
            SettingSelector(snapshot_name="my-snapshot", tag_filters=["env=prod"])

    def test_setting_selector_requires_key_filter_or_snapshot_name(self):
        """Test that SettingSelector requires either key_filter or snapshot_name."""
        with pytest.raises(ValueError, match=r"Either key_filter or snapshot_name must be specified\."):
            SettingSelector()

    def test_setting_selector_valid_combinations(self):
        """Test valid combinations of SettingSelector parameters."""
        # Valid: key_filter only
        selector = SettingSelector(key_filter="*")
        assert selector.key_filter == "*"
        assert selector.snapshot_name is None

        # Valid: snapshot_name only
        selector = SettingSelector(snapshot_name="my-snapshot")
        assert selector.snapshot_name == "my-snapshot"
        assert selector.key_filter is None

        # Valid: key_filter with other filters (no snapshot_name)
        selector = SettingSelector(key_filter="*", label_filter="prod", tag_filters=["env=prod"])
        assert selector.key_filter == "*"
        assert selector.label_filter == "prod"
        assert selector.tag_filters == ["env=prod"]
        assert selector.snapshot_name is None

    def test_feature_flag_selectors_with_snapshot_raises_error(self):
        """Test that feature_flag_selectors with snapshot_name raises ValueError during validation."""
        with pytest.raises(
            ValueError,
            match=r"snapshot_name cannot be used with feature_flag_selectors\. "
            r"Use snapshot_name with regular selects instead to load feature flags from snapshots\.",
        ):
            load(
                connection_string="Endpoint=test;Id=test;Secret=test",
                feature_flag_enabled=True,
                feature_flag_selectors=[SettingSelector(snapshot_name="my-snapshot")],
            )


class TestSnapshotProviderIntegration(AppConfigTestCase):
    """Integration tests for snapshot functionality with recorded tests."""

    def test_setting_selector_multiple_combinations(self):
        """Test using multiple selectors with mix of snapshot and regular selectors."""
        # This should be valid - mixing snapshot and regular selectors
        selectors = [
            SettingSelector(snapshot_name="config-snapshot"),  # Load from snapshot
            SettingSelector(key_filter="runtime.*"),  # Load runtime configs from current state
        ]

        # Validate each selector individually
        assert selectors[0].snapshot_name == "config-snapshot"
        assert selectors[0].key_filter is None

        assert selectors[1].key_filter == "runtime.*"
        assert selectors[1].snapshot_name is None

    def test_snapshot_with_different_names(self):
        """Test snapshot selectors with different snapshot names."""
        # Different snapshot names should be valid
        snapshot1 = SettingSelector(snapshot_name="prod-config-v1.0")
        snapshot2 = SettingSelector(snapshot_name="staging-config-v2.1")

        assert snapshot1.snapshot_name == "prod-config-v1.0"
        assert snapshot2.snapshot_name == "staging-config-v2.1"

        # Both should have no other filters
        for selector in [snapshot1, snapshot2]:
            assert selector.key_filter is None
            assert selector.label_filter == NULL_CHAR
            assert selector.tag_filters is None

    @app_config_decorator
    @recorded_by_proxy
    def test_load_provider_with_snapshot_not_found(self, appconfiguration_connection_string):
        """Test loading provider with a non-existent snapshot returns error."""
        # Try to load from a non-existent snapshot
        with pytest.raises(ResourceNotFoundError):
            self.create_client(
                connection_string=appconfiguration_connection_string,
                selects=[SettingSelector(snapshot_name="non-existent-snapshot")],
            )

    @app_config_decorator
    @recorded_by_proxy
    def test_load_provider_with_regular_selectors(self, appconfiguration_connection_string):
        """Test loading provider with regular selectors works (baseline test)."""
        # This should work - regular selector loading
        provider = self.create_client(
            connection_string=appconfiguration_connection_string,
            selects=[SettingSelector(key_filter="message")],  # Regular selector
        )

        # Verify we can access the configuration (message is set up by setup_configs)
        assert "message" in provider

    @app_config_decorator
    @recorded_by_proxy
    def test_snapshot_selector_parameter_validation_in_provider(self, appconfiguration_connection_string):
        """Test that snapshot selector parameter validation works when loading provider."""
        # Test that feature flag selectors with snapshots are rejected
        with pytest.raises(ValueError, match="snapshot_name cannot be used with feature_flag_selectors"):
            self.create_client(
                connection_string=appconfiguration_connection_string,
                feature_flag_enabled=True,
                feature_flag_selectors=[SettingSelector(snapshot_name="test-snapshot")],
            )

    @app_config_decorator
    @recorded_by_proxy
    def test_create_snapshot_and_load_provider(self, appconfiguration_connection_string, **kwargs):
        """Test creating a snapshot and loading provider from it."""
        # Create SDK client for setup
        sdk_client = self.create_sdk_client(appconfiguration_connection_string)

        # Create unique test configuration settings for the snapshot
        test_settings = [
            ConfigurationSetting(key="snapshot_test_key1", value="snapshot_test_value1", label=NULL_CHAR),
            ConfigurationSetting(key="snapshot_test_key2", value="snapshot_test_value2", label=NULL_CHAR),
            ConfigurationSetting(
                key="snapshot_test_json",
                value='{"nested": "snapshot_value"}',
                label=NULL_CHAR,
                content_type="application/json",
            ),
            ConfigurationSetting(key="refresh_test_key", value="original_refresh_value", label=NULL_CHAR),
        ]

        # Set the configuration settings
        for setting in test_settings:
            sdk_client.set_configuration_setting(setting)

        variables = kwargs.pop("variables", {})
        dynamic_snapshot_name_postfix = variables.setdefault("dynamic_snapshot_name_postfix", str(int(time.time())))

        # Create a unique snapshot name with timestamp to avoid conflicts
        snapshot_name = f"test-snapshot-{dynamic_snapshot_name_postfix}"

        try:
            # Create the snapshot
            snapshot = sdk_client.begin_create_snapshot(
                name=snapshot_name,
                filters=[ConfigurationSettingsFilter(key="snapshot_test_*")],  # Include all our test keys
                composition_type=SnapshotComposition.KEY,
                retention_period=3600,  # Min valid value is 1 hour
            ).result()

            # Verify snapshot was created successfully
            if is_live():
                assert snapshot.name == snapshot_name
            else:
                assert snapshot.name == "Sanitized"
            assert snapshot.status == SnapshotStatus.READY
            assert snapshot.composition_type == SnapshotComposition.KEY

            # Load provider using the snapshot with refresh enabled
            provider = self.create_client(
                connection_string=appconfiguration_connection_string,
                selects=[
                    SettingSelector(snapshot_name=snapshot_name),  # Snapshot data
                    SettingSelector(key_filter="refresh_test_key"),  # Non-snapshot key for refresh testing
                ],
                refresh_on=[WatchKey("refresh_test_key")],  # Watch non-snapshot key for refresh
                refresh_interval=1,  # Short refresh interval for testing
            )

            # Verify all snapshot settings are loaded
            assert provider["snapshot_test_key1"] == "snapshot_test_value1"
            assert provider["snapshot_test_key2"] == "snapshot_test_value2"
            assert provider["snapshot_test_json"]["nested"] == "snapshot_value"
            assert provider["refresh_test_key"] == "original_refresh_value"

            # Verify that snapshot settings and refresh key are loaded
            snapshot_keys = [key for key in provider.keys() if key.startswith("snapshot_test_")]
            assert len(snapshot_keys) == 3

            # Test snapshot immutability: modify the original settings
            modified_settings = [
                ConfigurationSetting(
                    key="snapshot_test_key1", value="MODIFIED_VALUE1", label=NULL_CHAR  # Changed value
                ),
                ConfigurationSetting(
                    key="snapshot_test_key2", value="MODIFIED_VALUE2", label=NULL_CHAR  # Changed value
                ),
                ConfigurationSetting(
                    key="snapshot_test_json",
                    value='{"nested": "MODIFIED_VALUE"}',  # Changed nested value
                    label=NULL_CHAR,
                    content_type="application/json",
                ),
                ConfigurationSetting(
                    key="refresh_test_key",
                    value="updated_refresh_value",  # Changed value to trigger refresh
                    label=NULL_CHAR,
                ),
            ]

            # Update the original settings with new values
            for setting in modified_settings:
                sdk_client.set_configuration_setting(setting)

            # Add a completely new key after initial load
            new_key = ConfigurationSetting(key="new_key_added_after_load", value="new_value", label=NULL_CHAR)
            sdk_client.set_configuration_setting(new_key)

            # Wait for refresh interval to pass
            time.sleep(1)

            # Refresh the existing provider (snapshots should remain immutable, but non-snapshot keys should update)
            provider.refresh()

            # Verify the snapshot still contains the original values after refresh (immutability)
            assert provider["snapshot_test_key1"] == "snapshot_test_value1"  # Original value
            assert provider["snapshot_test_key2"] == "snapshot_test_value2"  # Original value
            assert provider["snapshot_test_json"]["nested"] == "snapshot_value"  # Original value

            # Verify the non-snapshot key was updated during refresh
            assert provider["refresh_test_key"] == "updated_refresh_value"  # Updated value

            # Verify new keys are NOT added during refresh (only watched keys trigger full reload)
            assert "new_key_added_after_load" not in provider  # New key should not be loaded

            # Verify that loading without snapshot gets the modified values
            provider_current = self.create_client(
                connection_string=appconfiguration_connection_string,
                selects=[SettingSelector(key_filter="snapshot_test_*")],
            )

            # Current values should be the modified ones
            assert provider_current["snapshot_test_key1"] == "MODIFIED_VALUE1"  # Modified value
            assert provider_current["snapshot_test_key2"] == "MODIFIED_VALUE2"  # Modified value
            assert provider_current["snapshot_test_json"]["nested"] == "MODIFIED_VALUE"  # Modified value

        finally:
            # Clean up: delete the snapshot and test settings
            try:
                # Archive the snapshot (delete is not supported, but archive effectively removes it)
                sdk_client.archive_snapshot(snapshot_name)
            except Exception:
                pass

            # Clean up test settings
            for setting in test_settings:
                try:
                    sdk_client.delete_configuration_setting(key=setting.key, label=setting.label)
                except Exception:
                    pass

            # Clean up additional test keys
            try:
                sdk_client.delete_configuration_setting(key="new_key_added_after_load", label=NULL_CHAR)
            except Exception:
                pass
        return variables