Skip to content

Commit 43f506c

Browse files
{App config} Breakdown tests into different files (#30772)
1 parent 549a733 commit 43f506c

14 files changed

+3879
-3629
lines changed
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# --------------------------------------------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# Licensed under the MIT License. See License.txt in the project root for license information.
4+
# --------------------------------------------------------------------------------------------
5+
6+
# Run all tests for the appconfig module locally and clean up resources if needed
7+
8+
[CmdletBinding()]
9+
Param(
10+
[Parameter()]
11+
[switch]$CleanUp,
12+
13+
[Parameter()]
14+
[string]$ResourceGroupName = "cli-local-test-rg",
15+
16+
[Parameter()]
17+
[switch]$Live
18+
)
19+
20+
# Set rg for local testing
21+
$env:AZURE_CLI_TEST_DEV_RESOURCE_GROUP_NAME=$ResourceGroupName
22+
23+
# Unique prefix for resources
24+
$prefix=(Get-Date).ToString("yyyyMMddHHmm")
25+
$env:AZURE_CLI_LOCAL_TEST_RESOURCE_PREFIX=$prefix
26+
27+
# Run tests
28+
if ($Live) {
29+
Write-Host "Running all tests live"
30+
azdev test appconfig --live
31+
}
32+
else {
33+
Write-Host "Running all tests"
34+
azdev test appconfig
35+
}
36+
37+
function clean_up_resources {
38+
param (
39+
[string]$rgName,
40+
[string]$prefix
41+
)
42+
43+
# List all resources in the Resource Group
44+
$resources = az resource list --resource-group $rgName --query "[].{name: name, id: id}" | ConvertFrom-Json
45+
46+
if ($resources -eq "") {
47+
Write-Host "No resources found in resource group $rgName."
48+
return
49+
}
50+
51+
# Delete resources that start with the given prefix
52+
foreach ($resource in $resources) {
53+
if ($resource.name.StartsWith($prefix)) {
54+
Write-Host "Deleting resource: $($resource.name)"
55+
56+
try {
57+
# Delete the resource using its ID
58+
az resource delete --ids $resource.id
59+
Write-Host "Successfully deleted resource: $($resource.name)"
60+
} catch {
61+
Write-Host "Failed to delete resource: $($resource). Error: $_"
62+
}
63+
}
64+
}
65+
}
66+
67+
# Clean up
68+
if ($CleanUp) {
69+
Write-Host "Cleaning up resources"
70+
clean_up_resources -rgName $ResourceGroupName $prefix
71+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# --------------------------------------------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# Licensed under the MIT License. See License.txt in the project root for license information.
4+
# --------------------------------------------------------------------------------------------
5+
6+
import json
7+
import os
8+
9+
from azure.cli.testsdk.scenario_tests import RecordingProcessor
10+
from azure.cli.testsdk.scenario_tests.utilities import is_json_payload
11+
from azure.cli.core.util import shell_safe_json_parse
12+
13+
def create_config_store(test, kwargs):
14+
if 'retention_days' not in kwargs:
15+
kwargs.update({
16+
'retention_days': 1
17+
})
18+
test.cmd('appconfig create -n {config_store_name} -g {rg} -l {rg_loc} --sku {sku} --retention-days {retention_days}')
19+
20+
21+
def _get_local_test_resource_prefix():
22+
return os.environ.get("AZURE_CLI_LOCAL_TEST_RESOURCE_PREFIX")
23+
24+
def get_resource_name_prefix(prefix):
25+
resource_prefix = _get_local_test_resource_prefix()
26+
return prefix if resource_prefix is None else resource_prefix + prefix
27+
28+
class CredentialResponseSanitizer(RecordingProcessor):
29+
def process_response(self, response):
30+
if is_json_payload(response):
31+
try:
32+
json_data = shell_safe_json_parse(response["body"]["string"])
33+
34+
if isinstance(json_data.get("value"), list):
35+
for idx, credential in enumerate(json_data["value"]):
36+
self._try_replace_secret(credential, idx)
37+
38+
response["body"]["string"] = json.dumps(json_data)
39+
40+
elif isinstance(json_data, dict):
41+
self._try_replace_secret(json_data)
42+
43+
response["body"]["string"] = json.dumps(json_data)
44+
45+
except Exception:
46+
pass
47+
48+
return response
49+
50+
def _try_replace_secret(self, credential, idx = 0):
51+
if "connectionString" in credential:
52+
credential["id"] = "sanitized_id{}".format(idx + 1)
53+
credential["value"] = "sanitized_secret{}".format(idx + 1)
54+
55+
endpoint = next(
56+
filter(lambda x: x.startswith("Endpoint="), credential["connectionString"].split(";")))[len("Endpoint="):]
57+
58+
credential["connectionString"] = "Endpoint={};Id={};Secret={}".format(
59+
endpoint, credential["id"], credential["value"])
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
# --------------------------------------------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# Licensed under the MIT License. See License.txt in the project root for license information.
4+
# --------------------------------------------------------------------------------------------
5+
6+
# pylint: disable=line-too-long
7+
8+
import json
9+
import os
10+
import time
11+
12+
from knack.util import CLIError
13+
from azure.cli.testsdk import (ResourceGroupPreparer, live_only, ScenarioTest)
14+
from azure.cli.testsdk.scenario_tests import AllowLargeResponse
15+
from azure.cli.command_modules.appconfig.tests.latest._test_utils import create_config_store, CredentialResponseSanitizer, get_resource_name_prefix
16+
from azure.cli.command_modules.appconfig._constants import FeatureFlagConstants, KeyVaultConstants
17+
18+
TEST_DIR = os.path.abspath(os.path.join(os.path.abspath(__file__), '..'))
19+
20+
class AppConfigAadAuthLiveScenarioTest(ScenarioTest):
21+
22+
def __init__(self, *args, **kwargs):
23+
kwargs["recording_processors"] = kwargs.get("recording_processors", []) + [CredentialResponseSanitizer()]
24+
super().__init__(*args, **kwargs)
25+
26+
@live_only()
27+
@AllowLargeResponse()
28+
@ResourceGroupPreparer(parameter_name_for_location='location')
29+
def test_azconfig_aad_auth(self, resource_group, location):
30+
aad_store_prefix = get_resource_name_prefix('AADStore')
31+
config_store_name = self.create_random_name(prefix=aad_store_prefix, length=36)
32+
33+
location = 'eastus'
34+
sku = 'standard'
35+
self.kwargs.update({
36+
'config_store_name': config_store_name,
37+
'rg_loc': location,
38+
'rg': resource_group,
39+
'sku': sku
40+
})
41+
create_config_store(self, self.kwargs)
42+
43+
# Get connection string and add a key-value and feature flag using the default "key" auth mode
44+
credential_list = self.cmd(
45+
'appconfig credential list -n {config_store_name} -g {rg}').get_output_in_json()
46+
self.kwargs.update({
47+
'connection_string': credential_list[0]['connectionString']
48+
})
49+
50+
# Add a key-value
51+
entry_key = "Color"
52+
entry_value = "Red"
53+
self.kwargs.update({
54+
'key': entry_key,
55+
'value': entry_value
56+
})
57+
self.cmd('appconfig kv set --connection-string {connection_string} --key {key} --value {value} -y',
58+
checks=[self.check('key', entry_key),
59+
self.check('value', entry_value)])
60+
61+
# add a feature flag
62+
entry_feature = 'Beta'
63+
internal_feature_key = FeatureFlagConstants.FEATURE_FLAG_PREFIX + entry_feature
64+
default_description = ""
65+
default_conditions = "{{\'client_filters\': []}}"
66+
default_locked = False
67+
default_state = "off"
68+
self.kwargs.update({
69+
'feature': entry_feature,
70+
'description': default_description
71+
})
72+
self.cmd('appconfig feature set --connection-string {connection_string} --feature {feature} -y',
73+
checks=[self.check('locked', default_locked),
74+
self.check('name', entry_feature),
75+
self.check('key', internal_feature_key),
76+
self.check('description', default_description),
77+
self.check('state', default_state),
78+
self.check('conditions', default_conditions)])
79+
80+
# Get information about account logged in with 'az login'
81+
appconfig_id = self.cmd('appconfig show -n {config_store_name} -g {rg}').get_output_in_json()['id']
82+
account_info = self.cmd('account show').get_output_in_json()
83+
endpoint = "https://" + config_store_name + ".azconfig.io"
84+
self.kwargs.update({
85+
'appconfig_id': appconfig_id,
86+
'user_id': account_info['user']['name'],
87+
'endpoint': endpoint
88+
})
89+
90+
# Before assigning data reader role, read operation should fail with AAD auth.
91+
# The exception really depends on the which identity is used to run this testcase.
92+
with self.assertRaisesRegex(CLIError, "Operation returned an invalid status 'Forbidden'"):
93+
self.cmd('appconfig kv show --endpoint {endpoint} --auth-mode login --key {key}')
94+
95+
# Assign data reader role to current user
96+
self.cmd('role assignment create --assignee {user_id} --role "App Configuration Data Reader" --scope {appconfig_id}')
97+
time.sleep(900) # It takes around 15 mins for RBAC permissions to propagate
98+
99+
# After asssigning data reader role, read operation should succeed
100+
self.cmd('appconfig kv show --endpoint {endpoint} --auth-mode login --key {key}',
101+
checks=[self.check('key', entry_key),
102+
self.check('value', entry_value)])
103+
104+
# Since the logged in account also has "Contributor" role, providing --name instead of --endpoint should succeed
105+
self.cmd('appconfig feature show --name {config_store_name} --auth-mode login --feature {feature}',
106+
checks=[self.check('locked', default_locked),
107+
self.check('name', entry_feature),
108+
self.check('key', internal_feature_key),
109+
self.check('description', default_description),
110+
self.check('state', default_state),
111+
self.check('conditions', default_conditions)])
112+
113+
# Write operations should fail with "Forbidden" error
114+
updated_value = "Blue"
115+
self.kwargs.update({
116+
'value': updated_value
117+
})
118+
with self.assertRaisesRegex(CLIError, "Operation returned an invalid status 'Forbidden'"):
119+
self.cmd('appconfig kv set --endpoint {endpoint} --auth-mode login --key {key} --value {value} -y')
120+
121+
# Export from appconfig to file should succeed
122+
os.environ['AZURE_APPCONFIG_FM_COMPATIBLE'] = 'True'
123+
exported_file_path = os.path.join(TEST_DIR, 'export_aad_1.json')
124+
expected_exported_file_path = os.path.join(TEST_DIR, 'expected_export_aad_1.json')
125+
self.kwargs.update({
126+
'import_source': 'file',
127+
'imported_format': 'json',
128+
'separator': '/',
129+
'exported_file_path': exported_file_path
130+
})
131+
self.cmd(
132+
'appconfig kv export --endpoint {endpoint} --auth-mode login -d {import_source} --path "{exported_file_path}" --format {imported_format} --separator {separator} -y')
133+
with open(expected_exported_file_path) as json_file:
134+
expected_exported_kvs = json.load(json_file)
135+
with open(exported_file_path) as json_file:
136+
exported_kvs = json.load(json_file)
137+
assert expected_exported_kvs == exported_kvs
138+
os.remove(exported_file_path)
139+
140+
# Assign data owner role to current user
141+
self.cmd('role assignment create --assignee {user_id} --role "App Configuration Data Owner" --scope {appconfig_id}')
142+
time.sleep(900) # It takes around 15 mins for RBAC permissions to propagate
143+
144+
# After assigning data owner role, write operation should succeed
145+
self.cmd('appconfig kv set --endpoint {endpoint} --auth-mode login --key {key} --value {value} -y',
146+
checks=[self.check('key', entry_key),
147+
self.check('value', updated_value)])
148+
149+
# Add a KeyVault reference
150+
keyvault_key = "HostSecrets"
151+
keyvault_id = "https://fake.vault.azure.net/secrets/fakesecret"
152+
appconfig_keyvault_value = f"{{{json.dumps({'uri': keyvault_id})}}}"
153+
self.kwargs.update({
154+
'key': keyvault_key,
155+
'secret_identifier': keyvault_id
156+
})
157+
self.cmd('appconfig kv set-keyvault --endpoint {endpoint} --auth-mode login --key {key} --secret-identifier {secret_identifier} -y',
158+
checks=[self.check('contentType', KeyVaultConstants.KEYVAULT_CONTENT_TYPE),
159+
self.check('key', keyvault_key),
160+
self.check('value', appconfig_keyvault_value)])
161+
162+
# Import to appconfig should succeed
163+
imported_file_path = os.path.join(TEST_DIR, 'import_aad.json')
164+
exported_file_path = os.path.join(TEST_DIR, 'export_aad_2.json')
165+
expected_exported_file_path = os.path.join(TEST_DIR, 'expected_export_aad_2.json')
166+
self.kwargs.update({
167+
'imported_file_path': imported_file_path,
168+
'exported_file_path': exported_file_path
169+
})
170+
self.cmd(
171+
'appconfig kv import --endpoint {endpoint} --auth-mode login -s {import_source} --path "{imported_file_path}" --format {imported_format} --separator {separator} -y')
172+
173+
# Export from appconfig to file should succeed
174+
self.cmd(
175+
'appconfig kv export --endpoint {endpoint} --auth-mode login -d {import_source} --path "{exported_file_path}" --format {imported_format} --separator {separator} -y')
176+
with open(expected_exported_file_path) as json_file:
177+
expected_exported_kvs = json.load(json_file)
178+
with open(exported_file_path) as json_file:
179+
exported_kvs = json.load(json_file)
180+
assert expected_exported_kvs == exported_kvs
181+
os.remove(exported_file_path)

0 commit comments

Comments
 (0)