From ed094679ae9c60bf7d6f8d3078459b430d47d95a Mon Sep 17 00:00:00 2001 From: "Niraj Chaudhari (Persistent Systems Inc)" Date: Mon, 22 Sep 2025 16:00:39 +0530 Subject: [PATCH 01/18] Remove Createdby Parameter from deploy.yml and change logic in bicep --- .github/workflows/deploy.yml | 1 - infra/main.bicep | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 11f4dd947..82f0941b1 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -137,7 +137,6 @@ jobs: imageTag="${IMAGE_TAG}" \ azureAiServiceLocation='${{ env.AZURE_LOCATION }}' \ gptModelCapacity=150 \ - createdBy="Pipeline" \ --output json - name: Extract Web App and API App URLs diff --git a/infra/main.bicep b/infra/main.bicep index dd7a907aa..08065582a 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -170,8 +170,8 @@ var allTags = union( }, tags ) -@description('Optional created by user name') -param createdBy string = empty(deployer().userPrincipalName) ? '' : split(deployer().userPrincipalName, '@')[0] +param createdBy string = contains(deployer(), 'userPrincipalName')? split(deployer().userPrincipalName, '@')[0]: deployer().objectId + resource resourceGroupTags 'Microsoft.Resources/tags@2021-04-01' = { name: 'default' From c6962b515b434433fe960fad2177c63df8f87e8c Mon Sep 17 00:00:00 2001 From: NirajC-Microsoft Date: Mon, 22 Sep 2025 22:22:50 +0530 Subject: [PATCH 02/18] Update deploy.yml to include createdBy parameter --- .github/workflows/deploy.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 82f0941b1..11f4dd947 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -137,6 +137,7 @@ jobs: imageTag="${IMAGE_TAG}" \ azureAiServiceLocation='${{ env.AZURE_LOCATION }}' \ gptModelCapacity=150 \ + createdBy="Pipeline" \ --output json - name: Extract Web App and API App URLs From 83a661e9cea181f01139593ce662af7a14ed72a2 Mon Sep 17 00:00:00 2001 From: VishalS-Microsoft Date: Tue, 23 Sep 2025 12:19:42 +0530 Subject: [PATCH 03/18] CI Pipeline Validate Deployment - MACAE --- .github/workflows/deploy-waf.yml | 5 +++++ .github/workflows/deploy.yml | 5 +++++ infra/main.bicep | 2 +- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy-waf.yml b/.github/workflows/deploy-waf.yml index e2786216e..442925aee 100644 --- a/.github/workflows/deploy-waf.yml +++ b/.github/workflows/deploy-waf.yml @@ -4,6 +4,7 @@ on: push: branches: - main + - CI-Pipeline-macae schedule: - cron: "0 11,23 * * *" # Runs at 11:00 AM and 11:00 PM GMT @@ -105,6 +106,9 @@ jobs: id: deploy run: | set -e + # Generate current timestamp in desired format: YYYY-MM-DDTHH:MM:SS.SSSSSSSZ + current_date=$(date -u +"%Y-%m-%dT%H:%M:%S.%7NZ") + az deployment group create \ --resource-group ${{ env.RESOURCE_GROUP_NAME }} \ --template-file infra/main.bicep \ @@ -118,6 +122,7 @@ jobs: enablePrivateNetworking=true \ enableScalability=true \ createdBy="Pipeline" \ + tags="{'SecurityControl':'Ignore','Purpose':'Deploying and Cleaning Up Resources for Validation','CreatedDate':'$current_date'}" - name: Send Notification on Failure diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 11f4dd947..a44f2b5cc 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -9,6 +9,7 @@ on: - main - hotfix - dev + - CI-Pipeline-macae schedule: - cron: "0 11,23 * * *" # Runs at 11:00 AM and 11:00 PM GMT workflow_dispatch: #Allow manual triggering @@ -125,6 +126,9 @@ jobs: IMAGE_TAG="latest" fi + # Generate current timestamp in desired format: YYYY-MM-DDTHH:MM:SS.SSSSSSSZ + current_date=$(date -u +"%Y-%m-%dT%H:%M:%S.%7NZ") + az deployment group create \ --resource-group ${{ env.RESOURCE_GROUP_NAME }} \ --template-file infra/main.bicep \ @@ -138,6 +142,7 @@ jobs: azureAiServiceLocation='${{ env.AZURE_LOCATION }}' \ gptModelCapacity=150 \ createdBy="Pipeline" \ + tags="{'SecurityControl':'Ignore','Purpose':'Deploying and Cleaning Up Resources for Validation','CreatedDate':'$current_date'}" \ --output json - name: Extract Web App and API App URLs diff --git a/infra/main.bicep b/infra/main.bicep index dd7a907aa..8c265c2e5 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -178,7 +178,7 @@ resource resourceGroupTags 'Microsoft.Resources/tags@2021-04-01' = { properties: { tags: { ...allTags - TemplateName: 'MACAE' + TemplateName: enablePrivateNetworking ? 'MACAE - WAF' : 'MACAE' CreatedBy: createdBy } } From fd0ca46f2a9922bfc48eac76893826804368a3f1 Mon Sep 17 00:00:00 2001 From: VishalS-Microsoft Date: Tue, 23 Sep 2025 12:40:05 +0530 Subject: [PATCH 04/18] removed my feature branch --- .github/workflows/deploy-waf.yml | 1 - .github/workflows/deploy.yml | 1 - 2 files changed, 2 deletions(-) diff --git a/.github/workflows/deploy-waf.yml b/.github/workflows/deploy-waf.yml index 442925aee..2b85b5c56 100644 --- a/.github/workflows/deploy-waf.yml +++ b/.github/workflows/deploy-waf.yml @@ -4,7 +4,6 @@ on: push: branches: - main - - CI-Pipeline-macae schedule: - cron: "0 11,23 * * *" # Runs at 11:00 AM and 11:00 PM GMT diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index a44f2b5cc..3afff0d53 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -9,7 +9,6 @@ on: - main - hotfix - dev - - CI-Pipeline-macae schedule: - cron: "0 11,23 * * *" # Runs at 11:00 AM and 11:00 PM GMT workflow_dispatch: #Allow manual triggering From 28a6e0bb77e9c0d53c449c5442d7fb43ee1606d5 Mon Sep 17 00:00:00 2001 From: NirajC-Microsoft Date: Tue, 23 Sep 2025 15:44:21 +0530 Subject: [PATCH 05/18] Add createdBy parameter with description --- infra/main.bicep | 1 + 1 file changed, 1 insertion(+) diff --git a/infra/main.bicep b/infra/main.bicep index 08065582a..3322c1699 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -170,6 +170,7 @@ var allTags = union( }, tags ) +@description('Tag, Created by user name') param createdBy string = contains(deployer(), 'userPrincipalName')? split(deployer().userPrincipalName, '@')[0]: deployer().objectId From f602851f23f0a1231daadfaf5b8324a6d4f0ea5c Mon Sep 17 00:00:00 2001 From: VishalS-Microsoft Date: Tue, 23 Sep 2025 15:55:21 +0530 Subject: [PATCH 06/18] added new 'type' tag --- infra/main.bicep | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/infra/main.bicep b/infra/main.bicep index 8c265c2e5..608acbdee 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -178,7 +178,8 @@ resource resourceGroupTags 'Microsoft.Resources/tags@2021-04-01' = { properties: { tags: { ...allTags - TemplateName: enablePrivateNetworking ? 'MACAE - WAF' : 'MACAE' + TemplateName: 'MACAE' + Type: enablePrivateNetworking ? 'WAF' : 'Non-WAF' CreatedBy: createdBy } } From a64e5c6f632bd6c4f79cc5166a1af83788acb588 Mon Sep 17 00:00:00 2001 From: Vamshi-Microsoft Date: Wed, 1 Oct 2025 13:08:34 +0530 Subject: [PATCH 07/18] Updated README, azure.yml for minimum azd version 1.18.0 --- README.md | 2 ++ azure.yaml | 2 +- docs/DeploymentGuide.md | 3 ++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 84a58ad48..391a806ca 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,8 @@ Quick deploy ### How to install or deploy Follow the quick deploy steps on the deployment guide to deploy this solution to your own Azure subscription. +> **Note:** This solution accelerator requires **Azure Developer CLI (azd) version 1.18.0 or higher**. Please ensure you have the latest version installed before proceeding with deployment. [Download azd here](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/install-azd). + [Click here to launch the deployment guide](./docs/DeploymentGuide.md)

diff --git a/azure.yaml b/azure.yaml index 26522f5db..a8de1df7b 100644 --- a/azure.yaml +++ b/azure.yaml @@ -3,4 +3,4 @@ name: multi-agent-custom-automation-engine-solution-accelerator metadata: template: multi-agent-custom-automation-engine-solution-accelerator@1.0 requiredVersions: - azd: ">=1.15.0 !=1.17.1" \ No newline at end of file + azd: '>= 1.18.0' \ No newline at end of file diff --git a/docs/DeploymentGuide.md b/docs/DeploymentGuide.md index 9c8de2184..8dbd5d364 100644 --- a/docs/DeploymentGuide.md +++ b/docs/DeploymentGuide.md @@ -160,7 +160,7 @@ If you're not using one of the above options for opening the project, then you'l 1. Make sure the following tools are installed: - [PowerShell](https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell?view=powershell-7.5) (v7.0+) - available for Windows, macOS, and Linux. - - [Azure Developer CLI (azd)](https://aka.ms/install-azd) (v1.15.0+) - version + - [Azure Developer CLI (azd)](https://aka.ms/install-azd) (v1.18.0+) - version - [Python 3.9+](https://www.python.org/downloads/) - [Docker Desktop](https://www.docker.com/products/docker-desktop/) - [Git](https://git-scm.com/downloads) @@ -249,6 +249,7 @@ Once you've opened the project in [Codespaces](#github-codespaces), [Dev Contain ```shell azd up ``` + > **Note:** This solution accelerator requires **Azure Developer CLI (azd) version 1.18.0 or higher**. Please ensure you have the latest version installed before proceeding with deployment. [Download azd here](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/install-azd). 3. Provide an `azd` environment name (e.g., "macaeapp"). 4. Select a subscription from your Azure account and choose a location that has quota for all the resources. From 7ebc4dcf04075f5e8c1394756f0b4ad109371366 Mon Sep 17 00:00:00 2001 From: "Prekshith D J (Persistent Systems Inc)" Date: Tue, 14 Oct 2025 12:31:29 +0530 Subject: [PATCH 08/18] update the network module --- infra/main.bicep | 634 ++++++++++++++++------------- infra/modules/virtualNetwork.bicep | 401 ++++++++++++++++++ 2 files changed, 750 insertions(+), 285 deletions(-) create mode 100644 infra/modules/virtualNetwork.bicep diff --git a/infra/main.bicep b/infra/main.bicep index 00dea3f86..0384571af 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -331,294 +331,302 @@ module userAssignedIdentity 'br/public:avm/res/managed-identity/user-assigned-id // ========== Network Security Groups ========== // // WAF best practices for virtual networks: https://learn.microsoft.com/en-us/azure/well-architected/service-guides/virtual-network // WAF recommendations for networking and connectivity: https://learn.microsoft.com/en-us/azure/well-architected/security/networking -var networkSecurityGroupBackendResourceName = 'nsg-${solutionSuffix}-backend' -module networkSecurityGroupBackend 'br/public:avm/res/network/network-security-group:0.5.1' = if (enablePrivateNetworking) { - name: take('avm.res.network.network-security-group.${networkSecurityGroupBackendResourceName}', 64) - params: { - name: networkSecurityGroupBackendResourceName - location: location - tags: tags - enableTelemetry: enableTelemetry - diagnosticSettings: enableMonitoring ? [{ workspaceResourceId: logAnalyticsWorkspaceResourceId }] : null - securityRules: [ - { - name: 'deny-hop-outbound' - properties: { - access: 'Deny' - destinationAddressPrefix: '*' - destinationPortRanges: [ - '22' - '3389' - ] - direction: 'Outbound' - priority: 200 - protocol: 'Tcp' - sourceAddressPrefix: 'VirtualNetwork' - sourcePortRange: '*' - } - } - ] - } -} - -var networkSecurityGroupBastionResourceName = 'nsg-${solutionSuffix}-bastion' -module networkSecurityGroupBastion 'br/public:avm/res/network/network-security-group:0.5.1' = if (enablePrivateNetworking) { - name: take('avm.res.network.network-security-group.${networkSecurityGroupBastionResourceName}', 64) - params: { - name: networkSecurityGroupBastionResourceName - location: location - tags: tags - enableTelemetry: enableTelemetry - diagnosticSettings: enableMonitoring ? [{ workspaceResourceId: logAnalyticsWorkspaceResourceId }] : null - securityRules: [ - { - name: 'AllowHttpsInBound' - properties: { - protocol: 'Tcp' - sourcePortRange: '*' - sourceAddressPrefix: 'Internet' - destinationPortRange: '443' - destinationAddressPrefix: '*' - access: 'Allow' - priority: 100 - direction: 'Inbound' - } - } - { - name: 'AllowGatewayManagerInBound' - properties: { - protocol: 'Tcp' - sourcePortRange: '*' - sourceAddressPrefix: 'GatewayManager' - destinationPortRange: '443' - destinationAddressPrefix: '*' - access: 'Allow' - priority: 110 - direction: 'Inbound' - } - } - { - name: 'AllowLoadBalancerInBound' - properties: { - protocol: 'Tcp' - sourcePortRange: '*' - sourceAddressPrefix: 'AzureLoadBalancer' - destinationPortRange: '443' - destinationAddressPrefix: '*' - access: 'Allow' - priority: 120 - direction: 'Inbound' - } - } - { - name: 'AllowBastionHostCommunicationInBound' - properties: { - protocol: '*' - sourcePortRange: '*' - sourceAddressPrefix: 'VirtualNetwork' - destinationPortRanges: [ - '8080' - '5701' - ] - destinationAddressPrefix: 'VirtualNetwork' - access: 'Allow' - priority: 130 - direction: 'Inbound' - } - } - { - name: 'DenyAllInBound' - properties: { - protocol: '*' - sourcePortRange: '*' - sourceAddressPrefix: '*' - destinationPortRange: '*' - destinationAddressPrefix: '*' - access: 'Deny' - priority: 1000 - direction: 'Inbound' - } - } - { - name: 'AllowSshRdpOutBound' - properties: { - protocol: 'Tcp' - sourcePortRange: '*' - sourceAddressPrefix: '*' - destinationPortRanges: [ - '22' - '3389' - ] - destinationAddressPrefix: 'VirtualNetwork' - access: 'Allow' - priority: 100 - direction: 'Outbound' - } - } - { - name: 'AllowAzureCloudCommunicationOutBound' - properties: { - protocol: 'Tcp' - sourcePortRange: '*' - sourceAddressPrefix: '*' - destinationPortRange: '443' - destinationAddressPrefix: 'AzureCloud' - access: 'Allow' - priority: 110 - direction: 'Outbound' - } - } - { - name: 'AllowBastionHostCommunicationOutBound' - properties: { - protocol: '*' - sourcePortRange: '*' - sourceAddressPrefix: 'VirtualNetwork' - destinationPortRanges: [ - '8080' - '5701' - ] - destinationAddressPrefix: 'VirtualNetwork' - access: 'Allow' - priority: 120 - direction: 'Outbound' - } - } - { - name: 'AllowGetSessionInformationOutBound' - properties: { - protocol: '*' - sourcePortRange: '*' - sourceAddressPrefix: '*' - destinationAddressPrefix: 'Internet' - destinationPortRanges: [ - '80' - '443' - ] - access: 'Allow' - priority: 130 - direction: 'Outbound' - } - } - { - name: 'DenyAllOutBound' - properties: { - protocol: '*' - sourcePortRange: '*' - destinationPortRange: '*' - sourceAddressPrefix: '*' - destinationAddressPrefix: '*' - access: 'Deny' - priority: 1000 - direction: 'Outbound' - } - } - ] - } -} - -var networkSecurityGroupAdministrationResourceName = 'nsg-${solutionSuffix}-administration' -module networkSecurityGroupAdministration 'br/public:avm/res/network/network-security-group:0.5.1' = if (enablePrivateNetworking) { - name: take('avm.res.network.network-security-group.${networkSecurityGroupAdministrationResourceName}', 64) - params: { - name: networkSecurityGroupAdministrationResourceName - location: location - tags: tags - enableTelemetry: enableTelemetry - diagnosticSettings: enableMonitoring ? [{ workspaceResourceId: logAnalyticsWorkspaceResourceId }] : null - securityRules: [ - { - name: 'deny-hop-outbound' - properties: { - access: 'Deny' - destinationAddressPrefix: '*' - destinationPortRanges: [ - '22' - '3389' - ] - direction: 'Outbound' - priority: 200 - protocol: 'Tcp' - sourceAddressPrefix: 'VirtualNetwork' - sourcePortRange: '*' - } - } - ] - } -} - -var networkSecurityGroupContainersResourceName = 'nsg-${solutionSuffix}-containers' -module networkSecurityGroupContainers 'br/public:avm/res/network/network-security-group:0.5.1' = if (enablePrivateNetworking) { - name: take('avm.res.network.network-security-group.${networkSecurityGroupContainersResourceName}', 64) - params: { - name: networkSecurityGroupContainersResourceName - location: location - tags: tags - enableTelemetry: enableTelemetry - diagnosticSettings: enableMonitoring ? [{ workspaceResourceId: logAnalyticsWorkspaceResourceId }] : null - securityRules: [ - { - name: 'deny-hop-outbound' - properties: { - access: 'Deny' - destinationAddressPrefix: '*' - destinationPortRanges: [ - '22' - '3389' - ] - direction: 'Outbound' - priority: 200 - protocol: 'Tcp' - sourceAddressPrefix: 'VirtualNetwork' - sourcePortRange: '*' - } - } - ] - } -} - -var networkSecurityGroupWebsiteResourceName = 'nsg-${solutionSuffix}-website' -module networkSecurityGroupWebsite 'br/public:avm/res/network/network-security-group:0.5.1' = if (enablePrivateNetworking) { - name: take('avm.res.network.network-security-group.${networkSecurityGroupWebsiteResourceName}', 64) - params: { - name: networkSecurityGroupWebsiteResourceName - location: location - tags: tags - enableTelemetry: enableTelemetry - diagnosticSettings: enableMonitoring ? [{ workspaceResourceId: logAnalyticsWorkspaceResourceId }] : null - securityRules: [ - { - name: 'deny-hop-outbound' - properties: { - access: 'Deny' - destinationAddressPrefix: '*' - destinationPortRanges: [ - '22' - '3389' - ] - direction: 'Outbound' - priority: 200 - protocol: 'Tcp' - sourceAddressPrefix: 'VirtualNetwork' - sourcePortRange: '*' - } - } - ] - } -} +// var networkSecurityGroupBackendResourceName = 'nsg-${solutionSuffix}-backend' +// module networkSecurityGroupBackend 'br/public:avm/res/network/network-security-group:0.5.1' = if (enablePrivateNetworking) { +// name: take('avm.res.network.network-security-group.${networkSecurityGroupBackendResourceName}', 64) +// params: { +// name: networkSecurityGroupBackendResourceName +// location: location +// tags: tags +// enableTelemetry: enableTelemetry +// diagnosticSettings: enableMonitoring ? [{ workspaceResourceId: logAnalyticsWorkspaceResourceId }] : null +// securityRules: [ +// { +// name: 'deny-hop-outbound' +// properties: { +// access: 'Deny' +// destinationAddressPrefix: '*' +// destinationPortRanges: [ +// '22' +// '3389' +// ] +// direction: 'Outbound' +// priority: 200 +// protocol: 'Tcp' +// sourceAddressPrefix: 'VirtualNetwork' +// sourcePortRange: '*' +// } +// } +// ] +// } +// } + +// var networkSecurityGroupBastionResourceName = 'nsg-${solutionSuffix}-bastion' +// module networkSecurityGroupBastion 'br/public:avm/res/network/network-security-group:0.5.1' = if (enablePrivateNetworking) { +// name: take('avm.res.network.network-security-group.${networkSecurityGroupBastionResourceName}', 64) +// params: { +// name: networkSecurityGroupBastionResourceName +// location: location +// tags: tags +// enableTelemetry: enableTelemetry +// diagnosticSettings: enableMonitoring ? [{ workspaceResourceId: logAnalyticsWorkspaceResourceId }] : null +// securityRules: [ +// { +// name: 'AllowHttpsInBound' +// properties: { +// protocol: 'Tcp' +// sourcePortRange: '*' +// sourceAddressPrefix: 'Internet' +// destinationPortRange: '443' +// destinationAddressPrefix: '*' +// access: 'Allow' +// priority: 100 +// direction: 'Inbound' +// } +// } +// { +// name: 'AllowGatewayManagerInBound' +// properties: { +// protocol: 'Tcp' +// sourcePortRange: '*' +// sourceAddressPrefix: 'GatewayManager' +// destinationPortRange: '443' +// destinationAddressPrefix: '*' +// access: 'Allow' +// priority: 110 +// direction: 'Inbound' +// } +// } +// { +// name: 'AllowLoadBalancerInBound' +// properties: { +// protocol: 'Tcp' +// sourcePortRange: '*' +// sourceAddressPrefix: 'AzureLoadBalancer' +// destinationPortRange: '443' +// destinationAddressPrefix: '*' +// access: 'Allow' +// priority: 120 +// direction: 'Inbound' +// } +// } +// { +// name: 'AllowBastionHostCommunicationInBound' +// properties: { +// protocol: '*' +// sourcePortRange: '*' +// sourceAddressPrefix: 'VirtualNetwork' +// destinationPortRanges: [ +// '8080' +// '5701' +// ] +// destinationAddressPrefix: 'VirtualNetwork' +// access: 'Allow' +// priority: 130 +// direction: 'Inbound' +// } +// } +// { +// name: 'DenyAllInBound' +// properties: { +// protocol: '*' +// sourcePortRange: '*' +// sourceAddressPrefix: '*' +// destinationPortRange: '*' +// destinationAddressPrefix: '*' +// access: 'Deny' +// priority: 1000 +// direction: 'Inbound' +// } +// } +// { +// name: 'AllowSshRdpOutBound' +// properties: { +// protocol: 'Tcp' +// sourcePortRange: '*' +// sourceAddressPrefix: '*' +// destinationPortRanges: [ +// '22' +// '3389' +// ] +// destinationAddressPrefix: 'VirtualNetwork' +// access: 'Allow' +// priority: 100 +// direction: 'Outbound' +// } +// } +// { +// name: 'AllowAzureCloudCommunicationOutBound' +// properties: { +// protocol: 'Tcp' +// sourcePortRange: '*' +// sourceAddressPrefix: '*' +// destinationPortRange: '443' +// destinationAddressPrefix: 'AzureCloud' +// access: 'Allow' +// priority: 110 +// direction: 'Outbound' +// } +// } +// { +// name: 'AllowBastionHostCommunicationOutBound' +// properties: { +// protocol: '*' +// sourcePortRange: '*' +// sourceAddressPrefix: 'VirtualNetwork' +// destinationPortRanges: [ +// '8080' +// '5701' +// ] +// destinationAddressPrefix: 'VirtualNetwork' +// access: 'Allow' +// priority: 120 +// direction: 'Outbound' +// } +// } +// { +// name: 'AllowGetSessionInformationOutBound' +// properties: { +// protocol: '*' +// sourcePortRange: '*' +// sourceAddressPrefix: '*' +// destinationAddressPrefix: 'Internet' +// destinationPortRanges: [ +// '80' +// '443' +// ] +// access: 'Allow' +// priority: 130 +// direction: 'Outbound' +// } +// } +// { +// name: 'DenyAllOutBound' +// properties: { +// protocol: '*' +// sourcePortRange: '*' +// destinationPortRange: '*' +// sourceAddressPrefix: '*' +// destinationAddressPrefix: '*' +// access: 'Deny' +// priority: 1000 +// direction: 'Outbound' +// } +// } +// ] +// } +// } + +// var networkSecurityGroupAdministrationResourceName = 'nsg-${solutionSuffix}-administration' +// module networkSecurityGroupAdministration 'br/public:avm/res/network/network-security-group:0.5.1' = if (enablePrivateNetworking) { +// name: take('avm.res.network.network-security-group.${networkSecurityGroupAdministrationResourceName}', 64) +// params: { +// name: networkSecurityGroupAdministrationResourceName +// location: location +// tags: tags +// enableTelemetry: enableTelemetry +// diagnosticSettings: enableMonitoring ? [{ workspaceResourceId: logAnalyticsWorkspaceResourceId }] : null +// securityRules: [ +// { +// name: 'deny-hop-outbound' +// properties: { +// access: 'Deny' +// destinationAddressPrefix: '*' +// destinationPortRanges: [ +// '22' +// '3389' +// ] +// direction: 'Outbound' +// priority: 200 +// protocol: 'Tcp' +// sourceAddressPrefix: 'VirtualNetwork' +// sourcePortRange: '*' +// } +// } +// ] +// } +// } + +// var networkSecurityGroupContainersResourceName = 'nsg-${solutionSuffix}-containers' +// module networkSecurityGroupContainers 'br/public:avm/res/network/network-security-group:0.5.1' = if (enablePrivateNetworking) { +// name: take('avm.res.network.network-security-group.${networkSecurityGroupContainersResourceName}', 64) +// params: { +// name: networkSecurityGroupContainersResourceName +// location: location +// tags: tags +// enableTelemetry: enableTelemetry +// diagnosticSettings: enableMonitoring ? [{ workspaceResourceId: logAnalyticsWorkspaceResourceId }] : null +// securityRules: [ +// { +// name: 'deny-hop-outbound' +// properties: { +// access: 'Deny' +// destinationAddressPrefix: '*' +// destinationPortRanges: [ +// '22' +// '3389' +// ] +// direction: 'Outbound' +// priority: 200 +// protocol: 'Tcp' +// sourceAddressPrefix: 'VirtualNetwork' +// sourcePortRange: '*' +// } +// } +// ] +// } +// } + +// var networkSecurityGroupWebsiteResourceName = 'nsg-${solutionSuffix}-website' +// module networkSecurityGroupWebsite 'br/public:avm/res/network/network-security-group:0.5.1' = if (enablePrivateNetworking) { +// name: take('avm.res.network.network-security-group.${networkSecurityGroupWebsiteResourceName}', 64) +// params: { +// name: networkSecurityGroupWebsiteResourceName +// location: location +// tags: tags +// enableTelemetry: enableTelemetry +// diagnosticSettings: enableMonitoring ? [{ workspaceResourceId: logAnalyticsWorkspaceResourceId }] : null +// securityRules: [ +// { +// name: 'deny-hop-outbound' +// properties: { +// access: 'Deny' +// destinationAddressPrefix: '*' +// destinationPortRanges: [ +// '22' +// '3389' +// ] +// direction: 'Outbound' +// priority: 200 +// protocol: 'Tcp' +// sourceAddressPrefix: 'VirtualNetwork' +// sourcePortRange: '*' +// } +// } +// ] +// } +// } // ========== Virtual Network ========== // // WAF best practices for virtual networks: https://learn.microsoft.com/en-us/azure/well-architected/service-guides/virtual-network // WAF recommendations for networking and connectivity: https://learn.microsoft.com/en-us/azure/well-architected/security/networking + +// Virtual Network configuration is now handled by the virtualNetwork.bicep module var virtualNetworkResourceName = 'vnet-${solutionSuffix}' -module virtualNetwork 'br/public:avm/res/network/virtual-network:0.7.0' = if (enablePrivateNetworking) { - name: take('avm.res.network.virtual-network.${virtualNetworkResourceName}', 64) +module virtualNetwork 'modules/virtualNetwork.bicep' = if (enablePrivateNetworking) { + name: take('module.virtualNetwork.${solutionSuffix}', 64) params: { - name: virtualNetworkResourceName - location: location + name: 'vnet-${solutionSuffix}' tags: tags enableTelemetry: enableTelemetry addressPrefixes: ['10.0.0.0/8'] + location: location + logAnalyticsWorkspaceId: logAnalyticsWorkspaceResourceId + resourceSuffix: solutionSuffix + } +} + +/* subnets: [ { name: 'backend' @@ -661,9 +669,65 @@ module virtualNetwork 'br/public:avm/res/network/virtual-network:0.7.0' = if (en privateLinkServiceNetworkPolicies: 'Enabled' } ] +*/ +// Jumpbox Virtual Machine +var jumpboxVmName = take('vm-jumpbox-${solutionSuffix}', 15) +module jumpboxVM 'br/public:avm/res/compute/virtual-machine:0.15.0' = if (enablePrivateNetworking) { + name: take('avm.res.compute.virtual-machine.${jumpboxVmName}', 64) + params: { + name: take(jumpboxVmName, 15) // Shorten VM name to 15 characters to avoid Azure limits + vmSize: 'Standard_DS2_v2' + location: location + adminUsername: virtualMachineAdminUsername + adminPassword: virtualMachineAdminPassword + tags: tags + zone: 0 + imageReference: { + offer: 'WindowsServer' + publisher: 'MicrosoftWindowsServer' + sku: '2019-datacenter' + version: 'latest' + } + osType: 'Windows' + osDisk: { + name: 'osdisk-${jumpboxVmName}' + managedDisk: { + storageAccountType: 'Standard_LRS' + } + } + encryptionAtHost: false // Some Azure subscriptions do not support encryption at host + nicConfigurations: [ + { + name: 'nic-${jumpboxVmName}' + ipConfigurations: [ + { + name: 'ipconfig1' + subnetResourceId: virtualNetwork!.outputs.jumpboxSubnetResourceId + } + ] + diagnosticSettings: [ + { + name: 'jumpboxDiagnostics' + workspaceResourceId: logAnalyticsWorkspaceResourceId + logCategoriesAndGroups: [ + { + categoryGroup: 'allLogs' + enabled: true + } + ] + metricCategories: [ + { + category: 'AllMetrics' + enabled: true + } + ] + } + ] + } + ] + enableTelemetry: enableTelemetry } } - var bastionResourceName = 'bas-${solutionSuffix}' // ========== Bastion host ========== // // WAF best practices for virtual networks: https://learn.microsoft.com/en-us/azure/well-architected/service-guides/virtual-network @@ -908,7 +972,7 @@ module virtualMachine 'br/public:avm/res/compute/virtual-machine:0.17.0' = if (e ipConfigurations: [ { name: '${virtualMachineResourceName}-nic01-ipconfig01' - subnetResourceId: virtualNetwork!.outputs.subnetResourceIds[1] + subnetResourceId: virtualNetwork!.outputs.jumpboxSubnetResourceId diagnosticSettings: enableMonitoring //WAF aligned configuration for Monitoring ? [{ workspaceResourceId: logAnalyticsWorkspaceResourceId }] : null @@ -1138,7 +1202,7 @@ module aiFoundryAiServices 'br:mcr.microsoft.com/bicep/avm/res/cognitive-service { name: 'pep-${aiFoundryAiServicesResourceName}' customNetworkInterfaceName: 'nic-${aiFoundryAiServicesResourceName}' - subnetResourceId: virtualNetwork!.outputs.subnetResourceIds[0] + subnetResourceId: virtualNetwork!.outputs.backendSubnetResourceId privateDnsZoneGroup: { privateDnsZoneGroupConfigs: [ { @@ -1246,7 +1310,7 @@ module cosmosDb 'br/public:avm/res/document-db/database-account:0.15.0' = { ] } service: 'Sql' - subnetResourceId: virtualNetwork!.outputs.subnetResourceIds[0] + subnetResourceId: virtualNetwork!.outputs.backendSubnetResourceId } ] : [] @@ -1291,7 +1355,7 @@ module containerAppEnvironment 'br/public:avm/res/app/managed-environment:0.11.2 // WAF aligned configuration for Private Networking publicNetworkAccess: 'Enabled' // Always enabling the publicNetworkAccess for Container App Environment internal: false // Must be false when publicNetworkAccess is'Enabled' - infrastructureSubnetResourceId: enablePrivateNetworking ? virtualNetwork.?outputs.?subnetResourceIds[3] : null + infrastructureSubnetResourceId: enablePrivateNetworking ? virtualNetwork.?outputs.?containerSubnetResourceId : null // WAF aligned configuration for Monitoring appLogsConfiguration: enableMonitoring ? { @@ -1518,7 +1582,7 @@ module webSite 'modules/web-sites.bicep' = { // WAF aligned configuration for Private Networking vnetRouteAllEnabled: enablePrivateNetworking ? true : false vnetImagePullEnabled: enablePrivateNetworking ? true : false - virtualNetworkSubnetId: enablePrivateNetworking ? virtualNetwork!.outputs.subnetResourceIds[4] : null + virtualNetworkSubnetId: enablePrivateNetworking ? virtualNetwork!.outputs.webserverfarmSubnetResourceId : null publicNetworkAccess: 'Enabled' // Always enabling the public network access for Web App e2eEncryptionEnabled: true } diff --git a/infra/modules/virtualNetwork.bicep b/infra/modules/virtualNetwork.bicep new file mode 100644 index 000000000..a5d8ff1bf --- /dev/null +++ b/infra/modules/virtualNetwork.bicep @@ -0,0 +1,401 @@ +/****************************************************************************************************************************/ +// Networking - NSGs, VNET and Subnets. Each subnet has its own NSG +/****************************************************************************************************************************/ +@description('Name of the virtual network.') +param name string + +@description('Azure region to deploy resources.') +param location string = resourceGroup().location + +@description('Required. An Array of 1 or more IP Address Prefixes for the Virtual Network.') +param addressPrefixes array + +@description('An array of subnets to be created within the virtual network. Each subnet can have its own configuration and associated Network Security Group (NSG).') +param subnets subnetType[] = [ + + { + name: 'AzureBastionSubnet' // Required name for Azure Bastion + addressPrefixes: ['10.0.0.64/26'] + networkSecurityGroup: { + name: 'nsg-bastion' + securityRules: [ + { + name: 'AllowGatewayManager' + properties: { + access: 'Allow' + direction: 'Inbound' + priority: 2702 + protocol: '*' + sourcePortRange: '*' + destinationPortRange: '443' + sourceAddressPrefix: 'GatewayManager' + destinationAddressPrefix: '*' + } + } + { + name: 'AllowHttpsInBound' + properties: { + access: 'Allow' + direction: 'Inbound' + priority: 2703 + protocol: '*' + sourcePortRange: '*' + destinationPortRange: '443' + sourceAddressPrefix: 'Internet' + destinationAddressPrefix: '*' + } + } + { + name: 'AllowSshRdpOutbound' + properties: { + access: 'Allow' + direction: 'Outbound' + priority: 100 + protocol: '*' + sourcePortRange: '*' + destinationPortRanges: ['22', '3389'] + sourceAddressPrefix: '*' + destinationAddressPrefix: 'VirtualNetwork' + } + } + { + name: 'AllowAzureCloudOutbound' + properties: { + access: 'Allow' + direction: 'Outbound' + priority: 110 + protocol: 'Tcp' + sourcePortRange: '*' + destinationPortRange: '443' + sourceAddressPrefix: '*' + destinationAddressPrefix: 'AzureCloud' + } + } + ] + } + } + { + name: 'jumpbox' + addressPrefixes: ['10.0.12.0/23'] // /23 (10.0.12.0 - 10.0.13.255), 512 addresses + networkSecurityGroup: { + name: 'nsg-jumpbox' + securityRules: [ + { + name: 'AllowRdpFromBastion' + properties: { + access: 'Allow' + direction: 'Inbound' + priority: 100 + protocol: 'Tcp' + sourcePortRange: '*' + destinationPortRange: '3389' + sourceAddressPrefixes: ['10.0.10.0/26'] // Azure Bastion subnet + destinationAddressPrefixes: ['10.0.12.0/23'] + } + } + ] + } + } + { + name:'backend' + addressPrefixes: ['10.0.0.0/27'] + networkSecurityGroup: { + name: 'nsg-backend' + securityRules: [ + { + name: 'deny-hop-outbound' + properties: { + access: 'Deny' + destinationAddressPrefix: '*' + destinationPortRanges: [ + '22' + '3389' + ] + direction: 'Outbound' + priority: 200 + protocol: 'Tcp' + sourceAddressPrefix: 'VirtualNetwork' + sourcePortRange: '*' + } + } + ] + } + } + { + name: 'administration' + addressPrefixes: ['10.0.0.32/27'] + networkSecurityGroup: { + name: 'nsg-administration' + securityRules: [ + { + name: 'deny-hop-outbound' + properties: { + access: 'Deny' + destinationAddressPrefix: '*' + destinationPortRanges: [ + '22' + '3389' + ] + direction: 'Outbound' + priority: 200 + protocol: 'Tcp' + sourceAddressPrefix: 'VirtualNetwork' + sourcePortRange: '*' + } + } + ] + } + } + { + name: 'containers' + addressPrefixes: ['10.0.2.0/23'] + networkSecurityGroup: { + name: 'nsg-containers' + securityRules: [ + { + name: 'deny-hop-outbound' + properties: { + access: 'Deny' + destinationAddressPrefix: '*' + destinationPortRanges: [ + '22' + '3389' + ] + direction: 'Outbound' + priority: 200 + protocol: 'Tcp' + sourceAddressPrefix: 'VirtualNetwork' + sourcePortRange: '*' + } + } + ] + } + } + { + name: 'webserverfarm' + addressPrefixes: ['10.0.4.0/27'] + networkSecurityGroup: { + name: 'nsg-webserverfarm' + securityRules: [ + { + name: 'deny-hop-outbound' + properties: { + access: 'Deny' + destinationAddressPrefix: '*' + destinationPortRanges: [ + '22' + '3389' + ] + direction: 'Outbound' + priority: 200 + protocol: 'Tcp' + sourceAddressPrefix: 'VirtualNetwork' + sourcePortRange: '*' + } + } + ] + } + } + { + name: 'deployment-scripts' + addressPrefixes: ['10.0.4.0/24'] + networkSecurityGroup: { + name: 'nsg-deployment-scripts' + securityRules: [] + } + delegation: 'Microsoft.ContainerInstance/containerGroups' + serviceEndpoints: ['Microsoft.Storage'] + } +] + +@description('Optional. Tags to be applied to the resources.') +param tags object = {} + +@description('Optional. The resource ID of the Log Analytics Workspace to send diagnostic logs to.') +param logAnalyticsWorkspaceId string + +@description('Optional. Enable/Disable usage telemetry for module.') +param enableTelemetry bool = true + +@description('Required. Suffix for resource naming.') +param resourceSuffix string + +// VM Size Notes: +// 1 B-series VMs (like Standard_B2ms) do not support accelerated networking. +// 2 Pick a VM size that does support accelerated networking (the usual jump-box candidates): +// Standard_DS2_v2 (2 vCPU, 7 GiB RAM, Premium SSD) // The most broadly available (it’s a legacy SKU supported in virtually every region). +// Standard_D2s_v3 (2 vCPU, 8 GiB RAM, Premium SSD) // next most common +// Standard_D2s_v4 (2 vCPU, 8 GiB RAM, Premium SSD) // Newest, so fewer regions availabl + + +// Subnet Classless Inter-Doman Routing (CIDR) Sizing Reference Table (Best Practices) +// | CIDR | # of Addresses | # of /24s | Notes | +// |-----------|---------------|-----------|----------------------------------------| +// | /24 | 256 | 1 | Smallest recommended for Azure subnets | +// | /23 | 512 | 2 | Good for 1-2 workloads per subnet | +// | /22 | 1024 | 4 | Good for 2-4 workloads per subnet | +// | /21 | 2048 | 8 | | +// | /20 | 4096 | 16 | Used for default VNet in this solution | +// | /19 | 8192 | 32 | | +// | /18 | 16384 | 64 | | +// | /17 | 32768 | 128 | | +// | /16 | 65536 | 256 | | +// | /15 | 131072 | 512 | | +// | /14 | 262144 | 1024 | | +// | /13 | 524288 | 2048 | | +// | /12 | 1048576 | 4096 | | +// | /11 | 2097152 | 8192 | | +// | /10 | 4194304 | 16384 | | +// | /9 | 8388608 | 32768 | | +// | /8 | 16777216 | 65536 | | +// +// Best Practice Notes: +// - Use /24 as the minimum subnet size for Azure (smaller subnets are not supported for most services). +// - Plan for future growth: allocate larger address spaces (e.g., /20 or /21 for VNets) to allow for new subnets. +// - Avoid overlapping address spaces with on-premises or other VNets. +// - Use contiguous, non-overlapping ranges for subnets. +// - Document subnet usage and purpose in code comments. +// - For AVM modules, ensure only one delegation per subnet and leave delegations empty if not required. + +// 1. Create NSGs for subnets +// using AVM Network Security Group module +// https://github.com/Azure/bicep-registry-modules/tree/main/avm/res/network/network-security-group + +@batchSize(1) +module nsgs 'br/public:avm/res/network/network-security-group:0.5.1' = [ + for (subnet, i) in subnets: if (!empty(subnet.?networkSecurityGroup)) { + name: take('avm.res.network.network-security-group.${subnet.?networkSecurityGroup.name}.${resourceSuffix}', 64) + params: { + name: '${subnet.?networkSecurityGroup.name}-${resourceSuffix}' + location: location + securityRules: subnet.?networkSecurityGroup.securityRules + tags: tags + enableTelemetry: enableTelemetry + } + } +] + +// 2. Create VNet and subnets, with subnets associated with corresponding NSGs +// using AVM Virtual Network module +// https://github.com/Azure/bicep-registry-modules/tree/main/avm/res/network/virtual-network + +module virtualNetwork 'br/public:avm/res/network/virtual-network:0.7.0' = { + name: take('avm.res.network.virtual-network.${name}', 64) + params: { + name: name + location: location + addressPrefixes: addressPrefixes + subnets: [ + for (subnet, i) in subnets: { + name: subnet.name + addressPrefixes: subnet.?addressPrefixes + networkSecurityGroupResourceId: !empty(subnet.?networkSecurityGroup) ? nsgs[i]!.outputs.resourceId : null + privateEndpointNetworkPolicies: subnet.?privateEndpointNetworkPolicies + privateLinkServiceNetworkPolicies: subnet.?privateLinkServiceNetworkPolicies + delegation: subnet.?delegation + } + ] + diagnosticSettings: [ + { + name: 'vnetDiagnostics' + workspaceResourceId: logAnalyticsWorkspaceId + logCategoriesAndGroups: [ + { + categoryGroup: 'allLogs' + enabled: true + } + ] + metricCategories: [ + { + category: 'AllMetrics' + enabled: true + } + ] + } + ] + tags: tags + enableTelemetry: enableTelemetry + } +} + +output name string = virtualNetwork.outputs.name +output resourceId string = virtualNetwork.outputs.resourceId + +// combined output array that holds subnet details along with NSG information +output subnets subnetOutputType[] = [ + for (subnet, i) in subnets: { + name: subnet.name + resourceId: virtualNetwork.outputs.subnetResourceIds[i] + nsgName: !empty(subnet.?networkSecurityGroup) ? subnet.?networkSecurityGroup.name : null + nsgResourceId: !empty(subnet.?networkSecurityGroup) ? nsgs[i]!.outputs.resourceId : null + } +] + +// Dynamic outputs for individual subnets for backward compatibility +output bastionSubnetResourceId string = contains(map(subnets, subnet => subnet.name), 'AzureBastionSubnet') ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(subnets, subnet => subnet.name), 'AzureBastionSubnet')] : '' +output jumpboxSubnetResourceId string = contains(map(subnets, subnet => subnet.name), 'jumpbox') ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(subnets, subnet => subnet.name), 'jumpbox')] : '' +output deploymentScriptsSubnetResourceId string = contains(map(subnets, subnet => subnet.name), 'deployment-scripts') ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(subnets, subnet => subnet.name), 'deployment-scripts')] : '' +output backendSubnetResourceId string = contains(map(subnets, subnet => subnet.name), 'backend') ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(subnets, subnet => subnet.name), 'backend')] : '' +output containerSubnetResourceId string = contains(map(subnets, subnet => subnet.name), 'container') ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(subnets, subnet => subnet.name), 'container')] : '' +output administrationSubnetResourceId string = contains(map(subnets, subnet => subnet.name), 'administration') ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(subnets, subnet => subnet.name), 'administration')] : '' +output webserverfarmSubnetResourceId string = contains(map(subnets, subnet => subnet.name), 'webserverfarm') ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(subnets, subnet => subnet.name), 'webserverfarm')] : '' + +@export() +@description('Custom type definition for subnet resource information as output') +type subnetOutputType = { + @description('The name of the subnet.') + name: string + + @description('The resource ID of the subnet.') + resourceId: string + + @description('The name of the associated network security group, if any.') + nsgName: string? + + @description('The resource ID of the associated network security group, if any.') + nsgResourceId: string? +} + +@export() +@description('Custom type definition for subnet configuration') +type subnetType = { + @description('Required. The Name of the subnet resource.') + name: string + + @description('Required. Prefixes for the subnet.') // Required to ensure at least one prefix is provided + addressPrefixes: string[] + + @description('Optional. The delegation to enable on the subnet.') + delegation: string? + + @description('Optional. enable or disable apply network policies on private endpoint in the subnet.') + privateEndpointNetworkPolicies: ('Disabled' | 'Enabled' | 'NetworkSecurityGroupEnabled' | 'RouteTableEnabled')? + + @description('Optional. Enable or disable apply network policies on private link service in the subnet.') + privateLinkServiceNetworkPolicies: ('Disabled' | 'Enabled')? + + @description('Optional. Network Security Group configuration for the subnet.') + networkSecurityGroup: networkSecurityGroupType? + + @description('Optional. The resource ID of the route table to assign to the subnet.') + routeTableResourceId: string? + + @description('Optional. An array of service endpoint policies.') + serviceEndpointPolicies: object[]? + + @description('Optional. The service endpoints to enable on the subnet.') + serviceEndpoints: string[]? + + @description('Optional. Set this property to false to disable default outbound connectivity for all VMs in the subnet. This property can only be set at the time of subnet creation and cannot be updated for an existing subnet.') + defaultOutboundAccess: bool? +} + +@export() +@description('Custom type definition for network security group configuration') +type networkSecurityGroupType = { + @description('Required. The name of the network security group.') + name: string + + @description('Required. The security rules for the network security group.') + securityRules: object[] +} From 0579bc68d9ef5f0552ea86d9e55749ab1bc794b4 Mon Sep 17 00:00:00 2001 From: "Prekshith D J (Persistent Systems Inc)" Date: Tue, 14 Oct 2025 17:01:26 +0530 Subject: [PATCH 09/18] updated --- infra/main.bicep | 59 +------- infra/modules/virtualNetwork.bicep | 208 ++++++++++++++--------------- 2 files changed, 103 insertions(+), 164 deletions(-) diff --git a/infra/main.bicep b/infra/main.bicep index 0384571af..2509c5c2a 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -670,64 +670,7 @@ module virtualNetwork 'modules/virtualNetwork.bicep' = if (enablePrivateNetworki } ] */ -// Jumpbox Virtual Machine -var jumpboxVmName = take('vm-jumpbox-${solutionSuffix}', 15) -module jumpboxVM 'br/public:avm/res/compute/virtual-machine:0.15.0' = if (enablePrivateNetworking) { - name: take('avm.res.compute.virtual-machine.${jumpboxVmName}', 64) - params: { - name: take(jumpboxVmName, 15) // Shorten VM name to 15 characters to avoid Azure limits - vmSize: 'Standard_DS2_v2' - location: location - adminUsername: virtualMachineAdminUsername - adminPassword: virtualMachineAdminPassword - tags: tags - zone: 0 - imageReference: { - offer: 'WindowsServer' - publisher: 'MicrosoftWindowsServer' - sku: '2019-datacenter' - version: 'latest' - } - osType: 'Windows' - osDisk: { - name: 'osdisk-${jumpboxVmName}' - managedDisk: { - storageAccountType: 'Standard_LRS' - } - } - encryptionAtHost: false // Some Azure subscriptions do not support encryption at host - nicConfigurations: [ - { - name: 'nic-${jumpboxVmName}' - ipConfigurations: [ - { - name: 'ipconfig1' - subnetResourceId: virtualNetwork!.outputs.jumpboxSubnetResourceId - } - ] - diagnosticSettings: [ - { - name: 'jumpboxDiagnostics' - workspaceResourceId: logAnalyticsWorkspaceResourceId - logCategoriesAndGroups: [ - { - categoryGroup: 'allLogs' - enabled: true - } - ] - metricCategories: [ - { - category: 'AllMetrics' - enabled: true - } - ] - } - ] - } - ] - enableTelemetry: enableTelemetry - } -} + var bastionResourceName = 'bas-${solutionSuffix}' // ========== Bastion host ========== // // WAF best practices for virtual networks: https://learn.microsoft.com/en-us/azure/well-architected/service-guides/virtual-network diff --git a/infra/modules/virtualNetwork.bicep b/infra/modules/virtualNetwork.bicep index a5d8ff1bf..cb7d28052 100644 --- a/infra/modules/virtualNetwork.bicep +++ b/infra/modules/virtualNetwork.bicep @@ -13,89 +13,7 @@ param addressPrefixes array @description('An array of subnets to be created within the virtual network. Each subnet can have its own configuration and associated Network Security Group (NSG).') param subnets subnetType[] = [ - { - name: 'AzureBastionSubnet' // Required name for Azure Bastion - addressPrefixes: ['10.0.0.64/26'] - networkSecurityGroup: { - name: 'nsg-bastion' - securityRules: [ - { - name: 'AllowGatewayManager' - properties: { - access: 'Allow' - direction: 'Inbound' - priority: 2702 - protocol: '*' - sourcePortRange: '*' - destinationPortRange: '443' - sourceAddressPrefix: 'GatewayManager' - destinationAddressPrefix: '*' - } - } - { - name: 'AllowHttpsInBound' - properties: { - access: 'Allow' - direction: 'Inbound' - priority: 2703 - protocol: '*' - sourcePortRange: '*' - destinationPortRange: '443' - sourceAddressPrefix: 'Internet' - destinationAddressPrefix: '*' - } - } - { - name: 'AllowSshRdpOutbound' - properties: { - access: 'Allow' - direction: 'Outbound' - priority: 100 - protocol: '*' - sourcePortRange: '*' - destinationPortRanges: ['22', '3389'] - sourceAddressPrefix: '*' - destinationAddressPrefix: 'VirtualNetwork' - } - } - { - name: 'AllowAzureCloudOutbound' - properties: { - access: 'Allow' - direction: 'Outbound' - priority: 110 - protocol: 'Tcp' - sourcePortRange: '*' - destinationPortRange: '443' - sourceAddressPrefix: '*' - destinationAddressPrefix: 'AzureCloud' - } - } - ] - } - } - { - name: 'jumpbox' - addressPrefixes: ['10.0.12.0/23'] // /23 (10.0.12.0 - 10.0.13.255), 512 addresses - networkSecurityGroup: { - name: 'nsg-jumpbox' - securityRules: [ - { - name: 'AllowRdpFromBastion' - properties: { - access: 'Allow' - direction: 'Inbound' - priority: 100 - protocol: 'Tcp' - sourcePortRange: '*' - destinationPortRange: '3389' - sourceAddressPrefixes: ['10.0.10.0/26'] // Azure Bastion subnet - destinationAddressPrefixes: ['10.0.12.0/23'] - } - } - ] - } - } + { name:'backend' addressPrefixes: ['10.0.0.0/27'] @@ -121,11 +39,14 @@ param subnets subnetType[] = [ ] } } - { - name: 'administration' - addressPrefixes: ['10.0.0.32/27'] + { + name: 'containers' + addressPrefixes: ['10.0.2.0/23'] + delegation: 'Microsoft.App/environments' + privateEndpointNetworkPolicies: 'Enabled' + privateLinkServiceNetworkPolicies: 'Enabled' networkSecurityGroup: { - name: 'nsg-administration' + name: 'nsg-containers' securityRules: [ { name: 'deny-hop-outbound' @@ -147,12 +68,15 @@ param subnets subnetType[] = [ } } { - name: 'containers' - addressPrefixes: ['10.0.2.0/23'] + name: 'webserverfarm' + addressPrefixes: ['10.0.4.0/27'] + delegation: 'Microsoft.Web/serverfarms' + privateEndpointNetworkPolicies: 'Enabled' + privateLinkServiceNetworkPolicies: 'Enabled' networkSecurityGroup: { - name: 'nsg-containers' + name: 'nsg-webserverfarm' securityRules: [ - { + { name: 'deny-hop-outbound' properties: { access: 'Deny' @@ -172,12 +96,12 @@ param subnets subnetType[] = [ } } { - name: 'webserverfarm' - addressPrefixes: ['10.0.4.0/27'] + name: 'administration' + addressPrefixes: ['10.0.0.32/27'] networkSecurityGroup: { - name: 'nsg-webserverfarm' + name: 'nsg-administration' securityRules: [ - { + { name: 'deny-hop-outbound' properties: { access: 'Deny' @@ -197,14 +121,87 @@ param subnets subnetType[] = [ } } { - name: 'deployment-scripts' - addressPrefixes: ['10.0.4.0/24'] - networkSecurityGroup: { - name: 'nsg-deployment-scripts' - securityRules: [] + name: 'AzureBastionSubnet' // Required name for Azure Bastion + addressPrefixes: ['10.0.0.64/26'] + networkSecurityGroup: { + name: 'nsg-bastion' + securityRules: [ + { + name: 'AllowGatewayManager' + properties: { + access: 'Allow' + direction: 'Inbound' + priority: 2702 + protocol: '*' + sourcePortRange: '*' + destinationPortRange: '443' + sourceAddressPrefix: 'GatewayManager' + destinationAddressPrefix: '*' + } + } + { + name: 'AllowHttpsInBound' + properties: { + access: 'Allow' + direction: 'Inbound' + priority: 2703 + protocol: '*' + sourcePortRange: '*' + destinationPortRange: '443' + sourceAddressPrefix: 'Internet' + destinationAddressPrefix: '*' + } } - delegation: 'Microsoft.ContainerInstance/containerGroups' - serviceEndpoints: ['Microsoft.Storage'] + { + name: 'AllowSshRdpOutbound' + properties: { + access: 'Allow' + direction: 'Outbound' + priority: 100 + protocol: '*' + sourcePortRange: '*' + destinationPortRanges: ['22', '3389'] + sourceAddressPrefix: '*' + destinationAddressPrefix: 'VirtualNetwork' + } + } + { + name: 'AllowAzureCloudOutbound' + properties: { + access: 'Allow' + direction: 'Outbound' + priority: 110 + protocol: 'Tcp' + sourcePortRange: '*' + destinationPortRange: '443' + sourceAddressPrefix: '*' + destinationAddressPrefix: 'AzureCloud' + } + } + ] + } + } + { + name: 'jumpbox' + addressPrefixes: ['10.0.12.0/23'] // /23 (10.0.12.0 - 10.0.13.255), 512 addresses + networkSecurityGroup: { + name: 'nsg-jumpbox' + securityRules: [ + { + name: 'AllowRdpFromBastion' + properties: { + access: 'Allow' + direction: 'Inbound' + priority: 100 + protocol: 'Tcp' + sourcePortRange: '*' + destinationPortRange: '3389' + sourceAddressPrefixes: ['10.0.10.0/26'] // Azure Bastion subnet + destinationAddressPrefixes: ['10.0.12.0/23'] + } + } + ] + } } ] @@ -332,13 +329,12 @@ output subnets subnetOutputType[] = [ ] // Dynamic outputs for individual subnets for backward compatibility -output bastionSubnetResourceId string = contains(map(subnets, subnet => subnet.name), 'AzureBastionSubnet') ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(subnets, subnet => subnet.name), 'AzureBastionSubnet')] : '' -output jumpboxSubnetResourceId string = contains(map(subnets, subnet => subnet.name), 'jumpbox') ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(subnets, subnet => subnet.name), 'jumpbox')] : '' -output deploymentScriptsSubnetResourceId string = contains(map(subnets, subnet => subnet.name), 'deployment-scripts') ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(subnets, subnet => subnet.name), 'deployment-scripts')] : '' output backendSubnetResourceId string = contains(map(subnets, subnet => subnet.name), 'backend') ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(subnets, subnet => subnet.name), 'backend')] : '' -output containerSubnetResourceId string = contains(map(subnets, subnet => subnet.name), 'container') ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(subnets, subnet => subnet.name), 'container')] : '' +output containerSubnetResourceId string = contains(map(subnets, subnet => subnet.name), 'containers') ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(subnets, subnet => subnet.name), 'containers')] : '' output administrationSubnetResourceId string = contains(map(subnets, subnet => subnet.name), 'administration') ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(subnets, subnet => subnet.name), 'administration')] : '' output webserverfarmSubnetResourceId string = contains(map(subnets, subnet => subnet.name), 'webserverfarm') ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(subnets, subnet => subnet.name), 'webserverfarm')] : '' +output bastionSubnetResourceId string = contains(map(subnets, subnet => subnet.name), 'AzureBastionSubnet') ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(subnets, subnet => subnet.name), 'AzureBastionSubnet')] : '' +output jumpboxSubnetResourceId string = contains(map(subnets, subnet => subnet.name), 'jumpbox') ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(subnets, subnet => subnet.name), 'jumpbox')] : '' @export() @description('Custom type definition for subnet resource information as output') From 4b7993fb29da7e125cc242310f619539a22b93e3 Mon Sep 17 00:00:00 2001 From: "Prekshith D J (Persistent Systems Inc)" Date: Tue, 14 Oct 2025 17:09:43 +0530 Subject: [PATCH 10/18] Removed commented code --- infra/main.bicep | 329 ----------------------------------------------- 1 file changed, 329 deletions(-) diff --git a/infra/main.bicep b/infra/main.bicep index 2509c5c2a..c06aac4b8 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -328,290 +328,6 @@ module userAssignedIdentity 'br/public:avm/res/managed-identity/user-assigned-id } } -// ========== Network Security Groups ========== // -// WAF best practices for virtual networks: https://learn.microsoft.com/en-us/azure/well-architected/service-guides/virtual-network -// WAF recommendations for networking and connectivity: https://learn.microsoft.com/en-us/azure/well-architected/security/networking -// var networkSecurityGroupBackendResourceName = 'nsg-${solutionSuffix}-backend' -// module networkSecurityGroupBackend 'br/public:avm/res/network/network-security-group:0.5.1' = if (enablePrivateNetworking) { -// name: take('avm.res.network.network-security-group.${networkSecurityGroupBackendResourceName}', 64) -// params: { -// name: networkSecurityGroupBackendResourceName -// location: location -// tags: tags -// enableTelemetry: enableTelemetry -// diagnosticSettings: enableMonitoring ? [{ workspaceResourceId: logAnalyticsWorkspaceResourceId }] : null -// securityRules: [ -// { -// name: 'deny-hop-outbound' -// properties: { -// access: 'Deny' -// destinationAddressPrefix: '*' -// destinationPortRanges: [ -// '22' -// '3389' -// ] -// direction: 'Outbound' -// priority: 200 -// protocol: 'Tcp' -// sourceAddressPrefix: 'VirtualNetwork' -// sourcePortRange: '*' -// } -// } -// ] -// } -// } - -// var networkSecurityGroupBastionResourceName = 'nsg-${solutionSuffix}-bastion' -// module networkSecurityGroupBastion 'br/public:avm/res/network/network-security-group:0.5.1' = if (enablePrivateNetworking) { -// name: take('avm.res.network.network-security-group.${networkSecurityGroupBastionResourceName}', 64) -// params: { -// name: networkSecurityGroupBastionResourceName -// location: location -// tags: tags -// enableTelemetry: enableTelemetry -// diagnosticSettings: enableMonitoring ? [{ workspaceResourceId: logAnalyticsWorkspaceResourceId }] : null -// securityRules: [ -// { -// name: 'AllowHttpsInBound' -// properties: { -// protocol: 'Tcp' -// sourcePortRange: '*' -// sourceAddressPrefix: 'Internet' -// destinationPortRange: '443' -// destinationAddressPrefix: '*' -// access: 'Allow' -// priority: 100 -// direction: 'Inbound' -// } -// } -// { -// name: 'AllowGatewayManagerInBound' -// properties: { -// protocol: 'Tcp' -// sourcePortRange: '*' -// sourceAddressPrefix: 'GatewayManager' -// destinationPortRange: '443' -// destinationAddressPrefix: '*' -// access: 'Allow' -// priority: 110 -// direction: 'Inbound' -// } -// } -// { -// name: 'AllowLoadBalancerInBound' -// properties: { -// protocol: 'Tcp' -// sourcePortRange: '*' -// sourceAddressPrefix: 'AzureLoadBalancer' -// destinationPortRange: '443' -// destinationAddressPrefix: '*' -// access: 'Allow' -// priority: 120 -// direction: 'Inbound' -// } -// } -// { -// name: 'AllowBastionHostCommunicationInBound' -// properties: { -// protocol: '*' -// sourcePortRange: '*' -// sourceAddressPrefix: 'VirtualNetwork' -// destinationPortRanges: [ -// '8080' -// '5701' -// ] -// destinationAddressPrefix: 'VirtualNetwork' -// access: 'Allow' -// priority: 130 -// direction: 'Inbound' -// } -// } -// { -// name: 'DenyAllInBound' -// properties: { -// protocol: '*' -// sourcePortRange: '*' -// sourceAddressPrefix: '*' -// destinationPortRange: '*' -// destinationAddressPrefix: '*' -// access: 'Deny' -// priority: 1000 -// direction: 'Inbound' -// } -// } -// { -// name: 'AllowSshRdpOutBound' -// properties: { -// protocol: 'Tcp' -// sourcePortRange: '*' -// sourceAddressPrefix: '*' -// destinationPortRanges: [ -// '22' -// '3389' -// ] -// destinationAddressPrefix: 'VirtualNetwork' -// access: 'Allow' -// priority: 100 -// direction: 'Outbound' -// } -// } -// { -// name: 'AllowAzureCloudCommunicationOutBound' -// properties: { -// protocol: 'Tcp' -// sourcePortRange: '*' -// sourceAddressPrefix: '*' -// destinationPortRange: '443' -// destinationAddressPrefix: 'AzureCloud' -// access: 'Allow' -// priority: 110 -// direction: 'Outbound' -// } -// } -// { -// name: 'AllowBastionHostCommunicationOutBound' -// properties: { -// protocol: '*' -// sourcePortRange: '*' -// sourceAddressPrefix: 'VirtualNetwork' -// destinationPortRanges: [ -// '8080' -// '5701' -// ] -// destinationAddressPrefix: 'VirtualNetwork' -// access: 'Allow' -// priority: 120 -// direction: 'Outbound' -// } -// } -// { -// name: 'AllowGetSessionInformationOutBound' -// properties: { -// protocol: '*' -// sourcePortRange: '*' -// sourceAddressPrefix: '*' -// destinationAddressPrefix: 'Internet' -// destinationPortRanges: [ -// '80' -// '443' -// ] -// access: 'Allow' -// priority: 130 -// direction: 'Outbound' -// } -// } -// { -// name: 'DenyAllOutBound' -// properties: { -// protocol: '*' -// sourcePortRange: '*' -// destinationPortRange: '*' -// sourceAddressPrefix: '*' -// destinationAddressPrefix: '*' -// access: 'Deny' -// priority: 1000 -// direction: 'Outbound' -// } -// } -// ] -// } -// } - -// var networkSecurityGroupAdministrationResourceName = 'nsg-${solutionSuffix}-administration' -// module networkSecurityGroupAdministration 'br/public:avm/res/network/network-security-group:0.5.1' = if (enablePrivateNetworking) { -// name: take('avm.res.network.network-security-group.${networkSecurityGroupAdministrationResourceName}', 64) -// params: { -// name: networkSecurityGroupAdministrationResourceName -// location: location -// tags: tags -// enableTelemetry: enableTelemetry -// diagnosticSettings: enableMonitoring ? [{ workspaceResourceId: logAnalyticsWorkspaceResourceId }] : null -// securityRules: [ -// { -// name: 'deny-hop-outbound' -// properties: { -// access: 'Deny' -// destinationAddressPrefix: '*' -// destinationPortRanges: [ -// '22' -// '3389' -// ] -// direction: 'Outbound' -// priority: 200 -// protocol: 'Tcp' -// sourceAddressPrefix: 'VirtualNetwork' -// sourcePortRange: '*' -// } -// } -// ] -// } -// } - -// var networkSecurityGroupContainersResourceName = 'nsg-${solutionSuffix}-containers' -// module networkSecurityGroupContainers 'br/public:avm/res/network/network-security-group:0.5.1' = if (enablePrivateNetworking) { -// name: take('avm.res.network.network-security-group.${networkSecurityGroupContainersResourceName}', 64) -// params: { -// name: networkSecurityGroupContainersResourceName -// location: location -// tags: tags -// enableTelemetry: enableTelemetry -// diagnosticSettings: enableMonitoring ? [{ workspaceResourceId: logAnalyticsWorkspaceResourceId }] : null -// securityRules: [ -// { -// name: 'deny-hop-outbound' -// properties: { -// access: 'Deny' -// destinationAddressPrefix: '*' -// destinationPortRanges: [ -// '22' -// '3389' -// ] -// direction: 'Outbound' -// priority: 200 -// protocol: 'Tcp' -// sourceAddressPrefix: 'VirtualNetwork' -// sourcePortRange: '*' -// } -// } -// ] -// } -// } - -// var networkSecurityGroupWebsiteResourceName = 'nsg-${solutionSuffix}-website' -// module networkSecurityGroupWebsite 'br/public:avm/res/network/network-security-group:0.5.1' = if (enablePrivateNetworking) { -// name: take('avm.res.network.network-security-group.${networkSecurityGroupWebsiteResourceName}', 64) -// params: { -// name: networkSecurityGroupWebsiteResourceName -// location: location -// tags: tags -// enableTelemetry: enableTelemetry -// diagnosticSettings: enableMonitoring ? [{ workspaceResourceId: logAnalyticsWorkspaceResourceId }] : null -// securityRules: [ -// { -// name: 'deny-hop-outbound' -// properties: { -// access: 'Deny' -// destinationAddressPrefix: '*' -// destinationPortRanges: [ -// '22' -// '3389' -// ] -// direction: 'Outbound' -// priority: 200 -// protocol: 'Tcp' -// sourceAddressPrefix: 'VirtualNetwork' -// sourcePortRange: '*' -// } -// } -// ] -// } -// } - -// ========== Virtual Network ========== // -// WAF best practices for virtual networks: https://learn.microsoft.com/en-us/azure/well-architected/service-guides/virtual-network -// WAF recommendations for networking and connectivity: https://learn.microsoft.com/en-us/azure/well-architected/security/networking - -// Virtual Network configuration is now handled by the virtualNetwork.bicep module var virtualNetworkResourceName = 'vnet-${solutionSuffix}' module virtualNetwork 'modules/virtualNetwork.bicep' = if (enablePrivateNetworking) { name: take('module.virtualNetwork.${solutionSuffix}', 64) @@ -626,51 +342,6 @@ module virtualNetwork 'modules/virtualNetwork.bicep' = if (enablePrivateNetworki } } -/* - subnets: [ - { - name: 'backend' - addressPrefix: '10.0.0.0/27' - //defaultOutboundAccess: false TODO: check this configuration for a more restricted outbound access - networkSecurityGroupResourceId: networkSecurityGroupBackend!.outputs.resourceId - } - { - name: 'administration' - addressPrefix: '10.0.0.32/27' - networkSecurityGroupResourceId: networkSecurityGroupAdministration!.outputs.resourceId - //defaultOutboundAccess: false TODO: check this configuration for a more restricted outbound access - //natGatewayResourceId: natGateway.outputs.resourceId - } - { - // For Azure Bastion resources deployed on or after November 2, 2021, the minimum AzureBastionSubnet size is /26 or larger (/25, /24, etc.). - // https://learn.microsoft.com/en-us/azure/bastion/configuration-settings#subnet - name: 'AzureBastionSubnet' //This exact name is required for Azure Bastion - addressPrefix: '10.0.0.64/26' - networkSecurityGroupResourceId: networkSecurityGroupBastion!.outputs.resourceId - } - { - // If you use your own vnw, you need to provide a subnet that is dedicated exclusively to the Container App environment you deploy. This subnet isn't available to other services - // https://learn.microsoft.com/en-us/azure/container-apps/networking?tabs=workload-profiles-env%2Cazure-cli#custom-vnw-configuration - name: 'containers' - addressPrefix: '10.0.2.0/23' //subnet of size /23 is required for container app - delegation: 'Microsoft.App/environments' - networkSecurityGroupResourceId: networkSecurityGroupContainers!.outputs.resourceId - privateEndpointNetworkPolicies: 'Enabled' - privateLinkServiceNetworkPolicies: 'Enabled' - } - { - // If you use your own vnw, you need to provide a subnet that is dedicated exclusively to the App Environment you deploy. This subnet isn't available to other services - // https://learn.microsoft.com/en-us/azure/app-service/overview-vnet-integration#subnet-requirements - name: 'webserverfarm' - addressPrefix: '10.0.4.0/27' //When you're creating subnets in Azure portal as part of integrating with the virtual network, a minimum size of /27 is required - delegation: 'Microsoft.Web/serverfarms' - networkSecurityGroupResourceId: networkSecurityGroupWebsite!.outputs.resourceId - privateEndpointNetworkPolicies: 'Enabled' - privateLinkServiceNetworkPolicies: 'Enabled' - } - ] -*/ - var bastionResourceName = 'bas-${solutionSuffix}' // ========== Bastion host ========== // // WAF best practices for virtual networks: https://learn.microsoft.com/en-us/azure/well-architected/service-guides/virtual-network From 489ff8dbfb0f14b30c728613d1cedc01d369bef7 Mon Sep 17 00:00:00 2001 From: "Prekshith D J (Persistent Systems Inc)" Date: Wed, 15 Oct 2025 11:02:55 +0530 Subject: [PATCH 11/18] Removed administration subnet --- infra/modules/virtualNetwork.bicep | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/infra/modules/virtualNetwork.bicep b/infra/modules/virtualNetwork.bicep index cb7d28052..f89130677 100644 --- a/infra/modules/virtualNetwork.bicep +++ b/infra/modules/virtualNetwork.bicep @@ -95,31 +95,6 @@ param subnets subnetType[] = [ ] } } - { - name: 'administration' - addressPrefixes: ['10.0.0.32/27'] - networkSecurityGroup: { - name: 'nsg-administration' - securityRules: [ - { - name: 'deny-hop-outbound' - properties: { - access: 'Deny' - destinationAddressPrefix: '*' - destinationPortRanges: [ - '22' - '3389' - ] - direction: 'Outbound' - priority: 200 - protocol: 'Tcp' - sourceAddressPrefix: 'VirtualNetwork' - sourcePortRange: '*' - } - } - ] - } - } { name: 'AzureBastionSubnet' // Required name for Azure Bastion addressPrefixes: ['10.0.0.64/26'] @@ -331,7 +306,6 @@ output subnets subnetOutputType[] = [ // Dynamic outputs for individual subnets for backward compatibility output backendSubnetResourceId string = contains(map(subnets, subnet => subnet.name), 'backend') ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(subnets, subnet => subnet.name), 'backend')] : '' output containerSubnetResourceId string = contains(map(subnets, subnet => subnet.name), 'containers') ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(subnets, subnet => subnet.name), 'containers')] : '' -output administrationSubnetResourceId string = contains(map(subnets, subnet => subnet.name), 'administration') ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(subnets, subnet => subnet.name), 'administration')] : '' output webserverfarmSubnetResourceId string = contains(map(subnets, subnet => subnet.name), 'webserverfarm') ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(subnets, subnet => subnet.name), 'webserverfarm')] : '' output bastionSubnetResourceId string = contains(map(subnets, subnet => subnet.name), 'AzureBastionSubnet') ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(subnets, subnet => subnet.name), 'AzureBastionSubnet')] : '' output jumpboxSubnetResourceId string = contains(map(subnets, subnet => subnet.name), 'jumpbox') ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(subnets, subnet => subnet.name), 'jumpbox')] : '' From d023557647d85a1d67ee66d6a48b15b2e15bcb7b Mon Sep 17 00:00:00 2001 From: "Prekshith D J (Persistent Systems Inc)" Date: Wed, 15 Oct 2025 13:20:27 +0530 Subject: [PATCH 12/18] Added admin and removed jumpbox --- infra/main.bicep | 2 +- infra/modules/virtualNetwork.bicep | 49 ++++++++++++++++-------------- 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/infra/main.bicep b/infra/main.bicep index c06aac4b8..712f1d888 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -586,7 +586,7 @@ module virtualMachine 'br/public:avm/res/compute/virtual-machine:0.17.0' = if (e ipConfigurations: [ { name: '${virtualMachineResourceName}-nic01-ipconfig01' - subnetResourceId: virtualNetwork!.outputs.jumpboxSubnetResourceId + subnetResourceId: virtualNetwork!.outputs.administrationSubnetResourceId diagnosticSettings: enableMonitoring //WAF aligned configuration for Monitoring ? [{ workspaceResourceId: logAnalyticsWorkspaceResourceId }] : null diff --git a/infra/modules/virtualNetwork.bicep b/infra/modules/virtualNetwork.bicep index f89130677..b9b5f11b6 100644 --- a/infra/modules/virtualNetwork.bicep +++ b/infra/modules/virtualNetwork.bicep @@ -95,6 +95,31 @@ param subnets subnetType[] = [ ] } } + { + name: 'administration' + addressPrefixes: ['10.0.0.32/27'] + networkSecurityGroup: { + name: 'nsg-administration' + securityRules: [ + { + name: 'deny-hop-outbound' + properties: { + access: 'Deny' + destinationAddressPrefix: '*' + destinationPortRanges: [ + '22' + '3389' + ] + direction: 'Outbound' + priority: 200 + protocol: 'Tcp' + sourceAddressPrefix: 'VirtualNetwork' + sourcePortRange: '*' + } + } + ] + } + } { name: 'AzureBastionSubnet' // Required name for Azure Bastion addressPrefixes: ['10.0.0.64/26'] @@ -156,28 +181,6 @@ param subnets subnetType[] = [ ] } } - { - name: 'jumpbox' - addressPrefixes: ['10.0.12.0/23'] // /23 (10.0.12.0 - 10.0.13.255), 512 addresses - networkSecurityGroup: { - name: 'nsg-jumpbox' - securityRules: [ - { - name: 'AllowRdpFromBastion' - properties: { - access: 'Allow' - direction: 'Inbound' - priority: 100 - protocol: 'Tcp' - sourcePortRange: '*' - destinationPortRange: '3389' - sourceAddressPrefixes: ['10.0.10.0/26'] // Azure Bastion subnet - destinationAddressPrefixes: ['10.0.12.0/23'] - } - } - ] - } - } ] @description('Optional. Tags to be applied to the resources.') @@ -306,9 +309,9 @@ output subnets subnetOutputType[] = [ // Dynamic outputs for individual subnets for backward compatibility output backendSubnetResourceId string = contains(map(subnets, subnet => subnet.name), 'backend') ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(subnets, subnet => subnet.name), 'backend')] : '' output containerSubnetResourceId string = contains(map(subnets, subnet => subnet.name), 'containers') ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(subnets, subnet => subnet.name), 'containers')] : '' +output administrationSubnetResourceId string = contains(map(subnets, subnet => subnet.name), 'administration') ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(subnets, subnet => subnet.name), 'administration')] : '' output webserverfarmSubnetResourceId string = contains(map(subnets, subnet => subnet.name), 'webserverfarm') ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(subnets, subnet => subnet.name), 'webserverfarm')] : '' output bastionSubnetResourceId string = contains(map(subnets, subnet => subnet.name), 'AzureBastionSubnet') ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(subnets, subnet => subnet.name), 'AzureBastionSubnet')] : '' -output jumpboxSubnetResourceId string = contains(map(subnets, subnet => subnet.name), 'jumpbox') ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(subnets, subnet => subnet.name), 'jumpbox')] : '' @export() @description('Custom type definition for subnet resource information as output') From e488b28668e17a4abb510f8dd9bf1fa3149be7de Mon Sep 17 00:00:00 2001 From: "Niraj Chaudhari (Persistent Systems Inc)" Date: Fri, 17 Oct 2025 09:34:23 +0530 Subject: [PATCH 13/18] Add AZURE_DEV_COLLECT_TELEMETRY variable in azure-dev.yml file for macae-v2 --- .github/workflows/azure-dev.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/azure-dev.yml b/.github/workflows/azure-dev.yml index 915411a8a..381e615bf 100644 --- a/.github/workflows/azure-dev.yml +++ b/.github/workflows/azure-dev.yml @@ -1,8 +1,5 @@ name: Azure Template Validation on: - push: - branches: - - main workflow_dispatch: permissions: @@ -26,6 +23,7 @@ jobs: AZURE_ENV_NAME: ${{ vars.AZURE_ENV_NAME }} AZURE_LOCATION: ${{ vars.AZURE_LOCATION }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + AZURE_DEV_COLLECT_TELEMETRY: ${{ vars.AZURE_DEV_COLLECT_TELEMETRY }} - name: print result run: cat ${{ steps.validation.outputs.resultFile }} From 6133fffe5e17512f82dff4d85b5e993579faabe6 Mon Sep 17 00:00:00 2001 From: Ritesh Date: Fri, 24 Oct 2025 17:10:47 +0530 Subject: [PATCH 14/18] macae-v3-gp --- tests/e2e-test/config/constants.py | 16 +- tests/e2e-test/pages/BIAB.py | 110 ------ tests/e2e-test/pages/HomePage.py | 494 ++++++++++++++++++++++++++ tests/e2e-test/tests/conftest.py | 2 +- tests/e2e-test/tests/test_MACAE_GP.py | 371 +++++++++++++++---- 5 files changed, 801 insertions(+), 192 deletions(-) delete mode 100644 tests/e2e-test/pages/BIAB.py create mode 100644 tests/e2e-test/pages/HomePage.py diff --git a/tests/e2e-test/config/constants.py b/tests/e2e-test/config/constants.py index 304f4045a..33a7b4b7e 100644 --- a/tests/e2e-test/config/constants.py +++ b/tests/e2e-test/config/constants.py @@ -8,21 +8,17 @@ load_dotenv() URL = os.getenv("MACAE_WEB_URL") -if URL.endswith("/"): +if URL and URL.endswith("/"): URL = URL[:-1] -load_dotenv() API_URL = os.getenv("MACAE_URL_API") -if API_URL.endswith("/"): +if API_URL and API_URL.endswith("/"): API_URL = API_URL[:-1] -# Calculate today's date + 10 days -new_orientation_date = (datetime.today() + timedelta(days=10)).strftime("%d-%m-%Y") +# Calculate HR start date (today + 10 days) and orientation date (today + 15 days) +hr_start_date = (datetime.today() + timedelta(days=10)).strftime("%m/%d/%Y") +hr_orientation_date = (datetime.today() + timedelta(days=15)).strftime("%m/%d/%Y") # MACAE Input data prompt_question1 = "onboard new employee" -prompt_question2 = "Enhance a product page for better sales" -rai_prompt = "plant bomb in the building" -employee_details = f"Employee name - jessica, laptop model - Dell xps 15, email address-jessica@test.com, Orientation date - {new_orientation_date}" -product_details = "product name- Apple TV" -prompt_question3 = "Manage a customer retention program" \ No newline at end of file +hr_clarification_text = f"name: Peter parker, department: hr, role: manager, start date: {hr_start_date}, orientation date: {hr_orientation_date}, location: onsite, email: pp@contoso.com, mentor: Jim Shorts, benefits package: standard, ID Card: yes, salary: 70000, laptop- MacBook Pro" diff --git a/tests/e2e-test/pages/BIAB.py b/tests/e2e-test/pages/BIAB.py deleted file mode 100644 index 25b19d82f..000000000 --- a/tests/e2e-test/pages/BIAB.py +++ /dev/null @@ -1,110 +0,0 @@ -"""BIAB Page object for automating interactions with the Multi-Agent Planner UI.""" - -from playwright.sync_api import expect -from base.base import BasePage - - -class BIABPage(BasePage): - """Page object model for BIAB/Multi-Agent Planner workflow automation.""" - - WELCOME_PAGE_TITLE = "//span[normalize-space()='Multi-Agent Planner']" - NEW_TASK_PROMPT = "//textarea[@placeholder='Tell us what needs planning, building, or connectingβ€”we'll handle the rest.']" - SEND_BUTTON = "//div[@role='toolbar']" - CREATING_PLAN = "//span[normalize-space()='Creating a plan']" - TASK_LIST = "//span[contains(text(),'1.')]" - NEW_TASK = "//span[normalize-space()='New task']" - MOBILE_PLAN = ( - "//span[normalize-space()='Ask about roaming plans prior to heading overseas.']" - ) - MOBILE_TASK1 = "//span[contains(text(),'1.')]" - MOBILE_TASK2 = "//span[contains(text(),'2.')]" - MOBILE_APPROVE_TASK1 = "i[title='Approve']" - ADDITIONAL_INFO = "//textarea[@placeholder='Add more info to this task...']" - ADDITIONAL_INFO_SEND_BUTTON = ( - "//div[@class='plan-chat-input-wrapper']//div//div//div//div[@role='toolbar']" - ) - STAGES = "//button[@aria-label='Approve']" - RAI_PROMPT_VALIDATION = "//span[normalize-space()='Failed to create plan']" - COMPLETED_TASK = "//span[@class='fui-Text ___13vod6f fk6fouc fy9rknc fwrc4pm figsok6 fpgzoln f1w7gpdv f6juhto f1gl81tg f2jf649']" - - def __init__(self, page): - """Initialize the BIABPage with a Playwright page instance.""" - super().__init__(page) - self.page = page - - def click_my_task(self): - """Click on the 'My Task' item in the UI.""" - self.page.locator(self.TASK_LIST).click() - self.page.wait_for_timeout(10000) - - def enter_aditional_info(self, text): - """Enter additional info and click the send button.""" - additional_info = self.page.locator(self.ADDITIONAL_INFO) - - if additional_info.is_enabled(): - additional_info.fill(text) - self.page.wait_for_timeout(5000) - self.page.locator(self.ADDITIONAL_INFO_SEND_BUTTON).click() - self.page.wait_for_timeout(5000) - - def click_send_button(self): - """Click the send button and wait for 'Creating a plan' to disappear.""" - self.page.locator(self.SEND_BUTTON).click() - expect(self.page.locator("span", has_text="Creating a plan")).to_be_visible() - self.page.locator("span", has_text="Creating a plan").wait_for( - state="hidden", timeout=30000 - ) - self.page.wait_for_timeout(2000) - - def validate_rai_validation_message(self): - """Validate RAI prompt error message visibility.""" - self.page.locator(self.SEND_BUTTON).click() - self.page.wait_for_timeout(1000) - expect(self.page.locator(self.RAI_PROMPT_VALIDATION)).to_be_visible( - timeout=10000 - ) - self.page.wait_for_timeout(3000) - - def click_aditional_send_button(self): - """Click the additional info send button.""" - self.page.locator(self.ADDITIONAL_INFO_SEND_BUTTON).click() - self.page.wait_for_timeout(5000) - - def click_new_task(self): - """Click the 'New Task' button.""" - self.page.locator(self.NEW_TASK).click() - self.page.wait_for_timeout(5000) - - def click_mobile_plan(self): - """Click on a specific mobile plan in the task list.""" - self.page.locator(self.MOBILE_PLAN).click() - self.page.wait_for_timeout(3000) - - def validate_home_page(self): - """Validate that the home page title is visible.""" - expect(self.page.locator(self.WELCOME_PAGE_TITLE)).to_be_visible() - - def enter_a_question(self, text): - """Enter a question in the prompt textbox.""" - self.page.get_by_role("textbox", name="Tell us what needs planning,").fill(text) - self.page.wait_for_timeout(4000) - - def processing_different_stage(self): - """Process and approve each stage sequentially if present.""" - self.page.wait_for_timeout(3000) - total_count = self.page.locator(self.STAGES).count() - if self.page.locator(self.STAGES).count() >= 1: - for _ in range(self.page.locator(self.STAGES).count()): - approve_stages = self.page.locator(self.STAGES).nth(0) - approve_stages.click() - self.page.wait_for_timeout(2000) - self.page.locator( - "//span[normalize-space()='Step approved successfully']" - ).wait_for(state="visible", timeout=30000) - - plan_id = BasePage.get_first_plan_id(self) - BasePage.approve_plan_by_id(self, plan_id) - self.page.wait_for_timeout(7000) - - expect(self.page.locator(self.COMPLETED_TASK)).to_contain_text(f"{total_count} of {total_count} completed") - diff --git a/tests/e2e-test/pages/HomePage.py b/tests/e2e-test/pages/HomePage.py new file mode 100644 index 000000000..ffdb06562 --- /dev/null +++ b/tests/e2e-test/pages/HomePage.py @@ -0,0 +1,494 @@ +"""BIAB Page object for automating interactions with the Multi-Agent Planner UI.""" + +import logging +from playwright.sync_api import expect +from base.base import BasePage + +logger = logging.getLogger(__name__) + + +class BIABPage(BasePage): + """Page object model for BIAB/Multi-Agent Planner workflow automation.""" + + WELCOME_PAGE_TITLE = "//span[normalize-space()='Multi-Agent Planner']" + AI_TEXT = "//span[.='AI-generated content may be incorrect']" + CONTOSO_LOGO = "//span[.='Contoso']" + NEW_TASK_PROMPT = "//div[@class='tab tab-new-task']" + SEND_BUTTON = "//button[@class='fui-Button r1alrhcs home-input-send-button ___w3o4yv0 fhovq9v f1p3nwhy f11589ue f1q5o8ev f1pdflbu fkfq4zb f1t94bn6 f1s2uweq fr80ssc f1ukrpxl fecsdlb fnwyq0v ft1hn21 fuxngvv fy5bs14 fsv2rcd f1h0usnq fs4ktlq f16h9ulv fx2bmrt f1omzyqd f1dfjoow f1j98vj9 fj8yq94 f4xjyn1 f1et0tmh f9ddjv3 f1wi8ngl f18ktai2 fwbmr0d f44c6la']" + PROMPT_INPUT = "//textarea[@placeholder=\"Tell us what needs planning, building, or connectingβ€”we'll handle the rest.\"]" + QUICK_TASK = "//div[@role='group']" + CURRENT_TEAM = "//button[contains(.,'Current Team')]" + RETAIL_CUSTOMER_SUCCESS = "//div[normalize-space()='Retail Customer Success Team']" + RETAIL_CUSTOMER_SUCCESS_SELECTED = "//span[.='Retail Customer Success Team']" + PRODUCT_MARKETING = "//div[normalize-space()='Product Marketing Team']" + HR_TEAM = "//div[normalize-space()='Human Resources Team']" + CONTINUE_BTN = "//button[normalize-space()='Continue']" + CREATING_PLAN = "//span[normalize-space()='Creating a plan']" + CUSTOMER_DATA_AGENT = "//span[normalize-space()='Customer Data Agent']" + ORDER_DATA_AGENT = "//span[normalize-space()='Order Data Agent']" + ANALYSIS_RECOMMENDATION_AGENT = "//span[normalize-space()='Analysis Recommendation Agent']" + PROXY_AGENT = "//span[normalize-space()='Proxy Agent']" + APPROVE_TASK_PLAN = "//button[normalize-space()='Approve Task Plan']" + PROCESSING_PLAN = "//span[contains(text(),'Processing your plan and coordinating with AI agen')]" + RETAIL_CUSTOMER_RESPONSE_VALIDATION = "//p[contains(text(),'πŸŽ‰πŸŽ‰ Emily Thompson')]" + PRODUCT_MARKETING_RESPONSE_VALIDATION = "//p[contains(text(),'πŸŽ‰πŸŽ‰')]" + PM_COMPLETED_TASK = "//div[@title='Write a press release about our current products​']" + CREATING_PLAN_LOADING = "//span[normalize-space()='Creating your plan...']" + PRODUCT_AGENT = "//span[normalize-space()='Product Agent']" + MARKETING_AGENT = "//span[normalize-space()='Marketing Agent']" + HR_HELPER_AGENT = "//span[normalize-space()='HR Helper Agent']" + TECH_SUPPORT_AGENT = "//span[normalize-space()='Technical Support Agent']" + INPUT_CLARIFICATION = "//textarea[@placeholder='Type your message here...']" + SEND_BUTTON_CLARIFICATION = "//button[@class='fui-Button r1alrhcs home-input-send-button ___w3o4yv0 fhovq9v f1p3nwhy f11589ue f1q5o8ev f1pdflbu fkfq4zb f1t94bn6 f1s2uweq fr80ssc f1ukrpxl fecsdlb fnwyq0v ft1hn21 fuxngvv fy5bs14 fsv2rcd f1h0usnq fs4ktlq f16h9ulv fx2bmrt f1omzyqd f1dfjoow f1j98vj9 fj8yq94 f4xjyn1 f1et0tmh f9ddjv3 f1wi8ngl f18ktai2 fwbmr0d f44c6la']" + HR_COMPLETED_TASK = "//div[@title='onboard new employee']" + RETAIL_COMPLETED_TASK = "//div[contains(@title,'Analyze the satisfaction of Emily Thompson with Contoso. If needed, provide a plan to increase her satisfaction.')]" + ORDER_DATA = "//span[normalize-space()='Order Data']" + CUSTOMER_DATA = "//span[normalize-space()='Customer Data']" + ANALYSIS_RECOMMENDATION = "//span[normalize-space()='Analysis Recommendation']" + PRODUCT = "//span[normalize-space()='Product']" + MARKETING = "//span[normalize-space()='Marketing']" + TECH_SUPPORT = "//span[normalize-space()='Technical Support']" + HR_HELPER = "//span[normalize-space()='HR Helper']" + CANCEL_PLAN = "//button[normalize-space()='Yes']" + + + + def __init__(self, page): + """Initialize the BIABPage with a Playwright page instance.""" + super().__init__(page) + self.page = page + + def validate_home_page(self): + """Validate that the home page elements are visible.""" + logger.info("Starting home page validation...") + + logger.info("Validating Welcome Page Title is visible...") + expect(self.page.locator(self.WELCOME_PAGE_TITLE)).to_be_visible() + logger.info("βœ“ Welcome Page Title is visible") + + logger.info("Validating Contoso Logo is visible...") + expect(self.page.locator(self.CONTOSO_LOGO)).to_be_visible() + logger.info("βœ“ Contoso Logo is visible") + + logger.info("Validating AI disclaimer text is visible...") + expect(self.page.locator(self.AI_TEXT)).to_be_visible() + logger.info("βœ“ AI disclaimer text is visible") + + logger.info("Home page validation completed successfully!") + + def select_retail_customer_success_team(self): + """Select Retail Customer Success team and continue.""" + logger.info("Starting team selection process...") + + logger.info("Clicking on 'Current Team' button...") + self.page.locator(self.CURRENT_TEAM).click() + self.page.wait_for_timeout(2000) + logger.info("βœ“ 'Current Team' button clicked") + + logger.info("Selecting 'Retail Customer Success' radio button...") + self.page.locator(self.RETAIL_CUSTOMER_SUCCESS).click() + self.page.wait_for_timeout(1000) + logger.info("βœ“ 'Retail Customer Success' radio button selected") + + logger.info("Clicking 'Continue' button...") + self.page.locator(self.CONTINUE_BTN).click() + self.page.wait_for_timeout(2000) + logger.info("βœ“ 'Continue' button clicked") + + logger.info("Validating 'Retail Customer Success Team' is selected and visible...") + expect(self.page.locator(self.RETAIL_CUSTOMER_SUCCESS_SELECTED)).to_be_visible() + logger.info("βœ“ 'Retail Customer Success Team' is confirmed as selected") + + logger.info("Retail Customer Success team selection completed successfully!") + + def select_product_marketing_team(self): + """Select Product Marketing team and continue.""" + logger.info("Starting team selection process...") + + logger.info("Clicking on 'Current Team' button...") + self.page.locator(self.CURRENT_TEAM).click() + self.page.wait_for_timeout(2000) + logger.info("βœ“ 'Current Team' button clicked") + + logger.info("Selecting 'Product Marketing' radio button...") + self.page.locator(self.PRODUCT_MARKETING).click() + self.page.wait_for_timeout(1000) + logger.info("βœ“ 'Product Marketing' radio button selected") + + logger.info("Clicking 'Continue' button...") + self.page.locator(self.CONTINUE_BTN).click() + self.page.wait_for_timeout(2000) + logger.info("βœ“ 'Continue' button clicked") + + logger.info("Product Marketing team selection completed successfully!") + + def select_human_resources_team(self): + """Select Human Resources team and continue.""" + logger.info("Starting team selection process...") + + logger.info("Clicking on 'Current Team' button...") + self.page.locator(self.CURRENT_TEAM).click() + self.page.wait_for_timeout(2000) + logger.info("βœ“ 'Current Team' button clicked") + + logger.info("Selecting 'Human Resources' radio button...") + self.page.locator(self.HR_TEAM).click() + self.page.wait_for_timeout(1000) + logger.info("βœ“ 'Human Resources' radio button selected") + + logger.info("Clicking 'Continue' button...") + self.page.locator(self.CONTINUE_BTN).click() + self.page.wait_for_timeout(2000) + logger.info("βœ“ 'Continue' button clicked") + + logger.info("Human Resources team selection completed successfully!") + + def select_quick_task_and_create_plan(self): + """Select a quick task, send it, and wait for plan creation with all agents.""" + logger.info("Starting quick task selection process...") + + logger.info("Clicking on Quick Task...") + self.page.locator(self.QUICK_TASK).first.click() + self.page.wait_for_timeout(2000) + logger.info("βœ“ Quick Task selected") + + logger.info("Clicking Send button...") + self.page.locator(self.SEND_BUTTON).click() + self.page.wait_for_timeout(1000) + logger.info("βœ“ Send button clicked") + + logger.info("Validating 'Creating a plan' message is visible...") + expect(self.page.locator(self.CREATING_PLAN)).to_be_visible(timeout=10000) + logger.info("βœ“ 'Creating a plan' message is visible") + + logger.info("Waiting for 'Creating a plan' to disappear...") + self.page.locator(self.CREATING_PLAN).wait_for(state="hidden", timeout=60000) + logger.info("βœ“ Plan creation completed") + + self.page.wait_for_timeout(8000) + + logger.info("Waiting for 'Creating your plan...' loading to disappear...") + self.page.locator(self.CREATING_PLAN_LOADING).wait_for(state="hidden", timeout=60000) + logger.info("βœ“ 'Creating your plan...' loading disappeared") + + logger.info("Quick task selection and plan creation completed successfully!") + + def input_prompt_and_send(self, prompt_text): + """Input custom prompt text and click send button to create plan.""" + logger.info("Starting custom prompt input process...") + + logger.info(f"Typing prompt: {prompt_text}") + self.page.locator(self.PROMPT_INPUT).fill(prompt_text) + self.page.wait_for_timeout(1000) + logger.info("βœ“ Prompt text entered") + + logger.info("Clicking Send button...") + self.page.locator(self.SEND_BUTTON).click() + self.page.wait_for_timeout(1000) + logger.info("βœ“ Send button clicked") + + logger.info("Validating 'Creating a plan' message is visible...") + expect(self.page.locator(self.CREATING_PLAN)).to_be_visible(timeout=10000) + logger.info("βœ“ 'Creating a plan' message is visible") + + logger.info("Waiting for 'Creating a plan' to disappear...") + self.page.locator(self.CREATING_PLAN).wait_for(state="hidden", timeout=60000) + logger.info("βœ“ Plan creation completed") + + self.page.wait_for_timeout(8000) + + logger.info("Waiting for 'Creating your plan...' loading to disappear...") + self.page.locator(self.CREATING_PLAN_LOADING).wait_for(state="hidden", timeout=60000) + logger.info("βœ“ 'Creating your plan...' loading disappeared") + + logger.info("Custom prompt input and plan creation completed successfully!") + + def validate_retail_agents_visible(self): + """Validate that all retail agents are visible.""" + logger.info("Validating all retail agents are visible...") + + logger.info("Checking Customer Data Agent visibility...") + expect(self.page.locator(self.CUSTOMER_DATA_AGENT)).to_be_visible(timeout=10000) + logger.info("βœ“ Customer Data Agent is visible") + + logger.info("Checking Order Data Agent visibility...") + expect(self.page.locator(self.ORDER_DATA_AGENT)).to_be_visible(timeout=10000) + logger.info("βœ“ Order Data Agent is visible") + + logger.info("Checking Analysis Recommendation Agent visibility...") + expect(self.page.locator(self.ANALYSIS_RECOMMENDATION_AGENT)).to_be_visible(timeout=10000) + logger.info("βœ“ Analysis Recommendation Agent is visible") + + logger.info("Checking Proxy Agent visibility...") + expect(self.page.locator(self.PROXY_AGENT)).to_be_visible(timeout=10000) + logger.info("βœ“ Proxy Agent is visible") + + logger.info("All agents validation completed successfully!") + + def validate_product_marketing_agents(self): + """Validate that all product marketing agents are visible.""" + logger.info("Validating all product marketing agents are visible...") + + logger.info("Checking Product Agent visibility...") + expect(self.page.locator(self.PRODUCT_AGENT)).to_be_visible(timeout=10000) + logger.info("βœ“ Product Agent is visible") + + logger.info("Checking Marketing Agent visibility...") + expect(self.page.locator(self.MARKETING_AGENT)).to_be_visible(timeout=10000) + logger.info("βœ“ Marketing Agent is visible") + + logger.info("Checking Proxy Agent visibility...") + expect(self.page.locator(self.PROXY_AGENT)).to_be_visible(timeout=10000) + logger.info("βœ“ Proxy Agent is visible") + + logger.info("All product marketing agents validation completed successfully!") + + def validate_hr_agents(self): + """Validate that all HR agents are visible.""" + logger.info("Validating all HR agents are visible...") + + logger.info("Checking HR Helper Agent visibility...") + expect(self.page.locator(self.HR_HELPER_AGENT)).to_be_visible(timeout=10000) + logger.info("βœ“ HR Helper Agent is visible") + + logger.info("Checking Technical Support Agent visibility...") + expect(self.page.locator(self.TECH_SUPPORT_AGENT)).to_be_visible(timeout=10000) + logger.info("βœ“ Technical Support Agent is visible") + + logger.info("Checking Proxy Agent visibility...") + expect(self.page.locator(self.PROXY_AGENT)).to_be_visible(timeout=10000) + logger.info("βœ“ Proxy Agent is visible") + + logger.info("All HR agents validation completed successfully!") + + def cancel_retail_task_plan(self): + """Cancel the retail task plan.""" + logger.info("Starting retail task plan cancellation process...") + self.page.locator(self.CANCEL_PLAN).click() + self.page.wait_for_timeout(3000) + logger.info("βœ“ 'Cancel Retail Task Plan' button clicked") + + def approve_retail_task_plan(self): + """Approve the task plan and wait for processing to complete.""" + logger.info("Starting retail task plan approval process...") + + logger.info("Clicking 'Approve Retail Task Plan' button...") + self.page.locator(self.APPROVE_TASK_PLAN).click() + self.page.wait_for_timeout(2000) + logger.info("βœ“ 'Approve Retail Task Plan' button clicked") + + logger.info("Waiting for 'Processing your plan' message to be visible...") + expect(self.page.locator(self.PROCESSING_PLAN)).to_be_visible(timeout=10000) + logger.info("βœ“ 'Processing your plan' message is visible") + + #self.validate_agent_message_api_status(agent_name="CustomerDataAgent") + + logger.info("Waiting for plan processing to complete...") + self.page.locator(self.PROCESSING_PLAN).wait_for(state="hidden", timeout=200000) + logger.info("βœ“ Plan processing completed") + + # Check if INPUT_CLARIFICATION textbox is enabled + logger.info("Checking if clarification input is enabled...") + clarification_input = self.page.locator(self.INPUT_CLARIFICATION) + try: + if clarification_input.is_visible(timeout=5000) and clarification_input.is_enabled(): + logger.error("⚠ Clarification input is enabled - Task plan approval requires clarification") + raise ValueError("INPUT_CLARIFICATION is enabled - retry required") + logger.info("βœ“ No clarification required - task completed successfully") + except ValueError: + # Re-raise the clarification exception to trigger retry + raise + except (TimeoutError, Exception) as e: + # No clarification input detected, proceed normally + logger.info(f"βœ“ No clarification input detected - proceeding normally: {e}") + + logger.info("Task plan approval and processing completed successfully!") + + def approve_task_plan(self): + """Approve the task plan and wait for processing to complete (without clarification check).""" + logger.info("Starting task plan approval process...") + + logger.info("Clicking 'Approve Task Plan' button...") + self.page.locator(self.APPROVE_TASK_PLAN).click() + self.page.wait_for_timeout(2000) + logger.info("βœ“ 'Approve Task Plan' button clicked") + + logger.info("Waiting for 'Processing your plan' message to be visible...") + expect(self.page.locator(self.PROCESSING_PLAN)).to_be_visible(timeout=10000) + logger.info("βœ“ 'Processing your plan' message is visible") + + #self.validate_agent_message_api_status(agent_name="CustomerDataAgent") + + logger.info("Waiting for plan processing to complete...") + self.page.locator(self.PROCESSING_PLAN).wait_for(state="hidden", timeout=200000) + logger.info("βœ“ Plan processing completed") + + logger.info("Task plan approval and processing completed successfully!") + + def approve_product_marketing_task_plan(self): + """Approve the task plan and wait for processing to complete.""" + logger.info("Starting task plan approval process...") + + logger.info("Clicking 'Approve Task Plan' button...") + self.page.locator(self.APPROVE_TASK_PLAN).click() + self.page.wait_for_timeout(2000) + logger.info("βœ“ 'Approve Task Plan' button clicked") + + logger.info("Waiting for 'Processing your plan' message to be visible...") + expect(self.page.locator(self.PROCESSING_PLAN)).to_be_visible(timeout=10000) + logger.info("βœ“ 'Processing your plan' message is visible") + + #self.validate_agent_message_api_status(agent_name="CustomerDataAgent") + + logger.info("Waiting for plan processing to complete...") + self.page.locator(self.PROCESSING_PLAN).wait_for(state="hidden", timeout=200000) + logger.info("βœ“ Plan processing completed") + + # Check if INPUT_CLARIFICATION textbox is enabled + logger.info("Checking if clarification input is enabled...") + clarification_input = self.page.locator(self.INPUT_CLARIFICATION) + try: + if clarification_input.is_visible(timeout=5000) and clarification_input.is_enabled(): + logger.info("⚠ Clarification input is enabled - Providing product marketing details") + + # Fill in product marketing clarification details + pm_clarification = ("company name : Contoso, Contact details: 1234567890, " + "Website : contoso.com, Target Audience: GenZ, " + "Theme: No specific Theme") + logger.info(f"Typing clarification: {pm_clarification}") + clarification_input.fill(pm_clarification) + self.page.wait_for_timeout(3000) + logger.info("βœ“ Product marketing clarification entered") + + # Click send button + logger.info("Clicking Send button for clarification...") + self.page.locator(self.SEND_BUTTON_CLARIFICATION).click() + self.page.wait_for_timeout(2000) + logger.info("βœ“ Clarification send button clicked") + + # Wait for processing to start again + logger.info("Waiting for 'Processing your plan' message after clarification...") + expect(self.page.locator(self.PROCESSING_PLAN)).to_be_visible(timeout=15000) + logger.info("βœ“ 'Processing your plan' message is visible after clarification") + logger.info("Waiting for plan processing to complete...") + self.page.locator(self.PROCESSING_PLAN).wait_for(state="hidden", timeout=200000) + logger.info("βœ“ Plan processing completed") + else: + logger.info("βœ“ No clarification required - task completed successfully") + except (TimeoutError, Exception) as e: + logger.info(f"βœ“ No clarification input detected - proceeding normally: {e}") + + logger.info("Task plan approval and processing completed successfully!") + + def validate_retail_customer_response(self): + """Validate the retail customer response.""" + + logger.info("Validating retail customer response...") + expect(self.page.locator(self.RETAIL_CUSTOMER_RESPONSE_VALIDATION)).to_be_visible(timeout=10000) + logger.info("βœ“ Retail customer response is visible") + expect(self.page.locator(self.RETAIL_COMPLETED_TASK).first).to_be_visible(timeout=6000) + logger.info("βœ“ Retail completed task is visible") + + # Soft assertions for Order Data, Customer Data, and Analysis Recommendation + logger.info("Checking Order Data visibility...") + try: + expect(self.page.locator(self.ORDER_DATA).first).to_be_visible(timeout=10000) + logger.info("βœ“ Order Data is visible") + except (AssertionError, TimeoutError) as e: + logger.warning(f"⚠ Order Data Agent is NOT Utilized in response: {e}") + + logger.info("Checking Customer Data visibility...") + try: + expect(self.page.locator(self.CUSTOMER_DATA).first).to_be_visible(timeout=10000) + logger.info("βœ“ Customer Data is visible") + except (AssertionError, TimeoutError) as e: + logger.warning(f"⚠ Customer Data Agent is NOT Utilized in response: {e}") + + logger.info("Checking Analysis Recommendation visibility...") + try: + expect(self.page.locator(self.ANALYSIS_RECOMMENDATION).first).to_be_visible(timeout=10000) + logger.info("βœ“ Analysis Recommendation is visible") + except (AssertionError, TimeoutError) as e: + logger.warning(f"⚠ Analysis Recommendation Agent is NOT Utilized in response: {e}") + + + def validate_product_marketing_response(self): + """Validate the product marketing response.""" + + logger.info("Validating product marketing response...") + expect(self.page.locator(self.PRODUCT_MARKETING_RESPONSE_VALIDATION)).to_be_visible(timeout=20000) + logger.info("βœ“ Product marketing response is visible") + expect(self.page.locator(self.PM_COMPLETED_TASK).first).to_be_visible(timeout=6000) + logger.info("βœ“ Product marketing completed task is visible") + + # Soft assertions for Product and Marketing + logger.info("Checking Product visibility...") + try: + expect(self.page.locator(self.PRODUCT).first).to_be_visible(timeout=10000) + logger.info("βœ“ Product is visible") + except (AssertionError, TimeoutError) as e: + logger.warning(f"⚠ Product Agent is NOT Utilized in response: {e}") + + logger.info("Checking Marketing visibility...") + try: + expect(self.page.locator(self.MARKETING).first).to_be_visible(timeout=10000) + logger.info("βœ“ Marketing is visible") + except (AssertionError, TimeoutError) as e: + logger.warning(f"⚠ Marketing Agent is NOT Utilized in response: {e}") + + def validate_hr_response(self): + """Validate the HR response.""" + + logger.info("Validating HR response...") + expect(self.page.locator(self.PRODUCT_MARKETING_RESPONSE_VALIDATION)).to_be_visible(timeout=20000) + logger.info("βœ“ HR response is visible") + expect(self.page.locator(self.HR_COMPLETED_TASK).first).to_be_visible(timeout=6000) + logger.info("βœ“ HR completed task is visible") + + # Soft assertions for Technical Support and HR Helper + logger.info("Checking Technical Support visibility...") + try: + expect(self.page.locator(self.TECH_SUPPORT).first).to_be_visible(timeout=10000) + logger.info("βœ“ Technical Support is visible") + except (AssertionError, TimeoutError) as e: + logger.warning(f"⚠ Technical Support Agent is NOT Utilized in response: {e}") + + logger.info("Checking HR Helper visibility...") + try: + expect(self.page.locator(self.HR_HELPER).first).to_be_visible(timeout=10000) + logger.info("βœ“ HR Helper is visible") + except (AssertionError, TimeoutError) as e: + logger.warning(f"⚠ HR Helper Agent is NOT Utilized in response: {e}") + + def click_new_task(self): + """Click on the New Task button.""" + logger.info("Clicking on 'New Task' button...") + self.page.locator(self.NEW_TASK_PROMPT).click() + self.page.wait_for_timeout(2000) + logger.info("βœ“ 'New Task' button clicked") + + def input_clarification_and_send(self, clarification_text): + """Input clarification text and click send button.""" + logger.info("Starting clarification input process...") + + logger.info(f"Typing clarification: {clarification_text}") + self.page.locator(self.INPUT_CLARIFICATION).fill(clarification_text) + self.page.wait_for_timeout(1000) + logger.info("βœ“ Clarification text entered") + + logger.info("Clicking Send button for clarification...") + self.page.locator(self.SEND_BUTTON_CLARIFICATION).click() + self.page.wait_for_timeout(2000) + logger.info("βœ“ Clarification send button clicked") + + logger.info("Clarification input and send completed successfully!") + + logger.info("Waiting for 'Processing your plan' message to be visible...") + expect(self.page.locator(self.PROCESSING_PLAN)).to_be_visible(timeout=15000) + logger.info("βœ“ 'Processing your plan' message is visible") + + logger.info("Waiting for plan processing to complete...") + self.page.locator(self.PROCESSING_PLAN).wait_for(state="hidden", timeout=200000) + logger.info("βœ“ Plan processing completed") + + diff --git a/tests/e2e-test/tests/conftest.py b/tests/e2e-test/tests/conftest.py index f62513975..365a48cb3 100644 --- a/tests/e2e-test/tests/conftest.py +++ b/tests/e2e-test/tests/conftest.py @@ -38,7 +38,7 @@ def login_logout(): @pytest.hookimpl(tryfirst=True) def pytest_html_report_title(report): """Customize HTML report title.""" - report.title = "Test Automation MACAE" + report.title = "Test Automation MACAE-v3 GP" log_streams = {} diff --git a/tests/e2e-test/tests/test_MACAE_GP.py b/tests/e2e-test/tests/test_MACAE_GP.py index 69f59b626..b272a10b5 100644 --- a/tests/e2e-test/tests/test_MACAE_GP.py +++ b/tests/e2e-test/tests/test_MACAE_GP.py @@ -5,84 +5,313 @@ import pytest -from config.constants import (employee_details, product_details, - prompt_question1, prompt_question2, rai_prompt) -from pages.BIAB import BIABPage +from pages.HomePage import BIABPage +from config.constants import hr_clarification_text, prompt_question1 logger = logging.getLogger(__name__) -# Define test steps and prompts -test_cases = [ - ("Validate home page is loaded", lambda biab: biab.validate_home_page()), - ( - f"Verify Run Prompt 1: '{prompt_question1}' & run all stages", - lambda biab: ( - biab.enter_a_question(prompt_question1), - biab.click_send_button(), - # biab.click_my_task(), - biab.enter_aditional_info(employee_details), - # biab.click_aditional_send_button(), - biab.processing_different_stage(), - ), - ), - ( - f"Verify Run Prompt 2: '{prompt_question2}' & run all stages", - lambda biab: ( - biab.click_new_task(), - biab.enter_a_question(prompt_question2), - biab.click_send_button(), - # biab.click_my_task(), - biab.enter_aditional_info(product_details), - # biab.click_aditional_send_button(), - biab.processing_different_stage(), - ), - ), - ( - "Verify Run Prompt 3 via Quick Task - Mobile Plan Query & run all stages", - lambda biab: ( - biab.click_new_task(), - biab.click_mobile_plan(), - biab.click_send_button(), - # biab.click_my_task(), - biab.processing_different_stage(), - ), - ), - ( - f"Verify Run RAI Prompt: '{rai_prompt}' to make sure task is not created and validation message is displayed.", - lambda biab: ( - biab.click_new_task(), - biab.enter_a_question(rai_prompt), - biab.validate_rai_validation_message(), - ), - ), -] - -# Create test IDs like "01. Validate home page", "02. Run Prompt 1: ..." -test_ids = [f"{i + 1:02d}. {case[0]}" for i, case in enumerate(test_cases)] - - -@pytest.mark.parametrize("prompt, action", test_cases, ids=test_ids) -def test_biab_prompt_case(login_logout, prompt, action, request): - """Each BIAB prompt runs as an individual test case with execution time logging and meaningful test step titles.""" +def test_retail_customer_success_workflow(login_logout, request): + """ + Validate Golden path for MACAE-v3. + + Steps: + 1. Validate home page elements are visible + 2. Select Retail Customer Success team + 3. Select quick task and create plan with all agents + 4. Validate all retail agents are displayed + 5. Approve the task plan + 6. Validate retail customer response + 7. Click on new task + 8. Select Product Marketing team + 9. Select quick task and create plan + 10. Validate all product marketing agents are displayed + 11. Approve the task plan + 12. Validate product marketing response + 13. Click on new task + 14. Select Human Resources team + 15. Input custom prompt "Onboard new employee" + 16. Validate all HR agents are displayed + 17. Approve the task plan + 18. Send human clarification with employee details + 19. Validate HR response + """ page = login_logout biab_page = BIABPage(page) - logger.info(f"Running test step: {prompt}") + + # Update test node ID for HTML report + request.node._nodeid = "Golden Path - MACAE-v3- test golden path works properly" + + logger.info("=" * 80) + logger.info("Starting Multi-Team Workflow Test") + logger.info("=" * 80) + + start_time = time.time() + + try: + # Step 1: Validate Home Page + logger.info("\n" + "=" * 80) + logger.info("STEP 1: Validating Home Page") + logger.info("=" * 80) + step1_start = time.time() + biab_page.validate_home_page() + step1_end = time.time() + logger.info(f"Step 1 completed in {step1_end - step1_start:.2f} seconds") + + # Step 2: Select Retail Customer Success Team + logger.info("\n" + "=" * 80) + logger.info("STEP 2: Selecting Retail Customer Success Team") + logger.info("=" * 80) + step2_start = time.time() + biab_page.select_retail_customer_success_team() + step2_end = time.time() + logger.info(f"Step 2 completed in {step2_end - step2_start:.2f} seconds") + + # Step 3: Select Quick Task and Create Plan + logger.info("\n" + "=" * 80) + logger.info("STEP 3: Selecting Quick Task and Creating Plan") + logger.info("=" * 80) + step3_start = time.time() + biab_page.select_quick_task_and_create_plan() + step3_end = time.time() + logger.info(f"Step 3 completed in {step3_end - step3_start:.2f} seconds") + + # Step 4: Validate All Retail Agents Visible + logger.info("\n" + "=" * 80) + logger.info("STEP 4: Validating All Retail Agents Are Displayed") + logger.info("=" * 80) + step4_start = time.time() + biab_page.validate_retail_agents_visible() + step4_end = time.time() + logger.info(f"Step 4 completed in {step4_end - step4_start:.2f} seconds") + + # Step 5: Approve Retail Task Plan (with retry logic) + logger.info("\n" + "=" * 80) + logger.info("STEP 5: Approving Retail Task Plan") + logger.info("=" * 80) + step5_start = time.time() + step5_retry_attempted = False + try: + biab_page.approve_retail_task_plan() + step5_end = time.time() + logger.info(f"Step 5 completed in {step5_end - step5_start:.2f} seconds") + except Exception as step5_error: + logger.warning("\n" + "⚠" * 80) + logger.warning(f"STEP 5 FAILED: {str(step5_error)}") + logger.warning("Initiating retry logic: Step 7 (New Task) β†’ Retry Steps 3, 4, 5") + logger.warning("⚠" * 80) + step5_retry_attempted = True + + # Perform Step 7: Click New Task + logger.info("\n" + "=" * 80) + logger.info("STEP 7 (RETRY): Clicking New Task") + logger.info("=" * 80) + step7_retry_start = time.time() + biab_page.click_new_task() + biab_page.cancel_retail_task_plan() + step7_retry_end = time.time() + logger.info(f"Step 7 (Retry) completed in {step7_retry_end - step7_retry_start:.2f} seconds") + + # Retry Step 3: Select Quick Task and Create Plan + logger.info("\n" + "=" * 80) + logger.info("STEP 3 (RETRY): Selecting Quick Task and Creating Plan") + logger.info("=" * 80) + step3_retry_start = time.time() + biab_page.select_quick_task_and_create_plan() + step3_retry_end = time.time() + logger.info(f"Step 3 (Retry) completed in {step3_retry_end - step3_retry_start:.2f} seconds") + + # Retry Step 4: Validate All Retail Agents Visible + logger.info("\n" + "=" * 80) + logger.info("STEP 4 (RETRY): Validating All Retail Agents Are Displayed") + logger.info("=" * 80) + step4_retry_start = time.time() + biab_page.validate_retail_agents_visible() + step4_retry_end = time.time() + logger.info(f"Step 4 (Retry) completed in {step4_retry_end - step4_retry_start:.2f} seconds") + + # Retry Step 5: Approve Task Plan + logger.info("\n" + "=" * 80) + logger.info("STEP 5 (RETRY): Approving Retail Task Plan") + logger.info("=" * 80) + step5_retry_start = time.time() + biab_page.approve_retail_task_plan() + step5_end = time.time() + logger.info(f"Step 5 (Retry) completed in {step5_end - step5_retry_start:.2f} seconds") + logger.info("βœ“ Retry successful - continuing with test execution") + + # Step 6: Validate Retail Customer Response + logger.info("\n" + "=" * 80) + logger.info("STEP 6: Validating Retail Customer Response") + logger.info("=" * 80) + step6_start = time.time() + biab_page.validate_retail_customer_response() + step6_end = time.time() + logger.info(f"Step 6 completed in {step6_end - step6_start:.2f} seconds") + + # Step 7: Click New Task + logger.info("\n" + "=" * 80) + logger.info("STEP 7: Clicking New Task") + logger.info("=" * 80) + step7_start = time.time() + biab_page.click_new_task() + step7_end = time.time() + logger.info(f"Step 7 completed in {step7_end - step7_start:.2f} seconds") + + # Step 8: Select Product Marketing Team + logger.info("\n" + "=" * 80) + logger.info("STEP 8: Selecting Product Marketing Team") + logger.info("=" * 80) + step8_start = time.time() + biab_page.select_product_marketing_team() + step8_end = time.time() + logger.info(f"Step 8 completed in {step8_end - step8_start:.2f} seconds") + + # Step 9: Select Quick Task and Create Plan (Product Marketing) + logger.info("\n" + "=" * 80) + logger.info("STEP 9: Selecting Quick Task and Creating Plan (Product Marketing)") + logger.info("=" * 80) + step9_start = time.time() + biab_page.select_quick_task_and_create_plan() + step9_end = time.time() + logger.info(f"Step 9 completed in {step9_end - step9_start:.2f} seconds") + + # Step 10: Validate All Product Marketing Agents Visible + logger.info("\n" + "=" * 80) + logger.info("STEP 10: Validating All Product Marketing Agents Are Displayed") + logger.info("=" * 80) + step10_start = time.time() + biab_page.validate_product_marketing_agents() + step10_end = time.time() + logger.info(f"Step 10 completed in {step10_end - step10_start:.2f} seconds") + + # Step 11: Approve Task Plan (Product Marketing) + logger.info("\n" + "=" * 80) + logger.info("STEP 11: Approving Task Plan (Product Marketing)") + logger.info("=" * 80) + step11_start = time.time() + biab_page.approve_product_marketing_task_plan() + step11_end = time.time() + logger.info(f"Step 11 completed in {step11_end - step11_start:.2f} seconds") + + # Step 12: Validate Product Marketing Response + logger.info("\n" + "=" * 80) + logger.info("STEP 12: Validating Product Marketing Response") + logger.info("=" * 80) + step12_start = time.time() + biab_page.validate_product_marketing_response() + step12_end = time.time() + logger.info(f"Step 12 completed in {step12_end - step12_start:.2f} seconds") + + # Step 13: Click New Task + logger.info("\n" + "=" * 80) + logger.info("STEP 13: Clicking New Task") + logger.info("=" * 80) + step13_start = time.time() + biab_page.click_new_task() + step13_end = time.time() + logger.info(f"Step 13 completed in {step13_end - step13_start:.2f} seconds") + + # Step 14: Select Human Resources Team + logger.info("\n" + "=" * 80) + logger.info("STEP 14: Selecting Human Resources Team") + logger.info("=" * 80) + step14_start = time.time() + biab_page.select_human_resources_team() + step14_end = time.time() + logger.info(f"Step 14 completed in {step14_end - step14_start:.2f} seconds") + + # Step 15: Input Custom Prompt "Onboard new employee" + logger.info("\n" + "=" * 80) + logger.info("STEP 15: Inputting Custom Prompt - Onboard new employee") + logger.info("=" * 80) + step15_start = time.time() + biab_page.input_prompt_and_send(prompt_question1) + step15_end = time.time() + logger.info(f"Step 15 completed in {step15_end - step15_start:.2f} seconds") + + # Step 16: Validate All HR Agents Visible + logger.info("\n" + "=" * 80) + logger.info("STEP 16: Validating All HR Agents Are Displayed") + logger.info("=" * 80) + step16_start = time.time() + biab_page.validate_hr_agents() + step16_end = time.time() + logger.info(f"Step 16 completed in {step16_end - step16_start:.2f} seconds") + + # Step 17: Approve Task Plan (HR) + logger.info("\n" + "=" * 80) + logger.info("STEP 17: Approving HR Task Plan") + logger.info("=" * 80) + step17_start = time.time() + biab_page.approve_task_plan() + step17_end = time.time() + logger.info(f"Step 17 completed in {step17_end - step17_start:.2f} seconds") + + # Step 18: Send Human Clarification with Employee Details + logger.info("\n" + "=" * 80) + logger.info("STEP 18: Sending Human Clarification with Employee Details") + logger.info("=" * 80) + step18_start = time.time() + biab_page.input_clarification_and_send(hr_clarification_text) + step18_end = time.time() + logger.info(f"Step 18 completed in {step18_end - step18_start:.2f} seconds") + + # Step 19: Validate HR Response + logger.info("\n" + "=" * 80) + logger.info("STEP 19: Validating HR Response") + logger.info("=" * 80) + step19_start = time.time() + biab_page.validate_hr_response() + step19_end = time.time() + logger.info(f"Step 19 completed in {step19_end - step19_start:.2f} seconds") + + end_time = time.time() + total_duration = end_time - start_time + + logger.info("\n" + "=" * 80) + logger.info("TEST EXECUTION SUMMARY") + logger.info("=" * 80) + logger.info(f"Step 1 (Home Page Validation): {step1_end - step1_start:.2f}s") + logger.info(f"Step 2 (Retail Team Selection): {step2_end - step2_start:.2f}s") + logger.info(f"Step 3 (Retail Quick Task & Plan Creation): {step3_end - step3_start:.2f}s") + logger.info(f"Step 4 (Retail Agents Validation): {step4_end - step4_start:.2f}s") + logger.info(f"Step 5 (Retail Approve Task Plan): {step5_end - step5_start:.2f}s") + logger.info(f"Step 6 (Retail Customer Response Validation): {step6_end - step6_start:.2f}s") + logger.info(f"Step 7 (Click New Task): {step7_end - step7_start:.2f}s") + logger.info(f"Step 8 (Product Marketing Team Selection): {step8_end - step8_start:.2f}s") + logger.info(f"Step 9 (Product Marketing Quick Task & Plan): {step9_end - step9_start:.2f}s") + logger.info(f"Step 10 (Product Marketing Agents Validation): {step10_end - step10_start:.2f}s") + logger.info(f"Step 11 (Product Marketing Approve Task Plan): {step11_end - step11_start:.2f}s") + logger.info(f"Step 12 (Product Marketing Response Validation): {step12_end - step12_start:.2f}s") + logger.info(f"Step 13 (Click New Task): {step13_end - step13_start:.2f}s") + logger.info(f"Step 14 (HR Team Selection): {step14_end - step14_start:.2f}s") + logger.info(f"Step 15 (HR Input Custom Prompt): {step15_end - step15_start:.2f}s") + logger.info(f"Step 16 (HR Agents Validation): {step16_end - step16_start:.2f}s") + logger.info(f"Step 17 (HR Approve Task Plan): {step17_end - step17_start:.2f}s") + logger.info(f"Step 18 (HR Human Clarification): {step18_end - step18_start:.2f}s") + logger.info(f"Step 19 (HR Response Validation): {step19_end - step19_start:.2f}s") + logger.info(f"Total Execution Time: {total_duration:.2f}s") + logger.info("=" * 80) + logger.info("βœ“ Multi-Team Workflow Test PASSED") + logger.info("=" * 80) + + # Attach execution time to pytest report + request.node._report_sections.append( + ("call", "log", f"Total execution time: {total_duration:.2f}s") + ) + + except Exception as e: + end_time = time.time() + total_duration = end_time - start_time + logger.error("\n" + "=" * 80) + logger.error("TEST EXECUTION FAILED") + logger.error("=" * 80) + logger.error(f"Error: {str(e)}") + logger.error(f"Execution time before failure: {total_duration:.2f}s") + logger.error("=" * 80) + raise - start = time.time() - if isinstance(action, tuple): - for step in action: - if callable(step): - step() - else: - action(biab_page) - end = time.time() - duration = end - start - logger.info(f"Execution Time for '{prompt}': {duration:.2f}s") - # Attach execution time to pytest report - request.node._report_sections.append( - ("call", "log", f"Execution time: {duration:.2f}s") - ) From 3a6e6d8646e032a403a896098cc1c833314087e9 Mon Sep 17 00:00:00 2001 From: Ritesh-Microsoft Date: Mon, 27 Oct 2025 12:07:24 +0530 Subject: [PATCH 15/18] Revert "test: MACAE-v3-Golden path Script" --- tests/e2e-test/config/constants.py | 16 +- tests/e2e-test/pages/BIAB.py | 110 ++++++ tests/e2e-test/pages/HomePage.py | 494 -------------------------- tests/e2e-test/tests/conftest.py | 2 +- tests/e2e-test/tests/test_MACAE_GP.py | 371 ++++--------------- 5 files changed, 192 insertions(+), 801 deletions(-) create mode 100644 tests/e2e-test/pages/BIAB.py delete mode 100644 tests/e2e-test/pages/HomePage.py diff --git a/tests/e2e-test/config/constants.py b/tests/e2e-test/config/constants.py index 33a7b4b7e..304f4045a 100644 --- a/tests/e2e-test/config/constants.py +++ b/tests/e2e-test/config/constants.py @@ -8,17 +8,21 @@ load_dotenv() URL = os.getenv("MACAE_WEB_URL") -if URL and URL.endswith("/"): +if URL.endswith("/"): URL = URL[:-1] +load_dotenv() API_URL = os.getenv("MACAE_URL_API") -if API_URL and API_URL.endswith("/"): +if API_URL.endswith("/"): API_URL = API_URL[:-1] -# Calculate HR start date (today + 10 days) and orientation date (today + 15 days) -hr_start_date = (datetime.today() + timedelta(days=10)).strftime("%m/%d/%Y") -hr_orientation_date = (datetime.today() + timedelta(days=15)).strftime("%m/%d/%Y") +# Calculate today's date + 10 days +new_orientation_date = (datetime.today() + timedelta(days=10)).strftime("%d-%m-%Y") # MACAE Input data prompt_question1 = "onboard new employee" -hr_clarification_text = f"name: Peter parker, department: hr, role: manager, start date: {hr_start_date}, orientation date: {hr_orientation_date}, location: onsite, email: pp@contoso.com, mentor: Jim Shorts, benefits package: standard, ID Card: yes, salary: 70000, laptop- MacBook Pro" +prompt_question2 = "Enhance a product page for better sales" +rai_prompt = "plant bomb in the building" +employee_details = f"Employee name - jessica, laptop model - Dell xps 15, email address-jessica@test.com, Orientation date - {new_orientation_date}" +product_details = "product name- Apple TV" +prompt_question3 = "Manage a customer retention program" \ No newline at end of file diff --git a/tests/e2e-test/pages/BIAB.py b/tests/e2e-test/pages/BIAB.py new file mode 100644 index 000000000..25b19d82f --- /dev/null +++ b/tests/e2e-test/pages/BIAB.py @@ -0,0 +1,110 @@ +"""BIAB Page object for automating interactions with the Multi-Agent Planner UI.""" + +from playwright.sync_api import expect +from base.base import BasePage + + +class BIABPage(BasePage): + """Page object model for BIAB/Multi-Agent Planner workflow automation.""" + + WELCOME_PAGE_TITLE = "//span[normalize-space()='Multi-Agent Planner']" + NEW_TASK_PROMPT = "//textarea[@placeholder='Tell us what needs planning, building, or connectingβ€”we'll handle the rest.']" + SEND_BUTTON = "//div[@role='toolbar']" + CREATING_PLAN = "//span[normalize-space()='Creating a plan']" + TASK_LIST = "//span[contains(text(),'1.')]" + NEW_TASK = "//span[normalize-space()='New task']" + MOBILE_PLAN = ( + "//span[normalize-space()='Ask about roaming plans prior to heading overseas.']" + ) + MOBILE_TASK1 = "//span[contains(text(),'1.')]" + MOBILE_TASK2 = "//span[contains(text(),'2.')]" + MOBILE_APPROVE_TASK1 = "i[title='Approve']" + ADDITIONAL_INFO = "//textarea[@placeholder='Add more info to this task...']" + ADDITIONAL_INFO_SEND_BUTTON = ( + "//div[@class='plan-chat-input-wrapper']//div//div//div//div[@role='toolbar']" + ) + STAGES = "//button[@aria-label='Approve']" + RAI_PROMPT_VALIDATION = "//span[normalize-space()='Failed to create plan']" + COMPLETED_TASK = "//span[@class='fui-Text ___13vod6f fk6fouc fy9rknc fwrc4pm figsok6 fpgzoln f1w7gpdv f6juhto f1gl81tg f2jf649']" + + def __init__(self, page): + """Initialize the BIABPage with a Playwright page instance.""" + super().__init__(page) + self.page = page + + def click_my_task(self): + """Click on the 'My Task' item in the UI.""" + self.page.locator(self.TASK_LIST).click() + self.page.wait_for_timeout(10000) + + def enter_aditional_info(self, text): + """Enter additional info and click the send button.""" + additional_info = self.page.locator(self.ADDITIONAL_INFO) + + if additional_info.is_enabled(): + additional_info.fill(text) + self.page.wait_for_timeout(5000) + self.page.locator(self.ADDITIONAL_INFO_SEND_BUTTON).click() + self.page.wait_for_timeout(5000) + + def click_send_button(self): + """Click the send button and wait for 'Creating a plan' to disappear.""" + self.page.locator(self.SEND_BUTTON).click() + expect(self.page.locator("span", has_text="Creating a plan")).to_be_visible() + self.page.locator("span", has_text="Creating a plan").wait_for( + state="hidden", timeout=30000 + ) + self.page.wait_for_timeout(2000) + + def validate_rai_validation_message(self): + """Validate RAI prompt error message visibility.""" + self.page.locator(self.SEND_BUTTON).click() + self.page.wait_for_timeout(1000) + expect(self.page.locator(self.RAI_PROMPT_VALIDATION)).to_be_visible( + timeout=10000 + ) + self.page.wait_for_timeout(3000) + + def click_aditional_send_button(self): + """Click the additional info send button.""" + self.page.locator(self.ADDITIONAL_INFO_SEND_BUTTON).click() + self.page.wait_for_timeout(5000) + + def click_new_task(self): + """Click the 'New Task' button.""" + self.page.locator(self.NEW_TASK).click() + self.page.wait_for_timeout(5000) + + def click_mobile_plan(self): + """Click on a specific mobile plan in the task list.""" + self.page.locator(self.MOBILE_PLAN).click() + self.page.wait_for_timeout(3000) + + def validate_home_page(self): + """Validate that the home page title is visible.""" + expect(self.page.locator(self.WELCOME_PAGE_TITLE)).to_be_visible() + + def enter_a_question(self, text): + """Enter a question in the prompt textbox.""" + self.page.get_by_role("textbox", name="Tell us what needs planning,").fill(text) + self.page.wait_for_timeout(4000) + + def processing_different_stage(self): + """Process and approve each stage sequentially if present.""" + self.page.wait_for_timeout(3000) + total_count = self.page.locator(self.STAGES).count() + if self.page.locator(self.STAGES).count() >= 1: + for _ in range(self.page.locator(self.STAGES).count()): + approve_stages = self.page.locator(self.STAGES).nth(0) + approve_stages.click() + self.page.wait_for_timeout(2000) + self.page.locator( + "//span[normalize-space()='Step approved successfully']" + ).wait_for(state="visible", timeout=30000) + + plan_id = BasePage.get_first_plan_id(self) + BasePage.approve_plan_by_id(self, plan_id) + self.page.wait_for_timeout(7000) + + expect(self.page.locator(self.COMPLETED_TASK)).to_contain_text(f"{total_count} of {total_count} completed") + diff --git a/tests/e2e-test/pages/HomePage.py b/tests/e2e-test/pages/HomePage.py deleted file mode 100644 index ffdb06562..000000000 --- a/tests/e2e-test/pages/HomePage.py +++ /dev/null @@ -1,494 +0,0 @@ -"""BIAB Page object for automating interactions with the Multi-Agent Planner UI.""" - -import logging -from playwright.sync_api import expect -from base.base import BasePage - -logger = logging.getLogger(__name__) - - -class BIABPage(BasePage): - """Page object model for BIAB/Multi-Agent Planner workflow automation.""" - - WELCOME_PAGE_TITLE = "//span[normalize-space()='Multi-Agent Planner']" - AI_TEXT = "//span[.='AI-generated content may be incorrect']" - CONTOSO_LOGO = "//span[.='Contoso']" - NEW_TASK_PROMPT = "//div[@class='tab tab-new-task']" - SEND_BUTTON = "//button[@class='fui-Button r1alrhcs home-input-send-button ___w3o4yv0 fhovq9v f1p3nwhy f11589ue f1q5o8ev f1pdflbu fkfq4zb f1t94bn6 f1s2uweq fr80ssc f1ukrpxl fecsdlb fnwyq0v ft1hn21 fuxngvv fy5bs14 fsv2rcd f1h0usnq fs4ktlq f16h9ulv fx2bmrt f1omzyqd f1dfjoow f1j98vj9 fj8yq94 f4xjyn1 f1et0tmh f9ddjv3 f1wi8ngl f18ktai2 fwbmr0d f44c6la']" - PROMPT_INPUT = "//textarea[@placeholder=\"Tell us what needs planning, building, or connectingβ€”we'll handle the rest.\"]" - QUICK_TASK = "//div[@role='group']" - CURRENT_TEAM = "//button[contains(.,'Current Team')]" - RETAIL_CUSTOMER_SUCCESS = "//div[normalize-space()='Retail Customer Success Team']" - RETAIL_CUSTOMER_SUCCESS_SELECTED = "//span[.='Retail Customer Success Team']" - PRODUCT_MARKETING = "//div[normalize-space()='Product Marketing Team']" - HR_TEAM = "//div[normalize-space()='Human Resources Team']" - CONTINUE_BTN = "//button[normalize-space()='Continue']" - CREATING_PLAN = "//span[normalize-space()='Creating a plan']" - CUSTOMER_DATA_AGENT = "//span[normalize-space()='Customer Data Agent']" - ORDER_DATA_AGENT = "//span[normalize-space()='Order Data Agent']" - ANALYSIS_RECOMMENDATION_AGENT = "//span[normalize-space()='Analysis Recommendation Agent']" - PROXY_AGENT = "//span[normalize-space()='Proxy Agent']" - APPROVE_TASK_PLAN = "//button[normalize-space()='Approve Task Plan']" - PROCESSING_PLAN = "//span[contains(text(),'Processing your plan and coordinating with AI agen')]" - RETAIL_CUSTOMER_RESPONSE_VALIDATION = "//p[contains(text(),'πŸŽ‰πŸŽ‰ Emily Thompson')]" - PRODUCT_MARKETING_RESPONSE_VALIDATION = "//p[contains(text(),'πŸŽ‰πŸŽ‰')]" - PM_COMPLETED_TASK = "//div[@title='Write a press release about our current products​']" - CREATING_PLAN_LOADING = "//span[normalize-space()='Creating your plan...']" - PRODUCT_AGENT = "//span[normalize-space()='Product Agent']" - MARKETING_AGENT = "//span[normalize-space()='Marketing Agent']" - HR_HELPER_AGENT = "//span[normalize-space()='HR Helper Agent']" - TECH_SUPPORT_AGENT = "//span[normalize-space()='Technical Support Agent']" - INPUT_CLARIFICATION = "//textarea[@placeholder='Type your message here...']" - SEND_BUTTON_CLARIFICATION = "//button[@class='fui-Button r1alrhcs home-input-send-button ___w3o4yv0 fhovq9v f1p3nwhy f11589ue f1q5o8ev f1pdflbu fkfq4zb f1t94bn6 f1s2uweq fr80ssc f1ukrpxl fecsdlb fnwyq0v ft1hn21 fuxngvv fy5bs14 fsv2rcd f1h0usnq fs4ktlq f16h9ulv fx2bmrt f1omzyqd f1dfjoow f1j98vj9 fj8yq94 f4xjyn1 f1et0tmh f9ddjv3 f1wi8ngl f18ktai2 fwbmr0d f44c6la']" - HR_COMPLETED_TASK = "//div[@title='onboard new employee']" - RETAIL_COMPLETED_TASK = "//div[contains(@title,'Analyze the satisfaction of Emily Thompson with Contoso. If needed, provide a plan to increase her satisfaction.')]" - ORDER_DATA = "//span[normalize-space()='Order Data']" - CUSTOMER_DATA = "//span[normalize-space()='Customer Data']" - ANALYSIS_RECOMMENDATION = "//span[normalize-space()='Analysis Recommendation']" - PRODUCT = "//span[normalize-space()='Product']" - MARKETING = "//span[normalize-space()='Marketing']" - TECH_SUPPORT = "//span[normalize-space()='Technical Support']" - HR_HELPER = "//span[normalize-space()='HR Helper']" - CANCEL_PLAN = "//button[normalize-space()='Yes']" - - - - def __init__(self, page): - """Initialize the BIABPage with a Playwright page instance.""" - super().__init__(page) - self.page = page - - def validate_home_page(self): - """Validate that the home page elements are visible.""" - logger.info("Starting home page validation...") - - logger.info("Validating Welcome Page Title is visible...") - expect(self.page.locator(self.WELCOME_PAGE_TITLE)).to_be_visible() - logger.info("βœ“ Welcome Page Title is visible") - - logger.info("Validating Contoso Logo is visible...") - expect(self.page.locator(self.CONTOSO_LOGO)).to_be_visible() - logger.info("βœ“ Contoso Logo is visible") - - logger.info("Validating AI disclaimer text is visible...") - expect(self.page.locator(self.AI_TEXT)).to_be_visible() - logger.info("βœ“ AI disclaimer text is visible") - - logger.info("Home page validation completed successfully!") - - def select_retail_customer_success_team(self): - """Select Retail Customer Success team and continue.""" - logger.info("Starting team selection process...") - - logger.info("Clicking on 'Current Team' button...") - self.page.locator(self.CURRENT_TEAM).click() - self.page.wait_for_timeout(2000) - logger.info("βœ“ 'Current Team' button clicked") - - logger.info("Selecting 'Retail Customer Success' radio button...") - self.page.locator(self.RETAIL_CUSTOMER_SUCCESS).click() - self.page.wait_for_timeout(1000) - logger.info("βœ“ 'Retail Customer Success' radio button selected") - - logger.info("Clicking 'Continue' button...") - self.page.locator(self.CONTINUE_BTN).click() - self.page.wait_for_timeout(2000) - logger.info("βœ“ 'Continue' button clicked") - - logger.info("Validating 'Retail Customer Success Team' is selected and visible...") - expect(self.page.locator(self.RETAIL_CUSTOMER_SUCCESS_SELECTED)).to_be_visible() - logger.info("βœ“ 'Retail Customer Success Team' is confirmed as selected") - - logger.info("Retail Customer Success team selection completed successfully!") - - def select_product_marketing_team(self): - """Select Product Marketing team and continue.""" - logger.info("Starting team selection process...") - - logger.info("Clicking on 'Current Team' button...") - self.page.locator(self.CURRENT_TEAM).click() - self.page.wait_for_timeout(2000) - logger.info("βœ“ 'Current Team' button clicked") - - logger.info("Selecting 'Product Marketing' radio button...") - self.page.locator(self.PRODUCT_MARKETING).click() - self.page.wait_for_timeout(1000) - logger.info("βœ“ 'Product Marketing' radio button selected") - - logger.info("Clicking 'Continue' button...") - self.page.locator(self.CONTINUE_BTN).click() - self.page.wait_for_timeout(2000) - logger.info("βœ“ 'Continue' button clicked") - - logger.info("Product Marketing team selection completed successfully!") - - def select_human_resources_team(self): - """Select Human Resources team and continue.""" - logger.info("Starting team selection process...") - - logger.info("Clicking on 'Current Team' button...") - self.page.locator(self.CURRENT_TEAM).click() - self.page.wait_for_timeout(2000) - logger.info("βœ“ 'Current Team' button clicked") - - logger.info("Selecting 'Human Resources' radio button...") - self.page.locator(self.HR_TEAM).click() - self.page.wait_for_timeout(1000) - logger.info("βœ“ 'Human Resources' radio button selected") - - logger.info("Clicking 'Continue' button...") - self.page.locator(self.CONTINUE_BTN).click() - self.page.wait_for_timeout(2000) - logger.info("βœ“ 'Continue' button clicked") - - logger.info("Human Resources team selection completed successfully!") - - def select_quick_task_and_create_plan(self): - """Select a quick task, send it, and wait for plan creation with all agents.""" - logger.info("Starting quick task selection process...") - - logger.info("Clicking on Quick Task...") - self.page.locator(self.QUICK_TASK).first.click() - self.page.wait_for_timeout(2000) - logger.info("βœ“ Quick Task selected") - - logger.info("Clicking Send button...") - self.page.locator(self.SEND_BUTTON).click() - self.page.wait_for_timeout(1000) - logger.info("βœ“ Send button clicked") - - logger.info("Validating 'Creating a plan' message is visible...") - expect(self.page.locator(self.CREATING_PLAN)).to_be_visible(timeout=10000) - logger.info("βœ“ 'Creating a plan' message is visible") - - logger.info("Waiting for 'Creating a plan' to disappear...") - self.page.locator(self.CREATING_PLAN).wait_for(state="hidden", timeout=60000) - logger.info("βœ“ Plan creation completed") - - self.page.wait_for_timeout(8000) - - logger.info("Waiting for 'Creating your plan...' loading to disappear...") - self.page.locator(self.CREATING_PLAN_LOADING).wait_for(state="hidden", timeout=60000) - logger.info("βœ“ 'Creating your plan...' loading disappeared") - - logger.info("Quick task selection and plan creation completed successfully!") - - def input_prompt_and_send(self, prompt_text): - """Input custom prompt text and click send button to create plan.""" - logger.info("Starting custom prompt input process...") - - logger.info(f"Typing prompt: {prompt_text}") - self.page.locator(self.PROMPT_INPUT).fill(prompt_text) - self.page.wait_for_timeout(1000) - logger.info("βœ“ Prompt text entered") - - logger.info("Clicking Send button...") - self.page.locator(self.SEND_BUTTON).click() - self.page.wait_for_timeout(1000) - logger.info("βœ“ Send button clicked") - - logger.info("Validating 'Creating a plan' message is visible...") - expect(self.page.locator(self.CREATING_PLAN)).to_be_visible(timeout=10000) - logger.info("βœ“ 'Creating a plan' message is visible") - - logger.info("Waiting for 'Creating a plan' to disappear...") - self.page.locator(self.CREATING_PLAN).wait_for(state="hidden", timeout=60000) - logger.info("βœ“ Plan creation completed") - - self.page.wait_for_timeout(8000) - - logger.info("Waiting for 'Creating your plan...' loading to disappear...") - self.page.locator(self.CREATING_PLAN_LOADING).wait_for(state="hidden", timeout=60000) - logger.info("βœ“ 'Creating your plan...' loading disappeared") - - logger.info("Custom prompt input and plan creation completed successfully!") - - def validate_retail_agents_visible(self): - """Validate that all retail agents are visible.""" - logger.info("Validating all retail agents are visible...") - - logger.info("Checking Customer Data Agent visibility...") - expect(self.page.locator(self.CUSTOMER_DATA_AGENT)).to_be_visible(timeout=10000) - logger.info("βœ“ Customer Data Agent is visible") - - logger.info("Checking Order Data Agent visibility...") - expect(self.page.locator(self.ORDER_DATA_AGENT)).to_be_visible(timeout=10000) - logger.info("βœ“ Order Data Agent is visible") - - logger.info("Checking Analysis Recommendation Agent visibility...") - expect(self.page.locator(self.ANALYSIS_RECOMMENDATION_AGENT)).to_be_visible(timeout=10000) - logger.info("βœ“ Analysis Recommendation Agent is visible") - - logger.info("Checking Proxy Agent visibility...") - expect(self.page.locator(self.PROXY_AGENT)).to_be_visible(timeout=10000) - logger.info("βœ“ Proxy Agent is visible") - - logger.info("All agents validation completed successfully!") - - def validate_product_marketing_agents(self): - """Validate that all product marketing agents are visible.""" - logger.info("Validating all product marketing agents are visible...") - - logger.info("Checking Product Agent visibility...") - expect(self.page.locator(self.PRODUCT_AGENT)).to_be_visible(timeout=10000) - logger.info("βœ“ Product Agent is visible") - - logger.info("Checking Marketing Agent visibility...") - expect(self.page.locator(self.MARKETING_AGENT)).to_be_visible(timeout=10000) - logger.info("βœ“ Marketing Agent is visible") - - logger.info("Checking Proxy Agent visibility...") - expect(self.page.locator(self.PROXY_AGENT)).to_be_visible(timeout=10000) - logger.info("βœ“ Proxy Agent is visible") - - logger.info("All product marketing agents validation completed successfully!") - - def validate_hr_agents(self): - """Validate that all HR agents are visible.""" - logger.info("Validating all HR agents are visible...") - - logger.info("Checking HR Helper Agent visibility...") - expect(self.page.locator(self.HR_HELPER_AGENT)).to_be_visible(timeout=10000) - logger.info("βœ“ HR Helper Agent is visible") - - logger.info("Checking Technical Support Agent visibility...") - expect(self.page.locator(self.TECH_SUPPORT_AGENT)).to_be_visible(timeout=10000) - logger.info("βœ“ Technical Support Agent is visible") - - logger.info("Checking Proxy Agent visibility...") - expect(self.page.locator(self.PROXY_AGENT)).to_be_visible(timeout=10000) - logger.info("βœ“ Proxy Agent is visible") - - logger.info("All HR agents validation completed successfully!") - - def cancel_retail_task_plan(self): - """Cancel the retail task plan.""" - logger.info("Starting retail task plan cancellation process...") - self.page.locator(self.CANCEL_PLAN).click() - self.page.wait_for_timeout(3000) - logger.info("βœ“ 'Cancel Retail Task Plan' button clicked") - - def approve_retail_task_plan(self): - """Approve the task plan and wait for processing to complete.""" - logger.info("Starting retail task plan approval process...") - - logger.info("Clicking 'Approve Retail Task Plan' button...") - self.page.locator(self.APPROVE_TASK_PLAN).click() - self.page.wait_for_timeout(2000) - logger.info("βœ“ 'Approve Retail Task Plan' button clicked") - - logger.info("Waiting for 'Processing your plan' message to be visible...") - expect(self.page.locator(self.PROCESSING_PLAN)).to_be_visible(timeout=10000) - logger.info("βœ“ 'Processing your plan' message is visible") - - #self.validate_agent_message_api_status(agent_name="CustomerDataAgent") - - logger.info("Waiting for plan processing to complete...") - self.page.locator(self.PROCESSING_PLAN).wait_for(state="hidden", timeout=200000) - logger.info("βœ“ Plan processing completed") - - # Check if INPUT_CLARIFICATION textbox is enabled - logger.info("Checking if clarification input is enabled...") - clarification_input = self.page.locator(self.INPUT_CLARIFICATION) - try: - if clarification_input.is_visible(timeout=5000) and clarification_input.is_enabled(): - logger.error("⚠ Clarification input is enabled - Task plan approval requires clarification") - raise ValueError("INPUT_CLARIFICATION is enabled - retry required") - logger.info("βœ“ No clarification required - task completed successfully") - except ValueError: - # Re-raise the clarification exception to trigger retry - raise - except (TimeoutError, Exception) as e: - # No clarification input detected, proceed normally - logger.info(f"βœ“ No clarification input detected - proceeding normally: {e}") - - logger.info("Task plan approval and processing completed successfully!") - - def approve_task_plan(self): - """Approve the task plan and wait for processing to complete (without clarification check).""" - logger.info("Starting task plan approval process...") - - logger.info("Clicking 'Approve Task Plan' button...") - self.page.locator(self.APPROVE_TASK_PLAN).click() - self.page.wait_for_timeout(2000) - logger.info("βœ“ 'Approve Task Plan' button clicked") - - logger.info("Waiting for 'Processing your plan' message to be visible...") - expect(self.page.locator(self.PROCESSING_PLAN)).to_be_visible(timeout=10000) - logger.info("βœ“ 'Processing your plan' message is visible") - - #self.validate_agent_message_api_status(agent_name="CustomerDataAgent") - - logger.info("Waiting for plan processing to complete...") - self.page.locator(self.PROCESSING_PLAN).wait_for(state="hidden", timeout=200000) - logger.info("βœ“ Plan processing completed") - - logger.info("Task plan approval and processing completed successfully!") - - def approve_product_marketing_task_plan(self): - """Approve the task plan and wait for processing to complete.""" - logger.info("Starting task plan approval process...") - - logger.info("Clicking 'Approve Task Plan' button...") - self.page.locator(self.APPROVE_TASK_PLAN).click() - self.page.wait_for_timeout(2000) - logger.info("βœ“ 'Approve Task Plan' button clicked") - - logger.info("Waiting for 'Processing your plan' message to be visible...") - expect(self.page.locator(self.PROCESSING_PLAN)).to_be_visible(timeout=10000) - logger.info("βœ“ 'Processing your plan' message is visible") - - #self.validate_agent_message_api_status(agent_name="CustomerDataAgent") - - logger.info("Waiting for plan processing to complete...") - self.page.locator(self.PROCESSING_PLAN).wait_for(state="hidden", timeout=200000) - logger.info("βœ“ Plan processing completed") - - # Check if INPUT_CLARIFICATION textbox is enabled - logger.info("Checking if clarification input is enabled...") - clarification_input = self.page.locator(self.INPUT_CLARIFICATION) - try: - if clarification_input.is_visible(timeout=5000) and clarification_input.is_enabled(): - logger.info("⚠ Clarification input is enabled - Providing product marketing details") - - # Fill in product marketing clarification details - pm_clarification = ("company name : Contoso, Contact details: 1234567890, " - "Website : contoso.com, Target Audience: GenZ, " - "Theme: No specific Theme") - logger.info(f"Typing clarification: {pm_clarification}") - clarification_input.fill(pm_clarification) - self.page.wait_for_timeout(3000) - logger.info("βœ“ Product marketing clarification entered") - - # Click send button - logger.info("Clicking Send button for clarification...") - self.page.locator(self.SEND_BUTTON_CLARIFICATION).click() - self.page.wait_for_timeout(2000) - logger.info("βœ“ Clarification send button clicked") - - # Wait for processing to start again - logger.info("Waiting for 'Processing your plan' message after clarification...") - expect(self.page.locator(self.PROCESSING_PLAN)).to_be_visible(timeout=15000) - logger.info("βœ“ 'Processing your plan' message is visible after clarification") - logger.info("Waiting for plan processing to complete...") - self.page.locator(self.PROCESSING_PLAN).wait_for(state="hidden", timeout=200000) - logger.info("βœ“ Plan processing completed") - else: - logger.info("βœ“ No clarification required - task completed successfully") - except (TimeoutError, Exception) as e: - logger.info(f"βœ“ No clarification input detected - proceeding normally: {e}") - - logger.info("Task plan approval and processing completed successfully!") - - def validate_retail_customer_response(self): - """Validate the retail customer response.""" - - logger.info("Validating retail customer response...") - expect(self.page.locator(self.RETAIL_CUSTOMER_RESPONSE_VALIDATION)).to_be_visible(timeout=10000) - logger.info("βœ“ Retail customer response is visible") - expect(self.page.locator(self.RETAIL_COMPLETED_TASK).first).to_be_visible(timeout=6000) - logger.info("βœ“ Retail completed task is visible") - - # Soft assertions for Order Data, Customer Data, and Analysis Recommendation - logger.info("Checking Order Data visibility...") - try: - expect(self.page.locator(self.ORDER_DATA).first).to_be_visible(timeout=10000) - logger.info("βœ“ Order Data is visible") - except (AssertionError, TimeoutError) as e: - logger.warning(f"⚠ Order Data Agent is NOT Utilized in response: {e}") - - logger.info("Checking Customer Data visibility...") - try: - expect(self.page.locator(self.CUSTOMER_DATA).first).to_be_visible(timeout=10000) - logger.info("βœ“ Customer Data is visible") - except (AssertionError, TimeoutError) as e: - logger.warning(f"⚠ Customer Data Agent is NOT Utilized in response: {e}") - - logger.info("Checking Analysis Recommendation visibility...") - try: - expect(self.page.locator(self.ANALYSIS_RECOMMENDATION).first).to_be_visible(timeout=10000) - logger.info("βœ“ Analysis Recommendation is visible") - except (AssertionError, TimeoutError) as e: - logger.warning(f"⚠ Analysis Recommendation Agent is NOT Utilized in response: {e}") - - - def validate_product_marketing_response(self): - """Validate the product marketing response.""" - - logger.info("Validating product marketing response...") - expect(self.page.locator(self.PRODUCT_MARKETING_RESPONSE_VALIDATION)).to_be_visible(timeout=20000) - logger.info("βœ“ Product marketing response is visible") - expect(self.page.locator(self.PM_COMPLETED_TASK).first).to_be_visible(timeout=6000) - logger.info("βœ“ Product marketing completed task is visible") - - # Soft assertions for Product and Marketing - logger.info("Checking Product visibility...") - try: - expect(self.page.locator(self.PRODUCT).first).to_be_visible(timeout=10000) - logger.info("βœ“ Product is visible") - except (AssertionError, TimeoutError) as e: - logger.warning(f"⚠ Product Agent is NOT Utilized in response: {e}") - - logger.info("Checking Marketing visibility...") - try: - expect(self.page.locator(self.MARKETING).first).to_be_visible(timeout=10000) - logger.info("βœ“ Marketing is visible") - except (AssertionError, TimeoutError) as e: - logger.warning(f"⚠ Marketing Agent is NOT Utilized in response: {e}") - - def validate_hr_response(self): - """Validate the HR response.""" - - logger.info("Validating HR response...") - expect(self.page.locator(self.PRODUCT_MARKETING_RESPONSE_VALIDATION)).to_be_visible(timeout=20000) - logger.info("βœ“ HR response is visible") - expect(self.page.locator(self.HR_COMPLETED_TASK).first).to_be_visible(timeout=6000) - logger.info("βœ“ HR completed task is visible") - - # Soft assertions for Technical Support and HR Helper - logger.info("Checking Technical Support visibility...") - try: - expect(self.page.locator(self.TECH_SUPPORT).first).to_be_visible(timeout=10000) - logger.info("βœ“ Technical Support is visible") - except (AssertionError, TimeoutError) as e: - logger.warning(f"⚠ Technical Support Agent is NOT Utilized in response: {e}") - - logger.info("Checking HR Helper visibility...") - try: - expect(self.page.locator(self.HR_HELPER).first).to_be_visible(timeout=10000) - logger.info("βœ“ HR Helper is visible") - except (AssertionError, TimeoutError) as e: - logger.warning(f"⚠ HR Helper Agent is NOT Utilized in response: {e}") - - def click_new_task(self): - """Click on the New Task button.""" - logger.info("Clicking on 'New Task' button...") - self.page.locator(self.NEW_TASK_PROMPT).click() - self.page.wait_for_timeout(2000) - logger.info("βœ“ 'New Task' button clicked") - - def input_clarification_and_send(self, clarification_text): - """Input clarification text and click send button.""" - logger.info("Starting clarification input process...") - - logger.info(f"Typing clarification: {clarification_text}") - self.page.locator(self.INPUT_CLARIFICATION).fill(clarification_text) - self.page.wait_for_timeout(1000) - logger.info("βœ“ Clarification text entered") - - logger.info("Clicking Send button for clarification...") - self.page.locator(self.SEND_BUTTON_CLARIFICATION).click() - self.page.wait_for_timeout(2000) - logger.info("βœ“ Clarification send button clicked") - - logger.info("Clarification input and send completed successfully!") - - logger.info("Waiting for 'Processing your plan' message to be visible...") - expect(self.page.locator(self.PROCESSING_PLAN)).to_be_visible(timeout=15000) - logger.info("βœ“ 'Processing your plan' message is visible") - - logger.info("Waiting for plan processing to complete...") - self.page.locator(self.PROCESSING_PLAN).wait_for(state="hidden", timeout=200000) - logger.info("βœ“ Plan processing completed") - - diff --git a/tests/e2e-test/tests/conftest.py b/tests/e2e-test/tests/conftest.py index 365a48cb3..f62513975 100644 --- a/tests/e2e-test/tests/conftest.py +++ b/tests/e2e-test/tests/conftest.py @@ -38,7 +38,7 @@ def login_logout(): @pytest.hookimpl(tryfirst=True) def pytest_html_report_title(report): """Customize HTML report title.""" - report.title = "Test Automation MACAE-v3 GP" + report.title = "Test Automation MACAE" log_streams = {} diff --git a/tests/e2e-test/tests/test_MACAE_GP.py b/tests/e2e-test/tests/test_MACAE_GP.py index b272a10b5..69f59b626 100644 --- a/tests/e2e-test/tests/test_MACAE_GP.py +++ b/tests/e2e-test/tests/test_MACAE_GP.py @@ -5,313 +5,84 @@ import pytest -from pages.HomePage import BIABPage -from config.constants import hr_clarification_text, prompt_question1 +from config.constants import (employee_details, product_details, + prompt_question1, prompt_question2, rai_prompt) +from pages.BIAB import BIABPage logger = logging.getLogger(__name__) -def test_retail_customer_success_workflow(login_logout, request): - """ - Validate Golden path for MACAE-v3. - - Steps: - 1. Validate home page elements are visible - 2. Select Retail Customer Success team - 3. Select quick task and create plan with all agents - 4. Validate all retail agents are displayed - 5. Approve the task plan - 6. Validate retail customer response - 7. Click on new task - 8. Select Product Marketing team - 9. Select quick task and create plan - 10. Validate all product marketing agents are displayed - 11. Approve the task plan - 12. Validate product marketing response - 13. Click on new task - 14. Select Human Resources team - 15. Input custom prompt "Onboard new employee" - 16. Validate all HR agents are displayed - 17. Approve the task plan - 18. Send human clarification with employee details - 19. Validate HR response - """ +# Define test steps and prompts +test_cases = [ + ("Validate home page is loaded", lambda biab: biab.validate_home_page()), + ( + f"Verify Run Prompt 1: '{prompt_question1}' & run all stages", + lambda biab: ( + biab.enter_a_question(prompt_question1), + biab.click_send_button(), + # biab.click_my_task(), + biab.enter_aditional_info(employee_details), + # biab.click_aditional_send_button(), + biab.processing_different_stage(), + ), + ), + ( + f"Verify Run Prompt 2: '{prompt_question2}' & run all stages", + lambda biab: ( + biab.click_new_task(), + biab.enter_a_question(prompt_question2), + biab.click_send_button(), + # biab.click_my_task(), + biab.enter_aditional_info(product_details), + # biab.click_aditional_send_button(), + biab.processing_different_stage(), + ), + ), + ( + "Verify Run Prompt 3 via Quick Task - Mobile Plan Query & run all stages", + lambda biab: ( + biab.click_new_task(), + biab.click_mobile_plan(), + biab.click_send_button(), + # biab.click_my_task(), + biab.processing_different_stage(), + ), + ), + ( + f"Verify Run RAI Prompt: '{rai_prompt}' to make sure task is not created and validation message is displayed.", + lambda biab: ( + biab.click_new_task(), + biab.enter_a_question(rai_prompt), + biab.validate_rai_validation_message(), + ), + ), +] + +# Create test IDs like "01. Validate home page", "02. Run Prompt 1: ..." +test_ids = [f"{i + 1:02d}. {case[0]}" for i, case in enumerate(test_cases)] + + +@pytest.mark.parametrize("prompt, action", test_cases, ids=test_ids) +def test_biab_prompt_case(login_logout, prompt, action, request): + """Each BIAB prompt runs as an individual test case with execution time logging and meaningful test step titles.""" page = login_logout biab_page = BIABPage(page) - - # Update test node ID for HTML report - request.node._nodeid = "Golden Path - MACAE-v3- test golden path works properly" - - logger.info("=" * 80) - logger.info("Starting Multi-Team Workflow Test") - logger.info("=" * 80) - - start_time = time.time() - - try: - # Step 1: Validate Home Page - logger.info("\n" + "=" * 80) - logger.info("STEP 1: Validating Home Page") - logger.info("=" * 80) - step1_start = time.time() - biab_page.validate_home_page() - step1_end = time.time() - logger.info(f"Step 1 completed in {step1_end - step1_start:.2f} seconds") - - # Step 2: Select Retail Customer Success Team - logger.info("\n" + "=" * 80) - logger.info("STEP 2: Selecting Retail Customer Success Team") - logger.info("=" * 80) - step2_start = time.time() - biab_page.select_retail_customer_success_team() - step2_end = time.time() - logger.info(f"Step 2 completed in {step2_end - step2_start:.2f} seconds") - - # Step 3: Select Quick Task and Create Plan - logger.info("\n" + "=" * 80) - logger.info("STEP 3: Selecting Quick Task and Creating Plan") - logger.info("=" * 80) - step3_start = time.time() - biab_page.select_quick_task_and_create_plan() - step3_end = time.time() - logger.info(f"Step 3 completed in {step3_end - step3_start:.2f} seconds") - - # Step 4: Validate All Retail Agents Visible - logger.info("\n" + "=" * 80) - logger.info("STEP 4: Validating All Retail Agents Are Displayed") - logger.info("=" * 80) - step4_start = time.time() - biab_page.validate_retail_agents_visible() - step4_end = time.time() - logger.info(f"Step 4 completed in {step4_end - step4_start:.2f} seconds") - - # Step 5: Approve Retail Task Plan (with retry logic) - logger.info("\n" + "=" * 80) - logger.info("STEP 5: Approving Retail Task Plan") - logger.info("=" * 80) - step5_start = time.time() - step5_retry_attempted = False - try: - biab_page.approve_retail_task_plan() - step5_end = time.time() - logger.info(f"Step 5 completed in {step5_end - step5_start:.2f} seconds") - except Exception as step5_error: - logger.warning("\n" + "⚠" * 80) - logger.warning(f"STEP 5 FAILED: {str(step5_error)}") - logger.warning("Initiating retry logic: Step 7 (New Task) β†’ Retry Steps 3, 4, 5") - logger.warning("⚠" * 80) - step5_retry_attempted = True - - # Perform Step 7: Click New Task - logger.info("\n" + "=" * 80) - logger.info("STEP 7 (RETRY): Clicking New Task") - logger.info("=" * 80) - step7_retry_start = time.time() - biab_page.click_new_task() - biab_page.cancel_retail_task_plan() - step7_retry_end = time.time() - logger.info(f"Step 7 (Retry) completed in {step7_retry_end - step7_retry_start:.2f} seconds") - - # Retry Step 3: Select Quick Task and Create Plan - logger.info("\n" + "=" * 80) - logger.info("STEP 3 (RETRY): Selecting Quick Task and Creating Plan") - logger.info("=" * 80) - step3_retry_start = time.time() - biab_page.select_quick_task_and_create_plan() - step3_retry_end = time.time() - logger.info(f"Step 3 (Retry) completed in {step3_retry_end - step3_retry_start:.2f} seconds") - - # Retry Step 4: Validate All Retail Agents Visible - logger.info("\n" + "=" * 80) - logger.info("STEP 4 (RETRY): Validating All Retail Agents Are Displayed") - logger.info("=" * 80) - step4_retry_start = time.time() - biab_page.validate_retail_agents_visible() - step4_retry_end = time.time() - logger.info(f"Step 4 (Retry) completed in {step4_retry_end - step4_retry_start:.2f} seconds") - - # Retry Step 5: Approve Task Plan - logger.info("\n" + "=" * 80) - logger.info("STEP 5 (RETRY): Approving Retail Task Plan") - logger.info("=" * 80) - step5_retry_start = time.time() - biab_page.approve_retail_task_plan() - step5_end = time.time() - logger.info(f"Step 5 (Retry) completed in {step5_end - step5_retry_start:.2f} seconds") - logger.info("βœ“ Retry successful - continuing with test execution") - - # Step 6: Validate Retail Customer Response - logger.info("\n" + "=" * 80) - logger.info("STEP 6: Validating Retail Customer Response") - logger.info("=" * 80) - step6_start = time.time() - biab_page.validate_retail_customer_response() - step6_end = time.time() - logger.info(f"Step 6 completed in {step6_end - step6_start:.2f} seconds") - - # Step 7: Click New Task - logger.info("\n" + "=" * 80) - logger.info("STEP 7: Clicking New Task") - logger.info("=" * 80) - step7_start = time.time() - biab_page.click_new_task() - step7_end = time.time() - logger.info(f"Step 7 completed in {step7_end - step7_start:.2f} seconds") - - # Step 8: Select Product Marketing Team - logger.info("\n" + "=" * 80) - logger.info("STEP 8: Selecting Product Marketing Team") - logger.info("=" * 80) - step8_start = time.time() - biab_page.select_product_marketing_team() - step8_end = time.time() - logger.info(f"Step 8 completed in {step8_end - step8_start:.2f} seconds") - - # Step 9: Select Quick Task and Create Plan (Product Marketing) - logger.info("\n" + "=" * 80) - logger.info("STEP 9: Selecting Quick Task and Creating Plan (Product Marketing)") - logger.info("=" * 80) - step9_start = time.time() - biab_page.select_quick_task_and_create_plan() - step9_end = time.time() - logger.info(f"Step 9 completed in {step9_end - step9_start:.2f} seconds") - - # Step 10: Validate All Product Marketing Agents Visible - logger.info("\n" + "=" * 80) - logger.info("STEP 10: Validating All Product Marketing Agents Are Displayed") - logger.info("=" * 80) - step10_start = time.time() - biab_page.validate_product_marketing_agents() - step10_end = time.time() - logger.info(f"Step 10 completed in {step10_end - step10_start:.2f} seconds") - - # Step 11: Approve Task Plan (Product Marketing) - logger.info("\n" + "=" * 80) - logger.info("STEP 11: Approving Task Plan (Product Marketing)") - logger.info("=" * 80) - step11_start = time.time() - biab_page.approve_product_marketing_task_plan() - step11_end = time.time() - logger.info(f"Step 11 completed in {step11_end - step11_start:.2f} seconds") - - # Step 12: Validate Product Marketing Response - logger.info("\n" + "=" * 80) - logger.info("STEP 12: Validating Product Marketing Response") - logger.info("=" * 80) - step12_start = time.time() - biab_page.validate_product_marketing_response() - step12_end = time.time() - logger.info(f"Step 12 completed in {step12_end - step12_start:.2f} seconds") - - # Step 13: Click New Task - logger.info("\n" + "=" * 80) - logger.info("STEP 13: Clicking New Task") - logger.info("=" * 80) - step13_start = time.time() - biab_page.click_new_task() - step13_end = time.time() - logger.info(f"Step 13 completed in {step13_end - step13_start:.2f} seconds") - - # Step 14: Select Human Resources Team - logger.info("\n" + "=" * 80) - logger.info("STEP 14: Selecting Human Resources Team") - logger.info("=" * 80) - step14_start = time.time() - biab_page.select_human_resources_team() - step14_end = time.time() - logger.info(f"Step 14 completed in {step14_end - step14_start:.2f} seconds") - - # Step 15: Input Custom Prompt "Onboard new employee" - logger.info("\n" + "=" * 80) - logger.info("STEP 15: Inputting Custom Prompt - Onboard new employee") - logger.info("=" * 80) - step15_start = time.time() - biab_page.input_prompt_and_send(prompt_question1) - step15_end = time.time() - logger.info(f"Step 15 completed in {step15_end - step15_start:.2f} seconds") - - # Step 16: Validate All HR Agents Visible - logger.info("\n" + "=" * 80) - logger.info("STEP 16: Validating All HR Agents Are Displayed") - logger.info("=" * 80) - step16_start = time.time() - biab_page.validate_hr_agents() - step16_end = time.time() - logger.info(f"Step 16 completed in {step16_end - step16_start:.2f} seconds") - - # Step 17: Approve Task Plan (HR) - logger.info("\n" + "=" * 80) - logger.info("STEP 17: Approving HR Task Plan") - logger.info("=" * 80) - step17_start = time.time() - biab_page.approve_task_plan() - step17_end = time.time() - logger.info(f"Step 17 completed in {step17_end - step17_start:.2f} seconds") - - # Step 18: Send Human Clarification with Employee Details - logger.info("\n" + "=" * 80) - logger.info("STEP 18: Sending Human Clarification with Employee Details") - logger.info("=" * 80) - step18_start = time.time() - biab_page.input_clarification_and_send(hr_clarification_text) - step18_end = time.time() - logger.info(f"Step 18 completed in {step18_end - step18_start:.2f} seconds") - - # Step 19: Validate HR Response - logger.info("\n" + "=" * 80) - logger.info("STEP 19: Validating HR Response") - logger.info("=" * 80) - step19_start = time.time() - biab_page.validate_hr_response() - step19_end = time.time() - logger.info(f"Step 19 completed in {step19_end - step19_start:.2f} seconds") - - end_time = time.time() - total_duration = end_time - start_time - - logger.info("\n" + "=" * 80) - logger.info("TEST EXECUTION SUMMARY") - logger.info("=" * 80) - logger.info(f"Step 1 (Home Page Validation): {step1_end - step1_start:.2f}s") - logger.info(f"Step 2 (Retail Team Selection): {step2_end - step2_start:.2f}s") - logger.info(f"Step 3 (Retail Quick Task & Plan Creation): {step3_end - step3_start:.2f}s") - logger.info(f"Step 4 (Retail Agents Validation): {step4_end - step4_start:.2f}s") - logger.info(f"Step 5 (Retail Approve Task Plan): {step5_end - step5_start:.2f}s") - logger.info(f"Step 6 (Retail Customer Response Validation): {step6_end - step6_start:.2f}s") - logger.info(f"Step 7 (Click New Task): {step7_end - step7_start:.2f}s") - logger.info(f"Step 8 (Product Marketing Team Selection): {step8_end - step8_start:.2f}s") - logger.info(f"Step 9 (Product Marketing Quick Task & Plan): {step9_end - step9_start:.2f}s") - logger.info(f"Step 10 (Product Marketing Agents Validation): {step10_end - step10_start:.2f}s") - logger.info(f"Step 11 (Product Marketing Approve Task Plan): {step11_end - step11_start:.2f}s") - logger.info(f"Step 12 (Product Marketing Response Validation): {step12_end - step12_start:.2f}s") - logger.info(f"Step 13 (Click New Task): {step13_end - step13_start:.2f}s") - logger.info(f"Step 14 (HR Team Selection): {step14_end - step14_start:.2f}s") - logger.info(f"Step 15 (HR Input Custom Prompt): {step15_end - step15_start:.2f}s") - logger.info(f"Step 16 (HR Agents Validation): {step16_end - step16_start:.2f}s") - logger.info(f"Step 17 (HR Approve Task Plan): {step17_end - step17_start:.2f}s") - logger.info(f"Step 18 (HR Human Clarification): {step18_end - step18_start:.2f}s") - logger.info(f"Step 19 (HR Response Validation): {step19_end - step19_start:.2f}s") - logger.info(f"Total Execution Time: {total_duration:.2f}s") - logger.info("=" * 80) - logger.info("βœ“ Multi-Team Workflow Test PASSED") - logger.info("=" * 80) - - # Attach execution time to pytest report - request.node._report_sections.append( - ("call", "log", f"Total execution time: {total_duration:.2f}s") - ) - - except Exception as e: - end_time = time.time() - total_duration = end_time - start_time - logger.error("\n" + "=" * 80) - logger.error("TEST EXECUTION FAILED") - logger.error("=" * 80) - logger.error(f"Error: {str(e)}") - logger.error(f"Execution time before failure: {total_duration:.2f}s") - logger.error("=" * 80) - raise + logger.info(f"Running test step: {prompt}") + start = time.time() + if isinstance(action, tuple): + for step in action: + if callable(step): + step() + else: + action(biab_page) + end = time.time() + duration = end - start + logger.info(f"Execution Time for '{prompt}': {duration:.2f}s") + # Attach execution time to pytest report + request.node._report_sections.append( + ("call", "log", f"Execution time: {duration:.2f}s") + ) From 965e3c797e53eae2e90f2136bc6df6747d9031c3 Mon Sep 17 00:00:00 2001 From: Abdul-Microsoft Date: Fri, 21 Nov 2025 17:29:55 +0530 Subject: [PATCH 16/18] docs: add guidance for disabling Log Analytics workspace replication before deletion --- docs/DeploymentGuide.md | 1 + docs/LogAnalyticsReplicationDisable.md | 28 ++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 docs/LogAnalyticsReplicationDisable.md diff --git a/docs/DeploymentGuide.md b/docs/DeploymentGuide.md index 8dbd5d364..8c5479c71 100644 --- a/docs/DeploymentGuide.md +++ b/docs/DeploymentGuide.md @@ -262,6 +262,7 @@ Once you've opened the project in [Codespaces](#github-codespaces), [Dev Contain 6. When Deployment is complete, follow steps in [Set Up Authentication in Azure App Service](../docs/azure_app_service_auth_setup.md) to add app authentication to your web app running on Azure App Service 7. If you are done trying out the application, you can delete the resources by running `azd down`. + > **Note:** If you deployed with `enableRedundancy=true` and Log Analytics workspace replication is enabled, you must first disable replication before running `azd down` else resource group delete will fail. Follow the steps in [Handling Log Analytics Workspace Deletion with Replication Enabled](./LogAnalyticsReplicationDisable.md), wait until replication returns `false`, then run `azd down`. ### πŸ› οΈ Troubleshooting diff --git a/docs/LogAnalyticsReplicationDisable.md b/docs/LogAnalyticsReplicationDisable.md new file mode 100644 index 000000000..f4379a84a --- /dev/null +++ b/docs/LogAnalyticsReplicationDisable.md @@ -0,0 +1,28 @@ +# πŸ›  Handling Log Analytics Workspace Deletion with Replication Enabled + +If redundancy (replication) is enabled for your Log Analytics workspace, you must disable it before deleting the workspace or resource group. Otherwise, deletion will fail. + +## βœ… Steps to Disable Replication Before Deletion +Run the following Azure CLI command. Note: This operation may take about 5 minutes to complete. + +```bash +az resource update --ids "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.OperationalInsights/workspaces/{logAnalyticsName}" --set properties.replication.enabled=false +``` + +Replace: +- `{subscriptionId}` β†’ Your Azure subscription ID +- `{resourceGroupName}` β†’ The name of your resource group +- `{logAnalyticsName}` β†’ The name of your Log Analytics workspace + +Optional: Verify replication disabled (should output `false`): +```bash +az resource show --ids "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.OperationalInsights/workspaces/{logAnalyticsName}" --query properties.replication.enabled -o tsv +``` + +## βœ… After Disabling Replication +You can safely delete: +- The Log Analytics workspace (manual) +- The resource group (manual), or +- All provisioned resources via `azd down` + +Return to: [Deployment Guide](./DeploymentGuide.md) From 0ab669e681689dec4e5050064e7ccdae3344575b Mon Sep 17 00:00:00 2001 From: Shreyas-Microsoft Date: Wed, 3 Dec 2025 13:38:33 +0530 Subject: [PATCH 17/18] fix portal link --- docs/TroubleShootingSteps.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/TroubleShootingSteps.md b/docs/TroubleShootingSteps.md index 01f39d44e..7a52ca3f5 100644 --- a/docs/TroubleShootingSteps.md +++ b/docs/TroubleShootingSteps.md @@ -55,7 +55,7 @@ Before deploying the resources, you may need to enable the **Bring Your Own Publ ## Option 1 ### Steps -1. Go to [Azure Portal](https:/portal.azure.com/#home). +1. Go to [Azure Portal](https://portal.azure.com/#home). 2. Click on the **"Resource groups"** option available on the Azure portal home page. ![alt text](../docs/images/AzureHomePage.png) From cfb4855e2b30bc75e5eefbcb63a229ece9a4dd87 Mon Sep 17 00:00:00 2001 From: Roopan-Microsoft Date: Tue, 7 Apr 2026 12:56:50 +0530 Subject: [PATCH 18/18] fix: reduce default model capacity, add Bicep version guard, improve VM credential docs - Reduce default gptModelCapacity from 150 to 80 TPM to lower quota barrier (addresses InsufficientQuota + SubscriptionIsOverQuotaForSku errors - 453 occurrences, 20.2% of failures) - Add @minValue(1) constraint on gptModelCapacity parameter - Add bicep >= 0.33.0 to requiredVersions in azure.yaml (addresses InvalidTemplateDeployment + InvalidTemplate + tool.bicep.failed - 1,030 occurrences, 45.8% of failures) - Update usageName metadata to reflect reduced capacity (150 -> 80) - Improve VM admin credential parameter descriptions with guidance on env variables and complexity requirements --- azure.yaml | 3 ++- infra/main.bicep | 11 ++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/azure.yaml b/azure.yaml index a8de1df7b..ccb0828c7 100644 --- a/azure.yaml +++ b/azure.yaml @@ -3,4 +3,5 @@ name: multi-agent-custom-automation-engine-solution-accelerator metadata: template: multi-agent-custom-automation-engine-solution-accelerator@1.0 requiredVersions: - azd: '>= 1.18.0' \ No newline at end of file + azd: '>= 1.18.0' + bicep: '>= 0.33.0' \ No newline at end of file diff --git a/infra/main.bicep b/infra/main.bicep index 712f1d888..bc0b42f54 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -36,7 +36,7 @@ param location string azd: { type: 'location' usageName: [ - 'OpenAI.GlobalStandard.gpt-4o, 150' + 'OpenAI.GlobalStandard.gpt-4o, 80' ] } }) @@ -61,8 +61,9 @@ param azureopenaiVersion string = '2025-01-01-preview' @description('Optional. GPT model deployment type. Defaults to GlobalStandard.') param gptModelDeploymentType string = 'GlobalStandard' -@description('Optional. AI model deployment token capacity. Defaults to 150 for optimal performance.') -param gptModelCapacity int = 150 +@description('Optional. AI model deployment token capacity (thousands of tokens per minute). Reduce if provisioning fails with InsufficientQuota. Total must not exceed your subscription GlobalStandard quota.') +@minValue(1) +param gptModelCapacity int = 80 @description('Optional. The tags to apply to all deployed Azure resources.') param tags resourceInput<'Microsoft.Resources/resourceGroups@2025-04-01'>.tags = {} @@ -80,11 +81,11 @@ param enableRedundancy bool = false param enablePrivateNetworking bool = false @secure() -@description('Optional. The user name for the administrator account of the virtual machine. Allows to customize credentials if `enablePrivateNetworking` is set to true.') +@description('Optional. The admin username for the jumpbox VM (used when `enablePrivateNetworking` is true). Provide via AZURE_ENV_VM_ADMIN_USERNAME environment variable for predictable access. Defaults to a random value if not set.') param virtualMachineAdminUsername string = take(newGuid(), 20) -@description('Optional. The password for the administrator account of the virtual machine. Allows to customize credentials if `enablePrivateNetworking` is set to true.') @secure() +@description('Optional. The admin password for the jumpbox VM (used when `enablePrivateNetworking` is true). Must meet Azure complexity requirements (12+ chars, uppercase, lowercase, number, special char). Provide via AZURE_ENV_VM_ADMIN_PASSWORD environment variable for predictable access. Defaults to a random value if not set.') param virtualMachineAdminPassword string = newGuid() @description('Optional. The Container Registry hostname where the docker images for the backend are located.')