From e0c957a6e66f0b46f0da89f0c0e941f4c00d9a86 Mon Sep 17 00:00:00 2001 From: Jordan Selig Date: Wed, 25 Mar 2026 23:22:15 -0400 Subject: [PATCH 1/6] [App Service] Fix #29403, #28722, #27950: Fix SSL certificate pagination The list_by_resource_group() results for certificates were not being fully paginated. Wrapped the SDK pager calls in list() to ensure all pages are consumed, matching the pattern used by other list operations in the codebase. This fixes: - ssl list showing incomplete results - ssl delete failing with "thumbprint not found" for certs beyond first page - ssl binding operations missing certificates Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../cli/command_modules/appservice/custom.py | 8 +-- .../latest/test_webapp_commands_thru_mock.py | 72 ++++++++++++++++++- 2 files changed, 75 insertions(+), 5 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/appservice/custom.py b/src/azure-cli/azure/cli/command_modules/appservice/custom.py index 386ba088608..d86cc4b9e6b 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/custom.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/custom.py @@ -5860,7 +5860,7 @@ def _get_cert(certificate_password, certificate_file): def list_ssl_certs(cmd, resource_group_name): client = web_client_factory(cmd.cli_ctx) - return client.certificates.list_by_resource_group(resource_group_name) + return list(client.certificates.list_by_resource_group(resource_group_name)) def show_ssl_cert(cmd, resource_group_name, certificate_name): @@ -5870,7 +5870,7 @@ def show_ssl_cert(cmd, resource_group_name, certificate_name): def delete_ssl_cert(cmd, resource_group_name, certificate_thumbprint): client = web_client_factory(cmd.cli_ctx) - webapp_certs = client.certificates.list_by_resource_group(resource_group_name) + webapp_certs = list(client.certificates.list_by_resource_group(resource_group_name)) for webapp_cert in webapp_certs: if webapp_cert.thumbprint == certificate_thumbprint: return client.certificates.delete(resource_group_name, webapp_cert.name) @@ -6059,7 +6059,7 @@ def _update_ssl_binding(cmd, resource_group_name, name, certificate_thumbprint, raise ResourceNotFoundError("'{}' app doesn't exist".format(name)) cert_resource_group_name = parse_resource_id(webapp.server_farm_id)['resource_group'] - webapp_certs = client.certificates.list_by_resource_group(cert_resource_group_name) + webapp_certs = list(client.certificates.list_by_resource_group(cert_resource_group_name)) found_cert = None # search for a cert that matches in the app service plan's RG @@ -6068,7 +6068,7 @@ def _update_ssl_binding(cmd, resource_group_name, name, certificate_thumbprint, found_cert = webapp_cert # search for a cert that matches in the webapp's RG if not found_cert: - webapp_certs = client.certificates.list_by_resource_group(resource_group_name) + webapp_certs = list(client.certificates.list_by_resource_group(resource_group_name)) for webapp_cert in webapp_certs: if webapp_cert.thumbprint == certificate_thumbprint: found_cert = webapp_cert diff --git a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py index 853eadc1edd..3af5d181455 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py @@ -12,7 +12,8 @@ from knack.util import CLIError from azure.cli.core.azclierror import (InvalidArgumentValueError, MutuallyExclusiveArgumentError, - AzureResponseError) + AzureResponseError, + ResourceNotFoundError) from azure.cli.command_modules.appservice.custom import (set_deployment_user, update_git_token, add_hostname, update_site_configs, @@ -30,6 +31,8 @@ list_snapshots, restore_snapshot, create_managed_ssl_cert, + list_ssl_certs, + delete_ssl_cert, add_github_actions, update_app_settings, update_application_settings_polling, @@ -644,5 +647,72 @@ def __init__(self, status_code): self.status_code = status_code +class TestSSLCertPagination(unittest.TestCase): + """Tests for SSL certificate pagination fix (#29403, #28722, #27950).""" + + @mock.patch('azure.cli.command_modules.appservice.custom.web_client_factory', autospec=True) + def test_list_ssl_certs_returns_all_pages(self, client_factory_mock): + """Ensure list_ssl_certs fully consumes the pager and returns a list.""" + cmd_mock = _get_test_cmd() + + # Simulate a pager that yields certs across multiple "pages" + cert1 = mock.MagicMock() + cert1.name = 'cert1' + cert2 = mock.MagicMock() + cert2.name = 'cert2' + cert3 = mock.MagicMock() + cert3.name = 'cert3' + + # Use an iterator to simulate SDK pager behavior + client = mock.MagicMock() + client_factory_mock.return_value = client + client.certificates.list_by_resource_group.return_value = iter([cert1, cert2, cert3]) + + result = list_ssl_certs(cmd_mock, 'myRG') + + # Must be a list (not a lazy iterator) and contain all certs + self.assertIsInstance(result, list) + self.assertEqual(len(result), 3) + client.certificates.list_by_resource_group.assert_called_once_with('myRG') + + @mock.patch('azure.cli.command_modules.appservice.custom.web_client_factory', autospec=True) + def test_delete_ssl_cert_finds_cert_beyond_first_page(self, client_factory_mock): + """Ensure delete_ssl_cert can find a cert that would be on a later page.""" + cmd_mock = _get_test_cmd() + + # Create 100 certs; target is the last one (simulating beyond first page) + certs = [] + for i in range(100): + c = mock.MagicMock() + c.thumbprint = f'THUMB{i:04d}' + c.name = f'cert{i}' + certs.append(c) + + client = mock.MagicMock() + client_factory_mock.return_value = client + client.certificates.list_by_resource_group.return_value = iter(certs) + + target_thumbprint = 'THUMB0099' + delete_ssl_cert(cmd_mock, 'myRG', target_thumbprint) + + client.certificates.delete.assert_called_once_with('myRG', 'cert99') + + @mock.patch('azure.cli.command_modules.appservice.custom.web_client_factory', autospec=True) + def test_delete_ssl_cert_not_found_raises_error(self, client_factory_mock): + """Ensure delete_ssl_cert raises ResourceNotFoundError for missing thumbprint.""" + cmd_mock = _get_test_cmd() + + cert = mock.MagicMock() + cert.thumbprint = 'AAAA' + cert.name = 'cert1' + + client = mock.MagicMock() + client_factory_mock.return_value = client + client.certificates.list_by_resource_group.return_value = iter([cert]) + + with self.assertRaises(ResourceNotFoundError): + delete_ssl_cert(cmd_mock, 'myRG', 'NONEXISTENT') + + if __name__ == '__main__': unittest.main() From 63ce9a1e05f8f4ee6dda14d2dcc67f3c7e325933 Mon Sep 17 00:00:00 2001 From: Jordan Selig Date: Thu, 26 Mar 2026 10:20:37 -0400 Subject: [PATCH 2/6] Address Copilot review: add early break in _update_ssl_binding, improve test pagination mocks - Add break statements in _update_ssl_binding cert search loops for early termination once a matching thumbprint is found - Replace plain iter() test mocks with _FakePagedIterator that simulates real SDK multi-page behavior and tracks pages fetched - Tests now assert pages_fetched to verify full pagination is exercised - Keep list() materialization pattern consistent with codebase convention (web_apps.list_by_resource_group, app_service_plans.list_by_resource_group, etc.) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../cli/command_modules/appservice/custom.py | 2 + .../latest/test_webapp_commands_thru_mock.py | 76 ++++++++++++------- 2 files changed, 52 insertions(+), 26 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/appservice/custom.py b/src/azure-cli/azure/cli/command_modules/appservice/custom.py index d86cc4b9e6b..74bc3c0f3ba 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/custom.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/custom.py @@ -6066,12 +6066,14 @@ def _update_ssl_binding(cmd, resource_group_name, name, certificate_thumbprint, for webapp_cert in webapp_certs: if webapp_cert.thumbprint == certificate_thumbprint: found_cert = webapp_cert + break # search for a cert that matches in the webapp's RG if not found_cert: webapp_certs = list(client.certificates.list_by_resource_group(resource_group_name)) for webapp_cert in webapp_certs: if webapp_cert.thumbprint == certificate_thumbprint: found_cert = webapp_cert + break # search for a cert that matches in the subscription, filtering on the serverfarm if not found_cert: sub_certs = client.certificates.list(filter=f"ServerFarmId eq '{webapp.server_farm_id}'") diff --git a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py index 3af5d181455..31fd8714bb3 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py @@ -647,54 +647,80 @@ def __init__(self, status_code): self.status_code = status_code +class _FakePagedIterator: + """Simulates an Azure SDK paged iterator that yields items across multiple pages. + + Unlike a plain list or iter(), this class mimics the SDK behavior where + items are fetched page-by-page via continuation tokens. Calling list() + on the pager forces all pages to be consumed, which is the pattern used + throughout the appservice module. + """ + + def __init__(self, pages): + """pages: list of lists, each inner list represents one page of results.""" + self._pages = pages + self._page_fetch_count = 0 + + def __iter__(self): + for page in self._pages: + self._page_fetch_count += 1 + yield from page + + @property + def pages_fetched(self): + return self._page_fetch_count + + +def _make_cert(name, thumbprint=''): + cert = mock.MagicMock() + cert.name = name + cert.thumbprint = thumbprint + return cert + + class TestSSLCertPagination(unittest.TestCase): """Tests for SSL certificate pagination fix (#29403, #28722, #27950).""" @mock.patch('azure.cli.command_modules.appservice.custom.web_client_factory', autospec=True) def test_list_ssl_certs_returns_all_pages(self, client_factory_mock): - """Ensure list_ssl_certs fully consumes the pager and returns a list.""" + """Ensure list_ssl_certs fully consumes the pager and returns a concrete list.""" cmd_mock = _get_test_cmd() - # Simulate a pager that yields certs across multiple "pages" - cert1 = mock.MagicMock() - cert1.name = 'cert1' - cert2 = mock.MagicMock() - cert2.name = 'cert2' - cert3 = mock.MagicMock() - cert3.name = 'cert3' + page1 = [_make_cert('cert1'), _make_cert('cert2')] + page2 = [_make_cert('cert3')] + pager = _FakePagedIterator([page1, page2]) - # Use an iterator to simulate SDK pager behavior client = mock.MagicMock() client_factory_mock.return_value = client - client.certificates.list_by_resource_group.return_value = iter([cert1, cert2, cert3]) + client.certificates.list_by_resource_group.return_value = pager result = list_ssl_certs(cmd_mock, 'myRG') - # Must be a list (not a lazy iterator) and contain all certs + # Must be a concrete list (not a lazy iterator) so the CLI framework + # can serialize all results, and must contain items from every page. self.assertIsInstance(result, list) self.assertEqual(len(result), 3) + self.assertEqual(pager.pages_fetched, 2) client.certificates.list_by_resource_group.assert_called_once_with('myRG') @mock.patch('azure.cli.command_modules.appservice.custom.web_client_factory', autospec=True) def test_delete_ssl_cert_finds_cert_beyond_first_page(self, client_factory_mock): - """Ensure delete_ssl_cert can find a cert that would be on a later page.""" + """Ensure delete_ssl_cert can find a cert on a later page.""" cmd_mock = _get_test_cmd() - # Create 100 certs; target is the last one (simulating beyond first page) - certs = [] - for i in range(100): - c = mock.MagicMock() - c.thumbprint = f'THUMB{i:04d}' - c.name = f'cert{i}' - certs.append(c) + # Target cert is on page 2 — would be missed without full pagination + page1 = [_make_cert(f'cert{i}', f'THUMB{i:04d}') for i in range(50)] + page2 = [_make_cert(f'cert{i}', f'THUMB{i:04d}') for i in range(50, 100)] + pager = _FakePagedIterator([page1, page2]) client = mock.MagicMock() client_factory_mock.return_value = client - client.certificates.list_by_resource_group.return_value = iter(certs) + client.certificates.list_by_resource_group.return_value = pager - target_thumbprint = 'THUMB0099' - delete_ssl_cert(cmd_mock, 'myRG', target_thumbprint) + delete_ssl_cert(cmd_mock, 'myRG', 'THUMB0099') + # The cert on page 2 must be found and deleted + self.assertEqual(pager.pages_fetched, 2) client.certificates.delete.assert_called_once_with('myRG', 'cert99') @mock.patch('azure.cli.command_modules.appservice.custom.web_client_factory', autospec=True) @@ -702,13 +728,11 @@ def test_delete_ssl_cert_not_found_raises_error(self, client_factory_mock): """Ensure delete_ssl_cert raises ResourceNotFoundError for missing thumbprint.""" cmd_mock = _get_test_cmd() - cert = mock.MagicMock() - cert.thumbprint = 'AAAA' - cert.name = 'cert1' + pager = _FakePagedIterator([[_make_cert('cert1', 'AAAA')]]) client = mock.MagicMock() client_factory_mock.return_value = client - client.certificates.list_by_resource_group.return_value = iter([cert]) + client.certificates.list_by_resource_group.return_value = pager with self.assertRaises(ResourceNotFoundError): delete_ssl_cert(cmd_mock, 'myRG', 'NONEXISTENT') From be2f4ca804cc7db57147f35cb35b439cda96f120 Mon Sep 17 00:00:00 2001 From: Jordan Selig Date: Thu, 26 Mar 2026 09:53:29 -0400 Subject: [PATCH 3/6] [App Service] Fix #30357: `az webapp config ssl bind`: Use full Site object to avoid Azure Policy denial Previously, _update_host_name_ssl_state constructed a minimal Site object with only host_name_ssl_states, location, and tags. When passed to begin_create_or_update, this caused Azure Policy (e.g. 'HTTPS Only must be enabled') to deny the operation because policy-sensitive fields like httpsOnly were missing from the payload. The fix reuses the full existing Site object fetched from Azure, updating only the host_name_ssl_states field. This preserves all policy-sensitive properties (httpsOnly, ftpsState, etc.) in the PUT request. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../cli/command_modules/appservice/custom.py | 13 +++-- .../latest/test_webapp_commands_thru_mock.py | 54 +++++++++++++++++++ 2 files changed, 60 insertions(+), 7 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/appservice/custom.py b/src/azure-cli/azure/cli/command_modules/appservice/custom.py index 74bc3c0f3ba..7438f58a3f6 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/custom.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/custom.py @@ -6042,14 +6042,13 @@ def _check_service_principal_permissions(cmd, resource_group_name, key_vault_nam def _update_host_name_ssl_state(cmd, resource_group_name, webapp_name, webapp, host_name, ssl_state, thumbprint, slot=None): - Site, HostNameSslState = cmd.get_models('Site', 'HostNameSslState') - updated_webapp = Site(host_name_ssl_states=[HostNameSslState(name=host_name, - ssl_state=ssl_state, - thumbprint=thumbprint, - to_update=True)], - location=webapp.location, tags=webapp.tags) + HostNameSslState = cmd.get_models('HostNameSslState') + webapp.host_name_ssl_states = [HostNameSslState(name=host_name, + ssl_state=ssl_state, + thumbprint=thumbprint, + to_update=True)] return _generic_site_operation(cmd.cli_ctx, resource_group_name, webapp_name, 'begin_create_or_update', - slot, updated_webapp) + slot, webapp) def _update_ssl_binding(cmd, resource_group_name, name, certificate_thumbprint, ssl_type, hostname, slot=None): diff --git a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py index 31fd8714bb3..f876a619633 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py @@ -21,6 +21,7 @@ view_in_browser, sync_site_repo, _match_host_names_from_cert, + _update_host_name_ssl_state, bind_ssl_cert, list_publish_profiles, show_app, @@ -642,6 +643,59 @@ def test_update_webapp_platform_release_channel_latest(self): self.assertEqual(result.additional_properties["properties"]["platformReleaseChannel"], "Latest") +class TestUpdateHostNameSslState(unittest.TestCase): + @mock.patch('azure.cli.command_modules.appservice.custom._generic_site_operation', autospec=True) + def test_update_host_name_ssl_state_passes_full_site(self, generic_site_op_mock): + """Test that _update_host_name_ssl_state passes the full Site object (not a partial one) + to begin_create_or_update, preserving policy-sensitive fields like https_only.""" + cmd_mock = _get_test_cmd() + Site, HostNameSslState, SslState = cmd_mock.get_models('Site', 'HostNameSslState', 'SslState') + + webapp = Site(name='mySite', location='eastus', tags={'env': 'prod'}) + webapp.https_only = True + webapp.host_name_ssl_states = [ + HostNameSslState(name='existing.contoso.com', + ssl_state=SslState.sni_enabled, + thumbprint='EXISTINGTHUMB') + ] + + _update_host_name_ssl_state(cmd_mock, 'myRg', 'mySite', webapp, + 'www.contoso.com', SslState.sni_enabled, 'NEWTHUMB') + + generic_site_op_mock.assert_called_once() + call_args = generic_site_op_mock.call_args + passed_site = call_args[0][5] # (cli_ctx, rg, name, op, slot, site) + + # The passed object should be the original webapp, preserving all fields + self.assertTrue(passed_site.https_only, + "https_only must be preserved to avoid Azure Policy denial") + self.assertEqual(passed_site.location, 'eastus') + self.assertEqual(passed_site.tags, {'env': 'prod'}) + + # host_name_ssl_states should contain only the binding being updated + self.assertEqual(len(passed_site.host_name_ssl_states), 1) + ssl_state = passed_site.host_name_ssl_states[0] + self.assertEqual(ssl_state.name, 'www.contoso.com') + self.assertEqual(ssl_state.thumbprint, 'NEWTHUMB') + self.assertTrue(ssl_state.to_update) + + @mock.patch('azure.cli.command_modules.appservice.custom._generic_site_operation', autospec=True) + def test_update_host_name_ssl_state_with_slot(self, generic_site_op_mock): + """Test that slot parameter is correctly forwarded.""" + cmd_mock = _get_test_cmd() + Site, SslState = cmd_mock.get_models('Site', 'SslState') + webapp = Site(name='mySite', location='westus') + + _update_host_name_ssl_state(cmd_mock, 'myRg', 'mySite', webapp, + 'www.contoso.com', SslState.sni_enabled, 'THUMB', slot='staging') + + call_args = generic_site_op_mock.call_args + # slot is the 5th positional arg (index 4) after cli_ctx, rg, name, operation_name + self.assertEqual(call_args[0][4], 'staging') + # site is the 6th positional arg (index 5) + self.assertIs(call_args[0][5], webapp) + + class FakedResponse: # pylint: disable=too-few-public-methods def __init__(self, status_code): self.status_code = status_code From 84e3c7ac33422a2b510921d1b5f97eea258d2fb1 Mon Sep 17 00:00:00 2001 From: Jordan Selig Date: Thu, 26 Mar 2026 10:21:33 -0400 Subject: [PATCH 4/6] Address review: fetch slot Site and use property assertions - In _update_ssl_binding, use get_slot() when slot is provided to avoid sending production Site payload for slot updates (policy/settings issue) - Replace assertIs with property assertions in slot test for robustness Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../azure/cli/command_modules/appservice/custom.py | 5 ++++- .../tests/latest/test_webapp_commands_thru_mock.py | 6 ++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/appservice/custom.py b/src/azure-cli/azure/cli/command_modules/appservice/custom.py index 7438f58a3f6..abee8a9aa95 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/custom.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/custom.py @@ -6053,7 +6053,10 @@ def _update_host_name_ssl_state(cmd, resource_group_name, webapp_name, webapp, def _update_ssl_binding(cmd, resource_group_name, name, certificate_thumbprint, ssl_type, hostname, slot=None): client = web_client_factory(cmd.cli_ctx) - webapp = client.web_apps.get(resource_group_name, name) + if slot: + webapp = client.web_apps.get_slot(resource_group_name, name, slot) + else: + webapp = client.web_apps.get(resource_group_name, name) if not webapp: raise ResourceNotFoundError("'{}' app doesn't exist".format(name)) diff --git a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py index f876a619633..54739eb2253 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py @@ -692,8 +692,10 @@ def test_update_host_name_ssl_state_with_slot(self, generic_site_op_mock): call_args = generic_site_op_mock.call_args # slot is the 5th positional arg (index 4) after cli_ctx, rg, name, operation_name self.assertEqual(call_args[0][4], 'staging') - # site is the 6th positional arg (index 5) - self.assertIs(call_args[0][5], webapp) + # site is the 6th positional arg (index 5); verify it has the expected properties + site_arg = call_args[0][5] + self.assertEqual(site_arg.name, webapp.name) + self.assertEqual(site_arg.location, webapp.location) class FakedResponse: # pylint: disable=too-few-public-methods From 77a5431fbe396968a35c290ecb7d33f1fbbb6dfc Mon Sep 17 00:00:00 2001 From: Jordan Selig Date: Thu, 26 Mar 2026 10:49:52 -0400 Subject: [PATCH 5/6] [App Service] Fix #18697: `az webapp config ssl create`: Add `--wait` flag for managed certificate automation When --wait is set: - Extends polling timeout from 2 minutes to 10 minutes - Raises CLIError on timeout instead of silently returning None - Enables automation scripts to reliably chain ssl bind after ssl create Default behavior (without --wait) is unchanged: 2-minute timeout with warning. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../cli/command_modules/appservice/_help.py | 2 + .../cli/command_modules/appservice/_params.py | 3 + .../cli/command_modules/appservice/custom.py | 10 ++- .../latest/test_webapp_commands_thru_mock.py | 88 +++++++++++++++++++ 4 files changed, 101 insertions(+), 2 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/appservice/_help.py b/src/azure-cli/azure/cli/command_modules/appservice/_help.py index e0dad92e98c..d963d3fcb5e 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/_help.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/_help.py @@ -1776,6 +1776,8 @@ examples: - name: Create a Managed Certificate for cname.mycustomdomain.com. text: az webapp config ssl create --resource-group MyResourceGroup --name MyWebapp --hostname cname.mycustomdomain.com + - name: Create a Managed Certificate and wait for it to complete (up to 10 minutes). + text: az webapp config ssl create --resource-group MyResourceGroup --name MyWebapp --hostname cname.mycustomdomain.com --wait """ helps['webapp config storage-account'] = """ diff --git a/src/azure-cli/azure/cli/command_modules/appservice/_params.py b/src/azure-cli/azure/cli/command_modules/appservice/_params.py index bd4dc0b7ea5..9f0354f75b5 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/_params.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/_params.py @@ -521,6 +521,9 @@ def load_arguments(self, _): c.argument('hostname', help='The custom domain name') c.argument('name', options_list=['--name', '-n'], help='Name of the web app.') c.argument('resource-group', options_list=['--resource-group', '-g'], help='Name of resource group.') + c.argument('wait', options_list=['--wait'], action='store_true', default=False, + help='Wait up to 10 minutes for the certificate to be created. ' + 'Returns an error if creation times out instead of silently returning.') with self.argument_context(scope + ' config hostname') as c: c.argument('hostname', completer=get_hostname_completion_list, help="hostname assigned to the site, such as custom domains", id_part='child_name_1') diff --git a/src/azure-cli/azure/cli/command_modules/appservice/custom.py b/src/azure-cli/azure/cli/command_modules/appservice/custom.py index abee8a9aa95..6e97318ee04 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/custom.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/custom.py @@ -5960,7 +5960,7 @@ def import_ssl_cert(cmd, resource_group_name, key_vault, key_vault_certificate_n certificate_envelope=kv_cert_def) -def create_managed_ssl_cert(cmd, resource_group_name, name, hostname, slot=None, certificate_name=None): +def create_managed_ssl_cert(cmd, resource_group_name, name, hostname, slot=None, certificate_name=None, wait=False): Certificate = cmd.get_models('Certificate') hostname = hostname.lower() client = web_client_factory(cmd.cli_ctx) @@ -5997,7 +5997,8 @@ def create_managed_ssl_cert(cmd, resource_group_name, name, hostname, slot=None, poll_url = ex.response.headers['Location'] if 'Location' in ex.response.headers else None if ex.response.status_code == 202 and poll_url: r = send_raw_request(cmd.cli_ctx, method='get', url=poll_url) - poll_timeout = time.time() + 60 * 2 # 2 minute timeout + poll_timeout_minutes = 10 if wait else 2 + poll_timeout = time.time() + 60 * poll_timeout_minutes while r.status_code != 200 and time.time() < poll_timeout: time.sleep(5) @@ -6008,6 +6009,11 @@ def create_managed_ssl_cert(cmd, resource_group_name, name, hostname, slot=None, return r.json() except ValueError: return r.text + if wait: + raise CLIError("Managed Certificate creation for '{}' timed out after {} minutes. " + "Check status with 'az webapp config ssl show -g {} " + "--certificate-name {}'.".format(hostname, poll_timeout_minutes, + resource_group_name, certificate_name)) logger.warning("Managed Certificate creation in progress. Please use the command " "'az webapp config ssl show -g %s --certificate-name %s' " " to view your certificate once it is created", resource_group_name, certificate_name) diff --git a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py index 54739eb2253..4832c685135 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py @@ -473,6 +473,94 @@ 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.send_raw_request', autospec=True) + @mock.patch('azure.cli.command_modules.appservice.custom._verify_hostname_binding', autospec=True) + @mock.patch('azure.cli.command_modules.appservice.custom.web_client_factory', autospec=True) + @mock.patch('azure.cli.command_modules.appservice.custom._generic_site_operation', autospec=True) + def test_create_managed_ssl_cert_wait_timeout_raises_error(self, generic_site_op_mock, client_factory_mock, + verify_binding_mock, send_raw_request_mock): + """Test that --wait raises CLIError on timeout instead of returning None.""" + webapp_name = 'someWebAppName' + rg_name = 'someRgName' + farm_id = '/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg1/providers/Microsoft.Web/serverfarms/farm1' + host_name = 'www.contoso.com' + + client = mock.MagicMock() + client_factory_mock.return_value = client + cmd_mock = _get_test_cmd() + cli_ctx_mock = mock.MagicMock() + cli_ctx_mock.data = {'subscription_id': 'sub1'} + cmd_mock.cli_ctx = cli_ctx_mock + Site, Certificate = cmd_mock.get_models('Site', 'Certificate') + site = Site(name=webapp_name, location='westeurope') + site.server_farm_id = farm_id + generic_site_op_mock.return_value = site + verify_binding_mock.return_value = True + + # Simulate 202 with Location header + ex_response = mock.MagicMock() + ex_response.status_code = 202 + ex_response.headers = {'Location': 'https://polling-url'} + api_exception = Exception('accepted') + api_exception.response = ex_response + client.certificates.create_or_update.side_effect = api_exception + + # Polling always returns 202 (never completes) + poll_response = mock.MagicMock() + poll_response.status_code = 202 + send_raw_request_mock.return_value = poll_response + + # With wait=True and mocked time to simulate immediate timeout + with mock.patch('azure.cli.command_modules.appservice.custom.time') as time_mock: + time_mock.time.side_effect = [0, 999999] # Start, then past timeout + time_mock.sleep = mock.MagicMock() + with self.assertRaises(CLIError): + create_managed_ssl_cert(cmd_mock, rg_name, webapp_name, host_name, None, wait=True) + + @mock.patch('azure.cli.command_modules.appservice.custom.send_raw_request', autospec=True) + @mock.patch('azure.cli.command_modules.appservice.custom._verify_hostname_binding', autospec=True) + @mock.patch('azure.cli.command_modules.appservice.custom.web_client_factory', autospec=True) + @mock.patch('azure.cli.command_modules.appservice.custom._generic_site_operation', autospec=True) + def test_create_managed_ssl_cert_no_wait_returns_none(self, generic_site_op_mock, client_factory_mock, + verify_binding_mock, send_raw_request_mock): + """Test that without --wait, timeout returns None with a warning (default behavior).""" + webapp_name = 'someWebAppName' + rg_name = 'someRgName' + farm_id = '/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg1/providers/Microsoft.Web/serverfarms/farm1' + host_name = 'www.contoso.com' + + client = mock.MagicMock() + client_factory_mock.return_value = client + cmd_mock = _get_test_cmd() + cli_ctx_mock = mock.MagicMock() + cli_ctx_mock.data = {'subscription_id': 'sub1'} + cmd_mock.cli_ctx = cli_ctx_mock + Site, Certificate = cmd_mock.get_models('Site', 'Certificate') + site = Site(name=webapp_name, location='westeurope') + site.server_farm_id = farm_id + generic_site_op_mock.return_value = site + verify_binding_mock.return_value = True + + # Simulate 202 with Location header + ex_response = mock.MagicMock() + ex_response.status_code = 202 + ex_response.headers = {'Location': 'https://polling-url'} + api_exception = Exception('accepted') + api_exception.response = ex_response + client.certificates.create_or_update.side_effect = api_exception + + # Polling always returns 202 + poll_response = mock.MagicMock() + poll_response.status_code = 202 + send_raw_request_mock.return_value = poll_response + + # Without wait (default), should return None, not raise + with mock.patch('azure.cli.command_modules.appservice.custom.time') as time_mock: + time_mock.time.side_effect = [0, 999999] + time_mock.sleep = mock.MagicMock() + result = create_managed_ssl_cert(cmd_mock, rg_name, webapp_name, host_name, None, wait=False) + self.assertIsNone(result) + def test_update_app_settings_error_handling_no_parameters(self): """Test that MutuallyExclusiveArgumentError is raised when neither settings nor slot_settings are provided.""" cmd_mock = _get_test_cmd() From cf391c1566b6ab057510ca4aa89446e579c5e6a8 Mon Sep 17 00:00:00 2001 From: Jordan Selig Date: Thu, 26 Mar 2026 15:32:40 -0400 Subject: [PATCH 6/6] Address review: lazy iteration, early break, fix recording for get_slot - delete_ssl_cert: iterate pager lazily with early return instead of list() - _update_ssl_binding: iterate pagers lazily with early break instead of list() - _update_ssl_binding: use next() generator expression for subscription search - Fix test_webapp_ssl recording to match get_slot calls for slot bind/unbind - Add test_delete_ssl_cert_early_break_skips_remaining_pages test - Update _FakePagedIterator docstring for early-break behavior Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../cli/command_modules/appservice/custom.py | 14 ++++------ .../latest/recordings/test_webapp_ssl.yaml | 28 +++++++++---------- .../latest/test_webapp_commands_thru_mock.py | 25 +++++++++++++++-- 3 files changed, 42 insertions(+), 25 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/appservice/custom.py b/src/azure-cli/azure/cli/command_modules/appservice/custom.py index 6e97318ee04..c330a698c55 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/custom.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/custom.py @@ -5870,8 +5870,7 @@ def show_ssl_cert(cmd, resource_group_name, certificate_name): def delete_ssl_cert(cmd, resource_group_name, certificate_thumbprint): client = web_client_factory(cmd.cli_ctx) - webapp_certs = list(client.certificates.list_by_resource_group(resource_group_name)) - for webapp_cert in webapp_certs: + for webapp_cert in client.certificates.list_by_resource_group(resource_group_name): if webapp_cert.thumbprint == certificate_thumbprint: return client.certificates.delete(resource_group_name, webapp_cert.name) raise ResourceNotFoundError("Certificate for thumbprint '{}' not found".format(certificate_thumbprint)) @@ -6067,25 +6066,24 @@ def _update_ssl_binding(cmd, resource_group_name, name, certificate_thumbprint, raise ResourceNotFoundError("'{}' app doesn't exist".format(name)) cert_resource_group_name = parse_resource_id(webapp.server_farm_id)['resource_group'] - webapp_certs = list(client.certificates.list_by_resource_group(cert_resource_group_name)) found_cert = None # search for a cert that matches in the app service plan's RG - for webapp_cert in webapp_certs: + for webapp_cert in client.certificates.list_by_resource_group(cert_resource_group_name): if webapp_cert.thumbprint == certificate_thumbprint: found_cert = webapp_cert break # search for a cert that matches in the webapp's RG if not found_cert: - webapp_certs = list(client.certificates.list_by_resource_group(resource_group_name)) - for webapp_cert in webapp_certs: + for webapp_cert in client.certificates.list_by_resource_group(resource_group_name): if webapp_cert.thumbprint == certificate_thumbprint: found_cert = webapp_cert break # search for a cert that matches in the subscription, filtering on the serverfarm if not found_cert: - sub_certs = client.certificates.list(filter=f"ServerFarmId eq '{webapp.server_farm_id}'") - found_cert = next(iter([c for c in sub_certs if c.thumbprint == certificate_thumbprint]), None) + found_cert = next((c for c in client.certificates.list( + filter=f"ServerFarmId eq '{webapp.server_farm_id}'") + if c.thumbprint == certificate_thumbprint), None) if found_cert: if not hostname: if len(found_cert.host_names) == 1 and not found_cert.host_names[0].startswith('*'): diff --git a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/recordings/test_webapp_ssl.yaml b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/recordings/test_webapp_ssl.yaml index 115b33f6e69..8000de4135e 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/recordings/test_webapp_ssl.yaml +++ b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/recordings/test_webapp_ssl.yaml @@ -3476,22 +3476,22 @@ interactions: User-Agent: - AZURECLI/2.79.0 azsdk-python-core/1.35.0 Python/3.13.9 (Windows-11-10.0.26200-SP0) method: GET - uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.Web/sites/web-ssl-test000003?api-version=2024-11-01 + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.Web/sites/web-ssl-test000003/slots/slot-ssl-test000004?api-version=2024-11-01 response: body: - string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.Web/sites/web-ssl-test000003","name":"web-ssl-test000003","type":"Microsoft.Web/sites","kind":"app","location":"West - Europe","tags":{"web":"web1"},"properties":{"name":"web-ssl-test000003","state":"Running","hostNames":["web-ssl-test000003.azurewebsites.net"],"webSpace":"clitest.rg000001-WestEuropewebspace","selfLink":"https://waws-prod-am2-019.api.azurewebsites.windows.net:454/subscriptions/00000000-0000-0000-0000-000000000000/webspaces/clitest.rg000001-WestEuropewebspace/sites/web-ssl-test000003","repositorySiteName":"web-ssl-test000003","owner":null,"usageState":"Normal","enabled":true,"adminEnabled":true,"siteScopedCertificatesEnabled":false,"afdEnabled":false,"enabledHostNames":["web-ssl-test000003.azurewebsites.net","web-ssl-test000003.scm.azurewebsites.net"],"siteProperties":{"metadata":null,"properties":[{"name":"LinuxFxVersion","value":""},{"name":"WindowsFxVersion","value":null}],"appSettings":null},"availabilityState":"Normal","sslCertificates":null,"csrs":[],"cers":null,"siteMode":null,"hostNameSslStates":[{"name":"web-ssl-test000003.azurewebsites.net","sslState":"Disabled","ipBasedSslResult":null,"virtualIP":null,"virtualIPv6":null,"thumbprint":null,"certificateResourceId":null,"toUpdate":null,"toUpdateIpBasedSsl":null,"ipBasedSslState":"NotConfigured","hostType":"Standard"},{"name":"web-ssl-test000003.scm.azurewebsites.net","sslState":"Disabled","ipBasedSslResult":null,"virtualIP":null,"virtualIPv6":null,"thumbprint":null,"certificateResourceId":null,"toUpdate":null,"toUpdateIpBasedSsl":null,"ipBasedSslState":"NotConfigured","hostType":"Repository"}],"computeMode":null,"serverFarm":null,"serverFarmId":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.Web/serverfarms/ssl-test-plan000002","reserved":false,"isXenon":false,"hyperV":false,"sandboxType":null,"lastModifiedTimeUtc":"2025-10-30T22:13:18.483","storageRecoveryDefaultState":"Running","contentAvailabilityState":"Normal","runtimeAvailabilityState":"Normal","dnsConfiguration":{},"containerAllocationSubnet":null,"useContainerLocalhostBindings":null,"outboundVnetRouting":{"allTraffic":false,"applicationTraffic":false,"contentShareTraffic":false,"imagePullTraffic":false,"backupRestoreTraffic":false,"managedIdentityTraffic":false},"legacyServiceEndpointTrafficEvaluation":null,"siteConfig":{"numberOfWorkers":1,"defaultDocuments":null,"netFrameworkVersion":null,"phpVersion":null,"pythonVersion":null,"nodeVersion":null,"powerShellVersion":null,"linuxFxVersion":"","windowsFxVersion":null,"sandboxType":null,"windowsConfiguredStacks":null,"requestTracingEnabled":null,"remoteDebuggingEnabled":null,"remoteDebuggingVersion":null,"httpLoggingEnabled":null,"azureMonitorLogCategories":null,"acrUseManagedIdentityCreds":false,"acrUserManagedIdentityID":null,"logsDirectorySizeLimit":null,"detailedErrorLoggingEnabled":null,"publishingUsername":null,"publishingPassword":null,"appSettings":null,"metadata":null,"connectionStrings":null,"machineKey":null,"handlerMappings":null,"documentRoot":null,"scmType":null,"use32BitWorkerProcess":null,"webSocketsEnabled":null,"alwaysOn":true,"javaVersion":null,"javaContainer":null,"javaContainerVersion":null,"appCommandLine":null,"managedPipelineMode":null,"virtualApplications":null,"winAuthAdminState":null,"winAuthTenantState":null,"customAppPoolIdentityAdminState":null,"customAppPoolIdentityTenantState":null,"runtimeADUser":null,"runtimeADUserPassword":null,"loadBalancing":null,"routingRules":null,"experiments":null,"limits":null,"autoHealEnabled":null,"autoHealRules":null,"tracingOptions":null,"vnetName":null,"vnetRouteAllEnabled":null,"vnetPrivatePortsCount":null,"publicNetworkAccess":null,"cors":null,"push":null,"apiDefinition":null,"apiManagementConfig":null,"autoSwapSlotName":null,"localMySqlEnabled":null,"managedServiceIdentityId":null,"xManagedServiceIdentityId":null,"keyVaultReferenceIdentity":null,"ipSecurityRestrictions":null,"ipSecurityRestrictionsDefaultAction":null,"scmIpSecurityRestrictions":null,"scmIpSecurityRestrictionsDefaultAction":null,"scmIpSecurityRestrictionsUseMain":null,"http20Enabled":true,"minTlsVersion":null,"minTlsCipherSuite":null,"scmMinTlsCipherSuite":null,"supportedTlsCipherSuites":null,"scmSupportedTlsCipherSuites":null,"scmMinTlsVersion":null,"ftpsState":null,"preWarmedInstanceCount":null,"functionAppScaleLimit":0,"elasticWebAppScaleLimit":null,"healthCheckPath":null,"fileChangeAuditEnabled":null,"functionsRuntimeScaleMonitoringEnabled":null,"websiteTimeZone":null,"minimumElasticInstanceCount":0,"azureStorageAccounts":null,"http20ProxyFlag":null,"sitePort":null,"antivirusScanEnabled":null,"storageType":null,"sitePrivateLinkHostEnabled":null,"clusteringEnabled":false,"webJobsEnabled":false},"functionAppConfig":null,"daprConfig":null,"deploymentId":"web-ssl-test000003","slotName":null,"trafficManagerHostNames":null,"sku":"Standard","scmSiteAlsoStopped":false,"targetSwapSlot":null,"hostingEnvironment":null,"hostingEnvironmentProfile":null,"clientAffinityEnabled":true,"clientAffinityProxyEnabled":false,"useQueryStringAffinity":false,"blockPathTraversal":false,"clientCertEnabled":false,"clientCertMode":"Required","clientCertExclusionPaths":null,"clientCertExclusionEndPoints":null,"hostNamesDisabled":false,"ipMode":"IPv4","domainVerificationIdentifiers":null,"customDomainVerificationId":"06A754DDDA9E82CEAB4064B1FFBB341F2D951D8E36BC157906AEE1799EE3B407","kind":"app","managedEnvironmentId":null,"workloadProfileName":null,"resourceConfig":null,"inboundIpAddress":"13.69.68.36,104.45.14.249","possibleInboundIpAddresses":"13.69.68.36,104.45.14.249,13.69.68.36","inboundIpv6Address":"2603:1020:206:7::4a","possibleInboundIpv6Addresses":"2603:1020:206:7::4a","ftpUsername":"web-ssl-test000003\\$web-ssl-test000003","ftpsHostName":"ftps://waws-prod-am2-019.ftp.azurewebsites.windows.net/site/wwwroot","outboundIpAddresses":"104.45.14.250,104.45.14.251,104.45.14.252,104.45.14.253,13.69.68.36,104.45.14.249","possibleOutboundIpAddresses":"104.45.14.250,104.45.14.251,104.45.14.252,104.45.14.253,108.142.104.60,13.80.0.253,13.80.6.186,40.118.12.139,13.80.6.154,13.80.6.191,20.101.207.109,20.23.64.188,20.23.67.119,20.23.67.191,20.23.68.207,20.23.69.216,108.141.244.55,108.141.244.75,108.141.244.80,108.141.244.85,108.141.244.124,108.141.244.167,13.69.68.36,104.45.14.249","outboundIpv6Addresses":"2603:1020:203:f::3e3,2603:1020:203:10::39d,2603:1020:203:10::3a0,2603:1020:203:1e::361,2603:1020:206:7::4a,2603:10e1:100:2::682d:ef9","possibleOutboundIpv6Addresses":"2603:1020:203:f::3e3,2603:1020:203:10::39d,2603:1020:203:10::3a0,2603:1020:203:1e::361,2603:1020:203:17::3ea,2603:1020:203:a::405,2603:1020:203:11::3b1,2603:1020:203:f::3e6,2603:1020:203:6::3fa,2603:1020:203:f::3e8,2603:1020:203:5::3ca,2603:1020:203:1f::3cc,2603:1020:203:a::409,2603:1020:203:12::3c5,2603:1020:203:d::3d4,2603:1020:203:b::3e9,2603:1020:203:5::3cc,2603:1020:203:1a::3b5,2603:1020:203:15::3d0,2603:1020:203:7::105,2603:1020:203:e::3de,2603:1020:206:7::4a,2603:10e1:100:2::682d:ef9","containerSize":0,"dailyMemoryTimeQuota":0,"suspendedTill":null,"siteDisabledReason":0,"functionExecutionUnitsCache":null,"maxNumberOfWorkers":null,"homeStamp":"waws-prod-am2-019","cloningInfo":null,"hostingEnvironmentId":null,"tags":{"web":"web1"},"resourceGroup":"clitest.rg000001","defaultHostName":"web-ssl-test000003.azurewebsites.net","slotSwapStatus":null,"httpsOnly":true,"endToEndEncryptionEnabled":false,"functionsRuntimeAdminIsolationEnabled":false,"redundancyMode":"None","inProgressOperationId":null,"geoDistributions":null,"privateEndpointConnections":[],"publicNetworkAccess":"Enabled","buildVersion":null,"targetBuildVersion":null,"migrationState":null,"eligibleLogCategories":"AppServiceAppLogs,AppServiceAuditLogs,AppServiceConsoleLogs,AppServiceHTTPLogs,AppServiceIPSecAuditLogs,AppServicePlatformLogs,ScanLogs,AppServiceAuthenticationLogs","inFlightFeatures":["SiteContainers","RouteGeoCapacityClientTrafficToV2","RouteOperationClientTrafficToV2","RouteGeoPlanClientTrafficToV2","RouteGeoSourceControlKeyClientTrafficToV2","RouteGeoUserClientTrafficToV2"],"storageAccountRequired":false,"virtualNetworkSubnetId":null,"keyVaultReferenceIdentity":"SystemAssigned","autoGeneratedDomainNameLabelScope":null,"privateLinkIdentifiers":null,"sshEnabled":null}}' + string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.Web/sites/web-ssl-test000003/slots/slot-ssl-test000004","name":"web-ssl-test000003/slot-ssl-test000004","type":"Microsoft.Web/sites/slots","kind":"app","location":"West + Europe","properties":{"name":"web-ssl-test000003(slot-ssl-test000004)","state":"Running","hostNames":["web-ssl-test000003-slot-ssl-test000004.azurewebsites.net"],"webSpace":"clitest.rg000001-WestEuropewebspace","selfLink":"https://waws-prod-am2-019.api.azurewebsites.windows.net:454/subscriptions/00000000-0000-0000-0000-000000000000/webspaces/clitest.rg000001-WestEuropewebspace/sites/web-ssl-test000003","repositorySiteName":"web-ssl-test000003","owner":null,"usageState":"Normal","enabled":true,"adminEnabled":true,"siteScopedCertificatesEnabled":false,"afdEnabled":false,"enabledHostNames":["web-ssl-test000003-slot-ssl-test000004.azurewebsites.net","web-ssl-test000003-slot-ssl-test000004.scm.azurewebsites.net"],"siteProperties":{"metadata":null,"properties":[{"name":"LinuxFxVersion","value":""},{"name":"WindowsFxVersion","value":null}],"appSettings":null},"availabilityState":"Normal","sslCertificates":null,"csrs":[],"cers":null,"siteMode":null,"hostNameSslStates":[{"name":"web-ssl-test000003-slot-ssl-test000004.azurewebsites.net","sslState":"Disabled","ipBasedSslResult":null,"virtualIP":null,"virtualIPv6":null,"thumbprint":null,"certificateResourceId":null,"toUpdate":null,"toUpdateIpBasedSsl":null,"ipBasedSslState":"NotConfigured","hostType":"Standard"},{"name":"web-ssl-test000003-slot-ssl-test000004.scm.azurewebsites.net","sslState":"Disabled","ipBasedSslResult":null,"virtualIP":null,"virtualIPv6":null,"thumbprint":null,"certificateResourceId":null,"toUpdate":null,"toUpdateIpBasedSsl":null,"ipBasedSslState":"NotConfigured","hostType":"Repository"}],"computeMode":null,"serverFarm":null,"serverFarmId":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.Web/serverfarms/ssl-test-plan000002","reserved":false,"isXenon":false,"hyperV":false,"sandboxType":null,"lastModifiedTimeUtc":"2025-10-30T22:13:55.393","storageRecoveryDefaultState":"Running","contentAvailabilityState":"Normal","runtimeAvailabilityState":"Normal","dnsConfiguration":{},"containerAllocationSubnet":null,"useContainerLocalhostBindings":null,"outboundVnetRouting":{"allTraffic":false,"applicationTraffic":false,"contentShareTraffic":false,"imagePullTraffic":false,"backupRestoreTraffic":false,"managedIdentityTraffic":false},"legacyServiceEndpointTrafficEvaluation":null,"siteConfig":{"numberOfWorkers":1,"defaultDocuments":null,"netFrameworkVersion":null,"phpVersion":null,"pythonVersion":null,"nodeVersion":null,"powerShellVersion":null,"linuxFxVersion":"","windowsFxVersion":null,"sandboxType":null,"windowsConfiguredStacks":null,"requestTracingEnabled":null,"remoteDebuggingEnabled":null,"remoteDebuggingVersion":null,"httpLoggingEnabled":null,"azureMonitorLogCategories":null,"acrUseManagedIdentityCreds":false,"acrUserManagedIdentityID":null,"logsDirectorySizeLimit":null,"detailedErrorLoggingEnabled":null,"publishingUsername":null,"publishingPassword":null,"appSettings":null,"metadata":null,"connectionStrings":null,"machineKey":null,"handlerMappings":null,"documentRoot":null,"scmType":null,"use32BitWorkerProcess":null,"webSocketsEnabled":null,"alwaysOn":false,"javaVersion":null,"javaContainer":null,"javaContainerVersion":null,"appCommandLine":null,"managedPipelineMode":null,"virtualApplications":null,"winAuthAdminState":null,"winAuthTenantState":null,"customAppPoolIdentityAdminState":null,"customAppPoolIdentityTenantState":null,"runtimeADUser":null,"runtimeADUserPassword":null,"loadBalancing":null,"routingRules":null,"experiments":null,"limits":null,"autoHealEnabled":null,"autoHealRules":null,"tracingOptions":null,"vnetName":null,"vnetRouteAllEnabled":null,"vnetPrivatePortsCount":null,"publicNetworkAccess":null,"cors":null,"push":null,"apiDefinition":null,"apiManagementConfig":null,"autoSwapSlotName":null,"localMySqlEnabled":null,"managedServiceIdentityId":null,"xManagedServiceIdentityId":null,"keyVaultReferenceIdentity":null,"ipSecurityRestrictions":null,"ipSecurityRestrictionsDefaultAction":null,"scmIpSecurityRestrictions":null,"scmIpSecurityRestrictionsDefaultAction":null,"scmIpSecurityRestrictionsUseMain":null,"http20Enabled":true,"minTlsVersion":null,"minTlsCipherSuite":null,"scmMinTlsCipherSuite":null,"supportedTlsCipherSuites":null,"scmSupportedTlsCipherSuites":null,"scmMinTlsVersion":null,"ftpsState":null,"preWarmedInstanceCount":null,"functionAppScaleLimit":0,"elasticWebAppScaleLimit":null,"healthCheckPath":null,"fileChangeAuditEnabled":null,"functionsRuntimeScaleMonitoringEnabled":null,"websiteTimeZone":null,"minimumElasticInstanceCount":0,"azureStorageAccounts":null,"http20ProxyFlag":null,"sitePort":null,"antivirusScanEnabled":null,"storageType":null,"sitePrivateLinkHostEnabled":null,"clusteringEnabled":false,"webJobsEnabled":false},"functionAppConfig":null,"daprConfig":null,"deploymentId":"web-ssl-test000003__b662","slotName":null,"trafficManagerHostNames":null,"sku":"Standard","scmSiteAlsoStopped":false,"targetSwapSlot":null,"hostingEnvironment":null,"hostingEnvironmentProfile":null,"clientAffinityEnabled":true,"clientAffinityProxyEnabled":false,"useQueryStringAffinity":false,"blockPathTraversal":false,"clientCertEnabled":false,"clientCertMode":"Required","clientCertExclusionPaths":null,"clientCertExclusionEndPoints":null,"hostNamesDisabled":false,"ipMode":"IPv4","domainVerificationIdentifiers":null,"customDomainVerificationId":"06A754DDDA9E82CEAB4064B1FFBB341F2D951D8E36BC157906AEE1799EE3B407","kind":"app","managedEnvironmentId":null,"workloadProfileName":null,"resourceConfig":null,"inboundIpAddress":"13.69.68.36,104.45.14.249","possibleInboundIpAddresses":"13.69.68.36,104.45.14.249,13.69.68.36","inboundIpv6Address":"2603:1020:206:7::4a","possibleInboundIpv6Addresses":"2603:1020:206:7::4a","ftpUsername":"web-ssl-test000003__slot-ssl-test000004\\$web-ssl-test000003__slot-ssl-test000004","ftpsHostName":"ftps://waws-prod-am2-019.ftp.azurewebsites.windows.net/site/wwwroot","outboundIpAddresses":"104.45.14.250,104.45.14.251,104.45.14.252,104.45.14.253,13.69.68.36,104.45.14.249","possibleOutboundIpAddresses":"104.45.14.250,104.45.14.251,104.45.14.252,104.45.14.253,108.142.104.60,13.80.0.253,13.80.6.186,40.118.12.139,13.80.6.154,13.80.6.191,20.101.207.109,20.23.64.188,20.23.67.119,20.23.67.191,20.23.68.207,20.23.69.216,108.141.244.55,108.141.244.75,108.141.244.80,108.141.244.85,108.141.244.124,108.141.244.167,13.69.68.36,104.45.14.249","outboundIpv6Addresses":"2603:1020:203:f::3e3,2603:1020:203:10::39d,2603:1020:203:10::3a0,2603:1020:203:1e::361,2603:1020:206:7::4a,2603:10e1:100:2::682d:ef9","possibleOutboundIpv6Addresses":"2603:1020:203:f::3e3,2603:1020:203:10::39d,2603:1020:203:10::3a0,2603:1020:203:1e::361,2603:1020:203:17::3ea,2603:1020:203:a::405,2603:1020:203:11::3b1,2603:1020:203:f::3e6,2603:1020:203:6::3fa,2603:1020:203:f::3e8,2603:1020:203:5::3ca,2603:1020:203:1f::3cc,2603:1020:203:a::409,2603:1020:203:12::3c5,2603:1020:203:d::3d4,2603:1020:203:b::3e9,2603:1020:203:5::3cc,2603:1020:203:1a::3b5,2603:1020:203:15::3d0,2603:1020:203:7::105,2603:1020:203:e::3de,2603:1020:206:7::4a,2603:10e1:100:2::682d:ef9","containerSize":0,"dailyMemoryTimeQuota":0,"suspendedTill":null,"siteDisabledReason":0,"functionExecutionUnitsCache":null,"maxNumberOfWorkers":null,"homeStamp":"waws-prod-am2-019","cloningInfo":null,"hostingEnvironmentId":null,"tags":null,"resourceGroup":"clitest.rg000001","defaultHostName":"web-ssl-test000003-slot-ssl-test000004.azurewebsites.net","slotSwapStatus":null,"httpsOnly":true,"endToEndEncryptionEnabled":false,"functionsRuntimeAdminIsolationEnabled":false,"redundancyMode":"None","inProgressOperationId":null,"geoDistributions":null,"privateEndpointConnections":[],"publicNetworkAccess":"Enabled","buildVersion":null,"targetBuildVersion":null,"migrationState":null,"eligibleLogCategories":"AppServiceAppLogs,AppServiceAuditLogs,AppServiceConsoleLogs,AppServiceHTTPLogs,AppServiceIPSecAuditLogs,AppServicePlatformLogs,ScanLogs,AppServiceAuthenticationLogs","inFlightFeatures":["SiteContainers","RouteGeoCapacityClientTrafficToV2","RouteOperationClientTrafficToV2","RouteGeoPlanClientTrafficToV2","RouteGeoSourceControlKeyClientTrafficToV2","RouteGeoUserClientTrafficToV2"],"storageAccountRequired":false,"virtualNetworkSubnetId":null,"keyVaultReferenceIdentity":"SystemAssigned","autoGeneratedDomainNameLabelScope":null,"privateLinkIdentifiers":null,"sshEnabled":null}}' headers: cache-control: - no-cache content-length: - - '8403' + - '8613' content-type: - application/json date: - - Thu, 30 Oct 2025 22:14:02 GMT + - Thu, 30 Oct 2025 22:14:00 GMT etag: - - '"1DC49EA654A8630"' + - '"1DC49EA7B4A8B10"' expires: - '-1' pragma: @@ -3507,7 +3507,7 @@ interactions: x-ms-ratelimit-remaining-subscription-global-reads: - '16499' x-msedge-ref: - - 'Ref A: 4131094C4BAE48908A206E6D56ED0A57 Ref B: SN4AA2022304017 Ref C: 2025-10-30T22:14:02Z' + - 'Ref A: 51E447BEEBEE49328ED874720B59E09F Ref B: SN4AA2022301029 Ref C: 2025-10-30T22:13:59Z' x-powered-by: - ASP.NET status: @@ -4182,22 +4182,22 @@ interactions: User-Agent: - AZURECLI/2.79.0 azsdk-python-core/1.35.0 Python/3.13.9 (Windows-11-10.0.26200-SP0) method: GET - uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.Web/sites/web-ssl-test000003?api-version=2024-11-01 + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.Web/sites/web-ssl-test000003/slots/slot-ssl-test000004?api-version=2024-11-01 response: body: - string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.Web/sites/web-ssl-test000003","name":"web-ssl-test000003","type":"Microsoft.Web/sites","kind":"app","location":"West - Europe","tags":{"web":"web1"},"properties":{"name":"web-ssl-test000003","state":"Running","hostNames":["web-ssl-test000003.azurewebsites.net"],"webSpace":"clitest.rg000001-WestEuropewebspace","selfLink":"https://waws-prod-am2-019.api.azurewebsites.windows.net:454/subscriptions/00000000-0000-0000-0000-000000000000/webspaces/clitest.rg000001-WestEuropewebspace/sites/web-ssl-test000003","repositorySiteName":"web-ssl-test000003","owner":null,"usageState":"Normal","enabled":true,"adminEnabled":true,"siteScopedCertificatesEnabled":false,"afdEnabled":false,"enabledHostNames":["web-ssl-test000003.azurewebsites.net","web-ssl-test000003.scm.azurewebsites.net"],"siteProperties":{"metadata":null,"properties":[{"name":"LinuxFxVersion","value":""},{"name":"WindowsFxVersion","value":null}],"appSettings":null},"availabilityState":"Normal","sslCertificates":null,"csrs":[],"cers":null,"siteMode":null,"hostNameSslStates":[{"name":"web-ssl-test000003.azurewebsites.net","sslState":"Disabled","ipBasedSslResult":null,"virtualIP":null,"virtualIPv6":null,"thumbprint":null,"certificateResourceId":null,"toUpdate":null,"toUpdateIpBasedSsl":null,"ipBasedSslState":"NotConfigured","hostType":"Standard"},{"name":"web-ssl-test000003.scm.azurewebsites.net","sslState":"Disabled","ipBasedSslResult":null,"virtualIP":null,"virtualIPv6":null,"thumbprint":null,"certificateResourceId":null,"toUpdate":null,"toUpdateIpBasedSsl":null,"ipBasedSslState":"NotConfigured","hostType":"Repository"}],"computeMode":null,"serverFarm":null,"serverFarmId":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.Web/serverfarms/ssl-test-plan000002","reserved":false,"isXenon":false,"hyperV":false,"sandboxType":null,"lastModifiedTimeUtc":"2025-10-30T22:13:18.483","storageRecoveryDefaultState":"Running","contentAvailabilityState":"Normal","runtimeAvailabilityState":"Normal","dnsConfiguration":{},"containerAllocationSubnet":null,"useContainerLocalhostBindings":null,"outboundVnetRouting":{"allTraffic":false,"applicationTraffic":false,"contentShareTraffic":false,"imagePullTraffic":false,"backupRestoreTraffic":false,"managedIdentityTraffic":false},"legacyServiceEndpointTrafficEvaluation":null,"siteConfig":{"numberOfWorkers":1,"defaultDocuments":null,"netFrameworkVersion":null,"phpVersion":null,"pythonVersion":null,"nodeVersion":null,"powerShellVersion":null,"linuxFxVersion":"","windowsFxVersion":null,"sandboxType":null,"windowsConfiguredStacks":null,"requestTracingEnabled":null,"remoteDebuggingEnabled":null,"remoteDebuggingVersion":null,"httpLoggingEnabled":null,"azureMonitorLogCategories":null,"acrUseManagedIdentityCreds":false,"acrUserManagedIdentityID":null,"logsDirectorySizeLimit":null,"detailedErrorLoggingEnabled":null,"publishingUsername":null,"publishingPassword":null,"appSettings":null,"metadata":null,"connectionStrings":null,"machineKey":null,"handlerMappings":null,"documentRoot":null,"scmType":null,"use32BitWorkerProcess":null,"webSocketsEnabled":null,"alwaysOn":true,"javaVersion":null,"javaContainer":null,"javaContainerVersion":null,"appCommandLine":null,"managedPipelineMode":null,"virtualApplications":null,"winAuthAdminState":null,"winAuthTenantState":null,"customAppPoolIdentityAdminState":null,"customAppPoolIdentityTenantState":null,"runtimeADUser":null,"runtimeADUserPassword":null,"loadBalancing":null,"routingRules":null,"experiments":null,"limits":null,"autoHealEnabled":null,"autoHealRules":null,"tracingOptions":null,"vnetName":null,"vnetRouteAllEnabled":null,"vnetPrivatePortsCount":null,"publicNetworkAccess":null,"cors":null,"push":null,"apiDefinition":null,"apiManagementConfig":null,"autoSwapSlotName":null,"localMySqlEnabled":null,"managedServiceIdentityId":null,"xManagedServiceIdentityId":null,"keyVaultReferenceIdentity":null,"ipSecurityRestrictions":null,"ipSecurityRestrictionsDefaultAction":null,"scmIpSecurityRestrictions":null,"scmIpSecurityRestrictionsDefaultAction":null,"scmIpSecurityRestrictionsUseMain":null,"http20Enabled":true,"minTlsVersion":null,"minTlsCipherSuite":null,"scmMinTlsCipherSuite":null,"supportedTlsCipherSuites":null,"scmSupportedTlsCipherSuites":null,"scmMinTlsVersion":null,"ftpsState":null,"preWarmedInstanceCount":null,"functionAppScaleLimit":0,"elasticWebAppScaleLimit":null,"healthCheckPath":null,"fileChangeAuditEnabled":null,"functionsRuntimeScaleMonitoringEnabled":null,"websiteTimeZone":null,"minimumElasticInstanceCount":0,"azureStorageAccounts":null,"http20ProxyFlag":null,"sitePort":null,"antivirusScanEnabled":null,"storageType":null,"sitePrivateLinkHostEnabled":null,"clusteringEnabled":false,"webJobsEnabled":false},"functionAppConfig":null,"daprConfig":null,"deploymentId":"web-ssl-test000003","slotName":null,"trafficManagerHostNames":null,"sku":"Standard","scmSiteAlsoStopped":false,"targetSwapSlot":null,"hostingEnvironment":null,"hostingEnvironmentProfile":null,"clientAffinityEnabled":true,"clientAffinityProxyEnabled":false,"useQueryStringAffinity":false,"blockPathTraversal":false,"clientCertEnabled":false,"clientCertMode":"Required","clientCertExclusionPaths":null,"clientCertExclusionEndPoints":null,"hostNamesDisabled":false,"ipMode":"IPv4","domainVerificationIdentifiers":null,"customDomainVerificationId":"06A754DDDA9E82CEAB4064B1FFBB341F2D951D8E36BC157906AEE1799EE3B407","kind":"app","managedEnvironmentId":null,"workloadProfileName":null,"resourceConfig":null,"inboundIpAddress":"13.69.68.36,104.45.14.249","possibleInboundIpAddresses":"13.69.68.36,104.45.14.249,13.69.68.36","inboundIpv6Address":"2603:1020:206:7::4a","possibleInboundIpv6Addresses":"2603:1020:206:7::4a","ftpUsername":"web-ssl-test000003\\$web-ssl-test000003","ftpsHostName":"ftps://waws-prod-am2-019.ftp.azurewebsites.windows.net/site/wwwroot","outboundIpAddresses":"104.45.14.250,104.45.14.251,104.45.14.252,104.45.14.253,13.69.68.36,104.45.14.249","possibleOutboundIpAddresses":"104.45.14.250,104.45.14.251,104.45.14.252,104.45.14.253,108.142.104.60,13.80.0.253,13.80.6.186,40.118.12.139,13.80.6.154,13.80.6.191,20.101.207.109,20.23.64.188,20.23.67.119,20.23.67.191,20.23.68.207,20.23.69.216,108.141.244.55,108.141.244.75,108.141.244.80,108.141.244.85,108.141.244.124,108.141.244.167,13.69.68.36,104.45.14.249","outboundIpv6Addresses":"2603:1020:203:f::3e3,2603:1020:203:10::39d,2603:1020:203:10::3a0,2603:1020:203:1e::361,2603:1020:206:7::4a,2603:10e1:100:2::682d:ef9","possibleOutboundIpv6Addresses":"2603:1020:203:f::3e3,2603:1020:203:10::39d,2603:1020:203:10::3a0,2603:1020:203:1e::361,2603:1020:203:17::3ea,2603:1020:203:a::405,2603:1020:203:11::3b1,2603:1020:203:f::3e6,2603:1020:203:6::3fa,2603:1020:203:f::3e8,2603:1020:203:5::3ca,2603:1020:203:1f::3cc,2603:1020:203:a::409,2603:1020:203:12::3c5,2603:1020:203:d::3d4,2603:1020:203:b::3e9,2603:1020:203:5::3cc,2603:1020:203:1a::3b5,2603:1020:203:15::3d0,2603:1020:203:7::105,2603:1020:203:e::3de,2603:1020:206:7::4a,2603:10e1:100:2::682d:ef9","containerSize":0,"dailyMemoryTimeQuota":0,"suspendedTill":null,"siteDisabledReason":0,"functionExecutionUnitsCache":null,"maxNumberOfWorkers":null,"homeStamp":"waws-prod-am2-019","cloningInfo":null,"hostingEnvironmentId":null,"tags":{"web":"web1"},"resourceGroup":"clitest.rg000001","defaultHostName":"web-ssl-test000003.azurewebsites.net","slotSwapStatus":null,"httpsOnly":true,"endToEndEncryptionEnabled":false,"functionsRuntimeAdminIsolationEnabled":false,"redundancyMode":"None","inProgressOperationId":null,"geoDistributions":null,"privateEndpointConnections":[],"publicNetworkAccess":"Enabled","buildVersion":null,"targetBuildVersion":null,"migrationState":null,"eligibleLogCategories":"AppServiceAppLogs,AppServiceAuditLogs,AppServiceConsoleLogs,AppServiceHTTPLogs,AppServiceIPSecAuditLogs,AppServicePlatformLogs,ScanLogs,AppServiceAuthenticationLogs","inFlightFeatures":["SiteContainers","RouteGeoCapacityClientTrafficToV2","RouteOperationClientTrafficToV2","RouteGeoPlanClientTrafficToV2","RouteGeoSourceControlKeyClientTrafficToV2","RouteGeoUserClientTrafficToV2"],"storageAccountRequired":false,"virtualNetworkSubnetId":null,"keyVaultReferenceIdentity":"SystemAssigned","autoGeneratedDomainNameLabelScope":null,"privateLinkIdentifiers":null,"sshEnabled":null}}' + string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.Web/sites/web-ssl-test000003/slots/slot-ssl-test000004","name":"web-ssl-test000003/slot-ssl-test000004","type":"Microsoft.Web/sites/slots","kind":"app","location":"West + Europe","properties":{"name":"web-ssl-test000003(slot-ssl-test000004)","state":"Running","hostNames":["web-ssl-test000003-slot-ssl-test000004.azurewebsites.net"],"webSpace":"clitest.rg000001-WestEuropewebspace","selfLink":"https://waws-prod-am2-019.api.azurewebsites.windows.net:454/subscriptions/00000000-0000-0000-0000-000000000000/webspaces/clitest.rg000001-WestEuropewebspace/sites/web-ssl-test000003","repositorySiteName":"web-ssl-test000003","owner":null,"usageState":"Normal","enabled":true,"adminEnabled":true,"siteScopedCertificatesEnabled":false,"afdEnabled":false,"enabledHostNames":["web-ssl-test000003-slot-ssl-test000004.azurewebsites.net","web-ssl-test000003-slot-ssl-test000004.scm.azurewebsites.net"],"siteProperties":{"metadata":null,"properties":[{"name":"LinuxFxVersion","value":""},{"name":"WindowsFxVersion","value":null}],"appSettings":null},"availabilityState":"Normal","sslCertificates":null,"csrs":[],"cers":null,"siteMode":null,"hostNameSslStates":[{"name":"web-ssl-test000003-slot-ssl-test000004.azurewebsites.net","sslState":"Disabled","ipBasedSslResult":null,"virtualIP":null,"virtualIPv6":null,"thumbprint":null,"certificateResourceId":null,"toUpdate":null,"toUpdateIpBasedSsl":null,"ipBasedSslState":"NotConfigured","hostType":"Standard"},{"name":"web-ssl-test000003-slot-ssl-test000004.scm.azurewebsites.net","sslState":"Disabled","ipBasedSslResult":null,"virtualIP":null,"virtualIPv6":null,"thumbprint":null,"certificateResourceId":null,"toUpdate":null,"toUpdateIpBasedSsl":null,"ipBasedSslState":"NotConfigured","hostType":"Repository"}],"computeMode":null,"serverFarm":null,"serverFarmId":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.Web/serverfarms/ssl-test-plan000002","reserved":false,"isXenon":false,"hyperV":false,"sandboxType":null,"lastModifiedTimeUtc":"2025-10-30T22:13:55.393","storageRecoveryDefaultState":"Running","contentAvailabilityState":"Normal","runtimeAvailabilityState":"Normal","dnsConfiguration":{},"containerAllocationSubnet":null,"useContainerLocalhostBindings":null,"outboundVnetRouting":{"allTraffic":false,"applicationTraffic":false,"contentShareTraffic":false,"imagePullTraffic":false,"backupRestoreTraffic":false,"managedIdentityTraffic":false},"legacyServiceEndpointTrafficEvaluation":null,"siteConfig":{"numberOfWorkers":1,"defaultDocuments":null,"netFrameworkVersion":null,"phpVersion":null,"pythonVersion":null,"nodeVersion":null,"powerShellVersion":null,"linuxFxVersion":"","windowsFxVersion":null,"sandboxType":null,"windowsConfiguredStacks":null,"requestTracingEnabled":null,"remoteDebuggingEnabled":null,"remoteDebuggingVersion":null,"httpLoggingEnabled":null,"azureMonitorLogCategories":null,"acrUseManagedIdentityCreds":false,"acrUserManagedIdentityID":null,"logsDirectorySizeLimit":null,"detailedErrorLoggingEnabled":null,"publishingUsername":null,"publishingPassword":null,"appSettings":null,"metadata":null,"connectionStrings":null,"machineKey":null,"handlerMappings":null,"documentRoot":null,"scmType":null,"use32BitWorkerProcess":null,"webSocketsEnabled":null,"alwaysOn":false,"javaVersion":null,"javaContainer":null,"javaContainerVersion":null,"appCommandLine":null,"managedPipelineMode":null,"virtualApplications":null,"winAuthAdminState":null,"winAuthTenantState":null,"customAppPoolIdentityAdminState":null,"customAppPoolIdentityTenantState":null,"runtimeADUser":null,"runtimeADUserPassword":null,"loadBalancing":null,"routingRules":null,"experiments":null,"limits":null,"autoHealEnabled":null,"autoHealRules":null,"tracingOptions":null,"vnetName":null,"vnetRouteAllEnabled":null,"vnetPrivatePortsCount":null,"publicNetworkAccess":null,"cors":null,"push":null,"apiDefinition":null,"apiManagementConfig":null,"autoSwapSlotName":null,"localMySqlEnabled":null,"managedServiceIdentityId":null,"xManagedServiceIdentityId":null,"keyVaultReferenceIdentity":null,"ipSecurityRestrictions":null,"ipSecurityRestrictionsDefaultAction":null,"scmIpSecurityRestrictions":null,"scmIpSecurityRestrictionsDefaultAction":null,"scmIpSecurityRestrictionsUseMain":null,"http20Enabled":true,"minTlsVersion":null,"minTlsCipherSuite":null,"scmMinTlsCipherSuite":null,"supportedTlsCipherSuites":null,"scmSupportedTlsCipherSuites":null,"scmMinTlsVersion":null,"ftpsState":null,"preWarmedInstanceCount":null,"functionAppScaleLimit":0,"elasticWebAppScaleLimit":null,"healthCheckPath":null,"fileChangeAuditEnabled":null,"functionsRuntimeScaleMonitoringEnabled":null,"websiteTimeZone":null,"minimumElasticInstanceCount":0,"azureStorageAccounts":null,"http20ProxyFlag":null,"sitePort":null,"antivirusScanEnabled":null,"storageType":null,"sitePrivateLinkHostEnabled":null,"clusteringEnabled":false,"webJobsEnabled":false},"functionAppConfig":null,"daprConfig":null,"deploymentId":"web-ssl-test000003__b662","slotName":null,"trafficManagerHostNames":null,"sku":"Standard","scmSiteAlsoStopped":false,"targetSwapSlot":null,"hostingEnvironment":null,"hostingEnvironmentProfile":null,"clientAffinityEnabled":true,"clientAffinityProxyEnabled":false,"useQueryStringAffinity":false,"blockPathTraversal":false,"clientCertEnabled":false,"clientCertMode":"Required","clientCertExclusionPaths":null,"clientCertExclusionEndPoints":null,"hostNamesDisabled":false,"ipMode":"IPv4","domainVerificationIdentifiers":null,"customDomainVerificationId":"06A754DDDA9E82CEAB4064B1FFBB341F2D951D8E36BC157906AEE1799EE3B407","kind":"app","managedEnvironmentId":null,"workloadProfileName":null,"resourceConfig":null,"inboundIpAddress":"13.69.68.36,104.45.14.249","possibleInboundIpAddresses":"13.69.68.36,104.45.14.249,13.69.68.36","inboundIpv6Address":"2603:1020:206:7::4a","possibleInboundIpv6Addresses":"2603:1020:206:7::4a","ftpUsername":"web-ssl-test000003__slot-ssl-test000004\\$web-ssl-test000003__slot-ssl-test000004","ftpsHostName":"ftps://waws-prod-am2-019.ftp.azurewebsites.windows.net/site/wwwroot","outboundIpAddresses":"104.45.14.250,104.45.14.251,104.45.14.252,104.45.14.253,13.69.68.36,104.45.14.249","possibleOutboundIpAddresses":"104.45.14.250,104.45.14.251,104.45.14.252,104.45.14.253,108.142.104.60,13.80.0.253,13.80.6.186,40.118.12.139,13.80.6.154,13.80.6.191,20.101.207.109,20.23.64.188,20.23.67.119,20.23.67.191,20.23.68.207,20.23.69.216,108.141.244.55,108.141.244.75,108.141.244.80,108.141.244.85,108.141.244.124,108.141.244.167,13.69.68.36,104.45.14.249","outboundIpv6Addresses":"2603:1020:203:f::3e3,2603:1020:203:10::39d,2603:1020:203:10::3a0,2603:1020:203:1e::361,2603:1020:206:7::4a,2603:10e1:100:2::682d:ef9","possibleOutboundIpv6Addresses":"2603:1020:203:f::3e3,2603:1020:203:10::39d,2603:1020:203:10::3a0,2603:1020:203:1e::361,2603:1020:203:17::3ea,2603:1020:203:a::405,2603:1020:203:11::3b1,2603:1020:203:f::3e6,2603:1020:203:6::3fa,2603:1020:203:f::3e8,2603:1020:203:5::3ca,2603:1020:203:1f::3cc,2603:1020:203:a::409,2603:1020:203:12::3c5,2603:1020:203:d::3d4,2603:1020:203:b::3e9,2603:1020:203:5::3cc,2603:1020:203:1a::3b5,2603:1020:203:15::3d0,2603:1020:203:7::105,2603:1020:203:e::3de,2603:1020:206:7::4a,2603:10e1:100:2::682d:ef9","containerSize":0,"dailyMemoryTimeQuota":0,"suspendedTill":null,"siteDisabledReason":0,"functionExecutionUnitsCache":null,"maxNumberOfWorkers":null,"homeStamp":"waws-prod-am2-019","cloningInfo":null,"hostingEnvironmentId":null,"tags":null,"resourceGroup":"clitest.rg000001","defaultHostName":"web-ssl-test000003-slot-ssl-test000004.azurewebsites.net","slotSwapStatus":null,"httpsOnly":true,"endToEndEncryptionEnabled":false,"functionsRuntimeAdminIsolationEnabled":false,"redundancyMode":"None","inProgressOperationId":null,"geoDistributions":null,"privateEndpointConnections":[],"publicNetworkAccess":"Enabled","buildVersion":null,"targetBuildVersion":null,"migrationState":null,"eligibleLogCategories":"AppServiceAppLogs,AppServiceAuditLogs,AppServiceConsoleLogs,AppServiceHTTPLogs,AppServiceIPSecAuditLogs,AppServicePlatformLogs,ScanLogs,AppServiceAuthenticationLogs","inFlightFeatures":["SiteContainers","RouteGeoCapacityClientTrafficToV2","RouteOperationClientTrafficToV2","RouteGeoPlanClientTrafficToV2","RouteGeoSourceControlKeyClientTrafficToV2","RouteGeoUserClientTrafficToV2"],"storageAccountRequired":false,"virtualNetworkSubnetId":null,"keyVaultReferenceIdentity":"SystemAssigned","autoGeneratedDomainNameLabelScope":null,"privateLinkIdentifiers":null,"sshEnabled":null}}' headers: cache-control: - no-cache content-length: - - '8403' + - '8613' content-type: - application/json date: - - Thu, 30 Oct 2025 22:14:15 GMT + - Thu, 30 Oct 2025 22:14:00 GMT etag: - - '"1DC49EA654A8630"' + - '"1DC49EA7B4A8B10"' expires: - '-1' pragma: @@ -4213,7 +4213,7 @@ interactions: x-ms-ratelimit-remaining-subscription-global-reads: - '16499' x-msedge-ref: - - 'Ref A: 00DC694BD9AB4B8A9F057BF9133D7A3C Ref B: SN4AA2022305035 Ref C: 2025-10-30T22:14:16Z' + - 'Ref A: 51E447BEEBEE49328ED874720B59E09F Ref B: SN4AA2022301029 Ref C: 2025-10-30T22:13:59Z' x-powered-by: - ASP.NET status: diff --git a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py index 4832c685135..dca5df75cd3 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py @@ -795,9 +795,9 @@ class _FakePagedIterator: """Simulates an Azure SDK paged iterator that yields items across multiple pages. Unlike a plain list or iter(), this class mimics the SDK behavior where - items are fetched page-by-page via continuation tokens. Calling list() - on the pager forces all pages to be consumed, which is the pattern used - throughout the appservice module. + items are fetched page-by-page via continuation tokens. The pages_fetched + property tracks how many pages have been consumed, allowing tests to verify + both full-consumption (list_ssl_certs) and early-break (delete, bind) behavior. """ def __init__(self, pages): @@ -867,6 +867,25 @@ def test_delete_ssl_cert_finds_cert_beyond_first_page(self, client_factory_mock) self.assertEqual(pager.pages_fetched, 2) client.certificates.delete.assert_called_once_with('myRG', 'cert99') + @mock.patch('azure.cli.command_modules.appservice.custom.web_client_factory', autospec=True) + def test_delete_ssl_cert_early_break_skips_remaining_pages(self, client_factory_mock): + """Ensure delete_ssl_cert stops iterating once the cert is found (lazy).""" + cmd_mock = _get_test_cmd() + + page1 = [_make_cert('cert0', 'TARGET')] + page2 = [_make_cert('cert1', 'OTHER')] + pager = _FakePagedIterator([page1, page2]) + + client = mock.MagicMock() + client_factory_mock.return_value = client + client.certificates.list_by_resource_group.return_value = pager + + delete_ssl_cert(cmd_mock, 'myRG', 'TARGET') + + # Only page 1 should be fetched — early break avoids page 2 + self.assertEqual(pager.pages_fetched, 1) + client.certificates.delete.assert_called_once_with('myRG', 'cert0') + @mock.patch('azure.cli.command_modules.appservice.custom.web_client_factory', autospec=True) def test_delete_ssl_cert_not_found_raises_error(self, client_factory_mock): """Ensure delete_ssl_cert raises ResourceNotFoundError for missing thumbprint."""