Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import logging
import os
import time

from devtools_testutils import (
add_general_regex_sanitizer,
add_general_string_sanitizer,
Expand All @@ -7,22 +10,71 @@
remove_batch_sanitizers,
add_remove_header_sanitizer,
add_uri_string_sanitizer,
get_credential,
is_live,
)
import pytest
from azure.appconfiguration import AzureAppConfigurationClient
from azure.identity import DefaultAzureCredential
from azure.core.exceptions import HttpResponseError
from testcase import setup_configs, cleanup_test_resources


_LOGGER = logging.getLogger(__name__)
_RBAC_PROPAGATION_TIMEOUT = 15 * 60
_MAX_RETRY_DELAY = 30

# autouse=True will trigger this fixture on each pytest run, even if it's not explicitly used by a test method

# Module-level storage for snapshot names created during session setup
snapshot_names = {}


def _wait_for_data_plane_access(client, timeout=_RBAC_PROPAGATION_TIMEOUT):
deadline = time.monotonic() + timeout
retry_delay = 1

while True:
try:
next(client.list_configuration_settings(key_filter="__rbac_readiness_probe__"), None)
return
except HttpResponseError as error:
if error.status_code != 403:
raise

remaining_time = deadline - time.monotonic()
if remaining_time <= 0:
raise TimeoutError("App Configuration data-plane role assignment did not propagate in time.") from error

sleep_time = min(retry_delay, remaining_time)
_LOGGER.info(
"Waiting %.0f seconds for the App Configuration data-plane role assignment to propagate.",
sleep_time,
)
time.sleep(sleep_time)
retry_delay = min(retry_delay * 2, _MAX_RETRY_DELAY)


@pytest.fixture(scope="session", autouse=True)
def wait_for_data_plane_access():
if not is_live():
return

endpoint = os.environ.get("APPCONFIGURATION_ENDPOINT_STRING")
if not endpoint:
pytest.fail("APPCONFIGURATION_ENDPOINT_STRING must be set when running live tests.")

client = AzureAppConfigurationClient(endpoint, get_credential())
try:
_wait_for_data_plane_access(client)
finally:
client.close()


@pytest.fixture(scope="session", autouse=True)
def setup_app_config_keys():
def setup_app_config_keys(wait_for_data_plane_access):
"""Pre-populate App Configuration with test keys and snapshots once per session (live mode only)."""
del wait_for_data_plane_access

if not is_live():
yield
return
Expand All @@ -32,7 +84,7 @@ def setup_app_config_keys():
yield
return

credential = DefaultAzureCredential()
credential = get_credential()
client = AzureAppConfigurationClient(endpoint, credential)
keyvault_secret_url = os.environ.get("APPCONFIGURATION_KEY_VAULT_REFERENCE")
keyvault_secret_url2 = os.environ.get("APPCONFIGURATION_KEY_VAULT_REFERENCE2")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from unittest.mock import MagicMock, patch

import pytest
from azure.core.exceptions import HttpResponseError

from conftest import _wait_for_data_plane_access


def _http_error(status_code):
response = MagicMock()
response.status_code = status_code
response.reason = "Forbidden"
return HttpResponseError(response=response)


def test_wait_for_data_plane_access_succeeds_immediately():
client = MagicMock()
client.list_configuration_settings.return_value = iter([])

with patch("conftest.time.sleep") as sleep:
_wait_for_data_plane_access(client)

sleep.assert_not_called()


def test_wait_for_data_plane_access_retries_forbidden_response():
client = MagicMock()
client.list_configuration_settings.side_effect = [_http_error(403), iter([])]

with patch("conftest.time.sleep") as sleep:
_wait_for_data_plane_access(client)

sleep.assert_called_once_with(1)


def test_wait_for_data_plane_access_does_not_retry_other_errors():
client = MagicMock()
client.list_configuration_settings.side_effect = _http_error(500)

with patch("conftest.time.sleep") as sleep, pytest.raises(HttpResponseError):
_wait_for_data_plane_access(client)

sleep.assert_not_called()


def test_wait_for_data_plane_access_times_out():
client = MagicMock()
client.list_configuration_settings.side_effect = _http_error(403)

