diff --git a/.changes/unreleased/optimization-20251223-155633.yaml b/.changes/unreleased/optimization-20251223-155633.yaml new file mode 100644 index 00000000..e9451447 --- /dev/null +++ b/.changes/unreleased/optimization-20251223-155633.yaml @@ -0,0 +1,3 @@ +kind: optimization +body: Improve the error message to clearly indicate when the MPE creator does not have sufficient Azure permissions on the resource. +time: 2025-12-23T15:56:33.427481409Z diff --git a/src/fabric_cli/commands/fs/mkdir/fab_fs_mkdir_managedprivateendpoint.py b/src/fabric_cli/commands/fs/mkdir/fab_fs_mkdir_managedprivateendpoint.py index 9c2534ad..cadcfe59 100644 --- a/src/fabric_cli/commands/fs/mkdir/fab_fs_mkdir_managedprivateendpoint.py +++ b/src/fabric_cli/commands/fs/mkdir/fab_fs_mkdir_managedprivateendpoint.py @@ -11,6 +11,7 @@ from fabric_cli.core import fab_constant from fabric_cli.core.fab_exceptions import FabricCLIError from fabric_cli.core.hiearchy.fab_hiearchy import VirtualItem +from fabric_cli.errors import ErrorMessages from fabric_cli.utils import fab_cmd_mkdir_utils as mkdir_utils from fabric_cli.utils import fab_mem_store as utils_mem_store from fabric_cli.utils import fab_ui as utils_ui @@ -86,16 +87,22 @@ def exec(managed_private_endpoint: VirtualItem, args: Namespace) -> None: # Wait exponentially time.sleep(2**iteration) iteration += 1 - except Exception: - state = "Failed" + except FabricCLIError as exc: + if ( + exc.status_code == fab_constant.ERROR_FORBIDDEN + or exc.message == ErrorMessages.Common.forbidden() + ): + state = "Pending" + else: + state = "Failed" break - if state != "Succeeded": + if state not in ["Succeeded", "Pending"]: raise FabricCLIError( f"Managed Private Endpoint was created on Fabric but encountered an issue on Azure provisioning. State: {state}", fab_constant.ERROR_OPERATION_FAILED, ) - result_message = f"'{managed_private_endpoint.name}' created" + result_message = f"'{managed_private_endpoint.name}' created. {'Private endpoint provisioning in Azure is pending approval' if state == 'Pending' else ''}" if params.get("autoapproveenabled", "false").lower() == "true": diff --git a/tests/test_commands/recordings/test_commands/test_mkdir/class_setup.yaml b/tests/test_commands/recordings/test_commands/test_mkdir/class_setup.yaml index 1ff92c9e..be90973d 100644 --- a/tests/test_commands/recordings/test_commands/test_mkdir/class_setup.yaml +++ b/tests/test_commands/recordings/test_commands/test_mkdir/class_setup.yaml @@ -11,7 +11,7 @@ interactions: Content-Type: - application/json User-Agent: - - ms-fabric-cli/1.2.0 (None; Linux; x86_64; 6.6.87.2-microsoft-standard-WSL2) + - ms-fabric-cli/1.3.1 (None; Linux; x86_64; 6.6.87.2-microsoft-standard-WSL2) method: GET uri: https://api.fabric.microsoft.com/v1/workspaces response: @@ -26,15 +26,15 @@ interactions: Content-Encoding: - gzip Content-Length: - - '1054' + - '1662' Content-Type: - application/json; charset=utf-8 Date: - - Wed, 10 Dec 2025 14:36:12 GMT + - Wed, 24 Dec 2025 11:16:19 GMT Pragma: - no-cache RequestId: - - 46a76ab9-a143-451d-90d0-f9fd657b1dbe + - e9e82624-60b4-40ae-8d9d-f0a28bd1b1b3 Strict-Transport-Security: - max-age=31536000; includeSubDomains X-Content-Type-Options: @@ -60,7 +60,7 @@ interactions: Content-Type: - application/json User-Agent: - - ms-fabric-cli/1.2.0 (None; Linux; x86_64; 6.6.87.2-microsoft-standard-WSL2) + - ms-fabric-cli/1.3.1 (None; Linux; x86_64; 6.6.87.2-microsoft-standard-WSL2) method: GET uri: https://api.fabric.microsoft.com/v1/workspaces response: @@ -75,15 +75,15 @@ interactions: Content-Encoding: - gzip Content-Length: - - '1054' + - '1662' Content-Type: - application/json; charset=utf-8 Date: - - Wed, 10 Dec 2025 14:36:12 GMT + - Wed, 24 Dec 2025 11:16:20 GMT Pragma: - no-cache RequestId: - - b659f6f2-2390-446e-ab99-3b090cc635ea + - 7d6ce758-a563-429c-9a19-79023b416099 Strict-Transport-Security: - max-age=31536000; includeSubDomains X-Content-Type-Options: @@ -109,7 +109,7 @@ interactions: Content-Type: - application/json User-Agent: - - ms-fabric-cli/1.2.0 (None; Linux; x86_64; 6.6.87.2-microsoft-standard-WSL2) + - ms-fabric-cli/1.3.1 (None; Linux; x86_64; 6.6.87.2-microsoft-standard-WSL2) method: GET uri: https://api.fabric.microsoft.com/v1/capacities response: @@ -125,15 +125,15 @@ interactions: Content-Encoding: - gzip Content-Length: - - '873' + - '874' Content-Type: - application/json; charset=utf-8 Date: - - Wed, 10 Dec 2025 14:36:16 GMT + - Wed, 24 Dec 2025 11:16:23 GMT Pragma: - no-cache RequestId: - - b23c547e-bc6b-4a83-9f1c-248239404869 + - 23ef28a2-85d7-45a7-a94a-0e19f62a4fde Strict-Transport-Security: - max-age=31536000; includeSubDomains X-Content-Type-Options: @@ -162,12 +162,12 @@ interactions: Content-Type: - application/json User-Agent: - - ms-fabric-cli/1.2.0 (None; Linux; x86_64; 6.6.87.2-microsoft-standard-WSL2) + - ms-fabric-cli/1.3.1 (None; Linux; x86_64; 6.6.87.2-microsoft-standard-WSL2) method: POST uri: https://api.fabric.microsoft.com/v1/workspaces response: body: - string: '{"id": "b14aea11-ff9f-40fd-a872-21636c090395", "displayName": "fabriccli_WorkspacePerTestclass_000001", + string: '{"id": "030399b4-bbf7-4216-b61d-6698ae4292b6", "displayName": "fabriccli_WorkspacePerTestclass_000001", "description": "Created by fab", "type": "Workspace", "capacityId": "00000000-0000-0000-0000-000000000004"}' headers: Access-Control-Expose-Headers: @@ -177,17 +177,17 @@ interactions: Content-Encoding: - gzip Content-Length: - - '187' + - '188' Content-Type: - application/json; charset=utf-8 Date: - - Wed, 10 Dec 2025 14:36:23 GMT + - Wed, 24 Dec 2025 11:16:32 GMT Location: - - https://api.fabric.microsoft.com/v1/workspaces/b14aea11-ff9f-40fd-a872-21636c090395 + - https://api.fabric.microsoft.com/v1/workspaces/030399b4-bbf7-4216-b61d-6698ae4292b6 Pragma: - no-cache RequestId: - - 97eb7f80-4e28-4201-ae75-5570c99ad930 + - 5610440d-2709-49e0-add4-a27a5418d396 Strict-Transport-Security: - max-age=31536000; includeSubDomains X-Content-Type-Options: @@ -213,13 +213,13 @@ interactions: Content-Type: - application/json User-Agent: - - ms-fabric-cli/1.2.0 (mkdir; Linux; x86_64; 6.6.87.2-microsoft-standard-WSL2) + - ms-fabric-cli/1.3.1 (mkdir; Linux; x86_64; 6.6.87.2-microsoft-standard-WSL2) method: GET uri: https://api.fabric.microsoft.com/v1/workspaces response: body: string: '{"value": [{"id": "3634a139-2c9e-4205-910b-3b089a31be47", "displayName": - "My workspace", "description": "", "type": "Personal"}, {"id": "b14aea11-ff9f-40fd-a872-21636c090395", + "My workspace", "description": "", "type": "Personal"}, {"id": "030399b4-bbf7-4216-b61d-6698ae4292b6", "displayName": "fabriccli_WorkspacePerTestclass_000001", "description": "Created by fab", "type": "Workspace", "capacityId": "00000000-0000-0000-0000-000000000004"}]}' headers: @@ -230,15 +230,15 @@ interactions: Content-Encoding: - gzip Content-Length: - - '1091' + - '1693' Content-Type: - application/json; charset=utf-8 Date: - - Wed, 10 Dec 2025 14:36:57 GMT + - Wed, 24 Dec 2025 11:20:26 GMT Pragma: - no-cache RequestId: - - 9d54b68b-5a87-439a-90ba-d343916423c8 + - 8dc7f29e-0e56-4649-a292-e761d9e72634 Strict-Transport-Security: - max-age=31536000; includeSubDomains X-Content-Type-Options: @@ -264,9 +264,9 @@ interactions: Content-Type: - application/json User-Agent: - - ms-fabric-cli/1.2.0 (mkdir; Linux; x86_64; 6.6.87.2-microsoft-standard-WSL2) + - ms-fabric-cli/1.3.1 (mkdir; Linux; x86_64; 6.6.87.2-microsoft-standard-WSL2) method: GET - uri: https://api.fabric.microsoft.com/v1/workspaces/b14aea11-ff9f-40fd-a872-21636c090395/items + uri: https://api.fabric.microsoft.com/v1/workspaces/030399b4-bbf7-4216-b61d-6698ae4292b6/items response: body: string: '{"value": []}' @@ -282,11 +282,11 @@ interactions: Content-Type: - application/json; charset=utf-8 Date: - - Wed, 10 Dec 2025 14:36:58 GMT + - Wed, 24 Dec 2025 11:20:31 GMT Pragma: - no-cache RequestId: - - 353f6d45-0cff-4127-a474-a9b1317790c4 + - cd256475-316e-4801-814c-e88852073bb3 Strict-Transport-Security: - max-age=31536000; includeSubDomains X-Content-Type-Options: @@ -314,9 +314,9 @@ interactions: Content-Type: - application/json User-Agent: - - ms-fabric-cli/1.2.0 (mkdir; Linux; x86_64; 6.6.87.2-microsoft-standard-WSL2) + - ms-fabric-cli/1.3.1 (mkdir; Linux; x86_64; 6.6.87.2-microsoft-standard-WSL2) method: DELETE - uri: https://api.fabric.microsoft.com/v1/workspaces/b14aea11-ff9f-40fd-a872-21636c090395 + uri: https://api.fabric.microsoft.com/v1/workspaces/030399b4-bbf7-4216-b61d-6698ae4292b6 response: body: string: '' @@ -332,11 +332,11 @@ interactions: Content-Type: - application/octet-stream Date: - - Wed, 10 Dec 2025 14:36:59 GMT + - Wed, 24 Dec 2025 11:20:36 GMT Pragma: - no-cache RequestId: - - 522420d0-d4cb-4d93-8a13-a5a9bdf30d41 + - b97b5b4b-c4a7-43c8-a192-fece84c53bac Strict-Transport-Security: - max-age=31536000; includeSubDomains X-Content-Type-Options: diff --git a/tests/test_commands/recordings/test_commands/test_mkdir/test_mkdir_managed_private_endpoint_forbidden_azure_access_pending_success.yaml b/tests/test_commands/recordings/test_commands/test_mkdir/test_mkdir_managed_private_endpoint_forbidden_azure_access_pending_success.yaml new file mode 100644 index 00000000..53cc52ca --- /dev/null +++ b/tests/test_commands/recordings/test_commands/test_mkdir/test_mkdir_managed_private_endpoint_forbidden_azure_access_pending_success.yaml @@ -0,0 +1,409 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Type: + - application/json + User-Agent: + - ms-fabric-cli-test/1.3.1 + method: GET + uri: https://api.fabric.microsoft.com/v1/workspaces + response: + body: + string: '{"value": [{"id": "3634a139-2c9e-4205-910b-3b089a31be47", "displayName": + "My workspace", "description": "", "type": "Personal"}, {"id": "030399b4-bbf7-4216-b61d-6698ae4292b6", + "displayName": "fabriccli_WorkspacePerTestclass_000001", "description": "Created + by fab", "type": "Workspace", "capacityId": "00000000-0000-0000-0000-000000000004"}]}' + headers: + Access-Control-Expose-Headers: + - RequestId + Cache-Control: + - no-store, must-revalidate, no-cache + Content-Encoding: + - gzip + Content-Length: + - '1693' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 24 Dec 2025 11:16:32 GMT + Pragma: + - no-cache + RequestId: + - 53972d68-e3cd-478d-a495-39cc8cb233be + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + home-cluster-uri: + - https://wabi-us-central-b-primary-redirect.analysis.windows.net/ + request-redirected: + - 'true' + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Type: + - application/json + User-Agent: + - ms-fabric-cli-test/1.3.1 + method: GET + uri: https://api.fabric.microsoft.com/v1/workspaces/030399b4-bbf7-4216-b61d-6698ae4292b6/managedPrivateEndpoints + response: + body: + string: '{"value": []}' + headers: + Access-Control-Expose-Headers: + - RequestId + Cache-Control: + - no-store, must-revalidate, no-cache + Content-Encoding: + - gzip + Content-Length: + - '32' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 24 Dec 2025 11:16:33 GMT + Pragma: + - no-cache + RequestId: + - 18a7d9e0-c99b-44f6-b1eb-33b3d499cc11 + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + home-cluster-uri: + - https://wabi-us-central-b-primary-redirect.analysis.windows.net/ + request-redirected: + - 'true' + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Type: + - application/json + User-Agent: + - ms-fabric-cli-test/1.3.1 + method: GET + uri: https://api.fabric.microsoft.com/v1/workspaces/030399b4-bbf7-4216-b61d-6698ae4292b6/managedPrivateEndpoints + response: + body: + string: '{"value": []}' + headers: + Access-Control-Expose-Headers: + - RequestId + Cache-Control: + - no-store, must-revalidate, no-cache + Content-Encoding: + - gzip + Content-Length: + - '32' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 24 Dec 2025 11:16:33 GMT + Pragma: + - no-cache + RequestId: + - 10a38d5a-004c-4cdc-8017-a63309027298 + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + home-cluster-uri: + - https://wabi-us-central-b-primary-redirect.analysis.windows.net/ + request-redirected: + - 'true' + status: + code: 200 + message: OK +- request: + body: '{"name": "fabcli000001", "targetPrivateLinkResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/mocked_fabriccli_resource_group/providers/Microsoft.Sql/servers/mocked_sql_server_server", + "targetSubresourceType": "sqlServer", "requestMessage": "Created by fab"}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '268' + Content-Type: + - application/json + User-Agent: + - ms-fabric-cli-test/1.3.1 + method: POST + uri: https://api.fabric.microsoft.com/v1/workspaces/030399b4-bbf7-4216-b61d-6698ae4292b6/managedPrivateEndpoints + response: + body: + string: '{"id": "02ac2e30-0261-44b4-8ef2-89faf39e0cd0", "provisioningState": + "Provisioning", "connectionState": {"status": null, "description": null, "actionsRequired": + null}, "name": "fabcli000001", "targetPrivateLinkResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/mocked_fabriccli_resource_group/providers/Microsoft.Sql/servers/mocked_sql_server_server", + "targetSubresourceType": "sqlServer"}' + headers: + Access-Control-Expose-Headers: + - RequestId,Location + Cache-Control: + - no-store, must-revalidate, no-cache + Content-Encoding: + - gzip + Content-Length: + - '270' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 24 Dec 2025 11:16:34 GMT + Location: + - https://api.fabric.microsoft.com/v1/workspaces/030399b4-bbf7-4216-b61d-6698ae4292b6/managedPrivateEndpoints/02ac2e30-0261-44b4-8ef2-89faf39e0cd0 + Pragma: + - no-cache + RequestId: + - 3e4d6c51-b9f9-4fe7-b113-392ba108038a + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + home-cluster-uri: + - https://wabi-us-central-b-primary-redirect.analysis.windows.net/ + request-redirected: + - 'true' + status: + code: 201 + message: Created +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Type: + - application/json + User-Agent: + - ms-fabric-cli-test/1.3.1 + method: GET + uri: https://api.fabric.microsoft.com/v1/workspaces/030399b4-bbf7-4216-b61d-6698ae4292b6/managedPrivateEndpoints/02ac2e30-0261-44b4-8ef2-89faf39e0cd0 + response: + body: + string: '{"id": "02ac2e30-0261-44b4-8ef2-89faf39e0cd0", "provisioningState": + "Provisioning", "connectionState": {"status": null, "description": null, "actionsRequired": + null}, "name": "fabcli000001", "targetPrivateLinkResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/mocked_fabriccli_resource_group/providers/Microsoft.Sql/servers/mocked_sql_server_server", + "targetSubresourceType": "sqlServer"}' + headers: + Access-Control-Expose-Headers: + - RequestId + Cache-Control: + - no-store, must-revalidate, no-cache + Content-Encoding: + - gzip + Content-Length: + - '270' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 24 Dec 2025 11:16:36 GMT + Pragma: + - no-cache + RequestId: + - ceb20a95-9440-4c73-b4c1-adb3c098ffd8 + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + home-cluster-uri: + - https://wabi-us-central-b-primary-redirect.analysis.windows.net/ + request-redirected: + - 'true' + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Type: + - application/json + User-Agent: + - ms-fabric-cli-test/1.3.1 + method: GET + uri: https://api.fabric.microsoft.com/v1/workspaces + response: + body: + string: '{"value": [{"id": "3634a139-2c9e-4205-910b-3b089a31be47", "displayName": + "My workspace", "description": "", "type": "Personal"}, {"id": "030399b4-bbf7-4216-b61d-6698ae4292b6", + "displayName": "fabriccli_WorkspacePerTestclass_000001", "description": "Created + by fab", "type": "Workspace", "capacityId": "00000000-0000-0000-0000-000000000004"}]}' + headers: + Access-Control-Expose-Headers: + - RequestId + Cache-Control: + - no-store, must-revalidate, no-cache + Content-Encoding: + - gzip + Content-Length: + - '1693' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 24 Dec 2025 11:18:39 GMT + Pragma: + - no-cache + RequestId: + - 8437660a-78c1-4f30-a904-b6132545bf55 + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + home-cluster-uri: + - https://wabi-us-central-b-primary-redirect.analysis.windows.net/ + request-redirected: + - 'true' + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Type: + - application/json + User-Agent: + - ms-fabric-cli-test/1.3.1 + method: GET + uri: https://api.fabric.microsoft.com/v1/workspaces/030399b4-bbf7-4216-b61d-6698ae4292b6/managedPrivateEndpoints + response: + body: + string: '{"value": [{"id": "02ac2e30-0261-44b4-8ef2-89faf39e0cd0", "provisioningState": + "Succeeded", "connectionState": {"status": "Pending", "description": "Created + by fab", "actionsRequired": "None"}, "name": "fabcli000001", "targetPrivateLinkResourceId": + "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/mocked_fabriccli_resource_group/providers/Microsoft.Sql/servers/mocked_sql_server_server", + "targetSubresourceType": "sqlServer"}]}' + headers: + Access-Control-Expose-Headers: + - RequestId + Cache-Control: + - no-store, must-revalidate, no-cache + Content-Encoding: + - gzip + Content-Length: + - '300' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 24 Dec 2025 11:18:39 GMT + Pragma: + - no-cache + RequestId: + - 4b2c8cde-6597-4763-a06c-9385806c31ce + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + home-cluster-uri: + - https://wabi-us-central-b-primary-redirect.analysis.windows.net/ + request-redirected: + - 'true' + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '0' + Content-Type: + - application/json + User-Agent: + - ms-fabric-cli-test/1.3.1 + method: DELETE + uri: https://api.fabric.microsoft.com/v1/workspaces/030399b4-bbf7-4216-b61d-6698ae4292b6/managedPrivateEndpoints/02ac2e30-0261-44b4-8ef2-89faf39e0cd0 + response: + body: + string: '' + headers: + Access-Control-Expose-Headers: + - RequestId + Cache-Control: + - no-store, must-revalidate, no-cache + Content-Encoding: + - gzip + Content-Length: + - '0' + Content-Type: + - application/octet-stream + Date: + - Wed, 24 Dec 2025 11:20:14 GMT + Pragma: + - no-cache + RequestId: + - 0743ac6c-dca1-476d-8eca-ef0464977416 + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + home-cluster-uri: + - https://wabi-us-central-b-primary-redirect.analysis.windows.net/ + request-redirected: + - 'true' + status: + code: 200 + message: OK +version: 1 diff --git a/tests/test_commands/test_mkdir.py b/tests/test_commands/test_mkdir.py index 3a4a1fe2..e7f1de8e 100644 --- a/tests/test_commands/test_mkdir.py +++ b/tests/test_commands/test_mkdir.py @@ -21,6 +21,7 @@ ) from fabric_cli.core import fab_constant as constant from fabric_cli.core import fab_handle_context as handle_context +from fabric_cli.core.fab_exceptions import FabricCLIError from fabric_cli.core.fab_types import ( ItemType, VICMap, @@ -1169,6 +1170,60 @@ def test_mkdir_managed_private_endpoint_without_params_fail( upsert_managed_private_endpoint_to_cache.assert_not_called() spy_create_managed_private_endpoint.assert_not_called() + def test_mkdir_managed_private_endpoint_forbidden_azure_access_pending_success( + self, + workspace, + cli_executor, + mock_print_done, + vcr_instance, + cassette_name, + upsert_managed_private_endpoint_to_cache, + spy_create_managed_private_endpoint, + test_data: StaticTestData, + ): + """Test that when Azure access is forbidden during find_mpe_connection, the state is set to Pending.""" + # Setup + managed_private_endpoint_display_name = generate_random_string( + vcr_instance, cassette_name + ) + type = VirtualItemContainerType.MANAGED_PRIVATE_ENDPOINT + managed_private_endpoint_full_path = cli_path_join( + workspace.full_path, + str(type), + f"{managed_private_endpoint_display_name}.{str(VICMap[type])}", + ) + subscription_id = test_data.azure_subscription_id + resource_group = test_data.azure_resource_group + sql_server = test_data.sql_server.server + + with patch( + "fabric_cli.utils.fab_cmd_mkdir_utils.find_mpe_connection" + ) as mock_find_mpe: + mock_find_mpe.side_effect = FabricCLIError( + ErrorMessages.Common.forbidden(), + constant.ERROR_FORBIDDEN, + ) + + # Execute command + cli_executor.exec_command( + f"mkdir {managed_private_endpoint_full_path} -P targetPrivateLinkResourceId=/subscriptions/{subscription_id}/resourceGroups/{resource_group}/providers/Microsoft.Sql/servers/{sql_server},targetSubresourceType=sqlServer" + ) + + # Assert + spy_create_managed_private_endpoint.assert_called_once() + mock_print_done.assert_called_once() + upsert_managed_private_endpoint_to_cache.assert_called_once() + assert ( + f"'{managed_private_endpoint_display_name}.{str(VICMap[type])}' created. Private endpoint provisioning in Azure is pending approval\n" + == mock_print_done.call_args[0][0] + ) + + # Verify that find_mpe_connection was called and threw the forbidden exception + mock_find_mpe.assert_called_once() + + # Cleanup + rm(managed_private_endpoint_full_path) + # endregion # region ExternalDataShare @@ -1316,10 +1371,13 @@ def test_mkdir_connection_with_onpremises_gateway_params_success( # Assert mock_print_done.assert_called() assert mock_print_done.call_count == 1 - assert f"'{connection_display_name}.Connection' created\n" == mock_print_done.call_args[0][0] + assert ( + f"'{connection_display_name}.Connection' created\n" + == mock_print_done.call_args[0][0] + ) mock_print_done.reset_mock() - + # Cleanup rm(connection_full_path) @@ -1339,13 +1397,16 @@ def test_mkdir_connection_with_onpremises_gateway_params_ignore_params_success( ) cli_executor.exec_command( - f"mkdir {connection_full_path} -P gatewayId={test_data.onpremises_gateway_details.id},connectionDetails.type=SQL,connectivityType=OnPremisesGateway,connectionDetails.parameters.server={test_data.sql_server.server}.database.windows.net,connectionDetails.parameters.database={test_data.sql_server.database},credentialDetails.type=Basic,credentialDetails.values='[{{\"gatewayId\":\"{test_data.onpremises_gateway_details.id}\",\"encryptedCredentials\":\"{test_data.onpremises_gateway_details.encrypted_credentials}\",\"ignoreParameters\":\"ignoreParameters\"}}]'" + f'mkdir {connection_full_path} -P gatewayId={test_data.onpremises_gateway_details.id},connectionDetails.type=SQL,connectivityType=OnPremisesGateway,connectionDetails.parameters.server={test_data.sql_server.server}.database.windows.net,connectionDetails.parameters.database={test_data.sql_server.database},credentialDetails.type=Basic,credentialDetails.values=\'[{{"gatewayId":"{test_data.onpremises_gateway_details.id}","encryptedCredentials":"{test_data.onpremises_gateway_details.encrypted_credentials}","ignoreParameters":"ignoreParameters"}}]\'' ) # Assert mock_print_done.assert_called() assert mock_print_done.call_count == 1 - assert f"'{connection_display_name}.Connection' created\n" == mock_print_done.call_args[0][0] + assert ( + f"'{connection_display_name}.Connection' created\n" + == mock_print_done.call_args[0][0] + ) mock_print_warning.assert_called() assert mock_print_warning.call_count == 1 @@ -1354,7 +1415,6 @@ def test_mkdir_connection_with_onpremises_gateway_params_ignore_params_success( # Cleanup rm(connection_full_path) - def test_mkdir_connection_with_onpremises_gateway_params_failure( self, cli_executor, @@ -1397,7 +1457,7 @@ def test_mkdir_connection_with_onpremises_gateway_params_failure( # Test 3: Execute command with missing encryptedCredentials params in one of the values cli_executor.exec_command( - f"mkdir {connection_full_path} -P gatewayId={test_data.onpremises_gateway_details.id},connectionDetails.type=SQL,connectivityType=OnPremisesGateway,connectionDetails.parameters.server={test_data.sql_server.server}.database.windows.net,connectionDetails.parameters.database={test_data.sql_server.database},credentialDetails.type=Basic,credentialDetails.values='[{{\"gatewayId\":\"{test_data.onpremises_gateway_details.id}\",\"encryptedCredentials\":\"{test_data.onpremises_gateway_details.encrypted_credentials}\"}},{{\"encryptedCredentials\":\"{test_data.onpremises_gateway_details.encrypted_credentials}\"}}]'" + f'mkdir {connection_full_path} -P gatewayId={test_data.onpremises_gateway_details.id},connectionDetails.type=SQL,connectivityType=OnPremisesGateway,connectionDetails.parameters.server={test_data.sql_server.server}.database.windows.net,connectionDetails.parameters.database={test_data.sql_server.database},credentialDetails.type=Basic,credentialDetails.values=\'[{{"gatewayId":"{test_data.onpremises_gateway_details.id}","encryptedCredentials":"{test_data.onpremises_gateway_details.encrypted_credentials}"}},{{"encryptedCredentials":"{test_data.onpremises_gateway_details.encrypted_credentials}"}}]\'' ) # Assert @@ -1757,38 +1817,36 @@ def test_mkdir_workspace_verify_stderr_stdout_messages_json_format_success( # endregion # region Folders - + def test_mkdir_item_in_folder_listing_success( self, workspace, cli_executor, mock_print_done, mock_questionary_print, mock_fab_set_state_config, vcr_instance, cassette_name ): # Enable folder listing mock_fab_set_state_config(constant.FAB_FOLDER_LISTING_ENABLED, "true") - # Setup folder_name = f"{generate_random_string(vcr_instance, cassette_name)}.Folder" folder_full_path = cli_path_join(workspace.full_path, folder_name) - + # Create folder cli_executor.exec_command(f"mkdir {folder_full_path}") mock_print_done.assert_called_once() mock_print_done.reset_mock() - + # Create notebook in folder notebook_name = f"{generate_random_string(vcr_instance, cassette_name)}.Notebook" notebook_full_path = cli_path_join(folder_full_path, notebook_name) cli_executor.exec_command(f"mkdir {notebook_full_path}") - + # Verify notebook appears in folder listing cli_executor.exec_command(f"ls {folder_full_path}") printed_output = mock_questionary_print.call_args[0][0] assert notebook_name in printed_output - + # Cleanup rm(notebook_full_path) rm(folder_full_path) - def test_mkdir_folder_success(self, workspace, cli_executor, mock_print_done): # Setup folder_display_name = "folder" @@ -1842,7 +1900,13 @@ def test_mkdir_folder_name_already_exists_failure( # region Batch Output Tests def test_mkdir_single_item_creation_batch_output_structure_success( - self, workspace, cli_executor, mock_print_done, mock_questionary_print, vcr_instance, cassette_name + self, + workspace, + cli_executor, + mock_print_done, + mock_questionary_print, + vcr_instance, + cassette_name, ): """Test that single item creation uses batched output structure.""" # Setup @@ -1864,13 +1928,13 @@ def test_mkdir_single_item_creation_batch_output_structure_success( # Look for the table output with headers output_calls = [str(call) for call in mock_questionary_print.mock_calls] table_output = "\n".join(output_calls) - + # Check for standard table headers assert "id" in table_output or "ID" in table_output assert "type" in table_output or "Type" in table_output assert "displayName" in table_output or "DisplayName" in table_output assert "workspaceId" in table_output or "WorkspaceId" in table_output - + # Check for actual values assert lakehouse_display_name in table_output assert "Lakehouse" in table_output @@ -1879,7 +1943,13 @@ def test_mkdir_single_item_creation_batch_output_structure_success( rm(lakehouse_full_path) def test_mkdir_dependency_creation_batched_output_kql_database_success( - self, workspace, cli_executor, mock_print_done, mock_questionary_print, vcr_instance, cassette_name + self, + workspace, + cli_executor, + mock_print_done, + mock_questionary_print, + vcr_instance, + cassette_name, ): """Test that KQL Database creation with EventHouse dependency produces batched output.""" # Setup @@ -1895,30 +1965,36 @@ def test_mkdir_dependency_creation_batched_output_kql_database_success( # The current implementation may still have separate calls, but data should be collected assert mock_print_done.call_count >= 1 - # Verify both items are mentioned in output all_calls = [call.args[0] for call in mock_print_done.call_args_list] all_output = " ".join(all_calls) - assert f"'{kqldatabase_display_name}_auto.{ItemType.EVENTHOUSE.value}' and '{kqldatabase_display_name}.{ItemType.KQL_DATABASE.value}' created" in all_output + assert ( + f"'{kqldatabase_display_name}_auto.{ItemType.EVENTHOUSE.value}' and '{kqldatabase_display_name}.{ItemType.KQL_DATABASE.value}' created" + in all_output + ) # Verify headers and values in mock_questionary_print.mock_calls for batched output output_calls = [str(call) for call in mock_questionary_print.mock_calls] table_output = "\n".join(output_calls) - + # Check for standard table headers (should appear once for consolidated table) assert "id" in table_output or "ID" in table_output assert "type" in table_output or "Type" in table_output assert "displayName" in table_output or "DisplayName" in table_output assert "workspaceId" in table_output or "WorkspaceId" in table_output - + # Check for both item values in the output assert kqldatabase_display_name in table_output - assert f"{kqldatabase_display_name}_auto" in table_output # EventHouse dependency name + assert ( + f"{kqldatabase_display_name}_auto" in table_output + ) # EventHouse dependency name assert "KQLDatabase" in table_output or "KQL_DATABASE" in table_output assert "Eventhouse" in table_output or "EVENTHOUSE" in table_output # Cleanup - removing parent eventhouse removes the kqldatabase as well - eventhouse_full_path = kqldatabase_full_path.removesuffix(".KQLDatabase") + "_auto.Eventhouse" + eventhouse_full_path = ( + kqldatabase_full_path.removesuffix(".KQLDatabase") + "_auto.Eventhouse" + ) rm(eventhouse_full_path) # endregion diff --git a/tests/test_utils/test_fab_cmd_mkdir_utils.py b/tests/test_utils/test_fab_cmd_mkdir_utils.py new file mode 100644 index 00000000..b0fa180a --- /dev/null +++ b/tests/test_utils/test_fab_cmd_mkdir_utils.py @@ -0,0 +1,77 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +from argparse import Namespace +from unittest.mock import Mock, patch + +import pytest + +from fabric_cli.core import fab_constant +from fabric_cli.core.fab_exceptions import FabricCLIError +from fabric_cli.errors import ErrorMessages +from fabric_cli.utils.fab_cmd_mkdir_utils import find_mpe_connection + + +class TestFindMpeConnection: + """Test cases for find_mpe_connection function.""" + + def test_find_mpe_connection_return_403_success(self): + """Test that find_mpe_connection handles 403 forbidden error correctly when session.request returns 403.""" + # Arrange + mock_managed_private_endpoint = Mock() + mock_managed_private_endpoint.workspace.id = "test-workspace-id" + mock_managed_private_endpoint.short_name = "test-mpe-name" + + target_resource_id = "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.Sql/servers/test-server" + + # Mock the session.request response to return 403 + mock_response = Mock() + mock_response.status_code = 403 + mock_response.text = '{"error": {"code": "Forbidden", "message": "Access denied"}}' + mock_response.headers = {} + + with patch('requests.Session.request') as mock_session_request, \ + patch('fabric_cli.client.fab_api_utils.get_api_version') as mock_get_api_version, \ + patch('fabric_cli.core.fab_auth.FabAuth') as mock_fab_auth_class, \ + patch('fabric_cli.core.fab_context.Context') as mock_fab_context_class: + + mock_session_request.return_value = mock_response + mock_get_api_version.return_value = "2023-11-01" + + # Mock FabAuth().get_access_token(scope) + mock_fab_auth_instance = Mock() + mock_fab_auth_instance.get_access_token.return_value = "mock-access-token" + mock_fab_auth_class.return_value = mock_fab_auth_instance + + # Mock FabContext().command + mock_fab_context_instance = Mock() + mock_fab_context_instance.command = "test-command" + mock_fab_context_class.return_value = mock_fab_context_instance + + # Act & Assert + with pytest.raises(FabricCLIError) as exc_info: + find_mpe_connection(mock_managed_private_endpoint, target_resource_id) + + # Verify the exception details - this tests that do_request properly handles 403 + assert exc_info.value.status_code == fab_constant.ERROR_FORBIDDEN + assert exc_info.value.message == ErrorMessages.Common.forbidden() + + # Verify session.request was called + mock_session_request.assert_called_once() + + # Verify get_api_version was called with the target resource ID + mock_get_api_version.assert_called_once_with(target_resource_id) + + # Verify authentication and context calls + mock_fab_auth_class.assert_called_once() + mock_fab_auth_instance.get_access_token.assert_called_once() + mock_fab_context_class.assert_called_once() + + # Verify the call was made with correct method and URL contains expected parts + call_args = mock_session_request.call_args + assert call_args[1]['method'] == 'get' or call_args.kwargs['method'] == 'get' + # The URL should contain the target resource ID and privateEndpointConnections + called_url = call_args.args[1] if len(call_args.args) > 1 else call_args.kwargs['url'] + assert "privateEndpointConnections" in called_url + assert "api-version=2023-11-01" in called_url + \ No newline at end of file