diff --git a/.github/scripts/cleanup-cluster.sh b/.github/scripts/cleanup-cluster.sh new file mode 100755 index 00000000..fcecaa56 --- /dev/null +++ b/.github/scripts/cleanup-cluster.sh @@ -0,0 +1,53 @@ +#!/bin/bash + +# ------------------------------------------------------------ +# Copyright 2023 The Radius Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ------------------------------------------------------------ + +set -e + +echo "Cleaning up cluster" + +# Delete all test resources in queuemessages. +if kubectl get crd queuemessages.ucp.dev >/dev/null 2>&1; then + echo "delete all resources in queuemessages.ucp.dev" + kubectl delete queuemessages.ucp.dev -n radius-system --all +fi + +# Testing deletion of deployment.apps. + +# Delete all test resources in resources without proxy resource. +if kubectl get crd resources.ucp.dev >/dev/null 2>&1; then + echo "delete all resources in resources.ucp.dev" + resources=$(kubectl get resources.ucp.dev -n radius-system --no-headers -o custom-columns=":metadata.name") + for r in $resources; do + echo "delete resource: $r" + kubectl delete resources.ucp.dev $r -n radius-system --ignore-not-found=true + done +fi + +# Delete all test namespaces. +# Any namespace that is not in the list below will be deleted. +echo "Delete all test namespaces" +namespaces=$(kubectl get namespace | + grep -vE '(radius-system|kube-system|kube-public|kube-node-lease|gatekeeper-system|default|dapr-system|cert-manager)' | + awk '{print $1}') +for ns in $namespaces; do + if [ -z "$ns" ]; then + break + fi + echo "deleting namespaces: $ns" + kubectl delete namespace $ns --ignore-not-found=true +done diff --git a/.github/workflows/test-aks.yaml b/.github/workflows/test-aks.yaml new file mode 100644 index 00000000..390a6d4d --- /dev/null +++ b/.github/workflows/test-aks.yaml @@ -0,0 +1,374 @@ +name: Test Samples (AKS and EKS) + +on: + workflow_dispatch: + inputs: + version: + description: "Radius version number to use (e.g. 0.1.0, 0.1.0-rc1, edge). Defaults to edge." + required: false + default: "edge" + type: string + push: + branches: + - v*.* + - edge + paths: + - "samples/**" + - ".github/workflows/**" + pull_request: + types: [opened, synchronize, reopened] + branches: + - v*.* + - edge + schedule: # Run every 2 hours + - cron: "0 */2 * * *" +env: + RUN_IDENTIFIER: samplestest-${{ github.run_id }}-${{ github.run_attempt }} +jobs: + # setup the test environment + setup: + name: Setup + runs-on: ubuntu-latest + env: + BRANCH: ${{ github.base_ref || github.ref_name }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + AZURE_LOCATION: eastus + AKS_RESOURCE_GROUP: samples-test-rg + AKS_CLUSTER_NAME: samples-test-aks + AWS_REGION: us-west-2 + AWS_ZONES: us-west-2a,us-west-2b,us-west-2c + steps: + - name: Checkout code + uses: actions/checkout@v3 + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: 20 + - name: az CLI login + run: | + az login --service-principal \ + --username ${{ secrets.AZURE_SANDBOX_APP_ID }} \ + --password ${{ secrets.AZURE_SANDBOX_PASSWORD }} \ + --tenant ${{ secrets.AZURE_SANDBOX_TENANT_ID }} + - name: Get kubeconf credential for AKS cluster + run: | + az aks get-credentials \ + --subscription ${{ secrets.AZURE_SANDBOX_SUBSCRIPTION_ID }} \ + --resource-group ${{ env.AKS_RESOURCE_GROUP }} \ + --name ${{ env.AKS_CLUSTER_NAME }} + - name: Download rad CLI + run: | + RADIUS_VERSION="${{ inputs.version }}" + if [[ -z "${{ inputs.version }}" ]]; then + RADIUS_VERSION=edge + fi + ./.github/scripts/install-radius.sh $RADIUS_VERSION + - name: Clean up cluster + run: ./.github/scripts/cleanup-cluster.sh + - name: Reinstall Radius after cleanup + run: | + rad install kubernetes --reinstall + test: + name: Sample tests + runs-on: ${{ matrix.os }} + needs: setup + strategy: + fail-fast: false + matrix: + include: + - name: demo + os: ubuntu-latest + runOnPullRequest: true + app: demo + env: demo + path: ./samples/demo/app.bicep + deployArgs: --application demo -p image=ghcr.io/radius-project/samples/demo:latest + exposeArgs: --application demo + uiTestFile: tests/demo/demo.app.spec.ts + port: 3000 + container: demo + - name: dapr + os: ubuntu-latest-m + runOnPullRequest: true + app: dapr + env: dapr + path: ./samples/dapr/dapr.bicep + deployArgs: -p environment='/planes/radius/local/resourceGroups/dapr/providers/Applications.Core/environments/dapr' -p frontendImage=ghcr.io/radius-project/samples/dapr-frontend:latest -p backendImage=ghcr.io/radius-project/samples/dapr-backend:latest + - name: volumes + os: ubuntu-latest + runOnPullRequest: true + app: myapp + env: volumes + path: ./samples/volumes/app.bicep + deployArgs: -p image=ghcr.io/radius-project/samples/volumes:latest + - name: eshop-containers + os: ubuntu-latest-m + runOnPullRequest: true + app: eshop + env: containers + path: ./samples/eshop/eshop.bicep + uiTestFile: tests/eshop/eshop.app.spec.ts + deployArgs: -p environment='/planes/radius/local/resourceGroups/eshop-containers/providers/Applications.Core/environments/containers' + - name: eshop-azure + os: ubuntu-latest-m + runOnPullRequest: true + app: eshop-azure + env: azure + path: ./samples/eshop/eshop.bicep + uiTestFile: tests/eshop/eshop.app.spec.ts + deployArgs: -p environment='/planes/radius/local/resourceGroups/eshop-azure/providers/Applications.Core/environments/azure' -p applicationName=eshop-azure + credential: azure + - name: eshop-aws + os: ubuntu-latest-m + runOnPullRequest: true + app: eshop-aws + env: aws + path: ./samples/eshop/eshop.bicep + uiTestFile: tests/eshop/eshop.app.spec.ts + deployArgs: -p environment='/planes/radius/local/resourceGroups/eshop-aws/providers/Applications.Core/environments/aws' -p applicationName=eshop-aws + credential: aws + env: + BRANCH: ${{ github.base_ref || github.ref_name }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + AZURE_LOCATION: eastus + AKS_RESOURCE_GROUP: samples-test-rg + AKS_CLUSTER_NAME: samples-test-aks + AWS_REGION: us-west-2 + AWS_ZONES: us-west-2a,us-west-2b,us-west-2c + steps: + # Setup the test assets and configuration + - name: Generate output variables + id: gen-id + run: | + RUN_IDENTIFIER=${{ env.RUN_IDENTIFIER }}-${{ matrix.name }} + + if [[ "${{ github.event_name }}" == "pull_request" && "${{ matrix.runOnPullRequest }}" == "false" ]]; then + RUN_TEST=false + else + RUN_TEST=true + fi + + # Set output variables to be used in the other jobs + echo "RUN_IDENTIFIER=${RUN_IDENTIFIER}" >> $GITHUB_OUTPUT + echo "TEST_AZURE_RESOURCE_GROUP=rg-${RUN_IDENTIFIER}" >> $GITHUB_OUTPUT + echo "TEST_EKS_CLUSTER_NAME=eks-${RUN_IDENTIFIER}" >> $GITHUB_OUTPUT + echo "RUN_TEST=${RUN_TEST}" >> $GITHUB_OUTPUT + - name: Checkout code + if: steps.gen-id.outputs.RUN_TEST == 'true' + uses: actions/checkout@v3 + - name: Ensure inputs.version is valid semver + if: steps.gen-id.outputs.RUN_TEST == 'true' && inputs.version != '' + run: | + python ./.github/scripts/validate_semver.py ${{ inputs.version }} + - name: Setup Node + if: steps.gen-id.outputs.RUN_TEST == 'true' + uses: actions/setup-node@v3 + with: + node-version: 20 + - name: az CLI login + if: steps.gen-id.outputs.RUN_TEST == 'true' + run: | + az login --service-principal \ + --username ${{ secrets.AZURE_SANDBOX_APP_ID }} \ + --password ${{ secrets.AZURE_SANDBOX_PASSWORD }} \ + --tenant ${{ secrets.AZURE_SANDBOX_TENANT_ID }} + - name: Configure AWS + if: steps.gen-id.outputs.RUN_TEST == 'true' && matrix.credential == 'aws' + run: | + aws configure set aws_access_key_id ${{ secrets.AWS_ACCESS_KEY_ID }} + aws configure set aws_secret_access_key ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws configure set region ${{ env.AWS_REGION }} + aws configure set output json + - name: Get kubeconf credential for AKS cluster + if: steps.gen-id.outputs.RUN_TEST == 'true' && matrix.name != 'eshop-aws' + run: | + az aks get-credentials \ + --subscription ${{ secrets.AZURE_SANDBOX_SUBSCRIPTION_ID }} \ + --resource-group ${{ env.AKS_RESOURCE_GROUP }} \ + --name ${{ env.AKS_CLUSTER_NAME }} + - name: Login to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + # Create and install test environment + - name: Create Azure resource group + if: steps.gen-id.outputs.RUN_TEST == 'true' && matrix.credential == 'azure' + id: create-azure-resource-group + run: | + current_time=$(date +%s) + az group create \ + --location ${{ env.AZURE_LOCATION }} \ + --name ${{ steps.gen-id.outputs.TEST_AZURE_RESOURCE_GROUP }} \ + --subscription ${{ secrets.AZURE_SANDBOX_SUBSCRIPTION_ID }} \ + --tags creationTime=$current_time + while [ $(az group exists --name ${{ steps.gen-id.outputs.TEST_AZURE_RESOURCE_GROUP }} --subscription ${{ secrets.AZURE_SANDBOX_SUBSCRIPTION_ID }}) = false ]; do + echo "Waiting for resource group ${{ steps.gen-id.outputs.TEST_AZURE_RESOURCE_GROUP }} to be created..." + sleep 5 + done + - name: Create EKS Cluster + if: steps.gen-id.outputs.RUN_TEST == 'true' && matrix.credential == 'aws' + id: create-eks + run: | + curl --silent --location "https://github.com/weaveworks/eksctl/releases/latest/download/eksctl_$(uname -s)_amd64.tar.gz" | tar xz -C /tmp + sudo mv /tmp/eksctl /usr/local/bin + eksctl create cluster \ + --name ${{ steps.gen-id.outputs.TEST_EKS_CLUSTER_NAME }} \ + --nodes-min 1 --nodes-max 2 --node-type t3.large \ + --zones ${{ env.AWS_ZONES }} \ + --managed \ + --region ${{ env.AWS_REGION }} + while [[ "$(eksctl get cluster ${{ steps.gen-id.outputs.TEST_EKS_CLUSTER_NAME }} --region ${{ env.AWS_REGION }} -o json | jq -r .[0].Status)" != "ACTIVE" ]]; do + echo "Waiting for EKS cluster to be created..." + sleep 60 + done + aws eks update-kubeconfig --region ${{ env.AWS_REGION }} --name ${{ steps.gen-id.outputs.TEST_EKS_CLUSTER_NAME }} + timeout-minutes: 60 + continue-on-error: false + - name: Download rad CLI + if: steps.gen-id.outputs.RUN_TEST == 'true' + run: | + RADIUS_VERSION="${{ inputs.version }}" + if [[ -z "${{ inputs.version }}" ]]; then + RADIUS_VERSION=edge + fi + ./.github/scripts/install-radius.sh $RADIUS_VERSION + ## This step is temporary until we have Recipe Packs for Azure & AWS and update the eShop sample + - name: Configure Radius test workspace + if: steps.gen-id.outputs.RUN_TEST == 'true' + run: | + set -x + + export PATH=$GITHUB_WORKSPACE/bin:$PATH + which rad || { echo "cannot find rad"; exit 1; } + + # Install Radius for AWS + if [[ "${{ matrix.credential }}" == "aws" ]]; then + rad install kubernetes + fi + + echo "*** Create workspace, group and environment for test ***" + rad workspace create kubernetes --force + rad group create ${{ matrix.name }} + rad group switch ${{ matrix.name }} + rad env create ${{ matrix.env }} + rad env switch ${{ matrix.env }} + + if [[ "${{ matrix.credential }}" == "azure" ]]; then + rad deploy ./samples/eshop/environments/azure.bicep -p azureResourceGroup=${{ steps.gen-id.outputs.TEST_AZURE_RESOURCE_GROUP }} -p azureSubscriptionId=${{ secrets.AZURE_SANDBOX_SUBSCRIPTION_ID }} + rad env switch ${{ matrix.env }} + elif [[ "${{ matrix.credential }}" == "aws" ]]; then + rad deploy ./samples/eshop/environments/aws.bicep -p awsAccountId=${{ secrets.AWS_ACCOUNT_ID }} -p awsRegion=${{ env.AWS_REGION }} -p eksClusterName=${{ steps.gen-id.outputs.TEST_EKS_CLUSTER_NAME }} + rad env switch ${{ matrix.env }} + else + echo "Registering recipes for ${{ matrix.env }} environment..." + rad recipe register default -e ${{ matrix.env }} --template-kind bicep --template-path ghcr.io/radius-project/recipes/local-dev/rediscaches:latest --resource-type Applications.Datastores/redisCaches + rad recipe register default -e ${{ matrix.env }} --template-kind bicep --template-path ghcr.io/radius-project/recipes/local-dev/mongodatabases:latest --resource-type Applications.Datastores/mongoDatabases + rad recipe register default -e ${{ matrix.env }} --template-kind bicep --template-path ghcr.io/radius-project/recipes/local-dev/sqldatabases:latest --resource-type Applications.Datastores/sqlDatabases + rad recipe register default -e ${{ matrix.env }} --template-kind bicep --template-path ghcr.io/radius-project/recipes/local-dev/rabbitmqqueues:latest --resource-type Applications.Messaging/rabbitMQQueues + fi + - name: Configure cloud credentials + if: steps.gen-id.outputs.RUN_TEST == 'true' && ( matrix.credential == 'azure' || matrix.credential == 'aws') + run: | + if [[ "${{ matrix.credential }}" == "azure" ]]; then + rad env update ${{ matrix.env }} --azure-subscription-id ${{ secrets.AZURE_SANDBOX_SUBSCRIPTION_ID }} --azure-resource-group ${{ steps.gen-id.outputs.TEST_AZURE_RESOURCE_GROUP }} + rad credential register azure --client-id ${{ secrets.AZURE_SANDBOX_APP_ID }} --client-secret ${{ secrets.AZURE_SANDBOX_PASSWORD }} --tenant-id ${{ secrets.AZURE_SANDBOX_TENANT_ID }} + fi + if [[ "${{ matrix.credential }}" == "aws" ]]; then + rad env update ${{ matrix.env }} --aws-region ${{ env.AWS_REGION }} --aws-account-id ${{ secrets.AWS_ACCOUNT_ID }} + rad credential register aws --access-key-id ${{ secrets.AWS_ACCESS_KEY_ID }} --secret-access-key ${{ secrets.AWS_SECRET_ACCESS_KEY }} + fi + # Deploy application and run tests + - name: Deploy app + if: steps.gen-id.outputs.RUN_TEST == 'true' + id: deploy-app + run: rad deploy ${{ matrix.path }} ${{ matrix.deployArgs }} -e ${{ matrix.env }} + - name: Wait for all pods to be ready + if: steps.gen-id.outputs.RUN_TEST == 'true' + id: wait-for-pods + run: | + namespace="${{ matrix.env }}-${{ matrix.app }}" + label="radapp.io/application=${{ matrix.app }}" + kubectl rollout status deployment -l $label -n $namespace --timeout=90s + - name: Run Playwright Test + if: steps.gen-id.outputs.RUN_TEST == 'true' && matrix.uiTestFile != '' + id: run-playwright-test + run: | + if [[ "${{ matrix.container }}" != "" ]]; then + rad resource expose containers ${{ matrix.container }} ${{ matrix.exposeArgs }} --port ${{ matrix.port }} & + export ENDPOINT="http://localhost:3000/" + else + endpoint="$(rad app status -a ${{ matrix.app }} | sed 's/ /\n/g' | grep http)" + echo "Endpoint: $endpoint" + export ENDPOINT=$endpoint + fi + + cd playwright/ + npm ci + npx playwright install --with-deps + npx playwright test ${{ matrix.uiTestFile }} --retries 3 + - name: Upload Playwright Results + uses: actions/upload-artifact@v3 + if: always() && ( steps.run-playwright-test.outcome == 'success' || steps.run-playwright-test.outcome == 'failure' ) + with: + name: playwright-report-${{ matrix.name }} + path: playwright/playwright-report/ + retention-days: 30 + if-no-files-found: error + # Handle failures + - name: Get Pod logs for failed tests + id: get-pod-logs + if: failure() && (steps.run-playwright-test.outcome == 'failure' || steps.wait-for-pods.outcome == 'failure' || steps.deploy-app.outcome == 'failure') + run: | + # Create pod-logs directory + mkdir -p playwright/pod-logs/${{ matrix.name }} + # Get pod logs and save to file + namespace="${{ matrix.env }}-${{ matrix.app }}" + label="radapp.io/application=${{ matrix.app }}" + pod_names=($(kubectl get pods -l $label -n $namespace -o jsonpath='{.items[*].metadata.name}')) + for pod_name in "${pod_names[@]}"; do + kubectl logs $pod_name -n $namespace > playwright/pod-logs/${{ matrix.name }}/${pod_name}.txt + done + echo "Pod logs saved to playwright/pod-logs/${{ matrix.name }}/" + # Get kubernetes events and save to file + kubectl get events -n $namespace > playwright/pod-logs/${{ matrix.name }}/events.txt + - name: Upload Pod logs for failed tests + uses: actions/upload-artifact@v3 + if: failure() && steps.get-pod-logs.outcome == 'success' + with: + name: ${{ matrix.name }}-pod-logs + path: playwright/pod-logs/${{ matrix.name }} + retention-days: 30 + if-no-files-found: error + - name: Create GitHub issue on failure + if: failure() && github.event_name == 'schedule' + run: gh issue create --title "Samples deployment failed for ${{ matrix.app }}" --body "Test failed on ${{ github.repository }}. See [workflow logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for more details." --repo ${{ github.repository }} --label test-failure + # Cleanup + - name: Delete app and environment + if: always() && steps.gen-id.outputs.RUN_TEST == 'true' + run: | + if command -v rad &> /dev/null; then + rad app delete ${{ matrix.app }} -y + rad env delete ${{ matrix.env }} -y + fi + - name: Delete Azure resource group + if: always() && steps.gen-id.outputs.RUN_TEST == 'true' && steps.create-azure-resource-group.outcome == 'success' + run: | + # Delete Azure resources created by the test + # if deletion fails, purge workflow will purge the resource group and its resources later + az group delete \ + --subscription ${{ secrets.AZURE_SANDBOX_SUBSCRIPTION_ID }} \ + --name ${{ steps.gen-id.outputs.TEST_AZURE_RESOURCE_GROUP }} \ + --yes + - name: Delete AWS Resources + if: always() && steps.gen-id.outputs.RUN_TEST == 'true' && matrix.credential == 'aws' + run: | + # Delete all AWS resources created by the test + ./.github/scripts/delete-aws-resources.sh '/planes/radius/local/resourcegroups/${{ matrix.env }}/providers/Applications.Core/applications/${{ matrix.app }}' + - name: Delete EKS Cluster + if: always() && steps.gen-id.outputs.RUN_TEST == 'true' && matrix.credential == 'aws' + run: | + # Delete EKS cluster + echo "Deleting EKS cluster: ${{ steps.gen-id.outputs.TEST_EKS_CLUSTER_NAME }}" + eksctl delete cluster --name ${{ steps.gen-id.outputs.TEST_EKS_CLUSTER_NAME }} --region ${{ env.AWS_REGION }} --wait --force diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 0bee5a8a..043c33b1 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -1,12 +1,12 @@ -name: Test Samples +name: Test Samples (k3d and EKS) on: workflow_dispatch: inputs: version: - description: 'Radius version number to use (e.g. 0.1.0, 0.1.0-rc1, edge). Defaults to edge.' + description: "Radius version number to use (e.g. 0.1.0, 0.1.0-rc1, edge). Defaults to edge." required: false - default: 'edge' + default: "edge" type: string push: branches: @@ -20,8 +20,8 @@ on: branches: - v*.* - edge - schedule: # 7:45 AM Pacific Time - - cron: "45 15 * * *" + schedule: # Run every 2 hours + - cron: "0 */2 * * *" env: RUN_IDENTIFIER: samplestest-${{ github.run_id }}-${{ github.run_attempt }} jobs: @@ -75,7 +75,7 @@ jobs: uiTestFile: tests/eshop/eshop.app.spec.ts enableDapr: false - name: eshop-azure - os: ubuntu-latest + os: ubuntu-latest-m runOnPullRequest: false app: eshop env: azure @@ -84,7 +84,7 @@ jobs: credential: azure enableDapr: false - name: eshop-aws - os: ubuntu-latest + os: ubuntu-latest-m runOnPullRequest: false app: eshop env: aws @@ -134,7 +134,7 @@ jobs: if: steps.gen-id.outputs.RUN_TEST == 'true' uses: actions/setup-node@v3 with: - node-version: 16 + node-version: 20 - name: az CLI login if: steps.gen-id.outputs.RUN_TEST == 'true' && matrix.credential == 'azure' run: | @@ -214,7 +214,7 @@ jobs: run: | RADIUS_VERSION="${{ inputs.version }}" if [[ -z "${{ inputs.version }}" ]]; then - RADIUS_VERSION=edge + RADIUS_VERSION=edge fi ./.github/scripts/install-radius.sh $RADIUS_VERSION - name: Initialize default environment @@ -258,6 +258,7 @@ jobs: # Deploy application and run tests - name: Deploy app if: steps.gen-id.outputs.RUN_TEST == 'true' + id: deploy-app run: rad deploy ${{ matrix.path }} ${{ matrix.deployArgs }} - name: Wait for all pods to be ready if: steps.gen-id.outputs.RUN_TEST == 'true' @@ -272,6 +273,8 @@ jobs: run: | if [[ "${{ matrix.container }}" != "" ]]; then rad resource expose containers ${{ matrix.container }} ${{ matrix.exposeArgs }} --port ${{ matrix.port }} & + echo "Endpoint: http://localhost:${{ matrix.port }}" + export ENDPOINT="http://localhost:${{ matrix.port }}" else endpoint="$(rad app status -a ${{ matrix.app }} | sed 's/ /\n/g' | grep http)" echo "Endpoint: $endpoint" @@ -292,12 +295,12 @@ jobs: # Handle failures - name: Get Pod logs for failed tests id: get-pod-logs - if: failure() && (steps.run-playwright-test.outcome == 'failure' || steps.wait-for-pods.outcome == 'failure') + if: failure() && (steps.run-playwright-test.outcome == 'failure' || steps.wait-for-pods.outcome == 'failure' || steps.deploy-app.outcome == 'failure') run: | # Create pod-logs directory mkdir -p playwright/pod-logs/${{ matrix.name }} # Get pod logs and save to file - namespace="default-${{ matrix.app }}" + namespace="${{ matrix.env }}-${{ matrix.app }}" label="radapp.io/application=${{ matrix.app }}" pod_names=($(kubectl get pods -l $label -n $namespace -o jsonpath='{.items[*].metadata.name}')) for pod_name in "${pod_names[@]}"; do diff --git a/playwright/package-lock.json b/playwright/package-lock.json index 0cd663b4..062e31c2 100644 --- a/playwright/package-lock.json +++ b/playwright/package-lock.json @@ -9,44 +9,43 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "uuid": "^9.0.0" + "uuid": "^9.0.1" }, "devDependencies": { - "@playwright/test": "^1.35.0", - "@types/node": "^20.6.0", - "@types/uuid": "^9.0.3", - "typescript": "^5.2.2" + "@playwright/test": "^1.43.0", + "@types/node": "^20.12.6", + "@types/uuid": "^9.0.8", + "typescript": "^5.4.4" } }, "node_modules/@playwright/test": { - "version": "1.35.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.35.0.tgz", - "integrity": "sha512-6qXdd5edCBynOwsz1YcNfgX8tNWeuS9fxy5o59D0rvHXxRtjXRebB4gE4vFVfEMXl/z8zTnAzfOs7aQDEs8G4Q==", + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.43.0.tgz", + "integrity": "sha512-Ebw0+MCqoYflop7wVKj711ccbNlrwTBCtjY5rlbiY9kHL2bCYxq+qltK6uPsVBGGAOb033H2VO0YobcQVxoW7Q==", "dev": true, "dependencies": { - "@types/node": "*", - "playwright-core": "1.35.0" + "playwright": "1.43.0" }, "bin": { "playwright": "cli.js" }, "engines": { "node": ">=16" - }, - "optionalDependencies": { - "fsevents": "2.3.2" } }, "node_modules/@types/node": { - "version": "20.6.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.6.0.tgz", - "integrity": "sha512-najjVq5KN2vsH2U/xyh2opaSEz6cZMR2SetLIlxlj08nOcmPOemJmUK2o4kUzfLqfrWE0PIrNeE16XhYDd3nqg==", - "dev": true + "version": "20.12.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.6.tgz", + "integrity": "sha512-3KurE8taB8GCvZBPngVbp0lk5CKi8M9f9k1rsADh0Evdz5SzJ+Q+Hx9uHoFGsLnLnd1xmkDQr2hVhlA0Mn0lKQ==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } }, "node_modules/@types/uuid": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.3.tgz", - "integrity": "sha512-taHQQH/3ZyI3zP8M/puluDEIEvtQHVYcC6y3N8ijFtAd28+Ey/G4sg1u2gB01S8MwybLOKAp9/yCMu/uR5l3Ug==", + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", "dev": true }, "node_modules/fsevents": { @@ -63,10 +62,28 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/playwright": { + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.43.0.tgz", + "integrity": "sha512-SiOKHbVjTSf6wHuGCbqrEyzlm6qvXcv7mENP+OZon1I07brfZLGdfWV0l/efAzVx7TF3Z45ov1gPEkku9q25YQ==", + "dev": true, + "dependencies": { + "playwright-core": "1.43.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, "node_modules/playwright-core": { - "version": "1.35.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.35.0.tgz", - "integrity": "sha512-muMXyPmIx/2DPrCHOD1H1ePT01o7OdKxKj2ebmCAYvqhUy+Y1bpal7B0rdoxros7YrXI294JT/DWw2LqyiqTPA==", + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.43.0.tgz", + "integrity": "sha512-iWFjyBUH97+pUFiyTqSLd8cDMMOS0r2ZYz2qEsPjH8/bX++sbIJT35MSwKnp1r/OQBAqC5XO99xFbJ9XClhf4w==", "dev": true, "bin": { "playwright-core": "cli.js" @@ -76,9 +93,9 @@ } }, "node_modules/typescript": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", - "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.4.tgz", + "integrity": "sha512-dGE2Vv8cpVvw28v8HCPqyb08EzbBURxDpuhJvTrusShUfGnhHBafDsLdS1EhhxyL6BJQE+2cT3dDPAv+MQ6oLw==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -88,10 +105,20 @@ "node": ">=14.17" } }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, "node_modules/uuid": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", - "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], "bin": { "uuid": "dist/bin/uuid" } diff --git a/playwright/package.json b/playwright/package.json index 1a52aee0..f29f910b 100644 --- a/playwright/package.json +++ b/playwright/package.json @@ -8,12 +8,12 @@ "author": "", "license": "ISC", "devDependencies": { - "@playwright/test": "^1.35.0", - "@types/node": "^20.6.0", - "@types/uuid": "^9.0.3", - "typescript": "^5.2.2" + "@playwright/test": "^1.43.0", + "@types/node": "^20.12.6", + "@types/uuid": "^9.0.8", + "typescript": "^5.4.4" }, "dependencies": { - "uuid": "^9.0.0" + "uuid": "^9.0.1" } } diff --git a/playwright/playwright.config.ts b/playwright/playwright.config.ts index e2b6772b..4e300a13 100644 --- a/playwright/playwright.config.ts +++ b/playwright/playwright.config.ts @@ -1,4 +1,4 @@ -import { defineConfig, devices } from '@playwright/test'; +import { defineConfig, devices } from "@playwright/test"; /** * Read environment variables from file. @@ -10,7 +10,7 @@ import { defineConfig, devices } from '@playwright/test'; * See https://playwright.dev/docs/test-configuration. */ export default defineConfig({ - testDir: './tests', + testDir: "./tests", /* Run tests in files in parallel */ fullyParallel: true, /* Fail the build on CI if you accidentally left test.only in the source code. */ @@ -20,31 +20,34 @@ export default defineConfig({ /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 1 : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: 'html', + reporter: "html", /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. */ // baseURL: 'http://127.0.0.1:3000', /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: 'on-first-retry', + trace: "on-first-retry", }, - /* Configure projects for major browsers */ projects: [ { - name: 'chromium', - use: { ...devices['Desktop Chrome'] }, + name: "chromium", + use: { ...devices["Desktop Chrome"] }, }, { - name: 'firefox', - use: { ...devices['Desktop Firefox'] }, + name: "firefox", + use: { ...devices["Desktop Firefox"] }, }, { - name: 'webkit', - use: { ...devices['Desktop Safari'] }, + name: "webkit", + use: { ...devices["Desktop Safari"] }, }, ], + timeout: 1 * 60 * 1000, + expect: { + timeout: 30 * 1000, + }, }); diff --git a/playwright/tests/demo/demo.app.spec.ts b/playwright/tests/demo/demo.app.spec.ts index 1d69fa67..d8751c71 100644 --- a/playwright/tests/demo/demo.app.spec.ts +++ b/playwright/tests/demo/demo.app.spec.ts @@ -3,82 +3,106 @@ import { v4 as uuidv4 } from "uuid"; test("To-Do App Basic UI Checks", async ({ page }) => { // Listen for all console events and handle errors - page.on('console', msg => { - if (msg.type() === 'error') { + page.on("console", (msg) => { + if (msg.type() === "error") { console.log(`Error text: "${msg.text()}"`); } }); - // Go to http://localhost:3000/ - await page.goto("http://localhost:3000/"); + let endpoint = process.env.ENDPOINT; + expect(endpoint).toBeDefined(); + + // Remove quotes from the endpoint if they exist + endpoint = (endpoint as string).replace(/['"]+/g, ""); + console.log(`Endpoint: ${endpoint}`); + await page.goto(endpoint); // Expect page to have proper URL - expect(page).toHaveURL("http://localhost:3000/"); + expect(page).toHaveURL(endpoint); // Expect page to have proper title expect(page).toHaveTitle("Radius Demo"); // Make sure the tabs are visible - await expect(page.getByRole("link", { name: "Radius Demo" })) - .toBeVisible(); - await expect(page.getByRole("link", { name: "Container Info" })) - .toBeVisible(); - await expect(page.getByRole("link", { name: "Todo List" })) - .toBeVisible(); + await expect(page.getByRole("link", { name: "Radius Demo" })).toBeVisible(); + await expect( + page.getByRole("link", { name: "Container Info" }) + ).toBeVisible(); + await expect(page.getByRole("link", { name: "Todo List" })).toBeVisible(); // Make sure the tabs are clickable - await page.getByRole("link", { name: "Radius Demo" }) - .click(); - await page.getByRole("link", { name: "Container Info" }) - .click(); - await page.getByRole("link", { name: "Todo List" }) - .click(); + await page.getByRole("link", { name: "Radius Demo" }).click(); + await page.getByRole("link", { name: "Container Info" }).click(); + await page.getByRole("link", { name: "Todo List" }).click(); // Go back to Main Page - await page.getByRole("link", { name: "Radius Demo" }) - .click(); + await page.getByRole("link", { name: "Radius Demo" }).click(); - await page.getByRole('button', { name: '📄 Environment variables' }).click(); + await page.getByRole("button", { name: "📄 Environment variables" }).click(); // Make sure important environment variables are visible - await expect(page.getByRole('cell', { name: 'CONNECTION_REDIS_CONNECTIONSTRING' }).getByText('CONNECTION_REDIS_CONNECTIONSTRING')).toBeVisible(); - await expect(page.getByRole('cell', { name: 'CONNECTION_REDIS_HOST' }).getByText('CONNECTION_REDIS_HOST')).toBeVisible(); - await expect(page.getByRole('cell', { name: 'CONNECTION_REDIS_PORT' }).getByText('CONNECTION_REDIS_PORT')).toBeVisible(); + await expect( + page + .getByRole("cell", { name: "CONNECTION_REDIS_CONNECTIONSTRING" }) + .getByText("CONNECTION_REDIS_CONNECTIONSTRING") + ).toBeVisible(); + await expect( + page + .getByRole("cell", { name: "CONNECTION_REDIS_HOST" }) + .getByText("CONNECTION_REDIS_HOST") + ).toBeVisible(); + await expect( + page + .getByRole("cell", { name: "CONNECTION_REDIS_PORT" }) + .getByText("CONNECTION_REDIS_PORT") + ).toBeVisible(); }); test("Add an item and check basic functionality", async ({ page }) => { - await page.goto("http://localhost:3000/"); + // Listen for all console events and handle errors + page.on("console", (msg) => { + if (msg.type() === "error") { + console.log(`Error text: "${msg.text()}"`); + } + }); + + let endpoint = process.env.ENDPOINT; + expect(endpoint).toBeDefined(); + + // Remove quotes from the endpoint if they exist + endpoint = (endpoint as string).replace(/['"]+/g, ""); + console.log(`Endpoint: ${endpoint}`); + await page.goto(endpoint); + // Go to Todo List Page await page.getByRole("link", { name: "Todo List" }).click(); // Make sure the input is visible - await expect(page.getByPlaceholder("What do you need to do?")) - .toBeVisible(); + await expect(page.getByPlaceholder("What do you need to do?")).toBeVisible(); // Add a todo item const todoItem = `Test Item ${uuidv4()}`; - await page.getByPlaceholder("What do you need to do?") - .fill(todoItem); - await page.getByRole("button", { name: "Add send" }) - .click(); + await page.getByPlaceholder("What do you need to do?").fill(todoItem); + await page.getByRole("button", { name: "Add send" }).click(); // Make sure the todo item is visible now - await expect(page.getByRole("cell", { name: todoItem })) - .toBeVisible(); + await expect(page.getByRole("cell", { name: todoItem })).toBeVisible(); // Complete a todo item // At first we expect a lightbulb icon in the status column for the given todo item - await expect(page.getByRole("row", { name: `${todoItem} lightbulb` })) - .toBeVisible(); + await expect( + page.getByRole("row", { name: `${todoItem} lightbulb` }) + ).toBeVisible(); // Then we expect to have a Complete button for the given todo item - await expect(page - .getByRole("row", { - name: `${todoItem} lightbulb Complete done Delete delete`, - }) - .getByRole("button", { name: "Complete done" })) - .toBeVisible(); + await expect( + page + .getByRole("row", { + name: `${todoItem} lightbulb Complete done Delete delete`, + }) + .getByRole("button", { name: "Complete done" }) + ).toBeVisible(); // We click the Complete button await page @@ -89,12 +113,13 @@ test("Add an item and check basic functionality", async ({ page }) => { .click(); // Now we expect a checkmark icon represented as `done` in the status column for the given todo item - await expect(page - .getByRole("row", { - name: `${todoItem} done Complete done Delete delete`, - }) - .getByRole("button", { name: "Complete done" })) - .toBeVisible(); + await expect( + page + .getByRole("row", { + name: `${todoItem} done Complete done Delete delete`, + }) + .getByRole("button", { name: "Complete done" }) + ).toBeVisible(); // Delete a todo item await page @@ -105,6 +130,5 @@ test("Add an item and check basic functionality", async ({ page }) => { .click(); // Make sure the todo item is not visible now - await expect(page.getByRole("cell", { name: todoItem })) - .not.toBeVisible(); + await expect(page.getByRole("cell", { name: todoItem })).not.toBeVisible(); }); diff --git a/playwright/tests/eshop/eshop.app.spec.ts b/playwright/tests/eshop/eshop.app.spec.ts index 0b67ab26..8af8ffe8 100644 --- a/playwright/tests/eshop/eshop.app.spec.ts +++ b/playwright/tests/eshop/eshop.app.spec.ts @@ -1,103 +1,92 @@ import { test, expect } from "@playwright/test"; -test("eShop on Containers App Basic UI and Functionality Checks", async ({ page }) => { +test("eShop on Containers App Basic UI and Functionality Checks", async ({ + page, +}) => { // Listen for all console events and handle errors - page.on('console', msg => { - if (msg.type() === 'error') { + page.on("console", (msg) => { + if (msg.type() === "error") { console.log(`Error text: "${msg.text()}"`); } }); - let endpoint = process.env.ENDPOINT - expect(endpoint).toBeDefined() - + let endpoint = process.env.ENDPOINT; + expect(endpoint).toBeDefined(); + // Remove quotes from the endpoint if they exist - endpoint = (endpoint as string).replace(/['"]+/g, '') - + endpoint = (endpoint as string).replace(/['"]+/g, ""); + console.log(`Endpoint: ${endpoint}`); await page.goto(endpoint); // Expect page to have proper URL - expect(page).toHaveURL(endpoint+"/catalog"); - + await expect(page).toHaveURL(new RegExp(`${endpoint}/catalog.*`)); // Expect page to have proper title - expect(page).toHaveTitle("eShopOnContainers - SPA"); + await expect(page).toHaveTitle("eShopOnContainers - SPA"); - // Check for the LOGIN button - await expect(page.getByText("LOGIN")) - .toBeVisible(); - - // Click on the LOGIN button - await page.getByText("LOGIN").click(); + // Check for the LOGIN button in the home page + const loginButton = page.getByText("LOGIN"); + await expect(loginButton).toBeVisible(); + await loginButton.click(); - // Expect page to have proper title - expect(page).toHaveTitle("eShopOnContainers - Identity"); + // Expect login page to have proper title + await expect(page).toHaveTitle("eShopOnContainers - Identity"); // Fill in the username and password - expect(page.getByPlaceholder('Username')) - .toBeVisible(); - await page.getByPlaceholder('Username') - .click(); - await page.getByPlaceholder('Username') - .fill('alice'); - - expect(page.getByPlaceholder('Password')) - .toBeVisible(); - await page.getByPlaceholder('Password') - .click(); - await page.getByPlaceholder('Password') - .fill('Pass123$'); + const username = page.getByPlaceholder("Username"); + await expect(username).toBeVisible(); + await username.click(); + await username.fill("alice"); + + const password = page.getByPlaceholder("Password"); + await expect(password).toBeVisible(); + await password.click(); + await password.fill("Pass123$"); // Click on the LOGIN button - await page.getByRole('button', { name: 'Login' }) - .click(); + await page.getByRole("button", { name: "Login" }).click(); // After login, expect to be redirected to the catalog page // Expect page to have proper URL - expect(page).toHaveURL(endpoint+"/catalog"); - + await expect(page).toHaveURL(new RegExp(`${endpoint}/catalog.*`)); // Expect page to have proper title - expect(page).toHaveTitle("eShopOnContainers - SPA"); + await expect(page).toHaveTitle("eShopOnContainers - SPA"); // Logged user details should be visible - expect(page.getByText('AliceSmith@email.com')) - .toBeVisible(); - + const user = page.getByText("AliceSmith@email.com"); + await expect(user).toBeVisible(); // Click on the user details - await page.getByText('AliceSmith@email.com').click(); + await user.click(); // Check dropdown menu - expect(page.getByText('My orders')) - .toBeVisible(); - expect(page.getByText('Log Out')) - .toBeVisible(); + await expect(page.getByText("My orders")).toBeVisible(); + await expect(page.getByText("Log Out")).toBeVisible(); let numberOfItemsAdded = 0; // Add an item to the cart - await page.locator('div:nth-child(2) > .esh-catalog-item > .esh-catalog-thumbnail-wrapper > .esh-catalog-thumbnail-icon > .esh-catalog-thumbnail-icon-svg') - .click(); + const firstItem = page.locator("div:nth-child(1) > .esh-catalog-item"); + await expect(firstItem).toBeVisible(); + await firstItem.click(); + numberOfItemsAdded++; + + // Add an item to the cart + const secondItem = page.locator("div:nth-child(2) > .esh-catalog-item"); + await expect(secondItem).toBeVisible(); + await secondItem.click(); numberOfItemsAdded++; // Go to the cart - await page.getByRole('link', { name: `${numberOfItemsAdded}` }) - .click(); + const cartLink = page.getByRole("link", { name: `${numberOfItemsAdded}` }); + await expect(cartLink).toBeVisible(); + await cartLink.click(); // Expect page to have proper URL - expect(page).toHaveURL(endpoint+"/basket"); - + await expect(page).toHaveURL(new RegExp(`${endpoint}/basket.*`)); // Checkout - await page.getByRole('button', { name: 'Checkout' }) - .click(); - + await page.getByRole("button", { name: "Checkout" }).click(); // Place the order - await page.getByRole('button', { name: 'Place Order' }) - .click(); - + await page.getByRole("button", { name: "Place Order" }).click(); // Continue Shopping - await page.getByRole('link', { name: 'Continue Shopping' }) - .click(); - + await page.getByRole("link", { name: "Continue Shopping" }).click(); // Logout - await page.locator('div').filter({ hasText: 'Log Out' }) - .nth(0) - .click(); + await page.locator("div").filter({ hasText: "Log Out" }).nth(0).click(); }); diff --git a/samples/eshop/eshop.bicep b/samples/eshop/eshop.bicep index c7a47427..ee77d102 100644 --- a/samples/eshop/eshop.bicep +++ b/samples/eshop/eshop.bicep @@ -5,6 +5,9 @@ import radius as rad @description('Radius environment ID. Set automatically by Radius') param environment string +@description('Application name. Defaults to "eshop"') +param applicationName string = 'eshop' + @description('Container registry to pull from, with optional path. Defaults to "ghcr.io/radius-project/samples/eshop"') param imageRegistry string = 'ghcr.io/radius-project/samples/eshop' @@ -26,7 +29,7 @@ var AZURESERVICEBUSENABLED = contains(eshopEnvironment.properties.recipes, 'Appl // Application -------------------------------------------------------- resource eshopApplication 'Applications.Core/applications@2023-10-01-preview' = { - name: 'eshop' + name: applicationName properties: { environment: environment } @@ -120,7 +123,7 @@ module payment 'services/payment.bicep' = { module seq 'services/seq.bicep' = { name: 'seq' params: { - application: eshopApplication.id + application: eshopApplication.id } }