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"