import ast import json from pathlib import Path from unittest.mock import MagicMock, patch import demistomock as demisto import pytest import yaml import Azure from Azure import ( AzureClient, format_rule, update_security_rule_command, storage_account_update_command, storage_blob_service_properties_set_command, create_policy_assignment_command, set_postgres_config_command, set_webapp_config_command, update_webapp_auth_command, mysql_flexible_server_param_set_command, monitor_log_profile_update_command, disk_update_command, webapp_update_command, acr_update_command, update_key_vault_command, sql_db_threat_policy_update_command, sql_db_tde_set_command, cosmosdb_update_command, remove_member_from_group_command, get_azure_client, remove_member_from_role, postgres_server_update_command, extract_azure_resource_info, WEBAPP_API_VERSION, FLEXIBLE_API_VERSION, CommandResults, DemistoException, API_VERSION, SCOPE_BY_CONNECTION, PREFIX_URL_AZURE, storage_container_property_get_command, storage_container_create_command, storage_container_delete_command, storage_container_blob_get_command, storage_container_blob_tag_get_command, storage_container_blob_property_get_command, storage_container_blob_tag_set_command, storage_container_blob_property_set_command, storage_container_block_public_access_command, DEFAULT_RESOURCE, DEFAULT_SCOPE, TokenScope, get_command_resource, STORAGE_RESOURCE, STORAGE_SCOPE, STORAGE_DATE_FORMAT, get_command_and_token_scopes, create_set_tags_request_body, nsg_security_rules_list_command, ) from MicrosoftApiModule import Resources from requests import Response from requests.structures import CaseInsensitiveDict from COOCApiModule import CloudTypes INTEGRATION_DIR = Path(__file__).parent YML_PATH = INTEGRATION_DIR / "Azure.yml" PY_PATH = INTEGRATION_DIR / "Azure.py" @pytest.fixture def mock_params(): return { "app_id": "test_app_id", "subscription_id": "test_subscription_id", "resource_group_name": "test_resource_group", "insecure": False, "proxy": False, "auth_type": "Client Credentials", "tenant_id": "test_tenant_id", "credentials": {"password": "test_enc_key"}, } @pytest.fixture def client(mocker, mock_params): mocker.patch("MicrosoftApiModule.MicrosoftClient.http_request") mocker.patch("MicrosoftApiModule.MicrosoftClient.get_access_token") return AzureClient( app_id=mock_params.get("app_id", ""), subscription_id=mock_params.get("subscription_id", ""), resource_group_name=mock_params.get("resource_group_name", ""), verify=not mock_params.get("insecure", False), proxy=mock_params.get("proxy", False), tenant_id=mock_params.get("tenant_id"), enc_key=mock_params.get("credentials", {}).get("password"), headers={}, ) def util_load_json(path): with open(path, encoding="utf-8") as f: return json.loads(f.read()) def test_update_security_rule_command(mocker, client, mock_params): """ Given: An Azure client and a request to update a security rule. When: The update_security_rule_command function is called with valid parameters. Then: The function should return the updated rule information in the expected format. """ # Prepare mock responses rule_response = { "name": "test-rule", "id": "/subscriptions/sub-id/resourceGroups/test-rg/providers/Microsoft.Network/networkSecurityGroups/test-sg/" "securityRules/test-rule", "properties": { "protocol": "Tcp", "sourcePortRange": "*", "destinationPortRange": "443", "sourceAddressPrefix": "Internet", "destinationAddressPrefix": "10.0.0.0/24", "access": "Allow", "priority": 100, "direction": "Inbound", "description": "Test rule", }, } mocker.patch.object(client, "get_rule", return_value=rule_response) mocker.patch.object(client, "create_or_update_rule", return_value=rule_response) mocker.patch("Azure.demisto.command", return_value="azure-vn-security-rule-update") # Call the function args = { "security_group_name": "test-sg", "security_rule_name": "test-rule", "action": "Allow", "direction": "Inbound", "protocol": "Tcp", "source": "Internet", "destination": "10.0.0.0/24", "destination_ports": "443", "priority": "100", "description": "Test rule", "access": "Allow", } result = update_security_rule_command(client, mock_params, args) # Verify results assert result.outputs_prefix == "Azure.VirtualNetworks.SecurityRules" assert result.outputs_key_field == "id" assert result.outputs["name"] == "test-rule" assert result.outputs.get("properties", {}).get("access") == "Allow" def test_update_security_rule_command_deprecated(mocker, client, mock_params): """ Given: An Azure client and a request to update a security rule using the deprecated command. When: The update_security_rule_command function is called with the deprecated command name. Then: The function should return the updated rule information with the deprecated prefix. """ # Prepare mock responses access = "Allow" rule_response = { "name": "test-rule", "id": "/subscriptions/sub-id/resourceGroups/test-rg/providers/Microsoft.Network/networkSecurityGroups/test-sg/" "securityRules/test-rule", "properties": { "protocol": "Tcp", "sourcePortRange": "*", "destinationPortRange": "443", "sourceAddressPrefix": "Internet", "destinationAddressPrefix": "10.0.0.0/24", "access": access, "priority": 100, "direction": "Inbound", "description": "Test rule", }, } mocker.patch.object(client, "get_rule", return_value=rule_response) mocker.patch.object(client, "create_or_update_rule", return_value=rule_response) mocker.patch("Azure.demisto.command", return_value="azure-nsg-security-rule-update") # Call the function args = { "security_group_name": "test-sg", "security_rule_name": "test-rule", "action": "Allow", "direction": "Inbound", "protocol": "Tcp", "source": "Internet", "destination": "10.0.0.0/24", "destination_ports": "443", "priority": "100", "description": "Test rule", "access": access, } result = update_security_rule_command(client, mock_params, args) # Verify results assert result.outputs_prefix == "Azure.NSGRule" assert result.outputs_key_field == "id" assert result.outputs["name"] == "test-rule" assert result.outputs.get("access") == access def test_storage_account_update_command(mocker, client, mock_params): """ Given: An Azure client and a request to update a storage account. When: The storage_account_update_command function is called with valid parameters. Then: The function should return the updated storage account information in the expected format. """ # Prepare mock response storage_response = { "name": "teststorage", "id": "/subscriptions/sub-id/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/teststorage", "properties": { "networkAcls": {"bypass": "AzureServices", "defaultAction": "Deny"}, "allowCrossTenantReplication": False, "supportsHttpsTrafficOnly": True, }, } # Create a mock response object without using MagicMock class MockResponse: def __init__(self, json_data, text): self._json_data = json_data self.text = text def json(self): return self._json_data mock_response = MockResponse(storage_response, json.dumps(storage_response)) mocker.patch.object(client, "storage_account_update_request", return_value=mock_response) # Call the function args = { "account_name": "teststorage", "sku": "Standard_LRS", "kind": "StorageV2", "location": "eastus", "network_ruleset_bypass": "AzureServices", "network_ruleset_default_action": "Deny", "allow_cross_tenant_replication": "false", "supports_https_traffic_only": "true", } result = storage_account_update_command(client, mock_params, args) # Verify results assert result.outputs_prefix == "Azure.Storage.StorageAccounts" assert result.outputs_key_field == "id" assert result.outputs["name"] == "teststorage" assert result.outputs["properties"]["supportsHttpsTrafficOnly"] is True def test_storage_blob_service_properties_set_command(mocker, client, mock_params): """ Given: An Azure client and a request to set blob service properties. When: The storage_blob_service_properties_set_command function is called with valid parameters. Then: The function should return the updated blob service properties in the expected format. """ # Prepare mock response properties_response = { "name": "default", "id": "/subscriptions/sub-id/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/teststorage/blobServices/\ default", "properties": {"deleteRetentionPolicy": {"enabled": True, "days": 7}}, } mocker.patch.object(client, "storage_blob_service_properties_set_request", return_value=properties_response) mocker.patch("Azure.demisto.command", return_value="azure-storage-blob-service-properties-set") # Call the function args = {"account_name": "teststorage", "delete_rentention_policy_enabled": "true", "delete_rentention_policy_days": "7"} result = storage_blob_service_properties_set_command(client, mock_params, args) # Verify results assert result.outputs_prefix == "Azure.StorageAccountBlobServiceProperties" assert result.outputs_key_field == "id" assert result.outputs["name"] == "default" assert result.outputs["properties"]["deleteRetentionPolicy"]["enabled"] is True assert result.outputs["properties"]["deleteRetentionPolicy"]["days"] == 7 def test_create_policy_assignment_command(mocker, client, mock_params): """ Given: An Azure client and a request to create a policy assignment. When: The create_policy_assignment_command function is called with valid parameters. Then: The function should return the created policy assignment in the expected format. """ # Prepare mock response policy_response = { "name": "test-policy", "id": "/subscriptions/sub-id/providers/Microsoft.Authorization/policyAssignments/test-policy", "properties": { "policyDefinitionId": "/providers/Microsoft.Authorization/policyDefinitions/policy-def-id", "displayName": "Test Policy", "description": "Test policy description", "parameters": {}, }, } mocker.patch.object(client, "create_policy_assignment", return_value=policy_response) # Call the function args = { "name": "test-policy", "scope": "sub-id", "policy_definition_id": "/providers/Microsoft.Authorization/policyDefinitions/policy-def-id", "display_name": "Test Policy", "description": "Test policy description", "parameters": "{}", } result = create_policy_assignment_command(client, mock_params, args) # Verify results assert result.outputs_prefix == "Azure.Policy.PolicyAssignments" assert result.outputs_key_field == "id" assert result.outputs["name"] == "test-policy" assert result.outputs["properties"]["displayName"] == "Test Policy" def test_set_postgres_config_command(mocker, client, mock_params): """ Given: An Azure client and a request to set PostgreSQL configurations. When: The set_postgres_config_command function is called with valid parameters. Then: The function should return a success message. """ # Prepare mock mocker.patch.object(client, "set_postgres_config", return_value=None) # Call the function args = {"server_name": "test-postgres", "configuration_name": "log_checkpoints", "source": "user-override", "value": "on"} result = set_postgres_config_command(client, mock_params, args) # Verify results assert "Updated the configuration log_checkpoints of the server test-postgres" in result.readable_output @pytest.mark.parametrize( "command, expected_prefix", [ ("azure-webapp-config-set", "Azure.WebAppConfig"), ("azure-webapp-set-http2-quick-action", "Azure.WebAppConfig"), ("azure-set-function-app-http-version2-0-quick-action", "Azure.WebAppConfig"), ("azure-webapp-disable-remote-debugging-quick-action", "Azure.WebAppConfig"), ("azure-webapp-set-min-tls-version-quick-action", "Azure.WebAppConfig"), ("azure-function-app-set-min-tls-version-quick-action", "Azure.WebAppConfig"), ("azure-appservice-webapp-config-update", "Azure.AppService.WebAppConfiguration"), ("azure-appservice-webapp-config-update-http2-quick-action", "Azure.AppService.WebAppConfiguration"), ("azure-appservice-webapp-config-disable-remote-debugging-quick-action", "Azure.AppService.WebAppConfiguration"), ("azure-appservice-webapp-config-update-min-tls-version-quick-action", "Azure.AppService.WebAppConfiguration"), ], ) def test_set_webapp_config_command(mocker, client, mock_params, command, expected_prefix): """ Given: An Azure client and a request to set WebApp configurations. When: The set_webapp_config_command function is called with valid parameters. Then: The function should return the updated WebApp configurations in the expected format. """ # Prepare mock response webapp_response = { "name": "test-webapp", "id": "/subscriptions/sub-id/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-webapp", "properties": {"http20Enabled": True, "remoteDebuggingEnabled": False, "minTlsVersion": "1.2"}, } mocker.patch.object(client, "set_webapp_config", return_value=webapp_response) mocker.patch.object(demisto, "command", return_value=command) # Call the function args = {"name": "test-webapp", "http20_enabled": "true", "remote_debugging_enabled": "false", "min_tls_version": "1.2"} result = set_webapp_config_command(client, mock_params, args) # Verify results assert result.outputs_prefix == expected_prefix assert result.outputs_key_field == "id" assert result.outputs["name"] == "test-webapp" assert result.outputs["properties"]["http20Enabled"] is True @pytest.mark.parametrize( "command, expected_prefix", [ ("azure-webapp-auth-update", "Azure.WebAppAuth"), ("azure-webapp-auth-update-quick-action", "Azure.WebAppAuth"), ("azure-appservice-webapp-auth-settings-update", "Azure.AppService.WebAppAuthSettings"), ("azure-appservice-webapp-auth-settings-update-quick-action", "Azure.AppService.WebAppAuthSettings"), ], ) def test_update_webapp_auth_command(mocker, client, mock_params, command, expected_prefix): """ Given: An Azure client and a request to update WebApp authentication settings. When: The update_webapp_auth_command function is called with valid parameters. Then: The function should return the updated WebApp authentication settings in the expected format. """ # Prepare mock responses current_auth = { "name": "authsettings", "id": "/subscriptions/sub-id/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-webapp/config/authsettings", "properties": {"enabled": False}, } updated_auth = { "name": "authsettings", "id": "/subscriptions/sub-id/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-webapp/config/authsettings", "properties": {"enabled": True}, } mocker.patch.object(client, "get_webapp_auth", return_value=current_auth) mocker.patch.object(client, "update_webapp_auth", return_value=updated_auth) mocker.patch.object(demisto, "command", return_value=command) # Call the function args = {"name": "test-webapp", "enabled": "true"} result = update_webapp_auth_command(client, mock_params, args) # Verify results assert result.outputs_prefix == expected_prefix assert result.outputs_key_field == "id" assert result.outputs["name"] == "authsettings" assert result.outputs["properties"]["enabled"] is True def test_mysql_flexible_server_param_set_command(mocker, client, mock_params): """ Given: An Azure client and a request to set MySQL flexible server parameters. When: The mysql_flexible_server_param_set_command function is called with valid parameters. Then: The function should return a success message. """ # Prepare mock mocker.patch.object(client, "flexible_server_param_set", return_value=None) # Call the function args = { "server_name": "test-mysql", "configuration_name": "require_secure_transport", "source": "user-override", "value": "ON", } result = mysql_flexible_server_param_set_command(client, mock_params, args) # Verify results assert "Updated the configuration require_secure_transport of the server test-mysql" in result.readable_output def test_monitor_log_profile_update_command(mocker, client, mock_params): """ Given: An Azure client and a request to update monitor log profile. When: The monitor_log_profile_update_command function is called with valid parameters. Then: The function should return the updated log profile in the expected format. """ # Prepare mock responses current_profile = { "name": "test-profile", "id": "/subscriptions/sub-id/providers/Microsoft.Insights/logprofiles/test-profile", "location": "eastus", "properties": {"retentionPolicy": {"enabled": False, "days": 0}}, } updated_profile = { "name": "test-profile", "id": "/subscriptions/sub-id/providers/Microsoft.Insights/logprofiles/test-profile", "location": "westus", "properties": {"retentionPolicy": {"enabled": True, "days": 30}}, } mocker.patch.object(client, "get_monitor_log_profile", return_value=current_profile) mocker.patch.object(client, "monitor_log_profile_update", return_value=updated_profile) # Call the function args = { "log_profile_name": "test-profile", "location": "westus", "subscription_id": "sub-id", "retention_policy_days": "30", "retention_policy_enabled": "true", } result = monitor_log_profile_update_command(client, mock_params, args) # Verify results assert result.outputs_prefix == "Azure.Monitor.LogProfiles" assert result.outputs_key_field == "id" assert result.outputs["name"] == "test-profile" assert result.outputs["location"] == "westus" assert result.outputs["properties"]["retentionPolicy"]["enabled"] is True assert result.outputs["properties"]["retentionPolicy"]["days"] == 30 @pytest.mark.parametrize( "command, expected_prefix", [ ("azure-disk-update", "Azure.Disk"), ("azure-disk-set-data-access-ad-quick-action", "Azure.Disk"), ("azure-disable-public-private-access-vm-disk-quick-action", "Azure.Disk"), ("azure-compute-disk-update", "Azure.Compute.Disks"), ("azure-disable-public-n-private-access-vm-disk-quick-action", "Azure.Compute.Disks"), ("azure-compute-disk-update-data-access-ad-quick-action", "Azure.Compute.Disks"), ], ) def test_disk_update_command(mocker, client, mock_params, command, expected_prefix): """ Given: An Azure client and a request to update disk properties. When: The disk_update_command function is called with valid parameters. Then: The function should return the updated disk properties in the expected format. """ # Prepare mock response disk_response = { "name": "test-disk", "id": "/subscriptions/sub-id/resourceGroups/test-rg/providers/Microsoft.Compute/disks/test-disk", "properties": { "publicNetworkAccess": "Disabled", "networkAccessPolicy": "DenyAll", "dataAccessAuthMode": "AzureActiveDirectory", }, } mocker.patch.object(client, "disk_update", return_value=disk_response) mocker.patch.object(demisto, "command", return_value=command) # Call the function args = { "disk_name": "test-disk", "public_network_access": "Disabled", "network_access_policy": "DenyAll", "data_access_auth_mode": "AzureActiveDirectory", } result = disk_update_command(client, mock_params, args) # Verify results assert result.outputs_prefix == expected_prefix assert result.outputs_key_field == "id" assert result.outputs["name"] == "test-disk" assert result.outputs["properties"]["publicNetworkAccess"] == "Disabled" assert result.outputs["properties"]["networkAccessPolicy"] == "DenyAll" assert result.outputs["properties"]["dataAccessAuthMode"] == "AzureActiveDirectory" @pytest.mark.parametrize( "command, expected_prefix", [ ("azure-webapp-update", "Azure.WebApp"), ("azure-webapp-assign-managed-identity-quick-action", "Azure.WebApp"), ("azure-webapp-update-assign-managed-identity-quick-action", "Azure.WebApp"), ("azure-appservice-webapp-update", "Azure.AppService.WebApp"), ("azure-appservice-webapp-update-quick-action", "Azure.AppService.WebApp"), ], ) def test_webapp_update_command(mocker, client, mock_params, command, expected_prefix): """ Given: An Azure client and a request to update webapp properties. When: The webapp_update_command function is called with valid parameters. Then: The function should return the updated webapp properties in the expected format. """ # Prepare mock response webapp_response = { "name": "test-webapp", "id": "/subscriptions/sub-id/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-webapp", "identity": {"type": "SystemAssigned"}, "properties": {"httpsOnly": True, "clientCertEnabled": True}, } mocker.patch.object(client, "webapp_update", return_value=webapp_response) mocker.patch.object(demisto, "command", return_value=command) # Call the function args = {"name": "test-webapp", "identity_type": "SystemAssigned", "https_only": "true", "client_cert_enabled": "true"} result = webapp_update_command(client, mock_params, args) # Verify results assert result.outputs_prefix == expected_prefix assert result.outputs_key_field == "id" assert result.outputs["name"] == "test-webapp" assert result.outputs["identity"]["type"] == "SystemAssigned" assert result.outputs["properties"]["httpsOnly"] is True assert result.outputs["properties"]["clientCertEnabled"] is True @pytest.mark.parametrize( "command, expected_prefix", [ ("azure-acr-update", "Azure.ACR"), ("azure-acr-disable-public-private-access-quick-action", "Azure.ACR"), ("azure-acr-disable-authentication-as-arm-quick-action", "Azure.ACR"), ("azure-acr-disable-anonymous-pull-quick-action", "Azure.ACR"), ("azure-cr-registry-update", "Azure.ContainerRegistry.Registries"), ("azure-cr-disable-public-private-access-quick-action", "Azure.ContainerRegistry.Registries"), ("azure-cr-disable-anonymous-pull-quick-action", "Azure.ContainerRegistry.Registries"), ("azure-cr-disable-authentication-as-arm-quick-action", "Azure.ContainerRegistry.Registries"), ], ) def test_acr_update_command(mocker, client, mock_params, command, expected_prefix): """ Given: An Azure client and a request to update Azure Container Registry properties. When: The acr_update_command function is called with valid parameters. Then: The function should return the updated ACR properties in the expected format. """ # Prepare mock response acr_response = { "name": "testregistry", "id": "/subscriptions/sub-id/resourceGroups/test-rg/providers/Microsoft.ContainerRegistry/registries/testregistry", "properties": { "publicNetworkAccess": "Disabled", "anonymousPullEnabled": False, "policies": {"exportPolicy": {"status": "disabled"}, "azureADAuthenticationAsArmPolicy": {"status": "enabled"}}, }, } mocker.patch.object(client, "acr_update", return_value=acr_response) mocker.patch.object(demisto, "command", return_value=command) # Call the function args = { "registry_name": "testregistry", "allow_exports": "disabled", "public_network_access": "Disabled", "anonymous_pull_enabled": "false", "authentication_as_arm_policy": "enabled", } result = acr_update_command(client, mock_params, args) # Verify results assert result.outputs_prefix == expected_prefix assert result.outputs_key_field == "id" assert result.outputs["name"] == "testregistry" assert result.outputs["properties"]["publicNetworkAccess"] == "Disabled" assert result.outputs["properties"]["anonymousPullEnabled"] is False assert result.outputs["properties"]["policies"]["exportPolicy"]["status"] == "disabled" assert result.outputs["properties"]["policies"]["azureADAuthenticationAsArmPolicy"]["status"] == "enabled" def test_postgres_server_update_command(mocker, client, mock_params): """ Given: An Azure client and a request to update PostgreSQL server properties. When: The postgres_server_update_command function is called with valid parameters. Then: The function should successfully call the server update method. """ # Prepare mock response postgres_response = { "name": "test-postgres", "id": "/subscriptions/sub-id/resourceGroups/test-rg/providers/Microsoft.DBforPostgreSQL/servers/test-postgres", "properties": {"sslEnforcement": "Enabled"}, } mocker.patch.object(client, "postgres_server_update", return_value=postgres_response) # Call the function args = {"server_name": "test-postgres", "ssl_enforcement": "Enabled"} # This command doesn't have a return value to test, just ensure it calls the client method postgres_server_update_command(client, mock_params, args) # Verify the client's method was called with the correct parameters client.postgres_server_update.assert_called_once_with( mock_params.get("subscription_id"), mock_params.get("resource_group_name"), "test-postgres", "Enabled" ) def test_update_key_vault_command(mocker, client, mock_params): """ Given: An Azure client and a request to update Key Vault properties. When: The update_key_vault_command function is called with valid parameters. Then: The function should return the updated Key Vault properties in the expected format. """ # Prepare mock response keyvault_response = { "name": "test-keyvault", "id": "/subscriptions/sub-id/resourceGroups/test-rg/providers/Microsoft.KeyVault/vaults/test-keyvault", "properties": {"enableSoftDelete": True, "enablePurgeProtection": True}, } mocker.patch.object(client, "update_key_vault_request", return_value=keyvault_response) # Call the function args = {"vault_name": "test-keyvault", "enable_soft_delete": "true", "enable_purge_protection": "true"} result = update_key_vault_command(client, mock_params, args) # Verify results assert result.outputs_prefix == "Azure.KeyVault.Vault" assert result.outputs_key_field == "id" assert result.outputs["name"] == "test-keyvault" assert result.outputs["properties"]["enableSoftDelete"] is True assert result.outputs["properties"]["enablePurgeProtection"] is True def test_update_key_vault_command_deprecated(mocker, client, mock_params): """ Given: An Azure client and a request to update Key Vault properties using the deprecated command. When: The update_key_vault_command function is called with the deprecated command name. Then: The function should return the updated Key Vault properties with the deprecated prefix. """ # Prepare mock response keyvault_response = { "name": "test-keyvault", "id": "/subscriptions/sub-id/resourceGroups/test-rg/providers/Microsoft.KeyVault/vaults/test-keyvault", "properties": {"enableSoftDelete": True, "enablePurgeProtection": True}, } mocker.patch.object(client, "update_key_vault_request", return_value=keyvault_response) mocker.patch("Azure.demisto.command", return_value="azure-key-vault-update") # Call the function args = {"vault_name": "test-keyvault", "enable_soft_delete": "true", "enable_purge_protection": "true"} result = update_key_vault_command(client, mock_params, args) # Verify results assert result.outputs_prefix == "Azure.KeyVault" assert result.outputs_key_field == "id" assert result.outputs["name"] == "test-keyvault" assert result.outputs["properties"]["enableSoftDelete"] is True assert result.outputs["properties"]["enablePurgeProtection"] is True def test_sql_db_threat_policy_update_command(mocker, client, mock_params): """ Given: An Azure client and a request to update SQL database threat policy. When: The sql_db_threat_policy_update_command function is called with valid parameters. Then: The function should return the updated threat policy in the expected format. """ # Prepare mock responses current_policy = { "name": "default", "id": "/subscriptions/sub-id/resourceGroups/test-rg/providers/Microsoft.Sql/servers/test-server/databases/test-db/" "securityAlertPolicies/default", "properties": {"emailAccountAdmins": False}, } updated_policy = { "name": "default", "id": "/subscriptions/sub-id/resourceGroups/test-rg/providers/Microsoft.Sql/servers/test-server/databases/test-db/" "securityAlertPolicies/default", "properties": {"emailAccountAdmins": True}, } mocker.patch.object(client, "sql_db_threat_policy_get", return_value=current_policy) mocker.patch.object(client, "sql_db_threat_policy_update", return_value=updated_policy) # Call the function args = {"server_name": "test-server", "db_name": "test-db", "email_account_admins_enabled": "true"} result = sql_db_threat_policy_update_command(client, mock_params, args) # Verify results assert result.outputs_prefix == "Azure.SqlDB.SecurityAlertPolicies" assert result.outputs_key_field == "id" assert result.outputs["name"] == "default" assert result.outputs["properties"]["emailAccountAdmins"] is True def test_sql_db_threat_policy_update_command_deprecated(mocker, client, mock_params): """ Given: An Azure client and a request to update SQL database threat policy using the deprecated command. When: The sql_db_threat_policy_update_command function is called with the deprecated command name. Then: The function should return the updated threat policy with the deprecated prefix. """ # Prepare mock responses current_policy = { "name": "default", "id": "/subscriptions/sub-id/resourceGroups/test-rg/providers/Microsoft.Sql/servers/test-server/databases/test-db/" "securityAlertPolicies/default", "properties": {"emailAccountAdmins": False}, } updated_policy = { "name": "default", "id": "/subscriptions/sub-id/resourceGroups/test-rg/providers/Microsoft.Sql/servers/test-server/databases/test-db/" "securityAlertPolicies/default", "properties": {"emailAccountAdmins": True}, } mocker.patch.object(client, "sql_db_threat_policy_get", return_value=current_policy) mocker.patch.object(client, "sql_db_threat_policy_update", return_value=updated_policy) mocker.patch("Azure.demisto.command", return_value="azure-sql-db-threat-policy-update") # Call the function args = {"server_name": "test-server", "db_name": "test-db", "email_account_admins_enabled": "true"} result = sql_db_threat_policy_update_command(client, mock_params, args) # Verify results assert result.outputs_prefix == "Azure.SqlDBThreatPolicy" assert result.outputs_key_field == "id" assert result.outputs["name"] == "default" assert result.outputs["properties"]["emailAccountAdmins"] is True def test_sql_db_threat_policy_update_command_not_found(mocker, client, mock_params): """ Given: An Azure client and a request to update SQL database threat policy for a non-existent database. When: The sql_db_threat_policy_update_command function is called with invalid parameters. Then: The function should handle the error case properly. """ # Mock a realistic 404 error response def mock_get_threat_policy(*args, **kwargs): # Simulate what the actual Azure API would return raise DemistoException("Resource not found - Database 'test-db' does not exist on server 'test-server'") mocker.patch.object(client, "sql_db_threat_policy_get", side_effect=mock_get_threat_policy) # Call the function args = {"server_name": "test-server", "db_name": "test-db", "email_account_admins_enabled": "true"} # Test that the function handles the exception appropriately with pytest.raises(DemistoException) as excinfo: sql_db_threat_policy_update_command(client, mock_params, args) # Verify the error message contains expected information assert "test-db" in str(excinfo.value) assert "test-server" in str(excinfo.value) def test_sql_db_tde_set_command(mocker, client, mock_params): """ Given: An Azure client and a request to set transparent data encryption for a SQL database. When: The sql_db_tde_set_command function is called with valid parameters. Then: The function should return a success message. """ # Prepare mock mocker.patch.object(client, "sql_db_tde_set", return_value=None) # Call the function args = {"server_name": "test-server", "db_name": "test-db", "state": "Enabled"} result = sql_db_tde_set_command(client, mock_params, args) # Verify results assert "Updated SQL database test-db of the server test-server" in result.readable_output def test_cosmosdb_update_command(mocker, client, mock_params): """ Given: An Azure client and a request to update Cosmos DB settings. When: The cosmosdb_update_command function is called with valid parameters. Then: The function should return the updated Cosmos DB settings in the expected format. """ # Prepare mock response cosmos_response = { "name": "test-cosmos", "id": "/subscriptions/sub-id/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/test-cosmos", "properties": {"disableKeyBasedMetadataWriteAccess": True}, } mocker.patch.object(client, "cosmos_db_update", return_value=cosmos_response) # Call the function args = {"account_name": "test-cosmos", "disable_key_based_metadata_write_access": "true"} result = cosmosdb_update_command(client, mock_params, args) # Verify results assert result.outputs_prefix == "Azure.CosmosDB.DBAccounts" assert result.outputs_key_field == "id" assert result.outputs["name"] == "test-cosmos" assert result.outputs["properties"]["disableKeyBasedMetadataWriteAccess"] is True def test_cosmosdb_update_command_deprecated(mocker, client, mock_params): """ Given: An Azure client and a request to update Cosmos DB settings using the deprecated command. When: The cosmosdb_update_command function is called with the deprecated command name. Then: The function should return the updated Cosmos DB settings with the deprecated prefix. """ # Prepare mock response cosmos_response = { "name": "test-cosmos", "id": "/subscriptions/sub-id/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/test-cosmos", "properties": {"disableKeyBasedMetadataWriteAccess": True}, } mocker.patch.object(client, "cosmos_db_update", return_value=cosmos_response) mocker.patch("Azure.demisto.command", return_value="azure-cosmos-db-update") # Call the function args = {"account_name": "test-cosmos", "disable_key_based_metadata_write_access": "true"} result = cosmosdb_update_command(client, mock_params, args) # Verify results assert result.outputs_prefix == "Azure.CosmosDB" assert result.outputs_key_field == "id" assert result.outputs["name"] == "test-cosmos" assert result.outputs["properties"]["disableKeyBasedMetadataWriteAccess"] is True def test_storage_account_update_command_empty_response(mocker, client, mock_params): """ Given: An Azure client and a request to update a storage account that returns an empty response. When: The storage_account_update_command function is called with valid parameters. Then: The function should return a message indicating the account will be created shortly. """ # Prepare mock response with empty text class MockResponse: def __init__(self, text=""): self.text = text mock_response = MockResponse("") mocker.patch.object(client, "storage_account_update_request", return_value=mock_response) # Call the function args = {"account_name": "teststorage", "sku": "Standard_LRS", "kind": "StorageV2", "location": "eastus"} result = storage_account_update_command(client, mock_params, args) # Verify results assert isinstance(result, str) assert "The request was accepted - the account teststorage will be created shortly" in result def test_update_security_rule_command_rule_not_found(mocker, client, mock_params): """ Given: An Azure client and a request to update a non-existent security rule. When: The update_security_rule_command function is called with invalid parameters. Then: The function should handle the error case properly by raising a ValueError. """ # Mock a 404 error response by raising a ValueError with 404 in it error_message = 'Rule test-rule under subscription ID "test_subscription_id" and resource group "test_resource_group" was\ not found.' mocker.patch.object(client, "get_rule", side_effect=ValueError(error_message)) # Call the function and expect it to raise the ValueError args = {"security_group_name": "test-sg", "security_rule_name": "test-rule"} with pytest.raises(ValueError) as e: update_security_rule_command(client, mock_params, args) # Verify the error message assert error_message in str(e.value) def test_main_function_success(mocker): """ Given: A command and valid parameters. When: The main function is called. Then: The appropriate command function should be called and results returned. """ from Azure import main # Mock demisto functions mocker.patch.object(demisto, "command", return_value="azure-storage-account-update") mocker.patch.object( demisto, "params", return_value={ "app_id": "test_app_id", "subscription_id": "test_subscription_id", "resource_group_name": "test_resource_group", "auth_type": "Client Credentials", "tenant_id": "test_tenant_id", "credentials": {"password": "test_enc_key"}, }, ) mocker.patch.object( demisto, "args", return_value={"account_name": "teststorage", "sku": "Standard_LRS", "kind": "StorageV2", "location": "eastus"}, ) # mock is_gov_account mocker.patch("Azure.is_gov_account", return_value=False) # Mock return_results mock_return_results = mocker.patch("Azure.return_results") # Mock AzureClient mock_client = mocker.Mock() mocker.patch("Azure.AzureClient", return_value=mock_client) # Mock storage_account_update_command to return a CommandResults object mock_cmd_result = mocker.Mock() mock_storage_account_update = mocker.patch("Azure.storage_account_update_command", return_value=mock_cmd_result) # Call main function main() # Verify that storage_account_update_command was called and results returned mock_storage_account_update.assert_called_once() mock_return_results.assert_called_once_with(mock_cmd_result) def test_storage_blob_service_properties_set_command_empty_values(mocker, client, mock_params): """ Given: An Azure client and a request to set blob service properties with empty values. When: The storage_blob_service_properties_set_command function is called with minimal parameters. Then: The function should make the API call with only the specified parameters. """ # Prepare mock response properties_response = { "name": "default", "id": "/subscriptions/sub-id/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/teststorage/blobServices/\ default", "properties": {}, } mocker.patch.object(client, "storage_blob_service_properties_set_request", return_value=properties_response) mocker.patch("Azure.demisto.command", return_value="azure-storage-blob-service-property-set") # Call the function with minimal args args = {"account_name": "teststorage"} result = storage_blob_service_properties_set_command(client, mock_params, args) # Verify results assert result.outputs_prefix == "Azure.Storage.BlobServices" assert result.outputs_key_field == "id" assert result.outputs["name"] == "default" # Verify client method was called with None values for optional parameters client.storage_blob_service_properties_set_request.assert_called_once_with( mock_params.get("subscription_id"), mock_params.get("resource_group_name"), "teststorage", None, None ) def test_remove_member_from_role(mocker, client): """ Given: An Azure client and arguments for removing a member from a role. When: The remove_member_from_role function is called. Then: The function should call the client's remove_member_from_role method and return a success message. """ # Mock arguments args = {"role_id": "12345678-1234-1234-1234-123456789012", "user_id": "87654321-4321-4321-4321-210987654321"} # Mock the client's remove_member_from_role method mocker.patch.object(client, "remove_member_from_role") # Call the function result = remove_member_from_role(client, args) # Verify results assert isinstance(result, CommandResults) assert f"User ID {args['user_id']} has been removed from role {args['role_id']}" in result.readable_output client.remove_member_from_role.assert_called_once_with(args["role_id"], args["user_id"]) def test_remove_member_from_group_command(mocker, client): """ Given: An Azure client and arguments for removing a member from a group. When: The remove_member_from_group_command function is called. Then: The function should call the client's remove_member_from_group method and return a success message. """ # Mock arguments args = {"group_id": "11111111-2222-3333-4444-555555555555", "user_id": "87654321-4321-4321-4321-210987654321"} # Mock the client's remove_member_from_group method mocker.patch.object(client, "remove_member_from_group") # Call the function result = remove_member_from_group_command(client, args) # Verify results assert isinstance(result, CommandResults) assert f"User {args['user_id']} was removed from the Group \"{args['group_id']}\" successfully." in result.readable_output client.remove_member_from_group.assert_called_once_with(args["group_id"], args["user_id"]) def test_get_azure_client_no_token(mocker, mock_params): """ Given: Platform (connector) path where cloud credentials return no token. When: The get_azure_client function is called. Then: The function should raise an exception about the missing token. """ # Setup mocks args = {"subscription_id": "arg_subscription_id"} command = "command" mocker.patch("Azure.get_connector_id", return_value="connector-123") # Platform path mocker.patch("Azure.get_from_args_or_params", return_value="mocked_subscription_id") mocker.patch("Azure.get_cloud_credentials", return_value={}) # No token # Test without credentials and no token params = mock_params.copy() params["credentials"] = {} # Verify exception is raised with pytest.raises(DemistoException) as excinfo: get_azure_client(params, args, command) assert "Failed to retrieve AZURE access token" in str(excinfo.value) def test_get_azure_client_marketplace_missing_secret(mocker, mock_params): """ Given: Marketplace path (no connector) with the Client Credentials flow and no Client Secret. When: The get_azure_client function is called. Then: The function should raise a clear DemistoException about the missing Client Secret. """ args = {"subscription_id": "arg_subscription_id"} command = "command" mocker.patch("Azure.get_connector_id", return_value=None) # Marketplace path params = mock_params.copy() params["credentials"] = {} params["auth_type"] = "Client Credentials" with pytest.raises(DemistoException) as excinfo: get_azure_client(params, args, command) assert "Client Secret" in str(excinfo.value) def test_get_azure_client_with_stored_credentials(mocker, mock_params): """ Given: Marketplace path with a Client Secret configured (Client Credentials flow). When: The get_azure_client function is called. Then: The function should return an initialized Azure client using the secret without cloud (CTS) authentication. """ # Setup mocks args = {"subscription_id": "arg_subscription_id"} command = "command" mock_client = mocker.Mock() mocker.patch("Azure.get_connector_id", return_value=None) # Marketplace path mock_get_managed = mocker.patch("Azure.get_azure_managed_identities_client_id", return_value=None) mock_azure_client_constructor = mocker.patch("Azure.AzureClient", return_value=mock_client) # Test with credentials (stored credentials path) params = mock_params.copy() params["credentials"] = {"password": "test_password"} # Call the function result = get_azure_client(params, args, command) # Verify results assert result == mock_client # Verify AzureClient was instantiated with correct parameters mock_azure_client_constructor.assert_called_once_with( app_id=params["app_id"], subscription_id=params["subscription_id"], resource_group_name=params["resource_group_name"], verify=not params["insecure"], proxy=params["proxy"], tenant_id=params["tenant_id"], enc_key="test_password", resource=Resources.management_azure, scope=SCOPE_BY_CONNECTION.get("Client Credentials"), headers={}, connection_type="Client Credentials", azure_ad_endpoint="https://login.microsoftonline.com", auth_code=None, redirect_uri=None, managed_identities_client_id=mock_get_managed.return_value, ) def test_get_azure_client_with_cloud_credentials_azure_command(mocker, mock_params): """ Given: Platform (connector) path without stored credentials. When: The get_azure_client function is called. Then: The function should retrieve cloud credentials and return a client with proper headers and scope. """ # Setup mocks args = {"subscription_id": "arg_subscription_id"} command = "command" mock_client = mocker.Mock() mock_token = "mock_access_token" mocker.patch("Azure.get_connector_id", return_value="connector-123") # Platform path mock_get_managed = mocker.patch("Azure.get_azure_managed_identities_client_id", return_value=None) mocker.patch("Azure.get_from_args_or_params", return_value="test_subscription_id") mocker.patch("Azure.get_cloud_credentials", return_value={"access_token": mock_token}) mock_azure_client_constructor = mocker.patch("Azure.AzureClient", return_value=mock_client) # Test without stored credentials (cloud credentials path) params = mock_params.copy() params["credentials"] = {} # No stored credentials # Call the function result = get_azure_client(params, args, command) # Verify results assert result == mock_client # Verify cloud credentials were retrieved with correct parameters Azure.get_cloud_credentials.assert_called_once_with(CloudTypes.AZURE.value, "test_subscription_id", scopes=["DEFAULT"]) # Verify AzureClient was instantiated with correct parameters including headers expected_headers = {"Authorization": f"Bearer {mock_token}", "Content-Type": "application/json", "Accept": "application/json"} mock_azure_client_constructor.assert_called_once_with( app_id=params["app_id"], subscription_id=params["subscription_id"], resource_group_name=params["resource_group_name"], verify=not params["insecure"], proxy=params["proxy"], tenant_id=params["tenant_id"], enc_key=None, resource=Resources.management_azure, scope=SCOPE_BY_CONNECTION.get("Client Credentials"), headers=expected_headers, connection_type="Client Credentials", azure_ad_endpoint="https://login.microsoftonline.com", auth_code=None, redirect_uri=None, managed_identities_client_id=mock_get_managed.return_value, ) def test_get_azure_client_no_token_raises_exception(mocker, mock_params): """ Given: Platform (connector) path with cloud credentials that return no token. When: The get_azure_client function is called. Then: The function should raise a DemistoException about missing token. """ # Setup mocks args = {"subscription_id": "arg_subscription_id"} command = "command" mocker.patch("Azure.get_connector_id", return_value="connector-123") # Platform path mocker.patch("Azure.get_from_args_or_params", return_value="test_subscription_id") mocker.patch("Azure.get_cloud_credentials", return_value={}) # No access_token # Test without credentials and no token params = mock_params.copy() params["credentials"] = {} # Verify exception is raised with pytest.raises(DemistoException) as excinfo: get_azure_client(params, args, command) assert "Failed to retrieve AZURE access token - token is missing from credentials" in str(excinfo.value) def test_get_azure_client_insecure_and_proxy_settings(mocker, mock_params): """ Given: Parameters with insecure=True and proxy=True settings. When: The get_azure_client function is called. Then: The function should pass the correct verify and proxy parameters to AzureClient. """ # Setup mocks args = {"subscription_id": "arg_subscription_id"} command = "command" mock_client = mocker.Mock() mock_azure_client_constructor = mocker.patch("Azure.AzureClient", return_value=mock_client) # Test with insecure and proxy settings params = mock_params.copy() params["insecure"] = True params["proxy"] = True params["credentials"] = {"password": "test_password"} # Call the function result = get_azure_client(params, args, command) # Verify results assert result == mock_client # Verify correct verify and proxy parameters call_args = mock_azure_client_constructor.call_args assert call_args[1]["verify"] is False # insecure=True means verify=False assert call_args[1]["proxy"] is True def test_get_azure_client_missing_optional_params(mocker): """ Given: The mandatory Client Credentials params are provided, but the truly optional fields (resource_group_name, insecure, proxy) are omitted. When: The get_azure_client function is called. Then: The function builds the client using default values for the omitted optional fields. """ # Setup mocks args = {} mock_client = mocker.Mock() command = "command" mocker.patch("Azure.get_connector_id", return_value=None) # Marketplace path mock_azure_client_constructor = mocker.patch("Azure.AzureClient", return_value=mock_client) # Mandatory params for Client Credentials present; optional fields omitted. params = { "app_id": "test_app_id", "subscription_id": "test_subscription_id", "tenant_id": "test_tenant_id", "credentials": {"password": "test_password"}, } # Call the function result = get_azure_client(params, args, command) # Verify results assert result == mock_client # Verify default values were used for the optional fields call_args = mock_azure_client_constructor.call_args assert call_args[1]["resource_group_name"] == "" assert call_args[1]["verify"] is True # Default for insecure=False assert call_args[1]["proxy"] is False # Default def test_format_rule_dict_input(mocker): """ Given: A rule JSON as dictionary and security rule name. When: The format_rule function is called. Then: The function should format the rule properly and return CommandResults. """ # Prepare test data rule_json = { "name": "test-rule", "id": ( "/subscriptions/sub-id/resourceGroups/test-rg/providers/Microsoft.Network/networkSecurityGroups/test-sg/" "securityRules/test-rule" ), "properties": { "protocol": "Tcp", "sourcePortRange": "*", "destinationPortRange": "443", "access": "Allow", "priority": 100, "direction": "Inbound", }, } security_rule_name = "test-rule" # Mock tableToMarkdown mock_table = mocker.patch("Azure.tableToMarkdown", return_value="Mock Table") # Call the function result = format_rule(rule_json, security_rule_name) # Verify results assert isinstance(result, CommandResults) assert result.outputs_prefix == "Azure.NSGRule" assert result.outputs_key_field == "id" assert result.outputs["name"] == "test-rule" assert result.outputs["protocol"] == "Tcp" assert result.outputs["access"] == "Allow" assert "properties" not in result.outputs # Properties should be flattened # Verify tableToMarkdown was called mock_table.assert_called_once() def test_format_rule_list_input(mocker): """ Given: A rule JSON as list and security rule name. When: The format_rule function is called. Then: The function should format all rules properly and return CommandResults. """ # Prepare test data rule_json = [ { "name": "rule1", "id": ( "/subscriptions/sub-id/resourceGroups/test-rg/providers/Microsoft.Network/networkSecurityGroups/test-sg/" "securityRules/rule1" ), "properties": {"protocol": "Tcp", "access": "Allow"}, }, { "name": "rule2", "id": ( "/subscriptions/sub-id/resourceGroups/test-rg/providers/Microsoft.Network/networkSecurityGroups/test-sg/" "securityRules/rule2" ), "properties": {"protocol": "Udp", "access": "Deny"}, }, ] security_rule_name = "test-rules" # Mock tableToMarkdown mocker.patch("Azure.tableToMarkdown", return_value="Mock Table") # Call the function result = format_rule(rule_json, security_rule_name) # Verify results assert isinstance(result, CommandResults) assert result.outputs_prefix == "Azure.NSGRule" assert result.outputs_key_field == "id" assert len(result.outputs) == 2 assert result.outputs[0]["name"] == "rule1" assert result.outputs[0]["protocol"] == "Tcp" assert result.outputs[1]["name"] == "rule2" assert result.outputs[1]["protocol"] == "Udp" # Verify properties were flattened for all rules for rule in result.outputs: assert "properties" not in rule def test_azure_client_handle_azure_error_404(mocker, client): """ Given: An Azure client and a 404 error. When: The handle_azure_error method is called. Then: The function should raise a ValueError with appropriate message. """ # Prepare test data error = Exception("404 - Not Found") resource_name = "test-resource" resource_type = "Storage Account" subscription_id = "test-subscription" resource_group_name = "test-rg" # Verify ValueError is raised for 404 errors with pytest.raises(ValueError) as excinfo: client.handle_azure_error( e=error, resource_name=resource_name, resource_type=resource_type, api_function_name="test", subscription_id=subscription_id, resource_group_name=resource_group_name, ) assert 'Storage Account "test-resource"' in str(excinfo.value) assert 'subscription ID "test-subscription"' in str(excinfo.value) assert 'resource group "test-rg"' in str(excinfo.value) assert "was not found" in str(excinfo.value) def test_azure_client_handle_azure_error_using_return_multiple_permissions_error_function(mocker): """ Test the permission lookup logic and return_multiple_permissions_error call in handle_azure_error. Tests: 1. Permission found via API function mapping 2. Permission found via fallback method """ from Azure import AzureClient mock_get_permissions_from_api = mocker.patch("Azure.get_permissions_from_api_function_name") mock_get_permissions_from_required = mocker.patch("Azure.get_permissions_from_required_role_permissions_list") mock_return_multiple_permissions_error = mocker.patch("Azure.return_multiple_permissions_error") client = AzureClient("tenant_id", "client_id", "client_secret") # Test case 1: Permission found via API function mapping mock_get_permissions_from_api.return_value = ["Microsoft.Network/networkInterfaces/read"] mock_get_permissions_from_required.return_value = None exception_403 = Exception("403 Forbidden: Access denied") client.handle_azure_error( e=exception_403, resource_name="test-nic", resource_type="Network Interface", api_function_name="list_networks_interfaces_request", subscription_id="sub123", ) mock_get_permissions_from_api.assert_called_with("list_networks_interfaces_request", "403 forbidden: access denied") mock_get_permissions_from_required.assert_not_called() expected_error_entries = [ {"account_id": "sub123", "message": "403 forbidden: access denied", "name": "Microsoft.Network/networkInterfaces/read"} ] mock_return_multiple_permissions_error.assert_called_once_with(expected_error_entries) mock_get_permissions_from_api.reset_mock() mock_get_permissions_from_required.reset_mock() mock_return_multiple_permissions_error.reset_mock() # Test case 2: Permission found via fallback method mock_get_permissions_from_api.return_value = None mock_get_permissions_from_required.return_value = ["Microsoft.Storage/storageAccounts/write"] exception_401 = Exception("401 Unauthorized") client.handle_azure_error( e=exception_401, resource_name="test-storage", resource_type="Storage Account", api_function_name="storage_account_update_request", subscription_id="sub456", ) mock_get_permissions_from_api.assert_called_with("storage_account_update_request", "401 unauthorized") mock_get_permissions_from_required.assert_called_with("401 unauthorized") expected_error_entries = [ {"account_id": "sub456", "message": "401 unauthorized", "name": "Microsoft.Storage/storageAccounts/write"} ] mock_return_multiple_permissions_error.assert_called_once_with(expected_error_entries) def test_azure_client_handle_azure_error_400(mocker, client): """ Given: An Azure client and a 400 error. When: The handle_azure_error method is called. Then: The function should raise a DemistoException with bad request error message. """ # Prepare test data error = Exception("400 - Bad Request") resource_name = "test-resource" resource_type = "Disk" # Verify DemistoException is raised for 400 errors with pytest.raises(DemistoException) as excinfo: client.handle_azure_error(e=error, resource_name=resource_name, resource_type=resource_type, api_function_name="test") assert 'Invalid request for Disk "test-resource"' in str(excinfo.value) def test_azure_client_handle_azure_error_generic(mocker, client): """ Given: An Azure client and a generic error. When: The handle_azure_error method is called. Then: The function should raise a DemistoException with the original error. """ # Prepare test data error = Exception("Some other error") resource_name = "test-resource" resource_type = "Virtual Machine" # Verify DemistoException is raised for generic errors with pytest.raises(DemistoException) as excinfo: client.handle_azure_error(e=error, resource_name=resource_name, resource_type=resource_type, api_function_name="test") assert 'Failed to access Virtual Machine "test-resource"' in str(excinfo.value) assert "Some other error" in str(excinfo.value) def test_azure_client_http_request_with_headers(mocker, mock_params): """ Given: An Azure client with headers and request parameters. When: The http_request method is called. Then: The function should make the request with proper headers and proxy settings. """ # Setup mocks headers = {"Authorization": "Bearer token", "Content-Type": "application/json"} mock_base_client = mocker.Mock() mocker.patch("Azure.get_proxydome_token", return_value="proxy_token") mocker.patch("Azure.BaseClient", return_value=mock_base_client) # Create client with headers client = AzureClient(headers=headers) # Call the function client.http_request(method="GET", url_suffix="/test", params={"param1": "value1"}) # Verify BaseClient was used and proxydome token was added expected_headers = headers.copy() expected_headers["x-caller-id"] = "proxy_token" mock_base_client._http_request.assert_called_once() call_args = mock_base_client._http_request.call_args assert call_args[1]["headers"] == expected_headers assert "proxies" in call_args[1] def test_azure_client_http_request_without_headers(mocker, mock_params): """ Given: An Azure client without headers. When: The http_request method is called. Then: The function should use MicrosoftClient for the request. """ # Setup mocks mock_ms_client = mocker.Mock() mocker.patch("Azure.MicrosoftClient", return_value=mock_ms_client) # Create client without headers client = AzureClient() # Call the function client.http_request(method="GET", url_suffix="/test") # Verify MicrosoftClient was used mock_ms_client.http_request.assert_called_once_with( method="GET", url_suffix="/test", full_url=None, json_data=None, params={"api-version": "2022-09-01"}, resp_type="json" ) def test_azure_client_http_request_api_version_override(mocker, mock_params): """ Given: An Azure client and request parameters with custom api-version. When: The http_request method is called. Then: The function should use the provided api-version instead of default. """ # Setup mocks mock_ms_client = mocker.Mock() mocker.patch("Azure.MicrosoftClient", return_value=mock_ms_client) # Create client client = AzureClient() # Call the function with custom api-version custom_params = {"api-version": "2023-01-01", "other-param": "value"} client.http_request(method="GET", url_suffix="/test", params=custom_params) # Verify custom api-version was preserved mock_ms_client.http_request.assert_called_once() call_args = mock_ms_client.http_request.call_args assert call_args[1]["params"]["api-version"] == "2023-01-01" assert call_args[1]["params"]["other-param"] == "value" def test_azure_client_initialization_without_refresh_token(mocker): """ Given: An app_id without a refresh token. When: AzureClient is initialized. Then: The client should not modify the integration context. """ # Mock integration context functions mocker.patch("Azure.get_integration_context", return_value={}) mock_set_context = mocker.patch("Azure.set_integration_context") mocker.patch("Azure.MicrosoftClient") # Initialize client without refresh token AzureClient(app_id="test_app_id") # Verify integration context was not modified mock_set_context.assert_not_called() def test_azure_client_http_request_with_base_client(mocker): """ Given: An Azure client with headers configured. When: The http_request method is called. Then: The function should use BaseClient with proper headers and proxy settings. """ # Setup mocks headers = {"Authorization": "Bearer token", "Content-Type": "application/json"} mock_base_client = mocker.Mock() mock_base_client._http_request.return_value = {"result": "success"} mocker.patch("Azure.BaseClient", return_value=mock_base_client) mocker.patch("Azure.get_proxydome_token", return_value="proxy_token") # Create client with headers client = AzureClient(headers=headers) # Call the function result = client.http_request(method="GET", url_suffix="/test") # Verify BaseClient was used with correct parameters expected_headers = headers.copy() expected_headers["x-caller-id"] = "proxy_token" mock_base_client._http_request.assert_called_once() call_args = mock_base_client._http_request.call_args assert call_args[1]["headers"] == expected_headers assert "proxies" in call_args[1] assert result == {"result": "success"} def test_azure_client_http_request_with_microsoft_client(mocker): """ Given: An Azure client without headers configured. When: The http_request method is called. Then: The function should use MicrosoftClient. """ # Setup mocks mock_ms_client = mocker.Mock() mock_ms_client.http_request.return_value = {"result": "success"} mocker.patch("Azure.MicrosoftClient", return_value=mock_ms_client) # Create client without headers client = AzureClient() # Call the function result = client.http_request(method="POST", url_suffix="/test", json_data={"key": "value"}) # Verify MicrosoftClient was used mock_ms_client.http_request.assert_called_once_with( method="POST", url_suffix="/test", full_url=None, json_data={"key": "value"}, params={"api-version": "2022-09-01"}, resp_type="json", ) assert result == {"result": "success"} def test_azure_client_get_rule(mocker, client): """ Given: An Azure client and security rule parameters. When: The get_rule method is called. Then: The function should make the correct API call. """ # Setup mock response mock_response = { "name": "test-rule", "id": ( "/subscriptions/sub-id/resourceGroups/test-rg/providers/Microsoft.Network/networkSecurityGroups/test-sg/" "securityRules/test-rule" ), "properties": {"access": "Allow", "protocol": "Tcp"}, } mocker.patch.object(client, "http_request", return_value=mock_response) # Call the function result = client.get_rule("test-sg", "test-rule", "sub-id", "test-rg") # Verify correct API call was made expected_url = ( f"{PREFIX_URL_AZURE}sub-id/resourceGroups/test-rg/providers/Microsoft.Network/networkSecurityGroups/test-sg/" "securityRules/test-rule" ) client.http_request.assert_called_once_with("GET", full_url=expected_url) assert result == mock_response def test_azure_client_create_policy_assignment(mocker, client): """ Given: An Azure client and policy assignment parameters. When: The create_policy_assignment method is called. Then: The function should make the correct API call with policy properties. """ # Setup mock response mock_response = { "name": "test-policy", "properties": {"policyDefinitionId": "/providers/Microsoft.Authorization/policySetDefinitions/test-def"}, } mocker.patch.object(client, "http_request", return_value=mock_response) # Call the function client.create_policy_assignment( name="test-policy", policy_definition_id="test-def", display_name="Test Policy", description="Test description", parameters={"param1": "value1"}, scope="scope", ) # Verify correct API call was made expected_url = "https://management.azure.com/scope/providers/Microsoft.Authorization/policyAssignments/test-policy" client.http_request.assert_called_once() call_args = client.http_request.call_args assert call_args[1]["method"] == "PUT" assert call_args[1]["full_url"] == expected_url # Verify policy properties were included json_data = call_args[1]["json_data"] properties = json_data["properties"] assert properties["policyDefinitionId"] == "/providers/Microsoft.Authorization/policySetDefinitions/test-def" assert properties["displayName"] == "Test Policy" assert properties["description"] == "Test description" assert properties["parameters"] == {"param1": "value1"} def test_azure_client_create_rule_success(mocker, client): """ Given: An Azure client and valid rule creation parameters. When: The create_or_update_rule method is called. Then: The function should make the correct API call with rule properties and return the response. """ # Setup mock response mock_response = { "name": "test-rule", "id": ( "/subscriptions/sub-id/resourceGroups/test-rg/providers/Microsoft.Network/networkSecurityGroups/test-sg/" "securityRules/test-rule" ), "properties": { "protocol": "Tcp", "sourcePortRange": "*", "destinationPortRange": "443", "sourceAddressPrefix": "Internet", "destinationAddressPrefix": "10.0.0.0/24", "access": "Allow", "priority": 100, "direction": "Inbound", "description": "Test rule", }, } mocker.patch.object(client, "http_request", return_value=mock_response) # Prepare rule properties properties = { "protocol": "Tcp", "sourcePortRange": "*", "destinationPortRange": "443", "sourceAddressPrefix": "Internet", "destinationAddressPrefix": "10.0.0.0/24", "access": "Allow", "priority": 100, "direction": "Inbound", "description": "Test rule", } # Call the function result = client.create_or_update_rule( security_group="test-sg", rule_name="test-rule", properties=properties, subscription_id="sub-id", resource_group_name="test-rg", ) # Verify correct API call was made expected_url = ( f"{PREFIX_URL_AZURE}sub-id/resourceGroups/test-rg/providers/Microsoft.Network/networkSecurityGroups/test-sg/" "securityRules/test-rule?" ) client.http_request.assert_called_once_with("PUT", full_url=expected_url, json_data={"properties": properties}) # Verify response assert result == mock_response assert result["name"] == "test-rule" assert result["properties"]["protocol"] == "Tcp" assert result["properties"]["access"] == "Allow" def test_azure_client_create_rule_with_complex_properties(mocker, client): """ Given: An Azure client and complex rule properties with multiple ports and addresses. When: The create_or_update_rule method is called. Then: The function should handle complex properties correctly. """ # Setup mock response mock_response = { "name": "complex-rule", "id": ( "/subscriptions/sub-id/resourceGroups/test-rg/providers/Microsoft.Network/networkSecurityGroups/test-sg/" "securityRules/complex-rule" ), "properties": { "protocol": "*", "sourcePortRanges": ["80", "443", "8080-8090"], "destinationPortRanges": ["22", "3389"], "sourceAddressPrefixes": ["10.0.0.0/24", "192.168.1.0/24"], "destinationAddressPrefixes": ["172.16.0.0/16"], "access": "Deny", "priority": 200, "direction": "Outbound", "description": "Complex rule with multiple ranges", }, } mocker.patch.object(client, "http_request", return_value=mock_response) # Prepare complex rule properties properties = { "protocol": "*", "sourcePortRanges": ["80", "443", "8080-8090"], "destinationPortRanges": ["22", "3389"], "sourceAddressPrefixes": ["10.0.0.0/24", "192.168.1.0/24"], "destinationAddressPrefixes": ["172.16.0.0/16"], "access": "Deny", "priority": 200, "direction": "Outbound", "description": "Complex rule with multiple ranges", } # Call the function result = client.create_or_update_rule( security_group="test-sg", rule_name="complex-rule", properties=properties, subscription_id="sub-id", resource_group_name="test-rg", ) # Verify correct API call was made expected_url = ( f"{PREFIX_URL_AZURE}sub-id/resourceGroups/test-rg/providers/Microsoft.Network/networkSecurityGroups/test-sg/" "securityRules/complex-rule?" ) client.http_request.assert_called_once_with("PUT", full_url=expected_url, json_data={"properties": properties}) # Verify response contains complex properties assert result == mock_response assert result["properties"]["sourcePortRanges"] == ["80", "443", "8080-8090"] assert result["properties"]["destinationPortRanges"] == ["22", "3389"] assert result["properties"]["sourceAddressPrefixes"] == ["10.0.0.0/24", "192.168.1.0/24"] def test_azure_client_storage_account_update_request_success(mocker, client): """ Given: An Azure client and valid storage account update parameters. When: The storage_account_update_request method is called. Then: The function should make the correct API call with storage account properties and return the response. """ # Setup mock response mock_response = mocker.Mock() mock_response.text = '{"name": "teststorage", "properties": {"supportsHttpsTrafficOnly": true}}' mock_response.json.return_value = { "name": "teststorage", "id": "/subscriptions/sub-id/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/teststorage", "properties": {"supportsHttpsTrafficOnly": True, "networkAcls": {"bypass": "AzureServices", "defaultAction": "Deny"}}, } mocker.patch.object(client, "http_request", return_value=mock_response) # Prepare arguments args = { "account_name": "teststorage", "sku": "Standard_LRS", "kind": "StorageV2", "location": "eastus", "supports_https_traffic_only": "true", "network_ruleset_bypass": "AzureServices", "network_ruleset_default_action": "Deny", } # Call the function result = client.storage_account_update_request(subscription_id="sub-id", resource_group_name="test-rg", args=args) # Verify correct API call was made expected_url = f"{PREFIX_URL_AZURE}sub-id/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/teststorage" client.http_request.assert_called_once() call_args = client.http_request.call_args assert call_args[1]["method"] == "PATCH" assert call_args[1]["full_url"] == expected_url assert call_args[1]["params"]["api-version"] == API_VERSION assert call_args[1]["resp_type"] == "response" # Verify JSON data structure json_data = call_args[1]["json_data"] assert json_data["sku"]["name"] == "Standard_LRS" assert json_data["kind"] == "StorageV2" assert json_data["location"] == "eastus" assert json_data["properties"]["supportsHttpsTrafficOnly"] == "true" assert json_data["properties"]["networkAcls"]["bypass"] == "AzureServices" assert json_data["properties"]["networkAcls"]["defaultAction"] == "Deny" # Verify response assert result == mock_response def test_azure_client_storage_blob_service_properties_set_request_success(mocker, client): """ Given: An Azure client and valid blob service properties parameters. When: The storage_blob_service_properties_set_request method is called. Then: The function should make the correct API call with blob properties and return the response. """ # Setup mock response mock_response = { "name": "default", "id": ( "/subscriptions/sub-id/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/teststorage/" "blobServices/default" ), "properties": {"deleteRetentionPolicy": {"enabled": True, "days": 7}}, } mocker.patch.object(client, "http_request", return_value=mock_response) # Call the function result = client.storage_blob_service_properties_set_request( subscription_id="sub-id", resource_group_name="test-rg", account_name="teststorage", delete_rentention_policy_enabled="true", delete_rentention_policy_days="7", ) # Verify correct API call was made expected_url = ( f"{PREFIX_URL_AZURE}sub-id/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/teststorage/" "blobServices/default" ) client.http_request.assert_called_once() call_args = client.http_request.call_args assert call_args[1]["method"] == "PUT" assert call_args[1]["full_url"] == expected_url assert call_args[1]["params"]["api-version"] == API_VERSION # Verify JSON data structure json_data = call_args[1]["json_data"] assert json_data["properties"]["deleteRetentionPolicy"]["enabled"] == "true" assert json_data["properties"]["deleteRetentionPolicy"]["days"] == "7" # Verify response assert result == mock_response assert result["name"] == "default" assert result["properties"]["deleteRetentionPolicy"]["enabled"] is True assert result["properties"]["deleteRetentionPolicy"]["days"] == 7 def test_azure_client_handle_azure_error_other(client): """ Given: An unknown error from Azure API. When: handle_azure_error is called. Then: The function should raise DemistoException with generic message. """ error = Exception("500 - Internal Server Error") with pytest.raises(DemistoException) as excinfo: client.handle_azure_error(e=error, resource_name="test-resource", resource_type="SQL Database", api_function_name="test") assert 'Failed to access SQL Database "test-resource"' in str(excinfo.value) assert "500 - Internal Server Error" in str(excinfo.value) def test_azure_client_update_webapp_auth_success(mocker, client): """ Given: An Azure client and webapp authentication update parameters. When: The update_webapp_auth method is called. Then: The function should make the correct API call and return updated settings. """ # Setup mock response enabled = True mock_response = {"name": "authsettings", "properties": {"enabled": enabled}} mocker.patch.object(client, "http_request", return_value=mock_response) # Call the function result = client.update_webapp_auth( name="test-webapp", subscription_id="sub-id", resource_group_name="test-rg", enabled=enabled ) # Verify correct API call expected_url = ( f"{PREFIX_URL_AZURE}sub-id/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-webapp/config/authsettings" ) client.http_request.assert_called_once_with( method="PUT", full_url=expected_url, json_data={"properties": {"enabled": True}}, params={"api-version": WEBAPP_API_VERSION}, ) # Verify response assert result == mock_response def test_azure_client_flexible_server_param_set_success(mocker, client): """ Given: An Azure client and valid MySQL flexible server parameters. When: The flexible_server_param_set method is called. Then: The function should make the correct API call with proper parameters and return the response. """ # Setup mock response mock_response = { "name": "require_secure_transport", "id": ( "/subscriptions/sub-id/resourceGroups/test-rg/providers/Microsoft.DBforMySQL/flexibleServers/test-mysql/" "configurations/require_secure_transport" ), "properties": {"value": "ON", "source": "user-override", "description": "Whether to require SSL connections"}, } mocker.patch.object(client, "http_request", return_value=mock_response) # Call the function result = client.flexible_server_param_set( server_name="test-mysql", configuration_name="require_secure_transport", subscription_id="sub-id", resource_group_name="test-rg", source="user-override", value="ON", ) # Verify correct API call was made expected_url = ( f"{PREFIX_URL_AZURE}sub-id/resourceGroups/test-rg/providers/Microsoft.DBforMySQL/flexibleServers/test-mysql/" "configurations/require_secure_transport" ) client.http_request.assert_called_once_with( method="PUT", full_url=expected_url, json_data={"properties": {"source": "user-override", "value": "ON"}}, params={"api-version": FLEXIBLE_API_VERSION}, ) # Verify response assert result == mock_response assert result["name"] == "require_secure_transport" assert result["properties"]["value"] == "ON" assert result["properties"]["source"] == "user-override" def test_set_webapp_config(mocker, client): """ Given: An Azure client and arguments for setting webapp configuration. When: The set_webapp_config method is called. Then: The method should make the correct HTTP request with proper parameters and handle the response. """ # Mock arguments name = "test-webapp" subscription_id = "12345678-1234-1234-1234-123456789012" resource_group_name = "test-resource-group" http20_enabled = "true" remote_debugging_enabled = "false" min_tls_version = "1.2" # Mock response mock_response = { "id": ( f"/subscriptions/{subscription_id}/resourceGroups/{resource_group_name}/providers/Microsoft.Web/sites/{name}/config/" "web" ), "name": "web", "properties": {"http20Enabled": True, "remoteDebuggingEnabled": False, "minTlsVersion": "1.2"}, } # Mock the client's http_request method mocker.patch.object(client, "http_request", return_value=mock_response) # Call the method result = client.set_webapp_config( name=name, subscription_id=subscription_id, resource_group_name=resource_group_name, http20_enabled=http20_enabled, remote_debugging_enabled=remote_debugging_enabled, min_tls_version=min_tls_version, ) # Verify the HTTP request was called with correct parameters expected_url = ( f"https://management.azure.com/subscriptions/{subscription_id}/resourceGroups/{resource_group_name}" f"/providers/Microsoft.Web/sites/{name}/config/web" ) expected_data = { "properties": { "http20Enabled": http20_enabled, "remoteDebuggingEnabled": remote_debugging_enabled, "minTlsVersion": min_tls_version, } } expected_params = {"api-version": "2024-04-01"} client.http_request.assert_called_once_with( method="PATCH", full_url=expected_url, json_data=expected_data, params=expected_params ) # Verify the result matches the mock response assert result == mock_response def test_get_webapp_auth_error_handling(mocker, client): """ Given: An Azure client and arguments for getting webapp authentication settings. When: The get_webapp_auth method is called and an exception occurs. Then: The method should call handle_azure_error with appropriate parameters. """ # Mock arguments name = "test-webapp" subscription_id = "12345678-1234-1234-1234-123456789012" resource_group_name = "test-resource-group" # Mock exception mock_exception = Exception("404 Not Found") # Mock the client's methods mocker.patch.object(client, "http_request", side_effect=mock_exception) mocker.patch.object(client, "handle_azure_error", side_effect=ValueError("Web App not found")) # Call the method and expect it to raise an exception with pytest.raises(ValueError, match="Web App not found"): client.get_webapp_auth(name=name, subscription_id=subscription_id, resource_group_name=resource_group_name) # Verify handle_azure_error was called with correct parameters client.handle_azure_error.assert_called_once_with( e=mock_exception, resource_name=name, api_function_name="get_webapp_auth", resource_type="Web App", subscription_id=subscription_id, resource_group_name=resource_group_name, ) def test_update_webapp_auth_error_handling(mocker, client): """ Given: An Azure client and arguments for updating webapp authentication settings. When: The update_webapp_auth method is called and an exception occurs. Then: The method should call handle_azure_error with appropriate parameters. """ # Mock arguments name = "test-webapp" enabled = True subscription_id = "12345678-1234-1234-1234-123456789012" resource_group_name = "test-resource-group" # Mock exception mock_exception = Exception("403 Forbidden") # Mock the client's methods mocker.patch.object(client, "http_request", side_effect=mock_exception) mocker.patch.object(client, "handle_azure_error", side_effect=DemistoException("Insufficient permissions")) # Call the method and expect it to raise an exception with pytest.raises(DemistoException, match="Insufficient permissions"): client.update_webapp_auth( name=name, subscription_id=subscription_id, resource_group_name=resource_group_name, enabled=enabled ) # Verify handle_azure_error was called with correct parameters client.handle_azure_error.assert_called_once_with( e=mock_exception, resource_name=name, api_function_name="update_webapp_auth", resource_type="Web App", subscription_id=subscription_id, resource_group_name=resource_group_name, ) def test_flexible_server_param_set(mocker, client): """ Given: An Azure client and arguments for setting MySQL flexible server parameters. When: The flexible_server_param_set method is called. Then: The method should make the correct HTTP request with proper parameters. """ # Mock arguments server_name = "test-mysql-server" configuration_name = "max_connections" subscription_id = "12345678-1234-1234-1234-123456789012" resource_group_name = "test-resource-group" source = "user-override" value = "1000" # Mock response mock_response = { "id": ( f"/subscriptions/{subscription_id}/resourceGroups/{resource_group_name}/providers/Microsoft.DBforMySQL/" f"flexibleServers/{server_name}/configurations/{configuration_name}" ), "name": configuration_name, "properties": {"source": source, "value": value}, } # Mock the client's http_request method mocker.patch.object(client, "http_request", return_value=mock_response) # Call the method result = client.flexible_server_param_set( server_name=server_name, configuration_name=configuration_name, subscription_id=subscription_id, resource_group_name=resource_group_name, source=source, value=value, ) # Verify the HTTP request was called with correct parameters expected_url = ( f"https://management.azure.com/subscriptions/{subscription_id}/resourceGroups/{resource_group_name}" f"/providers/Microsoft.DBforMySQL/flexibleServers/{server_name}/configurations/{configuration_name}" ) expected_data = {"properties": {"source": source, "value": value}} expected_params = {"api-version": "2023-12-30"} client.http_request.assert_called_once_with( method="PUT", full_url=expected_url, json_data=expected_data, params=expected_params ) # Verify the result matches the mock response assert result == mock_response def test_flexible_server_param_set_error_handling(mocker, client): """ Given: An Azure client and arguments for setting MySQL flexible server parameters. When: The flexible_server_param_set method is called and an exception occurs. Then: The method should call handle_azure_error with appropriate parameters. """ # Mock arguments server_name = "test-mysql-server" configuration_name = "max_connections" subscription_id = "12345678-1234-1234-1234-123456789012" resource_group_name = "test-resource-group" source = "user-override" value = "1000" # Mock exception mock_exception = Exception("404 Not Found") # Mock the client's methods mocker.patch.object(client, "http_request", side_effect=mock_exception) mocker.patch.object(client, "handle_azure_error", side_effect=ValueError("MySQL Flexible Server Configuration not found")) # Call the method and expect it to raise an exception with pytest.raises(ValueError, match="MySQL Flexible Server Configuration not found"): client.flexible_server_param_set( server_name=server_name, configuration_name=configuration_name, subscription_id=subscription_id, resource_group_name=resource_group_name, source=source, value=value, ) # Verify handle_azure_error was called with correct parameters client.handle_azure_error.assert_called_once_with( e=mock_exception, resource_name=f"{server_name}/{configuration_name}", resource_type="MySQL Flexible Server Configuration", api_function_name="flexible_server_param_set", subscription_id=subscription_id, resource_group_name=resource_group_name, ) def test_get_monitor_log_profile(mocker, client): """ Given: An Azure client and arguments for getting a monitor log profile. When: The get_monitor_log_profile method is called. Then: The method should make the correct HTTP request and return the log profile. """ # Mock arguments subscription_id = "12345678-1234-1234-1234-123456789012" log_profile_name = "test-log-profile" # Mock response mock_response = { "id": f"/subscriptions/{subscription_id}/providers/Microsoft.Insights/logprofiles/{log_profile_name}", "name": log_profile_name, "location": "global", "properties": { "storageAccountId": "/subscriptions/test/resourceGroups/test/providers/Microsoft.Storage/storageAccounts/test", "retentionPolicy": {"enabled": True, "days": 30}, }, } # Mock the client's http_request method mocker.patch.object(client, "http_request", return_value=mock_response) # Call the method result = client.get_monitor_log_profile(subscription_id=subscription_id, log_profile_name=log_profile_name) # Verify the HTTP request was called with correct parameters expected_url = f"https://management.azure.com/subscriptions/{subscription_id}/providers/Microsoft.Insights/logprofiles/{log_profile_name}" expected_params = {"api-version": "2016-03-01"} client.http_request.assert_called_once_with(method="GET", full_url=expected_url, params=expected_params) # Verify the result matches the mock response assert result == mock_response def test_get_monitor_log_profile_error_handling(mocker, client): """ Given: An Azure client and arguments for getting a monitor log profile. When: The get_monitor_log_profile method is called and an exception occurs. Then: The method should call handle_azure_error with appropriate parameters. """ # Mock arguments subscription_id = "12345678-1234-1234-1234-123456789012" log_profile_name = "test-log-profile" # Mock exception mock_exception = Exception("404 Not Found") # Mock the client's methods mocker.patch.object(client, "http_request", side_effect=mock_exception) mocker.patch.object(client, "handle_azure_error", side_effect=ValueError("Monitor Log Profile not found")) # Call the method and expect it to raise an exception with pytest.raises(ValueError, match="Monitor Log Profile not found"): client.get_monitor_log_profile(subscription_id=subscription_id, log_profile_name=log_profile_name) # Verify handle_azure_error was called with correct parameters client.handle_azure_error.assert_called_once_with( e=mock_exception, resource_name=log_profile_name, api_function_name="get_monitor_log_profile", resource_type="Monitor Log Profile", subscription_id=subscription_id, resource_group_name=None, ) def test_format_rule(): """ Given: rule data and rule name Then: Command outputs is returned as expected and flattens the `properties` field. """ from Azure import format_rule rule = util_load_json("test_data/get_rule_response.json") cr = format_rule(rule_json=rule, security_rule_name="RuleName") assert cr.raw_response["name"] == "wow" assert cr.raw_response["sourceAddressPrefix"] == "3.2.3.2" assert "### Rules RuleName" in cr.readable_output def test_nsg_public_ip_addresses_list_command(mocker): """ Given: An Azure client mock and the list_public_ip_addresses_response.json file. When: nsg_public_ip_addresses_list_command is called 1. With a limit of 2 (all_results=False). 2. With all_results=True. Then: 1. It should return only 2 results when limited. 2. It should return all results when all_results=True. 3. The results should contain expected fields such as name, id, fqdn. 4. The etag field should be cleaned up (first 3 chars and last char removed). """ from Azure import nsg_public_ip_addresses_list_command mock_response = util_load_json("test_data/list_public_ip_addresses_response.json") mock_client = mocker.Mock() mock_client.list_public_ip_addresses_request.return_value = mock_response params = {"subscription_id": "subid", "resource_group_name": "rg1"} args = {"limit": "2", "all_results": "false"} result: CommandResults = nsg_public_ip_addresses_list_command(mock_client, params, args) assert isinstance(result, CommandResults) assert result.outputs_prefix == "Azure.VirtualNetworks.PublicIPAddresses" assert result.outputs_key_field == "id" assert len(result.outputs) == 2 assert "name" in result.outputs[0] assert "id" in result.outputs[0] # "123etag3" should become "etag" after [3:-1] first_item_with_etag = result.outputs[0] # First item has etag "123etag3" if first_item_with_etag.get("etag"): assert first_item_with_etag.get("etag") == "etag" args = {"all_results": "true"} result_all: CommandResults = nsg_public_ip_addresses_list_command(mock_client, params, args) assert isinstance(result_all, CommandResults) assert len(result_all.outputs) == len(mock_response["value"]) # Should be 3 items fqdn_values = [ out.get("properties", {}).get("dnsSettings", {}).get("fqdn") for out in result_all.outputs if out.get("properties", {}).get("dnsSettings", {}).get("fqdn") ] assert len(fqdn_values) == 2 assert "testlbl.westus.cloudapp.azure.com" in fqdn_values assert "testlbl.hxdwgjcdfgbhgebs.eastus.sysgen.cloudapp.azure.com" in fqdn_values # Check readable_output is generated assert result_all.readable_output assert "Public IP Addresses List" in result_all.readable_output def test_nsg_public_ip_addresses_list_command_deprecated(mocker): """ Given: An Azure client mock and the list_public_ip_addresses_response.json file using the deprecated command. When: nsg_public_ip_addresses_list_command is called with the deprecated command name. Then: It should return the public IP addresses data with the deprecated prefix. """ from Azure import nsg_public_ip_addresses_list_command mock_response = util_load_json("test_data/list_public_ip_addresses_response.json") mock_client = mocker.Mock() mock_client.list_public_ip_addresses_request.return_value = mock_response params = {"subscription_id": "subid", "resource_group_name": "rg1"} args = {"limit": "2", "all_results": "false"} mocker.patch("Azure.demisto.command", return_value="azure-nsg-public-ip-addresses-list") result: CommandResults = nsg_public_ip_addresses_list_command(mock_client, params, args) assert isinstance(result, CommandResults) assert result.outputs_prefix == "Azure.NSGPublicIPAddress" assert result.outputs_key_field == "id" assert len(result.outputs) == 2 assert "name" in result.outputs[0] assert "id" in result.outputs[0] def test_nsg_network_interfaces_list_command(mocker): """ Given: An Azure client mock and the list_networks_interfaces_response.json file. When: nsg_network_interfaces_list_command is called 1. With a limit of 1 (all_results=False). 2. With all_results=True. Then: 1. It should return only 1 result when limited. 2. It should return all results when all_results=True. 3. The results should contain expected fields such as name, id. 4. The etag field should be cleaned up (first 3 chars and last char removed). """ from Azure import nsg_network_interfaces_list_command mock_response = util_load_json("test_data/list_networks_interfaces_response.json") mock_client = mocker.Mock() mock_client.list_networks_interfaces_request.return_value = mock_response params = {"subscription_id": "subid", "resource_group_name": "rg1"} # --- Case 1: with limit=1 --- args = {"limit": "1", "all_results": "false"} result: CommandResults = nsg_network_interfaces_list_command(mock_client, params, args) assert result.outputs_prefix == "Azure.VirtualNetworks.NetworkInterfaces" assert result.outputs_key_field == "id" assert len(result.outputs) == 1 first = result.outputs[0] assert first["name"] == "test-nic" assert first["id"] == "/subscriptions/subid/resourceGroups/rg1/providers/Microsoft.Network/networkInterfaces/test-nic" # --- Case 2: with all_results=True --- args = {"all_results": "true"} result_all: CommandResults = nsg_network_interfaces_list_command(mock_client, params, args) assert isinstance(result_all, CommandResults) assert len(result_all.outputs) == len(mock_response["value"]) # Should be 2 items assert len(result_all.outputs) == 2 names = [item["name"] for item in result_all.outputs] assert "test-nic" in names assert "test-nic2" in names for item in result_all.outputs: if item.get("etag"): assert item["etag"] == "etag" assert result_all.readable_output assert "Network Interfaces List" in result_all.readable_output def test_nsg_network_interfaces_list_command_deprecated(mocker): """ Given: An Azure client mock and the list_networks_interfaces_response.json file using the deprecated command. When: nsg_network_interfaces_list_command is called with the deprecated command name. Then: It should return the network interfaces data with the deprecated prefix. """ from Azure import nsg_network_interfaces_list_command mock_response = util_load_json("test_data/list_networks_interfaces_response.json") mock_client = mocker.Mock() mock_client.list_networks_interfaces_request.return_value = mock_response params = {"subscription_id": "subid", "resource_group_name": "rg1"} args = {"limit": "1", "all_results": "false"} mocker.patch("Azure.demisto.command", return_value="azure-nsg-network-interfaces-list") result: CommandResults = nsg_network_interfaces_list_command(mock_client, params, args) assert result.outputs_prefix == "Azure.NSGNetworkInterfaces" assert result.outputs_key_field == "id" assert len(result.outputs) == 1 def test_nsg_resource_group_list_command(mocker): """ Given: An Azure client mock and the list_resource_groups_response.json file. When: nsg_resource_group_list_command is called 1. With a limit of 1. 2. Without limit (default). Then: 1. It should respect the limit argument. 2. It should return the resource group data with expected fields. 3. It should generate proper readable output. """ from Azure import nsg_resource_group_list_command mock_response = util_load_json("test_data/list_resource_groups_response.json") mock_client = mocker.Mock() mock_client.list_resource_groups_request.return_value = mock_response params = {"subscription_id": "subscription1"} # --- Case 1: with limit=1 --- args = {"limit": "1"} result: CommandResults = nsg_resource_group_list_command(mock_client, params, args) # Check that client method was called with correct parameters including limit mock_client.list_resource_groups_request.assert_called_with(subscription_id="subscription1", filter_by_tag="", limit="1") assert isinstance(result, CommandResults) assert result.outputs_prefix == "Azure.ResourceManagement.ResourceGroups" assert result.outputs_key_field == "id" assert len(result.outputs) == 1 first = result.outputs[0] assert first["name"] == "resourceGroup1" assert first["location"] == "centralus" assert "tags" in first assert "properties" in first assert first["properties"]["provisioningState"] == "Succeeded" # Check readable_output is generated assert result.readable_output assert "Resource Groups List" in result.readable_output # --- Case 2: no limit (default) --- args = {} result_default: CommandResults = nsg_resource_group_list_command(mock_client, params, args) assert isinstance(result_default, CommandResults) assert len(result_default.outputs) == 1 assert result_default.outputs[0]["id"] == "/subscriptions/subscription1/resourceGroups/resourceGroup1" def test_nsg_resource_group_list_command_deprecated(mocker): """ Given: An Azure client mock and the list_resource_groups_response.json file using the deprecated command. When: nsg_resource_group_list_command is called with the deprecated command name. Then: It should return the resource group data with the deprecated prefix. """ from Azure import nsg_resource_group_list_command mock_response = util_load_json("test_data/list_resource_groups_response.json") mock_client = mocker.Mock() mock_client.list_resource_groups_request.return_value = mock_response params = {"subscription_id": "subscription1"} args = {"limit": "1"} mocker.patch("Azure.demisto.command", return_value="azure-nsg-resource-group-list") result: CommandResults = nsg_resource_group_list_command(mock_client, params, args) assert isinstance(result, CommandResults) assert result.outputs_prefix == "Azure.NSGResourceGroup" assert result.outputs_key_field == "id" assert len(result.outputs) == 1 first = result.outputs[0] assert first["name"] == "resourceGroup1" assert first["location"] == "centralus" def test_nsg_security_rule_create_command(mocker): """ Given: An Azure client mock and arguments for creating a security rule. When: nsg_security_rule_create_command is called. Then: 1. It should call create_or_update_rule with correct properties. 2. The returned CommandResults should include the created rule data. 3. The etag should be cleaned up. 4. Readable output should be generated. """ from Azure import nsg_security_rule_create_command mock_response = util_load_json("test_data/create_or_update_rule_response.json") mock_client = mocker.Mock() mock_client.create_or_update_rule.return_value = mock_response params = {"subscription_id": "subid", "resource_group_name": "rg1"} args = { "security_group_name": "testnsg", "security_rule_name": "rule1", "action": "Deny", "direction": "Outbound", "priority": 100, "protocol": "Any", "source": "10.0.0.0/8", "destination": "11.0.0.0/8", "destination_ports": "8080", } result: CommandResults = nsg_security_rule_create_command(mock_client, params, args) # --- Check the properties passed to create_or_update_rule --- expected_properties = { "protocol": "*", "access": "Deny", "priority": 100, "direction": "Outbound", "sourcePortRange": "*", "destinationPortRange": "8080", "sourceAddressPrefix": "10.0.0.0/8", "destinationAddressPrefix": "11.0.0.0/8", } mock_client.create_or_update_rule.assert_called_once_with( security_group="testnsg", rule_name="rule1", properties=expected_properties, subscription_id="subid", resource_group_name="rg1", ) # --- Check the returned CommandResults --- assert isinstance(result, CommandResults) assert result.outputs_prefix == "Azure.VirtualNetworks.SecurityRules" assert result.outputs_key_field == "id" assert result.outputs["name"] == "rule1" # Check that etag is cleaned up if result.outputs.get("etag"): assert result.outputs.get("etag") == "etag" # Check readable_output is generated assert result.readable_output assert f"The security rule {args['security_rule_name']} was created successfully" in result.readable_output def test_nsg_security_rule_create_command_deprecated(mocker): """ Given: An Azure client mock and arguments for creating a security rule using the deprecated command. When: nsg_security_rule_create_command is called with the deprecated command name. Then: It should return the created rule data with the deprecated prefix. """ from Azure import nsg_security_rule_create_command mock_response = util_load_json("test_data/create_or_update_rule_response.json") mock_client = mocker.Mock() mock_client.create_or_update_rule.return_value = mock_response params = {"subscription_id": "subid", "resource_group_name": "rg1"} args = { "security_group_name": "testnsg", "security_rule_name": "rule1", "action": "Deny", "direction": "Outbound", "priority": 100, "protocol": "Any", "source": "10.0.0.0/8", "destination": "11.0.0.0/8", "destination_ports": "8080", } mocker.patch("Azure.demisto.command", return_value="azure-nsg-security-rule-create") result: CommandResults = nsg_security_rule_create_command(mock_client, params, args) assert isinstance(result, CommandResults) assert result.outputs_prefix == "Azure.NSGRule" assert result.outputs_key_field == "id" assert result.outputs["name"] == "rule1" def test_nsg_security_rule_get_command(mocker): """ Given: An Azure client mock and a security rule JSON. When: nsg_security_rule_get_command is called. Then: 1. It should call client.get_rule with correct arguments. 2. The returned CommandResults should contain the rule data. 3. The etag should be cleaned up. 4. Readable output should be generated. """ from Azure import nsg_security_rule_get_command mock_rule = util_load_json("test_data/get_rule_response.json") mock_client = mocker.Mock() mock_client.get_rule.return_value = mock_rule params = {"subscription_id": "subid", "resource_group_name": "rg1"} args = {"security_group_name": "testnsg", "security_rule_name": "wow"} result: CommandResults = nsg_security_rule_get_command(mock_client, params, args) # Check that get_rule was called correctly mock_client.get_rule.assert_called_once_with( security_group="testnsg", rule_name="wow", subscription_id="subid", resource_group_name="rg1", ) # Check the returned CommandResults assert isinstance(result, CommandResults) assert result.outputs_prefix == "Azure.VirtualNetworks.SecurityRules" assert result.outputs_key_field == "id" assert result.outputs == mock_rule # Check that etag is cleaned up if result.outputs.get("etag"): assert result.outputs["etag"] == "etag" # Check readable_output is generated assert result.readable_output assert f"Rule {args['security_rule_name']}" in result.readable_output def test_nsg_security_rule_get_command_deprecated(mocker): """ Given: An Azure client mock and a security rule JSON using the deprecated command. When: nsg_security_rule_get_command is called with the deprecated command name. Then: It should return the rule data with the deprecated prefix. """ from Azure import nsg_security_rule_get_command mock_rule = util_load_json("test_data/get_rule_response.json") mock_client = mocker.Mock() mock_client.get_rule.return_value = mock_rule params = {"subscription_id": "subid", "resource_group_name": "rg1"} args = {"security_group_name": "testnsg", "security_rule_name": "wow"} mocker.patch("Azure.demisto.command", return_value="azure-nsg-security-rule-get") result: CommandResults = nsg_security_rule_get_command(mock_client, params, args) assert isinstance(result, CommandResults) assert result.outputs_prefix == "Azure.NSGRule" assert result.outputs_key_field == "id" assert result.outputs["name"] == "wow" def test_nsg_security_groups_list_command(mocker): """ Given: An Azure client mock and the list_network_security_groups_response.json file. When: nsg_security_groups_list_command is called. Then: 1. It should call client.list_network_security_groups with correct parameters. 2. The etag fields should be cleaned up for both groups and default security rules. 3. The CommandResults should have correct outputs and readable_output. """ from Azure import nsg_security_groups_list_command mock_response = util_load_json("test_data/list_network_security_groups_response.json") mock_client = mocker.Mock() mock_client.list_network_security_groups.return_value = mock_response params = {"subscription_id": "subid", "resource_group_name": "rg1"} args = {} result: CommandResults = nsg_security_groups_list_command(mock_client, params, args) mock_client.list_network_security_groups.assert_called_once_with(subscription_id="subid", resource_group_name="rg1") assert isinstance(result, CommandResults) assert result.outputs_prefix == "Azure.VirtualNetworks.SecurityGroups" assert result.outputs_key_field == "id" assert len(result.outputs) == len(mock_response["value"]) # Check that etag fields are cleaned up for group in result.outputs: if group.get("etag"): assert group["etag"] == "etag" for rule in group.get("defaultSecurityRules", []): if rule.get("etag"): assert rule["etag"] == "etag" assert "name" in group assert "id" in group assert "location" in group # Check readable_output is generated assert result.readable_output assert "Network Security Groups" in result.readable_output # The readable_output should contain the NSG names for group in result.outputs: assert group["name"] in result.readable_output def test_nsg_security_groups_list_command_deprecated(mocker): """ Given: An Azure client mock and the list_network_security_groups_response.json file using the deprecated command. When: nsg_security_groups_list_command is called with the deprecated command name. Then: It should return the security groups data with the deprecated prefix. """ from Azure import nsg_security_groups_list_command mock_response = util_load_json("test_data/list_network_security_groups_response.json") mock_client = mocker.Mock() mock_client.list_network_security_groups.return_value = mock_response params = {"subscription_id": "subid", "resource_group_name": "rg1"} args = {} mocker.patch("Azure.demisto.command", return_value="azure-nsg-security-groups-list") result: CommandResults = nsg_security_groups_list_command(mock_client, params, args) assert isinstance(result, CommandResults) assert result.outputs_prefix == "Azure.NSGSecurityGroup" assert result.outputs_key_field == "id" assert len(result.outputs) == len(mock_response["value"]) def test_nsg_security_rule_delete_command(mocker): """ Given: An Azure client mock and various scenarios. When: nsg_security_rule_delete_command is called. Then: 1. It should call client.delete_rule with correct arguments for valid inputs. 2. It should return appropriate messages based on status codes (200=success, 202=async, 204=not found). 3. It should call return_error when required parameters are missing or empty. """ from Azure import nsg_security_rule_delete_command mock_client = mocker.Mock() params = {"subscription_id": "subid", "resource_group_name": "rg1"} mock_response = mocker.Mock() mock_response.status_code = 202 mock_client.delete_rule.return_value = mock_response args = {"security_group_name": "testnsg", "security_rule_name": "testrule"} result = nsg_security_rule_delete_command(mock_client, params, args) mock_client.delete_rule.assert_called_with( security_group_name="testnsg", security_rule_name="testrule", subscription_id="subid", resource_group_name="rg1", ) assert isinstance(result, CommandResults) assert "was accepted and the operation will complete asynchronously" in result.readable_output mock_response.status_code = 200 result = nsg_security_rule_delete_command(mock_client, params, args) assert "was successfully deleted" in result.readable_output mock_response.status_code = 204 result = nsg_security_rule_delete_command(mock_client, params, args) assert "was not found" in result.readable_output def test_get_permissions_from_api_function_name(mocker): """ Given: An API function name and an error message. When: get_permissions_from_api_function_name is called. Then: 1. It should return the matching permission found in the error message. 2. It should return None if no permission is found in the error message. 3. It should be case-insensitive when matching. 4. Multiple permissions in function, return all matched permissions. """ from Azure import get_permissions_from_api_function_name # Test case 1: Permission found in error message api_function_name = "list_networks_interfaces_request" error_msg = "Access denied. Missing permission: Microsoft.Network/networkInterfaces/read" result = get_permissions_from_api_function_name(api_function_name, error_msg) assert result == ["Microsoft.Network/networkInterfaces/read"] # Test case 2: Case-insensitive matching error_msg_upper = "Access denied. Missing permission: MICROSOFT.NETWORK/NETWORKINTERFACES/READ" result = get_permissions_from_api_function_name(api_function_name, error_msg_upper) assert result == ["Microsoft.Network/networkInterfaces/read"] # Test case 3: No permission found in error message error_msg_no_match = "Some unrelated error message" result = get_permissions_from_api_function_name(api_function_name, error_msg_no_match) assert result == [] # Test case 4: Multiple permissions in function, return all matched permissions api_function_name_multi = "acr_update" # Has both read and write permissions error_msg_write = ( "Missing Microsoft.ContainerRegistry/registries/read, Microsoft.ContainerRegistry/registries/write permissions" ) result = get_permissions_from_api_function_name(api_function_name_multi, error_msg_write) assert result == ["Microsoft.ContainerRegistry/registries/read", "Microsoft.ContainerRegistry/registries/write"] def test_get_permissions_from_required_role_permissions_list(mocker): """ Given: An error message. When: get_permissions_from_required_role_permissions_list is called. Then: 1. It should return the first matching permission found in the error message. 2. It should return None if no permission is found in the error message. 3. It should be case-insensitive when matching. 4. It should search through all required role permissions. """ from Azure import get_permissions_from_required_role_permissions_list # Test case 1: Permission found in error message error_msg = "Access denied. Missing permission: Microsoft.Network/networkSecurityGroups/read" result = get_permissions_from_required_role_permissions_list(error_msg) assert result == ["Microsoft.Network/networkSecurityGroups/read"] # Test case 2: Case-insensitive matching error_msg_mixed_case = "Access denied. Missing permission: microsoft.network/networksecuritygroups/READ" result = get_permissions_from_required_role_permissions_list(error_msg_mixed_case) assert result == ["Microsoft.Network/networkSecurityGroups/read"] # Test case 3: No permission found in error message error_msg_no_match = "Some completely unrelated error message without permissions" result = get_permissions_from_required_role_permissions_list(error_msg_no_match) assert result == ["N/A"] # Test case 4: Empty error message error_msg_empty = "" result = get_permissions_from_required_role_permissions_list(error_msg_empty) assert result == ["N/A"] def test_handle_azure_error_forbidden_text_match(mocker, client): """ Given: An Azure client and an error containing "forbidden" text. When: The handle_azure_error method is called. Then: The function should trigger permission error handling. """ mock_get_permissions_from_api = mocker.patch( "Azure.get_permissions_from_api_function_name", return_value=["Microsoft.ContainerRegistry/registries/read"] ) mock_get_permissions_from_required = mocker.patch("Azure.get_permissions_from_required_role_permissions_list") mock_return_multiple_permissions_error = mocker.patch("Azure.return_multiple_permissions_error") error = Exception("Access forbidden - insufficient privileges") resource_name = "test" resource_type = "test resource" subscription_id = "test-sub" client.handle_azure_error( e=error, resource_name=resource_name, resource_type=resource_type, api_function_name="acr_update", subscription_id=subscription_id, ) mock_get_permissions_from_api.assert_called_once_with("acr_update", "access forbidden - insufficient privileges") mock_get_permissions_from_required.assert_not_called() expected_error_entries = [ { "account_id": "test-sub", "message": "access forbidden - insufficient privileges", "name": "Microsoft.ContainerRegistry/registries/read", } ] mock_return_multiple_permissions_error.assert_called_once_with(expected_error_entries) def test_handle_azure_error_permission_error_no_permissions_found(mocker, client): """ Given: An Azure client and a permission error where no permissions are found. When: The handle_azure_error method is called. Then: The function should call return_multiple_permissions_error with empty list. """ mock_get_permissions_from_api = mocker.patch("Azure.get_permissions_from_api_function_name", return_value=None) mock_get_permissions_from_required = mocker.patch( "Azure.get_permissions_from_required_role_permissions_list", return_value=["N/A"] ) mock_return_multiple_permissions_error = mocker.patch("Azure.return_multiple_permissions_error") error = Exception("403 Forbidden") resource_name = "test-resource" resource_type = "Unknown Resource" client.handle_azure_error( e=error, resource_name=resource_name, resource_type=resource_type, api_function_name="unknown_function", ) mock_get_permissions_from_api.assert_not_called() mock_get_permissions_from_required.assert_called_once() mock_return_multiple_permissions_error.assert_called_once_with( [{"account_id": None, "message": str(error).lower(), "name": "N/A"}] ) def test_handle_azure_error_permission_error_multiple_permissions(mocker, client): """ Given: An Azure client and a permission error with multiple permissions found. When: The handle_azure_error method is called. Then: The function should call return_multiple_permissions_error with all permissions. """ mock_return_multiple_permissions_error = mocker.patch("Azure.return_multiple_permissions_error") error = Exception("401 Unauthorized missing Microsoft.Storage/storageAccounts/read") resource_name = "test-storage" resource_type = "Storage Account" subscription_id = "sub-123" resource_group_name = "rg-test" client.handle_azure_error( e=error, resource_name=resource_name, resource_type=resource_type, api_function_name="storage_account_update_request", subscription_id=subscription_id, resource_group_name=resource_group_name, ) expected_error_entries = [ { "account_id": "sub-123", "message": "401 unauthorized missing microsoft.storage/storageaccounts/read", "name": "Microsoft.Storage/storageAccounts/read", } ] mock_return_multiple_permissions_error.assert_called_once_with(expected_error_entries) def test_storage_blob_service_properties_get_command(mocker): """ Given: An Azure client mock and the get_blob_service_properties.json file. When: storage_blob_service_properties_get_command is called. Then: 1. It should call client.storage_blob_service_properties_get_request with correct parameters. 2. It should extract subscription_id, resource_group, and account_name from the response ID. 3. The CommandResults should have correct outputs, readable_output, and metadata. """ from Azure import storage_blob_service_properties_get_command mock_response = util_load_json("test_data/get_blob_service_properties.json") mock_client = mocker.Mock() mock_client.storage_blob_service_properties_get_request.return_value = mock_response mocker.patch("Azure.demisto.command", return_value="azure-storage-blob-service-properties-get") params = {"subscription_id": "subid", "resource_group_name": "rg1"} args = {"account_name": "teststorage"} result: CommandResults = storage_blob_service_properties_get_command(mock_client, params, args) mock_client.storage_blob_service_properties_get_request.assert_called_once_with( account_name="teststorage", resource_group_name="rg1", subscription_id="subid" ) assert isinstance(result, CommandResults) assert result.outputs_prefix == "Azure.StorageBlobServiceProperties" assert result.outputs_key_field == "id" assert result.outputs == mock_response assert result.raw_response == mock_response assert "Azure Storage Blob Service Properties" in result.readable_output assert "default" in result.readable_output assert "sto8607" in result.readable_output assert "subscription-id" in result.readable_output assert "res4410" in result.readable_output assert "true" in result.readable_output.lower() expected_headers = [ "Name", "Account Name", "Subscription ID", "Resource Group", "Change Feed", "Delete Retention Policy", "Versioning", ] for header in expected_headers: assert header in result.readable_output def test_storage_blob_containers_update_command(mocker): """ Given: An Azure client mock and the update_blob_container.json file. When: storage_blob_containers_update_command is called. Then: 1. It should call client.storage_blob_containers_create_update_request with correct parameters and PATCH method. 2. It should extract subscription_id, resource_group, and account_name from the response ID. 3. The CommandResults should have correct outputs, readable_output, and metadata. """ from Azure import storage_blob_containers_update_command mock_response = util_load_json("test_data/update_blob_container.json") mock_client = mocker.Mock() mock_client.storage_blob_containers_create_update_request.return_value = mock_response params = {"subscription_id": "subid", "resource_group_name": "rg1"} args = {"account_name": "teststorage", "container_name": "testcontainer"} result: CommandResults = storage_blob_containers_update_command(mock_client, params, args) mock_client.storage_blob_containers_create_update_request.assert_called_once_with( subscription_id="subid", resource_group_name="rg1", args=args, method="PATCH" ) assert isinstance(result, CommandResults) assert result.outputs_prefix == "Azure.Storage.BlobContainers" assert result.outputs_key_field == "id" assert result.outputs == mock_response assert result.raw_response == mock_response assert "Azure Storage Blob Containers Properties" in result.readable_output assert "container6185" in result.readable_output assert "sto328" in result.readable_output assert "subscription-id" in result.readable_output assert "res3376" in result.readable_output assert "Container" in result.readable_output expected_headers = ["Name", "Account Name", "Subscription ID", "Resource Group", "Public Access"] for header in expected_headers: assert header in result.readable_output def test_storage_blob_containers_update_command_deprecated(mocker): """ Given: An Azure client mock and the update_blob_container.json file using the deprecated command. When: storage_blob_containers_update_command is called with the deprecated command name. Then: It should return the updated container data with the deprecated prefix. """ from Azure import storage_blob_containers_update_command mock_response = util_load_json("test_data/update_blob_container.json") mock_client = mocker.Mock() mock_client.storage_blob_containers_create_update_request.return_value = mock_response params = {"subscription_id": "subid", "resource_group_name": "rg1"} args = {"account_name": "teststorage", "container_name": "testcontainer"} mocker.patch("Azure.demisto.command", return_value="azure-storage-blob-containers-update") result: CommandResults = storage_blob_containers_update_command(mock_client, params, args) assert isinstance(result, CommandResults) assert result.outputs_prefix == "Azure.StorageBlobContainer" assert result.outputs_key_field == "id" assert result.outputs == mock_response def test_extract_azure_resource_info(): """ Given: Various Azure resource ID formats. When: The extract_azure_resource_info function is called. Then: The function should correctly extract subscription_id, resource_group, and account_name components. """ # Test case 1: Complete Azure storage blob service resource ID resource_id = "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/teststorage/blobServices/default" # noqa: E501 subscription_id, resource_group, account_name = extract_azure_resource_info(resource_id) assert subscription_id == "12345678-1234-1234-1234-123456789012" assert resource_group == "test-rg" assert account_name == "teststorage" # Test case 2: Partial resource ID (only subscription and resource group) resource_id = "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Compute/virtualMachines/test-vm" # noqa: E501 subscription_id, resource_group, account_name = extract_azure_resource_info(resource_id) assert subscription_id == "12345678-1234-1234-1234-123456789012" assert resource_group == "test-rg" assert account_name is None # Test case 3: Empty string resource_id = "" subscription_id, resource_group, account_name = extract_azure_resource_info(resource_id) assert subscription_id is None assert resource_group is None assert account_name is None # Test case 4: Invalid format resource_id = "invalid-resource-id-format" subscription_id, resource_group, account_name = extract_azure_resource_info(resource_id) assert subscription_id is None assert resource_group is None assert account_name is None # Test case 5: Only subscription information resource_id = "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups" subscription_id, resource_group, account_name = extract_azure_resource_info(resource_id) assert subscription_id == "12345678-1234-1234-1234-123456789012" assert resource_group is None assert account_name is None # Test case 6: Complex names with hyphens and underscores resource_id = "/subscriptions/abcd-efgh-1234-5678-ijkl/resourceGroups/my-resource-group_v2/providers/Microsoft.Storage/storageAccounts/my_storage_account123/blobServices/default" # noqa: E501 subscription_id, resource_group, account_name = extract_azure_resource_info(resource_id) assert subscription_id == "abcd-efgh-1234-5678-ijkl" assert resource_group == "my-resource-group_v2" assert account_name == "my_storage_account123" # Test case 7: Storage account without blob services suffix resource_id = "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/teststorage" # noqa: E501 subscription_id, resource_group, account_name = extract_azure_resource_info(resource_id) assert subscription_id == "12345678-1234-1234-1234-123456789012" assert resource_group == "test-rg" assert account_name is None def test_storage_container_blob_create_command(mocker, client, mock_params): """ Given: An Azure client and a request to create a blob in a storage container. When: The storage_container_blob_create_command function is called with valid parameters. Then: The function should call the client's create_blob method and return a success message. """ # Mock arguments args = { "container_name": "testcontainer", "account_name": "testaccount", "file_entry_id": "test_file_entry_id", "blob_name": "test_blob.txt", } # Mock demisto.getFilePath mocker.patch.object(demisto, "getFilePath", return_value={"path": "/tmp/test_file.txt", "name": "test_file.txt"}) # Mock os.path.getsize mocker.patch("os.path.getsize", return_value=100) # Simulate a file size of 100 bytes # Mock builtins.open mock_file_content = b"This is a test file content." mocker.patch("builtins.open", mocker.mock_open(read_data=mock_file_content)) # Mock the client's storage_container_create_blob_request method mock_response = mocker.Mock(spec=Response) mock_response.status_code = 201 mock_response.text = "Blob created" mocker.patch.object(client, "storage_container_create_blob_request", return_value=mock_response) # Call the function result = Azure.storage_container_blob_create_command(client, mock_params, args) # Verify results assert isinstance(result, CommandResults) assert result.readable_output == "Blob test_blob.txt successfully created." assert result.raw_response is None def test_storage_container_property_get_command(mocker, client, mock_params): """ Given: An Azure client and a request to get storage container properties. When: The storage_container_property_get_command function is called with valid parameters. Then: 1. It should call client.get_storage_container_properties_request with correct parameters. 2. It should transform the response headers into the expected format. 3. The CommandResults should have correct outputs, readable_output, and raw_response. """ # Mock arguments args = {"container_name": "testcontainer", "account_name": "testaccount"} # Prepare the mocked HTTP response object with headers mock_response = mocker.Mock() # The CaseInsensitiveDict is what the requests library returns for headers raw_response_data = { "Content-Length": "0", "Etag": "0x8DB7F5589F2DC4A", "Last-Modified": "Wed, 14 Aug 2024 10:00:00 GMT", "Date": "Wed, 14 Aug 2024 10:05:00 GMT", "X-Ms-Request-Id": "req-id-12345", "X-Ms-Lease-Status": "unlocked", "X-Ms-Lease-State": "available", "X-Ms-Has-Immutability-Policy": "false", "X-Ms-Has-Legal-Hold": "false", } mock_response.headers = CaseInsensitiveDict(raw_response_data) # Configure the client method to return this mock response mocker.patch.object(client, "get_storage_container_properties_request", return_value=mock_response) # Call the function result = storage_container_property_get_command(client, mock_params, args) # Verify client.get_storage_container_properties_request was called with correct parameters client.get_storage_container_properties_request.assert_called_once_with("testaccount", "testcontainer") assert result.outputs_prefix == "Azure.Storage.Container" assert result.outputs_key_field == "ContainerName" def test_storage_container_create_command(mocker, client, mock_params): """ Given: An Azure client and a request to create a storage container. When: The storage_container_create_command function is called with valid parameters. Then: The function should call the client's create_storage_container_request method and return a success message. """ # Mock arguments args = {"container_name": "testcontainer", "account_name": "testaccount"} # Mock the client's create_storage_container_request method mock_response = mocker.Mock() mock_response.status_code = 201 mocker.patch.object(client, "create_storage_container_request", return_value=mock_response) # Call the function result = storage_container_create_command(client, mock_params, args) # Verify client.create_storage_container_request was called with correct parameters client.create_storage_container_request.assert_called_once_with("testcontainer", "testaccount") # Verify results assert isinstance(result, CommandResults) assert result.readable_output == f"Container {args['container_name']} successfully created." def test_storage_container_create_command_invalid_name(mocker, client, mock_params): """ Given: An Azure client and a request to create a storage container with an invalid name. When: The storage_container_create_command function is called with invalid container name. Then: The function should raise an exception about the invalid container name. """ # Mock arguments with invalid container name (uppercase not allowed) args = {"container_name": "TestContainer", "account_name": "testaccount"} # Call the function and expect an exception with pytest.raises(Exception) as excinfo: storage_container_create_command(client, mock_params, args) # Verify the exception message assert "The specified container name is invalid" in str(excinfo.value) def test_storage_container_delete_command_success(mocker, client, mock_params): """ Given: An Azure client and a request to delete a storage container. When: The storage_container_delete_command function is called with valid parameters. Then: The function should call the client's delete_storage_container_request method and return a success message. """ # Mock arguments args = {"container_name": "testcontainer", "account_name": "testaccount"} # Mock the client's delete_storage_container_request method mocker.patch.object(client, "delete_storage_container_request") # Call the function result = storage_container_delete_command(client, mock_params, args) # Verify client.delete_storage_container_request was called with correct parameters client.delete_storage_container_request.assert_called_once_with("testcontainer", "testaccount") # Verify results assert isinstance(result, CommandResults) assert result.readable_output == f"Container {args['container_name']} successfully deleted." def test_storage_container_delete_command_error(mocker, client, mock_params): """ Given: An Azure client and a request to delete a storage container that fails. When: The storage_container_delete_command function is called and the API call raises an exception. Then: The function should propagate the exception. """ # Mock arguments args = {"container_name": "testcontainer", "account_name": "testaccount"} # Mock the client's delete_storage_container_request method to raise an exception error_message = "Container not found or you don't have permission to delete it" mocker.patch.object(client, "delete_storage_container_request", side_effect=Exception(error_message)) # Call the function and expect an exception with pytest.raises(Exception) as excinfo: storage_container_delete_command(client, mock_params, args) # Verify the exception message assert error_message in str(excinfo.value) def test_storage_container_blob_get_command(mocker, client, mock_params): """ Given: An Azure client and a request to get a blob from a storage container. When: The storage_container_blob_get_command function is called with valid parameters. Then: The function should call the client's storage_container_blob_get_request method and return a fileResult. """ # Mock arguments args = {"container_name": "testcontainer", "blob_name": "testblob.txt", "account_name": "testaccount"} # Mock the client's storage_container_blob_get_request method mock_response = mocker.Mock() mock_response.content = b"Test blob content" mocker.patch.object(client, "storage_container_blob_get_request", return_value=mock_response) # Mock fileResult function mock_file_result = mocker.patch("Azure.fileResult", return_value="file_result_object") # Call the function result = storage_container_blob_get_command(client, mock_params, args) # Verify client.storage_container_blob_get_request was called with correct parameters client.storage_container_blob_get_request.assert_called_once_with("testcontainer", "testblob.txt", "testaccount") # Verify fileResult was called with correct parameters mock_file_result.assert_called_once_with(filename="testblob.txt", data=b"Test blob content") # Verify result assert result == "file_result_object" def test_storage_container_blob_tag_get_command(mocker, client, mock_params): """ Given: An Azure client and a request to get tags for a blob. When: The storage_container_blob_tag_get_command function is called with valid parameters. Then: The function should call the client's storage_container_blob_tag_get_request method and return the tags. """ # Mock arguments args = {"container_name": "testcontainer", "blob_name": "testblob.txt", "account_name": "testaccount"} # Mock XML response xml_response = """ tag1 value1 tag2 value2 """ # Mock the client's storage_container_blob_tag_get_request method mocker.patch.object(client, "storage_container_blob_tag_get_request", return_value=xml_response) # Mock ElementTree parsing mock_tree = mocker.Mock() mock_root = mocker.Mock() mock_tree.getroot.return_value = mock_root # Create mock Tag elements tag1 = mocker.Mock() tag1.findtext.side_effect = lambda x: "tag1" if x == "Key" else "value1" tag2 = mocker.Mock() tag2.findtext.side_effect = lambda x: "tag2" if x == "Key" else "value2" # Set up the iteration over Tag elements mock_root.iter.return_value = [tag1, tag2] mocker.patch("Azure.ET.ElementTree", return_value=mock_tree) mocker.patch("Azure.defused_ET.fromstring", return_value=mock_root) # Mock tableToMarkdown mocker.patch("Azure.tableToMarkdown", return_value="Mocked Table") # Call the function result = storage_container_blob_tag_get_command(client, mock_params, args) # Verify client.storage_container_blob_tag_get_request was called with correct parameters client.storage_container_blob_tag_get_request.assert_called_once_with("testcontainer", "testblob.txt", "testaccount") # Verify result assert isinstance(result, CommandResults) assert result.readable_output == "Mocked Table" assert result.outputs_prefix == "Azure.Storage.Blob" assert result.outputs_key_field == "name" assert result.outputs["ContainerName"] == "testcontainer" assert result.outputs["name"] == "testblob.txt" assert "Tag" in result.outputs def test_storage_container_blob_tag_get_command_deprecated(mocker, client, mock_params): """ Given: An Azure client and a request to get tags for a blob using the deprecated command. When: The storage_container_blob_tag_get_command function is called with valid parameters and the deprecated command name. Then: The function should call the client's storage_container_blob_tag_get_request method and return the tags with the deprecated prefix. """ # Mock arguments args = {"container_name": "testcontainer", "blob_name": "testblob.txt", "account_name": "testaccount"} # Mock XML response xml_response = """ tag1 value1 tag2 value2 """ # Mock the client's storage_container_blob_tag_get_request method mocker.patch.object(client, "storage_container_blob_tag_get_request", return_value=xml_response) # Mock ElementTree parsing mock_tree = mocker.Mock() mock_root = mocker.Mock() mock_tree.getroot.return_value = mock_root # Create mock Tag elements tag1 = mocker.Mock() tag1.findtext.side_effect = lambda x: "tag1" if x == "Key" else "value1" tag2 = mocker.Mock() tag2.findtext.side_effect = lambda x: "tag2" if x == "Key" else "value2" # Set up the iteration over Tag elements mock_root.iter.return_value = [tag1, tag2] mocker.patch("Azure.ET.ElementTree", return_value=mock_tree) mocker.patch("Azure.defused_ET.fromstring", return_value=mock_root) # Mock tableToMarkdown mocker.patch("Azure.tableToMarkdown", return_value="Mocked Table") # Mock demisto.command mocker.patch("Azure.demisto.command", return_value="azure-storage-container-blob-tag-get") # Call the function result = storage_container_blob_tag_get_command(client, mock_params, args) # Verify client.storage_container_blob_tag_get_request was called with correct parameters client.storage_container_blob_tag_get_request.assert_called_once_with("testcontainer", "testblob.txt", "testaccount") # Verify result assert isinstance(result, CommandResults) assert result.readable_output == "Mocked Table" assert result.outputs_prefix == "Azure.StorageContainer" assert result.outputs_key_field == "name" assert result.outputs["name"] == "testcontainer" assert result.outputs["Blob"]["name"] == "testblob.txt" assert "Tag" in result.outputs["Blob"] def test_storage_container_blob_tag_set_command(mocker, client, mock_params): """ Given: An Azure client and a request to set tags for a blob. When: The storage_container_blob_tag_set_command function is called with valid parameters. Then: The function should call the client's storage_container_blob_tags_set_request method. """ # Mock arguments args = { "container_name": "testcontainer", "blob_name": "testblob.txt", "account_name": "testaccount", "tags": '{"tag1": "value1", "tag2": "value2"}', } # Mock the client's storage_container_blob_tags_set_request method mocker.patch.object(client, "storage_container_blob_tags_set_request") # Mock create_set_tags_request_body mock_xml_data = b'tag1value1tag2value2' # noqa: E501 mocker.patch("Azure.create_set_tags_request_body", return_value=mock_xml_data) # Call the function result = storage_container_blob_tag_set_command(client, mock_params, args) # Verify create_set_tags_request_body was called with correct parameters Azure.create_set_tags_request_body.assert_called_once_with({"tag1": "value1", "tag2": "value2"}) # Verify client.storage_container_blob_tags_set_request was called with correct parameters client.storage_container_blob_tags_set_request.assert_called_once_with( "testcontainer", "testblob.txt", mock_xml_data, "testaccount" ) # Verify result assert isinstance(result, CommandResults) assert result.readable_output == "testblob.txt Tags successfully updated." def test_storage_container_blob_tag_set_command_append(mocker, client, mock_params): """ Given: An Azure client and a request to append tags for a blob. When: The storage_container_blob_tag_set_command function is called with valid parameters and append=True. Then: The function should call the client's storage_container_blob_tags_set_request method with the appended tags. """ # Mock arguments args = { "container_name": "testcontainer", "blob_name": "testblob.txt", "account_name": "testaccount", "tags": '{"tag3": "value3"}', "append": "true", } # Mock the client's storage_container_blob_tags_set_request method mocker.patch.object(client, "storage_container_blob_tags_set_request") # Mock storage_container_blob_tag_get_command to return existing tags mock_get_results = CommandResults(outputs={"Tag": [{"Key": "tag1", "Value": "value1"}, {"Key": "tag2", "Value": "value2"}]}) mocker.patch("Azure.storage_container_blob_tag_get_command", return_value=mock_get_results) # Mock create_set_tags_request_body mock_xml_data = b'tag3value3tag1value1tag2value2' # noqa: E501 mocker.patch("Azure.create_set_tags_request_body", return_value=mock_xml_data) # Call the function result = storage_container_blob_tag_set_command(client, mock_params, args) # Verify create_set_tags_request_body was called with correct parameters (appended tags) Azure.create_set_tags_request_body.assert_called_once_with({"tag3": "value3", "tag1": "value1", "tag2": "value2"}) # Verify client.storage_container_blob_tags_set_request was called with correct parameters client.storage_container_blob_tags_set_request.assert_called_once_with( "testcontainer", "testblob.txt", mock_xml_data, "testaccount" ) # Verify result assert isinstance(result, CommandResults) assert result.readable_output == "testblob.txt Tags successfully updated." def test_storage_container_blob_property_get_command(mocker, client, mock_params): """ Given: An Azure client and a request to get properties for a blob. When: The storage_container_blob_property_get_command function is called with valid parameters. Then: The function should call the client's storage_container_blob_property_get_request method and return the properties. """ # Mock arguments args = {"container_name": "testcontainer", "blob_name": "testblob.txt", "account_name": "testaccount"} # Mock response headers mock_headers = CaseInsensitiveDict( { "Content-Length": "1024", "Content-Type": "text/plain", "Etag": "0x8D8B92EFCFD9B41", "Last-Modified": "Wed, 14 Aug 2024 10:00:00 GMT", "x-ms-creation-time": "Wed, 14 Aug 2024 09:00:00 GMT", } ) # Mock the client's storage_container_blob_property_get_request method mock_response = mocker.Mock() mock_response.headers = mock_headers mocker.patch.object(client, "storage_container_blob_property_get_request", return_value=mock_response) # Mock tableToMarkdown mocker.patch("Azure.tableToMarkdown", return_value="Mocked Table") # Call the function result = storage_container_blob_property_get_command(client, mock_params, args) # Verify client.storage_container_blob_property_get_request was called with correct parameters client.storage_container_blob_property_get_request.assert_called_once_with("testcontainer", "testblob.txt", "testaccount") # Verify result assert isinstance(result, CommandResults) assert result.readable_output == "Mocked Table" assert result.outputs_prefix == "Azure.Storage.Blob" assert result.outputs_key_field == "name" assert result.outputs["ContainerName"] == "testcontainer" assert result.outputs["name"] == "testblob.txt" assert "Property" in result.outputs def test_storage_container_blob_property_get_command_deprecated(mocker, client, mock_params): """ Given: An Azure client and a request to get properties for a blob using the deprecated command. When: The storage_container_blob_property_get_command function is called with valid parameters and the deprecated command name. Then: The function should call the client's storage_container_blob_property_get_request method and return the properties with the deprecated prefix. """ # Mock arguments args = {"container_name": "testcontainer", "blob_name": "testblob.txt", "account_name": "testaccount"} # Mock response headers mock_headers = CaseInsensitiveDict( { "Content-Length": "1024", "Content-Type": "text/plain", "Etag": "0x8D8B92EFCFD9B41", "Last-Modified": "Wed, 14 Aug 2024 10:00:00 GMT", "x-ms-creation-time": "Wed, 14 Aug 2024 09:00:00 GMT", } ) # Mock the client's storage_container_blob_property_get_request method mock_response = mocker.Mock() mock_response.headers = mock_headers mocker.patch.object(client, "storage_container_blob_property_get_request", return_value=mock_response) # Mock tableToMarkdown mocker.patch("Azure.tableToMarkdown", return_value="Mocked Table") # Mock demisto.command mocker.patch("Azure.demisto.command", return_value="azure-storage-container-blob-property-get") # Call the function result = storage_container_blob_property_get_command(client, mock_params, args) # Verify client.storage_container_blob_property_get_request was called with correct parameters client.storage_container_blob_property_get_request.assert_called_once_with("testcontainer", "testblob.txt", "testaccount") # Verify result assert isinstance(result, CommandResults) assert result.readable_output == "Mocked Table" assert result.outputs_prefix == "Azure.StorageContainer" assert result.outputs_key_field == "name" assert result.outputs["name"] == "testcontainer" assert result.outputs["Blob"]["name"] == "testblob.txt" assert "Property" in result.outputs["Blob"] def test_storage_container_blob_property_set_command(mocker, client, mock_params): """ Given: An Azure client and a request to set properties for a blob. When: The storage_container_blob_property_set_command function is called with valid parameters. Then: The function should call the client's storage_container_blob_properties_set_request method. """ # Mock arguments args = { "container_name": "testcontainer", "blob_name": "testblob.txt", "account_name": "testaccount", "content_type": "application/json", "content_encoding": "gzip", "cache_control": "max-age=3600", } # Mock the client's storage_container_blob_properties_set_request method mocker.patch.object(client, "storage_container_blob_properties_set_request") # Mock remove_empty_elements expected_headers = { "x-ms-blob-cache-control": "max-age=3600", "x-ms-blob-content-type": "application/json", "x-ms-blob-content-encoding": "gzip", "Content-Length": "0", } mocker.patch("Azure.remove_empty_elements", return_value=expected_headers) # Call the function result = storage_container_blob_property_set_command(client, mock_params, args) # Verify remove_empty_elements was called with correct parameters Azure.remove_empty_elements.assert_called_once() # Verify client.storage_container_blob_properties_set_request was called with correct parameters client.storage_container_blob_properties_set_request.assert_called_once_with( "testcontainer", "testblob.txt", "testaccount", expected_headers ) # Verify result assert isinstance(result, CommandResults) assert result.readable_output == "Blob testblob.txt properties successfully updated." def test_storage_container_block_public_access_command(mocker, client, mock_params): """ Given: An Azure client and a request to block public access to a container. When: The storage_container_block_public_access_command function is called with valid parameters. Then: The function should call the client's storage_container_block_public_access_request method. """ # Mock arguments args = {"container_name": "testcontainer", "account_name": "testaccount"} # Mock the client's storage_container_block_public_access_request method mock_response = mocker.Mock() mocker.patch.object(client, "storage_container_block_public_access_request", return_value=mock_response) # Mock debug log mocker.patch.object(demisto, "debug") # Call the function result = storage_container_block_public_access_command(client, mock_params, args) # Verify client.storage_container_block_public_access_request was called with correct parameters client.storage_container_block_public_access_request.assert_called_once_with("testaccount", "testcontainer") # Verify debug was called demisto.debug.assert_called_once() # Verify result assert isinstance(result, CommandResults) assert result.readable_output == "Public access to container 'testcontainer' has been successfully blocked" class TestGetCommandAndTokenScopes: """Tests for the get_command_and_token_scopes function.""" def test_default_command(self): """Test that a default command returns DEFAULT_SCOPE and [TokenScope.DEFAULT].""" command = "azure-nsg-security-rule-update" client_scope, token_scopes = get_command_and_token_scopes(command) assert client_scope == DEFAULT_SCOPE assert token_scopes == [TokenScope.DEFAULT] def test_storage_container_command(self): """Test that a storage-container command returns STORAGE_SCOPE and [TokenScope.STORAGE].""" command = "azure-storage-container-create" client_scope, token_scopes = get_command_and_token_scopes(command) assert client_scope == STORAGE_SCOPE assert token_scopes == [TokenScope.STORAGE] def test_storage_blob_command(self): """Test that a storage-container-blob command returns STORAGE_SCOPE and [TokenScope.STORAGE].""" command = "azure-storage-container-blob-get" client_scope, token_scopes = get_command_and_token_scopes(command) assert client_scope == STORAGE_SCOPE assert token_scopes == [TokenScope.STORAGE] class TestGetCommandResource: """Tests for the get_command_resource function.""" def test_default_command(self): """Test that a default command returns DEFAULT_RESOURCE.""" command = "azure-nsg-security-rule-update" resource = get_command_resource(command) assert resource == DEFAULT_RESOURCE def test_storage_container_command(self): """Test that a storage-container command returns STORAGE_RESOURCE.""" command = "azure-storage-container-create" resource = get_command_resource(command) assert resource == STORAGE_RESOURCE def test_storage_blob_command(self): """Test that a storage-container-blob command returns STORAGE_RESOURCE.""" command = "azure-storage-container-blob-get" resource = get_command_resource(command) assert resource == STORAGE_RESOURCE @pytest.mark.parametrize( "command", [ "azure-storage-container-list", "azure-storage-blob-property-get", "azure-storage-blob-property-set", "azure-storage-blob-tag-get", "azure-storage-blob-create", "azure-storage-blob-get", "azure-storage-blob-tag-set", ], ) def test_get_command_and_token_scopes_storage(command): """ Given: A storage-related Azure command name (e.g. 'azure-storage-container-list', 'azure-storage-blob-get', etc.). When: Calling get_command_and_token_scopes with that command. Then: The returned scope equals STORAGE_SCOPE and the token scopes list contains only TokenScope.STORAGE. """ scope, token_scopes = get_command_and_token_scopes(command) assert scope == STORAGE_SCOPE assert token_scopes == [TokenScope.STORAGE] @pytest.mark.parametrize( "command", [ "azure-storage-container-list", "azure-storage-blob-property-get", "azure-storage-blob-property-set", "azure-storage-blob-tag-get", "azure-storage-blob-create", "azure-storage-blob-get", "azure-storage-blob-tag-set", ], ) def test_get_command_resource_storage(command): """ Given: A storage-related Azure command name (e.g. 'azure-storage-container-list', 'azure-storage-blob-get', etc.). When: Calling get_command_resource with that command. Then: The returned resource equals STORAGE_RESOURCE. """ resource = get_command_resource(command) assert resource == STORAGE_RESOURCE class TestGetAzureClient: """Tests for the get_azure_client function.""" @patch("Azure.get_connector_id", return_value="connector-123") @patch("Azure.get_azure_managed_identities_client_id", return_value=None) @patch("Azure.get_from_args_or_params") @patch("Azure.get_cloud_credentials") @patch("Azure.AzureClient") def test_with_cloud_credentials( self, mock_azure_client, mock_get_cloud_credentials, mock_get_from_args_or_params, mock_get_managed, mock_get_connector_id, ): """Test get_azure_client with cloud credentials (Platform path).""" # Setup mocks mock_get_from_args_or_params.return_value = "test-subscription-id" mock_get_cloud_credentials.return_value = {"access_token": "test-token"} mock_client = MagicMock() mock_azure_client.return_value = mock_client # Test parameters params = { "app_id": "test-app-id", "subscription_id": "test-subscription-id", "resource_group_name": "test-resource-group", "insecure": False, "proxy": False, "tenant_id": "test-tenant-id", "credentials": {}, # Empty credentials to trigger cloud credentials path } args = {} command = "azure-nsg-security-rule-update" # Call the function result = get_azure_client(params, args, command) # Verify results assert result == mock_client mock_get_cloud_credentials.assert_called_once_with( CloudTypes.AZURE.value, "test-subscription-id", scopes=[TokenScope.DEFAULT] ) mock_azure_client.assert_called_once_with( app_id="test-app-id", subscription_id="test-subscription-id", resource_group_name="test-resource-group", verify=True, proxy=False, tenant_id="test-tenant-id", enc_key=None, resource=DEFAULT_RESOURCE, scope=DEFAULT_SCOPE, headers={"Authorization": "Bearer test-token", "Content-Type": "application/json", "Accept": "application/json"}, connection_type="Client Credentials", azure_ad_endpoint="https://login.microsoftonline.com", auth_code=None, redirect_uri=None, managed_identities_client_id=None, ) @patch("Azure.get_connector_id", return_value="connector-123") @patch("Azure.get_azure_managed_identities_client_id", return_value=None) @patch("Azure.get_from_args_or_params") @patch("Azure.get_cloud_credentials") @patch("Azure.AzureClient") def test_with_storage_command( self, mock_azure_client, mock_get_cloud_credentials, mock_get_from_args_or_params, mock_get_managed, mock_get_connector_id, ): """Test get_azure_client with a storage command (Platform path).""" # Setup mocks mock_get_from_args_or_params.return_value = "test-subscription-id" mock_get_cloud_credentials.return_value = {"access_token": "test-token"} mock_client = MagicMock() mock_azure_client.return_value = mock_client # Test parameters params = { "app_id": "test-app-id", "subscription_id": "test-subscription-id", "resource_group_name": "test-resource-group", "insecure": False, "proxy": False, "tenant_id": "test-tenant-id", "credentials": {}, # Empty credentials to trigger cloud credentials path } args = {} command = "azure-storage-container-create" # Call the function result = get_azure_client(params, args, command) # Verify results assert result == mock_client mock_get_cloud_credentials.assert_called_once_with( CloudTypes.AZURE.value, "test-subscription-id", scopes=[TokenScope.STORAGE] ) mock_azure_client.assert_called_once_with( app_id="test-app-id", subscription_id="test-subscription-id", resource_group_name="test-resource-group", verify=True, proxy=False, tenant_id="test-tenant-id", enc_key=None, resource=STORAGE_RESOURCE, scope=STORAGE_SCOPE, headers={"Authorization": "Bearer test-token", "Content-Type": "application/json", "Accept": "application/json"}, connection_type="Client Credentials", azure_ad_endpoint="https://login.microsoftonline.com", auth_code=None, redirect_uri=None, managed_identities_client_id=None, ) def test_start_vm_command(mocker): """ Given: A subscription, resource group, and VM name. When: start_vm_command is called with these parameters. Then: It should call validate_provisioning_state and start_vm_request, and return correct CommandResults with VM starting state. """ from Azure import start_vm_command mock_client = mocker.Mock() params = {"subscription_id": "sub-id", "resource_group_name": "rg1"} args = {"subscription_id": "sub-id", "resource_group_name": "rg1", "virtual_machine_name": "vm1"} result = start_vm_command(mock_client, params, args) mock_client.validate_provisioning_state.assert_called_once_with("sub-id", "rg1", "vm1") mock_client.start_vm_request.assert_called_once_with("sub-id", "rg1", "vm1") assert isinstance(result, CommandResults) assert result.outputs_prefix == "Azure.Compute.VirtualMachines" assert result.outputs_key_field == "name" assert result.outputs["name"] == "vm1" assert result.outputs["resourceGroup"] == "rg1" assert result.outputs["powerState"] == "VM starting" assert "vm1" in result.readable_output def test_start_vm_command_deprecated(mocker): """ Given: A subscription, resource group, and VM name using the deprecated command. When: start_vm_command is called with the deprecated command name. Then: It should call validate_provisioning_state and start_vm_request, and return correct CommandResults with the deprecated prefix. """ from Azure import start_vm_command mock_client = mocker.Mock() params = {"subscription_id": "sub-id", "resource_group_name": "rg1"} args = {"subscription_id": "sub-id", "resource_group_name": "rg1", "virtual_machine_name": "vm1"} mocker.patch("Azure.demisto.command", return_value="azure-vm-instance-start") result = start_vm_command(mock_client, params, args) mock_client.validate_provisioning_state.assert_called_once_with("sub-id", "rg1", "vm1") mock_client.start_vm_request.assert_called_once_with("sub-id", "rg1", "vm1") assert isinstance(result, CommandResults) assert result.outputs_prefix == "Azure.Compute" assert result.outputs_key_field == "name" assert result.outputs["name"] == "vm1" assert result.outputs["resourceGroup"] == "rg1" assert result.outputs["powerState"] == "VM starting" assert "vm1" in result.readable_output def test_poweroff_vm_command(mocker): """ Given: A subscription, resource group, VM name, and optional skip_shutdown. When: poweroff_vm_command is called. Then: It should call validate_provisioning_state and poweroff_vm_request, and return correct CommandResults with VM stopping state. """ from Azure import poweroff_vm_command mock_client = mocker.Mock() params = {"subscription_id": "sub-id", "resource_group_name": "rg1"} args = {"subscription_id": "sub-id", "resource_group_name": "rg1", "virtual_machine_name": "vm1", "skip_shutdown": True} result = poweroff_vm_command(mock_client, params, args) mock_client.validate_provisioning_state.assert_called_once_with("sub-id", "rg1", "vm1") mock_client.poweroff_vm_request.assert_called_once_with("sub-id", "rg1", "vm1", True) assert isinstance(result, CommandResults) assert result.outputs_prefix == "Azure.Compute.VirtualMachines" assert result.outputs_key_field == "name" assert result.outputs["name"] == "vm1" assert result.outputs["resourceGroup"] == "rg1" assert result.outputs["powerState"] == "VM stopping" assert "vm1" in result.readable_output def test_poweroff_vm_command_deprecated(mocker): """ Given: A subscription, resource group, VM name, and optional skip_shutdown using the deprecated command. When: poweroff_vm_command is called with the deprecated command name. Then: It should call validate_provisioning_state and poweroff_vm_request, and return correct CommandResults with the deprecated prefix. """ from Azure import poweroff_vm_command mock_client = mocker.Mock() params = {"subscription_id": "sub-id", "resource_group_name": "rg1"} args = {"subscription_id": "sub-id", "resource_group_name": "rg1", "virtual_machine_name": "vm1", "skip_shutdown": True} mocker.patch("Azure.demisto.command", return_value="azure-vm-instance-power-off") result = poweroff_vm_command(mock_client, params, args) mock_client.validate_provisioning_state.assert_called_once_with("sub-id", "rg1", "vm1") mock_client.poweroff_vm_request.assert_called_once_with("sub-id", "rg1", "vm1", True) assert isinstance(result, CommandResults) assert result.outputs_prefix == "Azure.Compute" assert result.outputs_key_field == "name" assert result.outputs["name"] == "vm1" assert result.outputs["resourceGroup"] == "rg1" assert result.outputs["powerState"] == "VM stopping" assert "vm1" in result.readable_output def test_get_vm_command(mocker): """ Given: A subscription, resource group, and VM name. When: get_vm_command is called. Then: It should call get_vm_request and return correct CommandResults including OS, size, power state, and network interfaces. """ from Azure import get_vm_command mock_client = mocker.Mock() params = {"subscription_id": "sub-id", "resource_group_name": "rg1"} args = {"subscription_id": "sub-id", "resource_group_name": "rg1", "virtual_machine_name": "vm1", "expand": ""} mock_response = { "location": "eastus", "tags": {"env": "prod"}, "properties": { "vmId": "vm123", "provisioningState": "Succeeded", "storageProfile": {"osDisk": {"diskSizeGB": 128, "osType": "Linux"}}, "instanceView": {"statuses": [{"code": "PowerState/running", "displayStatus": "VM running"}]}, "networkProfile": {"networkInterfaces": [{"id": "nic1"}]}, "userData": "userdata", }, } mocker.patch.object(mock_client, "get_vm_request", return_value=mock_response) result = get_vm_command(mock_client, params, args) mock_client.get_vm_request.assert_called_once_with("sub-id", "rg1", "vm1", expand="") assert isinstance(result, CommandResults) assert result.outputs_prefix == "Azure.Compute.VirtualMachines" assert result.outputs_key_field == "name" assert result.outputs["properties"]["vmId"] == "vm123" assert result.outputs["properties"]["provisioningState"] == "Succeeded" assert result.outputs["properties"]["storageProfile"]["osDisk"]["osType"] == "Linux" assert result.outputs["properties"]["instanceView"]["statuses"][0]["displayStatus"] == "VM running" assert "vm1" in result.readable_output def test_get_vm_command_deprecated(mocker): """ Given: A subscription, resource group, and VM name using the deprecated command. When: get_vm_command is called with the deprecated command name. Then: It should call get_vm_request and return correct CommandResults with the deprecated prefix. """ from Azure import get_vm_command mock_client = mocker.Mock() params = {"subscription_id": "sub-id", "resource_group_name": "rg1"} args = {"subscription_id": "sub-id", "resource_group_name": "rg1", "virtual_machine_name": "vm1", "expand": ""} mock_response = { "location": "eastus", "tags": {"env": "prod"}, "properties": { "vmId": "vm123", "provisioningState": "Succeeded", "storageProfile": {"osDisk": {"diskSizeGB": 128, "osType": "Linux"}}, "instanceView": {"statuses": [{"code": "PowerState/running", "displayStatus": "VM running"}]}, "networkProfile": {"networkInterfaces": [{"id": "nic1"}]}, "userData": "userdata", }, } mocker.patch.object(mock_client, "get_vm_request", return_value=mock_response) mocker.patch("Azure.demisto.command", return_value="azure-vm-instance-details-get") result = get_vm_command(mock_client, params, args) mock_client.get_vm_request.assert_called_once_with("sub-id", "rg1", "vm1", expand="") assert isinstance(result, CommandResults) assert result.outputs_prefix == "Azure.Compute" assert result.outputs_key_field == "name" assert result.outputs["properties"]["vmId"] == "vm123" assert result.outputs["properties"]["provisioningState"] == "Succeeded" assert result.outputs["properties"]["storageProfile"]["osDisk"]["osType"] == "Linux" assert result.outputs["properties"]["instanceView"]["statuses"][0]["displayStatus"] == "VM running" assert "vm1" in result.readable_output def test_get_network_interface_command(mocker): """ Given: A subscription, resource group, and network interface name. When: get_network_interface_command is called with these parameters. Then: It should call get_network_interface_request and return correct CommandResults with properly formatted network interface details. """ from Azure import get_network_interface_command mock_client = mocker.Mock() mock_params = {"subscription_id": "sub-id", "resource_group_name": "rg1"} args = {"subscription_id": "sub-id", "resource_group_name": "rg1", "network_interface_name": "nic1"} mock_response = { "id": "/subscriptions/sub-id/resourceGroups/rg1/providers/Microsoft.Network/networkInterfaces/nic1", "name": "nic1", "location": "eastus", "properties": { "macAddress": "00:11:22:33:44:55", "primary": True, "networkSecurityGroup": {"id": "nsg-id"}, "nicType": "Standard", "virtualMachine": {"id": "vm-id"}, "dnsSettings": {"internalDomainNameSuffix": "internal.local"}, "ipConfigurations": [ { "name": "ipconfig1", "id": "ipconfig-id", "properties": {"privateIPAddress": "10.0.0.4", "publicIPAddress": {"id": "public-ip-id"}}, "etag": 'W/"12345"', } ], }, } mocker.patch.object(mock_client, "get_network_interface_request", return_value=mock_response) result = get_network_interface_command(mock_client, mock_params, args) mock_client.get_network_interface_request.assert_called_once_with("sub-id", "rg1", "nic1") assert isinstance(result, CommandResults) assert result.outputs_prefix == "Azure.VirtualNetworks.NetworkInterfaces" assert result.outputs_key_field == "name" assert result.outputs["name"] == "nic1" assert result.outputs["properties"]["macAddress"] == "00:11:22:33:44:55" assert result.outputs["properties"]["ipConfigurations"][0]["properties"]["privateIPAddress"] == "10.0.0.4" assert result.outputs["properties"]["ipConfigurations"][0]["properties"]["publicIPAddress"]["id"] == "public-ip-id" assert result.outputs["properties"]["ipConfigurations"][0]["etag"] == "12345" # etag cleaned assert "nic1" in result.readable_output def test_get_network_interface_command_deprecated(mocker): """ Given: A subscription, resource group, and network interface name using the deprecated command. When: get_network_interface_command is called with the deprecated command name. Then: It should call get_network_interface_request and return correct CommandResults with the deprecated prefix. """ from Azure import get_network_interface_command mock_client = mocker.Mock() mock_params = {"subscription_id": "sub-id", "resource_group_name": "rg1"} args = {"subscription_id": "sub-id", "resource_group_name": "rg1", "network_interface_name": "nic1"} mock_response = { "id": "/subscriptions/sub-id/resourceGroups/rg1/providers/Microsoft.Network/networkInterfaces/nic1", "name": "nic1", "location": "eastus", "properties": { "macAddress": "00:11:22:33:44:55", "primary": True, "networkSecurityGroup": {"id": "nsg-id"}, "nicType": "Standard", "virtualMachine": {"id": "vm-id"}, "dnsSettings": {"internalDomainNameSuffix": "internal.local"}, "ipConfigurations": [ { "name": "ipconfig1", "id": "ipconfig-id", "properties": {"privateIPAddress": "10.0.0.4", "publicIPAddress": {"id": "public-ip-id"}}, "etag": 'W/"12345"', } ], }, } mocker.patch.object(mock_client, "get_network_interface_request", return_value=mock_response) mocker.patch("Azure.demisto.command", return_value="azure-vm-network-interface-details-get") result = get_network_interface_command(mock_client, mock_params, args) mock_client.get_network_interface_request.assert_called_once_with("sub-id", "rg1", "nic1") assert isinstance(result, CommandResults) assert result.outputs_prefix == "Azure.Network.Interfaces" assert result.outputs_key_field == "name" assert result.outputs["name"] == "nic1" def test_get_single_ip_details_from_list_of_ip_details(): """ Given: A subscription, resource group, and public IP name. When: get_public_ip_details_command is called with these parameters. Then: It should call get_public_ip_details_request and return correct CommandResults. """ from Azure import get_single_ip_details_from_list_of_ip_details list_of_ips = [ {"properties": {"ipAddress": "1.1.1.1"}}, {"properties": {"ipAddress": "2.2.2.2"}}, {"properties": {"nested": {"ipAddress": "3.3.3.3"}}}, ] ip1 = get_single_ip_details_from_list_of_ip_details(list_of_ips, "1.1.1.1") ip3 = get_single_ip_details_from_list_of_ip_details(list_of_ips, "3.3.3.3") ip_missing = get_single_ip_details_from_list_of_ip_details(list_of_ips, "4.4.4.4") assert ip1 == {"properties": {"ipAddress": "1.1.1.1"}} assert ip3 == {"properties": {"nested": {"ipAddress": "3.3.3.3"}}} assert ip_missing is None def test_get_public_ip_details_command_with_resource_group(mocker): """ Given: A subscription, resource group, and public IP name. When: get_public_ip_details_command is called with these parameters. Then: It should call get_public_ip_details_request and return correct CommandResults. """ from Azure import get_public_ip_details_command mock_client = mocker.Mock() mock_params = {"subscription_id": "sub-id", "resource_group_name": "rg1"} args = {"subscription_id": "sub-id", "resource_group_name": "rg1", "address_name": "ip1"} mock_response = { "id": "/subscriptions/sub-id/resourceGroups/rg1/providers/Microsoft.Network/publicIPAddresses/ip1", "name": "ip1", "location": "eastus", "etag": 'W/"12345"', "properties": { "ipAddress": "1.2.3.4", "publicIPAddressVersion": "IPv4", "publicIPAllocationMethod": "Static", "ipConfiguration": {"id": "config-id"}, "dnsSettings": {"domainNameLabel": "label1", "fqdn": "ip1.eastus.cloudapp.azure.com"}, }, } mocker.patch.object(mock_client, "get_public_ip_details_request", return_value=mock_response) result = get_public_ip_details_command(mock_client, mock_params, args) mock_client.get_public_ip_details_request.assert_called_once_with("sub-id", "rg1", "ip1") assert isinstance(result, CommandResults) assert result.outputs_prefix == "Azure.VirtualNetworks.PublicIPAddresses" assert result.outputs_key_field == "id" assert result.outputs["properties"]["ipAddress"] == "1.2.3.4" assert result.outputs["properties"]["publicIPAddressVersion"] == "IPv4" assert result.outputs["properties"]["publicIPAllocationMethod"] == "Static" assert result.outputs["etag"] == "12345" assert "ip1" in result.readable_output def test_get_public_ip_details_command_deprecated(mocker): """ Given: A subscription, resource group, and public IP name using the deprecated command. When: get_public_ip_details_command is called with the deprecated command name. Then: It should call get_public_ip_details_request and return correct CommandResults with the deprecated prefix. """ from Azure import get_public_ip_details_command mock_client = mocker.Mock() mock_params = {"subscription_id": "sub-id", "resource_group_name": "rg1"} args = {"subscription_id": "sub-id", "resource_group_name": "rg1", "address_name": "ip1"} mock_response = { "id": "/subscriptions/sub-id/resourceGroups/rg1/providers/Microsoft.Network/publicIPAddresses/ip1", "name": "ip1", "location": "eastus", "etag": 'W/"12345"', "properties": { "ipAddress": "1.2.3.4", "publicIPAddressVersion": "IPv4", "publicIPAllocationMethod": "Static", "ipConfiguration": {"id": "config-id"}, "dnsSettings": {"domainNameLabel": "label1", "fqdn": "ip1.eastus.cloudapp.azure.com"}, }, } mocker.patch.object(mock_client, "get_public_ip_details_request", return_value=mock_response) mocker.patch("Azure.demisto.command", return_value="azure-vm-public-ip-details-get") result = get_public_ip_details_command(mock_client, mock_params, args) mock_client.get_public_ip_details_request.assert_called_once_with("sub-id", "rg1", "ip1") assert isinstance(result, CommandResults) assert result.outputs_prefix == "Azure.Network.IPConfigurations" assert result.outputs_key_field == "id" assert "ip1" in result.readable_output def test_get_public_ip_details_command_without_resource_group(mocker): """ Given: A subscription and public IP name, but no resource group. When: get_public_ip_details_command is called. Then: It should call get_all_public_ip_details_request, find the matching IP, and return details. """ from Azure import get_public_ip_details_command mock_client = mocker.Mock() mock_params = {"subscription_id": "sub-id"} args = {"subscription_id": "sub-id", "address_name": "ip1"} mock_all_ips = { "value": [ { "id": "/subscriptions/sub-id/resourceGroups/rg1/providers/Microsoft.Network/publicIPAddresses/ip1", "name": "ip1", "location": "eastus", "etag": 'W/"999"', "properties": { "ipAddress": "5.6.7.8", "publicIPAddressVersion": "IPv4", "publicIPAllocationMethod": "Dynamic", }, }, { "id": "/subscriptions/sub-id/resourceGroups/rg2/providers/Microsoft.Network/publicIPAddresses/ip2", "name": "ip2", "location": "westus", "etag": 'W/"888"', "properties": { "ipAddress": "9.9.9.9", "publicIPAddressVersion": "IPv6", "publicIPAllocationMethod": "Static", }, }, ] } # Mock the client and helper functions mocker.patch.object(mock_client, "get_all_public_ip_details_request", return_value=mock_all_ips) mocker.patch("Azure.get_single_ip_details_from_list_of_ip_details", return_value=mock_all_ips["value"][0]) result = get_public_ip_details_command(mock_client, mock_params, args) mock_client.get_all_public_ip_details_request.assert_called_once_with("sub-id") assert isinstance(result, CommandResults) assert result.outputs["properties"]["ipAddress"] == "5.6.7.8" assert result.outputs["etag"] == "999" assert "ip1" in result.readable_output assert "rg1" in result.readable_output def test_azure_billing_usage_list_command_success(mocker, client, mock_params): """ Given: An Azure client and valid billing usage arguments. When: azure_billing_usage_list_command is called successfully. Then: It should return CommandResults with usage data and proper outputs. """ from Azure import azure_billing_usage_list_command mock_response = { "value": [ { "name": "usage-item-1", "properties": { "product": "Virtual Machines", "meterName": "D2s v3", "paygCost": {"amount": 125.75}, "quantity": 24.5, "billingPeriodStartDate": "2025-10-01T00:00:00.0000000Z", "billingPeriodEndDate": "2025-10-01T23:59:59.0000000Z", }, } ], "nextLink": "https://management.azure.com/subscriptions/test/providers/Microsoft.Consumption/usageDetails?$skiptoken=abc123", } mocker.patch.object(client, "http_request", return_value=mock_response) args = {"subscription_id": "test-subscription-id", "max_results": "50", "filter": "properties/usageStart ge '2023-10-01'"} params = mock_params result = azure_billing_usage_list_command(client, params, args) assert isinstance(result, CommandResults) assert "Azure Billing Usage" in result.readable_output assert "Azure.Billing.Usage(val.name && val.name == obj.name)" in result.outputs assert "Azure.Billing(true)" in result.outputs assert ( "https://management.azure.com/subscriptions/test/providers/Microsoft.Consumption/usageDetails?$skiptoken=abc123" in result.outputs["Azure.Billing(true)"]["UsageNextToken"] ) assert len(result.outputs["Azure.Billing.Usage(val.name && val.name == obj.name)"]) == 1 assert ( result.outputs["Azure.Billing.Usage(val.name && val.name == obj.name)"][0]["properties"]["product"] == "Virtual Machines" ) assert result.raw_response == mock_response def test_azure_billing_forecast_list_command_success(mocker, client, mock_params): """ Given: An Azure client and valid billing forecast arguments. When: azure_billing_forecast_list_command is called successfully. Then: It should return CommandResults with forecast data and proper outputs. """ from Azure import azure_billing_forecast_list_command # The current implementation expects a table-like response under properties with columns and rows, # and it calls client.billing_forecast_list (not http_request) directly. mock_response = { "properties": { "columns": [ {"name": "UsageDate"}, {"name": "CostStatus"}, {"name": "Currency"}, {"name": "Pre Tax Cost USD"}, ], "rows": [ [20231015, "Forecast", "USD", 250.50], ], } } mocker.patch.object(client, "billing_forecast_list", return_value=mock_response) args = { "subscription_id": "test-subscription-id", "type": "Usage", "aggregation_function_name": "Pre Tax Cost USD", "filter": "properties/UsageDate ge '2023-10-15'", } params = mock_params result = azure_billing_forecast_list_command(client, params, args) assert isinstance(result, CommandResults) assert "Azure Billing Forecast" in result.readable_output # Validate context structure and parsed forecasts assert "Azure.Billing.Forecast" in result.outputs forecast_ctx = result.outputs["Azure.Billing.Forecast"] assert isinstance(forecast_ctx, list) assert len(forecast_ctx) == 1 row = forecast_ctx[0] # The command uses aggregation_function_name as a key in the result rows assert row["Pre Tax Cost USD"] == 250.50 assert row["CostStatus"] == "Forecast" assert row["Currency"] == "USD" # UsageDate should be formatted as YYYY-MM-DD from 20231015 assert row["UsageDate"] == "2023-10-15" # Raw response should be the original mock response assert result.raw_response == mock_response def test_azure_billing_budgets_list_command_success(mocker, client, mock_params): """ Given: An Azure client and valid billing budgets arguments. When: azure_billing_budgets_list_command is called successfully. Then: It should return CommandResults with budget data and proper outputs. """ from Azure import azure_billing_budgets_list_command mock_response = { "value": [ { "name": "test-budget", "type": "Microsoft.Consumption/budgets", "properties": { "timePeriod": {"startDate": "2023-10-01T00:00:00Z", "endDate": "2023-10-31T23:59:59Z"}, "amount": 1000.0, "currentSpend": {"amount": 750.25}, }, } ] } mocker.patch.object(client, "http_request", return_value=mock_response) args = {"subscription_id": "test-subscription-id"} params = mock_params result = azure_billing_budgets_list_command(client, params, args) assert isinstance(result, CommandResults) assert "Azure Budgets" in result.readable_output assert "Azure.Billing.Budget" in result.outputs assert len(result.outputs["Azure.Billing.Budget"]) == 1 assert result.outputs["Azure.Billing.Budget"][0]["name"] == "test-budget" assert result.outputs["Azure.Billing.Budget"][0]["properties"]["amount"] == 1000.0 assert result.outputs["Azure.Billing.Budget"][0]["properties"]["currentSpend"]["amount"] == 750.25 assert result.raw_response == mock_response def test_azure_billing_budgets_list_command_single_budget(mocker, client, mock_params): """ Given: An Azure client and arguments for retrieving a single budget by name. When: azure_billing_budgets_list_command is called with budget_name parameter. Then: It should return CommandResults with single budget data. """ from Azure import azure_billing_budgets_list_command mock_response = { "name": "specific-budget", "type": "Microsoft.Consumption/budgets", "properties": { "timePeriod": {"startDate": "2023-11-01T00:00:00Z", "endDate": "2023-11-30T23:59:59Z"}, "amount": 500.0, "currentSpend": {"amount": 200.75}, }, } mocker.patch.object(client, "http_request", return_value=mock_response) args = {"subscription_id": "test-subscription-id", "budget_name": "specific-budget"} params = mock_params result = azure_billing_budgets_list_command(client, params, args) assert isinstance(result, CommandResults) assert "Azure Budgets" in result.readable_output assert "Azure.Billing.Budget" in result.outputs assert len(result.outputs["Azure.Billing.Budget"]) == 1 assert result.outputs["Azure.Billing.Budget"][0]["name"] == "specific-budget" assert result.outputs["Azure.Billing.Budget"][0]["properties"]["amount"] == 500.0 assert result.raw_response == mock_response def test_azure_billing_usage_list_command_no_next_token(mocker, client, mock_params): """ Given: An Azure client with response containing no next token. When: azure_billing_usage_list_command is called successfully. Then: It should return CommandResults without next token in outputs. """ from Azure import azure_billing_usage_list_command mock_response = { "value": [ { "name": "usage-item-2", "properties": { "product": "Storage", "paygCost": {"amount": 15.25}, "quantity": 100.0, "billingPeriodStartDate": "2025-10-01T00:00:00.0000000Z", "billingPeriodEndDate": "2025-10-02T23:59:59.0000000Z", }, } ] } mocker.patch.object(client, "http_request", return_value=mock_response) args = {"subscription_id": "test-subscription-id", "max_results": "10"} params = mock_params result = azure_billing_usage_list_command(client, params, args) assert isinstance(result, CommandResults) assert result.outputs["Azure.Billing(true)"]["UsageNextToken"] == "" assert "Next Page Token" not in result.readable_output assert result.outputs["Azure.Billing.Usage(val.name && val.name == obj.name)"][0]["properties"]["product"] == "Storage" def test_azure_billing_usage_list_command_with_pagination_token(mocker, client, mock_params): """ Given: An Azure client and arguments with next page token. When: azure_billing_usage_list_command is called with pagination token. Then: It should include the token in the request parameters. """ from Azure import azure_billing_usage_list_command mock_response = { "value": [ { "name": "usage-item-page-2", "properties": { "product": "Networking", "paygCost": {"amount": 5.50}, "quantity": 10.0, "billingPeriodStartDate": "2025-10-04T00:00:00.0000000Z", "billingPeriodEndDate": "2025-10-04T23:59:59.0000000Z", }, } ] } mocker.patch.object(client, "http_request", return_value=mock_response) args = {"subscription_id": "test-subscription-id", "next_page_token": "existing-skiptoken"} params = mock_params result = azure_billing_usage_list_command(client, params, args) # Verify the token was passed to the client client.http_request.assert_called_once() call_args = client.http_request.call_args[1] assert call_args["params"].keys() == {"api-version"} assert isinstance(result, CommandResults) assert result.outputs["Azure.Billing.Usage(val.name && val.name == obj.name)"][0]["properties"]["product"] == "Networking" def test_parse_forecast_table_to_dict_success(): """ Given: A table-like Azure Cost Management response with columns and rows. When: parse_forecast_table_to_dict is invoked. Then: It should return a list of dict rows mapping column names to values. """ from Azure import parse_forecast_table_to_dict response = { "properties": { "columns": [ {"name": "UsageDate"}, {"name": "CostUSD"}, {"name": "CostStatus"}, ], "rows": [ ["2025-10-01", 12.34, "Forecast"], ["2025-10-02", 56.78, "Actual"], ], } } parsed = parse_forecast_table_to_dict(response) assert isinstance(parsed, list) assert parsed[0]["UsageDate"] == "2025-10-01" assert parsed[0]["CostUSD"] == 12.34 assert parsed[0]["CostStatus"] == "Forecast" assert parsed[1]["UsageDate"] == "2025-10-02" def test_parse_forecast_table_to_dict_mismatch_row_length(mocker): """ Given: Response where one row length doesn't match columns length. When: parse_forecast_table_to_dict runs. Then: It should skip the mismatched row and parse the valid one. """ from Azure import parse_forecast_table_to_dict mocker.patch.object(demisto, "debug") response = { "properties": { "columns": [{"name": "A"}, {"name": "B"}], "rows": [ [1], # mismatched (len 1 vs 2 columns) -> should be skipped [2, 3], # valid ], } } parsed = parse_forecast_table_to_dict(response) assert parsed == [{"A": 2, "B": 3}] def test_parse_forecast_table_to_dict_malformed_raises(): """ Given: Malformed response (columns missing 'name'). When: parse_forecast_table_to_dict runs. Then: It should raise DemistoException. """ from Azure import parse_forecast_table_to_dict, DemistoException bad_response = { "properties": { "columns": [{"wrong": "UsageDate"}], # will cause KeyError in parsing "rows": [["2025-10-01"]], } } with pytest.raises(DemistoException): parse_forecast_table_to_dict(bad_response) def test_remove_query_param_from_url_basic(): """ Given: A URL with multiple query parameters including duplicates for a key. When: remove_query_param_from_url is used to remove that key. Then: The resulting URL should not contain the removed parameter and others remain. """ from Azure import remove_query_param_from_url from urllib.parse import urlparse, parse_qs url = "https://example.com/path?a=1&b=2&b=3&c=x" out = remove_query_param_from_url(url, "b") parsed = urlparse(out) qs = parse_qs(parsed.query) assert "b" not in qs assert qs == {"a": ["1"], "c": ["x"]} def test_remove_query_param_from_url_param_absent(): """ Given: A URL without the specified parameter. When: remove_query_param_from_url is called. Then: The URL query mapping remains logically the same. """ from Azure import remove_query_param_from_url from urllib.parse import urlparse, parse_qs url = "https://example.com/path?a=1&c=x" out = remove_query_param_from_url(url, "b") assert parse_qs(urlparse(out).query) == {"a": ["1"], "c": ["x"]} def test_remove_query_param_from_url_no_query(): """ Given: A URL without any query string. When: remove_query_param_from_url is called. Then: The URL remains unchanged. """ from Azure import remove_query_param_from_url url = "https://example.com/path" out = remove_query_param_from_url(url, "b") assert out == url def test_storage_container_set_headers(mocker): """ Given: An Azure client instance When: The storage_container_set_headers function is called Then: The headers should be properly set with the correct values """ # Create a client instance client = AzureClient() # Mock datetime to get consistent test results mock_datetime = mocker.patch("Azure.dt.datetime") mock_datetime.utcnow.return_value.strftime.return_value = "Wed, 17 Nov 2025 09:30:00 GMT" # Call the function client.storage_container_set_headers(custom_headers={"x-custom-header": "test-value"}) # Verify headers were set correctly expected_headers = { "x-ms-version": "2023-11-03", "x-ms-date": "Wed, 17 Nov 2025 09:30:00 GMT", "x-custom-header": "test-value", } assert client.headers == expected_headers mock_datetime.utcnow.assert_called_once() mock_datetime.utcnow.return_value.strftime.assert_called_once_with(STORAGE_DATE_FORMAT) def test_create_set_tags_request_body(): """ Given: A dictionary of tags When: The create_set_tags_request_body function is called Then: The function should return a properly formatted XML string """ # Test data tags = {"key1": "value1", "key2": "value2"} # Call the function result = create_set_tags_request_body(tags) # Verify the result is bytes assert isinstance(result, bytes) # Convert to string for easier assertion result_str = result.decode("utf-8") # Verify XML structure assert "" in result_str assert "" in result_str assert "" in result_str assert "" in result_str assert "key1" in result_str assert "value1" in result_str assert "key2" in result_str assert "value2" in result_str # Parse XML to verify structure import xml.etree.ElementTree as ET root = ET.fromstring(result) # Check structure assert root.tag == "Tags" tag_set = root.find("TagSet") assert tag_set is not None # Check tags tags_elements = tag_set.findall("Tag") assert len(tags_elements) == 2 # Check first tag tag1 = tags_elements[0] assert tag1.find("Key").text in ["key1", "key2"] if tag1.find("Key").text == "key1": assert tag1.find("Value").text == "value1" else: assert tag1.find("Value").text == "value2" def test_nsg_security_rules_list_command_success(mocker): """ Given: The command arguments. When: Calling azure-nsg-security-rules-list command. Then: The command should successfully process and return network security rules. """ mock_client = mocker.Mock() mock_response = { "value": [ { "name": "rule1", "id": "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Network/networkSecurityGroups/nsg1" "/securityRules/rule1", "properties": {"direction": "Inbound", "priority": 100, "access": "Allow"}, }, { "name": "rule2", "id": "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Network/networkSecurityGroups/nsg1" "/securityRules/rule2", "properties": {"direction": "Outbound", "priority": 200, "access": "Deny"}, }, ] } mock_client.list_security_rules.return_value = mock_response params = {} args = {"subscription_id": "test-sub-id", "resource_group_name": "test-rg", "network_security_group_name": "test-nsg"} result = nsg_security_rules_list_command(mock_client, params, args) assert isinstance(result, CommandResults) assert result.outputs == mock_response["value"] assert "Security Groups List" in result.readable_output assert "rule1" in result.readable_output assert "rule2" in result.readable_output mock_client.list_security_rules.assert_called_once_with("test-sub-id", "test-rg", "test-nsg") def test_nsg_security_rules_list_command_deprecated(mocker): """ Given: The command arguments using the deprecated command. When: Calling azure-nsg-security-rules-list command with the deprecated command name. Then: The command should successfully process and return network security rules with the deprecated prefix. """ mock_client = mocker.Mock() mock_response = { "value": [ { "name": "rule1", "id": "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Network/networkSecurityGroups/nsg1" "/securityRules/rule1", "properties": {"direction": "Inbound", "priority": 100, "access": "Allow"}, }, { "name": "rule2", "id": "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Network/networkSecurityGroups/nsg1" "/securityRules/rule2", "properties": {"direction": "Outbound", "priority": 200, "access": "Deny"}, }, ] } mock_client.list_security_rules.return_value = mock_response params = {} args = {"subscription_id": "test-sub-id", "resource_group_name": "test-rg", "network_security_group_name": "test-nsg"} mocker.patch("Azure.demisto.command", return_value="azure-nsg-security-rules-list") result = nsg_security_rules_list_command(mock_client, params, args) assert isinstance(result, CommandResults) assert result.outputs_prefix == "Azure.NSGRule" assert result.outputs_key_field == "id" assert result.outputs == mock_response["value"] def test_nsg_security_rules_list_command_empty_response(mocker): """ Given: The command arguments. When: Calling azure-nsg-security-rules-list command. Then: The command should successfully process and return an empty list of network security rules. """ mock_client = mocker.Mock() mock_client.list_security_rules.return_value = {"value": []} params = {} args = {"subscription_id": "test-sub-id", "resource_group_name": "test-rg", "network_security_group_name": "test-nsg"} result = nsg_security_rules_list_command(mock_client, params, args) assert isinstance(result, CommandResults) assert result.outputs == [] assert result.raw_response == [] def test_nsg_security_rules_list_command_no_value_key(mocker): """ Given: The command arguments. When: Calling azure-nsg-security-rules-list command. Then: The command should return an empty list. """ mock_client = mocker.Mock() mock_client.list_security_rules.return_value = {} params = {} args = {"network_security_group_name": "test-nsg", "subscription_id": "test-sub-id", "resource_group_name": "test-rg"} result = nsg_security_rules_list_command(mock_client, params, args) assert isinstance(result, CommandResults) assert result.outputs == [] def test_nsg_security_rules_list_command_missing_properties(mocker): """ Given: The command arguments. When: Calling azure-nsg-security-rules-list command. Then: Test handling of rules with missing properties. The command should successfully process and return network security rules. """ mock_client = mocker.Mock() mock_response = { "value": [ { "name": "rule1", "id": "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Network/networkSecurityGroups/nsg1" "/securityRules/rule1", }, { "name": "rule2", "id": "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Network/networkSecurityGroups/nsg1" "/securityRules/rule2", "properties": {}, }, ] } mock_client.list_security_rules.return_value = mock_response params = {} args = {"subscription_id": "test-sub-id", "resource_group_name": "test-rg", "network_security_group_name": "test-nsg"} result = nsg_security_rules_list_command(mock_client, params, args) assert isinstance(result, CommandResults) assert len(result.outputs) == 2 def test_storage_blob_service_properties_set_command_new(mocker, client, mock_params): """ Given: An Azure client and a request to set blob service properties. When: The storage_blob_service_properties_set_command function is called with valid parameters with azure-storage-blob-service-property-set. Then: The function should return the updated blob service properties in the expected format, including backward compatibility outputs. """ from Azure import storage_blob_service_properties_set_command # Prepare mock response properties_response = { "name": "default", "id": "/subscriptions/sub-id/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/teststorage/blobServices" "/default", "properties": {"deleteRetentionPolicy": {"enabled": True, "days": 7}}, } mocker.patch.object(client, "storage_blob_service_properties_set_request", return_value=properties_response) mocker.patch("Azure.demisto.command", return_value="azure-storage-blob-service-property-set") # Call the function args = {"account_name": "teststorage", "delete_rentention_policy_enabled": "true", "delete_rentention_policy_days": "7"} result = storage_blob_service_properties_set_command(client, mock_params, args) assert result.outputs_prefix == "Azure.Storage.BlobServices" assert result.outputs_key_field == "id" assert result.outputs == properties_response assert result.raw_response == properties_response def test_storage_blob_service_properties_get_command_new(mocker): """ Given: An Azure client mock and the get_blob_service_properties.json file. When: storage_blob_service_properties_get_command is called with azure-storage-blob-service-property-get. Then: The CommandResults should have correct outputs, readable_output, and metadata, including backward compatibility outputs. """ from Azure import storage_blob_service_properties_get_command mock_response = util_load_json("test_data/get_blob_service_properties.json") mock_client = mocker.Mock() mock_client.storage_blob_service_properties_get_request.return_value = mock_response mocker.patch("Azure.demisto.command", return_value="azure-storage-blob-service-property-get") params = {"subscription_id": "subid", "resource_group_name": "rg1"} args = {"account_name": "teststorage"} result = storage_blob_service_properties_get_command(mock_client, params, args) assert result.outputs_prefix == "Azure.Storage.BlobServices" assert result.outputs_key_field == "id" assert result.outputs == mock_response assert result.raw_response == mock_response def test_create_network_security_group(mocker, client): """ Given: An Azure client and a request to create a network security group. When: The create_network_security_group function is called with valid parameters. Then: The function should return the created network security group information in the expected format. """ # Prepare mock response nsg_response = { "name": "test-nsg", "id": "/subscriptions/sub-id/resourceGroups/test-rg/providers/Microsoft.Network/networkSecurityGroups/test-nsg", "location": "eastus", "properties": {}, } mocker.patch.object(client, "http_request", return_value=nsg_response) result = client.create_network_security_group( subscription_id="sub-id", resource_group_name="test-rg", security_group_name="test-nsg", location="eastus", ) assert result == nsg_response client.http_request.assert_called_once_with( method="PUT", full_url="https://management.azure.com/subscriptions/sub-id/resourceGroups/test-rg/providers/Microsoft.Network/networkSecurityGroups/test-nsg", params={"api-version": "2025-05-01"}, json_data={"location": "eastus"}, ) def test_create_network_security_group_error(mocker, client): """ Given: An Azure client and a request to create a network security group. When: The create_network_security_group function is called and an exception is raised. Then: The function should call handle_azure_error. """ mocker.patch.object(client, "http_request", side_effect=Exception("test error")) mocker.patch.object(client, "handle_azure_error") client.create_network_security_group( subscription_id="sub-id", resource_group_name="test-rg", security_group_name="test-nsg", location="eastus", ) client.handle_azure_error.assert_called_once() def test_list_vm_request(mocker, client): """ Given: An Azure client and a request to list virtual machines. When: The list_vm_request function is called with valid parameters. Then: The function should return the list of virtual machines in the expected format. """ # Prepare mock response vm_response = { "value": [ { "name": "test-vm", "id": "/subscriptions/sub-id/resourceGroups/test-rg/providers/Microsoft.Compute/virtualMachines/test-vm", "location": "eastus", "properties": {}, } ] } mocker.patch.object(client, "http_request", return_value=vm_response) result = client.list_vm_request( subscription_id="sub-id", resource_group_name="test-rg", next_token="", ) assert result == vm_response client.http_request.assert_called_once_with( method="GET", full_url="https://management.azure.com/subscriptions/sub-id/resourceGroups/test-rg/providers/Microsoft.Compute/virtualMachines", params={"api-version": "2025-04-01"}, ) def test_list_vm_request_with_next_token(mocker, client): """ Given: An Azure client and a request to list virtual machines with a next token. When: The list_vm_request function is called with a next token. Then: The function should return the list of virtual machines using the next token. """ # Prepare mock response vm_response = { "value": [ { "name": "test-vm2", "id": "/subscriptions/sub-id/resourceGroups/test-rg/providers/Microsoft.Compute/virtualMachines/test-vm2", "location": "eastus", "properties": {}, } ] } mocker.patch.object(client, "http_request", return_value=vm_response) result = client.list_vm_request( subscription_id="sub-id", resource_group_name="test-rg", next_token="https://management.azure.com/next-page-url", ) assert result == vm_response client.http_request.assert_called_once_with( method="GET", full_url="https://management.azure.com/next-page-url", params={}, ) def test_list_vm_request_error(mocker, client): """ Given: An Azure client and a request to list virtual machines. When: The list_vm_request function is called and an exception is raised. Then: The function should call handle_azure_error. """ mocker.patch.object(client, "http_request", side_effect=Exception("test error")) mocker.patch.object(client, "handle_azure_error") client.list_vm_request( subscription_id="sub-id", resource_group_name="test-rg", next_token="", ) client.handle_azure_error.assert_called_once() def test_update_network_interface_request(mocker, client): """ Given: An Azure client and a request to update a network interface. When: The update_network_interface_request function is called with valid parameters. Then: The function should return the updated network interface information in the expected format. """ # Prepare mock response network_interface_response = { "name": "test-nic", "id": "/subscriptions/sub-id/resourceGroups/test-rg/providers/Microsoft.Network/networkInterfaces/test-nic", "properties": {"ipConfigurations": []}, } mocker.patch.object(client, "http_request", return_value=network_interface_response) result = client.update_network_interface_request( subscription_id="sub-id", resource_group_name="test-rg", interface_name="test-nic", network_interface_data={"properties": {"ipConfigurations": []}}, ) assert result == network_interface_response client.http_request.assert_called_once_with( method="PUT", full_url="https://management.azure.com/subscriptions/sub-id/resourceGroups/test-rg/providers/Microsoft.Network/networkInterfaces/test-nic", params={"api-version": "2023-05-01"}, json_data={"properties": {"ipConfigurations": []}}, ) def test_update_network_interface_request_error(mocker, client): """ Given: An Azure client and a request to update a network interface. When: The update_network_interface_request function is called and an exception is raised. Then: The function should call handle_azure_error. """ mocker.patch.object(client, "http_request", side_effect=Exception("test error")) mocker.patch.object(client, "handle_azure_error") client.update_network_interface_request( subscription_id="sub-id", resource_group_name="test-rg", interface_name="test-nic", network_interface_data={"properties": {"ipConfigurations": []}}, ) client.handle_azure_error.assert_called_once() def test_nsg_security_group_create_command(mocker, client, mock_params): """ Given: An Azure client and a request to create a network security group. When: The nsg_security_group_create_command function is called with valid parameters. Then: The function should return the created network security group information in the expected format. """ from Azure import nsg_security_group_create_command # Prepare mock response nsg_response = { "name": "test-nsg", "id": "/subscriptions/sub-id/resourceGroups/test-rg/providers/Microsoft.Network/networkSecurityGroups/test-nsg", "location": "eastus", "type": "Microsoft.Network/networkSecurityGroups", "etag": 'W/"test-etag"', "properties": {}, } mocker.patch.object(client, "create_network_security_group", return_value=nsg_response) args = { "subscription_id": "sub-id", "resource_group_name": "test-rg", "security_group_name": "test-nsg", "location": "eastus", } result = nsg_security_group_create_command(client=client, params=mock_params, args=args) assert result.outputs_prefix == "Azure.VirtualNetworks.SecurityGroups" assert result.outputs_key_field == "id" assert result.outputs == { "name": "test-nsg", "id": "/subscriptions/sub-id/resourceGroups/test-rg/providers/Microsoft.Network/networkSecurityGroups/test-nsg", "location": "eastus", "type": "Microsoft.Network/networkSecurityGroups", "etag": "test-etag", "properties": {}, } assert "The network security group test-nsg was created successfully" in result.readable_output client.create_network_security_group.assert_called_once_with( subscription_id="sub-id", resource_group_name="test-rg", security_group_name="test-nsg", location="eastus", ) def test_list_vm_command_success(mocker): """ Given: - Valid arguments for listing virtual machines. When: - Calling list_vm_command. Then: - Ensure the command returns the expected CommandResults object with the correct outputs. """ from Azure import list_vm_command client = mocker.MagicMock() client.list_vm_request.return_value = {"value": [{"id": "vm1", "name": "test-vm"}], "nextLink": "next_token_value"} args = { "subscription_id": "sub1", "resource_group_name": "rg1", } params = {} result = list_vm_command(client, params, args) assert result.outputs == { "Azure.Compute.VirtualMachines(val.id && val.id == obj.id)": [{"id": "vm1", "name": "test-vm"}], "Azure.Compute(true)": {"VirtualMachinesNextToken": "next_token_value"}, } client.list_vm_request.assert_called_once_with("sub1", "rg1", "") def test_list_vm_command_no_vms(mocker): """ Given: - Valid arguments but no virtual machines are returned. When: - Calling list_vm_command. Then: - Ensure the command returns a CommandResults object with a "No Virtual Machines found." message. """ from Azure import list_vm_command client = mocker.MagicMock() client.list_vm_request.return_value = {"value": []} args = {"subscription_id": "sub1", "resource_group_name": "rg1"} params = {} result = list_vm_command(client, params, args) assert result.readable_output == "No Virtual Machines found." client.list_vm_request.assert_called_once_with("sub1", "rg1", "") def test_network_interface_update_command_success(mocker): """ Given: - Valid arguments for updating a network interface. When: - Calling network_interface_update_command. Then: - Ensure the command returns the expected CommandResults object with the correct outputs. """ from Azure import network_interface_update_command client = mocker.MagicMock() client.get_network_interface_request.return_value = { "id": "nic1", "name": "test-nic", "location": "eastus", "properties": {"enableIPForwarding": False, "enableAcceleratedNetworking": False}, } client.update_network_interface_request.return_value = { "id": "nic1", "name": "test-nic", "location": "eastus", "etag": 'W/"etag-value"', "properties": { "enableIPForwarding": True, "enableAcceleratedNetworking": True, "dnsSettings": {"internalDnsNameLabel": "test-label", "dnsServers": ["1.1.1.1"]}, }, } args = { "subscription_id": "sub1", "resource_group_name": "rg1", "network_interface_name": "test-nic", "enable_ip_forwarding": "true", "enable_accelerate_networking": "true", "internal_dns_name_label": "test-label", "dns_servers": "1.1.1.1", } params = {} result = network_interface_update_command(client, params, args) assert result.outputs == { "id": "nic1", "name": "test-nic", "location": "eastus", "etag": "etag-value", "properties": { "enableIPForwarding": True, "enableAcceleratedNetworking": True, "dnsSettings": {"internalDnsNameLabel": "test-label", "dnsServers": ["1.1.1.1"]}, }, } assert result.outputs_prefix == "Azure.VirtualNetworks.NetworkInterfaces" assert result.outputs_key_field == "id" client.get_network_interface_request.assert_called_once_with("sub1", "rg1", "test-nic") client.update_network_interface_request.assert_called_once_with( subscription_id="sub1", resource_group_name="rg1", interface_name="test-nic", network_interface_data={ "id": "nic1", "name": "test-nic", "location": "eastus", "properties": { "enableIPForwarding": True, "enableAcceleratedNetworking": True, "dnsSettings": {"internalDnsNameLabel": "test-label", "dnsServers": ["1.1.1.1"]}, }, }, ) def test_network_interface_update_command_add_nsg(mocker): """ Given: - Valid arguments for updating a network interface, including adding a new NSG. When: - Calling network_interface_update_command. Then: - Ensure the command adds the NSG to the properties. """ from Azure import network_interface_update_command client = mocker.MagicMock() client.get_network_interface_request.return_value = {"id": "nic1", "name": "test-nic", "location": "eastus", "properties": {}} client.update_network_interface_request.return_value = { "id": "nic1", "name": "test-nic", "location": "eastus", "etag": 'W/"etag-value"', "properties": { "networkSecurityGroup": { "id": "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Network/networkSecurityGroups/nsg1" } }, } expected_updated_nic = { "id": "nic1", "name": "test-nic", "location": "eastus", "properties": { "networkSecurityGroup": { "id": "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Network/networkSecurityGroups/nsg1" } }, } args = { "subscription_id": "sub1", "resource_group_name": "rg1", "network_interface_name": "test-nic", "network_security_group_name": "nsg1", } params = {} result = network_interface_update_command(client, params, args) assert ( result.outputs.get("properties", {}).get("networkSecurityGroup", {}).get("id") == "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Network/networkSecurityGroups/nsg1" ) client.get_network_interface_request.assert_called_once_with("sub1", "rg1", "test-nic") client.update_network_interface_request.assert_called_once_with( subscription_id="sub1", resource_group_name="rg1", interface_name="test-nic", network_interface_data=expected_updated_nic ) def test_network_interface_update_command_remove_nsg(mocker): """ Given: - Valid arguments for updating a network interface, including removing the NSG. When: - Calling network_interface_update_command. Then: - Ensure the command removes the NSG from the properties. """ from Azure import network_interface_update_command client = mocker.MagicMock() client.get_network_interface_request.return_value = { "id": "nic1", "name": "test-nic", "location": "eastus", "properties": {"networkSecurityGroup": {"id": "nsg1"}}, } client.update_network_interface_request.return_value = { "id": "nic1", "name": "test-nic", "location": "eastus", "etag": 'W/"etag-value"', "properties": {}, } args = { "subscription_id": "sub1", "resource_group_name": "rg1", "network_interface_name": "test-nic", "remove_network_security_group": "yes", } params = {} result = network_interface_update_command(client, params, args) assert "networkSecurityGroup" not in result.outputs.get("properties", {}) client.get_network_interface_request.assert_called_once_with("sub1", "rg1", "test-nic") client.update_network_interface_request.assert_called_once_with( subscription_id="sub1", resource_group_name="rg1", interface_name="test-nic", network_interface_data={ "id": "nic1", "name": "test-nic", "location": "eastus", "properties": {}, }, ) def test_network_interface_update_command_conflict_nsg(mocker): """ Given: - Conflicting arguments for updating a network interface (both setting and removing NSG). When: - Calling network_interface_update_command. Then: - Ensure the command raises a DemistoException. """ from Azure import network_interface_update_command from CommonServerPython import DemistoException import pytest client = mocker.MagicMock() args = { "subscription_id": "sub1", "resource_group_name": "rg1", "network_interface_name": "test-nic", "network_security_group_name": "nsg1", "remove_network_security_group": "yes", } params = {} with pytest.raises( DemistoException, match="The remove_network_security_group option cannot be used with network_security_group_name." ): network_interface_update_command(client, params, args) def test_test_module_device_code_flow(mocker): """ Given: A client configured with the Device Code authentication type. When: test_module is called (Test button). Then: A DemistoException is raised, because the Device Code token lives in the saved instance context and the Test button cannot validate it. The user is directed to `!azure-auth-test`. No API call is made. """ client = AzureClient(app_id="test_app_id", connection_type="Device Code") mock_http = mocker.patch.object(client, "http_request", return_value={}) with pytest.raises(DemistoException) as excinfo: Azure.test_module(client) assert "azure-auth-test" in str(excinfo.value) mock_http.assert_not_called() def test_test_module_authorization_code_flow(mocker): """ Given: A client configured with the Authorization Code authentication type. When: test_module is called (Test button). Then: It validates directly via the roleAssignments call (the authorization code is in the instance parameters, so a token can be obtained on demand) and returns "ok". """ client = AzureClient(app_id="test_app_id", connection_type="Authorization Code") mock_http = mocker.patch.object(client, "http_request", return_value={}) assert Azure.test_module(client) == "ok" mock_http.assert_called_once() def test_test_module_client_credentials_ok(mocker, client): """ Given: A client configured with the Client Credentials flow (default) and a successful API call. When: test_module is called. Then: It returns "ok". """ mocker.patch.object(client, "connection_type", "Client Credentials") mocker.patch.object(client, "http_request", return_value={}) assert Azure.test_module(client) == "ok" def test_test_module_managed_identities_uses_resource_groups(mocker): """ Given: A client configured with the Azure Managed Identities authentication type. When: test_module is called (Test button). Then: It validates via the lightweight resource-groups list call (not the roleAssignments call), because a Managed Identity often lacks the roleAssignments/read permission while still being able to run other commands. It returns "ok". """ client = AzureClient(app_id="test_app_id", connection_type="Azure Managed Identities") client.subscription_id = "sub-123" mock_http = mocker.patch.object(client, "http_request", return_value={}) assert Azure.test_module(client) == "ok" mock_http.assert_called_once() _, kwargs = mock_http.call_args assert kwargs["full_url"].endswith("/subscriptions/sub-123/resourcegroups") assert "roleAssignments" not in kwargs["full_url"] def test_test_connection_success(mocker, client): """ Given: A client whose MicrosoftClient can fetch an access token. When: test_connection is called. Then: A success message is returned. """ mocker.patch.object(client.ms_client, "get_access_token") assert "Success" in Azure.test_connection(client) def test_start_auth(mocker, client): """ Given: A client. When: start_auth is called. Then: It returns CommandResults wrapping the MicrosoftClient start_auth output. """ mocker.patch.object(client.ms_client, "start_auth", return_value="follow these steps") result = Azure.start_auth(client) assert result.readable_output == "follow these steps" def test_complete_auth(mocker, client): """ Given: A client. When: complete_auth is called. Then: It fetches the access token and returns a success message. """ mock_get_token = mocker.patch.object(client.ms_client, "get_access_token") result = Azure.complete_auth(client) mock_get_token.assert_called_once() assert "completed successfully" in result def test_get_azure_client_device_code_no_secret(mocker, mock_params): """ Given: Marketplace path (no connector) with the Device Code flow and no Client Secret. When: get_azure_client is called. Then: It does NOT raise the missing-secret error and builds a client with the Device Code connection type. """ mocker.patch("Azure.get_connector_id", return_value=None) mocker.patch("Azure.get_azure_managed_identities_client_id", return_value=None) mock_azure_client = mocker.patch("Azure.AzureClient", return_value=mocker.Mock()) params = mock_params.copy() params["credentials"] = {} params["auth_type"] = "Device Code" get_azure_client(params, {}, "command") # The connection_type must be propagated to the client (no missing-secret exception raised). _, kwargs = mock_azure_client.call_args assert kwargs["connection_type"] == "Device Code" def test_get_azure_client_marketplace_storage_scope(mocker, mock_params): """ Given: Marketplace path (no connector), Client Credentials, and a storage-container command. When: get_azure_client is called. Then: The AzureClient is built with the STORAGE scope and STORAGE resource (not the management scope), so the storage-scoped token is requested. Regression test for the scope-override bug. """ from Azure import STORAGE_SCOPE, STORAGE_RESOURCE mocker.patch("Azure.get_connector_id", return_value=None) mocker.patch("Azure.get_azure_managed_identities_client_id", return_value=None) mock_azure_client = mocker.patch("Azure.AzureClient", return_value=mocker.Mock()) params = mock_params.copy() params["credentials"] = {"password": "secret"} params["auth_type"] = "Client Credentials" get_azure_client(params, {}, "azure-storage-container-create") _, kwargs = mock_azure_client.call_args assert kwargs["scope"] == STORAGE_SCOPE assert kwargs["resource"] == STORAGE_RESOURCE assert kwargs["connection_type"] == "Client Credentials" def test_azure_client_client_credentials_does_not_send_resource_to_v2_endpoint(mocker): """ Given: A Client Credentials client (uses the v2.0 token endpoint with a `.default` scope). When: AzureClient builds the MicrosoftClient. Then: No `resource` is forwarded to MicrosoftClient, so the token request sends only `scope`. Sending both `scope` and `resource` to the v2.0 endpoint causes Microsoft to return "invalid_target: The resource parameter provided in the request doesn't match with the requested scopes". Only Device Code (v1.0-style) uses `resource`. """ from Azure import DEFAULT_SCOPE, DEFAULT_RESOURCE captured = {} def fake_ms_client(**kwargs): captured.update(kwargs) return mocker.Mock() mocker.patch("Azure.MicrosoftClient", side_effect=fake_ms_client) AzureClient( app_id="app", connection_type="Client Credentials", tenant_id="my-tenant", scope=DEFAULT_SCOPE, resource=DEFAULT_RESOURCE, ) assert captured["scope"] == DEFAULT_SCOPE # resource must not be forwarded for the v2.0 client-credentials flow. assert captured.get("resource") is None def test_azure_client_device_code_default_resource_derives_management_scope(mocker): """ Given: A Device Code client for a management (default) command, i.e. resource=DEFAULT_RESOURCE. When: AzureClient builds the MicrosoftClient. Then: The MicrosoftClient receives the management resource (no trailing slash) and a Device Code delegated scope derived from it. Regression test: the branch derives scope/resource from the per-command resource instead of hardcoding management-only values. """ from Azure import DEFAULT_RESOURCE captured = {} def fake_ms_client(**kwargs): captured.update(kwargs) return mocker.Mock() mocker.patch("Azure.MicrosoftClient", side_effect=fake_ms_client) AzureClient(app_id="app", connection_type="Device Code", resource=DEFAULT_RESOURCE) expected_resource = DEFAULT_RESOURCE.rstrip("/") assert captured["resource"] == expected_resource assert captured["scope"] == f"{expected_resource}/user_impersonation offline_access user.read" assert captured["token_retrieval_url"] is not None def test_azure_client_device_code_storage_resource_derives_storage_scope(mocker): """ Given: A Device Code client for a storage-container command, i.e. resource=STORAGE_RESOURCE. When: AzureClient builds the MicrosoftClient. Then: The MicrosoftClient receives the storage resource (no trailing slash) and a Device Code delegated scope derived from it, so storage commands are storage-scoped under Device Code auth instead of always management-scoped. """ from Azure import STORAGE_RESOURCE captured = {} def fake_ms_client(**kwargs): captured.update(kwargs) return mocker.Mock() mocker.patch("Azure.MicrosoftClient", side_effect=fake_ms_client) AzureClient(app_id="app", connection_type="Device Code", resource=STORAGE_RESOURCE) expected_resource = STORAGE_RESOURCE.rstrip("/") assert captured["resource"] == expected_resource assert captured["scope"] == f"{expected_resource}/user_impersonation offline_access user.read" assert captured["token_retrieval_url"] is not None def test_azure_client_client_credentials_gov_endpoint_builds_gov_token_url(mocker): """ Given: A Client Credentials client configured with a US Gov Azure AD endpoint (https://login.microsoftonline.us). When: AzureClient builds the MicrosoftClient. Then: The token_retrieval_url points to the same (gov) authority so the confidential-client token request is not sent cross-cloud. Regression test for the Microsoft error "Confidential Client is not supported in Cross Cloud request". """ captured = {} def fake_ms_client(**kwargs): captured.update(kwargs) return mocker.Mock() mocker.patch("Azure.MicrosoftClient", side_effect=fake_ms_client) AzureClient( app_id="app", connection_type="Client Credentials", tenant_id="my-tenant", azure_ad_endpoint="https://login.microsoftonline.us", ) assert captured["token_retrieval_url"] == "https://login.microsoftonline.us/my-tenant/oauth2/v2.0/token" def test_azure_client_client_credentials_default_endpoint_builds_commercial_token_url(mocker): """ Given: A Client Credentials client using the default (commercial) Azure AD endpoint. When: AzureClient builds the MicrosoftClient. Then: The token_retrieval_url points to the commercial login.microsoftonline.com authority. """ from Azure import DEFAULT_AZURE_AD_ENDPOINT captured = {} def fake_ms_client(**kwargs): captured.update(kwargs) return mocker.Mock() mocker.patch("Azure.MicrosoftClient", side_effect=fake_ms_client) AzureClient( app_id="app", connection_type="Client Credentials", tenant_id="my-tenant", azure_ad_endpoint=DEFAULT_AZURE_AD_ENDPOINT, ) assert captured["token_retrieval_url"] == "https://login.microsoftonline.com/my-tenant/oauth2/v2.0/token" def test_azure_client_managed_identities_passes_mi_args_to_ms_client(mocker): """ Given: A client constructed with the Azure Managed Identities flow and a user-assigned client ID. When: AzureClient builds the MicrosoftClient. Then: The MicrosoftClient receives the managed_identities_client_id and the management Azure resource URI, matching the reference Azure packs (e.g. AzureNetworkSecurityGroups). The grant_type is None (the managed-identities path is selected by managed_identities_client_id, not by grant_type). No device-code token URL is set. """ from Azure import DEFAULT_RESOURCE, DEFAULT_SCOPE captured = {} def fake_ms_client(**kwargs): captured.update(kwargs) return mocker.Mock() mocker.patch("Azure.MicrosoftClient", side_effect=fake_ms_client) AzureClient( app_id="app", connection_type="Azure Managed Identities", managed_identities_client_id="my-mi-client-id", scope=DEFAULT_SCOPE, resource=DEFAULT_RESOURCE, ) assert captured["managed_identities_client_id"] == "my-mi-client-id" # The MI resource URI is the management Azure resource with the trailing slash stripped # (the MI branch derives it from `(resource or DEFAULT_RESOURCE).rstrip("/")`). assert captured["managed_identities_resource_uri"] == DEFAULT_RESOURCE.rstrip("/") assert captured["managed_identities_resource_uri"] == "https://management.azure.com" # Managed Identities is not a grant_type flow; the path is chosen by managed_identities_client_id. assert captured.get("grant_type") is None # Device-code-only token retrieval URL must not be set for the MI flow. assert captured.get("token_retrieval_url") is None def test_azure_client_client_credentials_empty_tenant_builds_token_url_without_none(mocker): """ Given: A non-device-code client (Client Credentials) with no tenant_id configured. When: AzureClient builds the MicrosoftClient. Then: The token_retrieval_url is built with an empty tenant segment (no literal "None" in the URL). Regression test for None stringification in the token URL. """ from Azure import DEFAULT_AZURE_AD_ENDPOINT captured = {} def fake_ms_client(**kwargs): captured.update(kwargs) return mocker.Mock() mocker.patch("Azure.MicrosoftClient", side_effect=fake_ms_client) AzureClient( app_id="app", connection_type="Client Credentials", tenant_id=None, azure_ad_endpoint=DEFAULT_AZURE_AD_ENDPOINT, ) assert "None" not in captured["token_retrieval_url"] # urljoin collapses the empty tenant segment, so the authority host is followed by a single slash. assert captured["token_retrieval_url"] == "https://login.microsoftonline.com/oauth2/v2.0/token" def test_azure_client_managed_identities_storage_resource_derives_storage_uri(mocker): """ Given: A Managed Identities client for a storage-container command (per-command resource is the storage resource). When: AzureClient builds the MicrosoftClient. Then: managed_identities_resource_uri is the storage resource (not the management default), so the MI token is storage-scoped. Regression test for storage commands failing with 401/403 under MI auth because the token was always management-scoped. """ from Azure import STORAGE_RESOURCE captured = {} def fake_ms_client(**kwargs): captured.update(kwargs) return mocker.Mock() mocker.patch("Azure.MicrosoftClient", side_effect=fake_ms_client) AzureClient( app_id="app", connection_type="Azure Managed Identities", managed_identities_client_id="my-mi-client-id", resource=STORAGE_RESOURCE, ) assert captured["managed_identities_resource_uri"] == STORAGE_RESOURCE.rstrip("/") assert captured["managed_identities_resource_uri"] == "https://storage.azure.com" def test_get_azure_client_managed_identities_resolves_client_id(mocker, mock_params): """ Given: Marketplace path (no connector) with auth_type "Azure Managed Identities" and a configured managed_identities_client_id credential. When: get_azure_client is called. Then: get_azure_managed_identities_client_id resolves the client ID from params and it is passed to AzureClient. The Client Credentials missing-secret guard is NOT triggered (no Client Secret required for the MI flow). """ mocker.patch("Azure.get_connector_id", return_value=None) mock_azure_client = mocker.patch("Azure.AzureClient", return_value=mocker.Mock()) params = mock_params.copy() params["credentials"] = {} # no client secret configured params["auth_type"] = "Azure Managed Identities" params["managed_identities_client_id"] = {"password": "resolved-mi-id"} get_azure_client(params, {}, "command") _, kwargs = mock_azure_client.call_args assert kwargs["connection_type"] == "Azure Managed Identities" assert kwargs["managed_identities_client_id"] == "resolved-mi-id" def test_get_azure_client_managed_identities_system_assigned(mocker, mock_params): """ Given: Marketplace path with auth_type "Azure Managed Identities" and no client ID configured. When: get_azure_client is called. Then: The system-assigned managed identity sentinel is resolved and passed to AzureClient. """ from MicrosoftApiModule import MANAGED_IDENTITIES_SYSTEM_ASSIGNED mocker.patch("Azure.get_connector_id", return_value=None) mock_azure_client = mocker.patch("Azure.AzureClient", return_value=mocker.Mock()) params = mock_params.copy() params["credentials"] = {} params["auth_type"] = "Azure Managed Identities" params["managed_identities_client_id"] = {} # no client id -> system assigned get_azure_client(params, {}, "command") _, kwargs = mock_azure_client.call_args assert kwargs["managed_identities_client_id"] == MANAGED_IDENTITIES_SYSTEM_ASSIGNED def test_get_azure_client_credentials_none(mocker, mock_params): """ Given: Marketplace path, Client Credentials flow, and credentials explicitly set to None. When: get_azure_client is called. Then: It raises a missing-parameter DemistoException (listing the Client Secret) without an AttributeError. """ mocker.patch("Azure.get_connector_id", return_value=None) params = mock_params.copy() params["credentials"] = None params["auth_type"] = "Client Credentials" with pytest.raises(DemistoException) as excinfo: get_azure_client(params, {}, "command") assert "Client Secret" in str(excinfo.value) assert "Client Credentials" in str(excinfo.value) @pytest.mark.parametrize( "auth_type, missing_key, expected_in_message", [ ("Client Credentials", "app_id", "Application ID"), ("Client Credentials", "tenant_id", "Tenant ID"), ("Client Credentials", "credentials", "Client Secret"), ("Client Credentials", "subscription_id", "Default Subscription ID"), ("Device Code", "app_id", "Application ID"), ("Device Code", "subscription_id", "Default Subscription ID"), ("Authorization Code", "app_id", "Application ID"), ("Authorization Code", "redirect_uri", "Application redirect URI"), ("Authorization Code", "auth_code", "Authorization code"), ("Authorization Code", "subscription_id", "Default Subscription ID"), ("Azure Managed Identities", "subscription_id", "Default Subscription ID"), ], ) def test_validate_auth_params_missing(auth_type, missing_key, expected_in_message): """ Given: A full set of params for an auth type, with exactly one mandatory param removed. When: validate_auth_params is called. Then: It raises a DemistoException naming the missing parameter and the auth type. """ from Azure import validate_auth_params full_params = { "app_id": "app", "subscription_id": "sub", "tenant_id": "tenant", "credentials": {"password": "secret"}, "auth_code": {"password": "code"}, "redirect_uri": "redirect-uri", "managed_identities_client_id": {"password": "mi-id"}, "auth_type": auth_type, } full_params.pop(missing_key) with pytest.raises(DemistoException) as excinfo: validate_auth_params(full_params, auth_type) assert expected_in_message in str(excinfo.value) assert auth_type in str(excinfo.value) @pytest.mark.parametrize( "auth_type, params", [ ( "Client Credentials", {"app_id": "app", "subscription_id": "sub", "tenant_id": "t", "credentials": {"password": "s"}}, ), ("Device Code", {"app_id": "app", "subscription_id": "sub"}), ( "Authorization Code", { "app_id": "app", "subscription_id": "sub", "redirect_uri": "redirect-uri", "auth_code": {"password": "c"}, }, ), ( "Azure Managed Identities", {"subscription_id": "sub", "managed_identities_client_id": {"password": "mi-id"}}, ), ], ) def test_validate_auth_params_valid(auth_type, params): """ Given: A complete set of mandatory params for an auth type. When: validate_auth_params is called. Then: It does not raise. """ from Azure import validate_auth_params params = {**params, "auth_type": auth_type} validate_auth_params(params, auth_type) # Should not raise def test_validate_auth_params_managed_identities_system_assigned(): """ Given: Azure Managed Identities with no explicit client ID (system-assigned) and a subscription. When: validate_auth_params is called. Then: It does not raise, because the system-assigned identity resolves to a sentinel client ID. """ from Azure import validate_auth_params params = { "auth_type": "Azure Managed Identities", "subscription_id": "sub", "managed_identities_client_id": {}, # no password -> system assigned } validate_auth_params(params, "Azure Managed Identities") # Should not raise def test_main_auth_reset(mocker): """ Given: The azure-auth-reset command on the marketplace path. When: main is called. Then: reset_auth is invoked and the client is not built. """ from Azure import main mocker.patch.object(demisto, "command", return_value="azure-auth-reset") mocker.patch.object(demisto, "params", return_value={}) mocker.patch.object(demisto, "args", return_value={}) mocker.patch("Azure.get_connector_id", return_value=None) mock_reset = mocker.patch("Azure.reset_auth", return_value="reset done") mock_get_client = mocker.patch("Azure.get_azure_client") mocker.patch("Azure.return_results") main() mock_reset.assert_called_once() mock_get_client.assert_not_called() # --------------------------------------------------------------------------- # YAML <-> Python wiring tests # # These tests read Azure.yml, extract the command names, argument names and # output prefixes, and assert that each one is actually wired up in Azure.py. # # Everything below is derived *statically* (yaml.safe_load + ast.parse). No # integration code is imported, instantiated or executed, so these tests make # no network calls, read no environment variables and do not depend on the # clock, the OS or the execution order of other tests. # --------------------------------------------------------------------------- # The name of the dict inside main() that maps command name -> handler function. DISPATCH_DICT_NAME = "commands_with_params_and_args" # Commands intentionally excluded from these wiring tests. # # - "*-quick-action" commands and "test-module" are excluded by request. # - Commands marked "deprecated: true" in the yml are excluded as well. They are # kept only so existing playbooks keep working, and are no longer expected to # hold their yml and their implementation in step. # - The auth/control commands below are routed by explicit if/elif branches in # main() rather than through the dispatch dict, and expose no yml arguments # or context outputs of the kind these tests inspect. QUICK_ACTION_SUFFIX = "-quick-action" # Arguments that are legitimately not read by the command handler. # # These are consumed earlier in main(), when the AzureClient itself is built by # get_azure_client(), and are also resolvable from the integration parameters # rather than the command arguments. A handler may therefore never mention them # even though the argument is fully wired up and honoured at runtime, so flagging # them here would be a false positive rather than a real defect. INFRASTRUCTURE_ARGUMENTS = frozenset( { "subscription_id", "resource_group_name", } ) EXCLUDED_COMMANDS = frozenset( { "test-module", "azure-auth-start", "azure-auth-complete", "azure-auth-test", "azure-auth-reset", "azure-generate-login-url", } ) def is_command_in_scope(command_name: str) -> bool: """Return True if the given command should be covered by the wiring tests. This is the single source of truth for test scope - every wiring test below filters through it, so the scope cannot drift between tests. Args: command_name (str): The command name as declared in Azure.yml or used as a key in the dispatch dict, for example "azure-storage-container-create". Returns: bool: True if the command should be checked by the wiring tests. False for "*-quick-action" commands, "test-module", and the auth/control commands listed in EXCLUDED_COMMANDS. """ if command_name in EXCLUDED_COMMANDS: return False return not command_name.endswith(QUICK_ACTION_SUFFIX) def load_raw_yml_commands() -> list[dict]: """Load Azure.yml and return its command list exactly as declared. Use the ``raw_yml_commands`` fixture rather than calling this directly, so Azure.yml is read and parsed only once for the whole module. Returns: list[dict]: Every command definition in the yml, with no filtering applied. """ with YML_PATH.open(encoding="utf-8") as yml_file: yml_content = yaml.safe_load(yml_file) return yml_content.get("script", {}).get("commands") or [] def select_deprecated_command_names(raw_commands: list[dict]) -> set[str]: """Return the names of the commands marked ``deprecated: true``. Deprecated commands are excluded from the wiring tests, but they are still routed in main(), so the names are needed to keep test_dispatch_commands_exist_in_yml from reporting them as undocumented. Args: raw_commands (list[dict]): The unfiltered yml command list. Returns: set[str]: The names of every command whose yml definition sets ``deprecated: true`` at the command level. """ return {command["name"] for command in raw_commands if command.get("deprecated") is True} def select_in_scope_commands(raw_commands: list[dict]) -> dict[str, dict]: """Return the in-scope commands keyed by command name. Args: raw_commands (list[dict]): The unfiltered yml command list. Returns: dict[str, dict]: Mapping of command name to the raw yml command definition (including its "arguments" and "outputs" entries). Out-of-scope commands are filtered out via is_command_in_scope, and commands marked ``deprecated: true`` are dropped as well, since a deprecated command is no longer expected to keep its yml and its implementation in step. """ return { command["name"]: command for command in raw_commands if is_command_in_scope(command.get("name", "")) and command.get("deprecated") is not True } def visible_arguments(command: dict) -> list[dict]: """Return a command's declared arguments, excluding the hidden ones. Arguments marked ``hidden: true`` are not offered to the user, so they are outside the yml <-> py contract these tests enforce in either direction. Args: command (dict): A raw yml command definition. Returns: list[dict]: The command's argument definitions that are not marked hidden. """ return [argument for argument in command.get("arguments") or [] if argument.get("hidden") is not True] def load_py_source_and_tree() -> tuple[str, ast.Module]: """Read Azure.py and return its source text along with the parsed AST. Args: None. The Azure.py path is derived from this test file's own location. Returns: tuple[str, ast.Module]: The raw source text of Azure.py and its parsed AST. The module is only parsed, never imported or executed. """ source = PY_PATH.read_text(encoding="utf-8") return source, ast.parse(source) def extract_dispatch_map(tree: ast.Module) -> dict[str, str]: """Extract the command -> handler-function-name mapping from main(). The dispatch dict is a local variable inside main(), which is marked "# pragma: no cover" and cannot be imported or safely executed, so it is lifted straight out of the AST instead. Args: tree (ast.Module): The parsed AST of Azure.py. Returns: dict[str, str]: Mapping of command name to the name of the handler function it is routed to, for example {"azure-storage-container-create": "storage_container_create_command"}. Raises: AssertionError: If the dispatch dict cannot be found in Azure.py, if any of its entries is in a shape this extractor cannot read (the offending entries are named in the message), or if it is found but yields no command -> handler pairs at all. In every case the dict shape has changed and this helper needs updating. """ for node in ast.walk(tree): if not isinstance(node, ast.Assign): continue targets = [t.id for t in node.targets if isinstance(t, ast.Name)] if DISPATCH_DICT_NAME not in targets or not isinstance(node.value, ast.Dict): continue dispatch: dict[str, str] = {} unreadable: list[str] = [] for key, value in zip(node.value.keys, node.value.values): if isinstance(key, ast.Constant) and isinstance(key.value, str) and isinstance(value, ast.Name): dispatch[key.value] = value.id else: # A dict-unpacking entry (**other) has no key node at all. key_source = "**" if key is None else ast.unparse(key) unreadable.append(f"{key_source}: {ast.unparse(value)}") # Guard against the dict being found but containing entries this extractor # cannot read (e.g. values changed to lambdas or partials). Dropping them # silently would report their commands as "not routed", wrongly blaming the # integration instead of the extractor, so name them explicitly instead. assert not unreadable, ( f"Found '{DISPATCH_DICT_NAME}' in Azure.py but could not read the following " "entries, so the commands they route would be wrongly reported as unrouted. " "The dict shape has changed and this test helper needs updating:\n" + "\n".join(unreadable) ) assert dispatch, ( f"Found '{DISPATCH_DICT_NAME}' in Azure.py but could not extract any " "command -> handler pairs from it. The dict shape has changed and this " "test helper needs updating." ) return dispatch raise AssertionError(f"Could not find the '{DISPATCH_DICT_NAME}' dict inside Azure.py") def build_symbol_index(tree: ast.Module) -> dict[str, ast.FunctionDef]: """Index module-level functions and AzureClient methods by name. Client methods are indexed under their bare name so that a handler calling ``client.storage_account_update_request(...)`` can be resolved. Args: tree (ast.Module): The parsed AST of Azure.py. Returns: dict[str, ast.FunctionDef]: Mapping of function/method name to its AST node. Module-level functions take precedence over class methods of the same name, since a bare call in a handler resolves to the module-level one. """ index: dict[str, ast.FunctionDef] = {} for node in tree.body: if isinstance(node, ast.FunctionDef): index[node.name] = node elif isinstance(node, ast.ClassDef): for child in node.body: if isinstance(child, ast.FunctionDef): index.setdefault(child.name, child) return index def collect_called_names(func_node: ast.FunctionDef) -> set[str]: """Return the names of every function/method directly called by func_node. Args: func_node (ast.FunctionDef): The AST node of the function to inspect. Returns: set[str]: The bare names of all called callables. Attribute calls contribute only the final attribute, so ``client.get_rule(...)`` yields "get_rule", which is what allows AzureClient methods to be looked up in the symbol index. """ called: set[str] = set() for node in ast.walk(func_node): if not isinstance(node, ast.Call): continue func = node.func if isinstance(func, ast.Name): called.add(func.id) elif isinstance(func, ast.Attribute): called.add(func.attr) return called def collect_string_constants(func_node: ast.FunctionDef) -> set[str]: """Return every string literal appearing anywhere inside func_node. Args: func_node (ast.FunctionDef): The AST node of the function to inspect. Returns: set[str]: All string constants in the function body, including argument keys such as "account_name" and docstring text. Docstrings may add harmless extra entries; they can only mask a failure, never invent one. """ return {node.value for node in ast.walk(func_node) if isinstance(node, ast.Constant) and isinstance(node.value, str)} def collect_reachable_strings(handler_name: str, symbol_index: dict[str, ast.FunctionDef]) -> set[str]: """Collect string literals in a handler plus those in its direct callees. One level of call following is required for correctness: several handlers (for example storage_account_update_command) hand the raw ``args`` dict to an AzureClient method, and it is that method - not the handler - which reads the individual argument keys. Args: handler_name (str): Name of the command handler function to start from. symbol_index (dict[str, ast.FunctionDef]): Index produced by build_symbol_index, used to resolve both the handler and the functions it calls. Returns: set[str]: Union of the string literals in the handler and in every function it calls directly (one level deep). Returns an empty set if the handler name is not present in the index. """ handler = symbol_index.get(handler_name) if handler is None: return set() strings = collect_string_constants(handler) for callee_name in collect_called_names(handler): callee = symbol_index.get(callee_name) if callee is not None and callee is not handler: strings |= collect_string_constants(callee) return strings def collect_read_argument_names(func_node: ast.FunctionDef) -> set[str]: """Return the command-argument names a function reads out of its ``args`` mapping. Unlike collect_string_constants, which returns every string literal, this looks only at the three shapes Azure.py uses to read a command argument: ``args.get("name")``, ``args["name"]`` and ``"name" in args``. That precision is what makes it safe to assert in the yml -> py direction: an unrelated literal such as a URL fragment or a response key can never be mistaken for a command argument. Args: func_node (ast.FunctionDef): The AST node of the function to inspect. Returns: set[str]: The argument names read from the ``args`` mapping. Dynamic reads such as ``args.get(key)`` contribute nothing, since the name is not a literal. """ read: set[str] = set() for node in ast.walk(func_node): # args.get("name") / args.get("name", default) if ( isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) and node.func.attr == "get" and isinstance(node.func.value, ast.Name) and node.func.value.id == "args" and node.args and isinstance(node.args[0], ast.Constant) and isinstance(node.args[0].value, str) ): read.add(node.args[0].value) # args["name"] elif ( isinstance(node, ast.Subscript) and isinstance(node.value, ast.Name) and node.value.id == "args" and isinstance(node.slice, ast.Constant) and isinstance(node.slice.value, str) ): read.add(node.slice.value) # "name" in args elif isinstance(node, ast.Compare) and isinstance(node.left, ast.Constant) and isinstance(node.left.value, str): for operator, comparator in zip(node.ops, node.comparators): if isinstance(operator, ast.In) and isinstance(comparator, ast.Name) and comparator.id == "args": read.add(node.left.value) return read def collect_reachable_read_arguments(handler_name: str, symbol_index: dict[str, ast.FunctionDef]) -> set[str]: """Collect the argument names read by a handler and by its direct callees. Mirrors collect_reachable_strings, but uses collect_read_argument_names so that only genuine ``args`` reads are returned. One level of call following is required for the same reason: several handlers pass the raw ``args`` dict to an AzureClient method, and it is that method which reads the individual keys. Args: handler_name (str): Name of the command handler function to start from. symbol_index (dict[str, ast.FunctionDef]): Index produced by build_symbol_index. Returns: set[str]: Argument names read by the handler or by any function it calls directly. Empty if the handler name is not present in the index. """ handler = symbol_index.get(handler_name) if handler is None: return set() read = collect_read_argument_names(handler) for callee_name in collect_called_names(handler): callee = symbol_index.get(callee_name) if callee is not None and callee is not handler: read |= collect_read_argument_names(callee) return read def extract_fallback_prefix(handler_name: str, symbol_index: dict[str, ast.FunctionDef]) -> set[str]: """Return the output prefixes a handler can produce, ignoring the lookup map. Covers both shapes used in Azure.py: the ``COMMANDS_TO_OUTPUTS_PREFIX.get(command, "")`` pattern and a prefix passed directly as ``outputs_prefix=``. Args: handler_name (str): Name of the command handler function to inspect. symbol_index (dict[str, ast.FunctionDef]): Index produced by build_symbol_index. Returns: set[str]: Every context prefix the handler may write to, for example {"Azure.VirtualNetworks.SecurityRules"}. Empty if the handler is unknown or builds its context another way, such as returning a plain outputs dict. """ handler = symbol_index.get(handler_name) if handler is None: return set() prefixes: set[str] = set() for node in ast.walk(handler): if not isinstance(node, ast.Call): continue # COMMANDS_TO_OUTPUTS_PREFIX.get(command, "Azure.Something") func = node.func if ( isinstance(func, ast.Attribute) and func.attr == "get" and isinstance(func.value, ast.Name) and func.value.id == "COMMANDS_TO_OUTPUTS_PREFIX" and len(node.args) == 2 and isinstance(node.args[1], ast.Constant) and isinstance(node.args[1].value, str) ): prefixes.add(node.args[1].value) # CommandResults(outputs_prefix="Azure.Something", ...) for keyword in node.keywords: if ( keyword.arg == "outputs_prefix" and isinstance(keyword.value, ast.Constant) and isinstance(keyword.value.value, str) ): prefixes.add(keyword.value.value) return prefixes @pytest.fixture(scope="module") def raw_yml_commands() -> list[dict]: """Every command declared in Azure.yml, unfiltered. Azure.yml is read and parsed here once per module, and every other yml-derived fixture is built from this one rather than re-reading the file. """ return load_raw_yml_commands() @pytest.fixture(scope="module") def yml_commands(raw_yml_commands: list[dict]) -> dict[str, dict]: """The in-scope, non-deprecated commands declared in Azure.yml, keyed by command name.""" return select_in_scope_commands(raw_yml_commands) @pytest.fixture(scope="module") def deprecated_commands(raw_yml_commands: list[dict]) -> set[str]: """The names of the commands marked deprecated in Azure.yml.""" return select_deprecated_command_names(raw_yml_commands) @pytest.fixture(scope="module") def py_tree() -> ast.Module: """The parsed AST of Azure.py.""" _, tree = load_py_source_and_tree() return tree @pytest.fixture(scope="module") def dispatch_map(py_tree: ast.Module) -> dict[str, str]: """Mapping of command name -> handler function name, lifted from main().""" return extract_dispatch_map(py_tree) @pytest.fixture(scope="module") def symbol_index(py_tree: ast.Module) -> dict[str, ast.FunctionDef]: """Index of module-level functions and AzureClient methods by name.""" return build_symbol_index(py_tree) # --------------------------------------------------------------------------- # The wiring tests themselves: Azure.yml checked against Azure.py. # --------------------------------------------------------------------------- def test_py_read_arguments_are_declared_in_yml(yml_commands, raw_yml_commands, dispatch_map, symbol_index): """ Given: - yml_commands (dict[str, dict]): The arguments declared for each in-scope command in Azure.yml. - raw_yml_commands (list[dict]): Every command in the yml, used to resolve arguments declared only by a deprecated sibling that shares a handler. - dispatch_map (dict[str, str]): The handler each command is routed to. - symbol_index (dict[str, ast.FunctionDef]): Used to resolve the handler and its direct callees. When: - Every argument the handler reads out of its args mapping is looked up in the command's declared yml arguments. Then: - No argument is consumed by the code without being documented, so a user can discover every argument the command honours. This is the reverse of test_command_arguments_are_read_by_handler, which checks the yml -> py direction. - Infrastructure arguments resolvable from the integration parameters are exempt, matching the exemption applied in the other direction. An argument declared ``hidden: true`` still counts as declared, so reading it is not a failure. - When several commands share one handler, an argument declared by any of them counts as declared for all of them, including by a deprecated sibling. A shared handler routinely reads a renamed argument on behalf of its deprecated predecessor - for example update_security_rule_command reads "action" for azure-nsg-security-rule-update, while its replacement declares "access" - and blaming the current command for that alias would be a false positive. """ # Given: a cache so each handler's reachable argument reads are computed once reachable_reads_cache: dict[str, set[str]] = {} undeclared_arguments: list[str] = [] # Given: every argument name declared by any command sharing the same handler. # This spans the raw yml rather than the filtered map, so an argument kept only for # a deprecated sibling still counts as declared for the handler they share. declared_per_handler: dict[str, set[str]] = {} for command in raw_yml_commands: handler_name = dispatch_map.get(command.get("name", "")) if handler_name is None: continue declared_per_handler.setdefault(handler_name, set()).update( argument.get("name") for argument in command.get("arguments") or [] ) for command_name in sorted(yml_commands): handler_name = dispatch_map.get(command_name) if handler_name is None: continue # covered by test_yml_commands_are_wired_in_dispatch if handler_name not in reachable_reads_cache: reachable_reads_cache[handler_name] = collect_reachable_read_arguments(handler_name, symbol_index) declared = declared_per_handler.get(handler_name, set()) # When: checking each argument the code reads against the declared ones for argument_name in sorted(reachable_reads_cache[handler_name]): if argument_name in declared or argument_name in INFRASTRUCTURE_ARGUMENTS: continue undeclared_arguments.append(f"{command_name}: '{argument_name}' (handler: {handler_name})") # Then: every argument the code consumes is documented in the yml assert not undeclared_arguments, "Arguments read by the command handler but not declared in Azure.yml:\n" + "\n".join( undeclared_arguments ) def test_yml_commands_are_wired_in_dispatch(yml_commands, dispatch_map, symbol_index): """ Given: - yml_commands (dict[str, dict]): The in-scope commands declared in Azure.yml. - dispatch_map (dict[str, str]): The command dispatch dict extracted from main() in Azure.py. - symbol_index (dict[str, ast.FunctionDef]): The index of every function and method defined in Azure.py. When: - Each yml command name is looked up in the dispatch dict, and the handler it points to is looked up in the symbol index. Then: - Every command resolves to a handler, so none would raise NotImplementedError at runtime. - Every resolved handler actually exists as a function in Azure.py. """ # Given: the yml command names and the dispatch table yml_command_names = set(yml_commands) # When: resolving each command to its handler, then confirming that handler is # a real function in Azure.py rather than a stale or dangling name unrouted = sorted(name for name in yml_command_names if name not in dispatch_map) missing_handlers = sorted( f"{name} -> {dispatch_map[name]}" for name in yml_command_names if name in dispatch_map and dispatch_map[name] not in symbol_index ) # Then: every command is routed to a handler that exists assert not unrouted, f"Commands declared in Azure.yml but not routed in main(): {unrouted}" assert not missing_handlers, f"Commands routed to functions that do not exist in Azure.py: {missing_handlers}" def test_dispatch_commands_exist_in_yml(yml_commands, dispatch_map, deprecated_commands): """ Given: - The command dispatch dict extracted from main() in Azure.py. - The in-scope, non-deprecated commands declared in Azure.yml. - deprecated_commands (set[str]): The commands the yml marks deprecated. When: - Each in-scope dispatch key is looked up in the yml. Then: - No dispatch entry is orphaned, i.e. every routed command is documented. - Deprecated commands are exempt: they are still routed in main() so that existing playbooks keep working, but they are intentionally absent from the filtered yml command map. """ # Given: the in-scope dispatch keys, excluding the deprecated ones still routed in_scope_dispatch = {name for name in dispatch_map if is_command_in_scope(name) and name not in deprecated_commands} # When: checking them against the declared yml commands undocumented = sorted(in_scope_dispatch - set(yml_commands)) # Then: every routed command is declared in the yml assert not undocumented, f"Commands routed in main() but not declared in Azure.yml: {undocumented}" def test_command_arguments_are_read_by_handler(yml_commands, dispatch_map, symbol_index): """ Given: - yml_commands (dict[str, dict]): The arguments declared for each in-scope command in Azure.yml. - dispatch_map (dict[str, str]): The handler each command is routed to. - symbol_index (dict[str, ast.FunctionDef]): Used to resolve the handler and its direct callees. When: - Each argument name is searched for as a string literal in the handler and in any AzureClient method or helper it calls directly. Then: - Every documented argument is read somewhere on the command's code path, proving no advertised argument is silently ignored. - Infrastructure arguments consumed before the handler runs are exempt, as are arguments marked ``hidden: true``, which are not offered to the user. """ # Given: a cache so each handler's reachable strings are computed once reachable_strings_cache: dict[str, set[str]] = {} unread_arguments: list[str] = [] for command_name, command in sorted(yml_commands.items()): handler_name = dispatch_map.get(command_name) if handler_name is None: continue # covered by test_yml_commands_are_wired_in_dispatch if handler_name not in reachable_strings_cache: reachable_strings_cache[handler_name] = collect_reachable_strings(handler_name, symbol_index) reachable_strings = reachable_strings_cache[handler_name] # When: checking each declared argument against the reachable literals for argument in visible_arguments(command): argument_name = argument.get("name") if not argument_name or argument_name in INFRASTRUCTURE_ARGUMENTS: continue if argument_name not in reachable_strings: unread_arguments.append(f"{command_name}: '{argument_name}' (handler: {handler_name})") # Then: no documented argument is ignored by the code serving the command assert not unread_arguments, "Arguments declared in Azure.yml but never read by the command handler:\n" + "\n".join( unread_arguments ) def test_command_output_prefixes_are_wired(yml_commands, dispatch_map, symbol_index): """ Given: - The contextPath outputs declared for each in-scope command in Azure.yml. - The output prefixes produced by the command's handler, either via the COMMANDS_TO_OUTPUTS_PREFIX map or a fallback/literal outputs_prefix. When: - The yml context paths are compared against the prefixes in the code. Then: - Every command whose handler declares a prefix writes context under a path the yml actually documents. """ # Given: the explicit command -> prefix lookup used by most handlers from Azure import COMMANDS_TO_OUTPUTS_PREFIX mismatches: list[str] = [] for command_name, command in sorted(yml_commands.items()): handler_name = dispatch_map.get(command_name) outputs = command.get("outputs") or [] if handler_name is None or not outputs: continue context_paths = [output.get("contextPath", "") for output in outputs] # When: resolving the prefix the code will actually use mapped_prefix = COMMANDS_TO_OUTPUTS_PREFIX.get(command_name) candidate_prefixes = {mapped_prefix} if mapped_prefix else extract_fallback_prefix(handler_name, symbol_index) if not candidate_prefixes: continue # handler builds context another way, e.g. a plain outputs dict # Then: at least one produced prefix must match a documented context path if not any(path == prefix or path.startswith(f"{prefix}.") for prefix in candidate_prefixes for path in context_paths): mismatches.append( f"{command_name}: code writes to {sorted(candidate_prefixes)} " f"but Azure.yml documents " f"{sorted({path.split('.')[0] + '.' + path.split('.')[1] for path in context_paths if '.' in path})}" ) assert not mismatches, "Output prefixes in Azure.py do not match the contextPath declared in Azure.yml:\n" + "\n".join( mismatches ) # --------------------------------------------------------------------------- # Unit tests for the helpers above. # # These guard the extractors themselves, so that a helper which silently stops # reading Azure.py is reported as a helper bug rather than surfacing as a # misleading failure - or a vacuous pass - in the wiring tests above. # --------------------------------------------------------------------------- def test_select_in_scope_commands_is_not_vacuous(yml_commands, raw_yml_commands, deprecated_commands): """ Given: - yml_commands (dict[str, dict]): The result of select_in_scope_commands() over the real Azure.yml, which filters out both out-of-scope and deprecated commands. - raw_yml_commands (list[dict]): The same yml before any filtering. - deprecated_commands (set[str]): The names the yml marks deprecated. When: - The loaded map is compared against the raw command list in Azure.yml. Then: - The map is not empty, so the wiring tests below cannot pass vacuously by silently iterating over nothing. - The out-of-scope and deprecated commands that really exist in the yml were dropped, and everything else was kept, proving both filters are applied. - The raw yml definitions are preserved intact, so the arguments and outputs the wiring tests inspect are present rather than skipped over. """ # Given: the raw, unfiltered command list straight from the yml raw_names = {command["name"] for command in raw_yml_commands} out_of_scope_names = {name for name in raw_names if not is_command_in_scope(name)} # When / Then: the selector returned something for the other tests to work on assert yml_commands, f"select_in_scope_commands() returned no commands - is {YML_PATH.name} readable and non-empty?" # Then: both filters kept the right commands and dropped the wrong ones assert out_of_scope_names, ( "Azure.yml no longer declares any out-of-scope commands, so this test can no " "longer prove that is_command_in_scope filtering is applied." ) assert deprecated_commands, ( "Azure.yml no longer declares any deprecated commands, so this test can no " "longer prove that deprecated commands are filtered out." ) assert set(yml_commands) == raw_names - out_of_scope_names - deprecated_commands # Then: the definitions kept their arguments and outputs, which the wiring tests # below silently skip when absent assert any( command.get("arguments") for command in yml_commands.values() ), "No loaded command declares 'arguments' - test_command_arguments_are_read_by_handler would pass vacuously." assert any( command.get("outputs") for command in yml_commands.values() ), "No loaded command declares 'outputs' - test_command_output_prefixes_are_wired would pass vacuously." def test_visible_arguments_drops_only_hidden_arguments(): """ Given: - A command declaring a plain argument, one explicitly marked hidden, and one explicitly marked not hidden. When: - visible_arguments is called on the command. Then: - Only the hidden argument is dropped. An argument the user cannot supply is outside the yml <-> py contract, while everything else must still be checked. - The surviving definitions are returned unchanged, so callers can still read their names. """ # Given: a command mixing hidden and visible arguments command = { "name": "azure-disk-update", "arguments": [ {"name": "disk_name"}, {"name": "internal_token", "hidden": True}, {"name": "public_network_access", "hidden": False}, ], } # When: filtering out the hidden arguments visible = visible_arguments(command) # Then: only the hidden one is gone, and the rest are untouched assert [argument["name"] for argument in visible] == ["disk_name", "public_network_access"] def test_visible_arguments_handles_command_without_arguments(): """ Given: - A command that declares no arguments at all, such as a simple list command. When: - visible_arguments is called on it. Then: - An empty list is returned rather than raising, so the wiring tests simply find nothing to check for that command. """ # Given / When / Then: a command with no arguments key yields nothing assert visible_arguments({"name": "azure-resource-group-list"}) == [] def test_extract_dispatch_map_reads_command_to_handler_pairs(): """ Given: - A parsed main() containing a dispatch dict of the shape Azure.py uses, mapping string command names to bare handler function names. When: - extract_dispatch_map is called on the tree. Then: - Every command name is mapped to the exact handler identifier it points to, which is the contract the wiring tests below depend on. """ # Given: a minimal main() holding a well-formed dispatch dict source = ( "def main():\n" f" {DISPATCH_DICT_NAME} = {{\n" " 'azure-storage-account-update': storage_account_update_command,\n" " 'azure-disk-update': disk_update_command,\n" " }\n" ) # When: lifting the dispatch table out of the AST dispatch = extract_dispatch_map(ast.parse(source)) # Then: both commands resolve to their handler names assert dispatch == { "azure-storage-account-update": "storage_account_update_command", "azure-disk-update": "disk_update_command", } def test_extract_dispatch_map_raises_naming_entries_it_cannot_read(): """ Given: - A dispatch dict mixing a readable "command": handler entry with entries this extractor does not support: a non-string key and a value that is a call expression rather than a bare function name. When: - extract_dispatch_map is called on the tree. Then: - An AssertionError is raised rather than the unsupported entries being dropped, so their commands are never wrongly reported as unrouted. - The message names every offending entry, pointing straight at what to fix. """ # Given: a dispatch dict containing entry shapes the extractor cannot read source = ( "def main():\n" f" {DISPATCH_DICT_NAME} = {{\n" " 'azure-disk-update': disk_update_command,\n" " SOME_CONSTANT: acr_update_command,\n" " 'azure-acr-update': partial(acr_update_command),\n" " }\n" ) # When: lifting the dispatch table out of the AST with pytest.raises(AssertionError) as error: extract_dispatch_map(ast.parse(source)) # Then: both unreadable entries are named, and the readable one is not blamed message = str(error.value) assert "SOME_CONSTANT: acr_update_command" in message assert "'azure-acr-update': partial(acr_update_command)" in message assert "disk_update_command" not in message.split("updating:")[-1] def test_extract_dispatch_map_raises_when_dict_is_missing(): """ Given: - A parsed main() that contains no dispatch dict at all, simulating the dict being renamed or removed from Azure.py. When: - extract_dispatch_map is called on the tree. Then: - An AssertionError naming the expected dict is raised, so the wiring tests fail loudly instead of reporting every command as unrouted. """ # Given: a main() with no dispatch dict source = "def main():\n some_other_mapping = {'azure-disk-update': disk_update_command}\n" # When / Then: the missing dict is reported explicitly with pytest.raises(AssertionError, match=DISPATCH_DICT_NAME): extract_dispatch_map(ast.parse(source)) def test_extract_dispatch_map_raises_when_dict_is_empty(): """ Given: - A dispatch dict that exists but is empty, simulating the routing being moved out of the dict entirely. When: - extract_dispatch_map is called on the tree. Then: - An AssertionError is raised rather than an empty map being returned, so the failure blames this helper instead of wrongly reporting every command in Azure.yml as unrouted. """ # Given: a main() whose dispatch dict has no entries source = f"def main():\n {DISPATCH_DICT_NAME} = {{}}\n" # When / Then: the empty dict is reported as a helper problem with pytest.raises(AssertionError, match="could not extract any"): extract_dispatch_map(ast.parse(source)) def test_build_symbol_index_indexes_module_functions_and_client_methods(): """ Given: - A module defining a top-level handler function alongside a client class whose methods are called as client.(...) by that handler. When: - build_symbol_index is called on the tree. Then: - Both the module-level function and the class methods are indexed under their bare names, which is what lets an attribute call be resolved back to its definition. - Each entry is the FunctionDef node itself, since callers walk its body. """ # Given: a module with a top-level function and a client class source = ( "def disk_update_command(client, params, args):\n" " pass\n" "\n" "class AzureClient:\n" " def disk_update_request(self, args):\n" " pass\n" "\n" " def storage_account_update_request(self, args):\n" " pass\n" ) # When: indexing the module index = build_symbol_index(ast.parse(source)) # Then: both kinds of definition are reachable by bare name, as AST nodes assert set(index) == {"disk_update_command", "disk_update_request", "storage_account_update_request"} assert all(isinstance(node, ast.FunctionDef) for node in index.values()) assert index["disk_update_command"].name == "disk_update_command" def test_build_symbol_index_prefers_module_function_over_class_method(): """ Given: - A module where a top-level function and a class method share the same name. When: - build_symbol_index is called on the tree. Then: - The module-level function wins, matching how Python resolves the bare call format_rule(...) inside a handler. Indexing the method instead would make the wiring tests inspect the wrong body. """ # Given: a name defined both at module level and as a class method source = ( "def format_rule(rule):\n" " module_level_marker = 1\n" "\n" "class AzureClient:\n" " def format_rule(self, rule):\n" " class_level_marker = 2\n" ) # When: indexing the module index = build_symbol_index(ast.parse(source)) # Then: the module-level definition is the one that was kept assert "module_level_marker" in { target.id for node in ast.walk(index["format_rule"]) if isinstance(node, ast.Assign) for target in node.targets if isinstance(target, ast.Name) } def test_build_symbol_index_ignores_nested_functions(): """ Given: - A module-level function containing a nested inner function. When: - build_symbol_index is called on the tree. Then: - Only the outer function is indexed. Nested helpers are not callable by bare name from a handler, so indexing them could resolve a call to a definition that is not actually in scope at the call site. """ # Given: a function with a closure defined inside it source = "def disk_update_command(client, args):\n def inner_helper():\n pass\n\n return inner_helper()\n" # When: indexing the module index = build_symbol_index(ast.parse(source)) # Then: the nested definition was not indexed assert set(index) == {"disk_update_command"} def test_build_symbol_index_returns_empty_for_module_without_functions(): """ Given: - A module that defines no functions at all, only constants. When: - build_symbol_index is called on the tree. Then: - An empty index is returned without raising, since this helper reports what it finds and leaves the "handler does not exist" verdict to the wiring tests. """ # Given: a module with no function definitions source = "API_VERSION = '2023-01-01'\nCOMMANDS_TO_OUTPUTS_PREFIX = {}\n" # When: indexing the module index = build_symbol_index(ast.parse(source)) # Then: nothing is indexed, and no error is raised assert index == {} def test_collect_called_names_collects_bare_and_attribute_calls(): """ Given: - A handler that calls a module-level helper by bare name and an AzureClient method through the client attribute, which are the two call shapes used throughout Azure.py. When: - collect_called_names is called on the handler. Then: - Both are returned, with the attribute call reduced to its final attribute. That reduction is what lets a client.(...) call be looked up in the symbol index, which indexes methods under their bare names. """ # Given: a handler using both call shapes source = ( "def disk_update_command(client, params, args):\n" " response = client.disk_update_request(args)\n" " return format_rule(response)\n" ) handler = ast.parse(source).body[0] # When: collecting the names it calls called = collect_called_names(handler) # Then: the bare call and the reduced attribute call are both present assert called == {"disk_update_request", "format_rule"} def test_collect_called_names_reduces_chained_calls_to_final_attribute(): """ Given: - A function using the chained attribute calls that Azure.py really makes, such as self.ms_client.http_request(...) and urllib.parse.urljoin(...). When: - collect_called_names is called on it. Then: - Each call contributes only its final attribute, and the intermediate attributes are not reported as calls. Treating an intermediate such as 'ms_client' or 'parse' as a call could resolve it to an unrelated same-named function in the symbol index. """ # Given: a client method using the chained call shapes found in Azure.py source = ( "def http_request(self, method, url_suffix, azure_ad_endpoint):\n" " token_url = urllib.parse.urljoin(azure_ad_endpoint, url_suffix)\n" " return self.ms_client.http_request(method=method, full_url=token_url)\n" ) handler = ast.parse(source).body[0] # When: collecting the names it calls called = collect_called_names(handler) # Then: only the final attributes are reported, not 'parse' or 'ms_client' assert called == {"urljoin", "http_request"} def test_collect_called_names_includes_nested_and_argument_calls(): """ Given: - A handler whose calls appear inside a nested block and as an argument to another call, rather than as plain top-level statements. When: - collect_called_names is called on the handler. Then: - Every call is found regardless of nesting depth, because the whole function body is walked. A handler that only reaches its client inside an if branch must still be seen, or the argument wiring test would report false failures. """ # Given: a handler with calls nested in a branch and inside another call source = ( "def acr_update_command(client, args):\n" " if args.get('enabled'):\n" " for item in build_items(args):\n" " client.acr_update_request(format_rule(item))\n" ) handler = ast.parse(source).body[0] # When: collecting the names it calls called = collect_called_names(handler) # Then: nested and nested-as-argument calls are all collected assert called == {"get", "build_items", "acr_update_request", "format_rule"} def test_collect_called_names_returns_empty_for_handler_without_calls(): """ Given: - A handler that makes no calls at all. When: - collect_called_names is called on it. Then: - An empty set is returned without raising, so collect_reachable_strings simply falls back to the handler's own string literals. """ # Given: a handler with no calls in its body source = "def disk_update_command(client, args):\n return args\n" handler = ast.parse(source).body[0] # When / Then: nothing is collected, and no error is raised assert collect_called_names(handler) == set() def test_collect_string_constants_collects_argument_keys_at_any_depth(): """ Given: - A handler that reads its argument keys inside a branch, a nested dict literal and a subscript, which is how Azure.py handlers build request payloads. When: - collect_string_constants is called on the handler. Then: - Every string literal is returned regardless of nesting depth, since the argument wiring test relies on finding an argument name anywhere on the handler's code path. """ # Given: a handler reading argument keys at several nesting depths source = ( "def disk_update_command(client, args):\n" ' if args.get("public_network_access"):\n' ' payload = {"properties": {"networkAccessPolicy": args["network_access_policy"]}}\n' " return client.disk_update_request(payload)\n" ) handler = ast.parse(source).body[0] # When: collecting its string literals strings = collect_string_constants(handler) # Then: keys nested in a branch, a dict literal and a subscript are all found assert strings == {"public_network_access", "properties", "networkAccessPolicy", "network_access_policy"} def test_collect_string_constants_ignores_non_string_constants(): """ Given: - A handler containing numeric, boolean and None constants alongside a single argument key. When: - collect_string_constants is called on the handler. Then: - Only the string literal is returned. Non-string constants can never match an argument name, so including them would only add noise that might mask a real unread argument. """ # Given: a handler mixing string and non-string constants source = ( "def disk_update_command(client, args):\n" " timeout = 30\n" " enabled = True\n" " missing = None\n" ' return client.disk_update_request(args["disk_name"], timeout, enabled, missing)\n' ) handler = ast.parse(source).body[0] # When: collecting its string literals strings = collect_string_constants(handler) # Then: only the argument key is returned assert strings == {"disk_name"} def test_collect_string_constants_includes_docstring_text(): """ Given: - A handler whose docstring mentions an argument name that the body never reads. When: - collect_string_constants is called on the handler. Then: - The docstring is returned along with the real literals, confirming the documented caveat that docstrings add harmless extra entries. They can only mask a failure, never invent one, so this is a known limitation of the argument wiring test rather than a defect. """ # Given: a handler documenting an argument it does not actually read source = ( "def disk_update_command(client, args):\n" ' """Update a disk, honouring data_access_auth_mode."""\n' ' return client.disk_update_request(args["disk_name"])\n' ) handler = ast.parse(source).body[0] # When: collecting its string literals strings = collect_string_constants(handler) # Then: the real key is found, and the docstring text is included as documented assert "disk_name" in strings assert any("data_access_auth_mode" in text for text in strings) def test_collect_string_constants_collects_fstring_literal_parts_only(): """ Given: - A client method building a URL with an f-string, the shape Azure.py uses for every request path. When: - collect_string_constants is called on it. Then: - The literal fragments around the placeholders are returned, and the interpolated names are not, since those are Name nodes rather than string constants. An argument referenced only by interpolation is therefore not matched by its fragment, which is why the handler must still read the argument key itself somewhere on the path. """ # Given: a client method interpolating a value into a request path source = ( "def disk_update_request(self, subscription_id, args):\n" ' url = f"/subscriptions/{subscription_id}/disks"\n' " return self.http_request(url)\n" ) handler = ast.parse(source).body[0] # When: collecting its string literals strings = collect_string_constants(handler) # Then: only the literal fragments are collected, not the interpolated name assert strings == {"/subscriptions/", "/disks"} def test_collect_read_argument_names_collects_the_three_read_shapes(): """ Given: - A handler reading arguments via args.get("x"), args.get("x", default), args["x"] and "x" in args, which are the shapes Azure.py uses. When: - collect_read_argument_names is called on the handler. Then: - Every argument name is returned, so the py -> yml test below sees the full set of arguments the code actually consumes. """ # Given: a handler using all four read forms source = ( "def storage_account_update_command(client, args):\n" ' account_name = args.get("account_name", "")\n' ' kind = args.get("kind")\n' ' tags = args["tags"].split(",")\n' ' if "use_sub_domain_name" in args:\n' " pass\n" ) handler = ast.parse(source).body[0] # When: collecting the argument names it reads read = collect_read_argument_names(handler) # Then: all four are found assert read == {"account_name", "kind", "tags", "use_sub_domain_name"} def test_collect_read_argument_names_ignores_unrelated_literals_and_mappings(): """ Given: - A handler containing string literals that are not command arguments: a read from the params mapping, a response key, a URL fragment, and a dynamic args.get(key) whose name is not a literal. When: - collect_read_argument_names is called on the handler. Then: - Only the genuine args read is returned. This precision is what makes the py -> yml assertion safe, since a stray literal would otherwise be reported as an argument missing from the yml. """ # Given: a handler mixing a real args read with unrelated literals source = ( "def disk_update_command(client, params, args, key):\n" ' subscription_id = params.get("subscription_id")\n' ' disk_name = args.get("disk_name")\n' " dynamic = args.get(key)\n" ' url = "/providers/Microsoft.Compute/disks"\n' ' return response["properties"]["diskState"]\n' ) handler = ast.parse(source).body[0] # When: collecting the argument names it reads read = collect_read_argument_names(handler) # Then: only the real args read is reported assert read == {"disk_name"} def test_extract_fallback_prefix_reads_both_prefix_shapes(): """ Given: - A handler using the COMMANDS_TO_OUTPUTS_PREFIX.get(command, "") lookup, and another passing outputs_prefix= directly to CommandResults, which are the two shapes Azure.py uses to declare a context prefix. When: - extract_fallback_prefix is called on each handler. Then: - The prefix is recovered from both shapes, so the output wiring test can compare it against the contextPath declared in the yml. """ # Given: one handler per prefix shape, indexed as build_symbol_index would source = ( "def disk_update_command(client, args):\n" ' prefix = COMMANDS_TO_OUTPUTS_PREFIX.get(command, "Azure.Compute.Disks")\n' " return CommandResults(outputs_prefix=prefix, outputs=response)\n" "\n" "def acr_update_command(client, args):\n" ' return CommandResults(outputs_prefix="Azure.ContainerRegistry", outputs=response)\n' ) symbol_index = build_symbol_index(ast.parse(source)) # When / Then: each shape yields its prefix assert extract_fallback_prefix("disk_update_command", symbol_index) == {"Azure.Compute.Disks"} assert extract_fallback_prefix("acr_update_command", symbol_index) == {"Azure.ContainerRegistry"} def test_extract_fallback_prefix_ignores_non_literal_and_unrelated_lookups(): """ Given: - A handler whose outputs_prefix is a variable rather than a literal, whose COMMANDS_TO_OUTPUTS_PREFIX lookup has no fallback argument, and which calls .get on an unrelated mapping. When: - extract_fallback_prefix is called on the handler. Then: - Nothing is returned, so the output wiring test skips the command rather than comparing the yml against a prefix that was never actually declared. """ # Given: a handler declaring its prefix in ways this extractor cannot read source = ( "def disk_update_command(client, args):\n" " prefix = COMMANDS_TO_OUTPUTS_PREFIX.get(command)\n" ' other = SOME_OTHER_MAP.get(command, "Azure.NotAPrefix")\n' " return CommandResults(outputs_prefix=prefix, outputs=response)\n" ) symbol_index = build_symbol_index(ast.parse(source)) # When / Then: no prefix is claimed assert extract_fallback_prefix("disk_update_command", symbol_index) == set() def test_extract_fallback_prefix_collects_every_prefix_a_handler_may_write(): """ Given: - A handler that returns a different context prefix on each branch, so more than one prefix is reachable at runtime. When: - extract_fallback_prefix is called on the handler. Then: - Every reachable prefix is returned. The output wiring test then passes if any of them matches the yml, which is the intended behaviour for a handler that serves several commands. """ # Given: a handler writing to two different prefixes source = ( "def storage_account_update_command(client, args):\n" ' if args.get("container_name"):\n' ' return CommandResults(outputs_prefix="Azure.Storage.Containers", outputs=response)\n' ' return CommandResults(outputs_prefix="Azure.Storage.StorageAccounts", outputs=response)\n' ) symbol_index = build_symbol_index(ast.parse(source)) # When / Then: both branches contribute their prefix assert extract_fallback_prefix("storage_account_update_command", symbol_index) == { "Azure.Storage.Containers", "Azure.Storage.StorageAccounts", } def test_extract_fallback_prefix_returns_empty_for_unknown_handler(): """ Given: - A handler name that is not present in the symbol index, which happens when a command is routed to a function that does not exist. When: - extract_fallback_prefix is called with that name. Then: - An empty set is returned rather than raising, leaving the missing-handler verdict to test_yml_commands_are_wired_in_dispatch, which reports it with a far clearer message. """ # Given: an index that does not contain the requested handler symbol_index = build_symbol_index(ast.parse("def disk_update_command(client, args):\n pass\n")) # When / Then: the unknown name yields nothing, and no error is raised assert extract_fallback_prefix("no_such_command", symbol_index) == set()