From 55e240e282b89abb52aa593e05226e05073bc3c9 Mon Sep 17 00:00:00 2001 From: Vamshi-Microsoft Date: Mon, 17 Nov 2025 14:55:16 +0530 Subject: [PATCH 1/2] Added Deploy-v2 pipeline with manual dispatch and input parameter support and Updated Smoke Testing scenarios --- .github/workflows/deploy-v2.yml | 893 ++++++++++++++++++ .github/workflows/test-automation-v2.yml | 195 ++++ docs/CustomizingAzdParameters.md | 3 + infra/main.bicep | 13 +- infra/main.parameters.json | 9 + infra/main.waf.parameters.json | 9 + tests/e2e-test/base/base.py | 30 +- tests/e2e-test/config/constants.py | 6 +- tests/e2e-test/pages/HomePage.py | 512 +++++++++- tests/e2e-test/pages/loginPage.py | 19 + tests/e2e-test/pytest.ini | 2 + tests/e2e-test/tests/conftest.py | 13 +- .../tests/test_contentProcessing_gp_tc.py | 392 ++++++-- 13 files changed, 2024 insertions(+), 72 deletions(-) create mode 100644 .github/workflows/deploy-v2.yml create mode 100644 .github/workflows/test-automation-v2.yml diff --git a/.github/workflows/deploy-v2.yml b/.github/workflows/deploy-v2.yml new file mode 100644 index 00000000..f4b38b1b --- /dev/null +++ b/.github/workflows/deploy-v2.yml @@ -0,0 +1,893 @@ +name: Deploy-Test-Cleanup (v2) +on: + pull_request: + branches: + - main + workflow_dispatch: + inputs: + azure_location: + description: 'Azure Location For Deployment' + required: false + default: 'australiaeast' + type: choice + options: + - 'australiaeast' + - 'centralus' + - 'eastasia' + - 'eastus2' + - 'japaneast' + - 'northeurope' + - 'southeastasia' + - 'uksouth' + - 'eastus' + resource_group_name: + description: 'Resource Group Name (Optional)' + required: false + default: '' + type: string + + waf_enabled: + description: 'Enable WAF' + required: false + default: false + type: boolean + EXP: + description: 'Enable EXP' + required: false + default: false + type: boolean + build_docker_image: + description: 'Build And Push Docker Image (Optional)' + required: false + default: false + type: boolean + + cleanup_resources: + description: 'Cleanup Deployed Resources' + required: false + default: false + type: boolean + + run_e2e_tests: + description: 'Run End-to-End Tests' + required: false + default: 'GoldenPath-Testing' + type: choice + options: + - 'GoldenPath-Testing' + - 'Smoke-Testing' + - 'None' + + AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: + description: 'Log Analytics Workspace ID (Optional)' + required: false + default: '' + type: string + AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: + description: 'AI Project Resource ID (Optional)' + required: false + default: '' + type: string + existing_webapp_url: + description: 'Existing Container WebApp URL (Skips Deployment)' + required: false + default: '' + type: string + + schedule: + - cron: '0 9,21 * * *' # Runs at 9:00 AM and 9:00 PM GMT +env: + GPT_MIN_CAPACITY: 100 + BRANCH_NAME: ${{ github.event.workflow_run.head_branch || github.head_ref || github.ref_name }} + # For automatic triggers (pull_request, workflow_run, schedule): force Non-WAF + Non-EXP + # For manual dispatch: use input values or defaults + WAF_ENABLED: ${{ github.event_name == 'workflow_dispatch' && (github.event.inputs.waf_enabled || false) || false }} + EXP: ${{ github.event_name == 'workflow_dispatch' && (github.event.inputs.EXP || false) || false }} + CLEANUP_RESOURCES: ${{ github.event_name == 'workflow_dispatch' && (github.event.inputs.cleanup_resources || true) || true }} + RUN_E2E_TESTS: ${{ github.event_name == 'workflow_dispatch' && (github.event.inputs.run_e2e_tests || 'GoldenPath-Testing') || 'GoldenPath-Testing' }} + BUILD_DOCKER_IMAGE: ${{ github.event_name == 'workflow_dispatch' && (github.event.inputs.build_docker_image || false) || false }} + +jobs: + docker-build: + if: github.event_name == 'workflow_dispatch' && github.event.inputs.build_docker_image == 'true' + runs-on: ubuntu-latest + outputs: + IMAGE_TAG: ${{ steps.generate_docker_tag.outputs.IMAGE_TAG }} + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Generate Unique Docker Image Tag + id: generate_docker_tag + run: | + echo "πŸ”¨ Building new Docker image - generating unique tag..." + # Generate unique tag for manual deployment runs + TIMESTAMP=$(date +%Y%m%d-%H%M%S) + RUN_ID="${{ github.run_id }}" + BRANCH_NAME="${{ github.head_ref || github.ref_name }}" + # Sanitize branch name for Docker tag (replace invalid characters with hyphens) + CLEAN_BRANCH_NAME=$(echo "$BRANCH_NAME" | sed 's/[^a-zA-Z0-9._-]/-/g' | sed 's/--*/-/g' | sed 's/^-\|-$//g') + UNIQUE_TAG="${CLEAN_BRANCH_NAME}-${TIMESTAMP}-${RUN_ID}" + echo "IMAGE_TAG=$UNIQUE_TAG" >> $GITHUB_ENV + echo "IMAGE_TAG=$UNIQUE_TAG" >> $GITHUB_OUTPUT + echo "Generated unique Docker tag: $UNIQUE_TAG" + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Azure Container Registry + uses: azure/docker-login@v2 + with: + login-server: ${{ secrets.ACR_TEST_LOGIN_SERVER }} + username: ${{ secrets.ACR_TEST_USERNAME }} + password: ${{ secrets.ACR_TEST_PASSWORD }} + + - name: Build and Push ContentProcessor Docker image + uses: docker/build-push-action@v6 + env: + DOCKER_BUILD_SUMMARY: false + with: + context: ./src/ContentProcessor + file: ./src/ContentProcessor/Dockerfile + push: true + tags: | + ${{ secrets.ACR_TEST_LOGIN_SERVER }}/contentprocessor:${{ steps.generate_docker_tag.outputs.IMAGE_TAG }} + ${{ secrets.ACR_TEST_LOGIN_SERVER }}/contentprocessor:${{ steps.generate_docker_tag.outputs.IMAGE_TAG }}_${{ github.run_number }} + + - name: Build and Push ContentProcessorAPI Docker image + uses: docker/build-push-action@v6 + env: + DOCKER_BUILD_SUMMARY: false + with: + context: ./src/ContentProcessorAPI + file: ./src/ContentProcessorAPI/Dockerfile + push: true + tags: | + ${{ secrets.ACR_TEST_LOGIN_SERVER }}/contentprocessorapi:${{ steps.generate_docker_tag.outputs.IMAGE_TAG }} + ${{ secrets.ACR_TEST_LOGIN_SERVER }}/contentprocessorapi:${{ steps.generate_docker_tag.outputs.IMAGE_TAG }}_${{ github.run_number }} + + - name: Build and Push ContentProcessorWeb Docker image + uses: docker/build-push-action@v6 + env: + DOCKER_BUILD_SUMMARY: false + with: + context: ./src/ContentProcessorWeb + file: ./src/ContentProcessorWeb/Dockerfile + push: true + tags: | + ${{ secrets.ACR_TEST_LOGIN_SERVER }}/contentprocessorweb:${{ steps.generate_docker_tag.outputs.IMAGE_TAG }} + ${{ secrets.ACR_TEST_LOGIN_SERVER }}/contentprocessorweb:${{ steps.generate_docker_tag.outputs.IMAGE_TAG }}_${{ github.run_number }} + + - name: Verify Docker Image Build + run: | + echo "βœ… Docker image successfully built and pushed" + echo "Image tag: ${{ steps.generate_docker_tag.outputs.IMAGE_TAG }}" + + - name: Generate Docker Build Summary + if: always() + run: | + # Extract ACR name from the secret + ACR_NAME=$(echo "${{ secrets.ACR_TEST_LOGIN_SERVER }}") + echo "## 🐳 Docker Build Job Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY + echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY + echo "| **Job Status** | ${{ job.status == 'success' && 'βœ… Success' || '❌ Failed' }} |" >> $GITHUB_STEP_SUMMARY + echo "| **Image Tag** | \`${{ steps.generate_docker_tag.outputs.IMAGE_TAG }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| **Trigger** | ${{ github.event_name }} |" >> $GITHUB_STEP_SUMMARY + echo "| **Branch** | ${{ env.BRANCH_NAME }} |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + if [[ "${{ job.status }}" == "success" ]]; then + echo "### βœ… Build Details" >> $GITHUB_STEP_SUMMARY + echo "Successfully built and pushed three Docker images to ACR:" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Built Images:**" >> $GITHUB_STEP_SUMMARY + echo "- \`${ACR_NAME}.azurecr.io/contentprocessor:${{ steps.generate_docker_tag.outputs.IMAGE_TAG }}\`" >> $GITHUB_STEP_SUMMARY + echo "- \`${ACR_NAME}.azurecr.io/contentprocessorapi:${{ steps.generate_docker_tag.outputs.IMAGE_TAG }}\`" >> $GITHUB_STEP_SUMMARY + echo "- \`${ACR_NAME}.azurecr.io/contentprocessorweb:${{ steps.generate_docker_tag.outputs.IMAGE_TAG }}\`" >> $GITHUB_STEP_SUMMARY + else + echo "### ❌ Build Failed" >> $GITHUB_STEP_SUMMARY + echo "- Docker build process encountered an error" >> $GITHUB_STEP_SUMMARY + echo "- Check the docker-build job for detailed error information" >> $GITHUB_STEP_SUMMARY + fi + + deploy: + if: always() && (github.event_name != 'workflow_dispatch' || github.event.inputs.existing_webapp_url == '' || github.event.inputs.existing_webapp_url == null) + needs: [docker-build] + runs-on: ubuntu-latest + outputs: + invoice_schema_id: ${{ steps.register.outputs.invoice_schema_id }} + propertydamageclaimform_schema_id: ${{ steps.register.outputs.propertylossdamageclaimform_schema_id }} + RESOURCE_GROUP_NAME: ${{ steps.check_create_rg.outputs.RESOURCE_GROUP_NAME }} + CONTAINER_WEB_APPURL: ${{ steps.get_output.outputs.CONTAINER_WEB_APPURL }} + ENV_NAME: ${{ steps.generate_env_name.outputs.ENV_NAME }} + AZURE_LOCATION: ${{ steps.set_region.outputs.AZURE_LOCATION }} + AZURE_ENV_OPENAI_LOCATION: ${{ steps.set_region.outputs.AZURE_ENV_OPENAI_LOCATION }} + IMAGE_TAG: ${{ steps.determine_image_tag.outputs.IMAGE_TAG }} + QUOTA_FAILED: ${{ steps.quota_failure_output.outputs.QUOTA_FAILED }} + env: + # For automatic triggers: force Non-WAF + Non-EXP, for manual dispatch: use inputs + WAF_ENABLED: ${{ github.event_name == 'workflow_dispatch' && (github.event.inputs.waf_enabled || false) || false }} + EXP: ${{ github.event_name == 'workflow_dispatch' && (github.event.inputs.EXP || false) || false }} + CLEANUP_RESOURCES: ${{ github.event_name == 'workflow_dispatch' && (github.event.inputs.cleanup_resources || true) || true }} + + steps: + - name: Display Workflow Configuration + run: | + echo "πŸš€ ===================================" + echo "πŸ“‹ WORKFLOW CONFIGURATION SUMMARY" + echo "πŸš€ ===================================" + echo "Trigger Type: ${{ github.event_name }}" + echo "Branch: ${{ env.BRANCH_NAME }}" + echo "" + echo "Configuration Settings:" + echo " β€’ WAF Enabled: ${{ env.WAF_ENABLED }}" + echo " β€’ EXP Enabled: ${{ env.EXP }}" + echo " β€’ Run E2E Tests: ${{ env.RUN_E2E_TESTS }}" + echo " β€’ Cleanup Resources: ${{ env.CLEANUP_RESOURCES }}" + echo " β€’ Build Docker Image: ${{ env.BUILD_DOCKER_IMAGE }}" + if [[ "${{ github.event_name }}" == "workflow_dispatch" && -n "${{ github.event.inputs.azure_location }}" ]]; then + echo " β€’ Selected Azure Location: ${{ github.event.inputs.azure_location }}" + else + echo " β€’ Azure Location: Will be determined by quota check" + fi + if [[ "${{ github.event.inputs.existing_webapp_url }}" != "" ]]; then + echo " β€’ Using Existing Webapp URL: ${{ github.event.inputs.existing_webapp_url }}" + echo " β€’ Skip Deployment: Yes" + else + echo " β€’ Skip Deployment: No" + fi + echo "" + if [[ "${{ github.event_name }}" != "workflow_dispatch" ]]; then + echo "ℹ️ Automatic Trigger: Using Non-WAF + Non-EXP configuration" + else + echo "ℹ️ Manual Trigger: Using user-specified configuration" + # Check if EXP was auto-enabled after user input validation + if [[ "${{ env.EXP }}" == "true" && "${{ github.event.inputs.EXP }}" != "true" ]]; then + echo "πŸ”§ Note: EXP was automatically enabled due to provided parameter values" + fi + fi + echo "πŸš€ ===================================" + + - name: Validate and Auto-Configure EXP + run: | + echo "πŸ” Validating EXP configuration..." + + # Check if EXP values were provided but EXP is disabled + if [[ "${{ github.event.inputs.EXP }}" != "true" ]]; then + if [[ -n "${{ github.event.inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }}" ]] || [[ -n "${{ github.event.inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }}" ]]; then + echo "πŸ”§ AUTO-ENABLING EXP: EXP parameter values were provided but EXP was not explicitly enabled." + echo "" + echo "You provided values for:" + [[ -n "${{ github.event.inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }}" ]] && echo " - Azure Log Analytics Workspace ID: '${{ github.event.inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }}'" + [[ -n "${{ github.event.inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }}" ]] && echo " - Azure AI Project Resource ID: '${{ github.event.inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }}'" + echo "" + echo "βœ… Automatically enabling EXP to use these values." + echo "EXP=true" >> $GITHUB_ENV + echo "πŸ“Œ EXP has been automatically enabled for this deployment." + fi + fi + + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Setup Azure CLI + run: | + curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash + az --version # Verify installation + + - name: Login to Azure + run: | + az login --service-principal -u ${{ secrets.AZURE_CLIENT_ID }} -p ${{ secrets.AZURE_CLIENT_SECRET }} --tenant ${{ secrets.AZURE_TENANT_ID }} + az account set --subscription ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + - name: Run Quota Check + id: quota-check + run: | + export AZURE_CLIENT_ID=${{ secrets.AZURE_CLIENT_ID }} + export AZURE_TENANT_ID=${{ secrets.AZURE_TENANT_ID }} + export AZURE_CLIENT_SECRET=${{ secrets.AZURE_CLIENT_SECRET }} + export AZURE_SUBSCRIPTION_ID="${{ secrets.AZURE_SUBSCRIPTION_ID }}" + export GPT_MIN_CAPACITY=${{ env.GPT_MIN_CAPACITY }} + export AZURE_REGIONS="${{ vars.AZURE_REGIONS }}" + + chmod +x infra/scripts/checkquota.sh + if ! infra/scripts/checkquota.sh; then + # If quota check fails due to insufficient quota, set the flag + if grep -q "No region with sufficient quota found" infra/scripts/checkquota.sh; then + echo "QUOTA_FAILED=true" >> $GITHUB_ENV + fi + exit 1 # Fail the pipeline if any other failure occurs + fi + + - name: Set Quota Failure Output + id: quota_failure_output + if: env.QUOTA_FAILED == 'true' + run: | + echo "QUOTA_FAILED=true" >> $GITHUB_OUTPUT + echo "Quota check failed - will notify via separate notification job" + + - name: Fail Pipeline if Quota Check Fails + if: env.QUOTA_FAILED == 'true' + run: exit 1 + + - name: Set Deployment Region + id: set_region + run: | + # Set AZURE_ENV_OPENAI_LOCATION from quota check result + echo "Selected Region from Quota Check: $VALID_REGION" + echo "AZURE_ENV_OPENAI_LOCATION=$VALID_REGION" >> $GITHUB_ENV + echo "AZURE_ENV_OPENAI_LOCATION=$VALID_REGION" >> $GITHUB_OUTPUT + + # Set AZURE_LOCATION from user input (for manual dispatch) or default to quota check result (for automatic triggers) + if [[ "${{ github.event_name }}" == "workflow_dispatch" && -n "${{ github.event.inputs.azure_location }}" ]]; then + USER_SELECTED_LOCATION="${{ github.event.inputs.azure_location }}" + echo "Using user-selected Azure location: $USER_SELECTED_LOCATION" + echo "AZURE_LOCATION=$USER_SELECTED_LOCATION" >> $GITHUB_ENV + echo "AZURE_LOCATION=$USER_SELECTED_LOCATION" >> $GITHUB_OUTPUT + else + echo "Using location from quota check for automatic triggers: $VALID_REGION" + echo "AZURE_LOCATION=$VALID_REGION" >> $GITHUB_ENV + echo "AZURE_LOCATION=$VALID_REGION" >> $GITHUB_OUTPUT + fi + + - name: Generate Resource Group Name + id: generate_rg_name + run: | + # Check if a resource group name was provided as input + if [[ -n "${{ github.event.inputs.resource_group_name }}" ]]; then + echo "Using provided Resource Group name: ${{ github.event.inputs.resource_group_name }}" + echo "RESOURCE_GROUP_NAME=${{ github.event.inputs.resource_group_name }}" >> $GITHUB_ENV + else + echo "Generating a unique resource group name..." + ACCL_NAME="cp" # Account name as specified + SHORT_UUID=$(uuidgen | cut -d'-' -f1) + UNIQUE_RG_NAME="arg-${ACCL_NAME}-${SHORT_UUID}" + echo "RESOURCE_GROUP_NAME=${UNIQUE_RG_NAME}" >> $GITHUB_ENV + echo "Generated RESOURCE_GROUP_NAME: ${UNIQUE_RG_NAME}" + fi + + - name: Setup Azure Developer CLI + run: | + curl -fsSL https://aka.ms/install-azd.sh | sudo bash + azd version + + - name: Login to Azure + id: login-azure + run: | + az login --service-principal -u ${{ secrets.AZURE_CLIENT_ID }} -p ${{ secrets.AZURE_CLIENT_SECRET }} --tenant ${{ secrets.AZURE_TENANT_ID }} + azd auth login --client-id ${{ secrets.AZURE_CLIENT_ID }} --client-secret ${{ secrets.AZURE_CLIENT_SECRET }} --tenant-id ${{ secrets.AZURE_TENANT_ID }} + + - name: Install Bicep CLI + run: az bicep install + + - name: Check and Create Resource Group + id: check_create_rg + run: | + set -e + echo "πŸ” Checking if resource group '$RESOURCE_GROUP_NAME' exists..." + rg_exists=$(az group exists --name $RESOURCE_GROUP_NAME) + if [ "$rg_exists" = "false" ]; then + echo "πŸ“¦ Resource group does not exist. Creating new resource group '$RESOURCE_GROUP_NAME' in location '$AZURE_LOCATION'..." + az group create --name $RESOURCE_GROUP_NAME --location $AZURE_LOCATION || { echo "❌ Error creating resource group"; exit 1; } + echo "βœ… Resource group '$RESOURCE_GROUP_NAME' created successfully." + else + echo "βœ… Resource group '$RESOURCE_GROUP_NAME' already exists. Deploying to existing resource group." + fi + echo "RESOURCE_GROUP_NAME=$RESOURCE_GROUP_NAME" >> $GITHUB_OUTPUT + echo "RESOURCE_GROUP_NAME=$RESOURCE_GROUP_NAME" >> $$GITHUB_ENV + + - name: Generate Unique Solution Prefix + id: generate_solution_prefix + run: | + set -e + COMMON_PART="psldg" + TIMESTAMP=$(date +%s) + UPDATED_TIMESTAMP=$(echo $TIMESTAMP | tail -c 6) + UNIQUE_SOLUTION_PREFIX="${COMMON_PART}${UPDATED_TIMESTAMP}" + echo "SOLUTION_PREFIX=${UNIQUE_SOLUTION_PREFIX}" >> $GITHUB_ENV + echo "Generated SOLUTION_PREFIX: ${UNIQUE_SOLUTION_PREFIX}" + + - name: Determine Docker Image Tag + id: determine_image_tag + run: | + if [[ "${{ env.BUILD_DOCKER_IMAGE }}" == "true" ]]; then + # Use the tag from docker-build job if it was built + if [[ "${{ needs.docker-build.result }}" == "success" ]]; then + IMAGE_TAG="${{ needs.docker-build.outputs.IMAGE_TAG }}" + echo "πŸ”— Using Docker image tag from build job: $IMAGE_TAG" + else + echo "❌ Docker build job failed or was skipped, but BUILD_DOCKER_IMAGE is true" + exit 1 + fi + else + echo "🏷️ Using existing Docker image based on branch..." + BRANCH_NAME="${{ env.BRANCH_NAME }}" + echo "Current branch: $BRANCH_NAME" + + # Determine image tag based on branch + if [[ "$BRANCH_NAME" == "main" ]]; then + IMAGE_TAG="latest" + echo "Using main branch - image tag: latest" + elif [[ "$BRANCH_NAME" == "dev" ]]; then + IMAGE_TAG="dev" + echo "Using dev branch - image tag: dev" + elif [[ "$BRANCH_NAME" == "demo" ]]; then + IMAGE_TAG="demo" + echo "Using demo branch - image tag: demo" + elif [[ "$BRANCH_NAME" == "hotfix" ]]; then + BASE_TAG="hotfix" + elif [[ "$BRANCH_NAME" == "dependabotchanges" ]]; then + BASE_TAG="dependabotchanges" + else + IMAGE_TAG="latest" + echo "Using default for branch '$BRANCH_NAME' - image tag: latest" + fi + + echo "Using existing Docker image tag: $IMAGE_TAG" + fi + + echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV + echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_OUTPUT + + - name: Generate Unique Environment Name + id: generate_env_name + run: | + COMMON_PART="pslc" + TIMESTAMP=$(date +%s) + UPDATED_TIMESTAMP=$(echo $TIMESTAMP | tail -c 6) + UNIQUE_ENV_NAME="${COMMON_PART}${UPDATED_TIMESTAMP}" + echo "ENV_NAME=${UNIQUE_ENV_NAME}" >> $GITHUB_ENV + echo "Generated Environment Name: ${UNIQUE_ENV_NAME}" + echo "ENV_NAME=${UNIQUE_ENV_NAME}" >> $GITHUB_OUTPUT + + - name: Configure Parameters Based on WAF Setting + run: | + if [[ "${{ env.WAF_ENABLED }}" == "true" ]]; then + echo "πŸ”§ Configuring WAF deployment - copying main.waf.parameters.json to main.parameters.json..." + cp infra/main.waf.parameters.json infra/main.parameters.json + echo "βœ… Successfully copied WAF parameters to main parameters file" + else + echo "πŸ”§ Configuring Non-WAF deployment - using default main.parameters.json..." + # Ensure we have the original parameters file if it was overwritten + if [[ -f infra/main.waf.parameters.json ]] && [[ ! -f infra/main.parameters.json.backup ]]; then + echo "Backing up original parameters file..." + git checkout HEAD -- infra/main.parameters.json || echo "Using existing main.parameters.json" + fi + fi + + - name: Deploy using azd up and extract values (${{ github.event.inputs.waf_enabled == 'true' && 'WAF' || 'Non-WAF' }}+${{ (github.event.inputs.EXP == 'true' || github.event.inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID != '' || github.event.inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID != '') && 'EXP' || 'Non-EXP' }}) + id: get_output + run: | + set -e + echo "Starting azd deployment..." + echo "WAF Enabled: ${{ env.WAF_ENABLED }}" + echo "EXP: ${{ env.EXP }}" + echo "Using Docker Image Tag: ${{ steps.determine_image_tag.outputs.IMAGE_TAG }}" + + # Install azd (Azure Developer CLI) + curl -fsSL https://aka.ms/install-azd.sh | bash + + # Generate current timestamp in desired format: YYYY-MM-DDTHH:MM:SS.SSSSSSSZ + current_date=$(date -u +"%Y-%m-%dT%H:%M:%S.%7NZ") + + echo "Creating environment..." + azd env new $ENV_NAME --no-prompt + echo "Environment created: $ENV_NAME" + + echo "Setting default subscription..." + azd config set defaults.subscription ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + # Set additional parameters + azd env set AZURE_SUBSCRIPTION_ID="${{ secrets.AZURE_SUBSCRIPTION_ID }}" + azd env set AZURE_ENV_AI_DEPLOYMENTS_LOCATION="$AZURE_ENV_OPENAI_LOCATION" + azd env set AZURE_LOCATION="$AZURE_LOCATION" + azd env set AZURE_RESOURCE_GROUP="$RESOURCE_GROUP_NAME" + azd env set AZURE_ENV_CONTAINER_IMAGE_TAG="${{ steps.determine_image_tag.outputs.IMAGE_TAG }}" + azd env set AZURE_DEV_COLLECT_TELEMETRY="${{ vars.AZURE_DEV_COLLECT_TELEMETRY }}" + # Set ACR name only when building Docker image + if [[ "${{ env.BUILD_DOCKER_IMAGE }}" == "true" ]]; then + # Extract ACR name from login server and set as environment variable + ACR_NAME=$(echo "${{ secrets.ACR_TEST_LOGIN_SERVER }}" ) + azd env set AZURE_ENV_CONTAINER_REGISTRY_ENDPOINT="$ACR_NAME" + echo "Set ACR name to: $ACR_NAME" + else + echo "Skipping ACR name configuration (using existing image)" + fi + + if [[ "${{ env.EXP }}" == "true" ]]; then + echo "βœ… EXP ENABLED - Setting EXP parameters..." + + # Set EXP variables dynamically + if [[ -n "${{ github.event.inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }}" ]]; then + EXP_LOG_ANALYTICS_ID="${{ github.event.inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }}" + else + EXP_LOG_ANALYTICS_ID="${{ secrets.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }}" + fi + + if [[ -n "${{ github.event.inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }}" ]]; then + EXP_AI_PROJECT_ID="${{ github.event.inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }}" + else + EXP_AI_PROJECT_ID="${{ secrets.AZURE_ENV_FOUNDRY_PROJECT_ID }}" + fi + + echo "AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: $EXP_LOG_ANALYTICS_ID" + echo "AZURE_ENV_FOUNDRY_PROJECT_ID: $EXP_AI_PROJECT_ID" + azd env set AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID="$EXP_LOG_ANALYTICS_ID" + azd env set AZURE_ENV_FOUNDRY_PROJECT_ID="$EXP_AI_PROJECT_ID" + else + echo "❌ EXP DISABLED - Skipping EXP parameters" + fi + + # Deploy using azd up + azd up --no-prompt + + echo "βœ… Deployment succeeded." + echo "$DEPLOY_OUTPUT" + + # Get deployment outputs using azd + echo "Extracting deployment outputs..." + DEPLOY_OUTPUT=$(azd env get-values --output json) + echo "Deployment output: $DEPLOY_OUTPUT" + + if [[ -z "$DEPLOY_OUTPUT" ]]; then + echo "Error: Deployment output is empty. Please check the deployment logs." + exit 1 + fi + + # Export variables only after successful deploy + export CONTAINER_API_APPURL="https://$(echo "$DEPLOY_OUTPUT" | jq -r '.CONTAINER_API_APP_FQDN // empty')" + echo "CONTAINER_API_APPURL=$CONTAINER_API_APPURL" >> $GITHUB_ENV + + export CONTAINER_API_APPNAME=$(echo "$DEPLOY_OUTPUT" | jq -r '.CONTAINER_API_APP_NAME // empty') + echo "CONTAINER_API_APPNAME=$CONTAINER_API_APPNAME" >> $GITHUB_ENV + + export CONTAINER_WEB_APPURL="https://$(echo "$DEPLOY_OUTPUT" | jq -r '.CONTAINER_WEB_APP_FQDN // empty')" + echo "CONTAINER_WEB_APPURL=$CONTAINER_WEB_APPURL" >> $GITHUB_ENV + echo "CONTAINER_WEB_APPURL=$CONTAINER_WEB_APPURL" >> $GITHUB_OUTPUT + + export CONTAINER_WEB_APPNAME=$(echo "$DEPLOY_OUTPUT" | jq -r '.CONTAINER_WEB_APP_NAME // empty') + echo "CONTAINER_WEB_APPNAME=$CONTAINER_WEB_APPNAME" >> $GITHUB_ENV + + - name: Register schemas + id: register + run: | + echo "Registering schemas..." + sleep 40 # Wait for the API to be ready + + cd src/ContentProcessorAPI/samples/schemas + chmod +x ./register_schema.sh + ./register_schema.sh ${{ env.CONTAINER_API_APPURL }}/schemavault/ schema_info_sh.json + + - name: Upload sample invoice and claim data + run: | + echo "Uploading sample data..." + cd src/ContentProcessorAPI/samples + chmod +x ./upload_files.sh + ./upload_files.sh ${{ env.CONTAINER_API_APPURL }}/contentprocessor/submit ./invoices '${{ steps.register.outputs.invoice_schema_id }}' + ./upload_files.sh ${{ env.CONTAINER_API_APPURL }}/contentprocessor/submit ./propertyclaims '${{ steps.register.outputs.propertylossdamageclaimform_schema_id }}' + + + - name: Disable Auth in Web App + run: | + az containerapp update --name ${{ env.CONTAINER_WEB_APPNAME }} \ + --resource-group ${{ env.RESOURCE_GROUP_NAME }} \ + --set-env-vars APP_AUTH_ENABLED=false + + - name: Disable Auth in API App + run: | + sleep 30 + az containerapp update --name ${{ env.CONTAINER_API_APPNAME }} \ + --resource-group ${{ env.RESOURCE_GROUP_NAME }} \ + --set-env-vars APP_AUTH_ENABLED=false + + - name: Logout from Azure + if: always() + run: | + az logout + echo "Logged out from Azure." + + - name: Generate Deploy Job Summary + if: always() + run: | + echo "## πŸš€ Deploy Job Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY + echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY + echo "| **Job Status** | ${{ job.status == 'success' && 'βœ… Success' || '❌ Failed' }} |" >> $GITHUB_STEP_SUMMARY + echo "| **Resource Group** | \`${{ steps.check_create_rg.outputs.RESOURCE_GROUP_NAME }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| **Azure Region (Infrastructure)** | \`${{ steps.set_region.outputs.AZURE_LOCATION }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| **Azure OpenAI Region** | \`${{ steps.set_region.outputs.AZURE_ENV_OPENAI_LOCATION }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| **Docker Image Tag** | \`${{ steps.determine_image_tag.outputs.IMAGE_TAG }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| **WAF Enabled** | ${{ env.WAF_ENABLED == 'true' && 'βœ… Yes' || '❌ No' }} |" >> $GITHUB_STEP_SUMMARY + echo "| **EXP Enabled** | ${{ env.EXP == 'true' && 'βœ… Yes' || '❌ No' }} |" >> $GITHUB_STEP_SUMMARY + echo "| **Trigger** | ${{ github.event_name }} |" >> $GITHUB_STEP_SUMMARY + echo "| **Branch** | ${{ env.BRANCH_NAME }} |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + if [[ "${{ job.status }}" == "success" ]]; then + echo "### βœ… Deployment Details" >> $GITHUB_STEP_SUMMARY + echo "- **Container Web App URL**: [${{ steps.get_output.outputs.CONTAINER_WEB_APPURL }}](${{ steps.get_output.outputs.CONTAINER_WEB_APPURL }})" >> $GITHUB_STEP_SUMMARY + echo "- **Container API App URL**: [${{ env.CONTAINER_API_APPURL }}](${{ env.CONTAINER_API_APPURL }})" >> $GITHUB_STEP_SUMMARY + echo "- **Configuration**: ${{ env.WAF_ENABLED == 'true' && 'WAF' || 'Non-WAF' }}+${{ env.EXP == 'true' && 'EXP' || 'Non-EXP' }}" >> $GITHUB_STEP_SUMMARY + echo "- Successfully deployed to Azure with all resources configured" >> $GITHUB_STEP_SUMMARY + echo "- Schemas registered and sample data uploaded successfully" >> $GITHUB_STEP_SUMMARY + else + echo "### ❌ Deployment Failed" >> $GITHUB_STEP_SUMMARY + echo "- Deployment process encountered an error" >> $GITHUB_STEP_SUMMARY + echo "- Check the deploy job for detailed error information" >> $GITHUB_STEP_SUMMARY + fi + + e2e-test: + if: always() && ((needs.deploy.result == 'success' && needs.deploy.outputs.CONTAINER_WEB_APPURL != '') || (github.event.inputs.existing_webapp_url != '' && github.event.inputs.existing_webapp_url != null)) && (github.event_name != 'workflow_dispatch' || (github.event.inputs.run_e2e_tests != 'None' && github.event.inputs.run_e2e_tests != '' && github.event.inputs.run_e2e_tests != null)) + needs: [docker-build, deploy] + uses: ./.github/workflows/test-automation-v2.yml + with: + CP_WEB_URL: ${{ needs.deploy.outputs.CONTAINER_WEB_APPURL || github.event.inputs.existing_webapp_url }} + TEST_SUITE: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.run_e2e_tests || 'GoldenPath-Testing' }} + secrets: inherit + + send-notification: + if: always() + needs: [docker-build, deploy, e2e-test] + runs-on: ubuntu-latest + env: + accelerator_name: "Content Processing" + steps: + - name: Determine Test Suite Display Name + id: test_suite + run: | + # Determine test suite display name based on RUN_E2E_TESTS value + if [ "${{ env.RUN_E2E_TESTS }}" = "GoldenPath-Testing" ]; then + TEST_SUITE_NAME="Golden Path Testing" + elif [ "${{ env.RUN_E2E_TESTS }}" = "Smoke-Testing" ]; then + TEST_SUITE_NAME="Smoke Testing" + elif [ "${{ env.RUN_E2E_TESTS }}" = "None" ]; then + TEST_SUITE_NAME="None" + else + TEST_SUITE_NAME="${{ env.RUN_E2E_TESTS }}" + fi + echo "TEST_SUITE_NAME=$TEST_SUITE_NAME" >> $GITHUB_OUTPUT + echo "Test Suite: $TEST_SUITE_NAME" + + - name: Send Quota Failure Notification + if: needs.deploy.result == 'failure' && needs.deploy.outputs.QUOTA_FAILED == 'true' + run: | + RUN_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + EMAIL_BODY=$(cat <Dear Team,