with patch("conftest.time.monotonic", side_effect=[0, 2]), patch(
"conftest.time.sleep"
) as sleep, pytest.raises(TimeoutError):
_wait_for_data_plane_access(client, timeout=1)

sleep.assert_not_called()
62 changes: 60 additions & 2 deletions sdk/appconfiguration/azure-appconfiguration/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,67 @@
# IN THE SOFTWARE.
#
# --------------------------------------------------------------------------
import pytest
import logging
import os
from devtools_testutils import add_general_regex_sanitizer, test_proxy, set_bodiless_matcher, remove_batch_sanitizers
import time

import pytest
from azure.appconfiguration import AzureAppConfigurationClient
from azure.core.exceptions import HttpResponseError
from devtools_testutils import (
add_general_regex_sanitizer,
get_credential,
is_live,
remove_batch_sanitizers,
set_bodiless_matcher,
test_proxy,
)


_LOGGER = logging.getLogger(__name__)
_RBAC_PROPAGATION_TIMEOUT = 15 * 60
_MAX_RETRY_DELAY = 30


def _wait_for_data_plane_access(client, timeout=_RBAC_PROPAGATION_TIMEOUT):
deadline = time.monotonic() + timeout
retry_delay = 1

while True:
try:
next(client.list_configuration_settings(key_filter="__rbac_readiness_probe__"), None)
return
except HttpResponseError as error:
if error.status_code != 403:
raise

remaining_time = deadline - time.monotonic()
if remaining_time <= 0:
raise TimeoutError("App Configuration data-plane role assignment did not propagate in time.") from error

sleep_time = min(retry_delay, remaining_time)
_LOGGER.info(
"Waiting %.0f seconds for the App Configuration data-plane role assignment to propagate.",
sleep_time,
)
time.sleep(sleep_time)
retry_delay = min(retry_delay * 2, _MAX_RETRY_DELAY)


@pytest.fixture(scope="session", autouse=True)
def wait_for_data_plane_access():
if not is_live():
return

endpoint = os.environ.get("APPCONFIGURATION_ENDPOINT_STRING")
if not endpoint:
pytest.fail("APPCONFIGURATION_ENDPOINT_STRING must be set when running live tests.")

client = AzureAppConfigurationClient(endpoint, get_credential())
try:
_wait_for_data_plane_access(client)
finally:
client.close()


@pytest.fixture(scope="session", autouse=True)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from unittest.mock import MagicMock, patch

import pytest
from azure.core.exceptions import HttpResponseError

from conftest import _wait_for_data_plane_access


def _http_error(status_code):
response = MagicMock()
response.status_code = status_code
response.reason = "Forbidden"
return HttpResponseError(response=response)


def test_wait_for_data_plane_access_succeeds_immediately():
client = MagicMock()
client.list_configuration_settings.return_value = iter([])

with patch("conftest.time.sleep") as sleep:
_wait_for_data_plane_access(client)

sleep.assert_not_called()


def test_wait_for_data_plane_access_retries_forbidden_response():
client = MagicMock()
client.list_configuration_settings.side_effect = [_http_error(403), iter([])]

with patch("conftest.time.sleep") as sleep:
_wait_for_data_plane_access(client)

sleep.assert_called_once_with(1)


def test_wait_for_data_plane_access_does_not_retry_other_errors():
client = MagicMock()
client.list_configuration_settings.side_effect = _http_error(500)

with patch("conftest.time.sleep") as sleep, pytest.raises(HttpResponseError):
_wait_for_data_plane_access(client)

sleep.assert_not_called()


def test_wait_for_data_plane_access_times_out():
client = MagicMock()
client.list_configuration_settings.side_effect = _http_error(403)

with patch("conftest.time.monotonic", side_effect=[0, 2]), patch(
"conftest.time.sleep"
) as sleep, pytest.raises(TimeoutError):
_wait_for_data_plane_access(client, timeout=1)

