From 796a06cf277ca6dfef506fd475e095644610d68d Mon Sep 17 00:00:00 2001 From: Yulia <40775064+yuliasantos@users.noreply.github.com> Date: Sat, 12 Aug 2023 23:11:32 +0200 Subject: [PATCH] Add end-to-end tests using playwright (#257) Add tests with playwright for: - add peer modal on first access - add peer modal on empty peer list - test install buttons and instructions for Linux, Docker, macOS, Windows and Android - check default ACL The tests are using a modified version of the getting started scripts to run a local environment of management services and run the dashboard from the current version Todo: - run tests before create docker container - add more tests --- .github/workflows/build_and_push.yml | 2 +- .github/workflows/e2e-tests.yml | 42 ++ .gitignore | 12 + e2e-tests/clean-test-env.sh | 3 + e2e-tests/create-test-env.sh | 697 ++++++++++++++++++ e2e-tests/pages/modals/add-peer-modal.ts | 111 +++ e2e-tests/pages/peers-page.ts | 15 + e2e-tests/pages/top-menu.ts | 15 + e2e-tests/tests/access-control.test.ts | 33 + e2e-tests/tests/peers.test.ts | 56 ++ package-lock.json | 49 ++ package.json | 3 +- playwright.config.ts | 80 ++ src/components/Navbar.tsx | 12 +- .../popups/addpeer/addpeer/AddPeerPopup.tsx | 10 +- .../popups/addpeer/addpeer/AndroidTab.tsx | 2 +- .../popups/addpeer/addpeer/DockerTab.tsx | 1 + .../popups/addpeer/addpeer/MacTab.tsx | 2 + .../popups/addpeer/addpeer/WindowsTab.tsx | 2 +- src/views/AccessControl.tsx | 2 +- src/views/Peers.tsx | 2 + 21 files changed, 1135 insertions(+), 16 deletions(-) create mode 100644 .github/workflows/e2e-tests.yml create mode 100644 e2e-tests/clean-test-env.sh create mode 100644 e2e-tests/create-test-env.sh create mode 100644 e2e-tests/pages/modals/add-peer-modal.ts create mode 100644 e2e-tests/pages/peers-page.ts create mode 100644 e2e-tests/pages/top-menu.ts create mode 100644 e2e-tests/tests/access-control.test.ts create mode 100644 e2e-tests/tests/peers.test.ts create mode 100644 playwright.config.ts diff --git a/.github/workflows/build_and_push.yml b/.github/workflows/build_and_push.yml index 2f18f40..50528f5 100644 --- a/.github/workflows/build_and_push.yml +++ b/.github/workflows/build_and_push.yml @@ -19,7 +19,7 @@ jobs: node-version: '16' cache: 'npm' - - name: Install dependecies + - name: Install dependencies run: npm install - name: Build diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml new file mode 100644 index 0000000..8058a47 --- /dev/null +++ b/.github/workflows/e2e-tests.yml @@ -0,0 +1,42 @@ +name: run e2e tests +on: + push: + branches: + - main + pull_request: + +jobs: + e2e_tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: setup-node + uses: actions/setup-node@v3 + with: + node-version: '16' + cache: 'npm' + + - name: Install dependencies + run: npm install + + - name: install playwright + run: npx playwright install + + - name: install playwright deps + run: npx playwright install-deps + + - name: create test environment + run: bash ./e2e-tests/create-test-env.sh + + - name: run e2e tests + run: npx playwright test --workers 2 + + - uses: actions/upload-artifact@v3 + if: always() + with: + name: playwright-report + path: | + playwright-report/ + test-results/ + retention-days: 3 diff --git a/.gitignore b/.gitignore index 362eed3..11d58b4 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,15 @@ src/auth_config.json src/.local-config*.json /public/OidcServiceWorker.js /public/OidcTrustedDomains.js +/e2e-tests/node_modules/ +/e2e-tests/playwright-report/ +/e2e-tests/test-results/ +/test-results/ +/playwright-report/ +.env +Caddyfile +docker-compose.yml +machinekey/ +management.json +turnserver.conf +zitadel.env diff --git a/e2e-tests/clean-test-env.sh b/e2e-tests/clean-test-env.sh new file mode 100644 index 0000000..013e15a --- /dev/null +++ b/e2e-tests/clean-test-env.sh @@ -0,0 +1,3 @@ +#!/bin/bash +docker-compose down --volumes +rm -f docker-compose.yml Caddyfile zitadel.env dashboard.env machinekey/zitadel-admin-sa.token turnserver.conf management.json \ No newline at end of file diff --git a/e2e-tests/create-test-env.sh b/e2e-tests/create-test-env.sh new file mode 100644 index 0000000..6f22f68 --- /dev/null +++ b/e2e-tests/create-test-env.sh @@ -0,0 +1,697 @@ +#!/bin/bash + +set -e + +handle_request_command_status() { + PARSED_RESPONSE=$1 + FUNCTION_NAME=$2 + RESPONSE=$3 + if [[ $PARSED_RESPONSE -ne 0 ]]; then + echo "ERROR calling $FUNCTION_NAME:" $(echo "$RESPONSE" | jq -r '.message') > /dev/stderr + exit 1 + fi +} + +handle_zitadel_request_response() { + PARSED_RESPONSE=$1 + FUNCTION_NAME=$2 + RESPONSE=$3 + if [[ $PARSED_RESPONSE == "null" ]]; then + echo "ERROR calling $FUNCTION_NAME:" $(echo "$RESPONSE" | jq -r '.message') > /dev/stderr + exit 1 + fi + sleep 1 +} + +check_docker_compose() { + if command -v docker-compose &> /dev/null + then + echo "docker-compose" + return + fi + if docker compose --help &> /dev/null + then + echo "docker compose" + return + fi + + echo "docker-compose is not installed or not in PATH. Please follow the steps from the official guide: https://docs.docker.com/engine/install/" > /dev/stderr + exit 1 +} + +check_jq() { + if ! command -v jq &> /dev/null + then + echo "jq is not installed or not in PATH, please install with your package manager. e.g. sudo apt install jq" > /dev/stderr + exit 1 + fi +} + +wait_crdb() { + set +e + while true; do + if $DOCKER_COMPOSE_COMMAND exec -T crdb curl -sf -o /dev/null 'http://localhost:8080/health?ready=1'; then + break + fi + echo -n " ." + sleep 5 + done + echo " done" + set -e +} + +init_crdb() { + echo -e "\nInitializing Zitadel's CockroachDB\n\n" + $DOCKER_COMPOSE_COMMAND up -d crdb + echo "" + # shellcheck disable=SC2028 + echo -n "Waiting cockroachDB to become ready " + wait_crdb + $DOCKER_COMPOSE_COMMAND exec -T crdb /bin/bash -c "cp /cockroach/certs/* /zitadel-certs/ && cockroach cert create-client --overwrite --certs-dir /zitadel-certs/ --ca-key /zitadel-certs/ca.key zitadel_user && chown -R 1000:1000 /zitadel-certs/" + handle_request_command_status $? "init_crdb failed" "" +} + +get_main_ip_address() { + if [[ "$OSTYPE" == "darwin"* ]]; then + interface=$(route -n get default | grep 'interface:' | awk '{print $2}') + ip_address=$(ifconfig "$interface" | grep 'inet ' | awk '{print $2}') + else + interface=$(ip route | grep default | awk '{print $5}' | head -n 1) + ip_address=$(ip addr show "$interface" | grep 'inet ' | awk '{print $2}' | cut -d'/' -f1) + fi + + echo "$ip_address" +} + +wait_pat() { + PAT_PATH=$1 + set +e + while true; do + if [[ -f "$PAT_PATH" ]]; then + break + fi + echo -n " ." + sleep 1 + done + echo " done" + set -e +} + +wait_api() { + INSTANCE_URL=$1 + PAT=$2 + set +e + while true; do + curl -s --fail -o /dev/null "$INSTANCE_URL/auth/v1/users/me" -H "Authorization: Bearer $PAT" + if [[ $? -eq 0 ]]; then + break + fi + echo -n " ." + sleep 1 + done + echo " done" + set -e +} + +create_new_project() { + INSTANCE_URL=$1 + PAT=$2 + PROJECT_NAME="NETBIRD" + + RESPONSE=$( + curl -sS -X POST "$INSTANCE_URL/management/v1/projects" \ + -H "Authorization: Bearer $PAT" \ + -H "Content-Type: application/json" \ + -d '{"name": "'"$PROJECT_NAME"'"}' + ) + PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.id') + handle_zitadel_request_response "$PARSED_RESPONSE" "create_new_project" "$RESPONSE" + echo "$PARSED_RESPONSE" +} + +create_new_application() { + INSTANCE_URL=$1 + PAT=$2 + APPLICATION_NAME=$3 + BASE_REDIRECT_URL1=$4 + BASE_REDIRECT_URL2=$5 + LOGOUT_URL=$6 + ZITADEL_DEV_MODE=$7 + + RESPONSE=$( + curl -sS -X POST "$INSTANCE_URL/management/v1/projects/$PROJECT_ID/apps/oidc" \ + -H "Authorization: Bearer $PAT" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "'"$APPLICATION_NAME"'", + "redirectUris": [ + "'"$BASE_REDIRECT_URL1"'", + "'"$BASE_REDIRECT_URL2"'" + ], + "postLogoutRedirectUris": [ + "'"$LOGOUT_URL"'" + ], + "RESPONSETypes": [ + "OIDC_RESPONSE_TYPE_CODE" + ], + "grantTypes": [ + "OIDC_GRANT_TYPE_AUTHORIZATION_CODE", + "OIDC_GRANT_TYPE_REFRESH_TOKEN" + ], + "appType": "OIDC_APP_TYPE_USER_AGENT", + "authMethodType": "OIDC_AUTH_METHOD_TYPE_NONE", + "version": "OIDC_VERSION_1_0", + "devMode": '"$ZITADEL_DEV_MODE"', + "accessTokenType": "OIDC_TOKEN_TYPE_JWT", + "accessTokenRoleAssertion": true, + "skipNativeAppSuccessPage": true + }' + ) + + PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.clientId') + handle_zitadel_request_response "$PARSED_RESPONSE" "create_new_application" "$RESPONSE" + echo "$PARSED_RESPONSE" +} + +create_service_user() { + INSTANCE_URL=$1 + PAT=$2 + + RESPONSE=$( + curl -sS -X POST "$INSTANCE_URL/management/v1/users/machine" \ + -H "Authorization: Bearer $PAT" \ + -H "Content-Type: application/json" \ + -d '{ + "userName": "netbird-service-account", + "name": "Netbird Service Account", + "description": "Netbird Service Account for IDP management", + "accessTokenType": "ACCESS_TOKEN_TYPE_JWT" + }' + ) + PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.userId') + handle_zitadel_request_response "$PARSED_RESPONSE" "create_service_user" "$RESPONSE" + echo "$PARSED_RESPONSE" +} + +create_service_user_secret() { + INSTANCE_URL=$1 + PAT=$2 + USER_ID=$3 + + RESPONSE=$( + curl -sS -X PUT "$INSTANCE_URL/management/v1/users/$USER_ID/secret" \ + -H "Authorization: Bearer $PAT" \ + -H "Content-Type: application/json" \ + -d '{}' + ) + SERVICE_USER_CLIENT_ID=$(echo "$RESPONSE" | jq -r '.clientId') + handle_zitadel_request_response "$SERVICE_USER_CLIENT_ID" "create_service_user_secret_id" "$RESPONSE" + SERVICE_USER_CLIENT_SECRET=$(echo "$RESPONSE" | jq -r '.clientSecret') + handle_zitadel_request_response "$SERVICE_USER_CLIENT_SECRET" "create_service_user_secret" "$RESPONSE" +} + +add_organization_user_manager() { + INSTANCE_URL=$1 + PAT=$2 + USER_ID=$3 + + RESPONSE=$( + curl -sS -X POST "$INSTANCE_URL/management/v1/orgs/me/members" \ + -H "Authorization: Bearer $PAT" \ + -H "Content-Type: application/json" \ + -d '{ + "userId": "'"$USER_ID"'", + "roles": [ + "ORG_USER_MANAGER" + ] + }' + ) + PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.details.creationDate') + handle_zitadel_request_response "$PARSED_RESPONSE" "add_organization_user_manager" "$RESPONSE" + echo "$PARSED_RESPONSE" +} + +create_admin_user() { + INSTANCE_URL=$1 + PAT=$2 + USERNAME=$3 + PASSWORD=$4 + RESPONSE=$( + curl -sS -X POST "$INSTANCE_URL/management/v1/users/human/_import" \ + -H "Authorization: Bearer $PAT" \ + -H "Content-Type: application/json" \ + -d '{ + "userName": "'"$USERNAME"'", + "profile": { + "firstName": "Zitadel", + "lastName": "Admin" + }, + "email": { + "email": "'"$USERNAME"'", + "isEmailVerified": true + }, + "password": "'"$PASSWORD"'", + "passwordChangeRequired": false + }' + ) + PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.userId') + handle_zitadel_request_response "$PARSED_RESPONSE" "create_admin_user" "$RESPONSE" + echo "$PARSED_RESPONSE" +} + +add_instance_admin() { + INSTANCE_URL=$1 + PAT=$2 + USER_ID=$3 + + RESPONSE=$( + curl -sS -X POST "$INSTANCE_URL/admin/v1/members" \ + -H "Authorization: Bearer $PAT" \ + -H "Content-Type: application/json" \ + -d '{ + "userId": "'"$USER_ID"'", + "roles": [ + "IAM_OWNER" + ] + }' + ) + PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.details.creationDate') + handle_zitadel_request_response "$PARSED_RESPONSE" "add_instance_admin" "$RESPONSE" + echo "$PARSED_RESPONSE" +} + +delete_auto_service_user() { + INSTANCE_URL=$1 + PAT=$2 + + RESPONSE=$( + curl -sS -X GET "$INSTANCE_URL/auth/v1/users/me" \ + -H "Authorization: Bearer $PAT" \ + -H "Content-Type: application/json" \ + ) + USER_ID=$(echo "$RESPONSE" | jq -r '.user.id') + handle_zitadel_request_response "$USER_ID" "delete_auto_service_user_get_user" "$RESPONSE" + + RESPONSE=$( + curl -sS -X DELETE "$INSTANCE_URL/admin/v1/members/$USER_ID" \ + -H "Authorization: Bearer $PAT" \ + -H "Content-Type: application/json" \ + ) + PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.details.changeDate') + handle_zitadel_request_response "$PARSED_RESPONSE" "delete_auto_service_user_remove_instance_permissions" "$RESPONSE" + + RESPONSE=$( + curl -sS -X DELETE "$INSTANCE_URL/management/v1/orgs/me/members/$USER_ID" \ + -H "Authorization: Bearer $PAT" \ + -H "Content-Type: application/json" \ + ) + PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.details.changeDate') + handle_zitadel_request_response "$PARSED_RESPONSE" "delete_auto_service_user_remove_org_permissions" "$RESPONSE" + echo "$PARSED_RESPONSE" +} + +init_zitadel() { + echo -e "\nInitializing Zitadel with NetBird's applications\n" + INSTANCE_URL="$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN:$NETBIRD_PORT" + + TOKEN_PATH=./machinekey/zitadel-admin-sa.token + + echo -n "Waiting for Zitadel's PAT to be created " + wait_pat "$TOKEN_PATH" + echo "Reading Zitadel PAT" + PAT=$(cat $TOKEN_PATH) + if [ "$PAT" = "null" ]; then + echo "Failed requesting getting Zitadel PAT" + exit 1 + fi + + echo -n "Waiting for Zitadel to become ready " + wait_api "$INSTANCE_URL" "$PAT" + + # create the zitadel project + echo "Creating new zitadel project" + PROJECT_ID=$(create_new_project "$INSTANCE_URL" "$PAT") + + ZITADEL_DEV_MODE=false + BASE_REDIRECT_URL=$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN + if [[ $NETBIRD_HTTP_PROTOCOL == "http" ]]; then + ZITADEL_DEV_MODE=true + fi + + # create zitadel spa applications + echo "Creating new Zitadel SPA Dashboard application" + DASHBOARD_APPLICATION_CLIENT_ID=$(create_new_application "$INSTANCE_URL" "$PAT" "Dashboard" "http://localhost:3000/nb-auth" "http://localhost:3000/nb-silent-auth" "http://localhost:3000/" "true") + + echo "Creating new Zitadel SPA Cli application" + CLI_APPLICATION_CLIENT_ID=$(create_new_application "$INSTANCE_URL" "$PAT" "Cli" "http://localhost:53000/" "http://localhost:54000/" "http://localhost:53000/" "true") + + MACHINE_USER_ID=$(create_service_user "$INSTANCE_URL" "$PAT") + + SERVICE_USER_CLIENT_ID="null" + SERVICE_USER_CLIENT_SECRET="null" + + create_service_user_secret "$INSTANCE_URL" "$PAT" "$MACHINE_USER_ID" + + DATE=$(add_organization_user_manager "$INSTANCE_URL" "$PAT" "$MACHINE_USER_ID") + + ZITADEL_ADMIN_USERNAME="admin@localhost" + ZITADEL_ADMIN_PASSWORD="testMe123@" + + HUMAN_USER_ID=$(create_admin_user "$INSTANCE_URL" "$PAT" "$ZITADEL_ADMIN_USERNAME" "$ZITADEL_ADMIN_PASSWORD") + + DATE="null" + + DATE=$(add_instance_admin "$INSTANCE_URL" "$PAT" "$HUMAN_USER_ID") + + DATE="null" + DATE=$(delete_auto_service_user "$INSTANCE_URL" "$PAT") + if [ "$DATE" = "null" ]; then + echo "Failed deleting auto service user" + echo "Please remove it manually" + fi + + export NETBIRD_AUTH_CLIENT_ID=$DASHBOARD_APPLICATION_CLIENT_ID + export NETBIRD_AUTH_CLIENT_ID_CLI=$CLI_APPLICATION_CLIENT_ID + export NETBIRD_IDP_MGMT_CLIENT_ID=$SERVICE_USER_CLIENT_ID + export NETBIRD_IDP_MGMT_CLIENT_SECRET=$SERVICE_USER_CLIENT_SECRET + export ZITADEL_ADMIN_USERNAME + export ZITADEL_ADMIN_PASSWORD +} + +check_nb_domain() { + DOMAIN=$1 + if [ "$DOMAIN-x" == "-x" ]; then + echo "The NETBIRD_DOMAIN variable cannot be empty." > /dev/stderr + return 1 + fi + + if [ "$DOMAIN" == "netbird.example.com" ]; then + echo "The NETBIRD_DOMAIN cannot be netbird.example.com" > /dev/stderr + retrun 1 + fi + return 0 +} + +read_nb_domain() { + READ_NETBIRD_DOMAIN="" + echo -n "Enter the domain you want to use for NetBird (e.g. netbird.my-domain.com): " > /dev/stderr + read -r READ_NETBIRD_DOMAIN < /dev/tty + if ! check_nb_domain "$READ_NETBIRD_DOMAIN"; then + read_nb_domain + fi + echo "$READ_NETBIRD_DOMAIN" +} + +initEnvironment() { + CADDY_SECURE_DOMAIN="" + ZITADEL_EXTERNALSECURE="false" + ZITADEL_TLS_MODE="disabled" + ZITADEL_MASTERKEY="$(openssl rand -base64 32 | head -c 32)" + NETBIRD_PORT=80 + NETBIRD_HTTP_PROTOCOL="http" + TURN_USER="self" + TURN_PASSWORD=$(openssl rand -base64 32 | sed 's/=//g') + TURN_MIN_PORT=49152 + TURN_MAX_PORT=65535 + + NETBIRD_DOMAIN=$(get_main_ip_address) + + if [[ "$OSTYPE" == "darwin"* ]]; then + ZIDATE_TOKEN_EXPIRATION_DATE=$(date -u -v+30M "+%Y-%m-%dT%H:%M:%SZ") + else + ZIDATE_TOKEN_EXPIRATION_DATE=$(date -u -d "+30 minutes" "+%Y-%m-%dT%H:%M:%SZ") + fi + + check_jq + + DOCKER_COMPOSE_COMMAND=$(check_docker_compose) + + if [ -f zitadel.env ]; then + echo "Generated files already exist, if you want to reinitialize the environment, please remove them first." + echo "You can use the following commands:" + echo " $DOCKER_COMPOSE_COMMAND down --volumes # to remove all containers and volumes" + echo " rm -f docker-compose.yml Caddyfile zitadel.env dashboard.env machinekey/zitadel-admin-sa.token turnserver.conf management.json" + echo "Be aware that this will remove all data from the database, and you will have to reconfigure the dashboard." + exit 1 + fi + + echo Rendering initial files... + renderDockerCompose > docker-compose.yml + renderCaddyfile > Caddyfile + renderZitadelEnv > zitadel.env + echo "" > turnserver.conf + echo "" > management.json + + mkdir -p machinekey + chmod 777 machinekey + + init_crdb + + echo -e "\nStarting Zidatel IDP for user management\n\n" + $DOCKER_COMPOSE_COMMAND up -d caddy zitadel + init_zitadel + + echo -e "\nRendering NetBird files...\n" + renderTurnServerConf > turnserver.conf + renderManagementJson > management.json + renderDashboardEnv > src/.local-config.json + + echo -e "\nStarting NetBird services\n" + $DOCKER_COMPOSE_COMMAND up -d + echo -e "\nDone!\n" + echo "You can access the NetBird dashboard at $NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN:$NETBIRD_PORT" + echo "Login with the following credentials:" + echo "Username: $ZITADEL_ADMIN_USERNAME" | tee .env + echo "Password: $ZITADEL_ADMIN_PASSWORD" | tee -a .env +} + +renderCaddyfile() { + cat < { + await expect(this.addPeerModal).toBeVisible(); + }) + } + + async assertPeerModalIsNotVisible() { + await test.step('Assert that add peer modal is not visible', async () => { + await expect(this.addPeerModal).not.toBeVisible(); + }) + } + + async openLinuxTab() { + await test.step('Open Linux tab on add peer modal', async () => { + await this.linuxTab.click(); + + }) + } + + async openWindowsTab() { + await test.step('Open Windows tab on add peer modal', async () => { + await this.windowsTab.click(); + + }) + } + + async openMacTab() { + await test.step('Open MacOS tab on add peer modal', async () => { + await this.macTab.click(); + + }) + } + + async openAndroidTab() { + await test.step('Open Android tab on add peer modal', async () => { + await this.androidTab.click(); + + }) + } + + async openDockerTab() { + await test.step('Open Docker tab on add peer modal', async () => { + await this.dockerTab.click(); + + }) + } + + async assertLinuxTabHasCorrectText() { + await test.step('Assert Linux tab has correct installation text', async () => { + await expect(this.linuxTabText).toBeVisible(); + }) + } + + async assertWindowsDownloadButtonHasCorrectLink() { + await test.step('Assert Windows download button has a correct link', async () => { + await expect(this.windowsDownloadButton).toHaveAttribute('href', 'https://pkgs.netbird.io/windows/x64'); + }) + } + + async assertIntelDownloadButtonHasCorrectLink() { + await test.step('Assert Intel download button has a correct link', async () => { + await expect(this.intelDownloadButton).toHaveAttribute('href', 'https://pkgs.netbird.io/macos/amd64'); + }) + } + + async assertM1M2DownloadButtonHasCorrectLink() { + await test.step('Assert M1 & M2 download button has a correct link', async () => { + await expect(this.m1M2DownloadButton).toHaveAttribute('href', 'https://pkgs.netbird.io/macos/arm64'); + }) + } + + async assertAndroidDownloadButtonHasCorrectLink() { + await test.step('Assert Android download button has a correct link', async () => { + await expect(this.androidDownloadButton).toHaveAttribute('href', 'https://play.google.com/store/apps/details?id=io.netbird.client'); + }) + } + + async assertDockerDownloadButtonHasCorrectLink() { + await test.step('Assert Docker download button has a correct link', async () => { + await expect(this.dockerDownloadButton).toHaveAttribute('href', 'https://docs.docker.com/engine/install/'); + }) + } + + async closeAddPeerModal() { + await test.step('Close Add peer modal', async () => { + await this.closeButton.click(); + }) + } + + } + +export default AddPeerModal; \ No newline at end of file diff --git a/e2e-tests/pages/peers-page.ts b/e2e-tests/pages/peers-page.ts new file mode 100644 index 0000000..52f9019 --- /dev/null +++ b/e2e-tests/pages/peers-page.ts @@ -0,0 +1,15 @@ +import { Page, test, expect } from "@playwright/test"; + +export class PeersPage { + private readonly addNewPeerButton = this.page.getByTestId('add-new-peer-button') + + constructor(private readonly page: Page) {} + + async clickOnAddNewPeerButton() { + await test.step('Click on Add new peer Button to open Add peer modal', async () => { + await this.addNewPeerButton.click(); + }) + } + } + +export default PeersPage; \ No newline at end of file diff --git a/e2e-tests/pages/top-menu.ts b/e2e-tests/pages/top-menu.ts new file mode 100644 index 0000000..47aa576 --- /dev/null +++ b/e2e-tests/pages/top-menu.ts @@ -0,0 +1,15 @@ +import { Page, test} from "@playwright/test"; + +export class TopMenu { + private readonly accessControlButton = this.page.getByTestId('access-control-page') + + constructor(private readonly page: Page) {} + + async clickOnAccessControlOnTopMenu() { + await test.step('Click on Access Control page on a top menu', async () => { + await this.accessControlButton.click(); + }) + } + } + +export default TopMenu; \ No newline at end of file diff --git a/e2e-tests/tests/access-control.test.ts b/e2e-tests/tests/access-control.test.ts new file mode 100644 index 0000000..7a542cc --- /dev/null +++ b/e2e-tests/tests/access-control.test.ts @@ -0,0 +1,33 @@ +import { test, expect } from '@playwright/test'; +import {AddPeerModal} from '../pages/modals/add-peer-modal' +import {TopMenu} from '../pages/top-menu'; + +const URL = 'https://app.netbird.io/' +const localUrl = 'http://localhost:3000/' +let addPeerModal: AddPeerModal; +let topMenu: TopMenu; + +test.beforeEach(async ({ page }) => { + addPeerModal = new AddPeerModal(page); + await page.goto(localUrl); + await page.getByPlaceholder('username@domain').fill('admin@localhost'); + await page.getByRole('button', { name: 'next' }).click(); + await page.getByLabel('Password').fill('testMe123@'); + await page.getByRole('button', { name: 'next' }).click(); + const skipButton = page.getByRole('button', { name: 'skip' }); + if (await skipButton.isVisible({ timeout: 300 })) { + await skipButton.click(); + } + await addPeerModal.assertPeerModalIsVisible(); +}); + + test('Confirm that new user has Default access', async ({ page }) => { + topMenu = new TopMenu(page); + await addPeerModal.closeAddPeerModal(); + await addPeerModal.assertPeerModalIsNotVisible(); + await topMenu.clickOnAccessControlOnTopMenu(); + await expect(page.getByRole('cell', { name: 'Default' })).toBeVisible(); + await page.getByRole('button', { name: 'Delete' }).click(); + await expect(page.getByTestId('confirm-delete-modal-title')).toBeVisible(); + await page.getByRole('button', { name: 'Cancel' }).click(); + }); \ No newline at end of file diff --git a/e2e-tests/tests/peers.test.ts b/e2e-tests/tests/peers.test.ts new file mode 100644 index 0000000..0337e06 --- /dev/null +++ b/e2e-tests/tests/peers.test.ts @@ -0,0 +1,56 @@ +import { test } from '@playwright/test'; +import {AddPeerModal} from '../pages/modals/add-peer-modal' +import {PeersPage} from '../pages/peers-page' + +const URL = 'https://app.netbird.io/' +const localUrl = 'http://localhost:3000/' +let addPeerModal: AddPeerModal; +let peersPage: PeersPage; + +test.beforeEach(async ({ page }) => { + addPeerModal = new AddPeerModal(page); + await page.goto(localUrl); + await page.getByPlaceholder('username@domain').fill('admin@localhost'); + await page.getByRole('button', { name: 'next' }).click(); + await page.getByLabel('Password').fill('testMe123@'); + await page.getByRole('button', { name: 'next' }).click(); + const skipButton = page.getByRole('button', { name: 'skip' }); + if (await skipButton.isVisible({ timeout: 300 })) { + await skipButton.click(); + } + await addPeerModal.assertPeerModalIsVisible(); +}); + +test('Test Linux tab on a first access add peer modal / @bc', async function ({ }) { + await addPeerModal.openLinuxTab(); + await addPeerModal.assertLinuxTabHasCorrectText(); + }); + +test('Test Windows tab on a first access add peer modal / @bc', async ({ }) => { + await addPeerModal.openWindowsTab(); + await addPeerModal.assertWindowsDownloadButtonHasCorrectLink(); +}); + +test('Test MacOS tab on a first access add peer modal / @bc', async ({ }) => { + await addPeerModal.openMacTab(); + await addPeerModal.assertIntelDownloadButtonHasCorrectLink(); + await addPeerModal.assertM1M2DownloadButtonHasCorrectLink(); +}); + +test('Test Android tab on a first access add peer modal', async ({ }) => { + await addPeerModal.openAndroidTab(); + await addPeerModal.assertAndroidDownloadButtonHasCorrectLink(); +}); + + test('Test Docker tab on a first access add peer modal', async ({ }) => { + await addPeerModal.openDockerTab(); + await addPeerModal.assertDockerDownloadButtonHasCorrectLink(); + }); + + test('Close and open Add peer modal', async ({ page }) => { + peersPage = new PeersPage(page); + await addPeerModal.closeAddPeerModal(); + await addPeerModal.assertPeerModalIsNotVisible(); + await peersPage.clickOnAddNewPeerButton(); + await addPeerModal.assertPeerModalIsVisible(); + }); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 7f11637..3ba2eef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -58,6 +58,7 @@ "web-vitals": "^2.1.4" }, "devDependencies": { + "@playwright/test": "^1.36.2", "@types/react-syntax-highlighter": "^15.5.3" } }, @@ -3087,6 +3088,25 @@ "opener": "^1.5.2" } }, + "node_modules/@playwright/test": { + "version": "1.37.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.37.0.tgz", + "integrity": "sha512-181WBLk4SRUyH1Q96VZl7BP6HcK0b7lbdeKisn3N/vnjitk+9HbdlFz/L5fey05vxaAhldIDnzo8KUoy8S3mmQ==", + "dev": true, + "dependencies": { + "@types/node": "*", + "playwright-core": "1.37.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, "node_modules/@pmmmwh/react-refresh-webpack-plugin": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.7.tgz", @@ -11712,6 +11732,18 @@ "node": ">=4" } }, + "node_modules/playwright-core": { + "version": "1.37.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.37.0.tgz", + "integrity": "sha512-1c46jhTH/myQw6sesrcuHVtLoSNfJv8Pfy9t3rs6subY7kARv0HRw5PpyfPYPpPtQvBOmgbE6K+qgYUpj81LAA==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/postcss": { "version": "8.4.16", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.16.tgz", @@ -19234,6 +19266,17 @@ "opener": "^1.5.2" } }, + "@playwright/test": { + "version": "1.37.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.37.0.tgz", + "integrity": "sha512-181WBLk4SRUyH1Q96VZl7BP6HcK0b7lbdeKisn3N/vnjitk+9HbdlFz/L5fey05vxaAhldIDnzo8KUoy8S3mmQ==", + "dev": true, + "requires": { + "@types/node": "*", + "fsevents": "2.3.2", + "playwright-core": "1.37.0" + } + }, "@pmmmwh/react-refresh-webpack-plugin": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.7.tgz", @@ -25594,6 +25637,12 @@ } } }, + "playwright-core": { + "version": "1.37.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.37.0.tgz", + "integrity": "sha512-1c46jhTH/myQw6sesrcuHVtLoSNfJv8Pfy9t3rs6subY7kARv0HRw5PpyfPYPpPtQvBOmgbE6K+qgYUpj81LAA==", + "dev": true + }, "postcss": { "version": "8.4.16", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.16.tgz", diff --git a/package.json b/package.json index 9c37fab..4a7a91e 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,7 @@ ] }, "devDependencies": { - "@types/react-syntax-highlighter": "^15.5.3" + "@types/react-syntax-highlighter": "^15.5.3", + "@playwright/test": "^1.36.2" } } diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..a2170ab --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,80 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './e2e-tests/tests', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* 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', + /* 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', + screenshot: 'only-on-failure', + video: 'retain-on-failure', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { channel: 'chrome', }, + }, + + { + name: 'firefox', + use: { browserName: 'firefox', }, + }, + + { + name: 'webkit', + use: { browserName: 'webkit', }, + }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + webServer: { + command: 'npm run start', + url: 'http://127.0.0.1:3000', + reuseExistingServer: !process.env.CI, + timeout: 180 * 1000, + }, +}); diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 15e0d12..36ee0ba 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -35,12 +35,12 @@ const Navbar = () => { const [isRefreshingUserState, setIsRefreshingUserState] = useState(false); const items = [ - { label: Peers, key: "/peers" }, - { label: Setup Keys, key: "/setup-keys" }, - { label: Access Control, key: "/acls" }, - { label: Network Routes, key: "/routes" }, - { label: DNS, key: "/dns" }, - { label: Users, key: "/users" }, + { label: Peers, key: "/peers" }, + { label: Setup Keys, key: "/setup-keys" }, + { label: Access Control, key: "/acls" }, + { label: Network Routes, key: "/routes" }, + { label: DNS, key: "/dns" }, + { label: Users, key: "/users" }, { label: Activity, key: "/activity" }, { label: Settings, key: "/settings" }, ] as ItemType[]; diff --git a/src/components/popups/addpeer/addpeer/AddPeerPopup.tsx b/src/components/popups/addpeer/addpeer/AddPeerPopup.tsx index 0e35e0f..3530bd6 100644 --- a/src/components/popups/addpeer/addpeer/AddPeerPopup.tsx +++ b/src/components/popups/addpeer/addpeer/AddPeerPopup.tsx @@ -39,27 +39,27 @@ export const AddPeerPopup: React.FC = ({ const items: TabsProps['items'] = [ { key: "1", - label: Linux, + label: Linux, children: , }, { key: "2", - label: Windows, + label: Windows, children: , }, { key: "3", - label: MacOS, + label: MacOS, children: , }, { key: "4", - label: Android, + label: Android, children: , }, { key: "5", - label: Docker, + label: Docker, children: , } ]; diff --git a/src/components/popups/addpeer/addpeer/AndroidTab.tsx b/src/components/popups/addpeer/addpeer/AndroidTab.tsx index adf00aa..889eb83 100644 --- a/src/components/popups/addpeer/addpeer/AndroidTab.tsx +++ b/src/components/popups/addpeer/addpeer/AndroidTab.tsx @@ -13,7 +13,7 @@ export const AndroidTab = () => { key: 1, title: 'Download and install the application from Google Play Store:', commands: ( - + ), diff --git a/src/components/popups/addpeer/addpeer/DockerTab.tsx b/src/components/popups/addpeer/addpeer/DockerTab.tsx index 5e03c1e..8ca92bf 100644 --- a/src/components/popups/addpeer/addpeer/DockerTab.tsx +++ b/src/components/popups/addpeer/addpeer/DockerTab.tsx @@ -16,6 +16,7 @@ export const DockerTab = () => { title: "Install Docker", commands: ( + ), copied: false } as StepCommand, diff --git a/src/views/AccessControl.tsx b/src/views/AccessControl.tsx index f5e72de..c3a12af 100644 --- a/src/views/AccessControl.tsx +++ b/src/views/AccessControl.tsx @@ -298,7 +298,7 @@ export const AccessControl = () => { setPolicyToAction(record as PolicyDataTable); confirm({ icon: , - title: Delete rule {record.name}, + title: Delete rule {record.name}, width: 500, content: ( diff --git a/src/views/Peers.tsx b/src/views/Peers.tsx index d139f40..a8046c4 100644 --- a/src/views/Peers.tsx +++ b/src/views/Peers.tsx @@ -757,6 +757,7 @@ export const Peers = () => { Get started by adding one to your network.