We would like to inform you that the ${{ env.accelerator_name }} deployment has failed due to insufficient quota in the requested regions.

Issue Details:
β€’ Quota check failed for GPT model
β€’ Required GPT Capacity: ${{ env.GPT_MIN_CAPACITY }}
β€’ Checked Regions: ${{ vars.AZURE_REGIONS }}

Run URL: ${RUN_URL}

Please resolve the quota issue and retry the deployment.

Best regards,
Your Automation Team

", + "subject": "${{ env.accelerator_name }} Pipeline - Failed (Insufficient Quota)" + } + EOF + ) + + curl -X POST "${{ secrets.EMAILNOTIFICATION_LOGICAPP_URL_TA }}" \ + -H "Content-Type: application/json" \ + -d "$EMAIL_BODY" || echo "Failed to send quota failure notification" + + - name: Send Deployment Failure Notification + if: needs.deploy.result == 'failure' && needs.deploy.outputs.QUOTA_FAILED != 'true' + run: | + RUN_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + RESOURCE_GROUP="${{ needs.deploy.outputs.RESOURCE_GROUP_NAME }}" + + EMAIL_BODY=$(cat <Dear Team,

We would like to inform you that the ${{ env.accelerator_name }} deployment process has encountered an issue and has failed to complete successfully.