sleep.assert_not_called()
73 changes: 8 additions & 65 deletions sdk/appconfiguration/test-resources.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
},
"variables": {
"roleDefinitionId": "[format('/subscriptions/{0}/providers/Microsoft.Authorization/roleDefinitions/5ae67dd6-50cb-40e7-96ff-dc2bfa4b606b', subscription().subscriptionId)]",
"roleAssignmentName": "[guid(resourceGroup().id, parameters('testApplicationOid'), variables('roleDefinitionId'))]",
"uniqueAzConfigName": "[format('{0}-{1}', parameters('baseName'), parameters('azConfigPrefix'))]",
"endpointValue": "[format('https://{0}-{1}{2}', parameters('baseName'), parameters('azConfigPrefix'), parameters('azConfigEndpointSuffix'))]",
"azureKeyVaultUrl": "[format('https://{0}{1}/', parameters('baseName'), parameters('keyVaultEndpointSuffix'))]",
Expand All @@ -66,21 +67,24 @@
"resources": [
{
"type": "Microsoft.AppConfiguration/configurationStores",
"apiVersion": "2019-10-01",
"apiVersion": "2024-06-01",
"name": "[variables('uniqueAzConfigName')]",
"location": "[parameters('location')]",
"sku": {
"name": "[parameters('sku')]"
},
"properties": {
"endpoint": "[variables('endpointValue')]",
"disableLocalAuth": true
"disableLocalAuth": true,
"dataPlaneProxy": {
"authenticationMode": "Pass-through",
"privateLinkDelegation": "Disabled"
}
}
},
{
"type": "Microsoft.Authorization/roleAssignments",
"apiVersion": "2018-09-01-preview",
"name": "[guid(resourceGroup().id)]",
"name": "[variables('roleAssignmentName')]",
"properties": {
"roleDefinitionId": "[variables('roleDefinitionId')]",
"principalId": "[parameters('testApplicationOid')]"
Expand Down Expand Up @@ -133,70 +137,9 @@
"properties": {
"value": "Very secret value 2"
}
},
{
"type": "Microsoft.AppConfiguration/configurationStores/keyValues",
"apiVersion": "2020-07-01-preview",
"name": "[concat(variables('uniqueAzConfigName'), '/', 'message')]",
"dependsOn": [
"[variables('uniqueAzConfigName')]"
],
"properties": {
"value": "hi"
}
},
{
"type": "Microsoft.AppConfiguration/configurationStores/keyValues",
"apiVersion": "2020-07-01-preview",
"name": "[concat(variables('uniqueAzConfigName'), '/', 'message$dev')]",
"dependsOn": [
"[variables('uniqueAzConfigName')]"
],
"properties": {
"value": "test"
}
},
{
"type": "Microsoft.AppConfiguration/configurationStores/keyValues",
"apiVersion": "2020-07-01-preview",
"name": "[concat(variables('uniqueAzConfigName'), '/', 'my_json')]",
"dependsOn": [
"[variables('uniqueAzConfigName')]"
],
"properties": {
"value": "{\"key\": \"value\"}",
"contentType": "application/json"
}
},
{
"type": "Microsoft.AppConfiguration/configurationStores/keyValues",
"apiVersion": "2020-07-01-preview",
"name": "[concat(variables('uniqueAzConfigName'), '/', 'test.trimmed')]",
"dependsOn": [
"[variables('uniqueAzConfigName')]"
],
"properties": {
"value": "key"
}
},
{
"type": "Microsoft.AppConfiguration/configurationStores/keyValues",
"apiVersion": "2020-07-01-preview",
"name": "[concat(variables('uniqueAzConfigName'), '/', '.appconfig.featureflag~2FAlpha')]",
"dependsOn": [
"[variables('uniqueAzConfigName')]"
],
"properties": {
"value": "{\"id\":\"Alpha\",\"description\":\"\",\"enabled\":false,\"conditions\":{\"client_filters\":[]}}",
"contentType": "application/vnd.microsoft.appconfig.ff+json;charset=utf-8"
}
}
],
"outputs": {
"APPCONFIGURATION_CONNECTION_STRING": {
"type": "string",
"value": "[listKeys(resourceId('Microsoft.AppConfiguration/configurationStores',variables('uniqueAzConfigName')), '2019-02-01-preview').value[0].connectionString]"
},
"APPCONFIGURATION_ENDPOINT_STRING": {
"type": "string",
"value": "[variables('endpointValue')]"
Expand Down
Loading