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..213d2fdf8 100644 --- a/.github/workflows/lighthouse.yml +++ b/.github/workflows/lighthouse.yml @@ -1,47 +1,31 @@ name: Lighthouse on: - push: - paths: - - govtool/frontend/** - - .github/workflows/lighthouse.yml + workflow_run: + workflows: + - Build and deploy GovTool test stack + types: + - completed 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 +34,4 @@ 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=https://lighthouse-govtool.cardanoapi.io --ignoreDuplicateBuildFailure diff --git a/.github/workflows/test_integration_playwright.yml b/.github/workflows/test_integration_playwright.yml index f9e10d27b..80f0cb971 100644 --- a/.github/workflows/test_integration_playwright.yml +++ b/.github/workflows/test_integration_playwright.yml @@ -5,7 +5,7 @@ on: paths: - .github/workflows/test_integration_playwright.yml workflow_run: - workflows: ["Build and deploy GovTool to TEST server"] + workflows: ["Build and deploy GovTool test stack"] types: [completed] jobs: 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/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..4198a8204 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.10.0-pre 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-2-1 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..dbf682933 100644 --- a/tests/govtool-frontend/playwright/.gitignore +++ b/tests/govtool-frontend/playwright/.gitignore @@ -13,4 +13,4 @@ allure-report/ .secrets .vars .lock-pool/ -.logs/ +.logs/ \ 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/lib/constants/environments.ts b/tests/govtool-frontend/playwright/lib/constants/environments.ts index ccdfd8c14..85aa51f79 100644 --- a/tests/govtool-frontend/playwright/lib/constants/environments.ts +++ b/tests/govtool-frontend/playwright/lib/constants/environments.ts @@ -3,7 +3,6 @@ const environments = { apiUrl: `${process.env.HOST_URL}/api` || "http://localhost:8080/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,8 +10,7 @@ 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, diff --git a/tests/govtool-frontend/playwright/lib/constants/staticWallets.ts b/tests/govtool-frontend/playwright/lib/constants/staticWallets.ts index 21a0289e6..bcad0a2e5 100644 --- a/tests/govtool-frontend/playwright/lib/constants/staticWallets.ts +++ b/tests/govtool-frontend/playwright/lib/constants/staticWallets.ts @@ -32,21 +32,21 @@ export const dRep01Wallet: StaticWallet = { dRepId: "drep1g654cyehkfenyycl8sdemrnk38ka9avnulnfhawu7rp8skl824l", }; -// 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 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: { diff --git a/tests/govtool-frontend/playwright/lib/datafactory/createAuth.ts b/tests/govtool-frontend/playwright/lib/datafactory/createAuth.ts index 05da4e0e5..2525ed5dc 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()); 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/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..a749d26c8 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}`); 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}`); 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)); 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..5ff069f41 --- /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( + "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(); + 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/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..5a18dc0a2 100644 --- a/tests/govtool-frontend/playwright/lib/types.ts +++ b/tests/govtool-frontend/playwright/lib/types.ts @@ -51,6 +51,7 @@ export type IDRepInfo = { bio?: string; extraContentLinks?: string[]; }; + export enum FilterOption { ProtocolParameterChange = "ParameterChange", InfoAction = "InfoAction", @@ -60,3 +61,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/playwright.config.ts b/tests/govtool-frontend/playwright/playwright.config.ts index 43a5cc701..8f476bd83 100644 --- a/tests/govtool-frontend/playwright/playwright.config.ts +++ b/tests/govtool-frontend/playwright/playwright.config.ts @@ -27,7 +27,14 @@ export default defineConfig({ /*use Allure Playwright's testPlanFilter() to determine the grep parameter*/ grep: testPlanFilter(), /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: process.env.CI ? [["line"], ["allure-playwright"]] : [["line"]], + reporter: process.env.CI + ? [ + ["line"], + [ + "allure-playwright" + ], + ] + : [["line"]], /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. */ @@ -89,7 +96,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", }, { @@ -105,9 +114,9 @@ export default defineConfig({ name: "independent (mobile)", use: { ...devices["Pixel 5"] }, testIgnore: [ - "**/*.tx.spec.ts", "**/*.loggedin.spec.ts", "**/*.dRep.spec.ts", + "**/*.delegation.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..a37e9ee5d --- /dev/null +++ b/tests/govtool-frontend/playwright/tests/2-delegation/delegation.drep.spec.ts @@ -0,0 +1,104 @@ +import environments from "@constants/environments"; +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 { 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 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(); +}); 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..dc9a069a0 --- /dev/null +++ b/tests/govtool-frontend/playwright/tests/2-delegation/delegationFunctionality.delegation.spec.ts @@ -0,0 +1,106 @@ +import environments from "@constants/environments"; +import { adaHolder01Wallet, dRep01Wallet } 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 { + registerStakeForWallet, + transferAdaForWallet, + waitForTxConfirmation, +} from "@helpers/transaction"; +import DRepDirectoryPage from "@pages/dRepDirectoryPage"; +import { expect } from "@playwright/test"; + +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); + + // Verify dRepId in dashboard + await page.goto("/dashboard"); + await expect(page.getByText(dRepId)).toBeVisible(); + }); + + test("2F. Should change delegated dRep", async ({ page }, testInfo) => { + test.setTimeout(testInfo.timeout + 2 * environments.txTimeOut); + + const dRepId = "drep1qzw234c0ly8csamxf8hrhfahvzwpllh2ckuzzvl38d22wwxxquu"; + + 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", () => { + test("2E. Should register as Sole voter", async ({ + page, + browser, + }, testInfo) => { + test.setTimeout(testInfo.timeout + 2 * environments.txTimeOut); + + const wallet = await ShelleyWallet.generate(); + const dRepId = extractDRepFromWallet(wallet); + + await transferAdaForWallet(wallet, 600); + await registerStakeForWallet(wallet); + + 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("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 + ); + }); +}); 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..a82e4687d 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,28 +3,34 @@ 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("4E. Should display DRep's voting power in governance actions page", async ({ - page, - }) => { - const votingPowerPromise = page.waitForResponse("**/get-voting-power/**"); - const governanceActionsPage = new GovernanceActionsPage(page); - await governanceActionsPage.goto(); +test.beforeEach(async () => { + await setAllureEpic("4. Proposal visibility"); +}); - const res = await votingPowerPromise; - const votingPower = await res.json(); +test("4E. Should display DRep's voting power in governance actions page", async ({ + page, +}) => { + const votingPowerPromise = page.waitForResponse("**/get-voting-power/**"); + const governanceActionsPage = new GovernanceActionsPage(page); + await governanceActionsPage.goto(); + + 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/auth.setup.ts b/tests/govtool-frontend/playwright/tests/auth.setup.ts index d2206b142..34ed65e2a 100644 --- a/tests/govtool-frontend/playwright/tests/auth.setup.ts +++ b/tests/govtool-frontend/playwright/tests/auth.setup.ts @@ -2,17 +2,25 @@ import { adaHolder01Wallet, + adaHolder02Wallet, dRep01Wallet, user01Wallet, } from "@constants/staticWallets"; import { importWallet } from "@fixtures/importWallet"; import { test as setup } from "@fixtures/walletExtension"; +import { setAllureStory, setAllureEpic } from "@helpers/allure"; import LoginPage from "@pages/loginPage"; const dRep01AuthFile = ".auth/dRep01.json"; const adaHolder01AuthFile = ".auth/adaHolder01.json"; +const adaHolder02AuthFile = ".auth/adaHolder02.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 +50,13 @@ 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 }); +}); diff --git a/tests/govtool-frontend/playwright/tests/dRep.setup.ts b/tests/govtool-frontend/playwright/tests/dRep.setup.ts index 5203f1e47..38c587663 100644 --- a/tests/govtool-frontend/playwright/tests/dRep.setup.ts +++ b/tests/govtool-frontend/playwright/tests/dRep.setup.ts @@ -15,7 +15,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); 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..52cdce8c9 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 { setAllureStory, setAllureEpic } from "@helpers/allure"; import { pollTransaction } from "@helpers/transaction"; import { test as setup } from "@playwright/test"; import { loadAmountFromFaucet } from "@services/faucetService"; @@ -7,6 +8,11 @@ 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; 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"