diff --git a/.github/workflows/build-and-deploy-test-stack.yml b/.github/workflows/build-and-deploy-test-stack.yml new file mode 100644 index 000000000..8b3a5a90d --- /dev/null +++ b/.github/workflows/build-and-deploy-test-stack.yml @@ -0,0 +1,49 @@ +name: Build and deploy GovTool test stack +run-name: Deploy by @${{ github.actor }} + +on: + push: + branches: + - test + +env: + ENVIRONMENT: "test" + CARDANO_NETWORK: "sanchonet" + +jobs: + deploy: + name: Deploy app + runs-on: ubuntu-latest + env: + GRAFANA_ADMIN_PASSWORD: ${{ secrets.GRAFANA_ADMIN_PASSWORD }} + GRAFANA_SLACK_RECIPIENT: ${{ secrets.GRAFANA_SLACK_RECIPIENT }} + GRAFANA_SLACK_OAUTH_TOKEN: ${{ secrets.GRAFANA_SLACK_OAUTH_TOKEN }} + SENTRY_DSN_BACKEND: ${{ secrets.SENTRY_DSN_BACKEND }} + GTM_ID: ${{ secrets.GTM_ID }} + SENTRY_DSN: ${{ secrets.SENTRY_DSN_FRONTEND }} + PIPELINE_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} + USERSNAP_SPACE_API_KEY: ${{ secrets.USERSNAP_SPACE_API_KEY }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup SSH agent + uses: webfactory/ssh-agent@v0.8.0 + with: + ssh-private-key: ${{ secrets.TEST_STACK_SSH_KEY }} + + - name: Run Ansible playbook + uses: dawidd6/action-ansible-playbook@v2 + with: + playbook: playbook.yml + directory: ./tests/test-infrastructure + key: ${{ secrets.TEST_STACK_SSH_KEY }} + inventory: | + [test_server] + ${{ secrets.TEST_STACK_SERVER_IP }} ansible_user=ec2-user + options: | + --verbose + env: + GOVTOOL_TAG: ${{ github.sha }} \ No newline at end of file diff --git a/.github/workflows/lighthouse.yml b/.github/workflows/lighthouse.yml index 2bf75ada6..1b24b16d1 100644 --- a/.github/workflows/lighthouse.yml +++ b/.github/workflows/lighthouse.yml @@ -1,47 +1,32 @@ name: Lighthouse on: - push: - paths: - - govtool/frontend/** - - .github/workflows/lighthouse.yml + workflow_run: + workflows: + - Build and deploy GovTool test stack + types: + - completed + workflow_dispatch: jobs: lighthouse: runs-on: ubuntu-latest - env: - NODE_OPTIONS: --max_old_space_size=4096 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v3 with: node-version: 16 - - name: Install dependencies - run: npm install - working-directory: ./govtool/frontend - - - name: Cache npm dependencies - id: npm-cache - uses: actions/cache@v3 - with: - path: | - ~/.npm - key: ${{ runner.os }}-npm-${{ hashFiles('govtool/frontend/package-lock.json', 'tests/govtool-frontend/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-npm- - - run: npm install -g @lhci/cli@0.12.x - - name: Run build and lighthouse task + - name: Run lighthouse task working-directory: ./govtool/frontend run: | - npm install - VITE_BASE_URL=https://staging.govtool.byron.network/ npm run build lhci collect - name: Evaluate reports if: github.repository_owner != 'IntersectMBO' + working-directory: ./govtool/frontend run: | lhci assert --preset "lighthouse:recommended" @@ -50,9 +35,6 @@ jobs: if: github.repository_owner == 'IntersectMBO' run: | lhci assert --preset lighthouse:recommended || echo "LightHouse Assertion error ignored ..." - lhci upload --githubAppToken="${{ secrets.LHCI_GITHUB_APP_TOKEN }}" --token="${{ secrets.LHCI_SERVER_TOKEN }}" --serverBaseUrl=https://lighthouse.cardanoapi.io --ignoreDuplicateBuildFailure - curl -X POST https://ligththouse.cardanoapi.io/api/metrics/build-reports \ - -d "@./lighthouseci/$(ls ./.lighthouseci |grep 'lhr.*\.json' | head -n 1)" \ - -H "commit-hash: $(git rev-parse HEAD)" \ - -H "secret-token: ${{ secrets.METRICS_SERVER_SECRET_TOKEN }}" \ - -H 'Content-Type: application/json' || echo "Metric Upload error ignored ..." + lhci upload --githubAppToken="${{ secrets.LHCI_GITHUB_APP_TOKEN }}" --token="${{ secrets.LHCI_SERVER_TOKEN }}" --serverBaseUrl=${LHCI_SERVER_URL} --ignoreDuplicateBuildFailure + env: + LHCI_SERVER_URL: https://lighthouse-govtool.cardanoapi.io diff --git a/.github/workflows/test_backend.yml b/.github/workflows/test_backend.yml index 7c53ee7b3..1a7deea44 100644 --- a/.github/workflows/test_backend.yml +++ b/.github/workflows/test_backend.yml @@ -2,23 +2,18 @@ name: Backend Test on: push: - paths: - - .github/workflows/test_backend.yml - # - govtool/backend - # - tests/govtool-backend - - schedule: - - cron: "0 0 * * *" + branches: + - test workflow_dispatch: inputs: deployment: required: true type: choice - default: "staging.govtool.byron.network/api" + default: "govtool.cardanoapi.io/api" options: - "sanchogov.tools/api" - "staging.govtool.byron.network/api" - - "govtool-sanchonet.cardanoapi.io/api" + - "govtool.cardanoapi.io/api" jobs: backend-tests: @@ -38,15 +33,15 @@ jobs: run: | python -m pip install --upgrade pip pip install -r requirements.txt + python ./setup.py python -m pytest --alluredir allure-results env: - BASE_URL: https://${{inputs.deployment || 'staging.govtool.byron.network/api' }} - METRICS_URL: https://metrics.cardanoapi.io - METRICS_API_SECRET: "${{ secrets.METRICS_SERVER_SECRET_TOKEN }}" + BASE_URL: https://${{inputs.deployment || 'govtool.cardanoapi.io/api' }} + FAUCET_API_KEY: ${{ secrets.FAUCET_API_KEY }} + KUBER_API_URL: https://kuber-govtool.cardanoapi.io - name: Upload report uses: actions/upload-artifact@v3 - if: always() with: name: allure-results path: tests/govtool-backend/allure-results @@ -70,7 +65,7 @@ jobs: ref: gh-pages path: gh-pages repository: ${{vars.GH_PAGES}} - token: ${{secrets.PERSONAL_TOKEN}} + ssh-key: ${{ secrets.DEPLOY_KEY }} - name: Register report id: register-project @@ -81,7 +76,7 @@ jobs: - if: steps.register-project.outputs.project_exists != 'true' uses: JamesIves/github-pages-deploy-action@v4 with: - token: ${{ secrets.PERSONAL_TOKEN }} + ssh-key: ${{ secrets.DEPLOY_KEY }} repository-name: ${{vars.GH_PAGES}} branch: gh-pages folder: project @@ -94,7 +89,6 @@ jobs: - name: Build report uses: simple-elf/allure-report-action@master - if: always() id: allure-report with: allure_results: allure-results @@ -113,12 +107,12 @@ jobs: - name: Deploy report to Github Pages uses: JamesIves/github-pages-deploy-action@v4 with: - token: ${{ secrets.PERSONAL_TOKEN }} + ssh-key: ${{ secrets.DEPLOY_KEY }} repository-name: ${{vars.GH_PAGES}} branch: gh-pages folder: build target-folder: ${{ env.REPORT_NAME }} env: - REPORT_NAME: backend + REPORT_NAME: govtool-backend GH_PAGES: ${{vars.GH_PAGES}} diff --git a/.github/workflows/test_integration_cypress.yml b/.github/workflows/test_integration_cypress.yml deleted file mode 100644 index f6f5d554b..000000000 --- a/.github/workflows/test_integration_cypress.yml +++ /dev/null @@ -1,71 +0,0 @@ -name: Integration Test [Cypress] -run-name: Integration Test on ${{ inputs.network ||'sanchonet' }} [${{ inputs.deployment || 'staging.govtool.byron.network' }}] - -on: - push: - branches: [feat/integration-test] - schedule: - - cron: '0 0 * * *' - - workflow_dispatch: - inputs: - network: - required: true - type: choice - default: "sanchonet" - options: - - "sanchonet" - deployment: - required: true - type: choice - default: "staging.govtool.byron.network" - options: - - "sanchogov.tools" - - "staging.govtool.byron.network" - - "govtool-sanchonet.cardanoapi.io" - -jobs: - cypress-tests: - defaults: - run: - working-directory: ./tests/govtool-frontend - runs-on: ubuntu-latest - env: - NODE_OPTIONS: --max_old_space_size=4096 - - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-node@v3 - with: - node-version: 16 - - - name: Get yarn cache directory path - id: yarn-cache-dir-path - run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT - - - uses: actions/cache@v3 - id: yarn-cache - with: - path: | - ${{ steps.yarn-cache-dir-path.outputs.dir }} - key: - ${{ runner.os }}-yarn-${{hashFiles('tests/govtool-frontend/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-yarn- - - - name: Cypress run - uses: cypress-io/github-action@v6 - with: - record: true - working-directory: ./tests/govtool-frontend - env: - CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} - # pass GitHub token to allow accurately detecting a build vs a re-run build - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - CYPRESS_baseUrl: https://${{inputs.deployment || 'staging.govtool.byron.network' }} - CYPRESS_apiUrl: https://${{ inputs.deployment || 'staging.govtool.byron.network' }}/api - CYPRESS_kuberApiUrl: https://${{ inputs.network || 'sanchonet' }}.kuber.cardanoapi.io - CYPRESS_kuberApiKey: ${{secrets.KUBER_API_KEY}} - CYPRESS_faucetApiUrl: https://faucet.${{inputs.network || 'sanchonet'}}.world.dev.cardano.org - CYPRESS_faucetApiKey: ${{ secrets.FAUCET_API_KEY }} diff --git a/.github/workflows/test_integration_playwright.yml b/.github/workflows/test_integration_playwright.yml index f9e10d27b..a1b2bacb7 100644 --- a/.github/workflows/test_integration_playwright.yml +++ b/.github/workflows/test_integration_playwright.yml @@ -2,8 +2,19 @@ name: Integration Test [Playwright] on: push: - paths: - - .github/workflows/test_integration_playwright.yml + branches: + - test + workflow_dispatch: + inputs: + deployment: + required: true + type: choice + default: "govtool.cardanoapi.io" + options: + - "sanchogov.tools" + - "staging.govtool.byron.network" + - "govtool.cardanoapi.io" + workflow_run: workflows: ["Build and deploy GovTool to TEST server"] types: [completed] @@ -11,7 +22,6 @@ on: jobs: integration-tests: runs-on: ubuntu-latest - if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'push' }} defaults: run: working-directory: tests/govtool-frontend/playwright @@ -40,6 +50,9 @@ jobs: - name: Run tests run: | + mkdir -p ./lib/_mock + chmod +w ./lib/_mock + npm run generate-wallets npm test - name: Upload report @@ -50,18 +63,20 @@ jobs: path: tests/govtool-frontend/playwright/allure-results env: - FRONTEND_URL: ${{vars.HOST_URL}} - API_URL: ${{vars.HOST_URL}}/api + HOST_URL: https://${{inputs.deployment || 'govtool.cardanoapi.io' }} + API_URL: https://${{inputs.deployment || 'govtool.cardanoapi.io' }}/api DOCS_URL: ${{ vars.DOCS_URL }} FAUCET_API_URL: ${{ vars.FAUCET_API_URL }} FAUCET_API_KEY: ${{secrets.FAUCET_API_KEY}} KUBER_API_URL: ${{vars.KUBER_API_URL}} KUBER_API_KEY: ${{secrets.KUBER_API_KEY}} - WORKERS: ${{vars.TEST_WORKERS}} + TEST_WORKERS: ${{vars.TEST_WORKERS}} + CI: ${{vars.CI}} + CARDANOAPI_METADATA_URL: ${{vars.CARDANOAPI_METADATA_URL}} publish-report: runs-on: ubuntu-latest - if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'push' }} + if: always() needs: integration-tests steps: - uses: actions/checkout@v4 @@ -128,5 +143,5 @@ jobs: target-folder: ${{ env.REPORT_NAME }} env: - REPORT_NAME: integration + REPORT_NAME: govtool-frontend GH_PAGES: ${{vars.GH_PAGES}} diff --git a/generate_latest_report_redirect.sh b/generate_latest_report_redirect.sh index de2ad8646..688e61239 100644 --- a/generate_latest_report_redirect.sh +++ b/generate_latest_report_redirect.sh @@ -11,7 +11,7 @@ cat < build/index.html - + Redirecting... diff --git a/gov-action-loader/backend/.env.example b/gov-action-loader/backend/.env.example index 654ec757e..8997b5adf 100644 --- a/gov-action-loader/backend/.env.example +++ b/gov-action-loader/backend/.env.example @@ -1,6 +1,2 @@ KUBER_API_URL=https://sanchonet.kuber.cardanoapi.io KUBER_API_KEY=xxxxxxxxxxxxx - -## Not required anymore -BLOCKFROST_API_URL= -BLOCKFROST_PROJECT_ID= diff --git a/gov-action-loader/backend/app/settings.py b/gov-action-loader/backend/app/settings.py index ce543cf33..02095e720 100644 --- a/gov-action-loader/backend/app/settings.py +++ b/gov-action-loader/backend/app/settings.py @@ -3,10 +3,6 @@ class Settings(BaseSettings): kuber_api_url: str - kuber_api_key: str - - blockfrost_api_url: str - blockfrost_project_id: str - + kuber_api_key: str = '' settings = Settings() diff --git a/govtool/analytics-dashboard/public/assets/svgs/favicon.svg b/govtool/analytics-dashboard/public/assets/svgs/favicon.svg new file mode 100644 index 000000000..534056abd --- /dev/null +++ b/govtool/analytics-dashboard/public/assets/svgs/favicon.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/govtool/analytics-dashboard/src/app/[locale]/layout.js b/govtool/analytics-dashboard/src/app/[locale]/layout.js index ec3d5f247..3f736cf34 100644 --- a/govtool/analytics-dashboard/src/app/[locale]/layout.js +++ b/govtool/analytics-dashboard/src/app/[locale]/layout.js @@ -5,6 +5,7 @@ import { unstable_setRequestLocale } from "next-intl/server"; import { notFound } from "next/navigation"; import '@/styles/index.css'; import ThemeProviderWrapper from "@/components/ThemeProviderWrapper"; +import Head from "next/head"; export function generateStaticParams() { @@ -14,8 +15,8 @@ export function generateStaticParams() { // Define common metadata for the application. export const metadata = { - title: "Web App Boilerplate", - description: "Web App Boilerplate", + title: "Participation dashboard", + description: "Participation dashboard", }; async function RootLayout({ children, params: { locale } }) { @@ -36,10 +37,10 @@ async function RootLayout({ children, params: { locale } }) { {metadata.title} - + {/* Apply font class and suppress hydration warning. */} - + {/* Provide internationalization context. */} {/* Wrap children in global state context */} diff --git a/govtool/analytics-dashboard/src/app/[locale]/page.js b/govtool/analytics-dashboard/src/app/[locale]/page.js index d5caace8b..c714503fd 100644 --- a/govtool/analytics-dashboard/src/app/[locale]/page.js +++ b/govtool/analytics-dashboard/src/app/[locale]/page.js @@ -5,6 +5,7 @@ import { PeopleAltOutlined, ArticleOutlined, AccountBalanceWalletOutlined, HowTo import { useTheme } from '@mui/material/styles'; import getGoogleData from '@/lib/api'; import { useEffect, useState } from 'react'; +import { Link } from '@/navigation'; function Dashboard() { @@ -57,7 +58,7 @@ function Dashboard() { Participation Dashboard theme?.palette?.text?.gray }}> - This dashboard show the overall participation and usage of govtool from 1 of January 2024 + This dashboard shows the overall participation and usage of SanchoNet Govtool from 1st of December 2023 @@ -88,9 +89,11 @@ function Dashboard() { © {new Date().getFullYear()} Intersect MBO - theme?.palette?.text?.primaryBlue }}> - Sancho Govtool - + + theme?.palette?.text?.primaryBlue }}> + Sancho Govtool + + diff --git a/govtool/analytics-dashboard/src/app/favicon.ico b/govtool/analytics-dashboard/src/app/favicon.ico index 718d6fea4..b5ec7f389 100644 Binary files a/govtool/analytics-dashboard/src/app/favicon.ico and b/govtool/analytics-dashboard/src/app/favicon.ico differ diff --git a/govtool/analytics-dashboard/src/app/favicon.svg b/govtool/analytics-dashboard/src/app/favicon.svg new file mode 100644 index 000000000..13fb7d158 --- /dev/null +++ b/govtool/analytics-dashboard/src/app/favicon.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/govtool/analytics-dashboard/src/pages/api/analytics.js b/govtool/analytics-dashboard/src/pages/api/analytics.js index 6a7ccae2c..65958a696 100644 --- a/govtool/analytics-dashboard/src/pages/api/analytics.js +++ b/govtool/analytics-dashboard/src/pages/api/analytics.js @@ -14,7 +14,7 @@ export default async function handler(req, res) { const [response] = await analyticsDataClient.runReport({ property: `properties/${propertyId}`, dateRanges: [{ - startDate: '2024-01-01', + startDate: '2023-12-01', endDate: 'today', }], dimensions: [{ name: 'eventName' }], diff --git a/govtool/backend/Dockerfile b/govtool/backend/Dockerfile index b8882627d..7da291284 100644 --- a/govtool/backend/Dockerfile +++ b/govtool/backend/Dockerfile @@ -1,5 +1,6 @@ -ARG BASE_IMAGE_TAG -FROM 733019650473.dkr.ecr.eu-west-1.amazonaws.com/backend-base:$BASE_IMAGE_TAG +ARG BASE_IMAGE_TAG=latest +ARG BASE_IMAGE_REPO=733019650473.dkr.ecr.eu-west-1.amazonaws.com/backend-base +FROM $BASE_IMAGE_REPO:$BASE_IMAGE_TAG WORKDIR /src COPY . . RUN cabal build diff --git a/govtool/frontend/.lighthouserc.yml b/govtool/frontend/.lighthouserc.yml index 5e963f7ae..fe06450bb 100644 --- a/govtool/frontend/.lighthouserc.yml +++ b/govtool/frontend/.lighthouserc.yml @@ -1,5 +1,6 @@ ci: collect: - staticDistDir: "./dist" url: - - "http://localhost" + - https://govtool.cardanoapi.io + - https://govtool.cardanoapi.io/drep_directory + - https://govtool.cardanoapi.io/governance_actions \ No newline at end of file diff --git a/scripts/govtool/Makefile b/scripts/govtool/Makefile index 6417313c3..236cb416c 100644 --- a/scripts/govtool/Makefile +++ b/scripts/govtool/Makefile @@ -10,8 +10,8 @@ include config.mk .DEFAULT_GOAL := info # image tags -cardano_node_image_tag := 8.10.0-pre -cardano_db_sync_image_tag := sancho-4-2-1 +cardano_node_image_tag := 8.11.0-sancho +cardano_db_sync_image_tag := sancho-4-3-0-docker .PHONY: all all: deploy-stack notify diff --git a/scripts/govtool/config.mk b/scripts/govtool/config.mk index 439d75495..d6b4c4772 100644 --- a/scripts/govtool/config.mk +++ b/scripts/govtool/config.mk @@ -98,8 +98,8 @@ $(target_config_dir)/grafana-provisioning/alerting/alerting.yml: $(template_conf -i $@ $(target_config_dir)/nginx/auth.conf: $(target_config_dir)/nginx/ - @:$(call check_defined, domain) - if [[ "$(domain)" == *"sanchonet.govtool.byron.network"* ]]; then \ + @:$(call check_defined, env) + if [[ "$(env)" != "beta" ]]; then \ echo 'map $$http_x_forwarded_for $$auth {' > $@; \ echo " default \"Restricted\";" >> $@; \ echo " $${IP_ADDRESS_BYPASSING_BASIC_AUTH1} \"off\";" >> $@; \ diff --git a/scripts/govtool/docker-compose.node+dbsync.yml b/scripts/govtool/docker-compose.node+dbsync.yml index 076c26da9..0f8ca6a9f 100644 --- a/scripts/govtool/docker-compose.node+dbsync.yml +++ b/scripts/govtool/docker-compose.node+dbsync.yml @@ -51,7 +51,7 @@ services: retries: 5 cardano-node: - image: ghcr.io/intersectmbo/cardano-node:8.8.0-pre + image: ghcr.io/intersectmbo/cardano-node:8.11.0-sancho environment: - NETWORK=sanchonet volumes: @@ -65,7 +65,7 @@ services: retries: 10 cardano-db-sync: - image: ghcr.io/intersectmbo/cardano-db-sync:sancho-4.1.0 + image: ghcr.io/intersectmbo/cardano-db-sync:sancho-4-3-0-docker environment: - NETWORK=sanchonet - POSTGRES_HOST=postgres diff --git a/tests/govtool-backend/.env.example b/tests/govtool-backend/.env.example index 64603fc5f..3bed52954 100644 --- a/tests/govtool-backend/.env.example +++ b/tests/govtool-backend/.env.example @@ -1,7 +1,8 @@ -BASE_URL = `URL where the api is hosted` +BASE_URL = "https://govtool.cardanoapi.io/api" RECORD_METRICS_API = `URL where metrics is posted` METRICS_API_SECRET= `api_secret` # required for setup -KUBER_API_URL = "" -KUBER_API_KEY = "" +KUBER_API_URL = "https://kuber-govtool.cardanoapi.io" +KUBER_API_KEY = "" # optional +FAUCET_API_KEY= """ \ No newline at end of file diff --git a/tests/govtool-backend/config.py b/tests/govtool-backend/config.py index f5f382162..fb601c31b 100644 --- a/tests/govtool-backend/config.py +++ b/tests/govtool-backend/config.py @@ -10,6 +10,6 @@ dotenv.load_dotenv() RECORD_METRICS_API = os.getenv("RECORD_METRICS_API") -METRICS_API_SECRET= os.getenv("METRICS_API_SECRET") -KUBER_API_URL = os.getenv("KUBER_API_URL") -KUBER_API_KEY= os.getenv("KUBER_API_KEY") +METRICS_API_SECRET = os.getenv("METRICS_API_SECRET") +KUBER_API_URL = os.getenv("KUBER_API_URL") +KUBER_API_KEY = os.getenv("KUBER_API_KEY") diff --git a/tests/govtool-backend/lib/__init__.py b/tests/govtool-backend/lib/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/govtool-backend/lib/faucet_api.py b/tests/govtool-backend/lib/faucet_api.py new file mode 100644 index 000000000..ed0150390 --- /dev/null +++ b/tests/govtool-backend/lib/faucet_api.py @@ -0,0 +1,41 @@ +import os +from typing import TypedDict + +import requests + + +class FaucetAmount(TypedDict): + lovelace: int + + +class Transaction(TypedDict): + amount: FaucetAmount + txid: str + txin: str + + +class CardanoFaucet: + def __init__(self, api_key: str, base_url: str = "https://faucet.sanchonet.world.dev.cardano.org"): + self.api_key = api_key + self.base_url = base_url + + @staticmethod + def from_env(): + api_key = os.getenv("FAUCET_API_KEY") + base_url = os.getenv("FAUCET_API_URL", "https://faucet.sanchonet.world.dev.cardano.org") + if not api_key: + raise ValueError("FAUCET_API_KEY environment variable not set.") + return CardanoFaucet(api_key, base_url) + + def send_money(self, address: str, tx_type: str = "default") -> Transaction: + endpoint = f"{self.base_url}/send-money" + params = {"address": address, "api_key": self.api_key, "type": tx_type} + response = requests.get(endpoint, params=params) + + if response.status_code == 200: + return response.json() + else: + response.raise_for_status() + + +"" diff --git a/tests/govtool-backend/test_cases/govtool_api.py b/tests/govtool-backend/lib/govtool_api.py similarity index 53% rename from tests/govtool-backend/test_cases/govtool_api.py rename to tests/govtool-backend/lib/govtool_api.py index c37f567ee..7036ddae5 100644 --- a/tests/govtool-backend/test_cases/govtool_api.py +++ b/tests/govtool-backend/lib/govtool_api.py @@ -9,7 +9,7 @@ from config import BUILD_ID -class GovToolApi(): +class GovToolApi: def __init__(self, base_url: str): self._base_url = base_url @@ -18,16 +18,15 @@ def __init__(self, base_url: str): self.requests_log = [] self.tests_log = [] - def __request(self, method: str, endpoint: str, param: Any | None = None, - body: Any | None = None) -> Response: - endpoint = endpoint if endpoint.startswith('/') else '/' + endpoint + def __request(self, method: str, endpoint: str, param: Any | None = None, body: Any | None = None) -> Response: + endpoint = endpoint if endpoint.startswith("/") else "/" + endpoint full_url = self._base_url + endpoint full_url = full_url + "/" + param if param else full_url - start_time = int(time.time()*1000000) + start_time = int(time.time() * 1000000) response = self._session.request(method, full_url, json=body) - end_time = int(time.time()*1000000) + end_time = int(time.time() * 1000000) response_time = end_time - start_time try: @@ -45,34 +44,57 @@ def __request(self, method: str, endpoint: str, param: Any | None = None, "response_json": response_json_str, "response_time": response_time, "start_date": int(start_time), - "build_id": BUILD_ID + "build_id": BUILD_ID, } self.requests_log.append(request_info) - assert 200 >= response.status_code <= 299, f"Expected {method}{endpoint} to succeed but got statusCode:{response.status_code} : body:{response.text}" + assert ( + 200 >= response.status_code <= 299 + ), f"Expected {method}{endpoint} to succeed but got statusCode:{response.status_code} : body:{response.text}" return response def __get(self, endpoint: str, param: str | None = None) -> Response: - return self.__request('GET', endpoint, param) + return self.__request("GET", endpoint, param) + + def __post(self, endpoint: str, param: str | None = None, body=None) -> Response: + return self.__request("POST", endpoint, param, body) def drep_list(self) -> Response: - return self.__get('/drep/list') + return self.__get("/drep/list") + + def drep_info(self, drep_id) -> Response: + return self.__get("/drep/info", drep_id) def drep_getVotes(self, drep_id) -> Response: - return self.__get('/drep/getVotes', drep_id) + return self.__get("/drep/getVotes", drep_id) def drep_get_voting_power(self, drep_id) -> Response: - return self.__get('/drep/get-voting-power', drep_id) + return self.__get("/drep/get-voting-power", drep_id) def proposal_list(self) -> Response: - return self.__get('/proposal/list') + return self.__get("/proposal/list") + + def get_proposal(self, id) -> Response: + return self.__get("/proposal/get", id) def ada_holder_get_current_delegation(self, stake_key: str) -> Response: - return self.__get('/ada-holder/get-current-delegation', stake_key) + return self.__get("/ada-holder/get-current-delegation", stake_key) def ada_holder_get_voting_power(self, stake_key) -> Response: - return self.__get('/ada-holder/get-voting-power', stake_key) + return self.__get("/ada-holder/get-voting-power", stake_key) + + def epoch_params(self) -> Response: + return self.__get("/epoch/params") + + def validate_metadata(self, metadata) -> Response: + return self.__post("/metadata/validate", body=metadata) + + def network_metrics(self) -> Response: + return self.__get("/network/metrics") + + def get_transaction_status(self, tx_id) -> Response: + return self.__get("/transaction/status", tx_id) def add_test_metrics(self, metrics: Metrics): - self.tests_log.append(metrics) + return self.tests_log.append(metrics) diff --git a/tests/govtool-backend/models/TestData.py b/tests/govtool-backend/models/TestData.py index e0ea45b22..a48179698 100644 --- a/tests/govtool-backend/models/TestData.py +++ b/tests/govtool-backend/models/TestData.py @@ -1,22 +1,46 @@ -from typing import TypedDict +from typing import TypedDict, Optional, List, Dict, Any + + +class ProposalListResponse(TypedDict): + page: int + pageSize: int + total: int + elements: List["Proposal"] + + +class GetProposalResponse(TypedDict): + votes: int + proposal: "Proposal" class Proposal(TypedDict): id: str + txHash: str + index: int type: str - details: str + details: Optional[dict] expiryDate: str + expiryEpochNo: int + createdDate: str + createdEpochNo: int url: str metadataHash: str + title: Optional[str] + about: Optional[str] + motivation: Optional[str] + rationale: Optional[str] + metadata: Optional[dict] + references: Optional[list] yesVotes: int noVotes: int abstainVotes: int + class Drep(TypedDict): drepId: str url: str metadataHash: str - deposit : int + deposit: int class Delegation(TypedDict): @@ -39,3 +63,71 @@ class Vote(TypedDict): class VoteonProposal(TypedDict): vote: Vote proposal: Proposal + + +class DrepInfo(TypedDict): + isRegisteredAsDRep: bool + wasRegisteredAsDRep: bool + isRegisteredAsSoleVoter: bool + wasRegisteredAsSoleVoter: bool + deposit: int + url: str + dataHash: str + votingPower: Optional[int] + dRepRegisterTxHash: str + dRepRetireTxHash: Optional[str] + soleVoterRegisterTxHash: Optional[str] + soleVoterRetireTxHash: Optional[str] + + +class EpochParam(TypedDict): + block_id: int + coins_per_utxo_size: int + collateral_percent: int + committee_max_term_length: int + committee_min_size: int + cost_model_id: int + decentralisation: int + drep_activity: int + drep_deposit: int + dvt_committee_no_confidence: float + dvt_committee_normal: float + dvt_hard_fork_initiation: float + dvt_motion_no_confidence: float + dvt_p_p_economic_group: float + dvt_p_p_gov_group: float + dvt_p_p_network_group: float + dvt_p_p_technical_group: float + dvt_treasury_withdrawal: float + dvt_update_to_constitution: float + epoch_no: int + extra_entropy: Optional[int] + gov_action_deposit: int + gov_action_lifetime: int + id: int + influence: float + key_deposit: int + max_bh_size: int + max_block_ex_mem: int + max_block_ex_steps: int + max_block_size: int + max_collateral_inputs: int + max_epoch: int + max_tx_ex_mem: int + + +class TxStatus(TypedDict): + transactionConfirmed: bool + + +class NetworkMetrics(TypedDict): + currentTime: str + currentEpoch: int + currentBlock: int + uniqueDelegators: int + totalDelegations: int + totalGovernanceActions: int + totalDRepVotes: int + totalRegisteredDReps: int + alwaysAbstainVotingPower: int + alwaysNoConfidenceVotingPower: int diff --git a/tests/govtool-backend/setup.py b/tests/govtool-backend/setup.py index a4b78da41..97f5d9192 100644 --- a/tests/govtool-backend/setup.py +++ b/tests/govtool-backend/setup.py @@ -1,15 +1,11 @@ import sys import requests import json -from config import KUBER_API_URL, KUBER_API_KEY +from lib.faucet_api import CardanoFaucet +from lib.kuber_api import KuberApi -if KUBER_API_URL is not None: - KUBER_API_URL = KUBER_API_URL[:-1] if KUBER_API_URL.endswith('/') else KUBER_API_URL - print(f"KUBER_API_URL: {KUBER_API_URL}") -else: - print("KUBER_API_URL environment variable is not set.", file=sys.stderr) - sys.exit(1) +kuber_api = KuberApi.from_env() # check fund for the main wallet main_wallet = { @@ -100,6 +96,16 @@ def main(): ada_wallets[0]["pay-skey"], ada_wallets[1]["stake-skey"], ada_wallets[1]["pay-skey"], + { + "type": "PaymentSigningKeyShelley_ed25519", + "description": "Payment Signing Key", + "cborHex": drep_wallets[0]["stake-skey"]["cborHex"], + }, + { + "type": "PaymentSigningKeyShelley_ed25519", + "description": "Payment Signing Key", + "cborHex": drep_wallets[1]["stake-skey"]["cborHex"], + }, ], "certificates": [ { @@ -123,7 +129,6 @@ def main(): ], "proposals": [ { - "deposit": 1000000000, "refundAccount": { "network": "Testnet", "credential": {"key hash": ada_wallets[0]["stake-vkey"]}, @@ -136,22 +141,36 @@ def main(): } ], } - kuber_url = KUBER_API_URL + "/api/v1/tx?submit=true" - print(json.dumps(kuber_json,indent=2)) + print(json.dumps(kuber_json, indent=2)) print("Submitting the above registration transaction..") - response = requests.post( - url=kuber_url, headers={"api-key": KUBER_API_KEY}, json=kuber_json + balance = kuber_api.get_balance(main_wallet["address"]) + protocol_params = kuber_api.get_protocol_params() + total_locked = ( + protocol_params["dRepDeposit"] * 2 + + protocol_params["stakeAddressDeposit"] * 2 + + protocol_params["govActionDeposit"] ) + if balance < (total_locked + 10 * 10000000000000): + print("Loading balance to the bootstrap wallet") + faucet = CardanoFaucet.from_env() + result = faucet.send_money(main_wallet["address"]) + if "error" in result: + print(result) + raise Exception("Failed to load balance from faucet") + kuber_api.wait_for_txout(result["txin"], log=True) + response = kuber_api.build_tx(kuber_json, submit=True) + if response.status_code == 200: print("Transaction submitted", response.text) + data = response.json() + kuber_api.wait_for_txout(data["hash"] + '#0', log=True) else: print("Server Replied with Error [ StatusCode=", response.status_code, "]", response.reason, response.text) - if('DRepAlreadyRegistered' in response.text or 'StakeKeyRegisteredDELEG'): + if ("DRepAlreadyRegistered" in response.text) or ("StakeKeyRegisteredDELEG" in response.text): print("-----") - print("This might mean that you have already run the setup script.") + print("This probably means that you have already run the setup script.") print("-----") sys.exit(0) - sys.exit(1) # vote from one of the dreps to the proposal @@ -175,17 +194,16 @@ def main(): }, ], } - print(json.dumps(kuber_json,indent=2)) - response = requests.post( - url=kuber_url, headers={"api-key": KUBER_API_KEY}, json=kuber_json - ) + print(json.dumps(kuber_json, indent=2)) + response = kuber_api.build_tx(kuber_json, submit=True) if response.status_code == 200: print("Transaction submitted", response.text) + data = response.json() + kuber_api.wait_for_txout(data["hash"] + '#0', log=True) else: - print("Server Replied with Error [ StatusCode=", response.status_code, "]", response.reason, response.text) + if "AlreadyRegistered" in response.text: + print("Server Replied with Error [ StatusCode=", response.status_code, "]", response.reason, response.text) + print("") sys.exit(1) - # write to the file in nice format -\ - main() diff --git a/tests/govtool-backend/test_cases/__init__.py b/tests/govtool-backend/test_cases/__init__.py index e69de29bb..0a0b2a9ea 100644 --- a/tests/govtool-backend/test_cases/__init__.py +++ b/tests/govtool-backend/test_cases/__init__.py @@ -0,0 +1 @@ +from test_cases.fixtures import * diff --git a/tests/govtool-backend/test_cases/conftest.py b/tests/govtool-backend/test_cases/conftest.py index fe20b385e..8e834d6a9 100644 --- a/tests/govtool-backend/test_cases/conftest.py +++ b/tests/govtool-backend/test_cases/conftest.py @@ -3,34 +3,34 @@ import sys import re +# import the fixtures. +from test_cases.fixtures import * import pytest import requests from models.TestResult import Metrics -from test_cases.govtool_api import GovToolApi +from lib.govtool_api import GovToolApi from config import CURRENT_GIT_HASH from config import BUILD_ID from config import METRICS_API_SECRET -from test_cases.fixtures.drep import registered_drep -from test_cases.fixtures.ada_holder import ada_holder_delegate_to_drep @pytest.fixture(scope="session") def govtool_api(): - base_url: str = os.environ.get('BASE_URL') - metrics_url: str = os.environ.get('METRICS_URL') + base_url: str = os.environ.get("BASE_URL") + metrics_url: str = os.environ.get("METRICS_URL") if base_url is not None: - base_url = base_url[:-1] if base_url.endswith('/') else base_url + base_url = base_url[:-1] if base_url.endswith("/") else base_url print(f"BASE_URL: {base_url}") else: print("BASE_URL environment variable is not set.", file=sys.stderr) sys.exit(1) if metrics_url is not None: - metrics_url = metrics_url[:-1] if metrics_url.endswith('/') else metrics_url + metrics_url = metrics_url[:-1] if metrics_url.endswith("/") else metrics_url print(f"METRICS_URL: {metrics_url}") else: print("METRICS_URL environment variable is not set.", file=sys.stderr) @@ -40,25 +40,35 @@ def govtool_api(): yield api if metrics_url: - endpoint_record_url = metrics_url + '/metrics/api-endpoints' + endpoint_record_url = metrics_url + "/metrics/api-endpoints" test_record_url = metrics_url + "/metrics/test-results" print() print("Uploading API endpoint metrics ...") for request_log in api.requests_log: - response = requests.post(url=endpoint_record_url, data=request_log,headers={ - 'secret-token': METRICS_API_SECRET - }) - if (response.status_code != 200): + response = requests.post( + url=endpoint_record_url, data=request_log, headers={"secret-token": METRICS_API_SECRET} + ) + if response.status_code != 200: print(response.json()) - print("Error Uploading API metrics:[ statuscode=",response.status_code ,"]", "endpoint="+request_log['endpoint'],"duration="+str(request_log['response_time']/1000)+"ms") + print( + "Error Uploading API metrics:[ statuscode=", + response.status_code, + "]", + "endpoint=" + request_log["endpoint"], + "duration=" + str(request_log["response_time"] / 1000) + "ms", + ) print("Uploading Test results ...") for test_log in api.tests_log: - response = requests.post(url=test_record_url, data=test_log,headers={ - 'secret-token': METRICS_API_SECRET - }) - if (response.status_code != 200): - print("Error Uploading Test result:[ statuscode=",response.status_code ,"]", "test="+test_log['test_name'],"result="+test_log['outcome'],) + response = requests.post(url=test_record_url, data=test_log, headers={"secret-token": METRICS_API_SECRET}) + if response.status_code != 200: + print( + "Error Uploading Test result:[ statuscode=", + response.status_code, + "]", + "test=" + test_log["test_name"], + "result=" + test_log["outcome"], + ) @pytest.hookimpl(wrapper=True, tryfirst=True) @@ -68,7 +78,7 @@ def pytest_runtest_makereport(item): if rep.when == "call": - test_func_name = re.search(r'(?<=::)(.*?)*(?=\[|$)', rep.nodeid).group() + test_func_name = re.search(r"(?<=::)(.*?)*(?=\[|$)", rep.nodeid).group() govtool_api_object.add_test_metrics( Metrics( @@ -76,8 +86,8 @@ def pytest_runtest_makereport(item): test_name=test_func_name, build_id=BUILD_ID, commit_hash=CURRENT_GIT_HASH, - start_date=int(rep.start*1000000), - end_date=int(rep.stop*1000000) + start_date=int(rep.start * 1000000), + end_date=int(rep.stop * 1000000), ) ) return rep diff --git a/tests/govtool-backend/test_cases/fixtures/__init__.py b/tests/govtool-backend/test_cases/fixtures/__init__.py new file mode 100644 index 000000000..9d4d4b99c --- /dev/null +++ b/tests/govtool-backend/test_cases/fixtures/__init__.py @@ -0,0 +1,2 @@ +from .ada_holder import * +from .drep import * diff --git a/tests/govtool-backend/test_cases/fixtures/ada_holder.py b/tests/govtool-backend/test_cases/fixtures/ada_holder.py index 57343cd2e..f0fddf23a 100644 --- a/tests/govtool-backend/test_cases/fixtures/ada_holder.py +++ b/tests/govtool-backend/test_cases/fixtures/ada_holder.py @@ -7,9 +7,6 @@ def ada_holder_delegate_to_drep(request, govtool_api): ada_holder: AdaHolder = request.param - delegation_data = Delegation( - stakeKey=ada_holder["stakeKey"], - dRepId=ada_holder["drepId"] - ) + delegation_data = Delegation(stakeKey=ada_holder["stakeKey"], dRepId=ada_holder["drepId"]) yield delegation_data diff --git a/tests/govtool-backend/test_cases/test_ada_holder.py b/tests/govtool-backend/test_cases/test_ada_holder.py index 2f0b37515..a51000be1 100644 --- a/tests/govtool-backend/test_cases/test_ada_holder.py +++ b/tests/govtool-backend/test_cases/test_ada_holder.py @@ -1,15 +1,17 @@ from models.TestData import AdaHolder, Delegation import allure + @allure.story("AdaHolder") -def test_ada_delegation(govtool_api, ada_holder_delegate_to_drep): +def test_ada_holder_current_delegation(govtool_api, ada_holder_delegate_to_drep): print(ada_holder_delegate_to_drep) response = govtool_api.ada_holder_get_current_delegation(ada_holder_delegate_to_drep["stakeKey"]) resp = response.json() if resp: assert ada_holder_delegate_to_drep["drepId"] in resp -@allure.story("Drep") + +@allure.story("AdaHolder") def test_check_voting_power(govtool_api, ada_holder_delegate_to_drep): response = govtool_api.ada_holder_get_voting_power(ada_holder_delegate_to_drep["stakeKey"]) ada_holder_voting_power = response.json() diff --git a/tests/govtool-backend/test_cases/test_drep.py b/tests/govtool-backend/test_cases/test_drep.py index b4b74e62f..3e3255ccd 100644 --- a/tests/govtool-backend/test_cases/test_drep.py +++ b/tests/govtool-backend/test_cases/test_drep.py @@ -1,7 +1,7 @@ -from models.TestData import Drep, VoteonProposal, Vote, Proposal +from models.TestData import Drep, VoteonProposal, Vote, Proposal, DrepInfo import allure -@allure.story("Drep") + def validate_drep_list(drep_list: [Drep]) -> bool: for item in drep_list: if not isinstance(item, dict): @@ -12,48 +12,64 @@ def validate_drep_list(drep_list: [Drep]) -> bool: return False return True -@allure.story("Drep") + def validate_voteonproposal_list(voteonproposal_list: [VoteonProposal]) -> bool: for item in voteonproposal_list: if not isinstance(item, dict): return False # Validate the 'vote' key against the Vote type - if 'vote' not in item or not isinstance(item['vote'], dict): + if "vote" not in item or not isinstance(item["vote"], dict): return False - if not all(key in item['vote'] for key in Vote.__annotations__): + if not all(key in item["vote"] for key in Vote.__annotations__): return False - if not all(isinstance(item['vote'][key], Vote.__annotations__[key]) for key in Vote.__annotations__): + if not all(isinstance(item["vote"][key], Vote.__annotations__[key]) for key in Vote.__annotations__): return False # Validate the 'proposal' key against the Proposal type - if 'proposal' not in item or not isinstance(item['proposal'], dict): + if "proposal" not in item or not isinstance(item["proposal"], dict): return False - if not all(key in item['proposal'] for key in Proposal.__annotations__): + if not all(key in item["proposal"] for key in Proposal.__annotations__): return False - if not all(isinstance(item['proposal'][key], Proposal.__annotations__[key]) for key in Proposal.__annotations__): + if not all( + isinstance(item["proposal"][key], Proposal.__annotations__[key]) for key in Proposal.__annotations__ + ): return False return True +def validate_drep_info(drep): + for key, val in DrepInfo.__annotations__.items(): + assert isinstance( + drep[key], DrepInfo.__annotations__[key] + ), f"drepInfo.{key} should be of type {DrepInfo.__annotations__[key]} got {type(drep[key])}" + + @allure.story("Drep") def test_list_drep(govtool_api): response = govtool_api.drep_list() drep_list = response.json() validate_drep_list(drep_list) + @allure.story("Drep") -def test_initialized_getVotes( govtool_api, registered_drep): +def test_drep_getVotes(govtool_api, registered_drep): response = govtool_api.drep_getVotes(registered_drep["drepId"]) validate_voteonproposal_list(response.json()) votes = response.json() proposals = map(lambda x: x["vote"]["proposalId"], votes) proposals = list(proposals) - assert len(proposals)==0 + assert len(proposals) == 0 @allure.story("Drep") -def test_initialized_getVotingPower(govtool_api, registered_drep): +def test_drep_voting_power(govtool_api, registered_drep): response = govtool_api.drep_get_voting_power(registered_drep["drepId"]) assert isinstance(response.json(), int) + + +@allure.story("Drep") +def test_drep_get_info(govtool_api, registered_drep): + response = govtool_api.drep_info(registered_drep["drepId"]) + validate_drep_info(response.json()) diff --git a/tests/govtool-backend/test_cases/test_misc.py b/tests/govtool-backend/test_cases/test_misc.py new file mode 100644 index 000000000..4178336d3 --- /dev/null +++ b/tests/govtool-backend/test_cases/test_misc.py @@ -0,0 +1,40 @@ +import allure + +from models.TestData import EpochParam, NetworkMetrics, TxStatus + + +def validate_epoch_param(epoch_param): + for key, val in EpochParam.__annotations__.items(): + assert isinstance( + epoch_param[key], EpochParam.__annotations__[key] + ), f"epochParam.{key} should be of type {EpochParam.__annotations__[key]} got {type(epoch_param[key])}" + + +def validate_network_metrics(network_metrics): + for key, val in NetworkMetrics.__annotations__.items(): + assert isinstance( + network_metrics[key], NetworkMetrics.__annotations__[key] + ), f"epochParam.{key} should be of type {NetworkMetrics.__annotations__[key]} got {type(network_metrics[key])}" + + +def validate_model(model, item): + for key, val in model.__annotations__.items(): + assert isinstance(item[key], val), f"{model.__name__}.{key} should be of type {val} got {type(item[key])}" + + +@allure.story("Misc") +def test_get_epoch_param(govtool_api): + epoch_param: EpochParam = govtool_api.epoch_params().json() + validate_epoch_param(epoch_param) + + +@allure.story("Misc") +def test_get_network_metrics(govtool_api): + network_metrics = govtool_api.network_metrics().json() + validate_network_metrics(network_metrics) + + +@allure.story("Misc") +def test_get_transaction_status(govtool_api): + tx_status = govtool_api.get_transaction_status("ff" * 32).json() + validate_model(TxStatus, tx_status) diff --git a/tests/govtool-backend/test_cases/test_proposal.py b/tests/govtool-backend/test_cases/test_proposal.py index aa32990cc..8f3293896 100644 --- a/tests/govtool-backend/test_cases/test_proposal.py +++ b/tests/govtool-backend/test_cases/test_proposal.py @@ -1,17 +1,15 @@ -from models.TestData import Proposal +from models.TestData import Proposal, ProposalListResponse, GetProposalResponse import allure -@allure.story("Proposal") -def validate_proposal_list(proposal_list: [Proposal]) -> bool: - for item in proposal_list: - if not isinstance(item, dict): - return False - if not all(key in item for key in Proposal.__annotations__): - return False - if not all(isinstance(item[key], Proposal.__annotations__[key]) for key in Proposal.__annotations__): - return False - if not all(isinstance(item[key], int) for key in ['yesVotes', 'noVotes', 'abstainVotes']): - return False + +def validate_proposal(proposal: Proposal) -> bool: + assert isinstance(proposal, dict), f"Expected Proposal to be of type dict, got {type(proposal)}" + + for key in Proposal.__annotations__: + assert key in proposal, f"Expected Proposal.{key} to be present" + assert isinstance( + proposal[key], Proposal.__annotations__[key] + ), f"drepInfo.{key} should be of type {Proposal.__annotations__[key]} got {type(proposal[key])}" return True @@ -19,4 +17,15 @@ def validate_proposal_list(proposal_list: [Proposal]) -> bool: def test_list_proposal(govtool_api): response = govtool_api.proposal_list() proposal_list = response.json() - assert validate_proposal_list(proposal_list) + for proposal in proposal_list["elements"]: + assert validate_proposal(proposal) + + +@allure.story("Proposal") +def test_get_proposal(govtool_api): + response: ProposalListResponse = govtool_api.proposal_list().json() + for proposal in response["elements"]: + proposal_get: GetProposalResponse = govtool_api.get_proposal( + proposal["txHash"] + "%23" + str(proposal["index"]) + ).json() + assert validate_proposal(proposal_get["proposal"]) diff --git a/tests/govtool-backend/test_data.json b/tests/govtool-backend/test_data.json index b28fb85db..34bf95e30 100644 --- a/tests/govtool-backend/test_data.json +++ b/tests/govtool-backend/test_data.json @@ -1 +1,70 @@ -{"drep_wallets": [{"pay-skey": {"type": "PaymentSigningKeyShelley_ed25519", "description": "Payment Signing Key", "cborHex": "58207da324397a403f89972ba63f2853c6c6043fd96dac3bdcc452f27c9ad5c75c83"}, "address": "addr_test1qzh73vyy0mtu5xfahdswmaclzcs9lrm8hsvq0n799ufhp53htvec6kdtxqls04v5ldacx342v5rsflxlep93s6t5k2hs70m6n2", "stake-skey": {"type": "StakeSigningKeyShelley_ed25519", "description": "Stake Signing Key", "cborHex": "582036742f9246e355e75318894cb31f7058510f827c6820f40f56cce9bbdab8ef08"}, "drep-id": "drep1xadn8r2e4vcr7p74jnahhq6x4fjswp8umlyykxrfwje2707cqh9", "stake-vkey": "375b338d59ab303f07d594fb7b8346aa650704fcdfc84b186974b2af", "url": "https://bit.ly/3zCH2HL", "data_hash": "1111111111111111111111111111111111111111111111111111111111111111"}, {"pay-skey": {"type": "PaymentSigningKeyShelley_ed25519", "description": "Payment Signing Key", "cborHex": "58205db2e13ca102a6bcfea2d4651d24559ee933ab6c355796307cace0bd23584b17"}, "address": "addr_test1qqu3ny5xjfhg9hqg3yfdf9arftg20dv92u3r8hkc94833xlv4tvazt5672duf338dx5zf0stl05zgc8g08qy0asathfs8fewtx", "stake-skey": {"type": "StakeSigningKeyShelley_ed25519", "description": "Stake Signing Key", "cborHex": "582042ab191b40e5b1364beaa4b0d27fea48156d89c92ba749b738bf7891e27fbb6a"}, "drep-id": "drep1aj4dn5fwntefh3xxya56sf97p0a7sfrqapuuq3lkr4waxeelmwd", "stake-vkey": "ecaad9d12e9af29bc4c62769a824be0bfbe82460e879c047f61d5dd3", "url": "https://bit.ly/3zCH2HL", "data_hash": "1111111111111111111111111111111111111111111111111111111111111111"}], "ada_holder_wallets": [{"address": "addr_test1qrqwl94r7zhxqwq8n26p6ql9dzylmzupln8vwaake9njg6wlrxfdmq43utplzwyuaqq8q8xyjvqdul88rda02l95lm9qpauf3k", "pay-skey": {"type": "PaymentSigningKeyShelley_ed25519", "description": "Payment Signing Key", "cborHex": "5820c5b5ad023d8eb7ddc67b271d79705522b65740b9c249e205e39fa30dec775deb"}, "stake-skey": {"type": "PaymentSigningKeyShelley_ed25519", "description": "Stake Signing Key", "cborHex": "5820ea031c372c0617cf7137e7cfbfb821d63e61aa3277af993f84d2b4cdb9199dd6"}, "drep-id": "drep1muve9hvzk83v8ufcnn5qququcjfsphnuuudh4atuknlv5kh84lc", "stake-vkey": "df1992dd82b1e2c3f1389ce800701cc49300de7ce71b7af57cb4feca"}, {"pay-skey": {"type": "PaymentSigningKeyShelley_ed25519", "description": "Payment Signing Key", "cborHex": "5820e6b1bac201091179f8ef213b2dc0f63c72b23de45bc26a7b68eccdc718f65c83"}, "address": "addr_test1qrz8rz38rv37cx4hsgsneavsx4e84ppupwysxp6xp6mv6c9nny8znayz56vw8rfyt0gyyftg6pt5umr9njeey8fjekhqwkrrew", "stake-skey": {"type": "PaymentSigningKeyShelley_ed25519", "description": "Stake Signing Key", "cborHex": "5820826d043e62e04259ffb24c24994e8c8ffd2272eda6e8a610a65977c233077b6d"}, "drep-id": "drep1kwvsu205s2nf3cudy3daqs39drg9wnnvvkwt8ysaxtx6up8cy06", "stake-vkey": "b3990e29f482a698e38d245bd0422568d0574e6c659cb3921d32cdae"}]} +{ + "drep_wallets": [ + { + "pay-skey": { + "type": "PaymentSigningKeyShelley_ed25519", + "description": "Payment Signing Key", + "cborHex": "58207da324397a403f89972ba63f2853c6c6043fd96dac3bdcc452f27c9ad5c75c83" + }, + "address": "addr_test1qzh73vyy0mtu5xfahdswmaclzcs9lrm8hsvq0n799ufhp53htvec6kdtxqls04v5ldacx342v5rsflxlep93s6t5k2hs70m6n2", + "stake-skey": { + "type": "StakeSigningKeyShelley_ed25519", + "description": "Stake Signing Key", + "cborHex": "582036742f9246e355e75318894cb31f7058510f827c6820f40f56cce9bbdab8ef08" + }, + "drep-id": "drep1xadn8r2e4vcr7p74jnahhq6x4fjswp8umlyykxrfwje2707cqh9", + "stake-vkey": "375b338d59ab303f07d594fb7b8346aa650704fcdfc84b186974b2af", + "url": "https://bit.ly/3zCH2HL", + "data_hash": "1111111111111111111111111111111111111111111111111111111111111111" + }, + { + "pay-skey": { + "type": "PaymentSigningKeyShelley_ed25519", + "description": "Payment Signing Key", + "cborHex": "58205db2e13ca102a6bcfea2d4651d24559ee933ab6c355796307cace0bd23584b17" + }, + "address": "addr_test1qqu3ny5xjfhg9hqg3yfdf9arftg20dv92u3r8hkc94833xlv4tvazt5672duf338dx5zf0stl05zgc8g08qy0asathfs8fewtx", + "stake-skey": { + "type": "StakeSigningKeyShelley_ed25519", + "description": "Stake Signing Key", + "cborHex": "582042ab191b40e5b1364beaa4b0d27fea48156d89c92ba749b738bf7891e27fbb6a" + }, + "drep-id": "drep1aj4dn5fwntefh3xxya56sf97p0a7sfrqapuuq3lkr4waxeelmwd", + "stake-vkey": "ecaad9d12e9af29bc4c62769a824be0bfbe82460e879c047f61d5dd3", + "url": "https://bit.ly/3zCH2HL", + "data_hash": "1111111111111111111111111111111111111111111111111111111111111111" + } + ], + "ada_holder_wallets": [ + { + "address": "addr_test1qrqwl94r7zhxqwq8n26p6ql9dzylmzupln8vwaake9njg6wlrxfdmq43utplzwyuaqq8q8xyjvqdul88rda02l95lm9qpauf3k", + "pay-skey": { + "type": "PaymentSigningKeyShelley_ed25519", + "description": "Payment Signing Key", + "cborHex": "5820c5b5ad023d8eb7ddc67b271d79705522b65740b9c249e205e39fa30dec775deb" + }, + "stake-skey": { + "type": "PaymentSigningKeyShelley_ed25519", + "description": "Stake Signing Key", + "cborHex": "5820ea031c372c0617cf7137e7cfbfb821d63e61aa3277af993f84d2b4cdb9199dd6" + }, + "drep-id": "drep1muve9hvzk83v8ufcnn5qququcjfsphnuuudh4atuknlv5kh84lc", + "stake-vkey": "df1992dd82b1e2c3f1389ce800701cc49300de7ce71b7af57cb4feca" + }, + { + "pay-skey": { + "type": "PaymentSigningKeyShelley_ed25519", + "description": "Payment Signing Key", + "cborHex": "5820e6b1bac201091179f8ef213b2dc0f63c72b23de45bc26a7b68eccdc718f65c83" + }, + "address": "addr_test1qrz8rz38rv37cx4hsgsneavsx4e84ppupwysxp6xp6mv6c9nny8znayz56vw8rfyt0gyyftg6pt5umr9njeey8fjekhqwkrrew", + "stake-skey": { + "type": "PaymentSigningKeyShelley_ed25519", + "description": "Stake Signing Key", + "cborHex": "5820826d043e62e04259ffb24c24994e8c8ffd2272eda6e8a610a65977c233077b6d" + }, + "drep-id": "drep1kwvsu205s2nf3cudy3daqs39drg9wnnvvkwt8ysaxtx6up8cy06", + "stake-vkey": "b3990e29f482a698e38d245bd0422568d0574e6c659cb3921d32cdae" + } + ] +} \ No newline at end of file diff --git a/tests/govtool-backend/test_data.py b/tests/govtool-backend/test_data.py index ae5f9eb02..49460b870 100644 --- a/tests/govtool-backend/test_data.py +++ b/tests/govtool-backend/test_data.py @@ -1,10 +1,34 @@ +import os import random import json from typing import List from models.TestData import Drep, AdaHolder -with open("test_data.json", "r") as file: - data = json.load(file) +file_path = "test_data.json" +alternative_file_path = "../test_data.json" -drep_data = list(map(lambda drep_wallet: {"drepId":drep_wallet["stake-vkey"], "url":drep_wallet["url"], "metadataHash":drep_wallet["data_hash"]} ,data["drep_wallets"])) -ada_holders = list(map(lambda wallets: {"drepId":wallets[0]["stake-vkey"], "stakeKey":wallets[1]["stake-vkey"]} ,list(zip(data["drep_wallets"], data["ada_holder_wallets"])))) +if os.path.exists(file_path): + with open(file_path, "r") as file: + data = json.load(file) +elif os.path.exists(alternative_file_path): + with open(alternative_file_path, "r") as file: + data = json.load(file) +else: + raise FileNotFoundError(f"Neither '{file_path}' nor '{alternative_file_path}' could be found.") + +drep_data = list( + map( + lambda drep_wallet: { + "drepId": drep_wallet["stake-vkey"], + "url": drep_wallet["url"], + "metadataHash": drep_wallet["data_hash"], + }, + data["drep_wallets"], + ) +) +ada_holders = list( + map( + lambda wallets: {"drepId": wallets[0]["stake-vkey"], "stakeKey": wallets[1]["stake-vkey"]}, + list(zip(data["drep_wallets"], data["ada_holder_wallets"])), + ) +) diff --git a/tests/govtool-frontend/playwright/.env.example b/tests/govtool-frontend/playwright/.env.example index 7dbc8561a..f86654032 100644 --- a/tests/govtool-frontend/playwright/.env.example +++ b/tests/govtool-frontend/playwright/.env.example @@ -6,9 +6,6 @@ DOCS_URL=https://docs.sanchogov.tools # 0 for testnet, 1 for mainnet NETWORK_ID=0 -# Create mock wallets if true -ONE_TIME_WALLET_SETUP=false - # Faucet FAUCET_API_URL=https://faucet.sanchonet.world.dev.cardano.org FAUCET_API_KEY= diff --git a/tests/govtool-frontend/playwright/.gitignore b/tests/govtool-frontend/playwright/.gitignore index 0a969ff71..504f04dab 100644 --- a/tests/govtool-frontend/playwright/.gitignore +++ b/tests/govtool-frontend/playwright/.gitignore @@ -7,10 +7,10 @@ node_modules/ tests-out/ .auth/ .download/ -lib/_mock/ allure-results/ allure-report/ .secrets .vars .lock-pool/ .logs/ +lib/_mock/wallets.json \ No newline at end of file diff --git a/tests/govtool-frontend/playwright/.prettierrc.json b/tests/govtool-frontend/playwright/.prettierrc.json new file mode 100644 index 000000000..757fd64ca --- /dev/null +++ b/tests/govtool-frontend/playwright/.prettierrc.json @@ -0,0 +1,3 @@ +{ + "trailingComma": "es5" +} diff --git a/tests/govtool-frontend/playwright/generate_wallets.ts b/tests/govtool-frontend/playwright/generate_wallets.ts new file mode 100644 index 000000000..e13de4499 --- /dev/null +++ b/tests/govtool-frontend/playwright/generate_wallets.ts @@ -0,0 +1,44 @@ +import { writeFile } from "fs"; +import { ShelleyWallet } from "./lib/helpers/crypto"; +import extractDRepFromWallet from "./lib/helpers/shellyWallet"; + +async function generateWallets(num: number): Promise { + const wallets: ShelleyWallet[] = []; + + for (let i = 0; i < num; i++) { + wallets.push(await ShelleyWallet.generate()); + } + return wallets; +} + +function saveWallets(wallets: ShelleyWallet[]): void { + const jsonWallets = []; + for (let i = 0; i < wallets.length; i++) { + const dRepId = extractDRepFromWallet(wallets[i]); + + jsonWallets.push({ + ...wallets[i].json(), + address: wallets[i].addressBech32(0), // testnet + dRepId, + }); + } + const jsonString = JSON.stringify(jsonWallets, null, 2); + writeFile("lib/_mock/wallets.json", jsonString, "utf-8", (err) => { + if (err) { + throw new Error("Failed to write wallets into file"); + } + }); +} + +// Get the number of wallets from command line arguments +const numWallets = parseInt(process.argv[2], 10); + +if (isNaN(numWallets) || numWallets <= 0) { + console.error("Please provide a valid number of wallets to generate."); + process.exit(1); +} + +(async () => { + const wallets = await generateWallets(numWallets); + saveWallets(wallets); +})(); diff --git a/tests/govtool-frontend/playwright/lib/_mock/index.ts b/tests/govtool-frontend/playwright/lib/_mock/index.ts new file mode 100644 index 000000000..4c6872043 --- /dev/null +++ b/tests/govtool-frontend/playwright/lib/_mock/index.ts @@ -0,0 +1,49 @@ +import { faker } from "@faker-js/faker"; + +export const invalid = { + url: () => { + const invalidSchemes = ["ftp", "unsupported", "unknown-scheme"]; + const invalidCharacters = "<>@!#$%^&*()"; + const invalidTlds = [".invalid", ".example", ".test"]; + + const scheme = + invalidSchemes[Math.floor(Math.random() * invalidSchemes.length)]; + const invalidChar = + invalidCharacters[Math.floor(Math.random() * invalidCharacters.length)]; + const invalidTld = + invalidTlds[Math.floor(Math.random() * invalidTlds.length)]; + + const randomDomain = `example${invalidChar}domain${invalidTld}`; + return `${scheme}://${randomDomain}`; + }, + + proposalTitle: () => { + const choice = faker.number.int({ min: 1, max: 2 }); + if (choice === 1) { + // maximum 80 words invalid + return faker.lorem.paragraphs(4).replace(/\s+/g, ""); + } + // empty invalid + return " "; + }, + + paragraph: () => { + const choice = faker.number.int({ min: 1, max: 2 }); + if (choice === 1) { + // maximum 500 words + return faker.lorem.paragraphs(40); + } + // empty invalid + return " "; + }, + + amount: () => { + const choice = faker.number.int({ min: 1, max: 2 }); + if (choice === 1) { + // only number is allowed + return faker.lorem.word(); + } + // empty invalid + return " "; + }, +}; diff --git a/tests/govtool-frontend/playwright/lib/constants/environments.ts b/tests/govtool-frontend/playwright/lib/constants/environments.ts index ccdfd8c14..7a8c65a6a 100644 --- a/tests/govtool-frontend/playwright/lib/constants/environments.ts +++ b/tests/govtool-frontend/playwright/lib/constants/environments.ts @@ -1,9 +1,13 @@ +const CARDANO_API_METADATA_HOST_URL = + process.env.CARDANOAPI_METADATA_URL || + "https://metadata-govtool.cardanoapi.io"; +const SERVER_HOST_URL = process.env.HOST_URL || "http://localhost:8080"; + const environments = { - frontendUrl: process.env.HOST_URL || "http://localhost:8080", - apiUrl: `${process.env.HOST_URL}/api` || "http://localhost:8080/api", + frontendUrl: SERVER_HOST_URL, + apiUrl: `${SERVER_HOST_URL}/api`, docsUrl: process.env.DOCS_URL || "https://docs.sanchogov.tools", networkId: parseInt(process.env.NETWORK_ID) || 0, - oneTimeWalletSetup: process.env.ONE_TIME_WALLET_SETUP === "true" || false, faucet: { apiUrl: process.env.FAUCET_API_URL || @@ -11,17 +15,12 @@ const environments = { apiKey: process.env.FAUCET_API_KEY || "", }, kuber: { - apiUrl: - process.env.KUBER_API_URL || "https://sanchonet.kuber.cardanoapi.io", + apiUrl: process.env.KUBER_API_URL || "https://kuber-govtool.cardanoapi.io", apiKey: process.env.KUBER_API_KEY || "", }, txTimeOut: parseInt(process.env.TX_TIMEOUT) || 240000, - metadataBucketUrl: - `${process.env.CARDANOAPI_METADATA_URL}/data` || - "https://metadata.cardanoapi.io/data", - lockInterceptorUrl: - `${process.env.CARDANOAPI_METADATA_URL}/data` || - "https://metadata.cardanoapi.io/lock", + metadataBucketUrl: `${CARDANO_API_METADATA_HOST_URL}/data`, + lockInterceptorUrl: `${CARDANO_API_METADATA_HOST_URL}/lock`, }; export default environments; diff --git a/tests/govtool-frontend/playwright/lib/constants/staticWallets.ts b/tests/govtool-frontend/playwright/lib/constants/staticWallets.ts index 21a0289e6..8c1b7d65a 100644 --- a/tests/govtool-frontend/playwright/lib/constants/staticWallets.ts +++ b/tests/govtool-frontend/playwright/lib/constants/staticWallets.ts @@ -1,122 +1,26 @@ +const staticWallets: StaticWallet[] = require("../_mock/wallets.json"); import { StaticWallet } from "@types"; -export const faucetWallet: StaticWallet = { - payment: { - pkh: "b5187cdefbc5b49ddc17b423c079f0717721a03882a3b265bd4c12e0", - private: "11abec096ef0ea7edbeeee01a1a3f0e9f24a7225c2ee99687fb328146fe85ba6", - public: "b6a42d4ccc4d26adaec67e8578bf31f13b1b7e640527356248f2ec547f9de6e4", - }, - stake: { - pkh: "80f326af300273d19d5a541d45baa42ebc04265816735b026b5f34a4", - private: "283fd7625ef596f04f21b50ee14a9f4b49f8b1a6f17773cd2e1e69841a111bc1", - public: "86b08ee3d86cb72d026197a5a710e248d66f28fcff21b4467b75f876b4e6d050", - }, - dRepId: "drep1zg6zq3ku422ppvfm835rnvzf9ckxtzmy3ayjwylck6s4q9zr5ve", - address: - "addr_test1qz63slx7l0zmf8wuz76z8sre7pchwgdq8zp28vn9h4xp9cyq7vn27vqzw0ge6kj5r4zm4fpwhszzvkqkwddsy66lxjjqxnc9zk", -}; +export const faucetWallet = staticWallets[0]; -export const dRep01Wallet: StaticWallet = { - payment: { - public: "891ed5096ee248bc7f31a3094ea90f34485483eb1050c7ee368e64d04b90a009", - private: "2f1053f22707b9881ea6112024027a660bd5508e22081cf5e4e95cc663802dd9", - pkh: "5775ad2fb14ca1b45381a40e40f0c06081edaf2261e02bbcebcf8dc3", - }, - stake: { - private: "39db531b1ba6d659f0e09ed609e86a080ba2a5629dc5fad3b29890bdba64a014", - public: "45a35ffab6c467531ee528fbdbe1a629de806c7af19dcb5aacb70e4286fd6b9a", - pkh: "46a95c1337b27332131f3c1b9d8e7689edd2f593e7e69bf5dcf0c278", - }, - address: - "addr_test1qpthttf0k9x2rdznsxjqus8scpsgrmd0yfs7q2aua08cms6x49wpxdajwvepx8eurwwcua5fahf0tyl8u6dlth8scfuqk8r352", - dRepId: "drep1g654cyehkfenyycl8sdemrnk38ka9avnulnfhawu7rp8skl824l", -}; +export const dRep01Wallet = staticWallets[1]; +export const dRep02Wallet = staticWallets[2]; -// export const dRep02Wallet: StaticWallet = { -// payment: { -// private: "71120ea01dc0c367da113a7ee7b3744a46f793edb4f30a06b46d800324b2c999", -// public: "66724455eaacb6dea6686ba09bc159d5deef3d82ebf9c6a60d61748b59e32627", -// pkh: "363547ffb44d337f8055515e75e8af516e557b3270bfa4d9198e7195", -// }, -// stake: { -// private: "4dfc89a9d680b237146dde69282c709e93ba91ac0b028e980bc40ec573c77f0f", -// public: "009c10056aff887d66135886d1fb9f046190bdf1d90a3f9cff954386f7cf37fb", -// pkh: "4d52d1d178157ab4c5ab6f8cb109ff91f750b367830463ef8344007e", -// }, -// address: -// "addr_test1qqmr23llk3xnxluq24g4ua0g4agku4tmxfctlfxerx88r92d2tgaz7q4026vt2m03jcsnlu37agtxeurq337lq6yqplqftpnqu", -// dRepId: "drep1f4fdr5tcz4atf3dtd7xtzz0lj8m4pvm8svzx8murgsq8u6dkmf4", -// }; - -export const adaHolder01Wallet: StaticWallet = { - payment: { - private: "63be29a8c8a73571ab410062f4555998c45a61f96a9bf1c5308c4f3eb7e4453f", - public: "dd1e7ca0deb26499a1336fbe2a5169ba3043a27763bb9e600625a95728be6167", - pkh: "daa1dd48181133eb21a376da773a1d31f72281008d790ecac885ff97", - }, - stake: { - private: "13e9e60b51768367c0d4c07a9f02b90d6511a9d7f7215b465fd87488171c687f", - public: "2819c4d6a988746ac7f5be3edc93c86d4cd0e3fae9c23a6ceeb23e6d0b207ad0", - pkh: "56ffa2a26e57c5b14c7c8d58455ebc24ce628f1c456be7e2e7448c8f", - }, - address: - "addr_test1qrd2rh2grqgn86ep5dmd5ae6r5clwg5pqzxhjrk2ezzll96kl732ymjhckc5clydtpz4a0pyee3g78z9d0n79e6y3j8smc7gzu", - dRepId: "drep12ml69gnw2lzmznru34vy2h4uyn8x9rcug4470ch8gjxg74htere", -}; - -export const adaHolder02Wallet: StaticWallet = { - payment: { - private: "6794cee96fd24aa68ae6e7df8548c15c6faf0373fc53c9714517b7c09b2ba6c0", - public: "58df2c02b5a2af09b51d5357f675b4f13ee019db57686adaab7536f6a3b8c29f", - pkh: "aec3d01a7fa061d027e945aedcfcb8e32eab0390063d19ef8ab89a88", - }, - stake: { - private: "cb4fa9d68add76c15b2b16b4bb7aeab043d95f42f4adec3d9d50396e6d1760e4", - public: "45dec9c5c130c23b1b9fedda680bdf1658e918087bd0f51c5548c471ca7d2991", - pkh: "49da6cb42b23f1c1f25f85e91dd325414b154f036bbf43a69dea27ee", - }, - address: - "addr_test1qzhv85q607sxr5p8a9z6ah8uhr3ja2crjqrr6x0032uf4zzfmfktg2er78qlyhu9aywaxf2pfv257qmthap6d802ylhqvz8qsf", - dRepId: "drep1f8dxedpty0curujlsh53m5e9g9932ncrdwl58f5aagn7u9psjya", -}; - -// export const soleVoterWallet: StaticWallet = { -// payment: { -// private: "98d35ef14dedc4520ed0153bc41e4db884deb0390f659ee1e28bb52da6045d4e", -// public: "5206735d1a1a1ac4ac625c973581ed97daec145d2e47a5c9bb14754527929f78", -// pkh: "0b5aa57cfd8010b00c649bf281520514de4efd952eba9c31eb7db187", -// }, -// stake: { -// private: "95ae1de1c2984c18207b8f57c450f1fbc54c2f0f1b878d3b24df11157277e1d1", -// public: "3bca5cb3599020808f69df269eb42b0e66ecf7455fb969e90c46ae0ac55e6572", -// pkh: "97265a1e13717c04a85e7d6dc156ba38340645b1e812a935823092f9", -// }, -// address: -// "addr_test1qq944ftulkqppvqvvjdl9q2jq52dunhaj5ht48p3ad7mrpuhyedpuym30sz2shnadhq4dw3cxsrytv0gz25ntq3sjtusesfzgd", -// dRepId: "drep1jun958snw97qf2z704kuz4468q6qv3d3aqf2jdvzxzf0jtlmwlf", -// }; +export const adaHolder01Wallet = staticWallets[3]; +export const adaHolder02Wallet = staticWallets[4]; +export const adaHolder03Wallet = staticWallets[6]; +export const adaHolder04Wallet = staticWallets[7]; // Does not takes part in transaction -export const user01Wallet: StaticWallet = { - payment: { - private: "a84d81412e41b55f9a484ce2cb5849660c7a8874df7ea11cb48120ec8efd2911", - public: "18cc2696ff588c19789f908df838fc58dd58986ebf6191b7b63d310c997f968b", - pkh: "56a6427fa7f8599d1e49271eb8123d0e02bc06bd44fc2d988e25455a", - }, - stake: { - private: "c3d9fde8d81c1533ab9a1e6fdbaddd792dfba8f58656c95e83f5e967087f4605", - public: "dd50a02daa77061ecd2d43d6fa1049e3db0192b94112ffc4f4e48975362987ff", - pkh: "cac600470e3b3027bf3ad3c363c35c690a54308bd001120194c1ba0b", - }, - address: - "addr_test1qpt2vsnl5lu9n8g7fyn3awqj858q90qxh4z0ctvc3cj52kk2ccqywr3mxqnm7wkncd3uxhrfpf2rpz7sqyfqr9xphg9s77zxlg", - dRepId: "drep1etrqq3cw8vcz00e660pk8s6udy99gvyt6qq3yqv5cxaqkuupyzg", -}; +export const user01Wallet: StaticWallet = staticWallets[5]; -export const adaHolderWallets = [adaHolder01Wallet, adaHolder02Wallet]; +export const adaHolderWallets = [ + adaHolder01Wallet, + adaHolder02Wallet, + adaHolder03Wallet, + adaHolder04Wallet, +]; export const userWallets = [user01Wallet]; -export const dRepWallets = [dRep01Wallet]; - -// export const soleVoterWallets = [soleVoterWallet]; +export const dRepWallets = [dRep01Wallet, dRep02Wallet]; diff --git a/tests/govtool-frontend/playwright/lib/datafactory/createAuth.ts b/tests/govtool-frontend/playwright/lib/datafactory/createAuth.ts index 05da4e0e5..0b23467d9 100644 --- a/tests/govtool-frontend/playwright/lib/datafactory/createAuth.ts +++ b/tests/govtool-frontend/playwright/lib/datafactory/createAuth.ts @@ -22,7 +22,7 @@ export async function createTempDRepAuth(page: Page, wallet: ShelleyWallet) { export async function createTempAdaHolderAuth( page: Page, - wallet: ShelleyWallet, + wallet: ShelleyWallet ) { await importWallet(page, wallet.json()); @@ -34,7 +34,9 @@ export async function createTempAdaHolderAuth( return tempAdaHolderAuth; } -export async function createTempUserAuth(page: Page) { +export async function createTempUserAuth(page: Page, wallet: ShelleyWallet) { + await importWallet(page, wallet.json()); + const loginPage = new LoginPage(page); await loginPage.login(); await loginPage.isLoggedIn(); diff --git a/tests/govtool-frontend/playwright/lib/fixtures/createWallet.ts b/tests/govtool-frontend/playwright/lib/fixtures/createWallet.ts index 3d6973292..0ab7f1c04 100644 --- a/tests/govtool-frontend/playwright/lib/fixtures/createWallet.ts +++ b/tests/govtool-frontend/playwright/lib/fixtures/createWallet.ts @@ -7,7 +7,7 @@ import { Page } from "@playwright/test"; export default async function createWallet( page: Page, - config?: CardanoTestWalletConfig, + config?: CardanoTestWalletConfig ) { const wallet = (await ShelleyWallet.generate()).json(); diff --git a/tests/govtool-frontend/playwright/lib/fixtures/importWallet.ts b/tests/govtool-frontend/playwright/lib/fixtures/importWallet.ts index d04a58a71..d825f8cf9 100644 --- a/tests/govtool-frontend/playwright/lib/fixtures/importWallet.ts +++ b/tests/govtool-frontend/playwright/lib/fixtures/importWallet.ts @@ -4,7 +4,7 @@ import { StaticWallet } from "@types"; export async function importWallet( page: Page, - wallet: StaticWallet | CardanoTestWallet, + wallet: StaticWallet | CardanoTestWallet ) { await page.addInitScript((wallet) => { // @ts-ignore diff --git a/tests/govtool-frontend/playwright/lib/fixtures/loadExtension.ts b/tests/govtool-frontend/playwright/lib/fixtures/loadExtension.ts index 5634fa3a3..44cea5367 100644 --- a/tests/govtool-frontend/playwright/lib/fixtures/loadExtension.ts +++ b/tests/govtool-frontend/playwright/lib/fixtures/loadExtension.ts @@ -6,11 +6,11 @@ import path = require("path"); export default async function loadDemosExtension( page: Page, - enableStakeSigning = false, + enableStakeSigning = false ) { const demosBundleScriptPath = path.resolve( __dirname, - "../../node_modules/@cardanoapi/cardano-test-wallet/script.js", + "../../node_modules/@cardanoapi/cardano-test-wallet/script.js" ); let walletConfig: CardanoTestWalletConfig = { enableStakeSigning, diff --git a/tests/govtool-frontend/playwright/lib/helpers/allure.ts b/tests/govtool-frontend/playwright/lib/helpers/allure.ts new file mode 100644 index 000000000..ec3f5823c --- /dev/null +++ b/tests/govtool-frontend/playwright/lib/helpers/allure.ts @@ -0,0 +1,18 @@ +import { allure } from "allure-playwright"; +import { isMobile } from "./mobile"; +import { chromium } from "@playwright/test"; + +export const setAllureEpic = async (groupName: string) => { + const browser = await chromium.launch(); + const page = await browser.newPage(); + if (isMobile(page)) { + await allure.epic("6. Miscellaneous"); + await allure.story("6A. Should be accessible from mobile"); + } else { + await allure.epic(groupName); + } +}; + +export const setAllureStory = async (groupName: string) => { + await allure.story(groupName); +}; diff --git a/tests/govtool-frontend/playwright/lib/helpers/crypto.ts b/tests/govtool-frontend/playwright/lib/helpers/crypto.ts index 24fe8fd26..9c7afc05b 100644 --- a/tests/govtool-frontend/playwright/lib/helpers/crypto.ts +++ b/tests/govtool-frontend/playwright/lib/helpers/crypto.ts @@ -29,7 +29,7 @@ export class Ed25519Key { } public static async fromPrivateKeyHex(privKey) { return await Ed25519Key.fromPrivateKey( - Uint8Array.from(Buffer.from(privKey, "hex")), + Uint8Array.from(Buffer.from(privKey, "hex")) ); } @@ -59,20 +59,20 @@ export class Ed25519Key { public static fromJson(json: any): Ed25519Key { if (!json || typeof json !== "object") { throw new Error( - "Invalid JSON format for Ed25519Key: Input must be a non-null object.", + "Invalid JSON format for Ed25519Key: Input must be a non-null object." ); } if (!json.private || !json.public || !json.pkh) { throw new Error( - "Invalid JSON format for Ed25519Key: Missing required fields (private, public, or pkh).", + "Invalid JSON format for Ed25519Key: Missing required fields (private, public, or pkh)." ); } return new Ed25519Key( Uint8Array.from(Buffer.from(json.private, "hex")), Uint8Array.from(Buffer.from(json.public, "hex")), - Uint8Array.from(Buffer.from(json.pkh, "hex")), + Uint8Array.from(Buffer.from(json.pkh, "hex")) ); } } @@ -92,7 +92,7 @@ export class ShelleyWallet { public static async generate() { const wallet = new ShelleyWallet( await Ed25519Key.generate(), - await Ed25519Key.generate(), + await Ed25519Key.generate() ); return wallet; } @@ -102,7 +102,7 @@ export class ShelleyWallet { return bech32.encode( prefix, bech32.toWords(Buffer.from(this.addressRawBytes(networkId))), - 200, + 200 ); } @@ -127,7 +127,7 @@ export class ShelleyWallet { return bech32.encode( prefix, bech32.toWords(Buffer.from(this.rewardAddressRawBytes(networkId))), - 200, + 200 ); } public json() { @@ -150,18 +150,18 @@ export class ShelleyWallet { if (!paymentKey || typeof paymentKey !== "object") { throw new Error( - "ShelleyWallet.fromJson : Invalid payment key: It must be an object.", + "ShelleyWallet.fromJson : Invalid payment key: It must be an object." ); } if (!stakeKey || typeof stakeKey !== "object") { throw new Error( - "ShelleyWallet.fromJson : Invalid stake key: It must be an object.", + "ShelleyWallet.fromJson : Invalid stake key: It must be an object." ); } return new ShelleyWallet( Ed25519Key.fromJson(paymentKey), - Ed25519Key.fromJson(stakeKey), + Ed25519Key.fromJson(stakeKey) ); } @@ -198,7 +198,7 @@ export class ShelleyWalletAddress implements Address { private constructor( network: number | "mainnet" | "testnet", pkh: Uint8Array, - skh: Uint8Array, + skh: Uint8Array ) { this.network = network == "mainnet" ? 1 : network == "testnet" ? 0 : network; @@ -215,7 +215,7 @@ export class ShelleyWalletAddress implements Address { "ShelleyAddress.fromRawBytes: Invalid byte array length. expected: " + ADDR_LENGTH + " got: " + - bytea.length, + bytea.length ); } bytebuffer = Buffer.from(bytea); @@ -227,7 +227,7 @@ export class ShelleyWalletAddress implements Address { return new ShelleyWalletAddress( bytebuffer.at(0), paymentKeyHash, - stakeKeyHash, + stakeKeyHash ); } toBech32(): string { @@ -235,7 +235,7 @@ export class ShelleyWalletAddress implements Address { return bech32.encode( prefix, bech32.toWords(Buffer.from(this.toRawBytes())), - 200, + 200 ); } toRawBytes(): Uint8Array { diff --git a/tests/govtool-frontend/playwright/lib/helpers/exceptionHandler.ts b/tests/govtool-frontend/playwright/lib/helpers/exceptionHandler.ts new file mode 100644 index 000000000..7410a57c4 --- /dev/null +++ b/tests/govtool-frontend/playwright/lib/helpers/exceptionHandler.ts @@ -0,0 +1,12 @@ +import { Expect, ExpectMatcherUtils } from "@playwright/test"; + +export async function expectWithInfo( + expectation: () => Promise, + errorMessage: string +) { + try { + await expectation(); + } catch (e) { + throw new Error(errorMessage); + } +} diff --git a/tests/govtool-frontend/playwright/lib/helpers/generateShellyWallets.ts b/tests/govtool-frontend/playwright/lib/helpers/generateShellyWallets.ts index 127cb577f..b7c1bd6db 100644 --- a/tests/govtool-frontend/playwright/lib/helpers/generateShellyWallets.ts +++ b/tests/govtool-frontend/playwright/lib/helpers/generateShellyWallets.ts @@ -1,7 +1,7 @@ import { ShelleyWallet } from "./crypto"; export default async function generateShellyWallets( - numWallets: number = 100, + numWallets: number = 100 ): Promise { const wallets: ShelleyWallet[] = []; diff --git a/tests/govtool-frontend/playwright/lib/helpers/mobile.ts b/tests/govtool-frontend/playwright/lib/helpers/mobile.ts index b0c0329ca..7552e09e9 100644 --- a/tests/govtool-frontend/playwright/lib/helpers/mobile.ts +++ b/tests/govtool-frontend/playwright/lib/helpers/mobile.ts @@ -8,9 +8,5 @@ export function isMobile(page: Page) { } export async function openDrawer(page: Page) { - await page.getByRole("img", { name: "drawer-icon" }).click(); //BUG testId -} - -export async function openDrawerLoggedIn(page: Page) { await page.getByTestId("open-drawer-button").click(); } diff --git a/tests/govtool-frontend/playwright/lib/helpers/page.ts b/tests/govtool-frontend/playwright/lib/helpers/page.ts index 8ab66186e..d3aca4d4b 100644 --- a/tests/govtool-frontend/playwright/lib/helpers/page.ts +++ b/tests/govtool-frontend/playwright/lib/helpers/page.ts @@ -1,6 +1,6 @@ import { importWallet } from "@fixtures/importWallet"; import loadDemosExtension from "@fixtures/loadExtension"; -import { Browser, Page } from "@playwright/test"; +import { Browser, Page, expect } from "@playwright/test"; import { ShelleyWallet } from "./crypto"; interface BrowserConfig { @@ -11,7 +11,7 @@ interface BrowserConfig { export async function createNewPageWithWallet( browser: Browser, - { storageState, wallet, enableStakeSigning }: BrowserConfig, + { storageState, wallet, enableStakeSigning }: BrowserConfig ): Promise { const context = await browser.newContext({ storageState: storageState, diff --git a/tests/govtool-frontend/playwright/lib/helpers/setupWallets.ts b/tests/govtool-frontend/playwright/lib/helpers/setupWallets.ts deleted file mode 100644 index ad64c4c85..000000000 --- a/tests/govtool-frontend/playwright/lib/helpers/setupWallets.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { faucetWallet } from "@constants/staticWallets"; -import { ShelleyWallet } from "@helpers/crypto"; -import kuberService from "@services/kuberService"; -import { pollTransaction } from "./transaction"; - -/* -Registers stake & fund wallets -*/ -export default async function setupWallets(wallets: ShelleyWallet[]) { - if (wallets.length === 0) { - throw new Error("No wallets to load balance"); - } - - const signingKey = faucetWallet.payment.private; - const { txId, address } = await kuberService.initializeWallets( - faucetWallet.address, - signingKey, - wallets, - ); - await pollTransaction(txId, address); - - console.debug(`[Setup Wallet] Successfully setup ${wallets.length} wallets`); -} diff --git a/tests/govtool-frontend/playwright/lib/helpers/extractDRepsFromStakePubkey.ts b/tests/govtool-frontend/playwright/lib/helpers/shellyWallet.ts similarity index 53% rename from tests/govtool-frontend/playwright/lib/helpers/extractDRepsFromStakePubkey.ts rename to tests/govtool-frontend/playwright/lib/helpers/shellyWallet.ts index f0983717e..e93062930 100644 --- a/tests/govtool-frontend/playwright/lib/helpers/extractDRepsFromStakePubkey.ts +++ b/tests/govtool-frontend/playwright/lib/helpers/shellyWallet.ts @@ -1,10 +1,14 @@ import { bech32 } from "bech32"; import { blake2bHex } from "blakejs"; +import convertBufferToHex from "./convertBufferToHex"; +import { ShelleyWallet } from "./crypto"; + +export default function extractDRepFromWallet(wallet: ShelleyWallet) { + const stakePubKey = convertBufferToHex(wallet.stakeKey.public); -export default function extractDRepsFromStakePubKey(stakePubKey: string) { const dRepKeyBytes = Buffer.from(stakePubKey, "hex"); const dRepId = blake2bHex(dRepKeyBytes, undefined, 28); const words = bech32.toWords(Buffer.from(dRepId, "hex")); const dRepIdBech32 = bech32.encode("drep", words); - return { dRepId, dRepIdBech32 }; + return dRepIdBech32; } diff --git a/tests/govtool-frontend/playwright/lib/helpers/transaction.ts b/tests/govtool-frontend/playwright/lib/helpers/transaction.ts index 5d455dd6f..009f0766a 100644 --- a/tests/govtool-frontend/playwright/lib/helpers/transaction.ts +++ b/tests/govtool-frontend/playwright/lib/helpers/transaction.ts @@ -3,6 +3,8 @@ import { Page, expect } from "@playwright/test"; import kuberService from "@services/kuberService"; import { LockInterceptor, LockInterceptorInfo } from "lib/lockInterceptor"; import { Logger } from "../../../cypress/lib/logger/logger"; +import convertBufferToHex from "./convertBufferToHex"; +import { ShelleyWallet } from "./crypto"; /** * Polls the transaction status until it's resolved or times out. @@ -10,7 +12,7 @@ import { Logger } from "../../../cypress/lib/logger/logger"; */ export async function pollTransaction( txHash: string, - lockInfo?: LockInterceptorInfo, + lockInfo?: LockInterceptorInfo ) { try { Logger.info(`Waiting for tx completion: ${txHash}`); @@ -23,7 +25,7 @@ export async function pollTransaction( }, { timeout: environments.txTimeOut, - }, + } ) .toBeGreaterThan(0); @@ -34,7 +36,7 @@ export async function pollTransaction( await LockInterceptor.releaseLockForAddress( lockInfo.address, lockInfo.lockId, - `Task completed for:${lockInfo.lockId}`, + `Task completed for:${lockInfo.lockId}` ); } catch (err) { if (lockInfo) { @@ -43,7 +45,7 @@ export async function pollTransaction( await LockInterceptor.releaseLockForAddress( lockInfo.address, lockInfo.lockId, - `Task failure: \n${JSON.stringify(errorMessage)}`, + `Task failure: \n${JSON.stringify(errorMessage)}` ); } @@ -53,7 +55,7 @@ export async function pollTransaction( export async function waitForTxConfirmation( page: Page, - triggerCallback?: () => Promise, + triggerCallback?: () => Promise ) { let transactionHash: string | undefined; const transactionStatusPromise = page.waitForRequest((request) => { @@ -64,9 +66,9 @@ export async function waitForTxConfirmation( await expect( page .getByTestId("alert-warning") - .getByText("Transaction in progress", { exact: false }), + .getByText("Transaction in progress", { exact: false }) ).toBeVisible({ - timeout: 10000, + timeout: 10_000, }); const url = (await transactionStatusPromise).url(); const regex = /\/transaction\/status\/([^\/]+)$/; @@ -77,6 +79,37 @@ export async function waitForTxConfirmation( if (transactionHash) { await pollTransaction(transactionHash); - await page.reload(); + await expect( + page.getByText("In Progress", { exact: true }).first() //FIXME: Only one element needs to be displayed + ).not.toBeVisible({ timeout: 20_000 }); } } + +export async function registerStakeForWallet(wallet: ShelleyWallet) { + const { txId, lockInfo } = await kuberService.registerStake( + convertBufferToHex(wallet.stakeKey.private), + convertBufferToHex(wallet.stakeKey.pkh), + convertBufferToHex(wallet.paymentKey.private), + wallet.addressBech32(environments.networkId) + ); + await pollTransaction(txId, lockInfo); +} + +export async function transferAdaForWallet( + wallet: ShelleyWallet, + amount?: number +) { + const { txId, lockInfo } = await kuberService.transferADA( + [wallet.addressBech32(environments.networkId)], + amount + ); + await pollTransaction(txId, lockInfo); +} + +export async function registerDRepForWallet(wallet: ShelleyWallet) { + const registrationRes = await kuberService.dRepRegistration( + convertBufferToHex(wallet.stakeKey.private), + convertBufferToHex(wallet.stakeKey.pkh) + ); + await pollTransaction(registrationRes.txId, registrationRes.lockInfo); +} diff --git a/tests/govtool-frontend/playwright/lib/lockInterceptor.ts b/tests/govtool-frontend/playwright/lib/lockInterceptor.ts index 6e56e237f..8db59c067 100644 --- a/tests/govtool-frontend/playwright/lib/lockInterceptor.ts +++ b/tests/govtool-frontend/playwright/lib/lockInterceptor.ts @@ -13,13 +13,13 @@ export interface LockInterceptorInfo { export class LockInterceptor { private static async acquireLock( address: string, - lockId: string, + lockId: string ): Promise { - const lockFilePath = path.resolve(__dirname, `../.lock-pool/${address}`); + const lockFilePath = path.resolve(__dirname, `../${address}`); try { await log( - `Initiator: ${address} \n---------------------> acquiring lock for:${lockId}`, + `Initiator: ${address} \n---------------------> acquiring lock for:${lockId}` ); await new Promise((resolve, reject) => { lockfile.lock(lockFilePath, (err) => { @@ -31,7 +31,7 @@ export class LockInterceptor { }); }); await log( - `Initiator: ${address} \n---------------------> acquired lock for:${lockId}`, + `Initiator: ${address} \n---------------------> acquired lock for:${lockId}` ); } catch (err) { throw err; @@ -40,13 +40,13 @@ export class LockInterceptor { private static async releaseLock( address: string, - lockId: string, + lockId: string ): Promise { - const lockFilePath = path.resolve(__dirname, `../.lock-pool/${address}`); + const lockFilePath = path.resolve(__dirname, `../${address}`); try { await log( - `Initiator: ${address} \n---------------------> releasing lock for:${lockId}`, + `Initiator: ${address} \n---------------------> releasing lock for:${lockId}` ); await new Promise((resolve, reject) => { lockfile.unlock(lockFilePath, async (err) => { @@ -58,7 +58,7 @@ export class LockInterceptor { }); }); await log( - `Initiator: ${address} \n---------------------> released lock for:${lockId}\n`, + `Initiator: ${address} \n---------------------> released lock for:${lockId}\n` ); } catch (err) { throw err; @@ -67,13 +67,13 @@ export class LockInterceptor { private static async waitForReleaseLock( address: string, - lockId: string, + lockId: string ): Promise { const pollInterval = 4000; // 4 secs try { await log( - `Initiator: ${address} \n ---------------------> waiting lock for:${lockId}`, + `Initiator: ${address} \n ---------------------> waiting lock for:${lockId}` ); return new Promise((resolve, reject) => { const pollFn = () => { @@ -100,7 +100,7 @@ export class LockInterceptor { address: string, callbackFn: () => Promise, lockId: string, - provider: "local" | "server" = "local", + provider: "local" | "server" = "local" ): Promise { while (true) { const isAddressLocked = checkAddressLock(address); @@ -134,7 +134,7 @@ export class LockInterceptor { static async releaseLockForAddress( address: string, lockId: string, - message?: string, + message?: string ) { try { message && (await log(message)); @@ -147,7 +147,7 @@ export class LockInterceptor { } function checkAddressLock(address: string): boolean { - const lockFilePath = path.resolve(__dirname, `../.lock-pool/${address}`); + const lockFilePath = path.resolve(__dirname, `../${address}`); return lockfile.checkSync(lockFilePath); } @@ -162,7 +162,7 @@ function log(message: string): Promise { hour12: false, timeZone: "Asia/Kathmandu", }; - const logFilePath = path.resolve(__dirname, "../.logs/lock_logs.txt"); + const logFilePath = path.resolve(__dirname, "../lock_logs.txt"); const logMessage = `[${new Date().toLocaleString("en-US", options)}] ${message}\n`; return new Promise((resolve, reject) => { fs.appendFile(logFilePath, logMessage, (err) => { diff --git a/tests/govtool-frontend/playwright/lib/pages/dRepDetailsPage.ts b/tests/govtool-frontend/playwright/lib/pages/dRepDetailsPage.ts new file mode 100644 index 000000000..9924669a2 --- /dev/null +++ b/tests/govtool-frontend/playwright/lib/pages/dRepDetailsPage.ts @@ -0,0 +1,18 @@ +import { Page } from "@playwright/test"; + +export default class DRepDetailsPage { + readonly copyIdBtn = this.page.getByTestId("copy-drep-id-button"); + readonly delegateBtn = this.page.getByTestId("delegate-button"); + readonly shareBtn = this.page.getByTestId("share-button"); + + constructor(private readonly page: Page) {} + + async goto(dRepId: string) { + await this.page.goto(`/connected/drep_directory/${dRepId}`); + } + + async shareLink() { + await this.shareBtn.click(); + await this.page.getByTestId("copy-link-from-share-button").click(); + } +} diff --git a/tests/govtool-frontend/playwright/lib/pages/dRepDirectoryPage.ts b/tests/govtool-frontend/playwright/lib/pages/dRepDirectoryPage.ts new file mode 100644 index 000000000..10f7e9758 --- /dev/null +++ b/tests/govtool-frontend/playwright/lib/pages/dRepDirectoryPage.ts @@ -0,0 +1,134 @@ +import { Page, expect } from "@playwright/test"; +import { IDRep } from "@types"; +import environments from "lib/constants/environments"; +import { withTxConfirmation } from "lib/transaction.decorator"; + +export default class DRepDirectoryPage { + readonly otherOptionsBtn = this.page.getByText("Other options"); + readonly nextStepBtn = this.page.getByTestId("next-step-button"); + readonly dRepInput = this.page.getByRole("textbox"); + readonly searchInput = this.page.getByTestId("search-input"); + readonly filterBtn = this.page.getByTestId("filters-button"); + readonly sortBtn = this.page.getByTestId("sort-button"); + readonly showMoreBtn = this.page.getByTestId("show-more-button"); + + readonly automaticDelegationOptionsDropdown = this.page.getByRole("button", { + name: "Automated Voting Options arrow", + }); // BUG: testId -> delegation-options-dropdown + + readonly delegateToDRepCard = this.page.getByTestId("delegate-to-drep-card"); + readonly signalNoConfidenceCard = this.page + .getByRole("region") + .locator("div") + .filter({ hasText: "Signal No Confidence on Every" }) + .nth(2); // BUG: testId -> signal-no-confidence-card + readonly abstainDelegationCard = this.page.getByText( + "Abstain from Every VoteSelect this to vote ABSTAIN to every vote.Voting Power₳" + ); // BUG: testId -> abstain-delegation-card + + readonly delegationErrorModal = this.page.getByTestId( + "delegate-transaction-error-modal" + ); + + readonly delegateBtns = this.page.locator( + '[data-testid$="-delegate-button"]' + ); + + constructor(private readonly page: Page) {} + + async goto() { + await this.page.goto( + `${environments.frontendUrl}/connected/dRep_directory` + ); + } + + @withTxConfirmation + async delegateToDRep(dRepId: string) { + await this.searchInput.fill(dRepId); + const delegateBtn = this.page.getByTestId(`${dRepId}-delegate-button`); + await expect(delegateBtn).toBeVisible(); + await this.page.getByTestId(`${dRepId}-delegate-button`).click(); + await this.searchInput.clear(); + } + + async resetDRepForm() { + if (await this.delegationErrorModal.isVisible()) { + await this.page.getByTestId("confirm-modal-button").click(); + } + await this.dRepInput.clear(); + } + async filterDReps(filterOptions: string[]) { + for (const option of filterOptions) { + await this.page.getByTestId(`${option}-checkbox`).click(); + } + } + + async unFilterDReps(filterOptions: string[]) { + for (const option of filterOptions) { + await this.page.getByTestId(`${option}-checkbox`).click(); + } + } + + async validateFilters(filters: string[], filterOptions: string[]) { + const excludedFilters = filterOptions.filter( + (filter) => !filters.includes(filter) + ); + + for (const filter of excludedFilters) { + await expect( + this.page.getByText(filter, { exact: true }), + `Expected "${filter}" to be excluded, but it's included` + ).toHaveCount(1); + } + + for (const filter of filters) { + expect( + (await this.page.getByText(filter, { exact: true }).all(), + `Expected to find "${filter}"`).length + ).toBeGreaterThanOrEqual(0); + } + } + + async sortDRep(option: string) {} + + async sortAndValidate( + option: string, + validationFn: (p1: IDRep, p2: IDRep) => boolean + ) { + const responsePromise = this.page.waitForResponse((response) => + response.url().includes(`&sort=${option}`) + ); + + await this.page.getByTestId(`${option}-radio`).click(); + const response = await responsePromise; + + const dRepList: IDRep[] = (await response.json()).elements; + + // API validation + for (let i = 0; i <= dRepList.length - 2; i++) { + const isValid = validationFn(dRepList[i], dRepList[i + 1]); + expect(isValid, "API Sorting validation failed").toBe(true); + } + + // Frontend validation + const dRepListFE = await this.getAllListedDRepIds(); + + for (let i = 0; i <= dRepListFE.length - 1; i++) { + expect(dRepListFE[i], "Frontend validation failed").toHaveText( + dRepList[i].view + ); + } + } + getDRepCard(dRepId: string) { + return this.page.getByRole("list").getByTestId(`${dRepId}-copy-id-button`); + } + + async getAllListedDRepIds() { + await this.page.waitForTimeout(2_000); + + return await this.page + .getByRole("list") + .locator('[data-testid$="-copy-id-button"]') + .all(); + } +} diff --git a/tests/govtool-frontend/playwright/lib/pages/dRepRegistrationPage.ts b/tests/govtool-frontend/playwright/lib/pages/dRepRegistrationPage.ts index 124f2e560..a8920d583 100644 --- a/tests/govtool-frontend/playwright/lib/pages/dRepRegistrationPage.ts +++ b/tests/govtool-frontend/playwright/lib/pages/dRepRegistrationPage.ts @@ -1,24 +1,35 @@ import { downloadMetadata } from "@helpers/metadata"; -import { Download, Page } from "@playwright/test"; +import { Download, Page, expect } from "@playwright/test"; import metadataBucketService from "@services/metadataBucketService"; import { IDRepInfo } from "@types"; import environments from "lib/constants/environments"; +import { withTxConfirmation } from "lib/transaction.decorator"; + +const formErrors = { + dRepName: [ + "max-80-characters-error", + "this-field-is-required-error", + "nickname-can-not-contain-whitespaces-error", + ], + email: "invalid-email-address-error", + link: "invalid-url-error", +}; export default class DRepRegistrationPage { readonly registerBtn = this.page.getByTestId("register-button"); readonly skipBtn = this.page.getByTestId("skip-button"); readonly confirmBtn = this.page.getByTestId("confirm-modal-button"); readonly registrationSuccessModal = this.page.getByTestId( - "governance-action-submitted-modal", + "governance-action-submitted-modal" ); - readonly continueBtn = this.page.getByTestId("retire-button"); // BUG testId -> continue-button - readonly addLinkBtn = this.page.getByRole("button", { name: "+ Add link" }); // BUG: testId -> add-link-button + readonly continueBtn = this.page.getByTestId("continue-button"); + readonly addLinkBtn = this.page.getByTestId("add-link-button"); // input fields - readonly nameInput = this.page.getByPlaceholder("ex. JohnDRep"); // BUG testId - readonly emailInput = this.page.getByPlaceholder("john.smith@email.com"); // BUG testId - readonly bioInput = this.page.getByPlaceholder("Enter your Bio"); // BUG testId - readonly linkInput = this.page.getByPlaceholder("https://website.com/"); // BUG: testId + readonly nameInput = this.page.getByTestId("name-input"); + readonly emailInput = this.page.locator('[data-testid="email-input"] input'); // BUG incorrect cannot interact with text input + readonly bioInput = this.page.getByTestId("bio-input"); + readonly linkInput = this.page.locator('[data-testid="link-input"] input'); // BUG incorrect cannot interact with text input constructor(private readonly page: Page) {} @@ -27,7 +38,8 @@ export default class DRepRegistrationPage { await this.continueBtn.click(); // BUG: testId -> continue-register-button } - async register(dRepInfo: IDRepInfo = { name: "Test_dRep" }) { + @withTxConfirmation + async register(dRepInfo: IDRepInfo) { await this.nameInput.fill(dRepInfo.name); if (dRepInfo.email != null) { @@ -38,28 +50,82 @@ export default class DRepRegistrationPage { } if (dRepInfo.extraContentLinks != null) { for (let i = 0; i < dRepInfo.extraContentLinks.length; i++) { + if (i > 0) { + await this.addLinkBtn.click(); + } await this.linkInput.nth(i).fill(dRepInfo.extraContentLinks[i]); } } + await this.continueBtn.click(); + await this.page.getByRole("checkbox").click(); + await this.continueBtn.click(); - this.page - .getByRole("button", { name: "download Vote_Context.jsonld" }) - .click(); + this.page.getByRole("button", { name: `${dRepInfo.name}.jsonld` }).click(); const dRepMetadata = await this.downloadVoteMetadata(); const url = await metadataBucketService.uploadMetadata( dRepMetadata.name, - dRepMetadata.data, + dRepMetadata.data ); - await this.continueBtn.click(); // BUG: testId -> submit-button - await this.page.getByRole("checkbox").click(); - await this.continueBtn.click(); // BUG: testId -> submit-button await this.page.getByPlaceholder("URL").fill(url); - await this.continueBtn.click(); + await this.page.getByTestId("register-button").click(); } async downloadVoteMetadata() { const download: Download = await this.page.waitForEvent("download"); return downloadMetadata(download); } + + async validateForm(name: string, email: string, bio: string, link: string) { + await this.nameInput.fill(name); + await this.emailInput.fill(email); + await this.bioInput.fill(bio); + await this.linkInput.fill(link); + + for (const err of formErrors.dRepName) { + await expect(this.page.getByTestId(err)).toBeHidden(); + } + + await expect(this.page.getByTestId(formErrors.email)).toBeHidden(); + + expect(await this.bioInput.textContent()).toEqual(bio); + + await expect(this.page.getByTestId(formErrors.link)).toBeHidden(); + + await expect(this.continueBtn).toBeEnabled(); + } + + async inValidateForm(name: string, email: string, bio: string, link: string) { + await this.nameInput.fill(name); + await this.emailInput.fill(email); + await this.bioInput.fill(bio); + await this.linkInput.fill(link); + + function convertTestIdToText(testId: string) { + let text = testId.replace("-error", ""); + text = text.replace(/-/g, " "); + return text[0].toUpperCase() + text.substring(1); + } + + const regexPattern = new RegExp( + formErrors.dRepName.map(convertTestIdToText).join("|") + ); + + const nameErrors = await this.page + .locator('[data-testid$="-error"]') + .filter({ + hasText: regexPattern, + }) + .all(); + + expect(nameErrors.length).toEqual(1); + + await expect(this.page.getByTestId(formErrors.email)).toBeVisible(); + + expect(await this.bioInput.textContent()).not.toEqual(bio); + + await expect(this.page.getByTestId(formErrors.link)).toBeVisible(); + + await expect(this.continueBtn).toBeDisabled(); + } } diff --git a/tests/govtool-frontend/playwright/lib/pages/delegationPage.ts b/tests/govtool-frontend/playwright/lib/pages/delegationPage.ts deleted file mode 100644 index 50a0f8f1e..000000000 --- a/tests/govtool-frontend/playwright/lib/pages/delegationPage.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { Page, expect } from "@playwright/test"; -import environments from "lib/constants/environments"; -import { withTxConfirmation } from "lib/transaction.decorator"; - -export default class DelegationPage { - readonly otherOptionsBtn = this.page.getByText("Other options"); - readonly nextStepBtn = this.page.getByTestId("next-step-button"); - readonly dRepInput = this.page.getByRole("textbox"); - readonly searchInput = this.page.getByTestId("search-input"); - - readonly delegationOptionsDropdown = this.page.getByRole("button", { - name: "Automated Voting Options arrow", - }); // BUG: testId -> delegation-options-dropdown - - readonly delegateToDRepCard = this.page.getByTestId("delegate-to-drep-card"); - readonly signalNoConfidenceCard = this.page - .getByRole("region") - .locator("div") - .filter({ hasText: "Signal No Confidence on Every" }) - .nth(2); // BUG: testId -> signal-no-confidence-card - readonly abstainDelegationCard = this.page.getByText( - "Abstain from Every VoteSelect this to vote ABSTAIN to every vote.Voting Power₳", - ); // BUG: testId -> abstain-delegation-card - - readonly delegationErrorModal = this.page.getByTestId( - "delegation-transaction-error-modal", - ); - - readonly delegateBtns = this.page.locator( - '[data-testid$="-delegate-button"]', - ); - - constructor(private readonly page: Page) {} - - async goto() { - await this.page.goto( - `${environments.frontendUrl}/connected/dRep_directory`, - ); - } - - @withTxConfirmation - async delegateToDRep(dRepId: string) { - await this.searchInput.fill(dRepId); - const delegateBtn = this.page.getByTestId(`${dRepId}-delegate-button`); - await expect(delegateBtn).toBeVisible(); - await this.page.getByTestId(`${dRepId}-delegate-button`).click(); - } - - async resetDRepForm() { - if (await this.delegationErrorModal.isVisible()) { - await this.page.getByTestId("confirm-modal-button").click(); - } - await this.dRepInput.clear(); - } -} diff --git a/tests/govtool-frontend/playwright/lib/pages/governanceActionDetailsPage.ts b/tests/govtool-frontend/playwright/lib/pages/governanceActionDetailsPage.ts index 49cffbe00..8b079d8ef 100644 --- a/tests/govtool-frontend/playwright/lib/pages/governanceActionDetailsPage.ts +++ b/tests/govtool-frontend/playwright/lib/pages/governanceActionDetailsPage.ts @@ -11,7 +11,7 @@ export default class GovernanceActionDetailsPage { readonly noVoteRadio = this.page.getByTestId("no-radio"); readonly abstainRadio = this.page.getByTestId("abstain-radio"); readonly governanceActionType = this.page.getByText( - "Governance Action Type:", + "Governance Action Type:" ); readonly showVotesBtn = this.page.getByTestId("show-votes-button"); readonly submittedDate = this.page.getByTestId("submission-date"); @@ -23,7 +23,7 @@ export default class GovernanceActionDetailsPage { name: "Provide context about your", }); // BUG testId readonly viewOtherDetailsLink = this.page.getByTestId( - "view-other-details-button", + "view-other-details-button" ); readonly continueModalBtn = this.page.getByTestId("continue-modal-button"); readonly confirmModalBtn = this.page.getByTestId("confirm-modal-button"); @@ -31,7 +31,7 @@ export default class GovernanceActionDetailsPage { readonly voteSuccessModal = this.page.getByTestId("alert-success"); readonly externalLinkModal = this.page.getByTestId("external-link-modal"); - readonly contextInput = this.page.getByPlaceholder("Provide context"); // BUG testId + readonly contextInput = this.page.getByTestId("provide-context-input"); readonly cancelModalBtn = this.page.getByTestId("cancel-modal-button"); constructor(private readonly page: Page) {} @@ -42,7 +42,7 @@ export default class GovernanceActionDetailsPage { async goto(proposalId: string) { await this.page.goto( - `${environments.frontendUrl}/governance_actions/${proposalId}`, + `${environments.frontendUrl}/governance_actions/${proposalId}` ); } diff --git a/tests/govtool-frontend/playwright/lib/pages/governanceActionsPage.ts b/tests/govtool-frontend/playwright/lib/pages/governanceActionsPage.ts index 07bd1b603..312ecc0ba 100644 --- a/tests/govtool-frontend/playwright/lib/pages/governanceActionsPage.ts +++ b/tests/govtool-frontend/playwright/lib/pages/governanceActionsPage.ts @@ -16,7 +16,7 @@ export default class GovernanceActionsPage { } async viewProposal( - proposal: IProposal, + proposal: IProposal ): Promise { const proposalId = `govaction-${proposal.txHash}#${proposal.index}-view-detail`; await this.page.getByTestId(proposalId).click(); @@ -41,7 +41,7 @@ export default class GovernanceActionsPage { } async viewVotedProposal( - proposal: IProposal, + proposal: IProposal ): Promise { const proposalId = `govaction-${proposal.txHash}#${proposal.index}-change-your-vote`; await this.page.getByTestId(proposalId).click(); @@ -64,6 +64,7 @@ export default class GovernanceActionsPage { } async getAllProposals() { + await this.page.waitForTimeout(2000); return this.page.locator('[data-test-id$="-card"]').all(); } @@ -73,11 +74,11 @@ export default class GovernanceActionsPage { for (const proposalCard of proposalCards) { const hasFilter = await this._validateFiltersInProposalCard( proposalCard, - filters, + filters ); expect( hasFilter, - "A proposal card does not contain any of the filters", + "A proposal card does not contain any of the filters" ).toBe(true); } } @@ -89,21 +90,21 @@ export default class GovernanceActionsPage { async validateSort( sortOption: string, validationFn: (p1: IProposal, p2: IProposal) => boolean, - filterKeys = Object.keys(FilterOption), + filterKeys = Object.keys(FilterOption) ) { const responses = await Promise.all( filterKeys.map((filterKey) => this.page.waitForResponse((response) => response .url() - .includes(`&type[]=${FilterOption[filterKey]}&sort=${sortOption}`), - ), - ), + .includes(`&type[]=${FilterOption[filterKey]}&sort=${sortOption}`) + ) + ) ); const proposalData = await Promise.all( responses.map(async (response) => { return await response.json(); - }), + }) ); expect(proposalData.length, "No proposals to sort").toBeGreaterThan(0); @@ -118,11 +119,12 @@ export default class GovernanceActionsPage { } }); + await this.page.waitForTimeout(2000); // Frontend validation const proposalCards = await Promise.all( filterKeys.map((key) => - this.page.getByTestId(`govaction-${key}-card`).allInnerTexts(), - ), + this.page.getByTestId(`govaction-${key}-card`).allInnerTexts() + ) ); for (let dIdx = 0; dIdx <= proposalData.length - 1; dIdx++) { @@ -130,7 +132,7 @@ export default class GovernanceActionsPage { for (let i = 0; i <= proposals.length - 1; i++) { expect( proposalCards[dIdx][i].includes(proposals[i].txHash), - "Frontend validation failed", + "Frontend validation failed" ).toBe(true); } } @@ -138,7 +140,7 @@ export default class GovernanceActionsPage { async _validateFiltersInProposalCard( proposalCard: Locator, - filters: string[], + filters: string[] ): Promise { for (const filter of filters) { try { diff --git a/tests/govtool-frontend/playwright/lib/pages/loginPage.ts b/tests/govtool-frontend/playwright/lib/pages/loginPage.ts index 1eaabc96f..6ebbc3dba 100644 --- a/tests/govtool-frontend/playwright/lib/pages/loginPage.ts +++ b/tests/govtool-frontend/playwright/lib/pages/loginPage.ts @@ -2,7 +2,7 @@ import { CIP30Instance, Cip95Instance, } from "@cardanoapi/cardano-test-wallet/types"; -import { isMobile, openDrawer, openDrawerLoggedIn } from "@helpers/mobile"; +import { isMobile, openDrawer } from "@helpers/mobile"; import { Page, expect } from "@playwright/test"; export default class LoginPage { @@ -23,14 +23,7 @@ export default class LoginPage { async login() { await this.goto(); - if (isMobile(this.page)) { - await openDrawer(this.page); - await this.page - .getByRole("button", { name: "Connect your wallet" }) // BUG testId should be same as connect-wallet-button - .click(); - } else { - await this.connectWalletBtn.click(); - } + await this.connectWalletBtn.click(); await this.demosWalletBtn.click({ force: true }); await this.acceptSanchoNetInfoBtn.click({ force: true }); @@ -47,7 +40,7 @@ export default class LoginPage { } return { stakeKeys, rewardAddresses }; - }, + } ); // Handle multiple stake keys @@ -62,14 +55,14 @@ export default class LoginPage { async logout() { if (isMobile(this.page)) { - await openDrawerLoggedIn(this.page); + await openDrawer(this.page); } await this.disconnectWalletBtn.click(); } async isLoggedIn() { if (isMobile(this.page)) { - await openDrawerLoggedIn(this.page); + await openDrawer(this.page); } await expect(this.disconnectWalletBtn).toBeVisible(); } diff --git a/tests/govtool-frontend/playwright/lib/pages/proposalSubmissionPage.ts b/tests/govtool-frontend/playwright/lib/pages/proposalSubmissionPage.ts new file mode 100644 index 000000000..60133f545 --- /dev/null +++ b/tests/govtool-frontend/playwright/lib/pages/proposalSubmissionPage.ts @@ -0,0 +1,275 @@ +import { faker } from "@faker-js/faker"; +import { expectWithInfo } from "@helpers/exceptionHandler"; +import { Logger } from "@helpers/logger"; +import { downloadMetadata } from "@helpers/metadata"; +import { invalid } from "@mock/index"; +import { Download, Page, expect } from "@playwright/test"; +import metadataBucketService from "@services/metadataBucketService"; +import { IProposalForm, ProposalType } from "@types"; +import environments from "lib/constants/environments"; +const formErrors = { + proposalTitle: ["max-80-characters-error", "this-field-is-required-error"], + abstract: "this-field-is-required-error", + motivation: "this-field-is-required-error", + Rationale: "this-field-is-required-error", + receivingAddress: "invalid-bech32-address-error", + amount: ["only-number-is-allowed-error", "this-field-is-required-error"], + link: "invalid-url-error", +}; + +export default class ProposalSubmissionPage { + // modals + readonly registrationSuccessModal = this.page.getByTestId( + "create-governance-action-submitted-modal" + ); + readonly registrationErrorModal = this.page.getByTestId( + "create-governance-action-error-modal" + ); + + // buttons + readonly registerBtn = this.page.getByTestId("register-button"); + readonly skipBtn = this.page.getByTestId("skip-button"); + readonly confirmBtn = this.page.getByTestId("confirm-modal-button"); + + readonly continueBtn = this.page.getByTestId("continue-button"); + readonly addLinkBtn = this.page.getByRole("button", { name: "+ Add link" }); // BUG testid= add-link-button + readonly infoRadioButton = this.page.getByTestId("Info-radio"); + readonly treasuryRadioButton = this.page.getByTestId("Treasury-radio"); + + // input fields + readonly titleInput = this.page.getByPlaceholder("A name for this Action"); // BUG testid = title-input + readonly abstractInput = this.page.getByPlaceholder("Summary"); // BUG testid = abstract-input + readonly motivationInput = this.page.getByPlaceholder( + "Problem this GA will solve" + ); // BUG testid = motivation-input + readonly rationaleInput = this.page.getByPlaceholder( + "Content of Governance Action" + ); // BUG testid = rationale-input + readonly linkInput = this.page.getByPlaceholder("https://website.com/"); // BUG testid = link-input + readonly receivingAddressInput = this.page.getByPlaceholder( + "The address to receive funds" + ); + readonly amountInput = this.page.getByPlaceholder("e.g."); + + constructor(private readonly page: Page) {} + + async goto() { + await this.page.goto( + `${environments.frontendUrl}/create_governance_action` + ); + await this.continueBtn.click(); + } + + async register(governanceProposal: IProposalForm) { + await this.fillupForm(governanceProposal); + + await this.continueBtn.click(); + await this.continueBtn.click(); + await this.page.getByRole("checkbox").click(); + await this.continueBtn.click(); + + this.page + .getByRole("button", { name: `${governanceProposal.type}.jsonld` }) + .click(); // BUG test id = metadata-download-button + + const dRepMetadata = await this.downloadVoteMetadata(); + const url = await metadataBucketService.uploadMetadata( + dRepMetadata.name, + dRepMetadata.data + ); + await this.page.getByPlaceholder("URL").fill(url); + await this.continueBtn.click(); + } + + async downloadVoteMetadata() { + const download: Download = await this.page.waitForEvent("download"); + return downloadMetadata(download); + } + + async fillupForm(governanceProposal: IProposalForm) { + await this.titleInput.fill(governanceProposal.title); + await this.abstractInput.fill(governanceProposal.abstract); + await this.motivationInput.fill(governanceProposal.motivation); + await this.rationaleInput.fill(governanceProposal.rationale); + + if (governanceProposal.type === "Treasury") { + await this.receivingAddressInput.fill( + governanceProposal.receivingAddress + ); + await this.amountInput.fill(governanceProposal.amount); + } + + if (governanceProposal.extraContentLinks != null) { + for (let i = 0; i < governanceProposal.extraContentLinks.length; i++) { + if (i > 0) { + this.page + .getByRole("button", { + name: "+ Add link", + }) + .click(); // BUG + } + await this.linkInput + .nth(i) + .fill(governanceProposal.extraContentLinks[i]); + } + } + } + + async validateForm(governanceProposal: IProposalForm) { + await this.fillupForm(governanceProposal); + + for (const err of formErrors.proposalTitle) { + await expect(this.page.getByTestId(err)).toBeHidden(); + } + + expect(await this.abstractInput.textContent()).toEqual( + governanceProposal.abstract + ); + + expect(await this.rationaleInput.textContent()).toEqual( + governanceProposal.rationale + ); + + expect(await this.motivationInput.textContent()).toEqual( + governanceProposal.motivation + ); + + if (governanceProposal.type === "Treasury") { + await expect( + this.page.getByTestId(formErrors.receivingAddress) + ).toBeHidden(); + + for (const err of formErrors.amount) { + await expect(this.page.getByTestId(err)).toBeHidden(); + } + } + + await expect(this.page.getByTestId(formErrors.link)).toBeHidden(); + + await expect(this.continueBtn).toBeEnabled(); + } + + async inValidateForm(governanceProposal: IProposalForm) { + await this.fillupForm(governanceProposal); + + function convertTestIdToText(testId: string) { + let text = testId.replace("-error", ""); + text = text.replace(/-/g, " "); + return text[0].toUpperCase() + text.substring(1); + } + + // Helper function to generate regex pattern from form errors + function generateRegexPattern(errors: string[]) { + return new RegExp(errors.map(convertTestIdToText).join("|")); + } + + // Helper function to get errors based on regex pattern + async function getErrorsByPattern(page: Page, regexPattern: RegExp) { + return await page + .locator('[data-testid$="-error"]') + .filter({ hasText: regexPattern }) + .all(); + } + + const proposalTitlePattern = generateRegexPattern(formErrors.proposalTitle); + const proposalTitleErrors = await getErrorsByPattern( + this.page, + proposalTitlePattern + ); + + expectWithInfo( + async () => expect(proposalTitleErrors.length).toEqual(1), + `valid title: ${governanceProposal.title}` + ); + if (governanceProposal.type === "Treasury") { + const receiverAddressErrors = await getErrorsByPattern( + this.page, + new RegExp(convertTestIdToText(formErrors.receivingAddress)) + ); + + expectWithInfo( + async () => expect(receiverAddressErrors.length).toEqual(1), + `valid address: ${governanceProposal.receivingAddress}` + ); + + const amountPattern = generateRegexPattern(formErrors.amount); + const amountErrors = await getErrorsByPattern(this.page, amountPattern); + + expectWithInfo( + async () => expect(amountErrors.length).toEqual(1), + `valid amount: ${governanceProposal.amount}` + ); + } + + expectWithInfo( + async () => + expect(await this.abstractInput.textContent()).not.toEqual( + governanceProposal.abstract + ), + `valid abstract: ${governanceProposal.abstract}` + ); + + expectWithInfo( + async () => + expect(await this.abstractInput.textContent()).not.toEqual( + governanceProposal.motivation + ), + `valid motivation: ${governanceProposal.motivation}` + ); + + expectWithInfo( + async () => + expect(await this.abstractInput.textContent()).not.toEqual( + governanceProposal.rationale + ), + `valid rationale: ${governanceProposal.rationale}` + ); + + expectWithInfo( + async () => + await expect(this.page.getByTestId(formErrors.link)).toBeVisible(), + `valid link: ${governanceProposal.extraContentLinks[0]}` + ); + + await expect(this.continueBtn).toBeDisabled(); + } + + generateValidProposalFormFields( + proposalType: ProposalType, + receivingAddress?: string + ) { + const proposal: IProposalForm = { + title: faker.lorem.sentence(6), + abstract: faker.lorem.paragraph(2), + motivation: faker.lorem.paragraphs(2), + rationale: faker.lorem.paragraphs(2), + + extraContentLinks: [faker.internet.url()], + type: proposalType, + }; + if (proposalType === ProposalType.treasury) { + (proposal.receivingAddress = receivingAddress), + (proposal.amount = faker.number + .int({ min: 100, max: 1000 }) + .toString()); + } + return proposal; + } + + generateInValidProposalFormFields(proposalType: ProposalType) { + const proposal: IProposalForm = { + title: invalid.proposalTitle(), + abstract: invalid.paragraph(), + motivation: invalid.paragraph(), + rationale: invalid.paragraph(), + + extraContentLinks: [invalid.url()], + type: proposalType, + }; + if (proposalType === ProposalType.treasury) { + (proposal.receivingAddress = faker.location.streetAddress()), + (proposal.amount = invalid.amount()); + } + return proposal; + } +} diff --git a/tests/govtool-frontend/playwright/lib/services/faucetService.ts b/tests/govtool-frontend/playwright/lib/services/faucetService.ts index 446ab78eb..a2f9971e3 100644 --- a/tests/govtool-frontend/playwright/lib/services/faucetService.ts +++ b/tests/govtool-frontend/playwright/lib/services/faucetService.ts @@ -10,11 +10,11 @@ interface IFaucetResponse { } export const loadAmountFromFaucet = async ( - walletAddress: string, + walletAddress: string ): Promise => { try { const res = await fetchClient( - `/send-money?type=default&action=funds&address=${walletAddress}&poolid=undefined&api_key=${environments.faucet.apiKey}`, + `/send-money?type=default&action=funds&address=${walletAddress}&poolid=undefined&api_key=${environments.faucet.apiKey}` ); const responseBody = await res.json(); // console.debug(`faucet response: ${JSON.stringify(responseBody)}`); diff --git a/tests/govtool-frontend/playwright/lib/services/kuberService.ts b/tests/govtool-frontend/playwright/lib/services/kuberService.ts index a1cdaea6d..65190100e 100644 --- a/tests/govtool-frontend/playwright/lib/services/kuberService.ts +++ b/tests/govtool-frontend/playwright/lib/services/kuberService.ts @@ -77,7 +77,7 @@ class Kuber { const signedTx = this.signTx(tx); const signedTxBody = Uint8Array.from(cborxEncoder.encode(tx)); const lockId = Buffer.from( - blake.blake2b(signedTxBody, undefined, 32), + blake.blake2b(signedTxBody, undefined, 32) ).toString("hex"); const submitTxCallback = async () => { return this.submitTx(signedTx, lockId); @@ -87,18 +87,18 @@ class Kuber { async submitTx(signedTx: any, lockId?: string) { Logger.info( - `Submitting tx: ${JSON.stringify({ lock_id: lockId, tx: signedTx })}`, + `Submitting tx: ${JSON.stringify({ lock_id: lockId, tx: signedTx })}` ); const res = (await callKuber( `/api/${this.version}/tx?submit=true`, "POST", - JSON.stringify(signedTx), + JSON.stringify(signedTx) )) as any; let decodedTx = cborxDecoder.decode(Buffer.from(res.cborHex, "hex")); const submittedTxBody = Uint8Array.from(cborxEncoder.encode(decodedTx[0])); const submittedTxHash = Buffer.from( - blake.blake2b(submittedTxBody, undefined, 32), + blake.blake2b(submittedTxBody, undefined, 32) ).toString("hex"); Logger.success(`Tx submitted: ${submittedTxHash}`); @@ -113,7 +113,7 @@ const kuberService = { initializeWallets: ( senderAddress: string, signingKey: string, - wallets: ShelleyWallet[], + wallets: ShelleyWallet[] ) => { const kuber = new Kuber(senderAddress, signingKey); const outputs = []; @@ -134,8 +134,8 @@ const kuberService = { certificates.push( Kuber.generateCert( "registerstake", - convertBufferToHex(wallet.stakeKey.pkh), - ), + convertBufferToHex(wallet.stakeKey.pkh) + ) ); } return kuber.signAndSubmitTx({ @@ -195,7 +195,7 @@ const kuberService = { addr: string, signingKey: string, stakePrivateKey: string, - pkh: string, + pkh: string ) => { const kuber = new Kuber(addr, signingKey); const selections = [ @@ -218,7 +218,7 @@ const kuberService = { signingKey: string, stakePrivateKey: string, pkh: string, - dRep: string | "abstain" | "noconfidence", + dRep: string | "abstain" | "noconfidence" ) => { const kuber = new Kuber(addr, signingKey); const selections = [ @@ -245,7 +245,7 @@ const kuberService = { const utxos: any[] = await callKuber(`/api/v3/utxo?address=${addr}`); const balanceInLovelace = utxos.reduce( (acc, utxo) => acc + utxo.value.lovelace, - 0, + 0 ); return balanceInLovelace / 1000000; }, @@ -254,7 +254,7 @@ const kuberService = { stakePrivateKey: string, pkh: string, signingKey: string, - addr: string, + addr: string ) => { const kuber = new Kuber(addr, signingKey); const selections = [ @@ -315,7 +315,7 @@ const kuberService = { signingKey: string, voter: string, // dRepHash dRepStakePrivKey: string, - proposal: string, + proposal: string ) { const kuber = new Kuber(addr, signingKey); const req = { @@ -343,7 +343,7 @@ const kuberService = { abstainDelegations( stakePrivKeys: string[], - stakePkhs: string[], + stakePkhs: string[] ): Promise { const kuber = new Kuber(faucetWallet.address, faucetWallet.payment.private); const selections = stakePrivKeys.map((key) => { @@ -372,7 +372,7 @@ async function callKuber( path: any, method: "GET" | "POST" = "GET", body?: BodyInit, - contentType = "application/json", + contentType = "application/json" ) { const url = config.apiUrl + path; @@ -405,7 +405,7 @@ async function callKuber( err = Error( `KuberApi [Status ${res.status}] : ${ json.message ? json.message : txt - }`, + }` ); } else { err = Error(`KuberApi [Status ${res.status}] : ${txt}`); diff --git a/tests/govtool-frontend/playwright/lib/transaction.decorator.ts b/tests/govtool-frontend/playwright/lib/transaction.decorator.ts index 26074bb2d..71c4e8a34 100644 --- a/tests/govtool-frontend/playwright/lib/transaction.decorator.ts +++ b/tests/govtool-frontend/playwright/lib/transaction.decorator.ts @@ -6,7 +6,7 @@ export function withTxConfirmation(value, { kind }) { return async function (...args: any) { await waitForTxConfirmation( this.page, - async () => await value.apply(this, args), + async () => await value.apply(this, args) ); }; } diff --git a/tests/govtool-frontend/playwright/lib/types.ts b/tests/govtool-frontend/playwright/lib/types.ts index 6cefe5543..db1b932d0 100644 --- a/tests/govtool-frontend/playwright/lib/types.ts +++ b/tests/govtool-frontend/playwright/lib/types.ts @@ -51,6 +51,23 @@ export type IDRepInfo = { bio?: string; extraContentLinks?: string[]; }; + +export type IProposalForm = { + title: string; + abstract: string; + motivation: string; + rationale: string; + extraContentLinks?: string[]; + type: ProposalType; + receivingAddress?: string; + amount?: string; +}; + +export enum ProposalType { + info = "Info", + treasury = "Treasury", +} + export enum FilterOption { ProtocolParameterChange = "ParameterChange", InfoAction = "InfoAction", @@ -60,3 +77,18 @@ export enum FilterOption { NewCommittee = "NewCommittee", UpdatetotheConstitution = "NewConstitution", } + +export type DRepStatus = "Active" | "Inactive" | "Retired"; + +export type IDRep = { + drepId: string; + view: string; + url: string; + metadataHash: string; + deposit: number; + votingPower: number; + status: DRepStatus; + type: string; + latestTxHash: string; + latestRegistrationDate: string; +}; diff --git a/tests/govtool-frontend/playwright/package-lock.json b/tests/govtool-frontend/playwright/package-lock.json index 6fed6820e..9eda884e5 100644 --- a/tests/govtool-frontend/playwright/package-lock.json +++ b/tests/govtool-frontend/playwright/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "@cardanoapi/cardano-test-wallet": "^1.1.1", + "@cardanoapi/cardano-test-wallet": "^1.1.2", "@faker-js/faker": "^8.4.1", "@noble/curves": "^1.3.0", "@noble/ed25519": "^2.0.0", @@ -35,6 +35,7 @@ "eslint": "^8.57.0", "prettier": "3.2.5", "ts-loader": "^9.5.1", + "ts-node": "^10.9.2", "tsconfig-paths-webpack-plugin": "^4.1.0", "typescript": "^5.4.5", "webpack": "^5.90.3", @@ -86,9 +87,9 @@ } }, "node_modules/@cardanoapi/cardano-test-wallet": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@cardanoapi/cardano-test-wallet/-/cardano-test-wallet-1.1.1.tgz", - "integrity": "sha512-bNseN0PY0vQ7o7FPKRQW3ZGhUgjQ9bavbbvUiIJ/oTTCwItAq1ds7CGRuVH4hDr8kPSUB76pHQMk8QvEXdMs5A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@cardanoapi/cardano-test-wallet/-/cardano-test-wallet-1.1.2.tgz", + "integrity": "sha512-1pdc47EXMsa1iLcsmZAlfJEDkWRo23wd93CnZtj5Z/clGyvXsVj+ZDqakqnYMvQFr+IVrwROujdFHc9G2/2YDw==" }, "node_modules/@cbor-extract/cbor-extract-linux-x64": { "version": "2.2.0", @@ -101,6 +102,28 @@ "linux" ] }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.7", "dev": true, @@ -365,6 +388,30 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, "node_modules/@types/eslint": { "version": "8.56.4", "dev": true, @@ -638,6 +685,15 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-walk": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/ajv": { "version": "8.12.0", "dev": true, @@ -728,6 +784,12 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -1036,6 +1098,12 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, "node_modules/cross-spawn": { "version": "7.0.3", "dev": true, @@ -1096,6 +1164,15 @@ "node": ">=8" } }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -2069,6 +2146,12 @@ "node": ">=10" } }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, "node_modules/md5": { "version": "2.3.0", "dev": true, @@ -2897,6 +2980,49 @@ "node": ">=10.13.0" } }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, "node_modules/tsconfig-paths": { "version": "4.2.0", "dev": true, @@ -3020,6 +3146,12 @@ "inherits": "2.0.3" } }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, "node_modules/watchpack": { "version": "2.4.0", "dev": true, @@ -3185,6 +3317,15 @@ "dev": true, "license": "ISC" }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -3206,14 +3347,35 @@ "dev": true }, "@cardanoapi/cardano-test-wallet": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@cardanoapi/cardano-test-wallet/-/cardano-test-wallet-1.1.1.tgz", - "integrity": "sha512-bNseN0PY0vQ7o7FPKRQW3ZGhUgjQ9bavbbvUiIJ/oTTCwItAq1ds7CGRuVH4hDr8kPSUB76pHQMk8QvEXdMs5A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@cardanoapi/cardano-test-wallet/-/cardano-test-wallet-1.1.2.tgz", + "integrity": "sha512-1pdc47EXMsa1iLcsmZAlfJEDkWRo23wd93CnZtj5Z/clGyvXsVj+ZDqakqnYMvQFr+IVrwROujdFHc9G2/2YDw==" }, "@cbor-extract/cbor-extract-linux-x64": { "version": "2.2.0", "optional": true }, + "@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "dependencies": { + "@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + } + } + }, "@discoveryjs/json-ext": { "version": "0.5.7", "dev": true @@ -3378,6 +3540,30 @@ "version": "2.3.0", "dev": true }, + "@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true + }, + "@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, "@types/eslint": { "version": "8.56.4", "dev": true, @@ -3590,6 +3776,12 @@ "dev": true, "requires": {} }, + "acorn-walk": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "dev": true + }, "ajv": { "version": "8.12.0", "dev": true, @@ -3645,6 +3837,12 @@ "color-convert": "^2.0.1" } }, + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, "argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -3818,6 +4016,12 @@ } } }, + "create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, "cross-spawn": { "version": "7.0.3", "dev": true, @@ -3854,6 +4058,12 @@ "version": "2.0.2", "optional": true }, + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + }, "doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -4505,6 +4715,12 @@ "yallist": "^4.0.0" } }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, "md5": { "version": "2.3.0", "dev": true, @@ -4979,6 +5195,27 @@ } } }, + "ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "requires": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + } + }, "tsconfig-paths": { "version": "4.2.0", "dev": true, @@ -5047,6 +5284,12 @@ "inherits": "2.0.3" } }, + "v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, "watchpack": { "version": "2.4.0", "dev": true, @@ -5148,6 +5391,12 @@ "version": "4.0.0", "dev": true }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true + }, "yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/tests/govtool-frontend/playwright/package.json b/tests/govtool-frontend/playwright/package.json index 23e5ab032..4fcd599d0 100644 --- a/tests/govtool-frontend/playwright/package.json +++ b/tests/govtool-frontend/playwright/package.json @@ -12,6 +12,7 @@ "eslint": "^8.57.0", "prettier": "3.2.5", "ts-loader": "^9.5.1", + "ts-node": "^10.9.2", "tsconfig-paths-webpack-plugin": "^4.1.0", "typescript": "^5.4.5", "webpack": "^5.90.3", @@ -23,10 +24,11 @@ "allure:open": "npx allure open ./allure-report", "allure:serve": "npx allure serve", "test": "npx playwright test", - "format": "prettier . --write" + "format": "prettier . --write", + "generate-wallets": "ts-node ./generate_wallets.ts 8" }, "dependencies": { - "@cardanoapi/cardano-test-wallet": "^1.1.1", + "@cardanoapi/cardano-test-wallet": "^1.1.2", "@faker-js/faker": "^8.4.1", "@noble/curves": "^1.3.0", "@noble/ed25519": "^2.0.0", diff --git a/tests/govtool-frontend/playwright/playwright.config.ts b/tests/govtool-frontend/playwright/playwright.config.ts index 43a5cc701..23722f668 100644 --- a/tests/govtool-frontend/playwright/playwright.config.ts +++ b/tests/govtool-frontend/playwright/playwright.config.ts @@ -61,12 +61,12 @@ export default defineConfig({ testMatch: "**/wallet.bootstrap.ts", dependencies: ["faucet setup"], }, - // { - // name: "transaction", - // use: { ...devices["Desktop Chrome"] }, - // testMatch: "**/*.tx.spec.ts", - // dependencies: process.env.CI ? ["auth setup", "wallet bootstrap"] : [], - // }, + { + name: "transaction", + use: { ...devices["Desktop Chrome"] }, + testMatch: "**/*.tx.spec.ts", + dependencies: process.env.CI ? ["auth setup", "wallet bootstrap"] : [], + }, { name: "loggedin (desktop)", use: { ...devices["Desktop Chrome"] }, @@ -89,7 +89,9 @@ export default defineConfig({ name: "delegation", use: { ...devices["Desktop Chrome"] }, testMatch: "**/*.delegation.spec.ts", - dependencies: process.env.CI ? ["auth setup", "dRep setup"] : [], + dependencies: process.env.CI + ? ["auth setup", "dRep setup", "wallet bootstrap"] + : [], teardown: process.env.CI && "cleanup delegation", }, { @@ -99,15 +101,17 @@ export default defineConfig({ "**/*.delegation.spec.ts", "**/*.loggedin.spec.ts", "**/*.dRep.spec.ts", + "**/*.tx.spec.ts", ], }, { name: "independent (mobile)", use: { ...devices["Pixel 5"] }, testIgnore: [ - "**/*.tx.spec.ts", "**/*.loggedin.spec.ts", "**/*.dRep.spec.ts", + "**/*.delegation.spec.ts", + "**/*.tx.spec.ts", ], }, { diff --git a/tests/govtool-frontend/playwright/tests/1-wallet-connect/walletConnect.loggedin.spec.ts b/tests/govtool-frontend/playwright/tests/1-wallet-connect/walletConnect.loggedin.spec.ts index ab080382a..b1a23c363 100644 --- a/tests/govtool-frontend/playwright/tests/1-wallet-connect/walletConnect.loggedin.spec.ts +++ b/tests/govtool-frontend/playwright/tests/1-wallet-connect/walletConnect.loggedin.spec.ts @@ -1,12 +1,14 @@ import { user01Wallet } from "@constants/staticWallets"; import { test } from "@fixtures/walletExtension"; +import { setAllureEpic } from "@helpers/allure"; import LoginPage from "@pages/loginPage"; test.use({ storageState: ".auth/user01.json", wallet: user01Wallet }); +test.beforeEach(async () => { + await setAllureEpic("1. Wallet connect"); +}); -test("1B: Should connect wallet with single stake key @smoke @fast", async ({ - page, -}) => { +test("1B: Should connect wallet with single stake key", async ({ page }) => { const loginPage = new LoginPage(page); await loginPage.goto(); await loginPage.isLoggedIn(); diff --git a/tests/govtool-frontend/playwright/tests/1-wallet-connect/walletConnect.spec.ts b/tests/govtool-frontend/playwright/tests/1-wallet-connect/walletConnect.spec.ts index 980271dbc..f23e5947e 100644 --- a/tests/govtool-frontend/playwright/tests/1-wallet-connect/walletConnect.spec.ts +++ b/tests/govtool-frontend/playwright/tests/1-wallet-connect/walletConnect.spec.ts @@ -1,17 +1,22 @@ import createWallet from "@fixtures/createWallet"; import { test } from "@fixtures/walletExtension"; +import { setAllureEpic } from "@helpers/allure"; import convertBufferToHex from "@helpers/convertBufferToHex"; import { ShelleyWallet } from "@helpers/crypto"; import LoginPage from "@pages/loginPage"; import { expect } from "@playwright/test"; -test("1A. Should connect wallet and choose stake-key to use @smoke @fast", async ({ +test.beforeEach(async () => { + await setAllureEpic("1. Wallet connect"); +}); + +test("1A. Should connect wallet and choose stake-key to use", async ({ page, }) => { const shellyWallet = await ShelleyWallet.generate(); const extraPubStakeKey = convertBufferToHex(shellyWallet.stakeKey.public); const extraRewardAddress = convertBufferToHex( - shellyWallet.rewardAddressRawBytes(0), + shellyWallet.rewardAddressRawBytes(0) ); await createWallet(page, { @@ -23,9 +28,7 @@ test("1A. Should connect wallet and choose stake-key to use @smoke @fast", async await loginPage.login(); }); -test("1C: Should disconnect Wallet When connected @smoke @fast", async ({ - page, -}) => { +test("1C: Should disconnect Wallet When connected", async ({ page }) => { await createWallet(page); const loginPage = new LoginPage(page); @@ -34,7 +37,7 @@ test("1C: Should disconnect Wallet When connected @smoke @fast", async ({ await loginPage.logout(); }); -test("1D. Should check correct network (Testnet/Mainnet) on connection @smoke @fast", async ({ +test("1D. Should check correct network (Testnet/Mainnet) on connection", async ({ page, }) => { const wrongNetworkId = 1; // mainnet network diff --git a/tests/govtool-frontend/playwright/tests/2-delegation/delegation.delegation.spec.ts b/tests/govtool-frontend/playwright/tests/2-delegation/delegation.delegation.spec.ts deleted file mode 100644 index 88749ea26..000000000 --- a/tests/govtool-frontend/playwright/tests/2-delegation/delegation.delegation.spec.ts +++ /dev/null @@ -1,92 +0,0 @@ -import environments from "@constants/environments"; -import { - adaHolder01Wallet, - adaHolder02Wallet, - dRep01Wallet, -} from "@constants/staticWallets"; -import { createTempDRepAuth } from "@datafactory/createAuth"; -import { test } from "@fixtures/walletExtension"; -import { ShelleyWallet } from "@helpers/crypto"; -import { createNewPageWithWallet } from "@helpers/page"; -import { pollTransaction, waitForTxConfirmation } from "@helpers/transaction"; -import DelegationPage from "@pages/delegationPage"; -import { expect } from "@playwright/test"; -import kuberService from "@services/kuberService"; - -test.describe("Delegate to others", () => { - test.use({ - storageState: ".auth/adaHolder01.json", - wallet: adaHolder01Wallet, - }); - - test("2A. Should show delegated DRep Id on dashboard after delegation @slow @critical", async ({ - page, - }, testInfo) => { - test.setTimeout(testInfo.timeout + 2 * environments.txTimeOut); - - const delegationPage = new DelegationPage(page); - await delegationPage.goto(); - - await delegationPage.delegateToDRep( - "drep1qzw234c0ly8csamxf8hrhfahvzwpllh2ckuzzvl38d22wwxxquu", - ); - - page.goto("/"); - await expect(page.getByTestId("delegated-dRep-id")).toHaveText( - dRep01Wallet.dRepId, - ); - }); -}); - -test.describe("Delegate to myself", () => { - test("2E. Should register as SoleVoter @slow @critical", async ({ - page, - browser, - }, testInfo) => { - test.setTimeout(testInfo.timeout + 2 * environments.txTimeOut); - - const wallet = await ShelleyWallet.generate(); - const txRes = await kuberService.transferADA( - [wallet.addressBech32(environments.networkId)], - 600, - ); - await pollTransaction(txRes.txId, txRes.lockInfo); - const dRepAuth = await createTempDRepAuth(page, wallet); - const dRepPage = await createNewPageWithWallet(browser, { - storageState: dRepAuth, - wallet, - enableStakeSigning: true, - }); - await dRepPage.goto("/"); - await dRepPage.getByTestId("register-as-sole-voter-button").click(); - await dRepPage.getByTestId("retire-button").click(); // BUG: Incorrect test-id , it should be continue-retirement - await expect( - dRepPage.getByTestId("registration-transaction-submitted-modal"), - ).toBeVisible(); - dRepPage.getByTestId("confirm-modal-button").click(); - await waitForTxConfirmation(dRepPage); - - await expect(dRepPage.getByText("You are a Sole Voter")).toBeVisible(); - }); -}); - -test.describe("Change Delegation", () => { - test.use({ - storageState: ".auth/adaHolder02.json", - wallet: adaHolder02Wallet, - }); - - // Skipped: Blocked because delegation is not working - test.skip("2F. Should change delegated dRep @slow @critical", async ({ - page, - }) => { - const delegationPage = new DelegationPage(page); - await delegationPage.goto(); - await delegationPage.delegateToDRep(dRep01Wallet.dRepId); - - // await delegationPage.goto("/"); - // await adaHolderPage.getByTestId("change-dRep-button").click(); - // await delegationPage.delegateToDRep(dRep02Wallet.dRepId); - // await waitForTxConfirmation(page); - }); -}); diff --git a/tests/govtool-frontend/playwright/tests/2-delegation/delegation.drep.spec.ts b/tests/govtool-frontend/playwright/tests/2-delegation/delegation.drep.spec.ts new file mode 100644 index 000000000..4e446cdc1 --- /dev/null +++ b/tests/govtool-frontend/playwright/tests/2-delegation/delegation.drep.spec.ts @@ -0,0 +1,152 @@ +import environments from "@constants/environments"; +import { dRep01Wallet, user01Wallet } from "@constants/staticWallets"; +import { createTempDRepAuth } from "@datafactory/createAuth"; +import { faker } from "@faker-js/faker"; +import { test } from "@fixtures/walletExtension"; +import { setAllureEpic } from "@helpers/allure"; +import { ShelleyWallet } from "@helpers/crypto"; +import { isMobile, openDrawer } from "@helpers/mobile"; +import { createNewPageWithWallet } from "@helpers/page"; +import extractDRepFromWallet from "@helpers/shellyWallet"; +import { transferAdaForWallet } from "@helpers/transaction"; +import DRepDetailsPage from "@pages/dRepDetailsPage"; +import DRepDirectoryPage from "@pages/dRepDirectoryPage"; +import DRepRegistrationPage from "@pages/dRepRegistrationPage"; +import { expect } from "@playwright/test"; + +test.beforeEach(async () => { + await setAllureEpic("2. Delegation"); +}); + +test("2C. Should open wallet connection popup on delegate in disconnected state", async ({ + page, +}) => { + await page.goto("/"); + if (isMobile(page)) { + openDrawer(page); + } + + await page.getByTestId("view-drep-directory-button").click(); + await page + .locator('[data-testid$="-connect-to-delegate-button"]') + .first() + .click(); + await expect(page.getByTestId("connect-your-wallet-modal")).toBeVisible(); +}); + +test("2L. Should copy DRepId", async ({ page, context }) => { + await context.grantPermissions(["clipboard-read", "clipboard-write"]); + + const dRepDirectory = new DRepDirectoryPage(page); + await dRepDirectory.goto(); + + await dRepDirectory.searchInput.fill(dRep01Wallet.dRepId); + await page.getByTestId(`${dRep01Wallet.dRepId}-copy-id-button`).click(); + await expect(page.getByText("Copied to clipboard")).toBeVisible(); + + const copiedText = await page.evaluate(() => navigator.clipboard.readText()); + expect(copiedText).toEqual(dRep01Wallet.dRepId); +}); + +test("2N. Should show DRep information on details page", async ({ + page, + browser, +}, testInfo) => { + test.setTimeout(testInfo.timeout + 2 * environments.txTimeOut); + + const wallet = await ShelleyWallet.generate(); + + await transferAdaForWallet(wallet, 600); + + const tempDRepAuth = await createTempDRepAuth(page, wallet); + const dRepPage = await createNewPageWithWallet(browser, { + storageState: tempDRepAuth, + wallet, + enableStakeSigning: true, + }); + + const dRepRegistrationPage = new DRepRegistrationPage(dRepPage); + await dRepRegistrationPage.goto(); + + const dRepId = extractDRepFromWallet(wallet); + const name = faker.person.firstName(); + const email = faker.internet.email({ firstName: name }); + const bio = faker.person.bio(); + const links = [ + faker.internet.url({ appendSlash: true }), + faker.internet.url(), + ]; + + await dRepRegistrationPage.register({ + name, + email, + bio, + extraContentLinks: links, + }); + + await dRepRegistrationPage.confirmBtn.click(); + + const dRepDirectory = new DRepDirectoryPage(dRepPage); + await dRepDirectory.goto(); + + await dRepDirectory.searchInput.fill(dRepId); + await dRepPage.getByTestId(`${dRepId}-view-details-button`).click(); + + // Verification + await expect(dRepPage.getByTestId("copy-drep-id-button")).toHaveText(dRepId); + await expect(dRepPage.getByText("Active", { exact: true })).toBeVisible(); + await expect(dRepPage.locator("dl").getByText("₳ 0")).toBeVisible(); + await expect(dRepPage.getByText(email, { exact: true })).toBeVisible(); + + for (const link of links) { + await expect(dRepPage.getByText(link, { exact: true })).toBeVisible(); + } + await expect(dRepPage.getByText(bio, { exact: true })).toBeVisible(); +}); + +test("2P. Should enable sharing of DRep details", async ({ page, context }) => { + await context.grantPermissions(["clipboard-read", "clipboard-write"]); + + const dRepDetailsPage = new DRepDetailsPage(page); + await dRepDetailsPage.goto(dRep01Wallet.dRepId); + + await dRepDetailsPage.shareLink(); + await expect(page.getByText("Copied to clipboard")).toBeVisible(); + + const copiedText = await page.evaluate(() => navigator.clipboard.readText()); + expect(copiedText).toEqual( + `${environments.frontendUrl}/drep_directory/${dRep01Wallet.dRepId}` + ); +}); + +test("2Q. Should include DRep status and voting power on the DRep card", async ({ + page, +}) => { + test.skip(); // Cannot access dRep card + + const dRepDirectory = new DRepDirectoryPage(page); + await dRepDirectory.goto(); + + const dRepCard = dRepDirectory.getDRepCard(dRep01Wallet.dRepId); + await expect(dRepCard).toHaveText("20"); +}); + +test.describe("Insufficient funds", () => { + test.use({ storageState: ".auth/user01.json", wallet: user01Wallet }); + + test("2T. Should show warning message on delegation when insufficient funds", async ({ + page, + }) => { + const dRepDirectoryPage = new DRepDirectoryPage(page); + await dRepDirectoryPage.goto(); + + await dRepDirectoryPage.searchInput.fill(dRep01Wallet.dRepId); + const delegateBtn = page.getByTestId( + `${dRep01Wallet.dRepId}-delegate-button` + ); + await expect(delegateBtn).toBeVisible(); + await page.getByTestId(`${dRep01Wallet.dRepId}-delegate-button`).click(); + + await expect(dRepDirectoryPage.delegationErrorModal).toBeVisible(); + }); +}); diff --git a/tests/govtool-frontend/playwright/tests/2-delegation/delegation.loggedin.spec.ts b/tests/govtool-frontend/playwright/tests/2-delegation/delegation.loggedin.spec.ts index 6beb4a2ed..c1271c825 100644 --- a/tests/govtool-frontend/playwright/tests/2-delegation/delegation.loggedin.spec.ts +++ b/tests/govtool-frontend/playwright/tests/2-delegation/delegation.loggedin.spec.ts @@ -1,49 +1,59 @@ -import { user01Wallet } from "@constants/staticWallets"; +import { dRep01Wallet, user01Wallet } from "@constants/staticWallets"; import { test } from "@fixtures/walletExtension"; -import DelegationPage from "@pages/delegationPage"; +import { setAllureEpic } from "@helpers/allure"; +import { ShelleyWallet } from "@helpers/crypto"; +import { isMobile } from "@helpers/mobile"; +import extractDRepFromWallet from "@helpers/shellyWallet"; +import DRepDirectoryPage from "@pages/dRepDirectoryPage"; import { expect } from "@playwright/test"; test.use({ storageState: ".auth/user01.json", wallet: user01Wallet }); -test("2B. Should access delegation to dRep page @smoke @fast", async ({ - page, -}) => { +test.beforeEach(async () => { + await setAllureEpic("2. Delegation"); +}); + +test("2B. Should access DRep Directory page", async ({ page }) => { await page.goto("/"); - await page.getByTestId("delegate-button").click(); // BUG incorrect test ID - await expect( - page.getByRole("navigation").getByText("DRep Directory"), - ).toBeVisible(); + await page.getByTestId("view-drep-directory-button").click(); + if (isMobile(page)) { + await expect(page.getByText("DRep Directory")).toBeVisible(); + } else { + await expect( + page.getByRole("navigation").getByText("DRep Directory") + ).toBeVisible(); + } }); -// Skipped: No need to insert dRep id to delegate -test.skip("2I. Should check validity of DRep Id @slow", async ({ page }) => { - // const urlToIntercept = "**/utxo?**"; - // const invalidDRepId = generateRandomDRepId(); - // const validDRepId = dRep01Wallet.dRepId; - // // Invalidity checks - // const delegationPage = new DelegationPage(page); - // await delegationPage.goto(); - // await delegationPage.delegateToDRep(invalidDRepId); - // await expect(delegationPage.delegationErrorModal).toBeVisible(); - // await delegationPage.resetDRepForm(); - // // Validity checks - // await delegationPage.dRepInput.fill(validDRepId); - // await delegationPage.delegateBtn.click(); - // const response = await page.waitForResponse(urlToIntercept); - // expect(response.body.length).toEqual(0); +test("2I. Should check validity of DRep Id", async ({ page }) => { + const dRepDirectory = new DRepDirectoryPage(page); + await dRepDirectory.goto(); + + await dRepDirectory.searchInput.fill(dRep01Wallet.dRepId); + await expect(dRepDirectory.getDRepCard(dRep01Wallet.dRepId)).toHaveText( + dRep01Wallet.dRepId + ); + + const wallet = await ShelleyWallet.generate(); + const invalidDRepId = extractDRepFromWallet(wallet); + + await dRepDirectory.searchInput.fill(invalidDRepId); + await expect(dRepDirectory.getDRepCard(invalidDRepId)).not.toBeVisible(); }); -test("2D. Verify Delegation Behavior in Connected State @smoke @fast", async ({ +test("2D. Should show delegation options in connected state", async ({ page, }) => { - const delegationPage = new DelegationPage(page); - await delegationPage.goto(); + const dRepDirectoryPage = new DRepDirectoryPage(page); + await dRepDirectoryPage.goto(); - // Verifying delegation options - await delegationPage.delegationOptionsDropdown.click(); - await expect(delegationPage.signalNoConfidenceCard).toBeVisible(); - await expect(delegationPage.abstainDelegationCard).toBeVisible(); + // Verifying automatic delegation options + await dRepDirectoryPage.automaticDelegationOptionsDropdown.click(); + await expect(dRepDirectoryPage.abstainDelegationCard).toBeVisible(); + await expect(dRepDirectoryPage.signalNoConfidenceCard).toBeVisible(); - expect(await delegationPage.delegateBtns.count()).toBeGreaterThanOrEqual(2); + expect(await dRepDirectoryPage.delegateBtns.count()).toBeGreaterThanOrEqual( + 2 + ); }); diff --git a/tests/govtool-frontend/playwright/tests/2-delegation/delegation.spec.ts b/tests/govtool-frontend/playwright/tests/2-delegation/delegation.spec.ts index 7104d728d..17cb4136d 100644 --- a/tests/govtool-frontend/playwright/tests/2-delegation/delegation.spec.ts +++ b/tests/govtool-frontend/playwright/tests/2-delegation/delegation.spec.ts @@ -1,14 +1,95 @@ +import { dRep01Wallet } from "@constants/staticWallets"; +import DRepDirectoryPage from "@pages/dRepDirectoryPage"; +import { setAllureEpic } from "@helpers/allure"; import { expect, test } from "@playwright/test"; +import { DRepStatus } from "@types"; -test("2C. Verify DRep Behavior in Disconnected State @smoke @fast", async ({ - page, -}) => { - await page.goto("/"); - - await page.getByTestId("delegate-connect-wallet-button").click(); - await page - .locator('[data-testid$="-connect-to-delegate-button"]') - .first() - .click(); - await expect(page.getByTestId("connect-your-wallet-modal")).toBeVisible(); +test.beforeEach(async () => { + await setAllureEpic("2. Delegation"); +}); + +test("2J. Should search by DRep id", async ({ page }) => { + const dRepDirectory = new DRepDirectoryPage(page); + await dRepDirectory.goto(); + + await dRepDirectory.searchInput.fill(dRep01Wallet.dRepId); + await expect(dRepDirectory.getDRepCard(dRep01Wallet.dRepId)).toHaveText( + dRep01Wallet.dRepId + ); +}); + +test("2K. Should filter DReps", async ({ page }) => { + const dRepFilterOptions: DRepStatus[] = ["Active", "Inactive", "Retired"]; + + const dRepDirectory = new DRepDirectoryPage(page); + await dRepDirectory.goto(); + + await dRepDirectory.filterBtn.click(); + + // Single filter + for (const option of dRepFilterOptions) { + await dRepDirectory.filterDReps([option]); + await dRepDirectory.validateFilters([option], dRepFilterOptions); + await dRepDirectory.unFilterDReps([option]); + } + + // Multiple filters + const multipleFilterOptionNames = [...dRepFilterOptions]; + while (multipleFilterOptionNames.length > 1) { + await dRepDirectory.filterDReps(multipleFilterOptionNames); + await dRepDirectory.validateFilters( + multipleFilterOptionNames, + dRepFilterOptions + ); + await dRepDirectory.unFilterDReps(multipleFilterOptionNames); + multipleFilterOptionNames.pop(); + } +}); + +test("2M. Should sort DReps", async ({ page }) => { + test.slow(); + + enum SortOption { + RegistrationDate = "RegistrationDate", + VotingPower = "VotingPower", + Status = "Status", + } + + const dRepDirectory = new DRepDirectoryPage(page); + await dRepDirectory.goto(); + + await dRepDirectory.sortBtn.click(); + + await dRepDirectory.sortAndValidate( + SortOption.RegistrationDate, + (d1, d2) => d1.latestRegistrationDate >= d2.latestRegistrationDate + ); + + await dRepDirectory.sortAndValidate( + SortOption.VotingPower, + (d1, d2) => d1.votingPower >= d2.votingPower + ); + + await dRepDirectory.sortAndValidate( + SortOption.Status, + (d1, d2) => d1.status >= d2.status + ); +}); + +test("2O. Should load more DReps on show more", async ({ page }) => { + const dRepDirectory = new DRepDirectoryPage(page); + await dRepDirectory.goto(); + + const dRepIdsBefore = await dRepDirectory.getAllListedDRepIds(); + await dRepDirectory.showMoreBtn.click(); + + const dRepIdsAfter = await dRepDirectory.getAllListedDRepIds(); + expect(dRepIdsAfter.length).toBeGreaterThanOrEqual(dRepIdsBefore.length); + + if (dRepIdsAfter.length > dRepIdsBefore.length) { + await expect(dRepDirectory.showMoreBtn).toBeVisible(); + expect(true).toBeTruthy(); + } else { + await expect(dRepDirectory.showMoreBtn).not.toBeVisible(); + } }); diff --git a/tests/govtool-frontend/playwright/tests/2-delegation/delegationFunctionality.delegation.spec.ts b/tests/govtool-frontend/playwright/tests/2-delegation/delegationFunctionality.delegation.spec.ts new file mode 100644 index 000000000..18a5556b0 --- /dev/null +++ b/tests/govtool-frontend/playwright/tests/2-delegation/delegationFunctionality.delegation.spec.ts @@ -0,0 +1,322 @@ +import environments from "@constants/environments"; +import { + adaHolder01Wallet, + adaHolder02Wallet, + adaHolder03Wallet, + adaHolder04Wallet, + dRep01Wallet, + dRep02Wallet, +} from "@constants/staticWallets"; +import { createTempDRepAuth } from "@datafactory/createAuth"; +import { test } from "@fixtures/walletExtension"; +import { setAllureEpic } from "@helpers/allure"; +import { ShelleyWallet } from "@helpers/crypto"; +import { createNewPageWithWallet } from "@helpers/page"; +import extractDRepFromWallet from "@helpers/shellyWallet"; +import { + registerDRepForWallet, + registerStakeForWallet, + transferAdaForWallet, + waitForTxConfirmation, +} from "@helpers/transaction"; +import DRepDirectoryPage from "@pages/dRepDirectoryPage"; +import { Page, expect } from "@playwright/test"; +import kuberService from "@services/kuberService"; + +test.beforeEach(async () => { + await setAllureEpic("2. Delegation"); +}); + +test.describe("Delegate to others", () => { + test.describe.configure({ mode: "serial" }); + + test.use({ + storageState: ".auth/adaHolder01.json", + wallet: adaHolder01Wallet, + }); + + test("2A. Should show delegated DRep Id (on Dashboard, and DRep Directory) after delegation", async ({ + page, + }, testInfo) => { + test.setTimeout(testInfo.timeout + 2 * environments.txTimeOut); + + const dRepId = dRep01Wallet.dRepId; + + const dRepDirectoryPage = new DRepDirectoryPage(page); + await dRepDirectoryPage.goto(); + + await dRepDirectoryPage.delegateToDRep(dRepId); + + // Verify dRepId in dRep directory + await expect( + page.getByTestId(`${dRepId}-delegate-button')`) + ).not.toBeVisible(); + await expect(page.getByText(dRepId)).toHaveCount(1, { timeout: 10_000 }); + + // Verify dRepId in dashboard + await page.goto("/dashboard"); + await expect(page.getByText(dRepId)).toBeVisible(); + }); + + test("2W. Should display voting power of DRep", async ({ page, browser }) => { + const dRepPage = await createNewPageWithWallet(browser, { + storageState: ".auth/dRep01.json", + wallet: ShelleyWallet.fromJson(dRep01Wallet), + enableStakeSigning: true, + }); + + const adaHolder01VotingPower = await kuberService.getBalance( + adaHolder01Wallet.address + ); + + await expect( + dRepPage.getByText(`Voting power:₳ ${adaHolder01VotingPower}`) + ).toBeVisible(); + console.log({ adaHolder01VotingPower }); + + await dRepPage.goto("/"); + }); + + test("2F. Should change delegated dRep", async ({ page }, testInfo) => { + test.setTimeout(testInfo.timeout + 2 * environments.txTimeOut); + + const dRepId = dRep02Wallet.dRepId; + + const dRepDirectoryPage = new DRepDirectoryPage(page); + await dRepDirectoryPage.goto(); + await dRepDirectoryPage.delegateToDRep(dRepId); + await expect(page.getByTestId(`${dRepId}-copy-id-button`)).toHaveText( + dRepId + ); // verify delegation + }); +}); + +test.describe("Delegate to myself", () => { + let dRepPage: Page; + let wallet: ShelleyWallet; + + test.beforeEach(async ({ page, browser }, testInfo) => { + test.setTimeout(testInfo.timeout + 2 * environments.txTimeOut); + + wallet = await ShelleyWallet.generate(); + + await transferAdaForWallet(wallet, 600); + await registerStakeForWallet(wallet); + + const dRepAuth = await createTempDRepAuth(page, wallet); + dRepPage = await createNewPageWithWallet(browser, { + storageState: dRepAuth, + wallet, + enableStakeSigning: true, + }); + }); + + test("2E. Should register as Sole voter", async ({ page }, testInfo) => { + test.setTimeout(testInfo.timeout + 2 * environments.txTimeOut); + + const dRepId = extractDRepFromWallet(wallet); + + await dRepPage.goto("/"); + await dRepPage.getByTestId("register-as-sole-voter-button").click(); + await dRepPage.getByTestId("continue-button").click(); + await expect( + dRepPage.getByTestId("registration-transaction-submitted-modal") + ).toBeVisible(); + await dRepPage.getByTestId("confirm-modal-button").click(); + await waitForTxConfirmation(dRepPage); + + // Checks in dashboard + await expect(page.getByText(dRepId)).toHaveText(dRepId); + + // Checks in dRep directory + await expect(dRepPage.getByText("You are a Direct Voter")).toBeVisible(); + await dRepPage.getByTestId("drep-directory-link").click(); + await expect(dRepPage.getByText("Direct Voter")).toBeVisible(); + await expect(dRepPage.getByTestId(`${dRepId}-copy-id-button`)).toHaveText( + dRepId + ); + }); + + test("2S. Should retire as a Direct Voter on delegating to another DRep", async () => { + await dRepPage.goto("/"); + await dRepPage.getByTestId("register-as-sole-voter-button").click(); + await dRepPage.getByTestId("continue-button").click(); + await expect( + dRepPage.getByTestId("registration-transaction-submitted-modal") + ).toBeVisible(); + await dRepPage.getByTestId("confirm-modal-button").click(); + await waitForTxConfirmation(dRepPage); + await expect(dRepPage.getByText("You are a Direct Voter")).toBeVisible(); + + const dRepDirectoryPage = new DRepDirectoryPage(dRepPage); + await dRepDirectoryPage.goto(); + + await dRepDirectoryPage.delegateToDRep(dRep01Wallet.dRepId); + await dRepPage.goto("/dashboard"); + + await expect( + dRepPage.getByText("You Have Retired as a Direct") + ).toBeVisible(); + }); +}); + +test.describe("Multiple delegations", () => { + test.use({ + storageState: ".auth/adaHolder02.json", + wallet: adaHolder02Wallet, + }); + + test("2R. Should display a modal indicating waiting for previous transaction when delegating if the previous transaction is not completed", async ({ + page, + }) => { + const dRepDirectoryPage = new DRepDirectoryPage(page); + await dRepDirectoryPage.goto(); + + await dRepDirectoryPage.searchInput.fill(dRep01Wallet.dRepId); + const delegateBtn = page.getByTestId( + `${dRep01Wallet.dRepId}-delegate-button` + ); + await expect(delegateBtn).toBeVisible(); + await page.getByTestId(`${dRep01Wallet.dRepId}-delegate-button`).click(); + + await page.waitForTimeout(2_000); + await dRepDirectoryPage.searchInput.fill(dRep02Wallet.dRepId); + await page.getByTestId(`${dRep02Wallet.dRepId}-delegate-button`).click(); + + await expect( + page.getByTestId("transaction-inprogress-modal") + ).toBeVisible(); + }); +}); + +test.describe("Abstain delegation", () => { + test.describe.configure({ mode: "serial" }); + + let dRepWallet: ShelleyWallet; + + test.beforeAll(async () => { + dRepWallet = await ShelleyWallet.generate(); + }); + + test("2U_1. Register DRep and Delegate", async ({ + page, + browser, + }, testInfo) => { + test.setTimeout(testInfo.timeout + 2 * environments.txTimeOut); + + await registerDRepForWallet(dRepWallet); + + const adaHolderPage = await createNewPageWithWallet(browser, { + storageState: ".auth/adaHolder03.json", + wallet: ShelleyWallet.fromJson(adaHolder03Wallet), + enableStakeSigning: true, + }); + const dRepDirectoryPage = new DRepDirectoryPage(adaHolderPage); + await dRepDirectoryPage.goto(); + + const dRepId = extractDRepFromWallet(dRepWallet); + await dRepDirectoryPage.delegateToDRep(dRepId); + console.debug(`Delegated to ${dRepId}`); + }); + + test("2U_2. Should delegate my own voting power to Abstain as registered DRep with delegated voting power", async ({ + page, + browser, + }, testInfo) => { + test.setTimeout(testInfo.timeout + 2 * environments.txTimeOut); + + await transferAdaForWallet(dRepWallet); + await registerStakeForWallet(dRepWallet); + + const dRepId = extractDRepFromWallet(dRepWallet); + console.debug(`Using ${dRepId}`); + const tempDRepAuth = await createTempDRepAuth(page, dRepWallet); + const dRepPage = await createNewPageWithWallet(browser, { + storageState: tempDRepAuth, + wallet: dRepWallet, + enableStakeSigning: true, + }); + + const dRepDirectoryPage = new DRepDirectoryPage(dRepPage); + await dRepDirectoryPage.goto(); + await dRepDirectoryPage.automaticDelegationOptionsDropdown.click(); + await dRepPage + .getByTestId("abstain-from-every-vote-delegate-button") + .click(); + await waitForTxConfirmation(dRepPage); + + const balance = await kuberService.getBalance( + dRepWallet.addressBech32(environments.networkId) + ); + await expect( + dRepPage.getByText(`You have delegated ₳${balance}`) + ).toBeVisible(); + console.log({ balance }); + }); +}); + +test.describe("No confidence delegation", () => { + test.describe.configure({ mode: "serial" }); + + let dRepWallet: ShelleyWallet; + + test.beforeAll(async () => { + dRepWallet = await ShelleyWallet.generate(); + }); + + test("2V_1. Register DRep and Delegate", async ({ + page, + browser, + }, testInfo) => { + test.setTimeout(testInfo.timeout + 2 * environments.txTimeOut); + + await registerDRepForWallet(dRepWallet); + + const adaHolderPage = await createNewPageWithWallet(browser, { + storageState: ".auth/adaHolder04.json", + wallet: ShelleyWallet.fromJson(adaHolder04Wallet), + enableStakeSigning: true, + }); + const dRepDirectoryPage = new DRepDirectoryPage(adaHolderPage); + await dRepDirectoryPage.goto(); + + const dRepId = extractDRepFromWallet(dRepWallet); + await dRepDirectoryPage.delegateToDRep(dRepId); + console.debug(`Delegated to ${dRepId}`); + }); + + test("2V_2. Should delegate my own voting power to Abstain as registered DRep with delegated voting power", async ({ + page, + browser, + }, testInfo) => { + test.setTimeout(testInfo.timeout + 2 * environments.txTimeOut); + + await transferAdaForWallet(dRepWallet); + await registerStakeForWallet(dRepWallet); + + const dRepId = extractDRepFromWallet(dRepWallet); + console.debug(`Using ${dRepId}`); + const tempDRepAuth = await createTempDRepAuth(page, dRepWallet); + const dRepPage = await createNewPageWithWallet(browser, { + storageState: tempDRepAuth, + wallet: dRepWallet, + enableStakeSigning: true, + }); + + const dRepDirectoryPage = new DRepDirectoryPage(dRepPage); + await dRepDirectoryPage.goto(); + await dRepDirectoryPage.automaticDelegationOptionsDropdown.click(); + await dRepPage + .getByTestId("signal-no-confidence-on-every-vote-delegate-button") + .click(); + await waitForTxConfirmation(dRepPage); + + const balance = await kuberService.getBalance( + dRepWallet.addressBech32(environments.networkId) + ); + await expect( + dRepPage.getByText(`You have delegated ₳${balance}`) + ).toBeVisible(); + console.log({ balance }); + }); +}); diff --git a/tests/govtool-frontend/playwright/tests/3-drep-registration/dRepRegistration.dRep.spec.ts b/tests/govtool-frontend/playwright/tests/3-drep-registration/dRepRegistration.dRep.spec.ts index e54e1c838..515179e7a 100644 --- a/tests/govtool-frontend/playwright/tests/3-drep-registration/dRepRegistration.dRep.spec.ts +++ b/tests/govtool-frontend/playwright/tests/3-drep-registration/dRepRegistration.dRep.spec.ts @@ -3,16 +3,23 @@ import { dRep01Wallet } from "@constants/staticWallets"; import { createTempDRepAuth } from "@datafactory/createAuth"; import { faker } from "@faker-js/faker"; import { test } from "@fixtures/walletExtension"; -import convertBufferToHex from "@helpers/convertBufferToHex"; +import { setAllureEpic } from "@helpers/allure"; import { ShelleyWallet } from "@helpers/crypto"; import { createNewPageWithWallet } from "@helpers/page"; -import { pollTransaction, waitForTxConfirmation } from "@helpers/transaction"; +import { + registerDRepForWallet, + transferAdaForWallet, + waitForTxConfirmation, +} from "@helpers/transaction"; import DRepRegistrationPage from "@pages/dRepRegistrationPage"; import GovernanceActionsPage from "@pages/governanceActionsPage"; import { expect } from "@playwright/test"; -import kuberService from "@services/kuberService"; import * as crypto from "crypto"; +test.beforeEach(async () => { + await setAllureEpic("3. DRep registration"); +}); + test.describe("Logged in DReps", () => { test.use({ storageState: ".auth/dRep01.json", wallet: dRep01Wallet }); @@ -21,14 +28,15 @@ test.describe("Logged in DReps", () => { }) => { await page.goto("/"); await expect(page.getByTestId("dRep-id-display")).toContainText( - dRep01Wallet.dRepId, + dRep01Wallet.dRepId ); // BUG: testId -> dRep-id-display-dashboard (It is taking sidebar dRep-id) }); test.use({ storageState: ".auth/dRep01.json", wallet: dRep01Wallet }); // Skipped: No option to update metadata - test.skip("3H. Should be able to update metadata @slow", async ({ page }) => { + test("3H. Should be able to update metadata ", async ({ page }) => { + test.skip(); page.getByTestId("change-metadata-button").click(); page.getByTestId("url-input").fill("https://google.com"); page.getByTestId("hash-input").fill(crypto.randomBytes(32).toString("hex")); @@ -37,18 +45,14 @@ test.describe("Logged in DReps", () => { }); test.describe("Temporary DReps", () => { - test("3G. Should show confirmation message with link to view transaction, when DRep registration txn is submitted @slow ", async ({ + test("3G. Should show confirmation message with link to view transaction, when DRep registration txn is submitted", async ({ page, browser, }, testInfo) => { test.setTimeout(testInfo.timeout + environments.txTimeOut); const wallet = await ShelleyWallet.generate(); - const res = await kuberService.transferADA( - [wallet.addressBech32(environments.networkId)], - 600, - ); - await pollTransaction(res.txId, res.lockInfo); + await transferAdaForWallet(wallet, 600); const tempDRepAuth = await createTempDRepAuth(page, wallet); const dRepPage = await createNewPageWithWallet(browser, { @@ -63,22 +67,18 @@ test.describe("Temporary DReps", () => { await expect(dRepRegistrationPage.registrationSuccessModal).toBeVisible(); await expect( - dRepRegistrationPage.registrationSuccessModal.getByText("this link"), + dRepRegistrationPage.registrationSuccessModal.getByText("this link") ).toBeVisible(); }); - test("3I. Should verify retire as DRep @slow", async ({ + test("3I. Should verify retire as DRep", async ({ page, browser, }, testInfo) => { test.setTimeout(testInfo.timeout + environments.txTimeOut); const wallet = await ShelleyWallet.generate(); - const registrationRes = await kuberService.dRepRegistration( - convertBufferToHex(wallet.stakeKey.private), - convertBufferToHex(wallet.stakeKey.pkh), - ); - await pollTransaction(registrationRes.txId, registrationRes.lockInfo); + await registerDRepForWallet(wallet); const tempDRepAuth = await createTempDRepAuth(page, wallet); const dRepPage = await createNewPageWithWallet(browser, { @@ -89,10 +89,10 @@ test.describe("Temporary DReps", () => { await dRepPage.goto("/"); await dRepPage.getByTestId("retire-button").click(); - await dRepPage.getByTestId("retire-button").click(); // BUG testId -> continue-retire-button + await dRepPage.getByTestId("continue-retirement-button").click(); await expect( - dRepPage.getByTestId("retirement-transaction-error-modal"), + dRepPage.getByTestId("retirement-transaction-error-modal") ).toBeVisible(); }); @@ -103,16 +103,9 @@ test.describe("Temporary DReps", () => { test.setTimeout(testInfo.timeout + 3 * environments.txTimeOut); const wallet = await ShelleyWallet.generate(); - const registrationRes = await kuberService.dRepRegistration( - convertBufferToHex(wallet.stakeKey.private), - convertBufferToHex(wallet.stakeKey.pkh), - ); - await pollTransaction(registrationRes.txId, registrationRes.lockInfo); + await registerDRepForWallet(wallet); - const res = await kuberService.transferADA([ - wallet.addressBech32(environments.networkId), - ]); - await pollTransaction(res.txId, res.lockInfo); + await transferAdaForWallet(wallet); const dRepAuth = await createTempDRepAuth(page, wallet); const dRepPage = await createNewPageWithWallet(browser, { @@ -123,9 +116,9 @@ test.describe("Temporary DReps", () => { await dRepPage.goto("/"); await dRepPage.getByTestId("retire-button").click(); - await dRepPage.getByTestId("retire-button").click(); // BUG: testId -> continue-retire-button + await dRepPage.getByTestId("continue-retirement-button").click(); await expect( - dRepPage.getByTestId("retirement-transaction-submitted-modal"), + dRepPage.getByTestId("retirement-transaction-submitted-modal") ).toBeVisible(); dRepPage.getByTestId("confirm-modal-button").click(); await waitForTxConfirmation(dRepPage); @@ -145,11 +138,7 @@ test.describe("Temporary DReps", () => { const wallet = await ShelleyWallet.generate(); - const res = await kuberService.transferADA( - [wallet.addressBech32(environments.networkId)], - 600 - ); - await pollTransaction(res.txId, res.lockInfo); + await transferAdaForWallet(wallet, 600); const dRepAuth = await createTempDRepAuth(page, wallet); const dRepPage = await createNewPageWithWallet(browser, { diff --git a/tests/govtool-frontend/playwright/tests/3-drep-registration/dRepRegistration.loggedin.spec.ts b/tests/govtool-frontend/playwright/tests/3-drep-registration/dRepRegistration.loggedin.spec.ts index a263302a8..8d6a74b70 100644 --- a/tests/govtool-frontend/playwright/tests/3-drep-registration/dRepRegistration.loggedin.spec.ts +++ b/tests/govtool-frontend/playwright/tests/3-drep-registration/dRepRegistration.loggedin.spec.ts @@ -1,6 +1,7 @@ import { user01Wallet } from "@constants/staticWallets"; import { faker } from "@faker-js/faker"; import { test } from "@fixtures/walletExtension"; +import { setAllureEpic } from "@helpers/allure"; import DRepRegistrationPage from "@pages/dRepRegistrationPage"; import { expect } from "@playwright/test"; @@ -9,18 +10,18 @@ test.use({ wallet: user01Wallet, }); -test("3B. Should access DRep registration page @fast @smoke", async ({ - page, -}) => { +test.beforeEach(async () => { + await setAllureEpic("3. DRep registration"); +}); + +test("3B. Should access DRep registration page", async ({ page }) => { await page.goto("/"); await page.getByTestId("register-button").click(); await expect(page.getByText("Become a DRep")).toBeVisible(); }); -test("3D.Verify DRep registration functionality with Wallet Connected State State @fast @smoke", async ({ - page, -}) => { +test("3D. Verify DRep registration form", async ({ page }) => { const dRepRegistrationPage = new DRepRegistrationPage(page); await dRepRegistrationPage.goto(); @@ -32,34 +33,85 @@ test("3D.Verify DRep registration functionality with Wallet Connected State Stat await expect(dRepRegistrationPage.continueBtn).toBeVisible(); }); -// Skipped: Because there are no fields for url and hash inputs. -test.skip("3E. Should reject invalid data and accept valid data @smoke @fast", async ({ - page, -}) => { +test("3E. Should accept valid data in DRep form", async ({ page }) => { + const dRepRegistrationPage = new DRepRegistrationPage(page); + await dRepRegistrationPage.goto(); + + for (let i = 0; i < 100; i++) { + await dRepRegistrationPage.validateForm( + faker.internet.displayName(), + faker.internet.email(), + faker.lorem.paragraph(), + faker.internet.url() + ); + } + + for (let i = 0; i < 6; i++) { + await expect(dRepRegistrationPage.addLinkBtn).toBeVisible(); + await dRepRegistrationPage.addLinkBtn.click(); + } + + await expect(dRepRegistrationPage.addLinkBtn).toBeHidden(); +}); + +test("3L. Should reject invalid data in DRep form", async ({ page }) => { const dRepRegistrationPage = new DRepRegistrationPage(page); await dRepRegistrationPage.goto(); - // Invalidity test - faker.helpers - .multiple(() => faker.internet.displayName(), { count: 100 }) - .forEach(async (dRepName) => { - await dRepRegistrationPage.nameInput.fill(dRepName); - await dRepRegistrationPage.nameInput.clear({ force: true }); - }); + function generateInvalidEmail() { + const choice = faker.number.int({ min: 1, max: 3 }); + + if (choice === 1) { + return faker.lorem.word() + faker.number + "@invalid.com"; + } else if (choice == 2) { + return faker.lorem.word() + "@"; + } + return faker.lorem.word() + "@gmail_com"; + } + function generateInvalidUrl() { + const choice = faker.number.int({ min: 1, max: 3 }); - // Validity test + if (choice === 1) { + return faker.internet.url().replace("https://", "http://"); + } else if (choice === 2) { + return faker.lorem.word() + ".invalid"; + } + return faker.lorem.word() + ".@com"; + } + function generateInvalidName() { + const choice = faker.number.int({ min: 1, max: 3 }); + if (choice === 1) { + // space invalid + return faker.lorem.word() + " " + faker.lorem.word(); + } else if (choice === 2) { + // maximum 80 words invalid + return faker.lorem.paragraphs().replace(/\s+/g, ""); + } + // empty invalid + return " "; + } + + for (let i = 0; i < 100; i++) { + await dRepRegistrationPage.inValidateForm( + generateInvalidName(), + generateInvalidEmail(), + faker.lorem.paragraph(40), + generateInvalidUrl() + ); + } }); -test("3F. Should create proper DRep registration request, when registered with data @slow", async ({ +test("3F. Should create proper DRep registration request, when registered with data", async ({ page, }) => { - const urlToIntercept = "**/utxo?**"; - const dRepRegistrationPage = new DRepRegistrationPage(page); await dRepRegistrationPage.goto(); - await dRepRegistrationPage.register({ name: "Test_dRep" }); + await dRepRegistrationPage.register({ name: "Test" }).catch((err) => { + // Fails because real tx is not submitted + }); - const response = await page.waitForResponse(urlToIntercept); - expect(response.body.length).toEqual(0); + await expect( + page.getByTestId("registration-transaction-error-modal") + ).toBeVisible(); }); diff --git a/tests/govtool-frontend/playwright/tests/3-drep-registration/dRepRegistration.spec.ts b/tests/govtool-frontend/playwright/tests/3-drep-registration/dRepRegistration.spec.ts index 9daf86969..d88415d53 100644 --- a/tests/govtool-frontend/playwright/tests/3-drep-registration/dRepRegistration.spec.ts +++ b/tests/govtool-frontend/playwright/tests/3-drep-registration/dRepRegistration.spec.ts @@ -1,6 +1,11 @@ -import { test, expect } from "@playwright/test"; +import { setAllureEpic } from "@helpers/allure"; +import { expect, test } from "@playwright/test"; -test("3C. Should open wallet connection popup, when Register as DRep from wallet unconnected state @smoke @fast", async ({ +test.beforeEach(async () => { + await setAllureEpic("3. DRep registration"); +}); + +test("3C. Should open wallet connection popup on DRep registration in disconnected state", async ({ page, }) => { await page.goto("/"); diff --git a/tests/govtool-frontend/playwright/tests/4-proposal-visibility/proposalVisibility.dRep.spec.ts b/tests/govtool-frontend/playwright/tests/4-proposal-visibility/proposalVisibility.dRep.spec.ts index 417eb6598..10102d754 100644 --- a/tests/govtool-frontend/playwright/tests/4-proposal-visibility/proposalVisibility.dRep.spec.ts +++ b/tests/govtool-frontend/playwright/tests/4-proposal-visibility/proposalVisibility.dRep.spec.ts @@ -3,19 +3,25 @@ import { dRep01Wallet } from "@constants/staticWallets"; import { createTempDRepAuth } from "@datafactory/createAuth"; import { faker } from "@faker-js/faker"; import { test } from "@fixtures/walletExtension"; +import { setAllureEpic } from "@helpers/allure"; import { lovelaceToAda } from "@helpers/cardano"; -import convertBufferToHex from "@helpers/convertBufferToHex"; import { ShelleyWallet } from "@helpers/crypto"; import { createNewPageWithWallet } from "@helpers/page"; -import { pollTransaction } from "@helpers/transaction"; +import { + registerDRepForWallet, + transferAdaForWallet, +} from "@helpers/transaction"; import GovernanceActionsPage from "@pages/governanceActionsPage"; import { Page, expect } from "@playwright/test"; -import kuberService from "@services/kuberService"; import { FilterOption, IProposal } from "@types"; test.describe("Logged in DRep", () => { test.use({ storageState: ".auth/dRep01.json", wallet: dRep01Wallet }); + test.beforeEach(async () => { + await setAllureEpic("4. Proposal visibility"); + }); + test("4E. Should display DRep's voting power in governance actions page", async ({ page, }) => { @@ -23,8 +29,8 @@ test.describe("Logged in DRep", () => { const governanceActionsPage = new GovernanceActionsPage(page); await governanceActionsPage.goto(); - const res = await votingPowerPromise; - const votingPower = await res.json(); + const res = await votingPowerPromise; + const votingPower = await res.json(); await expect( page.getByText(`₳ ${lovelaceToAda(votingPower)}`) @@ -43,51 +49,6 @@ test.describe("Logged in DRep", () => { await governanceActionsPage.viewFirstProposal(); await expect(govActionDetailsPage.voteBtn).not.toBeVisible(); }); - - test("4G. Should display correct vote counts on governance details page for DRep", async ({ - page, - }) => { - const responsesPromise = Object.keys(FilterOption).map((filterKey) => - page.waitForResponse((response) => - response.url().includes(`&type[]=${FilterOption[filterKey]}`) - ) - ); - - const governanceActionsPage = new GovernanceActionsPage(page); - await governanceActionsPage.goto(); - const responses = await Promise.all(responsesPromise); - const proposals: IProposal[] = ( - await Promise.all( - responses.map(async (response) => { - const data = await response.json(); - return data.elements; - }) - ) - ).flat(); - - expect(proposals.length, "No proposals found!").toBeGreaterThan(0); - - const proposalToCheck = proposals[0]; - const govActionDetailsPage = - await governanceActionsPage.viewProposal(proposalToCheck); - await govActionDetailsPage.showVotesBtn.click(); - - await expect( - page - .getByText("yes₳") - .getByText(`₳ ${lovelaceToAda(proposalToCheck.yesVotes)}`) - ).toBeVisible(); - await expect( - page - .getByText("abstain₳") - .getByText(`₳ ${lovelaceToAda(proposalToCheck.abstainVotes)}`) - ).toBeVisible(); - await expect( - page - .getByText("no₳") - .getByText(`₳ ${lovelaceToAda(proposalToCheck.noVotes)}`) - ).toBeVisible(); - }); }); test.describe("Temporary DReps", async () => { @@ -97,17 +58,8 @@ test.describe("Temporary DReps", async () => { test.setTimeout(testInfo.timeout + 2 * environments.txTimeOut); const wallet = await ShelleyWallet.generate(); - const registrationRes = await kuberService.dRepRegistration( - convertBufferToHex(wallet.stakeKey.private), - convertBufferToHex(wallet.stakeKey.pkh) - ); - await pollTransaction(registrationRes.txId, registrationRes.lockInfo); - - const res = await kuberService.transferADA( - [wallet.addressBech32(environments.networkId)], - 40 - ); - await pollTransaction(res.txId, registrationRes.lockInfo); + await registerDRepForWallet(wallet); + await transferAdaForWallet(wallet, 40); const tempDRepAuth = await createTempDRepAuth(page, wallet); diff --git a/tests/govtool-frontend/playwright/tests/4-proposal-visibility/proposalVisibility.loggedin.spec.ts b/tests/govtool-frontend/playwright/tests/4-proposal-visibility/proposalVisibility.loggedin.spec.ts index 0432f37bd..4501dab6e 100644 --- a/tests/govtool-frontend/playwright/tests/4-proposal-visibility/proposalVisibility.loggedin.spec.ts +++ b/tests/govtool-frontend/playwright/tests/4-proposal-visibility/proposalVisibility.loggedin.spec.ts @@ -1,7 +1,8 @@ import { user01Wallet } from "@constants/staticWallets"; import { test } from "@fixtures/walletExtension"; +import { setAllureEpic } from "@helpers/allure"; import extractExpiryDateFromText from "@helpers/extractExpiryDateFromText"; -import { isMobile, openDrawerLoggedIn } from "@helpers/mobile"; +import { isMobile, openDrawer } from "@helpers/mobile"; import removeAllSpaces from "@helpers/removeAllSpaces"; import GovernanceActionsPage from "@pages/governanceActionsPage"; import { expect } from "@playwright/test"; @@ -24,19 +25,23 @@ enum SortOption { test.use({ storageState: ".auth/user01.json", wallet: user01Wallet }); -test("4A.1: Should access Governance Actions page with connecting wallet @smoke @fast", async ({ +test.beforeEach(async () => { + await setAllureEpic("4. Proposal visibility"); +}); + +test("4A.1: Should access Governance Actions page with connecting wallet", async ({ page, }) => { await page.goto("/"); if (isMobile(page)) { - await openDrawerLoggedIn(page); + await openDrawer(page); } await page.getByTestId("governance-actions-link").click(); await expect(page.getByText(/Governance Actions/i)).toHaveCount(2); }); -test("4B.1: Should restrict voting for users who are not registered as DReps (with wallet connected) @fast", async ({ +test("4B.1: Should restrict voting for users who are not registered as DReps (with wallet connected)", async ({ page, }) => { const govActionsPage = new GovernanceActionsPage(page); @@ -46,7 +51,7 @@ test("4B.1: Should restrict voting for users who are not registered as DReps (wi await expect(govActionDetailsPage.voteBtn).not.toBeVisible(); }); -test("4C.1: Should filter Governance Action Type on governance actions page @slow", async ({ +test("4C.1: Should filter Governance Action Type on governance actions page", async ({ page, }) => { test.slow(); @@ -73,7 +78,7 @@ test("4C.1: Should filter Governance Action Type on governance actions page @slo } }); -test("4C.2: Should sort Governance Action Type on governance actions page @slow", async ({ +test("4C.2: Should sort Governance Action Type on governance actions page", async ({ page, }) => { test.slow(); @@ -86,23 +91,23 @@ test("4C.2: Should sort Governance Action Type on governance actions page @slow" govActionsPage.sortProposal(SortOption.SoonToExpire); await govActionsPage.validateSort( SortOption.SoonToExpire, - (p1, p2) => p1.expiryDate <= p2.expiryDate, + (p1, p2) => p1.expiryDate <= p2.expiryDate ); govActionsPage.sortProposal(SortOption.NewestFirst); await govActionsPage.validateSort( SortOption.NewestFirst, - (p1, p2) => p1.createdDate >= p2.createdDate, + (p1, p2) => p1.createdDate >= p2.createdDate ); govActionsPage.sortProposal(SortOption.HighestYesVotes); await govActionsPage.validateSort( SortOption.HighestYesVotes, - (p1, p2) => p1.yesVotes >= p2.yesVotes, + (p1, p2) => p1.yesVotes >= p2.yesVotes ); }); -test("4D: Should filter and sort Governance Action Type on governance actions page @slow", async ({ +test("4D: Should filter and sort Governance Action Type on governance actions page", async ({ page, }) => { test.slow(); @@ -119,7 +124,7 @@ test("4D: Should filter and sort Governance Action Type on governance actions pa await govActionsPage.validateSort( SortOption.SoonToExpire, (p1, p2) => p1.expiryDate <= p2.expiryDate, - [removeAllSpaces(filterOptionNames[0])], + [removeAllSpaces(filterOptionNames[0])] ); await govActionsPage.validateFilters([filterOptionNames[0]]); }); @@ -130,7 +135,6 @@ test("4H. Should verify none of the displayed governance actions have expired", const govActionsPage = new GovernanceActionsPage(page); await govActionsPage.goto(); - await page.waitForTimeout(4000); // BUG: Delay to load governance actions const proposalCards = await govActionsPage.getAllProposals(); for (const proposalCard of proposalCards) { diff --git a/tests/govtool-frontend/playwright/tests/4-proposal-visibility/proposalVisibility.spec.ts b/tests/govtool-frontend/playwright/tests/4-proposal-visibility/proposalVisibility.spec.ts index 19b5e5364..cb5c0f2e1 100644 --- a/tests/govtool-frontend/playwright/tests/4-proposal-visibility/proposalVisibility.spec.ts +++ b/tests/govtool-frontend/playwright/tests/4-proposal-visibility/proposalVisibility.spec.ts @@ -1,7 +1,12 @@ +import { setAllureEpic } from "@helpers/allure"; import GovernanceActionsPage from "@pages/governanceActionsPage"; import { expect, test } from "@playwright/test"; -test("4A.2: Should access Governance Actions page without connecting wallet @smoke @fast", async ({ +test.beforeEach(async () => { + await setAllureEpic("4. Proposal visibility"); +}); + +test("4A.2: Should access Governance Actions page without connecting wallet", async ({ page, }) => { await page.goto("/"); @@ -10,7 +15,7 @@ test("4A.2: Should access Governance Actions page without connecting wallet @smo await expect(page.getByText(/Governance actions/i)).toHaveCount(2); }); -test("4B.2: Should restrict voting for users who are not registered as DReps (without wallet connected) @flaky @fast", async ({ +test("4B.2: Should restrict voting for users who are not registered as DReps (without wallet connected)", async ({ page, }) => { const govActionsPage = new GovernanceActionsPage(page); diff --git a/tests/govtool-frontend/playwright/tests/5-proposal-functionality/proposalFunctionality.dRep.spec.ts b/tests/govtool-frontend/playwright/tests/5-proposal-functionality/proposalFunctionality.dRep.spec.ts index 6729b8e09..9cd656642 100644 --- a/tests/govtool-frontend/playwright/tests/5-proposal-functionality/proposalFunctionality.dRep.spec.ts +++ b/tests/govtool-frontend/playwright/tests/5-proposal-functionality/proposalFunctionality.dRep.spec.ts @@ -2,15 +2,23 @@ import environments from "@constants/environments"; import { dRep01Wallet } from "@constants/staticWallets"; import { createTempDRepAuth } from "@datafactory/createAuth"; import { test } from "@fixtures/walletExtension"; -import convertBufferToHex from "@helpers/convertBufferToHex"; +import { setAllureEpic } from "@helpers/allure"; import { ShelleyWallet } from "@helpers/crypto"; import { createNewPageWithWallet } from "@helpers/page"; -import { pollTransaction, waitForTxConfirmation } from "@helpers/transaction"; +import { + registerDRepForWallet, + transferAdaForWallet, + waitForTxConfirmation, +} from "@helpers/transaction"; import GovernanceActionDetailsPage from "@pages/governanceActionDetailsPage"; import GovernanceActionsPage from "@pages/governanceActionsPage"; import { expect } from "@playwright/test"; import kuberService from "@services/kuberService"; +test.beforeEach(async () => { + await setAllureEpic("5. Proposal functionality"); +}); + test.describe("Proposal checks", () => { test.use({ storageState: ".auth/dRep01.json", wallet: dRep01Wallet }); @@ -23,7 +31,7 @@ test.describe("Proposal checks", () => { govActionDetailsPage = await govActionsPage.viewFirstProposal(); }); - test("5A. Should show relevant details about governance action as DRep @slow", async () => { + test("5A. Should show relevant details about governance action as DRep", async () => { await expect(govActionDetailsPage.governanceActionType).toBeVisible(); await expect(govActionDetailsPage.submittedDate).toBeVisible(); await expect(govActionDetailsPage.expiryDate).toBeVisible(); @@ -37,11 +45,11 @@ test.describe("Proposal checks", () => { await expect(govActionDetailsPage.abstainRadio).toBeVisible(); }); - test("5B. Should view Vote button on governance action item on registered as DRep @slow", async () => { + test("5B. Should view Vote button on governance action item on registered as DRep", async () => { await expect(govActionDetailsPage.voteBtn).toBeVisible(); }); - test("5C. Should show required field in proposal voting on registered as DRep @slow", async () => { + test("5C. Should show required field in proposal voting on registered as DRep", async () => { await expect(govActionDetailsPage.voteBtn).toBeVisible(); await expect(govActionDetailsPage.yesVoteRadio).toBeVisible(); await expect(govActionDetailsPage.noVoteRadio).toBeVisible(); @@ -57,7 +65,8 @@ test.describe("Proposal checks", () => { }); // Skipped: No url/hash input to validate - test.skip("5D. Should validate proposal voting @slow", async () => { + test("5D. Should validate proposal voting", async () => { + test.skip(); // const invalidURLs = ["testdotcom", "https://testdotcom", "https://test.c"]; // invalidURLs.forEach(async (url) => { // govActionDetailsPage.urlInput.fill(url); @@ -88,7 +97,7 @@ test.describe("Proposal checks", () => { await expect( govActionDetailsPage.currentPage.getByText("Be careful", { exact: false, - }), + }) ).toBeVisible(); }); @@ -110,17 +119,8 @@ test.describe("Perform voting", () => { test.setTimeout(testInfo.timeout + 2 * environments.txTimeOut); const wallet = await ShelleyWallet.generate(); - const registrationRes = await kuberService.dRepRegistration( - convertBufferToHex(wallet.stakeKey.private), - convertBufferToHex(wallet.stakeKey.pkh), - ); - await pollTransaction(registrationRes.txId, registrationRes.lockInfo); - - const res = await kuberService.transferADA( - [wallet.addressBech32(environments.networkId)], - 40, - ); - await pollTransaction(res.txId, registrationRes.lockInfo); + await registerDRepForWallet(wallet); + await transferAdaForWallet(wallet, 40); const tempDRepAuth = await createTempDRepAuth(page, wallet); @@ -143,12 +143,12 @@ test.describe("Perform voting", () => { await waitForTxConfirmation(govActionDetailsPage.currentPage); const governanceActionsPage = new GovernanceActionsPage( - govActionDetailsPage.currentPage, + govActionDetailsPage.currentPage ); await governanceActionsPage.goto(); await governanceActionsPage.votedTab.click(); await expect( - govActionDetailsPage.currentPage.getByTestId("my-vote").getByText("Yes"), + govActionDetailsPage.currentPage.getByTestId("my-vote").getByText("Yes") ).toBeVisible(); govActionDetailsPage = await governanceActionsPage.viewFirstVotedProposal(); @@ -157,7 +157,7 @@ test.describe("Perform voting", () => { await governanceActionsPage.votedTab.click(); await expect( - govActionDetailsPage.currentPage.getByTestId("my-vote").getByText("No"), + govActionDetailsPage.currentPage.getByTestId("my-vote").getByText("No") ).toBeVisible(); }); @@ -173,12 +173,12 @@ test.describe("Perform voting", () => { await waitForTxConfirmation(govActionDetailsPage.currentPage); const governanceActionsPage = new GovernanceActionsPage( - govActionDetailsPage.currentPage, + govActionDetailsPage.currentPage ); await governanceActionsPage.goto(); await governanceActionsPage.votedTab.click(); await expect( - govActionDetailsPage.currentPage.getByTestId("my-vote").getByText("Yes"), + govActionDetailsPage.currentPage.getByTestId("my-vote").getByText("Yes") ).toBeVisible(); }); }); @@ -191,17 +191,8 @@ test.describe("Check voting power", () => { test.setTimeout(testInfo.timeout + 2 * environments.txTimeOut); const wallet = await ShelleyWallet.generate(); - const registrationRes = await kuberService.dRepRegistration( - convertBufferToHex(wallet.stakeKey.private), - convertBufferToHex(wallet.stakeKey.pkh) - ); - await pollTransaction(registrationRes.txId, registrationRes.lockInfo); - - const res = await kuberService.transferADA( - [wallet.addressBech32(environments.networkId)], - 40 - ); - await pollTransaction(res.txId, registrationRes.lockInfo); + await registerDRepForWallet(wallet); + await transferAdaForWallet(wallet, 40); const tempDRepAuth = await createTempDRepAuth(page, wallet); @@ -213,7 +204,7 @@ test.describe("Check voting power", () => { await dRepPage.goto("/"); await dRepPage.getByTestId("retire-button").click(); - await dRepPage.getByTestId("retire-button").click(); // BUG: testId -> continue-retire-button + await dRepPage.getByTestId("continue-retirement-button").click(); await expect( dRepPage.getByTestId("retirement-transaction-submitted-modal") ).toBeVisible(); diff --git a/tests/govtool-frontend/playwright/tests/5-proposal-functionality/proposalFunctionality.loggedin.spec.ts b/tests/govtool-frontend/playwright/tests/5-proposal-functionality/proposalFunctionality.loggedin.spec.ts index 17034e23e..5c5cae3b6 100644 --- a/tests/govtool-frontend/playwright/tests/5-proposal-functionality/proposalFunctionality.loggedin.spec.ts +++ b/tests/govtool-frontend/playwright/tests/5-proposal-functionality/proposalFunctionality.loggedin.spec.ts @@ -1,9 +1,14 @@ import { user01Wallet } from "@constants/staticWallets"; import { test } from "@fixtures/walletExtension"; +import { setAllureEpic } from "@helpers/allure"; import { expect } from "@playwright/test"; test.use({ storageState: ".auth/user01.json", wallet: user01Wallet }); +test.beforeEach(async () => { + await setAllureEpic("5. Proposal functionality"); +}); + test("5J. Should hide retirement option for non-registered DRep", async ({ page, }) => { diff --git a/tests/govtool-frontend/playwright/tests/6-miscellaneous/miscellaneous.loggedin.spec.ts b/tests/govtool-frontend/playwright/tests/6-miscellaneous/miscellaneous.loggedin.spec.ts index 8f58286fb..a1cf6ee2f 100644 --- a/tests/govtool-frontend/playwright/tests/6-miscellaneous/miscellaneous.loggedin.spec.ts +++ b/tests/govtool-frontend/playwright/tests/6-miscellaneous/miscellaneous.loggedin.spec.ts @@ -1,15 +1,18 @@ import { user01Wallet } from "@constants/staticWallets"; import { test } from "@fixtures/walletExtension"; +import DelegationPage from "@pages/dRepDirectoryPage"; +import { setAllureEpic } from "@helpers/allure"; import DRepRegistrationPage from "@pages/dRepRegistrationPage"; -import DelegationPage from "@pages/delegationPage"; import { expect } from "@playwright/test"; test.use({ storageState: ".auth/user01.json", wallet: user01Wallet }); +test.beforeEach(async () => { + await setAllureEpic("6. Miscellaneous"); +}); // Skipped: No dRepId to validate -test.skip("6B. Provides error for invalid format @fast @smoke", async ({ - page, -}) => { +test("6B. Provides error for invalid format", async ({ page }) => { + test.skip(); // invalid dRep delegation const delegationPage = new DelegationPage(page); await delegationPage.goto(); @@ -27,7 +30,7 @@ test.skip("6B. Provides error for invalid format @fast @smoke", async ({ // await expect(dRepRegistrationPage.hashInputError).toBeVisible(); }); -test("6D: Proper label and recognition of the testnet network @fast @smoke", async ({ +test("6D: Proper label and recognition of the testnet network", async ({ page, }) => { await page.goto("/"); diff --git a/tests/govtool-frontend/playwright/tests/6-miscellaneous/miscellaneous.spec.ts b/tests/govtool-frontend/playwright/tests/6-miscellaneous/miscellaneous.spec.ts index 0b9a7a33d..8c632c424 100644 --- a/tests/govtool-frontend/playwright/tests/6-miscellaneous/miscellaneous.spec.ts +++ b/tests/govtool-frontend/playwright/tests/6-miscellaneous/miscellaneous.spec.ts @@ -1,11 +1,13 @@ +import { setAllureEpic } from "@helpers/allure"; import { isMobile, openDrawer } from "@helpers/mobile"; import { expect, test } from "@playwright/test"; import environments from "lib/constants/environments"; -test("6C. Navigation within the dApp @smoke @fast", async ({ - page, - context, -}) => { +test.beforeEach(async () => { + await setAllureEpic("6. Miscellaneous"); +}); + +test("6C. Navigation within the dApp", async ({ page, context }) => { await page.goto("/"); if (isMobile(page)) { @@ -23,7 +25,7 @@ test("6C. Navigation within the dApp @smoke @fast", async ({ ]); await expect(guidesPage).toHaveURL( - `${environments.docsUrl}/about/what-is-sanchonet-govtool`, + `${environments.docsUrl}/about/what-is-sanchonet-govtool` ); if (isMobile(page)) { diff --git a/tests/govtool-frontend/playwright/tests/7-proposal-submission/proposalSubmission.loggedin.spec.ts b/tests/govtool-frontend/playwright/tests/7-proposal-submission/proposalSubmission.loggedin.spec.ts new file mode 100644 index 000000000..de191ccb3 --- /dev/null +++ b/tests/govtool-frontend/playwright/tests/7-proposal-submission/proposalSubmission.loggedin.spec.ts @@ -0,0 +1,72 @@ +import environments from "@constants/environments"; +import { user01Wallet } from "@constants/staticWallets"; +import { test } from "@fixtures/walletExtension"; +import { setAllureEpic } from "@helpers/allure"; +import ProposalSubmissionPage from "@pages/proposalSubmissionPage"; +import { expect } from "@playwright/test"; +import { IProposalForm, ProposalType } from "@types"; +import { bech32 } from "bech32"; + +test.use({ storageState: ".auth/user01.json", wallet: user01Wallet }); + +test.beforeEach(async () => { + await setAllureEpic("7. Proposal submission"); +}); + +test.describe("Accept valid data", () => { + Object.values(ProposalType).map((type: ProposalType, index) => { + test(`7E.${index + 1} Should accept valid data in ${type.toLowerCase()} proposal form`, async ({ + page, + }) => { + test.slow(); + + const proposalSubmissionPage = new ProposalSubmissionPage(page); + + await proposalSubmissionPage.goto(); + + await page.getByTestId(`${type}-radio`).click(); + await proposalSubmissionPage.continueBtn.click(); + + for (let i = 0; i < 100; i++) { + const randomBytes = new Uint8Array(10); + const bech32Address = bech32.encode("addr_test", randomBytes); + const formFields: IProposalForm = + proposalSubmissionPage.generateValidProposalFormFields( + type, + bech32Address + ); + await proposalSubmissionPage.validateForm(formFields); + } + + for (let i = 0; i < 7; i++) { + await expect(proposalSubmissionPage.addLinkBtn).toBeVisible(); + await proposalSubmissionPage.addLinkBtn.click(); + } + + await expect(proposalSubmissionPage.addLinkBtn).toBeHidden(); + }); + }); +}); + +test.describe("Reject invalid data", () => { + Object.values(ProposalType).map((type: ProposalType, index) => { + test(`7F.${index + 1} Should reject invalid data in ${type.toLowerCase()} Proposal form`, async ({ + page, + }) => { + test.slow(); + + const proposalSubmissionPage = new ProposalSubmissionPage(page); + + await proposalSubmissionPage.goto(); + + await page.getByTestId(`${type}-radio`).click(); + await proposalSubmissionPage.continueBtn.click(); + + const formFields: IProposalForm = + proposalSubmissionPage.generateInValidProposalFormFields(type); + for (let i = 0; i < 100; i++) { + await proposalSubmissionPage.inValidateForm(formFields); + } + }); + }); +}); diff --git a/tests/govtool-frontend/playwright/tests/7-proposal-submission/proposalSubmission.spec.ts b/tests/govtool-frontend/playwright/tests/7-proposal-submission/proposalSubmission.spec.ts new file mode 100644 index 000000000..d5c74e90b --- /dev/null +++ b/tests/govtool-frontend/playwright/tests/7-proposal-submission/proposalSubmission.spec.ts @@ -0,0 +1,14 @@ +import { setAllureEpic } from "@helpers/allure"; +import { expect, test } from "@playwright/test"; + +test.beforeEach(async () => { + await setAllureEpic("7. Proposal submission"); +}); + +test("7A. Should open wallet connection popup, when propose a governance action in disconnected state.", async ({ + page, +}) => { + await page.goto("/"); + await page.getByTestId("propose-a-governance-action-button").click(); + await expect(page.getByTestId("connect-your-wallet-modal")).toBeVisible(); +}); diff --git a/tests/govtool-frontend/playwright/tests/7-proposal-submission/proposalSubmissionFunctionality.tx.spec.ts b/tests/govtool-frontend/playwright/tests/7-proposal-submission/proposalSubmissionFunctionality.tx.spec.ts new file mode 100644 index 000000000..0af28bc9a --- /dev/null +++ b/tests/govtool-frontend/playwright/tests/7-proposal-submission/proposalSubmissionFunctionality.tx.spec.ts @@ -0,0 +1,51 @@ +import environments from "@constants/environments"; +import { createTempUserAuth } from "@datafactory/createAuth"; +import { test } from "@fixtures/walletExtension"; +import { setAllureEpic } from "@helpers/allure"; +import { ShelleyWallet } from "@helpers/crypto"; +import { createNewPageWithWallet } from "@helpers/page"; +import ProposalSubmissionPage from "@pages/proposalSubmissionPage"; +import { expect } from "@playwright/test"; +import { IProposalForm, ProposalType } from "@types"; + +test.beforeEach(async ({ browser, page }, testInfo) => { + await setAllureEpic("7. Proposal submission"); +}); + +test.describe("Proposal submission check", () => { + Object.values(ProposalType).map((type: ProposalType, index) => { + test(`7G.${index + 1}: Should open wallet connection popup, when registered with proper ${type.toLowerCase()} data`, async ({ + page, + browser, + }, testInfo) => { + test.setTimeout(testInfo.timeout + environments.txTimeOut); + + const wallet = await ShelleyWallet.generate(); + const tempUserAuth = await createTempUserAuth(page, wallet); + const governancePage = await createNewPageWithWallet(browser, { + storageState: tempUserAuth, + wallet, + enableStakeSigning: true, + }); + + const proposalSubmissionPage = new ProposalSubmissionPage(governancePage); + + await proposalSubmissionPage.goto(); + + await governancePage.getByTestId(`${type}-radio`).click(); + await proposalSubmissionPage.continueBtn.click(); + + const proposal: IProposalForm = + proposalSubmissionPage.generateValidProposalFormFields( + type, + wallet.rewardAddressBech32(environments.networkId) + ); + await proposalSubmissionPage.register({ ...proposal }); + await expect( + proposalSubmissionPage.registrationErrorModal.getByText( + "UTxO Balance Insufficient" + ) + ).toBeVisible(); + }); + }); +}); diff --git a/tests/govtool-frontend/playwright/tests/7-proposal-submission/proposalSubmissionVisibility.spec.ts b/tests/govtool-frontend/playwright/tests/7-proposal-submission/proposalSubmissionVisibility.spec.ts new file mode 100644 index 000000000..f3bd8f210 --- /dev/null +++ b/tests/govtool-frontend/playwright/tests/7-proposal-submission/proposalSubmissionVisibility.spec.ts @@ -0,0 +1,56 @@ +import { user01Wallet } from "@constants/staticWallets"; +import { test } from "@fixtures/walletExtension"; +import { setAllureEpic } from "@helpers/allure"; +import ProposalSubmissionPage from "@pages/proposalSubmissionPage"; +import { expect } from "@playwright/test"; +import { ProposalType } from "@types"; + +test.beforeEach(async () => { + await setAllureEpic("7. Proposal submission"); +}); + +test.use({ storageState: ".auth/user01.json", wallet: user01Wallet }); + +test("7B. Should access proposal submission page", async ({ page }) => { + await page.goto("/"); + await page.getByTestId("propose-governance-actions-button").click(); + + await expect( + page.getByText("Create a Governance Action", { exact: true }) + ).toBeVisible(); +}); + +test("7C. Should list governance action types", async ({ page }) => { + const proposalSubmissionPage = new ProposalSubmissionPage(page); + await proposalSubmissionPage.goto(); + + await expect(proposalSubmissionPage.infoRadioButton).toBeVisible(); + await expect(proposalSubmissionPage.treasuryRadioButton).toBeVisible(); +}); + +test.describe("Verify Proposal form", () => { + Object.values(ProposalType).map((type: ProposalType, index) => { + test(`7D.${index + 1}: Verify ${type.toLocaleLowerCase()} proposal form`, async ({ + page, + }) => { + const proposalSubmissionPage = new ProposalSubmissionPage(page); + await proposalSubmissionPage.goto(); + + await page.getByTestId(`${type}-radio`).click(); + await proposalSubmissionPage.continueBtn.click(); + + await expect(proposalSubmissionPage.titleInput).toBeVisible(); + await expect(proposalSubmissionPage.abstractInput).toBeVisible(); + await expect(proposalSubmissionPage.motivationInput).toBeVisible(); + await expect(proposalSubmissionPage.rationaleInput).toBeVisible(); + await expect(proposalSubmissionPage.addLinkBtn).toBeVisible(); + if (type === ProposalType.treasury) { + await expect( + proposalSubmissionPage.receivingAddressInput + ).toBeVisible(); + + await expect(proposalSubmissionPage.amountInput).toBeVisible(); + } + }); + }); +}); diff --git a/tests/govtool-frontend/playwright/tests/auth.setup.ts b/tests/govtool-frontend/playwright/tests/auth.setup.ts index d2206b142..6df7aad0f 100644 --- a/tests/govtool-frontend/playwright/tests/auth.setup.ts +++ b/tests/govtool-frontend/playwright/tests/auth.setup.ts @@ -2,17 +2,30 @@ import { adaHolder01Wallet, + adaHolder02Wallet, + adaHolder03Wallet, + adaHolder04Wallet, dRep01Wallet, user01Wallet, } from "@constants/staticWallets"; import { importWallet } from "@fixtures/importWallet"; import { test as setup } from "@fixtures/walletExtension"; +import { setAllureEpic, setAllureStory } from "@helpers/allure"; import LoginPage from "@pages/loginPage"; const dRep01AuthFile = ".auth/dRep01.json"; const adaHolder01AuthFile = ".auth/adaHolder01.json"; +const adaHolder02AuthFile = ".auth/adaHolder02.json"; +const adaHolder03AuthFile = ".auth/adaHolder03.json"; +const adaHolder04AuthFile = ".auth/adaHolder04.json"; + const user01AuthFile = ".auth/user01.json"; +setup.beforeEach(async () => { + await setAllureEpic("Setup"); + await setAllureStory("Authentication"); +}); + setup("Create DRep 01 auth", async ({ page, context }) => { await importWallet(page, dRep01Wallet); @@ -42,3 +55,33 @@ setup("Create AdaHolder 01 auth", async ({ page, context }) => { await context.storageState({ path: adaHolder01AuthFile }); }); + +setup("Create AdaHolder 02 auth", async ({ page, context }) => { + await importWallet(page, adaHolder02Wallet); + + const loginPage = new LoginPage(page); + await loginPage.login(); + await loginPage.isLoggedIn(); + + await context.storageState({ path: adaHolder02AuthFile }); +}); + +setup("Create AdaHolder 03 auth", async ({ page, context }) => { + await importWallet(page, adaHolder03Wallet); + + const loginPage = new LoginPage(page); + await loginPage.login(); + await loginPage.isLoggedIn(); + + await context.storageState({ path: adaHolder03AuthFile }); +}); + +setup("Create AdaHolder 04 auth", async ({ page, context }) => { + await importWallet(page, adaHolder04Wallet); + + const loginPage = new LoginPage(page); + await loginPage.login(); + await loginPage.isLoggedIn(); + + await context.storageState({ path: adaHolder04AuthFile }); +}); diff --git a/tests/govtool-frontend/playwright/tests/dRep.setup.ts b/tests/govtool-frontend/playwright/tests/dRep.setup.ts index 5203f1e47..f00ac7919 100644 --- a/tests/govtool-frontend/playwright/tests/dRep.setup.ts +++ b/tests/govtool-frontend/playwright/tests/dRep.setup.ts @@ -3,11 +3,8 @@ import { dRepWallets } from "@constants/staticWallets"; import { pollTransaction } from "@helpers/transaction"; import { expect, test as setup } from "@playwright/test"; import kuberService from "@services/kuberService"; -import { Logger } from "../../cypress/lib/logger/logger"; import fetch = require("node-fetch"); -const dRepInfo = require("../lib/_mock/dRepInfo.json"); - setup.describe.configure({ timeout: environments.txTimeOut }); dRepWallets.forEach((wallet) => { @@ -15,7 +12,7 @@ dRepWallets.forEach((wallet) => { try { const res = await kuberService.dRepRegistration( wallet.stake.private, - wallet.stake.pkh, + wallet.stake.pkh ); await pollTransaction(res.txId, res.lockInfo); @@ -28,16 +25,3 @@ dRepWallets.forEach((wallet) => { } }); }); - -setup("Setup dRep metadata", async () => { - try { - const res = await fetch(`${environments.metadataBucketUrl}/Test_dRep`, { - method: "PUT", - body: JSON.stringify(dRepInfo), - }); - Logger.success("Uploaded dRep metadata to bucket"); - } catch (err) { - Logger.fail(`Failed to upload dRep metadata: ${err}`); - throw err; - } -}); diff --git a/tests/govtool-frontend/playwright/tests/delegation.teardown.ts b/tests/govtool-frontend/playwright/tests/delegation.teardown.ts index 2831db009..343dde873 100644 --- a/tests/govtool-frontend/playwright/tests/delegation.teardown.ts +++ b/tests/govtool-frontend/playwright/tests/delegation.teardown.ts @@ -1,18 +1,22 @@ import environments from "@constants/environments"; import { adaHolderWallets } from "@constants/staticWallets"; +import { setAllureStory, setAllureEpic } from "@helpers/allure"; import { pollTransaction } from "@helpers/transaction"; import { test as cleanup } from "@playwright/test"; import kuberService from "@services/kuberService"; cleanup.describe.configure({ timeout: environments.txTimeOut }); - +cleanup.beforeEach(async () => { + await setAllureEpic("Setup"); + await setAllureStory("Cleanup"); +}); cleanup(`Abstain delegation`, async () => { const stakePrivKeys = adaHolderWallets.map((wallet) => wallet.stake.private); const stakePkhs = adaHolderWallets.map((wallet) => wallet.stake.pkh); const { txId, lockInfo } = await kuberService.abstainDelegations( stakePrivKeys, - stakePkhs, + stakePkhs ); await pollTransaction(txId, lockInfo); }); diff --git a/tests/govtool-frontend/playwright/tests/faucet.setup.ts b/tests/govtool-frontend/playwright/tests/faucet.setup.ts index 5aeb32639..eca65a562 100644 --- a/tests/govtool-frontend/playwright/tests/faucet.setup.ts +++ b/tests/govtool-frontend/playwright/tests/faucet.setup.ts @@ -1,4 +1,5 @@ import { faucetWallet } from "@constants/staticWallets"; +import { setAllureEpic, setAllureStory } from "@helpers/allure"; import { pollTransaction } from "@helpers/transaction"; import { test as setup } from "@playwright/test"; import { loadAmountFromFaucet } from "@services/faucetService"; @@ -7,9 +8,14 @@ import environments from "lib/constants/environments"; setup.describe.configure({ mode: "serial", timeout: environments.txTimeOut }); +setup.beforeEach(async () => { + await setAllureEpic("Setup"); + await setAllureStory("Fund"); +}); + setup("Fund faucet wallet", async () => { const balance = await kuberService.getBalance(faucetWallet.address); - if (balance > 2000) return; + if (balance > 10000) return; const res = await loadAmountFromFaucet(faucetWallet.address); await pollTransaction(res.txid); diff --git a/tests/govtool-frontend/playwright/tests/wallet.bootstrap.ts b/tests/govtool-frontend/playwright/tests/wallet.bootstrap.ts index 80c9848c2..2bcc49c80 100644 --- a/tests/govtool-frontend/playwright/tests/wallet.bootstrap.ts +++ b/tests/govtool-frontend/playwright/tests/wallet.bootstrap.ts @@ -1,25 +1,18 @@ import { adaHolderWallets, dRepWallets } from "@constants/staticWallets"; -import { ShelleyWallet } from "@helpers/crypto"; -import extractDRepsFromStakePubKey from "@helpers/extractDRepsFromStakePubkey"; -import generateShellyWallets from "@helpers/generateShellyWallets"; -import setupWallets from "@helpers/setupWallets"; +import { setAllureStory, setAllureEpic } from "@helpers/allure"; import { pollTransaction } from "@helpers/transaction"; import { expect, test as setup } from "@playwright/test"; import kuberService from "@services/kuberService"; -import { writeFile } from "fs"; import environments from "lib/constants/environments"; setup.describe.configure({ mode: "serial", timeout: environments.txTimeOut }); -setup("Setup mock wallets", async () => { - setup.skip(!environments.oneTimeWalletSetup); - - const wallets = await generateShellyWallets(6); - await setupWallets(wallets); - saveWallets(wallets); +setup.beforeEach(async () => { + await setAllureEpic("Setup"); }); setup("Fund static wallets", async () => { + await setAllureStory("Fund"); const addresses = [...adaHolderWallets, ...dRepWallets].map((e) => e.address); const res = await kuberService.transferADA(addresses); await pollTransaction(res.txId); @@ -27,12 +20,13 @@ setup("Fund static wallets", async () => { for (const wallet of [...adaHolderWallets, ...dRepWallets]) { setup(`Register stake of static wallet: ${wallet.address}`, async () => { + await setAllureStory("Register stake"); try { const { txId, lockInfo } = await kuberService.registerStake( wallet.stake.private, wallet.stake.pkh, wallet.payment.private, - wallet.address, + wallet.address ); await pollTransaction(txId, lockInfo); } catch (err) { @@ -44,25 +38,3 @@ for (const wallet of [...adaHolderWallets, ...dRepWallets]) { } }); } - -function saveWallets(wallets: ShelleyWallet[]) { - const jsonWallets = []; - for (let i = 0; i < wallets.length; i++) { - const stakePublicKey = Buffer.from(wallets[i].stakeKey.public).toString( - "hex", - ); - const { dRepIdBech32 } = extractDRepsFromStakePubKey(stakePublicKey); - - jsonWallets.push({ - ...wallets[i].json(), - address: wallets[i].addressBech32(environments.networkId), - dRepId: dRepIdBech32, - }); - } - const jsonString = JSON.stringify(jsonWallets, null, 2); - writeFile("lib/_mock/wallets.json", jsonString, "utf-8", (err) => { - if (err) { - throw Error("Failed to write wallets into file"); - } - }); -} diff --git a/tests/test-infrastructure/.env.example b/tests/test-infrastructure/.env.example index fd9687eeb..81ff08a2b 100644 --- a/tests/test-infrastructure/.env.example +++ b/tests/test-infrastructure/.env.example @@ -1,4 +1,4 @@ -STACK_NAME=govtool -BASE_DOMAIN=cardanoapi.io -BLOCKFROST_API_URL="" -BLOCKFROST_PROJECT_ID="" +PROJECT_NAME=govtool +CARDANO_NETWORK=sanchonet +BASE_DOMAIN=govtool.cardanoapi.io +GOVTOOL_TAG=test \ No newline at end of file diff --git a/tests/test-infrastructure/.gitignore b/tests/test-infrastructure/.gitignore index e433f6cb7..990529fba 100644 --- a/tests/test-infrastructure/.gitignore +++ b/tests/test-infrastructure/.gitignore @@ -1,5 +1,4 @@ secrets/ configs/ -docker-compose-rendered.yml -docker-compose-swarm-rendered.yml -docker-compose-services-rendered.yml +/*-rendered.yml + diff --git a/tests/test-infrastructure/README.md b/tests/test-infrastructure/README.md index d91eeaa44..054cda6f5 100644 --- a/tests/test-infrastructure/README.md +++ b/tests/test-infrastructure/README.md @@ -1,134 +1,41 @@ GovTool Test Infrastructure ==================== -Services required for testing GovTool +Compose files and scripts to deploy and test environment of govtool. +Additionally, it deploys services required to perform integration test on the environment -## 1. Setting up the services +## Compose files and services +1. [basic-services](./docker-compose-basic-services.yml) : postgres and gateway +2. [cardano](./docker-compose-cardano.yml) : node, dbsync and kuber +3. [govtool](./docker-compose-govtool.yml) : govtool-frontend and govtool-backend +4. [govaction-loader](./docker-compose-govaction-loader.yml) : govaction-loader frontend and badkcne +5. [test](./docker-compose-test.yml) : lighthouse-server and metadata-api +## Setting up the services -#### a. Deploy with docker on swarm mode. + +#### a. Update .env file and DNS records - Create `.env` file by copying `.env.example` and update it. - Make sure that DNS is pointed to the right server. Following are the domains used. - - lighthouse.BASE_DOMAIN - - metabase.BASE_DOMAIN - - sonarqube.BASE_DOMAIN - - metrics.BASE_DOMAIN - - kuber.BASE_DOMAIN - - -`docker stack deploy` command doesn't support `.env` file secret/config files. -There's a helper script `deploy-swarm.sh` to load the environment variables from `.env` file and generate rendered docker compose file. -```bash -cd ./test/test-infrastructire # cd into the test-infrastructure folder -docker swarm init # if swarm mode is not enabled yet. -docker compose build # build the images -docker node update xxxx --label-add govtool-test-stack=true ## set the node to be used for deploying the services -./gen-configs.sh # generate configs and secrets. -./deploy-swarm.sh prepare # start postgres and nginx -sleep 30 # wait for 30 secs for postgres to be healthy -./deploy-swarm.sh finalize # deploy all the required services. -``` - -#### b. Setup -When the stack is ready, further configuration is required it the services and github repo secrets and workflow files. - -# 2. Services List - -## SonarQube Server -#### Requires -- postgres database - -#### Used by -- Github Action to submit sonar-sacanner result - -`sonar-scanner` is used for static analysis of code. -The analysis generated by sonar-scanner is saved to SonarQube server for better visibility and to see progress over time. - - -**Docker Image:** [mc1arke/sonarqube-with-community-branch-plugin:9.9-community](https://hub.docker.com/layers/mc1arke/sonarqube-with-community-branch-plugin/9.9-community/images/sha256-b91ac551bea0fc3b394eaf7f82ea79115e03db9ab47d26610b9e1566723a07a5?context=explore) - -**See :** [sonar-scanner](https://docs.sonarsource.com/sonarqube/latest/analyzing-source-code/scanners/sonarscanner/), [actions/sonar-scanner](https://github.com/marketplace/actions/sonar-scanner) - -### Initial configuration. - -- Login and change the initial password. -``` -username: admin -password: admin -``` -- Create new project and set the projectKey in file [govtool/frontend/sonar-project.properties](../../govtool/frontend/sonar-project.properties) -- Update the github action secrets - - SONAR_HOST_URL - - SONAR_TOKEN - - -## Metabase Server -#### Requires -- postgres database - -Metabase provides UI to show graphs and visualization from different datasource. -It is used for visualizing the test metrics and the api response times over time. - -**Docker Image:** [metabase/metabase:v0.46.6.4](https://hub.docker.com/layers/metabase/metabase/v0.46.6.4/images/sha256-95c60db0c87c5da9cb81f6aefd0cd548fe2c14ff8c8dcba2ea58a338865cdbd9?context=explore) - -### Initial Configuration - - Setup initial account for ligin via the webapp. - - Under database section in admin settings, add the `govtool_lithghouse` and `govtool_metrics` databases - - Select the database and add visualizations, queries for the data. - -## LightHouse Report Server -#### Requires -- postgres database - -#### Used by -- Github Action to submit lighthouse report. - -Lighthouse has audits for performance, accessibility, progressive web apps, SEO, and more. -Lighthouse-Server is used to host and display the audits generated by lighthouse. - -**Docker Image:** [patrickhulce/lhci-server:0.12.0](https://hub.docker.com/r/patrickhulce/lhci-server) - -### Initial Configuration -- install lhci locally and run `lhci wizard` to setup project -- update `--serverBaseUrl={{...}}` parameter in [.github/workflows/lighthouse.yml](../../.github/workflows/lighthouse.yml) -- update `LHCI_SERVER_TOKEN` in github secrets. -- install lighthouse github app on the repo -- obtain app token from lighthouse app and update `LHCI_GITHUB_APP_TOKEN` secret - -See: **[lighthouse-server-docs](https://googlechrome.github.io/lighthouse-ci/docs/server.html)** - - -## Metrics API Server -#### Requires -- postgres database -- metabase *(for result visualization) - - -#### Used by -- Github Action - backend test to submit test metrics. - -Metrics API Server receives metrics collected during backend test and saves them to database. -The results are visualized in metabase. - -### Initial Configuration -- update `RECORD_METRICS_API` variable in file [.github/workflows/test_backend.yml](../../.github/workflows/test_backend.yml) - - -**Source Code:** [tests/test-metrics-api](../test-metrics-api) - -## Kuber Server -#### Requires -- cardano-node's socket connection - -#### Used by -- Cypress integration test -- Governance Data Loader - -Opensource API server for transaction building and querying the ledger . -Kuber makes it easy to construct and submit transaction from the frontend. - -**Docker Image:** [dquadrant/kuber:70be9b0166177eab5cf33e603fd3dc579e14cf31](https://hub.docker.com/layers/dquadrant/kuber/70be9b0166177eab5cf33e603fd3dc579e14cf31/images/sha256-d3b3f7c2304da8c4777155b26220238b682c81a3ff2b14753a5dc41c4f151364?context=explore) + - lighthouse-{BASE_DOMAIN} + - kuber-{BASE_DOMAIN} + - metadata-{BASE_DOMAIN} + - governance-{BASE_DOMAIN} + +### b. Prepare the machine. + - Buy a virtual server + - Install `docker` and enable `docker compose` plugin. + - execute `docker swarm init` command. + +### c. One time setup on the machine. + - Generate secrets and configurations required by the services + `./gen-configs.sh` + - Mark the nodes with labels to specify where the services should be run. In case of single node + docker swarm, all labels can be set to single node. + `./deploy.sh prepare` + +### d. Build images and deploy the stacks. + - `./build-images.sh` + - `./deploy.sh stack all` -### Initial Configuration -- update `CYPRESS_kuberApiUrl` variable in [.github/workflows/test_integration_cypress.yml](../../.github/workflows/test_integration_cypress.yml) diff --git a/tests/test-infrastructure/build-and-deploy.sh b/tests/test-infrastructure/build-and-deploy.sh new file mode 100755 index 000000000..919f64a15 --- /dev/null +++ b/tests/test-infrastructure/build-and-deploy.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +export BASE_IMAGE_NAME=govtool +export PROJECT_NAME=govtool +export CARDANO_NETWORK=sanchonet +export BASE_DOMAIN=govtool.cardanoapi.io + +if [ -z "$GOVTOOL_TAG" ]; then + GOVTOOL_TAG="$(git rev-parse HEAD)" +fi +export GOVTOOL_TAG + +. ./scripts/deploy-stack.sh + +check_env + +# Build images +./build-images.sh +function update-service(){ + docker service update --image "$2" "$1" +} + +if [[ "$1" == "update-images" ]] +then + update-service govtool_backend "$BASE_IMAGE_NAME"/backend:${GOVTOOL_TAG} + update-service govtool_frontend "$BASE_IMAGE_NAME"/frontend:${GOVTOOL_TAG} + update-service govtool_metadata-validation "$BASE_IMAGE_NAME"/metadata-validation:${GOVTOOL_TAG} + + update-service govaction-loader_backend "$BASE_IMAGE_NAME"/gov-action-loader-backend:${GOVTOOL_TAG} + update-service govaction-loader_frontend "$BASE_IMAGE_NAME"/gov-action-loader-frontend:${GOVTOOL_TAG} + + # test metadata API + update-service test_metadata-api "$BASE_IMAGE_NAME"/metadata-api:${GOVTOOL_TAG} + +elif [[ $1 == "full" ]] +then + ./deploy.sh stack all +fi diff --git a/tests/test-infrastructure/build-images.sh b/tests/test-infrastructure/build-images.sh new file mode 100755 index 000000000..e86103e7a --- /dev/null +++ b/tests/test-infrastructure/build-images.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -e +export BASE_IMAGE_NAME="govtool" +BASE_IMAGE_EXISTS=$(docker images -q "$BASE_IMAGE_NAME"/backend-base) + +if [ -z "$BASE_IMAGE_EXISTS" ]; then + echo "Building the base image..." + docker build -t "$BASE_IMAGE_NAME"/backend-base -f ../../govtool/backend/Dockerfile.base ../../govtool/backend +else + echo "Base image already exists. Skipping build." +fi + +docker compose -f ./docker-compose-govtool.yml build +docker compose -f ./docker-compose-govaction-loader.yml build +docker compose -f ./docker-compose-test.yml build \ No newline at end of file diff --git a/tests/test-infrastructure/configs_template/backend_config.json b/tests/test-infrastructure/configs_template/backend_config.json new file mode 100644 index 000000000..43cf7f251 --- /dev/null +++ b/tests/test-infrastructure/configs_template/backend_config.json @@ -0,0 +1,15 @@ +{ + "dbsyncconfig" : { + "host" : "postgres", + "dbname" : "${DBSYNC_DATABASE}", + "user" : "postgres", + "password" : "${POSTGRES_PASSWORD}", + "port" : 5432 + }, + "port" : 8080, + "host" : "0.0.0.0", + "cachedurationseconds": 20, + "sentrydsn": "https://username:password@senty.host/id", + "metadatavalidationhost": "http://metadata-validation", + "metadatavalidationport": 3000 +} \ No newline at end of file diff --git a/tests/test-infrastructure/configs_template/postgres_db_setup.sql b/tests/test-infrastructure/configs_template/postgres_db_setup.sql index 2934a2840..1c87ab3f1 100644 --- a/tests/test-infrastructure/configs_template/postgres_db_setup.sql +++ b/tests/test-infrastructure/configs_template/postgres_db_setup.sql @@ -1,4 +1,4 @@ -CREATE database ${STACK_NAME}_metabase; -CREATE database ${STACK_NAME}_lighthouse; -CREATE database ${STACK_NAME}_metrics; -CREATE database ${STACK_NAME}_sonarqube; +CREATE database ${PROJECT_NAME}_lighthouse; +CREATE database ${PROJECT_NAME}_metrics; +CREATE database ${PROJECT_NAME}_sonarqube; +CREATE database ${PROJECßT_NAME}_dbsync; \ No newline at end of file diff --git a/tests/test-infrastructure/deploy-swarm.sh b/tests/test-infrastructure/deploy-swarm.sh deleted file mode 100755 index 90c7fc269..000000000 --- a/tests/test-infrastructure/deploy-swarm.sh +++ /dev/null @@ -1,55 +0,0 @@ -#!/bin/bash -## Load environment variables and deploy to the docker swarm. -## -## Usages: -## ./deploy-swarm prepare -## -set -eo pipefail -set -a -. ./.env -set +a - -if [ "$1" == "destroy" ] -then - echo "This will remove everything in your stack including volumes" - echo "Are you Sure? (Y/N)" - read user_input - if ! ( [ "$user_input" = "y" ] || [ "$user_input" = "Y" ]) - then - exit 1 - fi - echo "Proceeding..." # Delete the Docker stack if "destroy" argument is provided - docker stack rm "${STACK_NAME}-services" || echo "${STACK_NAME}-services doesn't exist" - docker stack rm ${STACK_NAME} || echo "${STACK_NAME} doesn't exist" - ./gen-configs.sh clean - - for VOLUME in $(docker volume ls --filter "label=com.docker.stack.namespace=${STACK_NAME}" -q) "${STACK_NAME}-services_postgres" - do - echo -n "Removing Volume : " - docker volume rm "$VOLUME" - done - -elif [ "$1" == "prepare" ] -then - ## apply the enviroment to services compose file - ## and deploy the stack - envsubst < ./docker-compose-services.yml > ./docker-compose-services-rendered.yml - docker stack deploy -c './docker-compose-services-rendered.yml' ${STACK_NAME}-services - -elif [ "$1" == "finalize" ] -then - ## apply the environment to compose file - ## deploy the govtool test infrastructure stack - envsubst < ./docker-compose.yml > ./docker-compose-rendered.yml - docker stack deploy -c './docker-compose-rendered.yml' ${STACK_NAME} -else - echo "Something is wrong with the command" - echo - echo " Usage:" - echo " $0 (prepare | destroy | finalize)" - echo '' - echo " Options:" - echo " prepare -> deploys the services required by the test stack. i.e 'postgres' and 'reverse-proxy'" - echo " finalize -> deploys the test infrastructure services" - echo " destroy -> teardown everything except the volumes" -fi diff --git a/tests/test-infrastructure/deploy.sh b/tests/test-infrastructure/deploy.sh new file mode 100755 index 000000000..c14d575b9 --- /dev/null +++ b/tests/test-infrastructure/deploy.sh @@ -0,0 +1,112 @@ +#!/bin/bash +## Load environment variables and deploy to the docker swarm. +## +## Usages: +## ./deploy-swarm prepare +## +set -eo pipefail +. ./scripts/deploy-stack.sh +load_env + +DOCKER_STACKS=("basic-services" "cardano" "govaction-loader" "govtool" "test") + +if [ "$1" == "destroy" ] +then + echo "This will remove everything in your stack except volumes, configs and secrets" + echo "Are you Sure? (Y/N)" + read user_input + if ! ( [ "$user_input" = "y" ] || [ "$user_input" = "Y" ]) + then + exit 1 + fi + echo "Proceeding..." # Delete the Docker stack if "destroy" argument is provided + + REVERSE_STACKS=() + for ((i=${#STACKS[@]}-1; i>=0; i--)); do + REVERSE_STACKS+=("${STACKS[i]}") + done + + for CUR_STACK in "${REVERSE_STACKS[@]}"; do + docker stack rm "$CUR_STACK" + sleep 6 # wait 6 seconds for each stack cleanup. + done + +# ./gen-configs.sh clean + +# for VOLUME in $(docker volume ls --filter "label=com.docker.stack.namespace=${STACK_NAME}" -q) "${STACK_NAME}-services_postgres" +# do +# echo -n "Removing Volume : " +# docker volume rm "$VOLUME" +# done +elif [ "$1" == 'prepare' ] +then + + # Get the number of nodes in the swarm + NODES=$(docker node ls --format "{{.ID}}" | wc -l) + + # If there is only one node, set the labels + if [ "$NODES" -eq 1 ]; then + NODE_ID=$(docker node ls --format "{{.ID}}") + + docker node update --label-add govtool-test-stack=true \ + --label-add blockchain=true \ + --label-add gateway=true \ + --label-add govtool=true \ + --label-add gov-action-loader=true \ + "$NODE_ID" + + echo "Labels set on node: $NODE_ID" + else + echo "There are multiple nodes in the docker swarm." + echo "Please set the following labels to correct nodes manually." + echo " - govtool-test-stack " + echo " - blockchain" + echo " - gateway" + echo " - govtool" + echo " - gov-action-loader" + echo "" + echo " e.g. $ docker node update xxxx --label-add gateway=true" + + exit 1 + fi + +elif [ "$1" == 'stack' ] +then + if [ "$#" -ne 2 ] + then + echo "stack requires the stack name". + echo "Usage :" + echo " > $0 stack [stack-name]". + echo "" + echo " stack-name : One of the following"ß + echo " $DOCKER_STACKS" + else + case "$2" in + all) + + for DEPLOY_STACK in "${DOCKER_STACKS[@]}"; do + deploy-stack "$DEPLOY_STACK" "docker-compose-$DEPLOY_STACK.yml" + done + + ;; + *) + if [[ ! -f ./"docker-compose-$2.yml" ]] + then + echo "Invalid stack name. $2" + else + deploy-stack $2 "docker-compose-$2.yml" + fi + ;; + esac + fi +else + echo "Something is wrong with the command" + echo + echo " Usage:" + echo " $0 (prepare | destroy | deploy)" + echo '' + echo " Options:" + echo " prepare -> set required labels to docker swarm node." + echo " destroy -> teardown everything except the volumes" + echo " deploy [stack_name] -> Deploy the stack." +fi diff --git a/tests/test-infrastructure/docker-compose-services.yml b/tests/test-infrastructure/docker-compose-basic-services.yml similarity index 75% rename from tests/test-infrastructure/docker-compose-services.yml rename to tests/test-infrastructure/docker-compose-basic-services.yml index d7563a2ad..e0b417494 100644 --- a/tests/test-infrastructure/docker-compose-services.yml +++ b/tests/test-infrastructure/docker-compose-basic-services.yml @@ -2,24 +2,15 @@ version: "3.9" secrets: postgres_user: external: true - name: ${STACK_NAME}_postgres_user + name: ${PROJECT_NAME}_postgres_user postgres_password: external: true - name: ${STACK_NAME}_postgres_password + name: ${PROJECT_NAME}_postgres_password configs: postgres_db_setup.sql: external: true - name: ${STACK_NAME}_postgres_db_setup.sql + name: ${PROJECT_NAME}_postgres_db_setup.sql -### secrets and configs in docker compose -# secrets: -# postgres_user: -# file: "./secrets/${STACK_NAME}_postgres_user" -# postgres_password: -# file: "./secrets/${STACK_NAME}_postgres_password" -# configs: -# postgres_db_setup.sql: -# file: "./configs/${STACK_NAME}_postgres_db_setup.sql" volumes: postgres: nginx_dhparam: @@ -54,7 +45,7 @@ services: deploy: placement: constraints: - - node.labels.govtool-test-stack == true + - node.labels.gateway == true restart_policy: delay: "10s" postgres: @@ -73,6 +64,8 @@ services: - postgres volumes: - postgres:/var/lib/postgresql/data + ports: + - 5432:5432 restart: always healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] @@ -83,6 +76,6 @@ services: deploy: placement: constraints: - - node.labels.govtool-test-stack == true + - node.labels.blockchain == true restart_policy: delay: "30s" diff --git a/tests/test-infrastructure/docker-compose-cardano.yml b/tests/test-infrastructure/docker-compose-cardano.yml new file mode 100644 index 000000000..514324cfc --- /dev/null +++ b/tests/test-infrastructure/docker-compose-cardano.yml @@ -0,0 +1,103 @@ +version: "3.9" +secrets: + postgres_user: + external: true + name: ${PROJECT_NAME}_postgres_user + postgres_password: + external: true + name: ${PROJECT_NAME}_postgres_password + dbsync_database: + external: true + name: ${PROJECT_NAME}_dbsync_database + +volumes: + node_data: + node_ipc: + dbsync_data: + +networks: + postgres: + external: true + frontend: + external: true + cardano: + attachable: true + name: cardano + +services: + node: + image: ghcr.io/intersectmbo/cardano-node:8.11.0-sancho + environment: + NETWORK: ${CARDANO_NETWORK} + volumes: + - node_data:/data + - node_ipc:/ipc + stop_grace_period: 1m + logging: + driver: "json-file" + options: + max-size: "10M" + max-file: "10" + ports: + - target: 3001 + published: 3001 + protocol: tcp + mode: host + deploy: + placement: + constraints: + - node.labels.blockchain==true + restart_policy: + condition: on-failure + delay: 15s + dbsync: + image: ghcr.io/intersectmbo/cardano-db-sync:sancho-4-2-1 + networks: + - postgres + environment: + NETWORK: ${CARDANO_NETWORK} + POSTGRES_HOST: postgres + POSTGRES_PORT: 5432 + DISABLE_CACHE: "" + DISABLE_LEDGER: "" + DISABLE_EPOCH: "" + secrets: + - postgres_user + - source: dbsync_database + target: postgres_db + - postgres_password + volumes: + - dbsync_data:/var/lib/cexplorer + - node_ipc:/node-ipc + logging: + driver: "json-file" + options: + max-size: "10M" + max-file: "10" + deploy: + labels: + "co_elastic_logs/enable": "false" + placement: + constraints: + - node.labels.blockchain== true + restart_policy: + condition: on-failure + delay: 15s + kuber: + image: dquadrant/kuber:4c3c5230db9a9b8ac84487fbc11ccd28b0cd5917-amd64 + environment: + CARDANO_NODE_SOCKET_PATH: /ipc/node.socket + VIRTUAL_HOST: https://kuber-${BASE_DOMAIN} + NETWORK: 4 + START_ERA: CONWAY + volumes: + - node_ipc:/ipc/ + networks: + - cardano + - frontend + deploy: + placement: + constraints: + - node.labels.blockchain== true + restart_policy: + delay: "30s" diff --git a/tests/test-infrastructure/docker-compose-govaction-loader.yml b/tests/test-infrastructure/docker-compose-govaction-loader.yml new file mode 100644 index 000000000..269bc4202 --- /dev/null +++ b/tests/test-infrastructure/docker-compose-govaction-loader.yml @@ -0,0 +1,55 @@ +version: "3.9" + +networks: + frontend: + external: true + cardano: + external: true + +services: + + frontend: + image: govtool/gov-action-loader-frontend:${GOVTOOL_TAG} + build: + context: ../../gov-action-loader/frontend + dockerfile: Dockerfile + environment: + VIRTUAL_HOST: https://governance-${BASE_DOMAIN} + networks: + - frontend + deploy: + placement: + constraints: + - node.labels.gov-action-loader == true + restart_policy: + delay: "30s" + resources: + limits: + memory: 500M + reservations: + memory: 100M + + backend: + image: govtool/gov-action-loader-backend:${GOVTOOL_TAG} + build: + context: ../../gov-action-loader/backend + dockerfile: Dockerfile + environment: + KUBER_API_URL: "http://kuber:8081" + KUBER_API_KEY: "" + VIRTUAL_HOST: https://governance-${BASE_DOMAIN}/api/ -> /api/ + networks: + - default + - frontend + - cardano + deploy: + placement: + constraints: + - node.labels.gov-action-loader == true + restart_policy: + delay: "30s" + resources: + limits: + memory: 1G + reservations: + memory: 500M \ No newline at end of file diff --git a/tests/test-infrastructure/docker-compose-govtool.yml b/tests/test-infrastructure/docker-compose-govtool.yml new file mode 100644 index 000000000..b31f5b9ae --- /dev/null +++ b/tests/test-infrastructure/docker-compose-govtool.yml @@ -0,0 +1,67 @@ +version: "3.9" +networks: + frontend: + external: true + postgres: + external: true +configs: + config.json: + name: govtool_backend_config.json + external: true +services: + backend: + image: govtool/backend:${GOVTOOL_TAG} + build: + context: ../../govtool/backend + args: + BASE_IMAGE_REPO: govtool/backend-base + entrypoint: + - sh + - -c + - vva-be -c /config.json start-app + environment: + VIRTUAL_HOST: https://${BASE_DOMAIN}/api/ -> :8080/ + VIRTUAL_HOST_2: https://${BASE_DOMAIN}/swagger -> :8080/swagger + + networks: + - frontend + - postgres + configs: + - config.json + deploy: + restart_policy: + delay: "30s" + placement: + constraints: + - node.labels.govtool==true + frontend: + image: govtool/frontend:${GOVTOOL_TAG} + build: + context: ../../govtool/frontend + args: + VITE_BASE_URL: "/api" + environment: + VIRTUAL_HOST: https://${BASE_DOMAIN} + networks: + - frontend + deploy: + restart_policy: + delay: "30s" + placement: + constraints: + - node.labels.govtool==true + metadata-validation: + image: govtool/metadata-validation:${GOVTOOL_TAG} + build: + context: ../../govtool/metadata-validation + environment: + VIRTUAL_HOST: https://${BASE_DOMAIN}/metadata-validation/ -> :3000 + PORT: '3000' + networks: + - frontend + deploy: + restart_policy: + delay: "30s" + placement: + constraints: + - node.labels.govtool==true diff --git a/tests/test-infrastructure/docker-compose-test.yml b/tests/test-infrastructure/docker-compose-test.yml new file mode 100644 index 000000000..6b03e358d --- /dev/null +++ b/tests/test-infrastructure/docker-compose-test.yml @@ -0,0 +1,56 @@ +version: "3.9" +secrets: + lighthouserc.json: + external: true + name: ${PROJECT_NAME}_lighthouserc.json + +volumes: + lhci_data: + metadata_data: +networks: + postgres: + external: true + frontend: + external: true + +services: + lhci-server: + image: patrickhulce/lhci-server:0.12.0 + environment: + VIRTUAL_HOST: https://lighthouse-${BASE_DOMAIN} -> :9001 + volumes: + - lhci_data:/data + secrets: + - source: lighthouserc.json + target: /usr/src/lhci/lighthouserc.json + networks: + - postgres + - frontend + deploy: + placement: + constraints: + - node.labels.govtool-test-stack == true + restart_policy: + delay: "30s" + resources: + limits: + memory: 1G + reservations: + memory: 300M + + metadata-api: + image: govtool/metadata-api:${GOVTOOL_TAG} + build: + context: ../test-metadata-api + environment: + VIRTUAL_HOST: https://metadata-${BASE_DOMAIN} -> :3000 + networks: + - frontend + volumes: + - metadata_data:/data + deploy: + restart_policy: + delay: "30s" + placement: + constraints: + - node.labels.govtool-test-stack==true \ No newline at end of file diff --git a/tests/test-infrastructure/docker-compose.yml b/tests/test-infrastructure/docker-compose.yml deleted file mode 100644 index 9e8f77da5..000000000 --- a/tests/test-infrastructure/docker-compose.yml +++ /dev/null @@ -1,246 +0,0 @@ -version: "3.9" -secrets: - postgres_user: - external: true - name: ${STACK_NAME}_postgres_user - postgres_password: - external: true - name: ${STACK_NAME}_postgres_password - lighthouserc.json: - external: true - name: ${STACK_NAME}_lighthouserc.json - metrics_api_secret_token: - external: true - name: ${STACK_NAME}_metrics_api_secret - -## secrets syntax for docker compose stack -# secrets: -# postgres_user: -# file: "./secrets/${STACK_NAME}_postgres_user" -# postgres_password: -# file: "./secrets/${STACK_NAME}_postgres_password" -# postgres_db: -# file: "./secrets/${STACK_NAME}_postgres_user" -# lighthouserc.json: -# file: "./secrets/${STACK_NAME}_lighthouserc.json" -# metrics_api_secret_token: -# file: "./secrets/${STACK_NAME}_metrics_api_secret" -volumes: - lhci_data: - sonar_data: - sonar_logs: - node_data: - node_ipc: - -networks: - postgres: - external: true - frontend: - external: true - -services: - metabase: - image: metabase/metabase:v0.46.6.2 - hostname: metabase - volumes: - - /dev/urandom:/dev/random:ro - environment: - VIRTUAL_HOST: https://metabase.${BASE_DOMAIN} - MB_DB_TYPE: postgres - MB_DB_DBNAME: ${STACK_NAME}_metabase - MB_DB_PORT: 5432 - MB_DB_USER_FILE: /run/secrets/postgres_user - MB_DB_PASS_FILE: /run/secrets/postgres_password - MB_DB_HOST: postgres - networks: - - postgres - - frontend - secrets: - - postgres_password - - postgres_user - deploy: - placement: - constraints: - - node.labels.govtool-test-stack == true - restart_policy: - delay: "30s" - resources: - limits: - memory: 3G - reservations: - memory: 1.8G - - healthcheck: - test: curl --fail -I http://localhost:3000/api/health || exit 1 - interval: 15s - timeout: 5s - retries: 5 - - metrics_api: - image: voltaire-era/govtool-metrics-api - build: - context: ../test-metrics-api - - environment: - VIRTUAL_HOST: https://metrics.${BASE_DOMAIN}/ -> :3000/ - PGHOST: postgres - PGDATABASE: ${STACK_NAME}_metrics - secrets: - - source: postgres_password - target: /run/secrets/pgpassword - - source: postgres_user - target: /run/secrets/pguser - - source: metrics_api_secret_token - target: /run/secrets/api_secret_token - networks: - - postgres - - frontend - deploy: - placement: - constraints: - - node.labels.govtool-test-stack == true - restart_policy: - delay: "30s" - resources: - limits: - memory: 600M - reservations: - memory: 100M - - lhci-server: - image: patrickhulce/lhci-server:0.12.0 - environment: - VIRTUAL_HOST: https://lighthouse.${BASE_DOMAIN} -> :9001 - volumes: - - lhci_data:/data - secrets: - - source: lighthouserc.json - target: /usr/src/lhci/lighthouserc.json - networks: - - postgres - - frontend - deploy: - placement: - constraints: - - node.labels.govtool-test-stack == true - restart_policy: - delay: "30s" - resources: - limits: - memory: 1G - reservations: - memory: 300M - - governance-action-loader-ui: - image: voltaire-era/govtool-governance-action-loader - build: - context: ../../src/gov-action-loader-fe - dockerfile: Dockerfile - environment: - VIRTUAL_HOST: https://govtool-governance.${BASE_DOMAIN} - networks: - - frontend - deploy: - placement: - constraints: - - node.labels.govtool-test-stack == true - restart_policy: - delay: "30s" - resources: - limits: - memory: 500M - reservations: - memory: 100M - - governance-action-loader-api: - image: voltaire-era/govtool-kuber-proposal-loader-proxy - build: - context: ../../src/gov-action-loader-be - dockerfile: Dockerfile - environment: - KUBER_API_URL: "http://kuber:8081" - KUBER_API_KEY: "" - BLOCKFROST_API_URL: "${BLOCKFROST_API_URL}" - BLOCKFROST_PROJECT_ID: "${BLOCKFROST_PROJECT_ID}" - VIRTUAL_HOST: https://govtool-governance.${BASE_DOMAIN}/api/ -> /api/ - networks: - - default - - frontend - deploy: - placement: - constraints: - - node.labels.govtool-test-stack == true - restart_policy: - delay: "30s" - resources: - limits: - memory: 1G - reservations: - memory: 500M - - sonarqube_server: - image: mc1arke/sonarqube-with-community-branch-plugin:9.9-community - networks: - - frontend - - postgres - environment: - SONAR_JDBC_URL: jdbc:postgresql://postgres:5432/${STACK_NAME}_sonarqube - VIRTUAL_HOST: https+wss://sonarqube.${BASE_DOMAIN} -> :9000 - SONAR_JDBC_USERNAME: postgres - volumes: - - sonar_data:/opt/sonarqube/data - - sonar_logs:/opt/sonarqube/logs - entrypoint: "sh -c 'SONAR_JDBC_PASSWORD=\"$$( cat /run/secrets/postgres_password )\" /opt/sonarqube/docker/entrypoint.sh'" - secrets: - - postgres_password - deploy: - placement: - constraints: - - node.labels.govtool-test-stack == true - restart_policy: - delay: 15s - resources: - limits: - memory: 3.5G - reservations: - memory: 2.2G - cardano-node: - image: ghcr.io/intersectmbo/cardano-node:8.7.1-pre - environment: - NETWORK: sanchonet - volumes: - - node_data:/data - - node_ipc:/ipc - stop_grace_period: 1m - logging: - driver: "json-file" - options: - max-size: "200k" - max-file: "10" - ports: - - target: 3001 - published: 30004 - protocol: tcp - mode: host - deploy: - placement: - constraints: - - node.labels.govtool-test-stack == true - restart_policy: - condition: on-failure - delay: 15s - kuber: - image: dquadrant/kuber - environment: - CARDANO_NODE_SOCKET_PATH: /ipc/node.socket - VIRTUAL_HOST: https://kuber.${BASE_DOMAIN} - NETWORK: 4 - START_ERA: CONWAY - volumes: - - node_ipc:/ipc/ - deploy: - placement: - constraints: - - node.labels.govtool-test-stack == true - restart_policy: - delay: "30s" diff --git a/tests/test-infrastructure/gen-configs.sh b/tests/test-infrastructure/gen-configs.sh index 19acbcc75..7d8d83e65 100755 --- a/tests/test-infrastructure/gen-configs.sh +++ b/tests/test-infrastructure/gen-configs.sh @@ -2,55 +2,53 @@ ####### Script for generating docker secret files and configs. ####### If the docker is in swarm mode, it will also generate the docker swarm secrets. ####### +set -e if ! [ -f ./.env ] then echo ".env file is missing" exit 1 fi + set -a . ./.env set +a + # Function to generate a random secret in base64 format without padding and '+' function generate_secret() { - openssl rand -base64 16 | tr -d '=+/' + local filename=$2 + local var_name=$1 + if [ -s "$filename" ]; then + export "$var_name"=$(<"$filename") + else + local secret=$(openssl rand -base64 16 | tr -d '=+/') + echo -n "$secret" > "$filename" + export "$var_name"="$secret" + fi } -# Generate random secrets -export POSTGRES_USER=postgres -export POSTGRES_PASSWORD=$(generate_secret) -metrics_api_secret=$(generate_secret) - - if [ "$1" == "clean" ]; then - set -x - rm -rf ./configs; - rm -rf ./secrets; - - set +x - docker info | grep 'Swarm: active' > /dev/null 2>/dev/null || exit 0 - for CONFIG_FILE in $(ls ./configs_template) + # Create secrets from files + for SECRET_FILE in $(ls ./secrets) do - echo -n "Removing Config : " - docker config rm "${STACK_NAME}_${CONFIG_FILE}" || true + SECRET_NAME="$(basename $SECRET_FILE)" + echo -n "Removing secret: ${PROJECT_NAME}_${SECRET_NAME}" + docker secret rm "${PROJECT_NAME}_${SECRET_NAME}" || true done - for SECRET_FILE in "$(ls ./secrets_template)" "postgres_user" "postgres_password" "metrics_api_secret" + # Create configs from files + for CONFIG_FILE in $(ls ./configs) do - echo -n "Removing Secret : " - docker secret rm "${STACK_NAME}_${SECRET_FILE}" ||true + CONFIG_NAME=$(basename $CONFIG_FILE) + echo -n "Removing config: ${PROJECT_NAME}_${CONFIG_NAME}" + docker config rm "${PROJECT_NAME}_${CONFIG_NAME}" || true done - exit 0 -fi -## Check if one fo the secrets already exists -if [[ -f ./secrets/govtool_postgres_user ]] -then - echo "File ./secrets/govtool_postgres_user already exists." - echo "Assuming that the secrets were already generated" - echo " Use:" - echo " > ./gen-configs.sh clean" - echo " To clean up the configs and secrets" + set -x + rm -rf ./configs; + rm -rf ./secrets; + + set +x; exit 0 fi @@ -59,53 +57,48 @@ mkdir -p ./configs; mkdir -p ./secrets; -## save secrets to secrets folder -echo -n $POSTGRES_USER > ./secrets/govtool_postgres_user -echo -n $POSTGRES_PASSWORD > ./secrets/govtool_postgres_password -echo -n $metrics_api_secret > ./secrets/govtool_metrics_api_secret +# Generate random secrets +export POSTGRES_USER=postgres +export DBSYNC_DATABASE="${PROJECT_NAME}_dbsync" +# Save secrets to files +echo -n $POSTGRES_USER > ./secrets/postgres_user +echo -n "$DBSYNC_DATABASE" > ./secrets/dbsync_database -## loop over templates and updaete them. +# generate or load the secret +generate_secret "POSTGRES_PASSWORD" "./secrets/postgres_password" +## loop over templates and update them. for CONFIG_FILE in $(ls ./configs_template) do - echo -n "Config ${STACK_NAME}_${CONFIG_FILE}: " - envsubst < "./configs_template/$CONFIG_FILE" > "./configs/${STACK_NAME}_${CONFIG_FILE}" + echo -n "Config ${PROJECT_NAME}_${CONFIG_FILE}: " + envsubst < "./configs_template/$CONFIG_FILE" > "./configs/${CONFIG_FILE}" done for SECRET_FILE in $(ls ./secrets_template) do - echo -n "Secret ${STACK_NAME}_${SECRET_FILE}: " - envsubst < "./secrets_template/$SECRET_FILE" > "./secrets/${STACK_NAME}_${SECRET_FILE}" + echo -n "Secret ${PROJECT_NAME}_${SECRET_FILE}: " + envsubst < "./secrets_template/$SECRET_FILE" > "./secrets/${SECRET_FILE}" done - - ################################################################################ ################ Create secret/config for swarm ############################### ################################################################################ docker info | grep 'Swarm: active' > /dev/null 2>/dev/null || exit 0 -echo "Creating Secret: ${STACK_NAME}_postgres_user" -echo "$POSTGRES_USER" | (docker secret create "${STACK_NAME}_postgres_user" - ) || true - -echo "Generating Secret: ${STACK_NAME}_postgres_password" -echo "$POSTGRES_PASSWORD" | (docker secret create "${STACK_NAME}_postgres_password" - ) || true - -echo "Generating Secret: ${STACK_NAME}_metrics_api_secret" -echo "$metrics_api_secret" | (docker secret create "${STACK_NAME}_metrics_api_secret" - )|| true - - - -for CONFIG_FILE in $(ls ./configs_template) -do - echo -n "Creating Config: ${STACK_NAME}_${CONFIG_FILE} " - cat "./configs/${STACK_NAME}_${CONFIG_FILE}" | docker config create "${STACK_NAME}_${CONFIG_FILE}" - || true +# Create secrets from files +ls ./secrets | while IFS= read -r SECRET_FILE; do + SECRET_NAME=$(basename "$SECRET_FILE") + echo -n "Secret: ${PROJECT_NAME}_${SECRET_NAME}: " + cat "./secrets/$SECRET_NAME" | (docker secret create "${PROJECT_NAME}_${SECRET_NAME}" -) || true done -for SECRET_FILE in $(ls ./secrets_template) + +# Create configs from files +for CONFIG_FILE in $(ls ./configs) do - echo -n "Creating Secret: ${STACK_NAME}_${SECRET_FILE} " - cat "./secrets/${STACK_NAME}_${SECRET_FILE}" | docker secret create "${STACK_NAME}_${SECRET_FILE}" - ||true -done + CONFIG_NAME=$(basename $CONFIG_FILE) + echo -n "Config: ${PROJECT_NAME}_${CONFIG_NAME}: " + cat "./configs/$CONFIG_NAME" | (docker config create "${PROJECT_NAME}_${CONFIG_NAME}" -) || true +done \ No newline at end of file diff --git a/tests/test-infrastructure/playbook.yml b/tests/test-infrastructure/playbook.yml new file mode 100644 index 000000000..37656787a --- /dev/null +++ b/tests/test-infrastructure/playbook.yml @@ -0,0 +1,22 @@ +--- +- name: Update deployed images + hosts: test_server + gather_facts: no + tasks: + - name: Checkout to GOVTOOL_TAG commit + ansible.builtin.git: + repo: https://github.com/intersectmbo/govtool + dest: /opt/govtool + version: "{{ lookup('env', 'GOVTOOL_TAG') }}" + force: yes + update: yes + clone: yes + become: yes + + - name: Execute build-and-deploy.sh + ansible.builtin.shell: "/opt/govtool/tests/test-infrastructure/build-and-deploy.sh update-images" + args: + chdir: "/opt/govtool/tests/test-infrastructure" + environment: + GOVTOOL_TAG: "{{ lookup('env', 'GOVTOOL_TAG') }}" + become: yes \ No newline at end of file diff --git a/tests/test-infrastructure/scripts/deploy-stack.sh b/tests/test-infrastructure/scripts/deploy-stack.sh new file mode 100755 index 000000000..38920f07e --- /dev/null +++ b/tests/test-infrastructure/scripts/deploy-stack.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +## Docker swarm doesn't read .env file. +## This script reads env file and variables +## and apply them to compose file and +## then execute `docker stack deploy` + +set -eo pipefail + +function load_env(){ + if [[ -f ./.env ]] + then + set -a + . ./.env + set +a + fi + check_env +} + + +function check_env(){ + + # Path to the .env.example file + EXAMPLE_FILE=".env.example" + + unset_keys=() + + # Read each line of the .env.example file + while IFS= read -r line || [ -n "$line" ]; do + # Skip empty lines + if [ -z "$line" ]; then + continue + fi + + line=$(echo "$line" | sed -e 's/^[[:space:]]*//') + + # Extract the key from each line + key=$(echo "$line" | cut -d'=' -f1) + + if [ -z "${!key}" ]; then + unset_keys+=("$key") + fi + done < "$EXAMPLE_FILE" + + # Print error message for unset keys + if [ ${#unset_keys[@]} -gt 0 ]; then + echo "The following keys are not set in the environment:" + for key in "${unset_keys[@]}"; do + echo "- $key" + done + echo " Exiting due to missing env variables" + exit 2 + fi +} +function deploy-stack(){ + echo "++ deploy-stack" "$@" + ## apply the environment to compose file + ## deploy the govtool test infrastructure stack + ## first argument is stack name and 2nd argument is the file name + STACK_NAME=$1 + COMPOSE_FILE=$2 + FILENAME=$(basename -- "$COMPOSE_FILE") + EXTENSION="${FILENAME##*.}" + FILENAME_WITHOUT_EXT="${FILENAME%.*}" + RENDERED_FILENAME="${FILENAME_WITHOUT_EXT}-rendered.${EXTENSION}" + envsubst < "$COMPOSE_FILE" > "$RENDERED_FILENAME" + docker stack deploy -c "$RENDERED_FILENAME" ${STACK_NAME} +} \ No newline at end of file diff --git a/tests/test-infrastructure/secrets_template/lighthouserc.json b/tests/test-infrastructure/secrets_template/lighthouserc.json index 65930f8fa..ee7be38d2 100644 --- a/tests/test-infrastructure/secrets_template/lighthouserc.json +++ b/tests/test-infrastructure/secrets_template/lighthouserc.json @@ -3,7 +3,7 @@ "server": { "storage": { "sqlDialect": "postgres", - "sqlConnectionUrl": "postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres/${STACK_NAME}_lighthouse?application_name=lighthouse-ci-server" + "sqlConnectionUrl": "postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres/${PROJECT_NAME}_lighthouse?application_name=lighthouse-ci-server" } } } diff --git a/tests/test-metadata-api/.dockerignore b/tests/test-metadata-api/.dockerignore new file mode 100644 index 000000000..7501e1983 --- /dev/null +++ b/tests/test-metadata-api/.dockerignore @@ -0,0 +1,3 @@ +json_files +Dockerfile +README.md \ No newline at end of file diff --git a/tests/test-metadata-api/.gitignore b/tests/test-metadata-api/.gitignore new file mode 100644 index 000000000..995da051a --- /dev/null +++ b/tests/test-metadata-api/.gitignore @@ -0,0 +1,2 @@ +json_files +node_modules \ No newline at end of file diff --git a/tests/test-metadata-api/Dockerfile b/tests/test-metadata-api/Dockerfile new file mode 100644 index 000000000..b30a1fd6b --- /dev/null +++ b/tests/test-metadata-api/Dockerfile @@ -0,0 +1,9 @@ +FROM node:18-alpine +WORKDIR /src +COPY package.json yarn.lock ./ +RUN yarn install +COPY . . +VOLUME /data +ENV DATA_DIR=/data +EXPOSE 3000 +CMD [ "yarn", "start"] \ No newline at end of file diff --git a/tests/test-metadata-api/README.md b/tests/test-metadata-api/README.md new file mode 100644 index 000000000..3a98a748b --- /dev/null +++ b/tests/test-metadata-api/README.md @@ -0,0 +1,47 @@ +Test metadata API +================= + +Simple service to host json metadata during testing. + +## Installation + +``` +git clone https://github.com/your/repository.git +yarn install +yarn start +``` +#### Swagger UI + +``` +http://localhost:3000/docs +``` + +## Metadata Endpoints + +### 1. Save File + +- **Endpoint:** `PUT /data/{filename}` +- **Description:** Saves data to a file with the specified filename. + +### 2. Get File + +- **Endpoint:** `GET /data/{filename}` +- **Description:** Retrieves the content of the file with the specified filename. + +### 3. Delete File + +- **Endpoint:** `DELETE /data/{filename}` +- **Description:** Deletes the file with the specified filename. + +## Locks Endpoint +### 1. Acquire Lock +- **Endpoint:** `POST /lock/{key}?expiry={expiry_secs}` +- **Description:** Acquire a lock for the specified key for given time. By default the lock is set for 180 secs. +- **Responses:** + - `200 OK`: Lock acquired successfully. + - `423 Locked`: Lock not available. + +### 2. Release Lock + +- **Endpoint:** `POST/unlock/{key}` +- **Description:** Release a lock for the specified key. diff --git a/tests/test-metadata-api/index.js b/tests/test-metadata-api/index.js new file mode 100644 index 000000000..b689ac0f1 --- /dev/null +++ b/tests/test-metadata-api/index.js @@ -0,0 +1,151 @@ +const express = require('express'); +const fs = require('fs'); +const path = require('path'); +const lock_api = require('./locks_api') + +const swaggerUi = require('swagger-ui-express'); +const swaggerJsdoc = require('swagger-jsdoc'); +const app = express(); + + +const dataDir = process.env.DATA_DIR || path.join(__dirname, 'json_files'); + +if (!fs.existsSync(dataDir)) { + fs.mkdirSync(dataDir, { recursive: true }); +} +// Middleware to parse text request bodies +app.use(express.text()); + +// Swagger configuration +const swaggerOptions = { + definition: { + openapi: '3.0.0', + info: { + title: 'File API', + version: '1.0.0', + description: 'API for saving and deleting files', + }, + }, + apis: ['index.js','locks_api.js'], // Update the path to reflect the compiled JavaScript file +}; + +const swaggerSpec = swaggerJsdoc(swaggerOptions); + +// Serve Swagger UI +app.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec)); + +// PUT endpoint to save a file +/** + * @swagger + * /data/{filename}: + * put: + * summary: Save data to a file + * tags: [Metadata File] + * parameters: + * - in: path + * name: filename + * schema: + * type: string + * required: true + * description: The name of the file to save + * requestBody: + * required: true + * content: + * text/plain: + * schema: + * type: string + * responses: + * '201': + * description: File saved successfully + */ +app.put('/data/:filename', (req, res) => { + const filename = req.params.filename; + const filePath = path.join(dataDir, filename); + + fs.writeFile(filePath, req.body, (err) => { + if (err) { + console.error(err); + return res.status(500).send('Failed to save file'); + } + res.status(201).send({'success': true}); + }); +}); + + +// GET endpoint to retrieve a file +/** + * @swagger + * /data/{filename}: + * get: + * summary: Get a file + * tags: [Metadata File] + * parameters: + * - in: path + * name: filename + * schema: + * type: string + * required: true + * description: The name of the file to retrieve + * responses: + * '200': + * description: File retrieved successfully + * content: + * text/plain: + * schema: + * type: string + */ +app.get('/data/:filename', (req, res) => { + const filename = req.params.filename; + const filePath = path.join(dataDir, filename); + + fs.readFile(filePath, 'utf8', (err, data) => { + if (err) { + console.error(err); + return res.status(404).send({'message': 'File not found'}); + } + res.status(200).send(data); + }); +}); + + + +// DELETE endpoint to delete a file +/** + * @swagger + * /data/{filename}: + * delete: + * summary: Delete a file + * tags: [Metadata File] + * parameters: + * - in: path + * name: filename + * schema: + * type: string + * required: true + * description: The name of the file to delete + * responses: + * '200': + * description: File deleted successfully + */ +app.delete('/data/:filename', (req, res) => { + const filename = req.params.filename; + const filePath = path.join(dataDir, filename); + + fs.unlink(filePath, (err) => { + if (err) { + console.error(err); + return res.status(500).send({'message':'Failed to delete file'}); + } + res.send('File deleted successfully'); + }); +}); + +app.get('/', (req, res) => { + res.redirect('/docs'); +}); +lock_api.setup(app) +// Start the server +const PORT = process.env.PORT || 3000; +app.listen(PORT, () => { + console.log(`Server is running on port ${PORT}`); +}); diff --git a/tests/test-metadata-api/locks_api.js b/tests/test-metadata-api/locks_api.js new file mode 100644 index 000000000..5196a70ea --- /dev/null +++ b/tests/test-metadata-api/locks_api.js @@ -0,0 +1,109 @@ +const { v4: uuidv4 } = require('uuid'); +const lock = {}; + +function acquireLock(key, expiry_secs = 180) { + const now = Date.now(); + if (!lock[key] || lock[key].expiry < now) { + const uuid = uuidv4(); + lock[key] = { + locked: true, + expiry: now + expiry_secs * 1000, + uuid: uuid, + }; + return uuid + } +} +function releaseLock(key,uuid) { + if(uuid){ + _lock=lock[key] + if(_lock && (_lock.uuid != uuid)){ + // if the uuid doesn't match, the lock should + // have expired and obtained by process. + return; + } + } + delete lock[key]; +} + + +function setup(app) { + /** + * @swagger + * /lock/{key}: + * post: + * summary: Acquire lock + * tags: [Locks] + * parameters: + * - in: path + * name: key + * schema: + * type: string + * required: true + * description: The key of the lock to acquire + * - in: query + * name: expiry_secs + * schema: + * type: integer + * minimum: 1 + * default: 180 + * description: The expiration time of the lock in seconds (default is 180s) + * responses: + * '200': + * description: Lock acquired successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * uuid: + * type: string + * description: The UUID of the acquired lock + * '423': + * description: Lock not available + */ + app.post('/lock/:key', (req, res) => { + const key = req.params.key; + const expiry_secs = req.query.expiry_secs ? parseInt(req.query.expiry_secs) : 180; + const lock_uuid=acquireLock(key, expiry_secs) + if(lock_uuid){ + res.json({ uuid: lock_uuid }) + }else{ + res.status(423).json({ status: 423, message: 'Lock not available' }); + + } + }); + + /** + * @swagger + * /unlock/{key}: + * post: + * summary: Release lock + * tags: [Locks] + * parameters: + * - in: path + * name: key + * schema: + * type: string + * required: true + * description: The key of the lock to release + * - in: query + * name: uuid + * schema: + * type: string + * required: false + * description: The UUID of the lock to release + * responses: + * '200': + * description: Lock released successfully + */ + app.post('/unlock/:key', (req, res) => { + const key = req.params.key; + const uuid = req.query.uuid; + + releaseLock(key, uuid); + res.send('Lock released.'); + + }); +} + +module.exports.setup = setup; diff --git a/tests/test-metadata-api/package.json b/tests/test-metadata-api/package.json new file mode 100644 index 000000000..520a75019 --- /dev/null +++ b/tests/test-metadata-api/package.json @@ -0,0 +1,15 @@ +{ + "name": "test-metadata-api", + "version": "0.0.1", + "main": "index.js", + "license": "MIT", + "scripts": { + "start": "node index.js" + }, + "dependencies": { + "express": "^4.19.2", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.0", + "uuid": "^9.0.1" + } +} diff --git a/tests/test-metadata-api/yarn.lock b/tests/test-metadata-api/yarn.lock new file mode 100644 index 000000000..cf7a4ed22 --- /dev/null +++ b/tests/test-metadata-api/yarn.lock @@ -0,0 +1,683 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@apidevtools/json-schema-ref-parser@^9.0.6": + version "9.1.2" + resolved "https://registry.yarnpkg.com/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz#8ff5386b365d4c9faa7c8b566ff16a46a577d9b8" + integrity sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg== + dependencies: + "@jsdevtools/ono" "^7.1.3" + "@types/json-schema" "^7.0.6" + call-me-maybe "^1.0.1" + js-yaml "^4.1.0" + +"@apidevtools/openapi-schemas@^2.0.4": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz#9fa08017fb59d80538812f03fc7cac5992caaa17" + integrity sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ== + +"@apidevtools/swagger-methods@^3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz#b789a362e055b0340d04712eafe7027ddc1ac267" + integrity sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg== + +"@apidevtools/swagger-parser@10.0.3": + version "10.0.3" + resolved "https://registry.yarnpkg.com/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz#32057ae99487872c4dd96b314a1ab4b95d89eaf5" + integrity sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g== + dependencies: + "@apidevtools/json-schema-ref-parser" "^9.0.6" + "@apidevtools/openapi-schemas" "^2.0.4" + "@apidevtools/swagger-methods" "^3.0.2" + "@jsdevtools/ono" "^7.1.3" + call-me-maybe "^1.0.1" + z-schema "^5.0.1" + +"@jsdevtools/ono@^7.1.3": + version "7.1.3" + resolved "https://registry.yarnpkg.com/@jsdevtools/ono/-/ono-7.1.3.tgz#9df03bbd7c696a5c58885c34aa06da41c8543796" + integrity sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg== + +"@types/json-schema@^7.0.6": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + +accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +body-parser@1.20.2: + version "1.20.2" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd" + integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA== + dependencies: + bytes "3.1.2" + content-type "~1.0.5" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.11.0" + raw-body "2.5.2" + type-is "~1.6.18" + unpipe "1.0.0" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +call-bind@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" + +call-me-maybe@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.2.tgz#03f964f19522ba643b1b0693acb9152fe2074baa" + integrity sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ== + +commander@6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.0.tgz#b990bfb8ac030aedc6d11bc04d1488ffef56db75" + integrity sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q== + +commander@^10.0.0: + version "10.0.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" + integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@~1.0.4, content-type@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + +cookie@0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051" + integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +doctrine@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + get-intrinsic "^1.2.4" + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +express@^4.19.2: + version "4.19.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465" + integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.2" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.6.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.2.0" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.7" + qs "6.11.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.18.0" + serve-static "1.15.0" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +finalhandler@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" + integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +get-intrinsic@^1.1.3, get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + +glob@7.1.6: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + +has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + +has-proto@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" + integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== + +has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +hasown@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +lodash.get@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" + integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ== + +lodash.isequal@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" + integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ== + +lodash.mergewith@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55" + integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ== + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +minimatch@^3.0.4: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +ms@2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +object-inspect@^1.13.1: + version "1.13.1" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2" + integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ== + +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== + +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +qs@6.11.0: + version "6.11.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== + dependencies: + side-channel "^1.0.4" + +range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +safe-buffer@5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +send@0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" + integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + +serve-static@1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" + integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.18.0" + +set-function-length@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +side-channel@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" + integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +swagger-jsdoc@^6.2.8: + version "6.2.8" + resolved "https://registry.yarnpkg.com/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz#6d33d9fb07ff4a7c1564379c52c08989ec7d0256" + integrity sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ== + dependencies: + commander "6.2.0" + doctrine "3.0.0" + glob "7.1.6" + lodash.mergewith "^4.6.2" + swagger-parser "^10.0.3" + yaml "2.0.0-1" + +swagger-parser@^10.0.3: + version "10.0.3" + resolved "https://registry.yarnpkg.com/swagger-parser/-/swagger-parser-10.0.3.tgz#04cb01c18c3ac192b41161c77f81e79309135d03" + integrity sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg== + dependencies: + "@apidevtools/swagger-parser" "10.0.3" + +swagger-ui-dist@>=5.0.0: + version "5.17.2" + resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-5.17.2.tgz#de31813b18ff34e9a428cd6b9ede521164621996" + integrity sha512-V/NqUw6QoTrjSpctp2oLQvxrl3vW29UsUtZyq7B1CF0v870KOFbYGDQw8rpKaKm0JxTwHpWnW1SN9YuKZdiCyw== + +swagger-ui-express@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/swagger-ui-express/-/swagger-ui-express-5.0.0.tgz#7a00a18dd909574cb0d628574a299b9ba53d4d49" + integrity sha512-tsU9tODVvhyfkNSvf03E6FAk+z+5cU3lXAzMy6Pv4av2Gt2xA0++fogwC4qo19XuFf6hdxevPuVCSKFuMHJhFA== + dependencies: + swagger-ui-dist ">=5.0.0" + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +uuid@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" + integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== + +validator@^13.7.0: + version "13.11.0" + resolved "https://registry.yarnpkg.com/validator/-/validator-13.11.0.tgz#23ab3fd59290c61248364eabf4067f04955fbb1b" + integrity sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ== + +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +yaml@2.0.0-1: + version "2.0.0-1" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.0.0-1.tgz#8c3029b3ee2028306d5bcf396980623115ff8d18" + integrity sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ== + +z-schema@^5.0.1: + version "5.0.6" + resolved "https://registry.yarnpkg.com/z-schema/-/z-schema-5.0.6.tgz#46d6a687b15e4a4369e18d6cb1c7b8618fc256c5" + integrity sha512-+XR1GhnWklYdfr8YaZv/iu+vY+ux7V5DS5zH1DQf6bO5ufrt/5cgNhVO5qyhsjFXvsqQb/f08DWE9b6uPscyAg== + dependencies: + lodash.get "^4.4.2" + lodash.isequal "^4.5.0" + validator "^13.7.0" + optionalDependencies: + commander "^10.0.0"