Deployment Details:
β€’ Resource Group: ${RESOURCE_GROUP}
β€’ WAF Enabled: ${{ env.WAF_ENABLED }}
β€’ EXP Enabled: ${{ env.EXP }}

Run URL: ${RUN_URL}

Please investigate the deployment failure at your earliest convenience.

Best regards,
Your Automation Team

", + "subject": "${{ env.accelerator_name }} Pipeline - Failed" + } + EOF + ) + + curl -X POST "${{ secrets.EMAILNOTIFICATION_LOGICAPP_URL_TA }}" \ + -H "Content-Type: application/json" \ + -d "$EMAIL_BODY" || echo "Failed to send deployment failure notification" + + - name: Send Success Notification + if: needs.deploy.result == 'success' && (needs.e2e-test.result == 'skipped' || needs.e2e-test.outputs.TEST_SUCCESS == 'true') + run: | + RUN_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + WEBAPP_URL="${{ needs.deploy.outputs.CONTAINER_WEB_APPURL || github.event.inputs.existing_webapp_url }}" + RESOURCE_GROUP="${{ needs.deploy.outputs.RESOURCE_GROUP_NAME }}" + TEST_REPORT_URL="${{ needs.e2e-test.outputs.TEST_REPORT_URL }}" + TEST_SUITE_NAME="${{ steps.test_suite.outputs.TEST_SUITE_NAME }}" + + # Create email body based on test result + if [ "${{ needs.e2e-test.result }}" = "skipped" ]; then + EMAIL_BODY=$(cat <Dear Team,

We would like to inform you that the ${{ env.accelerator_name }} deployment has completed successfully.

Deployment Details:
β€’ Resource Group: ${RESOURCE_GROUP}
β€’ Web App URL: ${WEBAPP_URL}
β€’ E2E Tests: Skipped (as configured)

Configuration:
β€’ WAF Enabled: ${{ env.WAF_ENABLED }}
β€’ EXP Enabled: ${{ env.EXP }}

Run URL: ${RUN_URL}

Best regards,
Your Automation Team

", + "subject": "${{ env.accelerator_name }} Pipeline - Deployment Success" + } + EOF + ) + else + EMAIL_BODY=$(cat <Dear Team,

We would like to inform you that the ${{ env.accelerator_name }} deployment and testing process has completed successfully.

Deployment Details:
β€’ Resource Group: ${RESOURCE_GROUP}
β€’ Web App URL: ${WEBAPP_URL}
β€’ E2E Tests: Passed βœ…
β€’ Test Suite: ${TEST_SUITE_NAME}
β€’ Test Report: View Report

Configuration:
β€’ WAF Enabled: ${{ env.WAF_ENABLED }}
β€’ EXP Enabled: ${{ env.EXP }}

Run URL: ${RUN_URL}

Best regards,
Your Automation Team

", + "subject": "${{ env.accelerator_name }} Pipeline - Test Automation - Success" + } + EOF + ) + fi + + curl -X POST "${{ secrets.EMAILNOTIFICATION_LOGICAPP_URL_TA }}" \ + -H "Content-Type: application/json" \ + -d "$EMAIL_BODY" || echo "Failed to send success notification" + + - name: Send Test Failure Notification + if: needs.deploy.result == 'success' && needs.e2e-test.result != 'skipped' && needs.e2e-test.outputs.TEST_SUCCESS != 'true' + run: | + RUN_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + TEST_REPORT_URL="${{ needs.e2e-test.outputs.TEST_REPORT_URL }}" + WEBAPP_URL="${{ needs.deploy.outputs.CONTAINER_WEB_APPURL || github.event.inputs.existing_webapp_url }}" + RESOURCE_GROUP="${{ needs.deploy.outputs.RESOURCE_GROUP_NAME }}" + TEST_SUITE_NAME="${{ steps.test_suite.outputs.TEST_SUITE_NAME }}" + + EMAIL_BODY=$(cat <Dear Team,

We would like to inform you that ${{ env.accelerator_name }} accelerator test automation process has encountered issues and failed to complete successfully.

Deployment Details:
β€’ Resource Group: ${RESOURCE_GROUP}
β€’ Web App URL: ${WEBAPP_URL}
β€’ Deployment Status: βœ… Success
β€’ E2E Tests: ❌ Failed
β€’ Test Suite: ${TEST_SUITE_NAME}

Test Details:
β€’ Test Report: View Report

Run URL: ${RUN_URL}

Please investigate the matter at your earliest convenience.

Best regards,
Your Automation Team

