Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
3e1176b
Improves slot setting handling in app settings update
jcassanji-southworks Oct 20, 2025
a5e185a
Enhances error handling in update_app_settings function and adds unit…
jcassanji-southworks Oct 20, 2025
40e0045
Update src/azure-cli/azure/cli/command_modules/appservice/custom.py
jcassanji-southworks Oct 20, 2025
f0f7e9d
Update src/azure-cli/azure/cli/command_modules/appservice/custom.py
jcassanji-southworks Oct 20, 2025
7dc68fd
Adds error handling test for update_app_settings to validate invalid …
jcassanji-southworks Oct 20, 2025
ce9d520
Merge branch '32290-function-app-slot-settings-parameter-fails-to-upd…
jcassanji-southworks Oct 20, 2025
5be2f8d
Refactor update_app_settings to improve handling of JSON objects and …
jcassanji-southworks Oct 20, 2025
301c400
Fix update_app_settings to correctly update settings dictionary with …
jcassanji-southworks Oct 20, 2025
261a86b
Refactor update_app_settings to simplify iteration over keys in slot …
jcassanji-southworks Oct 20, 2025
47a6108
Update src/azure-cli/azure/cli/command_modules/appservice/custom.py
jcassanji-southworks Oct 20, 2025
f65e501
Update src/azure-cli/azure/cli/command_modules/appservice/custom.py
jcassanji-southworks Oct 20, 2025
36ba1ca
Refactor update_app_settings and its tests to improve clarity in hand…
jcassanji-southworks Oct 20, 2025
71ed94f
Fix update_app_settings to correctly handle setting assignment and im…
jcassanji-southworks Oct 20, 2025
ff33936
Update src/azure-cli/azure/cli/command_modules/appservice/tests/lates…
jcassanji-southworks Oct 20, 2025
39f759e
Update src/azure-cli/azure/cli/command_modules/appservice/tests/lates…
jcassanji-southworks Oct 20, 2025
4ed5e9a
Update src/azure-cli/azure/cli/command_modules/appservice/tests/lates…
jcassanji-southworks Oct 20, 2025
5f445af
Update src/azure-cli/azure/cli/command_modules/appservice/tests/lates…
jcassanji-southworks Oct 20, 2025
a916b4b
Update src/azure-cli/azure/cli/command_modules/appservice/tests/lates…
jcassanji-southworks Oct 20, 2025
fdf6f2e
Update src/azure-cli/azure/cli/command_modules/appservice/tests/lates…
jcassanji-southworks Oct 20, 2025
4c27292
Refactor update_app_settings to modularize parsing logic for key=valu…
jcassanji-southworks Oct 20, 2025
cef992e
Merge branch '32290-function-app-slot-settings-parameter-fails-to-upd…
jcassanji-southworks Oct 20, 2025
2446b64
Update src/azure-cli/azure/cli/command_modules/appservice/custom.py
jcassanji-southworks Oct 20, 2025
0ae0770
Update src/azure-cli/azure/cli/command_modules/appservice/custom.py
jcassanji-southworks Oct 20, 2025
8e63318
Update src/azure-cli/azure/cli/command_modules/appservice/custom.py
jcassanji-southworks Oct 20, 2025
6f88a7f
Refactor update_app_settings to remove unnecessary whitespace, improv…
jcassanji-southworks Oct 21, 2025
b871eef
Merge branch '32290-function-app-slot-settings-parameter-fails-to-upd…
jcassanji-southworks Oct 21, 2025
988fca4
Refactor _parse_json_setting to remove unused parameter and simplify …
jcassanji-southworks Oct 21, 2025
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
50 changes: 33 additions & 17 deletions src/azure-cli/azure/cli/command_modules/appservice/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -513,7 +513,7 @@ def update_app_settings_functionapp(cmd, resource_group_name, name, settings=Non

def update_app_settings(cmd, resource_group_name, name, settings=None, slot=None, slot_settings=None):
if not settings and not slot_settings:
raise MutuallyExclusiveArgumentError('Usage Error: --settings |--slot-settings')
raise MutuallyExclusiveArgumentError('Please provide either --settings or --slot-settings parameter.')

settings = settings or []
slot_settings = slot_settings or []
Expand Down Expand Up @@ -545,26 +545,33 @@ def update_app_settings(cmd, resource_group_name, name, settings=None, slot=None
slot_result[t['name']] = True
result[t['name']] = t['value']
else:
dest.update(temp)
except CLIError:
setting_name, value = s.split('=', 1)
dest[setting_name] = value
result.update(dest)
# Handle JSON objects - when using --slot-settings with object format,
# treat all keys as slot settings
if setting_type == "SlotSettings":
# For slot settings, we need to add values to result (for app settings)
# AND mark them as slot settings in slot_result
result.update(temp) # Add actual values to result
for key in temp.keys():
slot_result[key] = True # Mark as slot setting
else:
dest.update(temp) # Regular settings go to dest (which is result)
except InvalidArgumentValueError:
try:
setting_name, value = s.split('=', 1)
dest[setting_name] = value
result.update(dest)
except ValueError as ex:
raise InvalidArgumentValueError(
f"Invalid setting format: '{s}'. Expected 'key=value' format or valid JSON.",
recommendation="Use 'key=value' format or provide valid JSON like '{\"key\": \"value\"}'."
) from ex

for setting_name, value in result.items():
app_settings.properties[setting_name] = value
client = web_client_factory(cmd.cli_ctx)


# TODO: Centauri currently return wrong payload for update appsettings, remove this once backend has the fix.
if is_centauri_functionapp(cmd, resource_group_name, name):
update_application_settings_polling(cmd, resource_group_name, name, app_settings, slot, client)
result = _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 'list_application_settings', slot)
else:
result = _generic_settings_operation(cmd.cli_ctx, resource_group_name, name,
'update_application_settings',
app_settings, slot, client)

# Process slot configurations BEFORE updating application settings
# This ensures that slot settings are properly configured before the values are applied
app_settings_slot_cfg_names = []
if slot_result:
slot_cfg_names = client.web_apps.list_slot_configuration_names(resource_group_name, name)
Expand All @@ -578,6 +585,15 @@ def update_app_settings(cmd, resource_group_name, name, settings=None, slot=None
app_settings_slot_cfg_names = slot_cfg_names.app_setting_names
client.web_apps.update_slot_configuration_names(resource_group_name, name, slot_cfg_names)

# TODO: Centauri currently return wrong payload for update appsettings, remove this once backend has the fix.
if is_centauri_functionapp(cmd, resource_group_name, name):
update_application_settings_polling(cmd, resource_group_name, name, app_settings, slot, client)
result = _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 'list_application_settings', slot)
else:
result = _generic_settings_operation(cmd.cli_ctx, resource_group_name, name,
'update_application_settings',
app_settings, slot, client)

return _build_app_settings_output(result.properties, app_settings_slot_cfg_names, redact=True)


Expand All @@ -597,7 +613,7 @@ def update_application_settings_polling(cmd, resource_group_name, name, app_sett
time.sleep(5)
r = send_raw_request(cmd.cli_ctx, method='get', url=poll_url)
else:
raise CLIError(ex)
raise AzureResponseError(f"Failed to update application settings: {str(ex)}") from ex


def add_azure_storage_account(cmd, resource_group_name, name, custom_id, storage_type, account_name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@

from azure.mgmt.web import WebSiteManagementClient
from knack.util import CLIError
from azure.cli.core.azclierror import (InvalidArgumentValueError,
MutuallyExclusiveArgumentError,
AzureResponseError)
from azure.cli.command_modules.appservice.custom import (set_deployment_user,
update_git_token, add_hostname,
update_site_configs,
Expand All @@ -27,7 +30,9 @@
list_snapshots,
restore_snapshot,
create_managed_ssl_cert,
add_github_actions)
add_github_actions,
update_app_settings,
update_application_settings_polling)

# pylint: disable=line-too-long
from azure.cli.core.profiles import ResourceType
Expand Down Expand Up @@ -463,6 +468,142 @@ def test_create_managed_ssl_cert(self, generic_site_op_mock, client_factory_mock
certificate_envelope=cert_def)


@mock.patch('azure.cli.command_modules.appservice.custom._generic_site_operation')
@mock.patch('azure.cli.command_modules.appservice.custom.web_client_factory')
@mock.patch('azure.cli.command_modules.appservice.custom.is_centauri_functionapp')
@mock.patch('azure.cli.command_modules.appservice.custom._generic_settings_operation')
@mock.patch('azure.cli.command_modules.appservice.custom._build_app_settings_output')
def test_update_app_settings_error_handling_no_parameters(self, mock_build, mock_settings_op, mock_centauri,
mock_client_factory, mock_site_op):
"""Test that MutuallyExclusiveArgumentError is raised when neither settings nor slot_settings are provided"""
cmd_mock = _get_test_cmd()

# Test missing both parameters
with self.assertRaisesRegex(MutuallyExclusiveArgumentError,
"Please provide either --settings or --slot-settings parameter"):
update_app_settings(cmd_mock, 'test-rg', 'test-app')

# Ensure mocks weren't called since we should fail early
mock_site_op.assert_not_called()
mock_client_factory.assert_not_called()

@mock.patch('azure.cli.command_modules.appservice.custom._generic_site_operation')
@mock.patch('azure.cli.command_modules.appservice.custom.web_client_factory')
@mock.patch('azure.cli.command_modules.appservice.custom.is_centauri_functionapp')
@mock.patch('azure.cli.command_modules.appservice.custom._generic_settings_operation')
@mock.patch('azure.cli.command_modules.appservice.custom._build_app_settings_output')
@mock.patch('azure.cli.command_modules.appservice.custom.shell_safe_json_parse')
def test_update_app_settings_error_handling_invalid_format(self, mock_json_parse, mock_build, mock_settings_op,
mock_centauri, mock_client_factory, mock_site_op):
"""Test that InvalidArgumentValueError is raised for invalid setting formats"""
cmd_mock = _get_test_cmd()

# Setup mocks
mock_app_settings = mock.MagicMock()
mock_app_settings.properties = {}
mock_site_op.return_value = mock_app_settings

mock_client = mock.MagicMock()
mock_client_factory.return_value = mock_client
mock_centauri.return_value = False
mock_settings_op.return_value = mock_app_settings
mock_build.return_value = {"success": True}

# Mock shell_safe_json_parse to raise InvalidArgumentValueError (simulating invalid JSON)
mock_json_parse.side_effect = InvalidArgumentValueError("Invalid JSON format")

# Test invalid format that can't be parsed as JSON or key=value
invalid_setting = "invalid_format_no_equals_no_json"

with self.assertRaisesRegex(InvalidArgumentValueError,
r"Invalid setting format.*Expected 'key=value' format or valid JSON"):
update_app_settings(cmd_mock, 'test-rg', 'test-app', settings=[invalid_setting])

@mock.patch('azure.cli.command_modules.appservice.custom._generic_site_operation')
@mock.patch('azure.cli.command_modules.appservice.custom.web_client_factory')
@mock.patch('azure.cli.command_modules.appservice.custom.is_centauri_functionapp')
@mock.patch('azure.cli.command_modules.appservice.custom._generic_settings_operation')
@mock.patch('azure.cli.command_modules.appservice.custom._build_app_settings_output')
def test_update_app_settings_success_key_value_format(self, mock_build, mock_settings_op, mock_centauri,
mock_client_factory, mock_site_op):
"""Test successful processing of key=value format settings"""
cmd_mock = _get_test_cmd()

# Setup mocks
mock_app_settings = mock.MagicMock()
mock_app_settings.properties = {}
mock_site_op.return_value = mock_app_settings

mock_client = mock.MagicMock()
mock_client_factory.return_value = mock_client
mock_centauri.return_value = False
mock_settings_op.return_value = mock_app_settings
mock_build.return_value = {"KEY1": "value1", "KEY2": "value2"}

# Test valid key=value format
result = update_app_settings(cmd_mock, 'test-rg', 'test-app',
settings=['KEY1=value1', 'KEY2=value2'])

# Verify the function completed successfully
self.assertEqual(result["KEY1"], "value1")
self.assertEqual(result["KEY2"], "value2")
mock_build.assert_called_once()

@mock.patch('azure.cli.command_modules.appservice.custom.send_raw_request')
def test_update_application_settings_polling_error_handling(self, mock_send_request):
"""Test that AzureResponseError is raised in polling function when appropriate"""
cmd_mock = _get_test_cmd()

# Mock an exception that doesn't have the expected structure
class MockException(Exception):
def __init__(self):
self.response = mock.MagicMock()
self.response.status_code = 400 # Not 202
self.response.headers = {}

# Mock _generic_settings_operation to raise the exception
with mock.patch('azure.cli.command_modules.appservice.custom._generic_settings_operation') as mock_settings_op:
mock_settings_op.side_effect = MockException()

with self.assertRaisesRegex(AzureResponseError, "Failed to update application settings"):
update_application_settings_polling(cmd_mock, 'test-rg', 'test-app',
mock.MagicMock(), None, mock.MagicMock())

@mock.patch('azure.cli.command_modules.appservice.custom._generic_site_operation')
@mock.patch('azure.cli.command_modules.appservice.custom.web_client_factory')
@mock.patch('azure.cli.command_modules.appservice.custom.is_centauri_functionapp')
@mock.patch('azure.cli.command_modules.appservice.custom._generic_settings_operation')
@mock.patch('azure.cli.command_modules.appservice.custom._build_app_settings_output')
def test_update_app_settings_success_with_slot_settings(self, mock_build, mock_settings_op, mock_centauri,
mock_client_factory, mock_site_op):
"""Test successful processing with slot settings"""
cmd_mock = _get_test_cmd()

# Setup mocks
mock_app_settings = mock.MagicMock()
mock_app_settings.properties = {}
mock_site_op.return_value = mock_app_settings

mock_client = mock.MagicMock()
mock_slot_config = mock.MagicMock()
mock_slot_config.app_setting_names = []
mock_client.web_apps.list_slot_configuration_names.return_value = mock_slot_config
mock_client_factory.return_value = mock_client
mock_centauri.return_value = False
mock_settings_op.return_value = mock_app_settings
mock_build.return_value = {"SLOT_KEY": "slot_value"}

# Test with slot settings
result = update_app_settings(cmd_mock, 'test-rg', 'test-app',
settings=['REGULAR_KEY=regular_value'],
slot_settings=['SLOT_KEY=slot_value'])

# Verify slot configuration was updated
mock_client.web_apps.list_slot_configuration_names.assert_called_once()
mock_client.web_apps.update_slot_configuration_names.assert_called_once()
mock_build.assert_called_once()


class FakedResponse: # pylint: disable=too-few-public-methods
def __init__(self, status_code):
self.status_code = status_code
Expand Down
Loading