", + "subject": "${{ env.accelerator_name }} Pipeline - Test Automation - Failed" + } + EOF + ) + + curl -X POST "${{ secrets.EMAILNOTIFICATION_LOGICAPP_URL_TA }}" \ + -H "Content-Type: application/json" \ + -d "$EMAIL_BODY" || echo "Failed to send test failure notification" + + - name: Send Existing URL Success Notification + # Scenario: Deployment skipped (existing URL provided) AND e2e tests passed + if: needs.deploy.result == 'skipped' && github.event.inputs.existing_webapp_url != '' && needs.e2e-test.result == 'success' && (needs.e2e-test.outputs.TEST_SUCCESS == 'true' || needs.e2e-test.outputs.TEST_SUCCESS == '') + run: | + RUN_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + EXISTING_URL="${{ github.event.inputs.existing_webapp_url }}" + TEST_REPORT_URL="${{ needs.e2e-test.outputs.TEST_REPORT_URL }}" + TEST_SUITE_NAME="${{ steps.test_suite.outputs.TEST_SUITE_NAME }}" + + EMAIL_BODY=$(cat <Dear Team,

The ${{ env.accelerator_name }} pipeline executed against the existing WebApp URL and testing process has completed successfully.

Test Results:
β€’ Status: βœ… Passed
β€’ Test Suite: ${TEST_SUITE_NAME}
${TEST_REPORT_URL:+β€’ Test Report: View Report}
β€’ Target URL: ${EXISTING_URL}

Deployment: Skipped

Run URL: ${RUN_URL}

Best regards,
Your Automation Team

", + "subject": "${{ env.accelerator_name }} Pipeline - Test Automation Passed (Existing URL)" + } + EOF + ) + + curl -X POST "${{ secrets.EMAILNOTIFICATION_LOGICAPP_URL_TA }}" \ + -H "Content-Type: application/json" \ + -d "$EMAIL_BODY" || echo "Failed to send existing URL success notification" + + - name: Send Existing URL Test Failure Notification + # Scenario: Deployment skipped (existing URL provided) AND e2e tests failed + if: needs.deploy.result == 'skipped' && github.event.inputs.existing_webapp_url != '' && needs.e2e-test.result == 'failure' + run: | + RUN_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + EXISTING_URL="${{ github.event.inputs.existing_webapp_url }}" + TEST_REPORT_URL="${{ needs.e2e-test.outputs.TEST_REPORT_URL }}" + TEST_SUITE_NAME="${{ steps.test_suite.outputs.TEST_SUITE_NAME }}" + + EMAIL_BODY=$(cat <Dear Team,

The ${{ env.accelerator_name }} pipeline executed against the existing WebApp URL and the test automation has encountered issues and failed to complete successfully.

Failure Details:
β€’ Target URL: ${EXISTING_URL}
${TEST_REPORT_URL:+β€’ Test Report: View Report}
β€’ Test Suite: ${TEST_SUITE_NAME}
β€’ Deployment: Skipped

Run URL: ${RUN_URL}

Best regards,
Your Automation Team

", + "subject": "${{ env.accelerator_name }} Pipeline - Test Automation Failed (Existing URL)" + } + EOF + ) + + curl -X POST "${{ secrets.EMAILNOTIFICATION_LOGICAPP_URL_TA }}" \ + -H "Content-Type: application/json" \ + -d "$EMAIL_BODY" || echo "Failed to send existing URL test failure notification" + + cleanup-deployment: + if: always() && needs.deploy.result == 'success' && needs.deploy.outputs.RESOURCE_GROUP_NAME != '' && github.event.inputs.existing_webapp_url == '' && (github.event_name != 'workflow_dispatch' || github.event.inputs.cleanup_resources == 'true' || github.event.inputs.cleanup_resources == null) + needs: [docker-build, deploy, e2e-test] + runs-on: ubuntu-latest + env: + RESOURCE_GROUP_NAME: ${{ needs.deploy.outputs.RESOURCE_GROUP_NAME }} + AZURE_LOCATION: ${{ needs.deploy.outputs.AZURE_LOCATION }} + AZURE_ENV_OPENAI_LOCATION: ${{ needs.deploy.outputs.AZURE_ENV_OPENAI_LOCATION }} + ENV_NAME: ${{ needs.deploy.outputs.ENV_NAME }} + IMAGE_TAG: ${{ needs.deploy.outputs.IMAGE_TAG }} + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Setup Azure Developer CLI + run: | + curl -fsSL https://aka.ms/install-azd.sh | sudo bash + azd version + + - name: Login to Azure + run: | + azd auth login --client-id ${{ secrets.AZURE_CLIENT_ID }} --client-secret ${{ secrets.AZURE_CLIENT_SECRET }} --tenant-id ${{ secrets.AZURE_TENANT_ID }} + azd config set defaults.subscription ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + - name: Setup Azure CLI for Docker cleanup + run: | + curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash + az --version + + - name: Login to Azure CLI for Docker cleanup + run: | + az login --service-principal -u ${{ secrets.AZURE_CLIENT_ID }} -p ${{ secrets.AZURE_CLIENT_SECRET }} --tenant ${{ secrets.AZURE_TENANT_ID }} + + - name: Delete Docker Images from ACR + if: github.event.inputs.existing_webapp_url == '' + run: | + set -e + echo "πŸ—‘οΈ Cleaning up Docker images from Azure Container Registry..." + + # Determine the image tag to delete - check if docker-build job ran + if [[ "${{ needs.docker-build.result }}" == "success" ]]; then + IMAGE_TAG="${{ needs.docker-build.outputs.IMAGE_TAG }}" + echo "Using image tag from docker-build job: $IMAGE_TAG" + else + IMAGE_TAG="${{ needs.deploy.outputs.IMAGE_TAG }}" + echo "Using image tag from deploy job: $IMAGE_TAG" + fi + + if [[ -n "$IMAGE_TAG" && "$IMAGE_TAG" != "latest" && "$IMAGE_TAG" != "dev" && "$IMAGE_TAG" != "demo" && "$IMAGE_TAG" != "hotfix" && "$IMAGE_TAG" != "dependabotchanges" ]]; then + echo "Deleting Docker images with tag: $IMAGE_TAG" + + # Delete the main image + echo "Deleting image: ${{ secrets.ACR_TEST_LOGIN_SERVER }}/webapp:$IMAGE_TAG" + az acr repository delete --name $(echo "${{ secrets.ACR_TEST_LOGIN_SERVER }}" | cut -d'.' -f1) \ + --image webapp:$IMAGE_TAG --yes || echo "Warning: Failed to delete main image or image not found" + + echo "βœ… Docker images cleanup completed" + else + echo "⚠️ Skipping Docker image cleanup (using standard branch image: $IMAGE_TAG)" + fi + + - name: Select Environment and Delete deployment using azd + run: | + set -e + # Try to select the environment if it exists, otherwise create a minimal environment for cleanup + azd env list + if azd env list | grep -q "${{ env.ENV_NAME }}"; then + echo "Environment ${{ env.ENV_NAME }} found, selecting it..." + azd env select ${{ env.ENV_NAME }} + else + echo "Environment ${{ env.ENV_NAME }} not found, creating minimal environment for cleanup..." + azd env new ${{ env.ENV_NAME }} --no-prompt + azd env set AZURE_RESOURCE_GROUP "${{ env.RESOURCE_GROUP_NAME }}" + azd env set AZURE_SUBSCRIPTION_ID "${{ secrets.AZURE_SUBSCRIPTION_ID }}" + azd env set AZURE_ENV_AI_DEPLOYMENTS_LOCATION="${{ env.AZURE_ENV_OPENAI_LOCATION }}" + azd env set AZURE_LOCATION="${{ env.AZURE_LOCATION }}" + fi + + echo "Deleting deployment..." + azd down --purge --force --no-prompt + echo "Deployment deleted successfully." + + - name: Logout from Azure + if: always() + run: | + azd auth logout + az logout || echo "Warning: Failed to logout from Azure CLI" + echo "Logged out from Azure." + + - name: Generate Cleanup Job Summary + if: always() + run: | + echo "## 🧹 Cleanup Job Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY + echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY + echo "| **Job Status** | ${{ job.status == 'success' && 'βœ… Success' || '❌ Failed' }} |" >> $GITHUB_STEP_SUMMARY + echo "| **Resource Group** | \`${{ env.RESOURCE_GROUP_NAME }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| **Azure Region (Infrastructure)** | \`${{ env.AZURE_LOCATION }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| **Azure OpenAI Region** | \`${{ env.AZURE_ENV_OPENAI_LOCATION }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| **Docker Image Tag** | \`${{ env.IMAGE_TAG }}\` |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + if [[ "${{ job.status }}" == "success" ]]; then + echo "### βœ… Cleanup Details" >> $GITHUB_STEP_SUMMARY + echo "- Successfully deleted Azure deployment using \`azd down --purge\`" >> $GITHUB_STEP_SUMMARY + echo "- Resource group \`${{ env.RESOURCE_GROUP_NAME }}\` and all associated resources removed" >> $GITHUB_STEP_SUMMARY + else + echo "### ❌ Cleanup Failed" >> $GITHUB_STEP_SUMMARY + echo "- Cleanup process encountered an error" >> $GITHUB_STEP_SUMMARY + echo "- Manual cleanup may be required for:" >> $GITHUB_STEP_SUMMARY + echo " - Resource Group: \`${{ env.RESOURCE_GROUP_NAME }}\`" >> $GITHUB_STEP_SUMMARY + echo "- Check the cleanup-deployment job logs for detailed error information" >> $GITHUB_STEP_SUMMARY + fi \ No newline at end of file diff --git a/.github/workflows/test-automation-v2.yml b/.github/workflows/test-automation-v2.yml new file mode 100644 index 00000000..e865a348 --- /dev/null +++ b/.github/workflows/test-automation-v2.yml @@ -0,0 +1,195 @@ +name: Test Automation Content Processing-v2 + +on: + workflow_call: + inputs: + CP_WEB_URL: + required: true + type: string + description: "Web URL for Content Processing" + TEST_SUITE: + required: false + type: string + default: "GoldenPath-Testing" + description: "Test suite to run: 'Smoke-Testing', 'GoldenPath-Testing' " + secrets: + EMAILNOTIFICATION_LOGICAPP_URL_TA: + required: false + description: "Logic App URL for email notifications" + outputs: + TEST_SUCCESS: + description: "Whether tests passed" + value: ${{ jobs.test.outputs.TEST_SUCCESS }} + TEST_REPORT_URL: + description: "URL to test report artifact" + value: ${{ jobs.test.outputs.TEST_REPORT_URL }} + +env: + url: ${{ inputs.CP_WEB_URL }} + accelerator_name: "Content Processing" + test_suite: ${{ inputs.TEST_SUITE }} + +jobs: + test: + runs-on: ubuntu-latest + outputs: + TEST_SUCCESS: ${{ steps.test1.outcome == 'success' || steps.test2.outcome == 'success' || steps.test3.outcome == 'success' }} + TEST_REPORT_URL: ${{ steps.upload_report.outputs.artifact-url }} + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: '3.13' + + - name: Login to Azure + run: | + az login --service-principal -u ${{ secrets.AZURE_CLIENT_ID }} -p ${{ secrets.AZURE_CLIENT_SECRET }} --tenant ${{ secrets.AZURE_TENANT_ID }} + az account set --subscription ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r tests/e2e-test/requirements.txt + + - name: Ensure browsers are installed + run: python -m playwright install --with-deps chromium + + - name: Validate URL + run: | + if [ -z "${{ env.url }}" ]; then + echo "ERROR: No URL provided for testing" + exit 1 + fi + echo "Testing URL: ${{ env.url }}" + echo "Test Suite: ${{ env.test_suite }}" + + + - name: Wait for Application to be Ready + run: | + echo "Waiting for application to be ready at ${{ env.url }} " + max_attempts=10 + attempt=1 + + while [ $attempt -le $max_attempts ]; do + echo "Attempt $attempt: Checking if application is ready..." + if curl -f -s "${{ env.url }}" > /dev/null; then + echo "Application is ready!" + break + + fi + + if [ $attempt -eq $max_attempts ]; then + echo "Application is not ready after $max_attempts attempts" + exit 1 + fi + + echo "Application not ready, waiting 30 seconds..." + sleep 30 + attempt=$((attempt + 1)) + done + + - name: Run tests(1) + id: test1 + run: | + if [ "${{ env.test_suite }}" == "GoldenPath-Testing" ]; then + xvfb-run pytest -m gp --headed --html=report/report.html --self-contained-html + else + xvfb-run pytest --headed --html=report/report.html --self-contained-html + fi + working-directory: tests/e2e-test + continue-on-error: true + + - name: Sleep for 30 seconds + if: ${{ steps.test1.outcome == 'failure' }} + run: sleep 30s + shell: bash + + - name: Run tests(2) + id: test2 + if: ${{ steps.test1.outcome == 'failure' }} + run: | + if [ "${{ env.test_suite }}" == "GoldenPath-Testing" ]; then + xvfb-run pytest -m gp --headed --html=report/report.html --self-contained-html + else + xvfb-run pytest --headed --html=report/report.html --self-contained-html + fi + working-directory: tests/e2e-test + continue-on-error: true + + - name: Sleep for 60 seconds + if: ${{ steps.test2.outcome == 'failure' }} + run: sleep 60s + shell: bash + + - name: Run tests(3) + id: test3 + if: ${{ steps.test2.outcome == 'failure' }} + run: | + if [ "${{ env.test_suite }}" == "GoldenPath-Testing" ]; then + xvfb-run pytest -m gp --headed --html=report/report.html --self-contained-html + else + xvfb-run pytest --headed --html=report/report.html --self-contained-html + fi + working-directory: tests/e2e-test + + - name: Upload test report + id: upload_report + uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: test-report + path: tests/e2e-test/report/* + + - name: Generate E2E Test Summary + if: always() + run: | + # Determine test suite type for title + if [ "${{ env.test_suite }}" == "GoldenPath-Testing" ]; then + echo "## πŸ§ͺ E2E Test Job Summary : Golden Path Testing" >> $GITHUB_STEP_SUMMARY + else + echo "## πŸ§ͺ E2E Test Job Summary : Smoke Testing" >> $GITHUB_STEP_SUMMARY + fi + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY + echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY + + # Determine overall test result + OVERALL_SUCCESS="${{ steps.test1.outcome == 'success' || steps.test2.outcome == 'success' || steps.test3.outcome == 'success' }}" + if [[ "$OVERALL_SUCCESS" == "true" ]]; then + echo "| **Job Status** | βœ… Success |" >> $GITHUB_STEP_SUMMARY + else + echo "| **Job Status** | ❌ Failed |" >> $GITHUB_STEP_SUMMARY + fi + + echo "| **Target URL** | [${{ env.url }}](${{ env.url }}) |" >> $GITHUB_STEP_SUMMARY + echo "| **Test Suite** | \`${{ env.test_suite }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| **Test Report** | [Download Artifact](${{ steps.upload_report.outputs.artifact-url }}) |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + echo "### πŸ“‹ Test Execution Details" >> $GITHUB_STEP_SUMMARY + echo "| Attempt | Status | Notes |" >> $GITHUB_STEP_SUMMARY + echo "|---------|--------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| **Test Run 1** | ${{ steps.test1.outcome == 'success' && 'βœ… Passed' || '❌ Failed' }} | Initial test execution |" >> $GITHUB_STEP_SUMMARY + + if [[ "${{ steps.test1.outcome }}" == "failure" ]]; then + echo "| **Test Run 2** | ${{ steps.test2.outcome == 'success' && 'βœ… Passed' || steps.test2.outcome == 'failure' && '❌ Failed' || '⏸️ Skipped' }} | Retry after 30s delay |" >> $GITHUB_STEP_SUMMARY + fi + + if [[ "${{ steps.test2.outcome }}" == "failure" ]]; then + echo "| **Test Run 3** | ${{ steps.test3.outcome == 'success' && 'βœ… Passed' || steps.test3.outcome == 'failure' && '❌ Failed' || '⏸️ Skipped' }} | Final retry after 60s delay |" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + + if [[ "$OVERALL_SUCCESS" == "true" ]]; then + echo "### βœ… Test Results" >> $GITHUB_STEP_SUMMARY + echo "- End-to-end tests completed successfully" >> $GITHUB_STEP_SUMMARY + echo "- Application is functioning as expected" >> $GITHUB_STEP_SUMMARY + else + echo "### ❌ Test Results" >> $GITHUB_STEP_SUMMARY + echo "- All test attempts failed" >> $GITHUB_STEP_SUMMARY + echo "- Check the e2e-test/test job for detailed error information" >> $GITHUB_STEP_SUMMARY + fi \ No newline at end of file diff --git a/docs/CustomizingAzdParameters.md b/docs/CustomizingAzdParameters.md index 3ffc7635..cc1d10e0 100644 --- a/docs/CustomizingAzdParameters.md +++ b/docs/CustomizingAzdParameters.md @@ -17,6 +17,9 @@ By default this template will use the environment name as the prefix to prevent | `AZURE_ENV_MODEL_VERSION` | string | `2024-08-06` | Specifies the GPT model version (allowed values: `2024-08-06`). | | `AZURE_ENV_MODEL_CAPACITY` | integer | `30` | Sets the model capacity (choose based on your subscription's available GPT capacity). | | `AZURE_ENV_IMAGETAG` | boolean | `latest` | Set the Image tag Like (allowed values: latest, dev, hotfix) | +| `AZURE_ENV_CONTAINER_REGISTRY_ENDPOINT` | string | `cpscontainerreg.azurecr.io` | Sets the Azure Container Registry name (allowed value: `cpscontainerreg.azurecr.io`) | +| `AZURE_ENV_CONTAINER_IMAGE_TAG` | string | `latest` | Sets the container image tag (e.g., `latest`, `dev`, `hotfix`). | +| `AZURE_LOCATION` | string | `eastus` | Sets the primary Azure region for resource deployment. | | `AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID` | string | Guide to get your [Existing Workspace ID](/docs/re-use-log-analytics.md) | Reuses an existing Log Analytics Workspace instead of provisioning a new one. | | `AZURE_EXISTING_AI_PROJECT_RESOURCE_ID` | string | `` | Reuses an existing AIFoundry and AIFoundryProject instead of creating a new one. | diff --git a/infra/main.bicep b/infra/main.bicep index 1b329d0a..6631a08d 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -60,6 +60,9 @@ param secondaryLocation string = (location == 'eastus2') ? 'westus2' : 'eastus2' @description('Optional. The public container image endpoint.') param publicContainerImageEndpoint string = 'cpscontainerreg.azurecr.io' +@description('Optional. The image tag for the container images.') +param imageTag string = 'latest' + @description('Optional. The resource group location.') param resourceGroupLocation string = resourceGroup().location @@ -717,7 +720,7 @@ module avmContainerApp 'br/public:avm/res/app/container-app:0.17.0' = { containers: [ { name: 'ca-${solutionSuffix}' - image: '${publicContainerImageEndpoint}/contentprocessor:latest' + image: '${publicContainerImageEndpoint}/contentprocessor:${imageTag}' resources: { cpu: '4' @@ -766,7 +769,7 @@ module avmContainerApp_API 'br/public:avm/res/app/container-app:0.17.0' = { containers: [ { name: 'ca-${solutionSuffix}-api' - image: '${publicContainerImageEndpoint}/contentprocessorapi:latest' + image: '${publicContainerImageEndpoint}/contentprocessorapi:${imageTag}' resources: { cpu: '4' memory: '8.0Gi' @@ -896,7 +899,7 @@ module avmContainerApp_Web 'br/public:avm/res/app/container-app:0.17.0' = { containers: [ { name: 'ca-${solutionSuffix}-web' - image: '${publicContainerImageEndpoint}/contentprocessorweb:latest' + image: '${publicContainerImageEndpoint}/contentprocessorweb:${imageTag}' resources: { cpu: '4' memory: '8.0Gi' @@ -1190,7 +1193,7 @@ module avmContainerApp_update 'br/public:avm/res/app/container-app:0.17.0' = { containers: [ { name: 'ca-${solutionSuffix}' - image: '${publicContainerImageEndpoint}/contentprocessor:latest' + image: '${publicContainerImageEndpoint}/contentprocessor:${imageTag}' resources: { cpu: '4' @@ -1250,7 +1253,7 @@ module avmContainerApp_API_update 'br/public:avm/res/app/container-app:0.17.0' = containers: [ { name: 'ca-${solutionSuffix}-api' - image: '${publicContainerImageEndpoint}/contentprocessorapi:latest' + image: '${publicContainerImageEndpoint}/contentprocessorapi:${imageTag}' resources: { cpu: '4' memory: '8.0Gi' diff --git a/infra/main.parameters.json b/infra/main.parameters.json index 2d8f9eb3..adcde897 100644 --- a/infra/main.parameters.json +++ b/infra/main.parameters.json @@ -31,6 +31,15 @@ }, "existingFoundryProjectResourceId": { "value": "${AZURE_ENV_FOUNDRY_PROJECT_ID}" + }, + "publicContainerImageEndpoint": { + "value": "${AZURE_ENV_CONTAINER_REGISTRY_ENDPOINT}" + }, + "imageTag": { + "value": "${AZURE_ENV_CONTAINER_IMAGE_TAG}" + }, + "location": { + "value": "${AZURE_LOCATION}" } } } \ No newline at end of file diff --git a/infra/main.waf.parameters.json b/infra/main.waf.parameters.json index c003947d..2159a8dc 100644 --- a/infra/main.waf.parameters.json +++ b/infra/main.waf.parameters.json @@ -46,6 +46,15 @@ }, "virtualMachineAdminPassword": { "value": "${AZURE_ENV_VM_ADMIN_PASSWORD}" + }, + "publicContainerImageEndpoint": { + "value": "${AZURE_ENV_CONTAINER_REGISTRY_ENDPOINT}" + }, + "imageTag": { + "value": "${AZURE_ENV_CONTAINER_IMAGE_TAG}" + }, + "location": { + "value": "${AZURE_LOCATION}" } } } \ No newline at end of file diff --git a/tests/e2e-test/base/base.py b/tests/e2e-test/base/base.py index 5992ab6a..648346be 100644 --- a/tests/e2e-test/base/base.py +++ b/tests/e2e-test/base/base.py @@ -1,10 +1,38 @@ +""" +Base page module providing common functionality for all page objects. +""" + + class BasePage: + """Base class for all page objects with common methods.""" + def __init__(self, page): + """ + Initialize the BasePage with a Playwright page instance. + + Args: + page: Playwright page object + """ self.page = page def scroll_into_view(self, locator): + """ + Scroll the last element matching the locator into view. + + Args: + locator: Playwright locator object + """ reference_list = locator locator.nth(reference_list.count() - 1).scroll_into_view_if_needed() def is_visible(self, locator): - locator.is_visible() + """ + Check if an element is visible on the page. + + Args: + locator: Playwright locator object + + Returns: + bool: True if visible, False otherwise + """ + return locator.is_visible() diff --git a/tests/e2e-test/config/constants.py b/tests/e2e-test/config/constants.py index f5f4c9ac..28566894 100644 --- a/tests/e2e-test/config/constants.py +++ b/tests/e2e-test/config/constants.py @@ -1,8 +1,12 @@ +""" +Configuration constants module for test environment settings. +""" + import os from dotenv import load_dotenv load_dotenv() URL = os.getenv("url") -if URL.endswith("/"): +if URL and URL.endswith("/"): URL = URL[:-1] diff --git a/tests/e2e-test/pages/HomePage.py b/tests/e2e-test/pages/HomePage.py index ce091f44..b4597be7 100644 --- a/tests/e2e-test/pages/HomePage.py +++ b/tests/e2e-test/pages/HomePage.py @@ -1,10 +1,37 @@ +""" +Home page module for Content Processing Solution Accelerator. +""" + import os.path +import logging from base.base import BasePage from playwright.sync_api import expect +logger = logging.getLogger(__name__) + class HomePage(BasePage): + """ + Home page object containing all locators and methods for interacting + with the Content Processing home page. + """ + # HOMEPAGE + PROCESSING_QUEUE = "//span[normalize-space()='Processing Queue']" + OUTPUT_REVIEW = "//span[normalize-space()='Output Review']" + SOURCE_DOC = "//span[normalize-space()='Source Document']" + PROCESSING_QUEUE_BTN = "//button[normalize-space()='Processing Queue']" + OUTPUT_REVIEW_BTN = "//button[normalize-space()='Output Review']" + SOURCE_DOC_BTN = "//button[normalize-space()='Source Document']" + INVOICE_SELECTED_SCHEMA = "//span[.='Selected Schema : Invoice ']" + PROP_SELECTED_SCHEMA = "//span[.='Selected Schema : Property Loss Damage Claim Form ']" + INVOICE_SELECT_VALIDATION = "//div[contains(text(),'Please Select Schema')]" + SEARCH_BOX = "//input[@placeholder='Search']" + PROCESSING_QUEUE_CP = "//div[@class='panelLeft']//button[@title='Collapse Panel']" + COLLAPSE_PANEL_BTN = "//button[@title='Collapse Panel']" + API_DOCUMENTATION = "//span[.='API Documentation']" + INVALID_FILE_VALIDATION = "//p[contains(.,'Only PDF and JPEG, PNG image files are available.')]" + TITLE_TEXT = "//span[normalize-space()='Processing Queue']" SELECT_SCHEMA = "//input[@placeholder='Select Schema']" IMPORT_CONTENT = "//button[normalize-space()='Import Content']" @@ -12,6 +39,9 @@ class HomePage(BasePage): BROWSE_FILES = "//button[normalize-space()='Browse Files']" UPLOAD_BTN = "//button[normalize-space()='Upload']" SUCCESS_MSG = "/div[@class='file-item']//*[name()='svg']" + UPLOAD_WARNING_MESSAGE = "//div[contains(text(),'Please upload files specific to')]" + SCHEMA_NAME_IN_WARNING = "//div[contains(text(),'Invoice')]" + CLOSE_BTN = "//button[normalize-space()='Close']" STATUS = "//div[@role='cell']" PROCESS_STEPS = "//button[@value='process-history']" @@ -90,26 +120,71 @@ class HomePage(BasePage): ) def __init__(self, page): + """ + Initialize the HomePage. + + Args: + page: Playwright page object + """ + super().__init__(page) self.page = page def validate_home_page(self): - expect(self.page.locator(self.TITLE_TEXT)).to_be_visible() + """Validate that the home page elements are visible.""" + logger.info("Starting home page validation...") + + logger.info("Validating Processing Queue is visible...") + expect(self.page.locator(self.PROCESSING_QUEUE)).to_be_visible() + logger.info("βœ“ Processing Queue is visible") + + logger.info("Validating Output Review is visible...") + expect(self.page.locator(self.OUTPUT_REVIEW)).to_be_visible() + logger.info("βœ“ Output Review is visible") + + logger.info("Validating Source Document is visible...") + expect(self.page.locator(self.SOURCE_DOC)).to_be_visible() + logger.info("βœ“ Source Document is visible") + self.page.wait_for_timeout(2000) + logger.info("Home page validation completed successfully") def select_schema(self, SchemaName): + """Select a schema from the dropdown.""" + logger.info(f"Starting schema selection for: {SchemaName}") + self.page.wait_for_timeout(5000) + + logger.info("Clicking on Select Schema dropdown...") self.page.locator(self.SELECT_SCHEMA).click() + logger.info("βœ“ Select Schema dropdown clicked") + if SchemaName == "Invoice": + logger.info("Selecting 'Invoice' option...") self.page.get_by_role("option", name="Invoice").click() + logger.info("βœ“ 'Invoice' option selected") else: + logger.info("Selecting 'Property Loss Damage Claim' option...") self.page.get_by_role("option", name="Property Loss Damage Claim").click() + logger.info("βœ“ 'Property Loss Damage Claim' option selected") + + logger.info(f"Schema selection completed for: {SchemaName}") def upload_files(self, schemaType): + """Upload files based on schema type.""" + logger.info(f"Starting file upload for schema type: {schemaType}") + with self.page.expect_file_chooser() as fc_info: + logger.info("Clicking Import Content button...") self.page.locator(self.IMPORT_CONTENT).click() + logger.info("βœ“ Import Content button clicked") + + logger.info("Clicking Browse Files button...") self.page.locator(self.BROWSE_FILES).click() + logger.info("βœ“ Browse Files button clicked") + self.page.wait_for_timeout(5000) # self.page.wait_for_load_state('networkidle') + file_chooser = fc_info.value current_working_dir = os.getcwd() file_path1 = os.path.join( @@ -118,149 +193,580 @@ def upload_files(self, schemaType): file_path2 = os.path.join(current_working_dir, "testdata", "ClaimForm_1.pdf") if schemaType == "Invoice": + logger.info(f"Selecting file: {file_path1}") file_chooser.set_files([file_path1]) + logger.info("βœ“ Invoice file selected") else: + logger.info(f"Selecting file: {file_path2}") file_chooser.set_files([file_path2]) + logger.info("βœ“ Claim form file selected") + self.page.wait_for_timeout(5000) self.page.wait_for_load_state("networkidle") + + logger.info("Clicking Upload button...") self.page.locator(self.UPLOAD_BTN).click() + logger.info("βœ“ Upload button clicked") + self.page.wait_for_timeout(10000) + + logger.info("Validating success message is visible...") expect( self.page.get_by_role("alertdialog", name="Import Content") .locator("path") .nth(1) ).to_be_visible() + logger.info("βœ“ Success message is visible") + + logger.info("Closing upload dialog...") self.page.locator(self.CLOSE_BTN).click() + logger.info("βœ“ Upload dialog closed") + + logger.info(f"File upload completed successfully for schema type: {schemaType}") def refresh(self): + """Refresh and wait for processing to complete.""" + logger.info("Starting refresh process to monitor file processing status...") + status_ele = self.page.locator(self.STATUS).nth(2) - max_retries = 15 + max_retries = 20 for i in range(max_retries): status_text = status_ele.inner_text().strip() + logger.info(f"Attempt {i + 1}/{max_retries}: Current status = '{status_text}'") if status_text == "Completed": + logger.info("βœ“ Processing completed successfully") break elif status_text == "Error": + logger.error(f"Process failed with status: 'Error' after {i + 1} retries") raise Exception( f"Process failed with status: 'Error' after {i + 1} retries." ) + logger.info("Clicking Refresh button...") self.page.locator(self.REFRESH).click() + logger.info("βœ“ Refresh button clicked, waiting 5 seconds...") self.page.wait_for_timeout(5000) else: # Executed only if the loop did not break (i.e., status is neither Completed nor Error) + logger.error(f"Process did not complete. Final status was '{status_text}' after {max_retries} retries") raise Exception( f"Process did not complete. Final status was '{status_text}' after {max_retries} retries." ) + logger.info("Refresh process completed successfully") + def validate_invoice_extracted_result(self): + """Validate all extracted invoice data fields.""" + logger.info("Starting invoice extracted result validation...") + + logger.info("Validating Customer Name...") expect(self.page.locator(self.CUSTOMER_NAME)).to_contain_text( "Paris Fashion Group SARL" ) + logger.info("βœ“ Customer Name validated: Paris Fashion Group SARL") + + logger.info("Validating Customer Street...") expect(self.page.locator(self.CUSTOMER_STREET)).to_contain_text( "10 Rue de Rivoli" ) + logger.info("βœ“ Customer Street validated: 10 Rue de Rivoli") + + logger.info("Validating Customer City...") expect(self.page.locator(self.CUSTOMER_CITY)).to_contain_text("Paris") + logger.info("βœ“ Customer City validated: Paris") + + logger.info("Validating Customer Zip Code...") expect(self.page.locator(self.CUSTOMER_ZIP_CODE)).to_contain_text("75001") + logger.info("βœ“ Customer Zip Code validated: 75001") + + logger.info("Validating Customer Country...") expect(self.page.locator(self.CUSTOMER_COUNTRY)).to_contain_text("France") + logger.info("βœ“ Customer Country validated: France") + + logger.info("Validating Shipping Street...") expect(self.page.locator(self.SHIPPING_STREET)).to_contain_text( "25 Avenue Montaigne" ) + logger.info("βœ“ Shipping Street validated: 25 Avenue Montaigne") + + logger.info("Validating Shipping City...") expect(self.page.locator(self.SHIPPING_CITY)).to_contain_text("Paris") + logger.info("βœ“ Shipping City validated: Paris") + + logger.info("Validating Shipping Postal Code...") expect(self.page.locator(self.SHIPPING_POSTAL_CODE)).to_contain_text("75008") + logger.info("βœ“ Shipping Postal Code validated: 75008") + + logger.info("Validating Shipping Country...") expect(self.page.locator(self.SHIPPING_COUNTRY)).to_contain_text("France") + logger.info("βœ“ Shipping Country validated: France") + + logger.info("Validating Purchase Order...") expect(self.page.locator(self.PURCHASE_ORDER)).to_contain_text("PO-34567") + logger.info("βœ“ Purchase Order validated: PO-34567") + + logger.info("Validating Invoice ID...") expect(self.page.locator(self.INVOICE_ID)).to_contain_text("INV-20231005") + logger.info("βœ“ Invoice ID validated: INV-20231005") + + logger.info("Validating Invoice Date...") expect(self.page.locator(self.INVOICE_DATE)).to_contain_text("2023-10-05") - expect(self.page.locator(self.INVOICE_DATE)).to_contain_text("2023-10-05") + logger.info("βœ“ Invoice Date validated: 2023-10-05") + + logger.info("Validating Payable By Date...") expect(self.page.locator(self.payable_by)).to_contain_text("2023-11-04") + logger.info("βœ“ Payable By Date validated: 2023-11-04") + + logger.info("Validating Vendor Name...") expect(self.page.locator(self.vendor_name)).to_contain_text( "Fabrikam Unlimited Company" ) + logger.info("βœ“ Vendor Name validated: Fabrikam Unlimited Company") + + logger.info("Validating Vendor Street...") expect(self.page.locator(self.v_street)).to_contain_text("Wilton Place") + logger.info("βœ“ Vendor Street validated: Wilton Place") + + logger.info("Validating Vendor City...") expect(self.page.locator(self.v_city)).to_contain_text("Brooklyn") + logger.info("βœ“ Vendor City validated: Brooklyn") + + logger.info("Validating Vendor State...") expect(self.page.locator(self.v_state)).to_contain_text("NY") + logger.info("βœ“ Vendor State validated: NY") + + logger.info("Validating Vendor Zip Code...") expect(self.page.locator(self.v_zip_code)).to_contain_text("22345") + logger.info("βœ“ Vendor Zip Code validated: 22345") + + logger.info("Validating Vendor Tax ID...") expect(self.page.locator(self.vendor_tax_id)).to_contain_text("FR123456789") + logger.info("βœ“ Vendor Tax ID validated: FR123456789") + + logger.info("Validating Subtotal...") expect(self.page.locator(self.SUBTOTAL)).to_contain_text("16859.1") + logger.info("βœ“ Subtotal validated: 16859.1") + + logger.info("Validating Total Tax...") expect(self.page.locator(self.TOTAL_TAX)).to_contain_text("11286") + logger.info("βœ“ Total Tax validated: 11286") + + logger.info("Validating Invoice Total...") expect(self.page.locator(self.INVOICE_TOTAL)).to_contain_text("22516.08") + logger.info("βœ“ Invoice Total validated: 22516.08") + + logger.info("Validating Payment Terms...") expect(self.page.locator(self.PAYMENT_TERMS)).to_contain_text("Net 30") + logger.info("βœ“ Payment Terms validated: Net 30") + + logger.info("Validating Product Code...") expect(self.page.locator(self.product_code1)).to_contain_text("EM032") + logger.info("βœ“ Product Code validated: EM032") + + logger.info("Validating Product Description...") expect(self.page.locator(self.p1_description)).to_contain_text( "Item: Terminal Lug" ) + logger.info("βœ“ Product Description validated: Item: Terminal Lug") + + logger.info("Validating Product Quantity...") expect(self.page.locator(self.p1_quantity)).to_contain_text("163") + logger.info("βœ“ Product Quantity validated: 163") + + logger.info("Validating Product Tax...") expect(self.page.locator(self.p1_tax)).to_contain_text("2934") + logger.info("βœ“ Product Tax validated: 2934") + + logger.info("Validating Product Unit Price...") expect(self.page.locator(self.p1_unit_price)).to_contain_text("2.5") + logger.info("βœ“ Product Unit Price validated: 2.5") + + logger.info("Validating Product Total...") expect(self.page.locator(self.p1_total)).to_contain_text("407.5") + logger.info("βœ“ Product Total validated: 407.5") + + logger.info("Invoice extracted result validation completed successfully") def modify_and_submit_extracted_data(self): + """Modify shipping address and submit the changes.""" + logger.info("Starting modification of extracted data...") + + logger.info("Double-clicking on Shipping Street field...") self.page.get_by_text('"25 Avenue Montaigne"').dblclick() + logger.info("βœ“ Shipping Street field double-clicked") + + logger.info("Updating Shipping Street to '25 Avenue Montaigne updated'...") self.page.locator(self.SHIPPING_ADD_STREET).fill("25 Avenue Montaigne updated") + logger.info("βœ“ Shipping Street updated") + + logger.info("Clicking Edit Confirm button...") self.page.locator(self.EDIT_CONFIRM).click() + logger.info("βœ“ Edit Confirm button clicked") + + logger.info("Adding comment: 'Updated Shipping street address'...") self.page.locator(self.COMMENTS).fill("Updated Shipping street address") + logger.info("βœ“ Comment added") + + logger.info("Clicking Save button...") self.page.locator(self.SAVE_BTN).click() + logger.info("βœ“ Save button clicked") + self.page.wait_for_timeout(6000) + logger.info("Data modification and submission completed successfully") def validate_process_steps(self): + """Validate all process steps (extract, map, evaluate).""" + logger.info("Starting process steps validation...") + + logger.info("Clicking on Process Steps tab...") self.page.locator(self.PROCESS_STEPS).click() + logger.info("βœ“ Process Steps tab clicked") + + # Extract Step + logger.info("Validating Extract step...") self.page.locator(self.EXTRACT).click() self.page.wait_for_timeout(3000) + + logger.info("Checking 'extract' text is visible...") expect(self.page.get_by_text('"extract"')).to_be_visible() + logger.info("βœ“ 'extract' text is visible") + + logger.info("Checking 'Succeeded' status is visible...") expect(self.page.get_by_text('"Succeeded"')).to_be_visible() + logger.info("βœ“ 'Succeeded' status is visible for Extract step") + self.page.locator(self.EXTRACT).click() self.page.wait_for_timeout(3000) + + # Map Step + logger.info("Validating Map step...") self.page.locator(self.MAP).click() self.page.wait_for_timeout(3000) + + logger.info("Checking 'map' text is visible...") expect(self.page.get_by_text('"map"')).to_be_visible() + logger.info("βœ“ 'map' text is visible for Map step") + self.page.locator(self.MAP).click() self.page.wait_for_timeout(3000) + + # Evaluate Step + logger.info("Validating Evaluate step...") self.page.locator(self.EVALUATE).click() self.page.wait_for_timeout(3000) + + logger.info("Checking 'evaluate' text is visible...") expect(self.page.get_by_text('"evaluate"')).to_be_visible() + logger.info("βœ“ 'evaluate' text is visible for Evaluate step") + self.page.locator(self.EVALUATE).click() self.page.wait_for_timeout(3000) + + logger.info("Clicking on Extracted Result tab...") self.page.locator(self.EXTRACTED_RESULT).click() self.page.wait_for_timeout(3000) + logger.info("βœ“ Extracted Result tab clicked") + + logger.info("Process steps validation completed successfully") def validate_property_extracted_result(self): + """Validate all extracted property claim data fields.""" + logger.info("Starting property extracted result validation...") + + logger.info("Validating First Name...") expect(self.page.locator(self.first_name)).to_contain_text("Sophia") + logger.info("βœ“ First Name validated: Sophia") + + logger.info("Validating Last Name...") expect(self.page.locator(self.last_name)).to_contain_text("Kim") + logger.info("βœ“ Last Name validated: Kim") + + logger.info("Validating Telephone Number...") expect(self.page.locator(self.tel_no)).to_contain_text("646-555-0789") + logger.info("βœ“ Telephone Number validated: 646-555-0789") + + logger.info("Validating Policy Number...") expect(self.page.locator(self.policy_no)).to_contain_text("PH5678901") + logger.info("βœ“ Policy Number validated: PH5678901") + + logger.info("Validating Coverage Type...") expect(self.page.locator(self.coverage_type)).to_contain_text("Homeowners") + logger.info("βœ“ Coverage Type validated: Homeowners") + + logger.info("Validating Claim Number...") expect(self.page.locator(self.claim_number)).to_contain_text("CLM5432109") + logger.info("βœ“ Claim Number validated: CLM5432109") + + logger.info("Validating Policy Effective Date...") expect(self.page.locator(self.policy_effective_date)).to_contain_text( "2022-07-01" ) + logger.info("βœ“ Policy Effective Date validated: 2022-07-01") + + logger.info("Validating Policy Expiration Date...") expect(self.page.locator(self.policy_expiration_date)).to_contain_text( "2023-07-01" ) + logger.info("βœ“ Policy Expiration Date validated: 2023-07-01") + + logger.info("Validating Damage Deductible...") expect(self.page.locator(self.damage_deductible)).to_contain_text("1000") + logger.info("βœ“ Damage Deductible validated: 1000") + + logger.info("Validating Damage Deductible Currency...") expect(self.page.locator(self.damage_deductible_currency)).to_contain_text( "USD" ) + logger.info("βœ“ Damage Deductible Currency validated: USD") + + logger.info("Validating Date of Damage/Loss...") expect(self.page.locator(self.date_of_damage_loss)).to_contain_text( "2023-05-10" ) + logger.info("βœ“ Date of Damage/Loss validated: 2023-05-10") + + logger.info("Validating Time of Loss...") expect(self.page.locator(self.time_of_loss)).to_contain_text("13:20") + logger.info("βœ“ Time of Loss validated: 13:20") + + logger.info("Validating Date Prepared...") expect(self.page.locator(self.date_prepared)).to_contain_text("2023-05-11") + logger.info("βœ“ Date Prepared validated: 2023-05-11") + + logger.info("Validating Item...") expect(self.page.locator(self.item)).to_contain_text("Apple") + logger.info("βœ“ Item validated: Apple") + + logger.info("Validating Description...") expect(self.page.locator(self.description)).to_contain_text( '"High-performance tablet with a large, vibrant display' ) + logger.info("βœ“ Description validated") + + logger.info("Validating Date Acquired...") expect(self.page.locator(self.date_acquired)).to_contain_text("2022-01-20") + logger.info("βœ“ Date Acquired validated: 2022-01-20") + + logger.info("Validating Cost New...") expect(self.page.locator(self.cost_new)).to_contain_text("1100") + logger.info("βœ“ Cost New validated: 1100") + + logger.info("Validating Cost New Currency...") expect(self.page.locator(self.cost_new_currency)).to_contain_text("USD") + logger.info("βœ“ Cost New Currency validated: USD") + + logger.info("Validating Replacement/Repair...") expect(self.page.locator(self.replacement_repair)).to_contain_text("350") + logger.info("βœ“ Replacement/Repair validated: 350") + + logger.info("Validating Replacement/Repair Currency...") expect(self.page.locator(self.replacement_repair_currency)).to_contain_text( "USD" ) + logger.info("βœ“ Replacement/Repair Currency validated: USD") + + logger.info("Property extracted result validation completed successfully") def delete_files(self): + """Delete uploaded files from the processing queue.""" + logger.info("Starting file deletion process...") + + logger.info("Clicking on Delete File menu button...") self.page.locator(self.DELETE_FILE).nth(0).click() + logger.info("βœ“ Delete File menu button clicked") + + logger.info("Clicking on Delete menu item...") self.page.get_by_role("menuitem", name="Delete").click() + logger.info("βœ“ Delete menu item clicked") + + logger.info("Clicking on Confirm button...") self.page.get_by_role("button", name="Confirm").click() + logger.info("βœ“ Confirm button clicked") + self.page.wait_for_timeout(6000) + logger.info("File deletion completed successfully") + + def validate_import_without_schema(self): + """Validate import content validation when no schema is selected.""" + logger.info("Starting validation for import without schema selection...") + + logger.info("Clicking on Import Content button without selecting schema...") + self.page.locator(self.IMPORT_CONTENT).click() + logger.info("βœ“ Import Content button clicked") + + logger.info("Validating 'Please Select Schema' message is visible...") + expect(self.page.locator(self.INVOICE_SELECT_VALIDATION)).to_be_visible() + logger.info("βœ“ 'Please Select Schema' validation message is visible") + + logger.info("Import without schema validation completed successfully") + + def validate_invoice_schema_selected(self): + """Validate that Invoice schema is selected and visible.""" + logger.info("Starting validation for Invoice schema selection...") + + logger.info("Clicking on Import Content button...") + self.page.locator(self.IMPORT_CONTENT).click() + logger.info("βœ“ Import Content button clicked") + + logger.info("Validating 'Selected Schema : Invoice' message is visible...") + expect(self.page.locator(self.INVOICE_SELECTED_SCHEMA)).to_be_visible() + logger.info("βœ“ 'Selected Schema : Invoice' is visible") + + logger.info("Invoice schema selection validation completed successfully") + + def validate_property_schema_selected(self): + """Validate that Property Loss Damage Claim Form schema is selected and visible.""" + logger.info("Starting validation for Property Loss Damage Claim Form schema selection...") + + logger.info("Clicking on Import Content button...") + self.page.locator(self.IMPORT_CONTENT).click() + logger.info("βœ“ Import Content button clicked") + + logger.info("Validating 'Selected Schema : Property Loss Damage Claim Form' message is visible...") + expect(self.page.locator(self.PROP_SELECTED_SCHEMA)).to_be_visible() + logger.info("βœ“ 'Selected Schema : Property Loss Damage Claim Form' is visible") + + logger.info("Property Loss Damage Claim Form schema selection validation completed successfully") + + def close_upload_popup(self): + """Close the upload popup dialog.""" + logger.info("Starting to close upload popup...") + + logger.info("Clicking on Close button...") + self.page.locator(self.CLOSE_BTN).click() + logger.info("βœ“ Close button clicked") + + logger.info("Upload popup closed successfully") + + def refresh_page(self): + """Refresh the current page using browser reload.""" + logger.info("Starting page refresh...") + + logger.info("Reloading the page...") + self.page.reload() + logger.info("βœ“ Page reloaded") + + self.page.wait_for_timeout(3000) + logger.info("Page refresh completed successfully") + + def validate_search_functionality(self): + """Validate search functionality in extracted results.""" + logger.info("Starting search functionality validation...") + + logger.info("Entering search text 'Fabrikam' in Search Box...") + self.page.locator(self.SEARCH_BOX).fill("Fabrikam") + logger.info("βœ“ Search text 'Fabrikam' entered") + + self.page.wait_for_timeout(2000) + + logger.info("Validating vendor name contains 'Fabrikam'...") + expect(self.page.locator("//div[@id='vendor_name_display']")).to_contain_text("Fabrikam") + logger.info("βœ“ Vendor name contains 'Fabrikam'") + + logger.info("Search functionality validation completed successfully") + + def validate_api_document_link(self): + """Validate API Documentation link opens and displays correct content.""" + logger.info("Starting API Documentation link validation...") + + # Store reference to original page + original_page = self.page + logger.info("Stored reference to original page/tab") + + with self.page.context.expect_page() as new_page_info: + logger.info("Clicking on API Documentation link...") + self.page.locator(self.API_DOCUMENTATION).nth(0).click() + logger.info("βœ“ API Documentation link clicked") + + new_page = new_page_info.value + new_page.wait_for_load_state() + logger.info("New tab/page opened successfully") + + # Switch to new tab + logger.info("Switching to new tab...") + new_page.bring_to_front() + logger.info("βœ“ Switched to new tab") + + logger.info("Validating title heading is visible...") + expect(new_page.locator("//h1[@class='title']")).to_be_visible() + logger.info("βœ“ Title heading is visible") + + logger.info("Validating 'contentprocessor' text is visible...") + expect(new_page.locator("//span[normalize-space()='contentprocessor']")).to_be_visible() + logger.info("βœ“ 'contentprocessor' text is visible") + + logger.info("Validating 'schemavault' text is visible...") + expect(new_page.locator("//span[normalize-space()='schemavault']")).to_be_visible() + logger.info("βœ“ 'schemavault' text is visible") + + logger.info("Validating 'default' text is visible...") + expect(new_page.locator("//span[normalize-space()='default']")).to_be_visible() + logger.info("βœ“ 'default' text is visible") + + logger.info("Validating 'Schemas' text is visible...") + expect(new_page.locator("//span[normalize-space()='Schemas']")).to_be_visible() + logger.info("βœ“ 'Schemas' text is visible") + + logger.info("Closing API Documentation tab...") + new_page.close() + logger.info("βœ“ API Documentation tab closed") + + # Switch back to original tab + logger.info("Switching back to original tab...") + original_page.bring_to_front() + logger.info("βœ“ Switched back to original tab") + + logger.info("API Documentation link validation completed successfully") + + def validate_collapsible_panels(self): + """Validate collapsible section functionality for each panel (Processing Queue, Output Review, Source Document).""" + logger.info("Starting collapsible panels validation...") + + # Collapse Processing Queue panel + logger.info("Collapsing Processing Queue panel...") + self.page.locator(self.COLLAPSE_PANEL_BTN).nth(0).click() + logger.info("βœ“ Collapse button for Processing Queue clicked") + + self.page.wait_for_timeout(2000) + logger.info("Waited 2 seconds after collapsing Processing Queue") + + # Expand Processing Queue panel + logger.info("Expanding Processing Queue panel...") + self.page.locator(self.PROCESSING_QUEUE_BTN).click() + logger.info("βœ“ Processing Queue clicked to expand") + + self.page.wait_for_timeout(2000) + + # Collapse Output Review panel + logger.info("Collapsing Output Review panel...") + self.page.locator(self.COLLAPSE_PANEL_BTN).nth(1).click() + logger.info("βœ“ Collapse button for Output Review clicked") + + self.page.wait_for_timeout(2000) + logger.info("Waited 2 seconds after collapsing Output Review") + + # Expand Output Review panel + logger.info("Expanding Output Review panel...") + self.page.locator(self.OUTPUT_REVIEW_BTN).click() + logger.info("βœ“ Output Review clicked to expand") + + self.page.wait_for_timeout(2000) + + # Collapse Source Document panel + logger.info("Collapsing Source Document panel...") + self.page.locator(self.COLLAPSE_PANEL_BTN).nth(2).click() + logger.info("βœ“ Collapse button for Source Document clicked") + + self.page.wait_for_timeout(2000) + logger.info("Waited 2 seconds after collapsing Source Document") + + # Expand Source Document panel + logger.info("Expanding Source Document panel...") + self.page.locator(self.SOURCE_DOC_BTN).click() + logger.info("βœ“ Source Document clicked to expand") + + self.page.wait_for_timeout(2000) + + logger.info("Collapsible panels validation completed successfully") diff --git a/tests/e2e-test/pages/loginPage.py b/tests/e2e-test/pages/loginPage.py index 0b412556..490e8b4b 100644 --- a/tests/e2e-test/pages/loginPage.py +++ b/tests/e2e-test/pages/loginPage.py @@ -1,7 +1,12 @@ +""" +Login page module for authentication functionality. +""" + from base.base import BasePage class LoginPage(BasePage): + """Login page object with authentication methods.""" EMAIL_TEXT_BOX = "//input[@type='email']" NEXT_BUTTON = "//input[@type='submit']" @@ -11,9 +16,23 @@ class LoginPage(BasePage): PERMISSION_ACCEPT_BUTTON = "//input[@type='submit']" def __init__(self, page): + """ + Initialize the LoginPage. + + Args: + page: Playwright page object + """ + super().__init__(page) self.page = page def authenticate(self, username, password): + """ + Authenticate user with username and password. + + Args: + username: User email address + password: User password + """ # login with username and password in web url self.page.locator(self.EMAIL_TEXT_BOX).fill(username) self.page.locator(self.NEXT_BUTTON).click() diff --git a/tests/e2e-test/pytest.ini b/tests/e2e-test/pytest.ini index 05b7f91c..31a3bee1 100644 --- a/tests/e2e-test/pytest.ini +++ b/tests/e2e-test/pytest.ini @@ -5,3 +5,5 @@ log_file = logs/tests.log log_file_level = INFO addopts = -p no:warnings --tb=short +markers = + gp: Golden Path tests \ No newline at end of file diff --git a/tests/e2e-test/tests/conftest.py b/tests/e2e-test/tests/conftest.py index a6459bf2..4260ec09 100644 --- a/tests/e2e-test/tests/conftest.py +++ b/tests/e2e-test/tests/conftest.py @@ -6,7 +6,6 @@ import atexit import logging from pathlib import Path -from venv import logger import pytest from bs4 import BeautifulSoup @@ -17,6 +16,9 @@ # Global dictionary to store log streams for each test LOG_STREAMS = {} +# Get logger for this module +logger = logging.getLogger(__name__) + @pytest.fixture(scope="session") def login_logout(): @@ -42,6 +44,12 @@ def login_logout(): browser.close() +@pytest.hookimpl(tryfirst=True) +def pytest_html_report_title(report): + """Customize HTML report title.""" + report.title = "Test Automation Content Processing" + + @pytest.hookimpl(tryfirst=True) def pytest_runtest_setup(item): """ @@ -109,9 +117,10 @@ def rename_duration_column(): for th in headers: if th.text.strip() == "Duration": th.string = "Execution Time" + logger.info("Renamed 'Duration' column to 'Execution Time'") break else: - print("'Duration' column not found in report.") + logger.info("'Duration' column not found in report.") with report_path.open("w", encoding="utf-8") as file: file.write(str(soup)) diff --git a/tests/e2e-test/tests/test_contentProcessing_gp_tc.py b/tests/e2e-test/tests/test_contentProcessing_gp_tc.py index 7fe90c2c..7d30e731 100644 --- a/tests/e2e-test/tests/test_contentProcessing_gp_tc.py +++ b/tests/e2e-test/tests/test_contentProcessing_gp_tc.py @@ -1,72 +1,344 @@ +""" +Test module for Content Processing Solution Accelerator end-to-end tests. +""" + import logging -import time import pytest from pages.HomePage import HomePage logger = logging.getLogger(__name__) -# Define step-wise test actions for Golden Path -golden_path_steps = [ - ("Validate home page is loaded", lambda home: home.validate_home_page()), - ("Select Invoice Schema", lambda home: home.select_schema("Invoice")), - ("Upload Invoice documents", lambda home: home.upload_files("Invoice")), - ("Refreshing the page until the 'Invoice' file status is updated to 'Completed'", lambda home: home.refresh()), - ( - "Validate extracted result for Invoice", - lambda home: home.validate_invoice_extracted_result(), - ), - ( - "Modify Extracted Data JSON & submit comments", - lambda home: home.modify_and_submit_extracted_data(), - ), - ("Validate process steps for Invoice", lambda home: home.validate_process_steps()), - ( - "Select Property Loss Damage Claim Form Schema", - lambda home: home.select_schema("Property"), - ), - ( - "Upload Property Loss Damage Claim Form documents", - lambda home: home.upload_files("Property"), - ), - ("Refreshing the page until the 'Claim Form' status is updated to 'Completed'", lambda home: home.refresh()), - ( - "Validate extracted result for Property Loss Damage Claim Form", - lambda home: home.validate_property_extracted_result(), - ), - ( - "Validate process steps for Property Loss Damage Claim Form", - lambda home: home.validate_process_steps(), - ), - ("Validate user able to delete file", lambda home: home.delete_files()), -] - -# Generate readable test step IDs -golden_path_ids = [ - f"{i+1:02d}. {desc}" for i, (desc, _) in enumerate(golden_path_steps) -] - - -@pytest.mark.parametrize("description, action", golden_path_steps, ids=golden_path_ids) -def test_content_processing_steps(login_logout, description, action, request): - """ - Executes Golden Path content processing steps with individual log entries. - """ - request.node._nodeid = description + +@pytest.mark.gp +def test_content_processing_golden_path(login_logout, request): + """ + Content Processing - Validate Golden path works as expected + + Executes golden path test steps for Content Processing Solution Accelerator with detailed logging. + """ + request.node._nodeid = "Content Processing - Validate Golden path works as expected" + page = login_logout home = HomePage(page) - logger.info(f"Running test step: {description}") + # Define step-wise test actions for Golden Path + golden_path_steps = [ + ("01. Validate home page is loaded", lambda: home.validate_home_page()), + ("02. Select Invoice Schema", lambda: home.select_schema("Invoice")), + ("03. Upload Invoice documents", lambda: home.upload_files("Invoice")), + ("04. Refresh until Invoice file status is Completed", lambda: home.refresh()), + ("05. Validate extracted result for Invoice", lambda: home.validate_invoice_extracted_result()), + ("06. Modify Extracted Data JSON & submit comments", lambda: home.modify_and_submit_extracted_data()), + ("07. Validate process steps for Invoice", lambda: home.validate_process_steps()), + ("08. Select Property Loss Damage Claim Form Schema", lambda: home.select_schema("Property")), + ("09. Upload Property Loss Damage Claim Form documents", lambda: home.upload_files("Property")), + ("10. Refresh until Claim Form status is Completed", lambda: home.refresh()), + ("11. Validate extracted result for Property Loss Damage Claim Form", lambda: home.validate_property_extracted_result()), + ("12. Validate process steps for Property Loss Damage Claim Form", lambda: home.validate_process_steps()), + ("13. Validate user able to delete file", lambda: home.delete_files()), + ] + + # Execute all steps sequentially + for description, action in golden_path_steps: + logger.info(f"Running test step: {description}") + try: + action() + logger.info(f"Step passed: {description}") + except Exception: # pylint: disable=broad-exception-caught + logger.error(f"Step failed: {description}", exc_info=True) + raise - start_time = time.time() + +def test_content_processing_sections_display(login_logout, request): + """ + Content Processing - All the sections need to be displayed properly + + Validates that all main sections (Processing Queue, Output Review, Source Document) + are displayed correctly on the home page. + """ + request.node._nodeid = "Content Processing - All the sections need to be displayed properly" + + page = login_logout + home = HomePage(page) + + logger.info("Running test: Validate all sections are displayed properly") try: - action(home) - duration = time.time() - start_time - message = "Step passed: %s (Duration: %.2f seconds)" % (description, duration) - logger.info(message) - request.node._report_sections.append(("call", "log", message)) - - except Exception: - duration = time.time() - start_time - logger.error("Step failed: %s (Duration: %.2f seconds)", description, duration, exc_info=True) + home.validate_home_page() + logger.info("Test passed: All sections displayed properly") + except Exception: # pylint: disable=broad-exception-caught + logger.error("Test failed: All sections display validation", exc_info=True) raise - request.node._report_sections.append(("call", "log", f"Step passed: {description}")) + + +def test_content_processing_file_upload(login_logout, request): + """ + Content Processing - Files need to be uploaded successfully + + Validates that files can be uploaded successfully for both Invoice and Property schemas. + """ + request.node._nodeid = "Content Processing - Files need to be uploaded successfully" + + page = login_logout + home = HomePage(page) + + # Define file upload test steps + upload_steps = [ + ("01. Select Invoice Schema", lambda: home.select_schema("Invoice")), + ("02. Upload Invoice documents", lambda: home.upload_files("Invoice")), + ("03. Select Property Loss Damage Claim Form Schema", lambda: home.select_schema("Property")), + ("04. Upload Property Loss Damage Claim Form documents", lambda: home.upload_files("Property")), + ] + + # Execute all upload steps sequentially + for description, action in upload_steps: + logger.info(f"Running test step: {description}") + try: + action() + logger.info(f"Step passed: {description}") + except Exception: # pylint: disable=broad-exception-caught + logger.error(f"Step failed: {description}", exc_info=True) + raise + + +def test_content_processing_refresh_screen(login_logout, request): + """ + Content Processing - Refreshing the screen + + Validates that screen refresh works properly after uploading files. + """ + request.node._nodeid = "Content Processing - Refreshing the screen" + + page = login_logout + home = HomePage(page) + + # Define refresh test steps + refresh_steps = [ + ("01. Select Invoice Schema", lambda: home.select_schema("Invoice")), + ("02. Upload Invoice documents", lambda: home.upload_files("Invoice")), + ("03. Refresh until file status is Completed", lambda: home.refresh()), + ] + + # Execute all refresh steps sequentially + for description, action in refresh_steps: + logger.info(f"Running test step: {description}") + try: + action() + logger.info(f"Step passed: {description}") + except Exception: # pylint: disable=broad-exception-caught + logger.error(f"Step failed: {description}", exc_info=True) + raise + + +def test_content_processing_schema_validation(login_logout, request): + """ + Content Processing - Validate Content Processing - Alert user to upload file correctly as per the selected schema + + Validates that the system correctly displays the selected schema and alerts users to upload + files specific to the selected schema (Invoice and Property Loss Damage Claim Form). + """ + request.node._nodeid = "Content Processing - Validate Content Processing - Alert user to upload file correctly as per the selected schema" + + page = login_logout + home = HomePage(page) + + # Define schema validation test steps + schema_validation_steps = [ + ("01. Validate home page is loaded", lambda: home.validate_home_page()), + ("02. Select Invoice Schema", lambda: home.select_schema("Invoice")), + ("03. Validate Invoice schema is selected correctly", lambda: home.validate_invoice_schema_selected()), + ("04. Close upload popup", lambda: home.close_upload_popup()), + ("05. Select Property Loss Damage Claim Form Schema", lambda: home.select_schema("Property")), + ("06. Validate Property schema is selected correctly", lambda: home.validate_property_schema_selected()), + ("07. Close upload popup", lambda: home.close_upload_popup()), + ("08: Refresh screen", lambda: home.refresh_page()) + ] + + # Execute all schema validation steps sequentially + for description, action in schema_validation_steps: + logger.info(f"Running test step: {description}") + try: + action() + logger.info(f"Step passed: {description}") + except Exception: # pylint: disable=broad-exception-caught + logger.error(f"Step failed: {description}", exc_info=True) + raise + + +def test_content_processing_import_without_schema(login_logout, request): + """ + Content Processing - Once cleared Select Schema dropdown, import content shows validation + + Validates that when no schema is selected (or schema is cleared), clicking Import Content + button displays appropriate validation message prompting user to select a schema first. + """ + request.node._nodeid = "Content Processing - Once cleared Select Schema dropdown, import content shows validation" + + page = login_logout + home = HomePage(page) + + # Define import without schema validation test steps + import_validation_steps = [ + ("01. Validate home page is loaded", lambda: home.validate_home_page()), + ("02. Validate import content without schema selection", lambda: home.validate_import_without_schema()), + ] + + # Execute all import validation steps sequentially + for description, action in import_validation_steps: + logger.info(f"Running test step: {description}") + try: + action() + logger.info(f"Step passed: {description}") + except Exception: # pylint: disable=broad-exception-caught + logger.error(f"Step failed: {description}", exc_info=True) + raise + + +def test_content_processing_delete_file(login_logout, request): + """ + Content Processing - Delete File + + Validates that uploaded files can be successfully deleted from the processing queue. + Uploads a file first, then verifies the delete functionality works correctly. + """ + request.node._nodeid = "Content Processing - Delete File" + + page = login_logout + home = HomePage(page) + + # Define delete file test steps + delete_file_steps = [ + ("01. Validate home page is loaded", lambda: home.validate_home_page()), + ("02. Delete uploaded file", lambda: home.delete_files()), + ] + + # Execute all delete file steps sequentially + for description, action in delete_file_steps: + logger.info(f"Running test step: {description}") + try: + action() + logger.info(f"Step passed: {description}") + except Exception: # pylint: disable=broad-exception-caught + logger.error(f"Step failed: {description}", exc_info=True) + raise + + +def test_content_processing_search_functionality(login_logout, request): + """ + Content Processing - Search box inside extracted results + + Validates that the search functionality works correctly in the extracted results section. + Uploads an Invoice file, waits for processing to complete, and then validates search functionality. + """ + request.node._nodeid = "Content Processing - Search box inside extracted results" + + page = login_logout + home = HomePage(page) + + # Define search functionality test steps + search_functionality_steps = [ + ("01. Validate home page is loaded", lambda: home.validate_home_page()), + ("02. Select Invoice Schema", lambda: home.select_schema("Invoice")), + ("03. Upload Invoice documents", lambda: home.upload_files("Invoice")), + ("04. Refresh until file status is Completed", lambda: home.refresh()), + ("05. Validate search functionality in extracted results", lambda: home.validate_search_functionality()), + ] + + # Execute all search functionality steps sequentially + for description, action in search_functionality_steps: + logger.info(f"Running test step: {description}") + try: + action() + logger.info(f"Step passed: {description}") + except Exception: # pylint: disable=broad-exception-caught + logger.error(f"Step failed: {description}", exc_info=True) + raise + + +def test_content_processing_collapsible_panels(login_logout, request): + """ + Content Processing - Collapsible section for each panel + + Validates that each panel (Processing Queue, Output Review, Source Document) can be + collapsed and expanded correctly, ensuring the UI controls work as expected. + """ + request.node._nodeid = "Content Processing - Collapsible section for each panel" + + page = login_logout + home = HomePage(page) + + # Define collapsible panels test steps + collapsible_panels_steps = [ + ("01. Validate home page is loaded", lambda: home.validate_home_page()), + ("02. Validate collapsible panels functionality", lambda: home.validate_collapsible_panels()), + ] + + # Execute all collapsible panels steps sequentially + for description, action in collapsible_panels_steps: + logger.info(f"Running test step: {description}") + try: + action() + logger.info(f"Step passed: {description}") + except Exception: # pylint: disable=broad-exception-caught + logger.error(f"Step failed: {description}", exc_info=True) + raise + + +def test_content_processing_api_documentation(login_logout, request): + """ + Content Processing - API Document + + Validates that the API Documentation link opens correctly in a new page and displays + all required API documentation sections including contentprocessor, schemavault, and Schemas. + """ + request.node._nodeid = "Content Processing - API Document" + + page = login_logout + home = HomePage(page) + + # Define API documentation test steps + api_documentation_steps = [ + ("01. Validate home page is loaded", lambda: home.validate_home_page()), + ("02. Validate API Documentation link and content", lambda: home.validate_api_document_link()), + ] + + # Execute all API documentation steps sequentially + for description, action in api_documentation_steps: + logger.info(f"Running test step: {description}") + try: + action() + logger.info(f"Step passed: {description}") + except Exception: # pylint: disable=broad-exception-caught + logger.error(f"Step failed: {description}", exc_info=True) + raise + + +def test_content_processing_expandable_process_steps(login_logout, request): + """ + Content Processing - Expandable section under each process + + Validates that each process step (extract, map, evaluate) can be expanded and collapsed correctly, + and displays the expected content and status information. + """ + request.node._nodeid = "Content Processing - Expandable section under each process" + + page = login_logout + home = HomePage(page) + + # Define expandable process steps test steps + expandable_process_steps = [ + ("01. Validate home page is loaded", lambda: home.validate_home_page()), + ("02. Select Invoice Schema", lambda: home.select_schema("Invoice")), + ("03. Upload Invoice documents", lambda: home.upload_files("Invoice")), + ("04. Refresh until file status is Completed", lambda: home.refresh()), + ("05. Validate expandable process steps functionality", lambda: home.validate_process_steps()), + ] + + # Execute all expandable process steps sequentially + for description, action in expandable_process_steps: + logger.info(f"Running test step: {description}") + try: + action() + logger.info(f"Step passed: {description}") + except Exception: # pylint: disable=broad-exception-caught + logger.error(f"Step failed: {description}", exc_info=True) + raise From d8e1cf0c2edfcd29b530bc9dff7d92b60239d6f3 Mon Sep 17 00:00:00 2001 From: Vamshi-Microsoft Date: Mon, 17 Nov 2025 15:36:36 +0530 Subject: [PATCH 2/2] Changed Trigger type as per previous pipeline --- .github/workflows/deploy-v2.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy-v2.yml b/.github/workflows/deploy-v2.yml index f4b38b1b..ef7266b5 100644 --- a/.github/workflows/deploy-v2.yml +++ b/.github/workflows/deploy-v2.yml @@ -1,6 +1,6 @@ name: Deploy-Test-Cleanup (v2) on: - pull_request: + push: branches: - main workflow_dispatch: