diff --git a/.github/workflows/build-and-deploy-beta.yml b/.github/workflows/build-and-deploy-beta.yml index cb2090349..6af69324b 100644 --- a/.github/workflows/build-and-deploy-beta.yml +++ b/.github/workflows/build-and-deploy-beta.yml @@ -13,11 +13,20 @@ on: options: - "enabled" - "disabled" + forceRebuildDockerImages: + description: "Force rebuild the docker images" + required: false + type: choice + default: "false" + options: + - "true" + - "false" env: ENVIRONMENT: "beta" CARDANO_NETWORK: "sanchonet" DOMAIN: "sanchogov.tools" + FORCE_REBUILD: ${{inputs.forceRebuildDockerImages == 'true'}} jobs: deploy: diff --git a/.github/workflows/build-and-deploy-dev.yml b/.github/workflows/build-and-deploy-dev.yml index c07d88741..7755dc10f 100644 --- a/.github/workflows/build-and-deploy-dev.yml +++ b/.github/workflows/build-and-deploy-dev.yml @@ -12,11 +12,20 @@ on: options: - "enabled" - "disabled" + forceRebuildDockerImages: + description: "Force rebuild the docker images" + required: false + type: choice + default: "false" + options: + - "true" + - "false" env: ENVIRONMENT: "dev" CARDANO_NETWORK: "sanchonet" DOMAIN: "dev-sanchonet.govtool.byron.network" + FORCE_REBUILD: ${{inputs.forceRebuildDockerImages == 'true'}} jobs: deploy: diff --git a/.github/workflows/build-and-deploy-staging.yml b/.github/workflows/build-and-deploy-staging.yml index c841a1a15..7534040d3 100644 --- a/.github/workflows/build-and-deploy-staging.yml +++ b/.github/workflows/build-and-deploy-staging.yml @@ -15,11 +15,20 @@ on: options: - "enabled" - "disabled" + forceRebuildDockerImages: + description: "Force rebuild the docker images" + required: false + type: choice + default: "false" + options: + - "true" + - "false" env: ENVIRONMENT: "staging" CARDANO_NETWORK: "sanchonet" DOMAIN: "staging.govtool.byron.network" + FORCE_REBUILD: ${{inputs.forceRebuildDockerImages == 'true'}} jobs: deploy: diff --git a/.github/workflows/build-and-deploy-test-stack.yml b/.github/workflows/build-and-deploy-test-stack.yml index 76a40607f..bd59c79c7 100644 --- a/.github/workflows/build-and-deploy-test-stack.yml +++ b/.github/workflows/build-and-deploy-test-stack.yml @@ -22,9 +22,10 @@ jobs: SENTRY_DSN_BACKEND: ${{ secrets.SENTRY_DSN_BACKEND }} GTM_ID: ${{ secrets.GTM_ID }} NPMRC_TOKEN: ${{ secrets.NPMRC_TOKEN }} - SENTRY_DSN_FRONTEND: ${{ secrets.INTERSECT_SENTRY_DSN_FRONTEND }} + SENTRY_DSN_FRONTEND: ${{ 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 }} + APP_ENV: test steps: - name: Checkout code uses: actions/checkout@v4 diff --git a/.github/workflows/build-and-deploy-test.yml b/.github/workflows/build-and-deploy-test.yml index 45fd36015..b2c99fd83 100644 --- a/.github/workflows/build-and-deploy-test.yml +++ b/.github/workflows/build-and-deploy-test.yml @@ -15,11 +15,20 @@ on: options: - "enabled" - "disabled" + forceRebuildDockerImages: + description: "Force rebuild the docker images" + required: false + type: choice + default: "false" + options: + - "true" + - "false" env: ENVIRONMENT: "test" CARDANO_NETWORK: "sanchonet" DOMAIN: "test-sanchonet.govtool.byron.network" + FORCE_REBUILD: ${{inputs.forceRebuildDockerImages == 'true'}} jobs: deploy: diff --git a/.github/workflows/frontend_sonar_scan.yml b/.github/workflows/frontend_sonar_scan.yml index 09b8f994f..5a78a37be 100644 --- a/.github/workflows/frontend_sonar_scan.yml +++ b/.github/workflows/frontend_sonar_scan.yml @@ -26,27 +26,19 @@ jobs: uses: actions/setup-node@v4 with: node-version-file: "govtool/frontend/.nvmrc" - + registry-url: "https://registry.npmjs.org/" + scope: "@intersect.mbo" - name: 🧪 Test working-directory: govtool/frontend env: - NODE_OPTIONS: "--max_old_space_size=4096" + NODE_OPTIONS: "--max_old_space_size=6144" + NODE_AUTH_TOKEN: ${{ secrets.NPMRC_TOKEN }} run: | - npm install + npm ci npm run test:coverage -# Running with docker -# -# - name: Run SonarQube Scanner -# run: | -# docker run --rm \ -# -e SONAR_HOST_URL="https://sonarcloud.io" \ -# -e SONAR_TOKEN="ec4183646e59dd70c8077acfabe52062ccbea7a9" \ -# -v "$(pwd):/usr/src" \ -# --workdir=/usr/src/govtool/frontend \ -# sonarsource/sonar-scanner-cli:5.0.1 - - uses: sonarsource/sonarqube-scan-action@master + if: always() with: projectBaseDir: govtool/frontend env: diff --git a/govtool/frontend/storybook.Dockerfile b/govtool/frontend/storybook.Dockerfile new file mode 100644 index 000000000..599f88e16 --- /dev/null +++ b/govtool/frontend/storybook.Dockerfile @@ -0,0 +1,28 @@ +FROM node:18-alpine as deps +ARG NPMRC_TOKEN + +WORKDIR /src + +# Set npm configuration settings using environment variables +RUN npm config set @intersect.mbo:registry "https://registry.npmjs.org/" --location=global \ + && npm config set //registry.npmjs.org/:_authToken ${NPMRC_TOKEN} --location=global + +COPY package.json package-lock.json ./ +RUN npm install + +FROM node:18-alpine as builder +ARG NPMRC_TOKEN +ENV NODE_OPTIONS=--max_old_space_size=8192 +WORKDIR /src + +COPY --from=deps /src/node_modules ./node_modules +COPY . . + +RUN npm run build-storybook --quiet + +FROM nginx:stable-alpine +EXPOSE 80 + +COPY --from=builder /src/storybook-static /usr/share/nginx/html + +CMD ["nginx", "-g", "daemon off;"] diff --git a/scripts/govtool/common.mk b/scripts/govtool/common.mk index 7e801fed4..77cd61747 100644 --- a/scripts/govtool/common.mk +++ b/scripts/govtool/common.mk @@ -34,10 +34,15 @@ check_defined = \ __check_defined = \ $(if $(value $1),, \ $(error Undefined $1$(if $2, ($2)))) - + +force_rebuild := $(shell echo $${FORCE_REBUILD:-false}) # helper function for checking if image exists on ECR check_image_on_ecr = \ - $(docker) manifest inspect "$(repo_url)/$1:$2" > /dev/null 2>&1 + if [ "$(force_rebuild)" = "true" ]; then \ + false; \ + else \ + $(docker) manifest inspect "$(repo_url)/$1:$2" > /dev/null 2>&1; \ + fi .PHONY: check-env-defined check-env-defined: diff --git a/tests/govtool-frontend/playwright/.env.example b/tests/govtool-frontend/playwright/.env.example index f86654032..d443e1d96 100644 --- a/tests/govtool-frontend/playwright/.env.example +++ b/tests/govtool-frontend/playwright/.env.example @@ -3,6 +3,9 @@ API_URL=http://localhost:3000/api DOCS_URL=https://docs.sanchogov.tools + +PDF_URL=https://dev.api.pdf.gov.tools + # 0 for testnet, 1 for mainnet NETWORK_ID=0 diff --git a/tests/govtool-frontend/playwright/.gitignore b/tests/govtool-frontend/playwright/.gitignore index 28723c962..38697b044 100644 --- a/tests/govtool-frontend/playwright/.gitignore +++ b/tests/govtool-frontend/playwright/.gitignore @@ -16,4 +16,5 @@ allure-report/ lib/_mock/registerDRepWallets.json lib/_mock/registeredDRepWallets.json lib/_mock/wallets.json +lib/_mock/proposals.json ./lock_logs.txt diff --git a/tests/govtool-frontend/playwright/lib/_mock/proposal.json b/tests/govtool-frontend/playwright/lib/_mock/proposal.json new file mode 100644 index 000000000..be8ef6380 --- /dev/null +++ b/tests/govtool-frontend/playwright/lib/_mock/proposal.json @@ -0,0 +1,44 @@ +{ + "data": { + "id": 128, + "attributes": { + "prop_likes": 0, + "prop_dislikes": 0, + "prop_comments_number": 3, + "prop_submited": false, + "prop_status_id": null, + "user_id": "52", + "createdAt": "2024-06-14T08:28:18.000Z", + "updatedAt": "2024-06-14T08:28:18.000Z", + "content": { + "id": 121, + "attributes": { + "proposal_id": "128", + "prop_rev_active": true, + "prop_abstract": "Calamitas suppono coniuratio aiunt pecto uberrime deleniti tepidus acerbitas. Nihil vitium conservo abeo tametsi odit creator basium.", + "prop_motivation": "Demonstro apparatus torrens patrocinor. Concedo campana possimus agnosco tutamen astrum conventus defendo sublime.", + "prop_rationale": "Brevis suppellex coadunatio vis. Alii terreo carbo sono utilis vicissitudo.", + "gov_action_type_id": "1", + "prop_name": "Labadie, Stehr and Rosenbaum", + "prop_receiving_address": "addr_test1qqqqqqqqqqa4kpmh", + "prop_amount": 402, + "createdAt": "2024-06-14T08:28:18.012Z", + "updatedAt": "2024-06-14T08:28:18.012Z", + "is_draft": false, + "user_id": "52", + "proposal_links": [], + "gov_action_type": { + "id": 1, + "attributes": { + "gov_action_type_name": "Info", + "createdAt": "2024-05-27T15:06:15.640Z", + "updatedAt": "2024-05-27T15:06:15.640Z" + } + } + } + }, + "user_govtool_username": "Jett.Hagenes21" + } + }, + "meta": {} +} diff --git a/tests/govtool-frontend/playwright/lib/_mock/proposalComments.json b/tests/govtool-frontend/playwright/lib/_mock/proposalComments.json new file mode 100644 index 000000000..12f0415e2 --- /dev/null +++ b/tests/govtool-frontend/playwright/lib/_mock/proposalComments.json @@ -0,0 +1,51 @@ +{ + "data": [ + { + "id": 139, + "attributes": { + "proposal_id": "128", + "comment_parent_id": null, + "user_id": "20", + "comment_text": "Hello", + "createdAt": "2024-06-14T13:38:35.830Z", + "updatedAt": "2024-06-14T13:38:35.830Z", + "user_govtool_username": "Anonymous", + "subcommens_number": 0 + } + }, + { + "id": 138, + "attributes": { + "proposal_id": "128", + "comment_parent_id": null, + "user_id": "20", + "comment_text": "Nice proposal", + "createdAt": "2024-06-14T13:38:31.279Z", + "updatedAt": "2024-06-14T13:38:31.279Z", + "user_govtool_username": "Anonymous", + "subcommens_number": 0 + } + }, + { + "id": 137, + "attributes": { + "proposal_id": "128", + "comment_parent_id": null, + "user_id": "20", + "comment_text": "Go Ahead", + "createdAt": "2024-06-14T13:38:27.286Z", + "updatedAt": "2024-06-14T13:38:27.286Z", + "user_govtool_username": "Anonymous", + "subcommens_number": 0 + } + } + ], + "meta": { + "pagination": { + "page": 1, + "pageSize": 25, + "pageCount": 1, + "total": 3 + } + } +} diff --git a/tests/govtool-frontend/playwright/lib/_mock/proposalPoll.json b/tests/govtool-frontend/playwright/lib/_mock/proposalPoll.json new file mode 100644 index 000000000..87c486958 --- /dev/null +++ b/tests/govtool-frontend/playwright/lib/_mock/proposalPoll.json @@ -0,0 +1,24 @@ +{ + "data": [ + { + "id": 39, + "attributes": { + "proposal_id": "128", + "poll_yes": 0, + "poll_no": 0, + "poll_start_dt": "2024-06-14T08:26:34.400Z", + "is_poll_active": true, + "createdAt": "2024-06-14T08:28:20.210Z", + "updatedAt": "2024-06-14T08:28:20.210Z" + } + } + ], + "meta": { + "pagination": { + "page": 1, + "pageSize": 1, + "pageCount": 1, + "total": 1 + } + } +} diff --git a/tests/govtool-frontend/playwright/lib/constants/environments.ts b/tests/govtool-frontend/playwright/lib/constants/environments.ts index 460bd382a..5c0071691 100644 --- a/tests/govtool-frontend/playwright/lib/constants/environments.ts +++ b/tests/govtool-frontend/playwright/lib/constants/environments.ts @@ -10,6 +10,7 @@ const environments = { frontendUrl: SERVER_HOST_URL, apiUrl: `${SERVER_HOST_URL}/api`, docsUrl: process.env.DOCS_URL || "https://docs.sanchogov.tools", + pdfUrl: process.env.PDF_URL || "https://dev.api.pdf.gov.tools", networkId: parseInt(process.env.NETWORK_ID) || 0, faucet: { apiUrl: diff --git a/tests/govtool-frontend/playwright/lib/constants/staticWallets.ts b/tests/govtool-frontend/playwright/lib/constants/staticWallets.ts index f30c090d3..38e6dfa8a 100644 --- a/tests/govtool-frontend/playwright/lib/constants/staticWallets.ts +++ b/tests/govtool-frontend/playwright/lib/constants/staticWallets.ts @@ -16,6 +16,9 @@ export const adaHolder06Wallet = staticWallets[9]; // Does not takes part in transaction export const user01Wallet: StaticWallet = staticWallets[5]; +// Username is already set +export const proposal01Wallet: StaticWallet = staticWallets[10]; + export const adaHolderWallets = [ adaHolder01Wallet, adaHolder02Wallet, @@ -28,3 +31,5 @@ export const adaHolderWallets = [ export const userWallets = [user01Wallet]; export const dRepWallets = [dRep01Wallet, dRep02Wallet]; + +export const proposalWallets = [proposal01Wallet]; diff --git a/tests/govtool-frontend/playwright/lib/fixtures/proposal.ts b/tests/govtool-frontend/playwright/lib/fixtures/proposal.ts new file mode 100644 index 000000000..686629f9a --- /dev/null +++ b/tests/govtool-frontend/playwright/lib/fixtures/proposal.ts @@ -0,0 +1,37 @@ +import { proposal01Wallet } from "@constants/staticWallets"; +import { test as base } from "@fixtures/walletExtension"; +import { createNewPageWithWallet } from "@helpers/page"; +import ProposalDiscussionDetailsPage from "@pages/proposalDiscussionDetailsPage"; +import ProposalDiscussionPage from "@pages/proposalDiscussionPage"; + +type TestOptions = { + proposalId: number; + pollEnabled: boolean; +}; + +export const test = base.extend({ + pollEnabled: [false, { option: true }], + + proposalId: async ({ page, browser, pollEnabled }, use) => { + // setup + const proposalPage = await createNewPageWithWallet(browser, { + storageState: ".auth/proposal01.json", + wallet: proposal01Wallet, + }); + + const proposalDiscussionPage = new ProposalDiscussionPage(proposalPage); + await proposalDiscussionPage.goto(); + const proposalId = await proposalDiscussionPage.createProposal(); + const proposalDetailsPage = new ProposalDiscussionDetailsPage(proposalPage); + + if (pollEnabled) { + await proposalDetailsPage.addPollBtn.click(); + } + + await use(proposalId); + + // cleanup + await proposalDetailsPage.goto(proposalId); + await proposalDetailsPage.deleteProposal(); + }, +}); diff --git a/tests/govtool-frontend/playwright/lib/helpers/cardano.ts b/tests/govtool-frontend/playwright/lib/helpers/cardano.ts index 3718d13a3..42eb3bd4e 100644 --- a/tests/govtool-frontend/playwright/lib/helpers/cardano.ts +++ b/tests/govtool-frontend/playwright/lib/helpers/cardano.ts @@ -1,5 +1,12 @@ +import { bech32 } from "bech32"; + export function lovelaceToAda(lovelace: number) { if (lovelace === 0) return 0; return lovelace / 1e6; } + +export function generateWalletAddress() { + const randomBytes = new Uint8Array(10); + return bech32.encode("addr_test", randomBytes); +} diff --git a/tests/govtool-frontend/playwright/lib/helpers/file.ts b/tests/govtool-frontend/playwright/lib/helpers/file.ts new file mode 100644 index 000000000..a803ab288 --- /dev/null +++ b/tests/govtool-frontend/playwright/lib/helpers/file.ts @@ -0,0 +1,20 @@ +import { writeFile } from "fs"; +const path = require("path"); + +const baseFilePath = path.resolve(__dirname, "../_mock"); + +export async function createFile(fileName: string, data?: any) { + await new Promise((resolve, reject) => + writeFile( + `${baseFilePath}/${fileName}`, + JSON.stringify(data, null, 2), + (err) => { + if (err) { + reject(err); + } else { + resolve(); + } + } + ) + ); +} diff --git a/tests/govtool-frontend/playwright/lib/helpers/string.ts b/tests/govtool-frontend/playwright/lib/helpers/string.ts new file mode 100644 index 000000000..68c20208e --- /dev/null +++ b/tests/govtool-frontend/playwright/lib/helpers/string.ts @@ -0,0 +1,3 @@ +export function extractProposalIdFromUrl(url: string) { + return parseInt(url.split("/").pop()); +} diff --git a/tests/govtool-frontend/playwright/lib/pages/loginPage.ts b/tests/govtool-frontend/playwright/lib/pages/loginPage.ts index 6ebbc3dba..a4a4c4457 100644 --- a/tests/govtool-frontend/playwright/lib/pages/loginPage.ts +++ b/tests/govtool-frontend/playwright/lib/pages/loginPage.ts @@ -25,8 +25,24 @@ export default class LoginPage { await this.connectWalletBtn.click(); await this.demosWalletBtn.click({ force: true }); + + /** + * TODO: Remove this + * This has been set to tackle dashboard white screen issue on initial login + */ + await this.page.reload(); + /** + * TODO: Uncomment this + * Accept sanchonet info modal is not showing for now + */ await this.acceptSanchoNetInfoBtn.click({ force: true }); + /** + * TODO: Remove this + * This has been set to tackle dashboard white screen issue on initial login + */ + await this.page.reload(); + const { stakeKeys, rewardAddresses } = await this.page.evaluate( async () => { const walletInstance: CIP30Instance | Cip95Instance = diff --git a/tests/govtool-frontend/playwright/lib/pages/proposalDiscussionDetailsPage.ts b/tests/govtool-frontend/playwright/lib/pages/proposalDiscussionDetailsPage.ts new file mode 100644 index 000000000..cedee9c55 --- /dev/null +++ b/tests/govtool-frontend/playwright/lib/pages/proposalDiscussionDetailsPage.ts @@ -0,0 +1,121 @@ +import environments from "@constants/environments"; +import { Page, expect } from "@playwright/test"; +import { CommentResponse } from "@types"; + +export default class ProposalDiscussionDetailsPage { + // Buttons + readonly likeBtn = this.page.getByRole("button", { + name: "proposal likes", + }); + readonly dislikeBtn = this.page.getByRole("button", { + name: "proposal dislikes", + }); + readonly commentBtn = this.page.getByRole("button", { + name: "Comment", + exact: true, + }); // this.page.getByTestId("comment-button"); + readonly addPollBtn = this.page.getByRole("button", { name: "Add Poll" }); // BUG missing test id + readonly SubmitBtn = this.page.getByTestId("submit-button"); + readonly menuBtn = this.page.getByTestId("menu-button"); + readonly editProposalBtn = this.page.getByTestId("edit-proposal"); + readonly deleteProposalBtn = this.page.getByTestId("delete-proposal"); + readonly reviewVersionsBtn = this.page.getByTestId("review-versions"); + readonly closePollBtn = this.page.getByRole("button", { name: "Close Poll" }); // BUG missing test id + readonly sortBtn = this.page + .locator("div") + .filter({ hasText: /^Comments$/ }) + .getByRole("button"); // this.page.getByTestId("sort-button"); + readonly proposeGovernanceAction = this.page.getByTestId("propose-GA-button"); + readonly replyBtn = this.page.getByTestId("reply-button"); + readonly pollYesBtn = this.page.getByRole("button", { name: "Yes" }); //BUG missing test id + readonly pollNoBtn = this.page.getByRole("button", { name: "No" }); //BUG missing test id + readonly showReplyBtn = this.page.getByTestId("show-more-reply"); + readonly closePollYesBtn = this.page.getByRole("button", { + name: "Yes, close Poll", + }); // BUG missing test id + readonly changeVoteBtn = this.page.getByRole("button", { + name: "Change Vote", + }); + + // Indicators + readonly likesCounts = this.page.getByTestId("likes-count"); + readonly dislikesCounts = this.page.getByTestId("dislikse-count"); + readonly commentsCount = this.page.getByTestId("comments-count"); + + // Cards + readonly pollVoteCard = this.page.getByTestId("poll-vote-card"); + readonly pollResultCard = this.page.getByTestId("poll-result-card"); + readonly commentCard = + this.proposeGovernanceAction.getByTestId("comment-card"); + + //inputs + readonly commentInput = this.page.getByRole("textbox"); + + constructor(private readonly page: Page) {} + + async goto(proposalId: number) { + await this.page.goto( + `${environments.frontendUrl}/connected/proposal_pillar/proposal_discussion/${proposalId}` + ); + } + + async closeUsernamePrompt() { + await this.page + .locator("div") + .filter({ hasText: /^Hey, setup your username$/ }) + .getByRole("button") + .click(); + } + + async addComment(comment: string) { + await this.commentInput.fill(comment); + await this.page + .getByRole("button", { name: "Comment", exact: true }) + .click(); + } + + async replyComment(reply: string) { + await this.page.getByRole("button", { name: "Reply" }).click(); + await this.page.getByPlaceholder("Add comment").fill(reply); + await this.page.getByRole("button", { name: "Comment" }).nth(2).click(); + } + + async sortAndValidate( + order: string, + validationFn: (date1: string, date2: string) => boolean + ) { + const responsePromise = this.page.waitForResponse((response) => + response.url().includes(`&sort[createdAt]=${order}`) + ); + + await this.sortBtn.click(); + const response = await responsePromise; + + const comments: CommentResponse[] = (await response.json()).data; + + // API validation + for (let i = 0; i < comments.length - 1; i++) { + const isValid = validationFn( + comments[i].attributes.updatedAt, + comments[i + 1].attributes.updatedAt + ); + expect(isValid).toBe(true); + } + } + + async voteOnPoll(vote: string) { + await this.page.getByRole("button", { name: `${vote}` }).click(); + } + + async deleteProposal() { + await this.page.waitForTimeout(2_000); + + await this.page.locator("#menu-button").click(); + await this.page.getByRole("menuitem", { name: "Delete Proposal" }).click(); + + // confirm deletion + await this.page + .getByRole("button", { name: "Yes, delete my proposal" }) + .click(); + } +} diff --git a/tests/govtool-frontend/playwright/lib/pages/proposalDiscussionPage.ts b/tests/govtool-frontend/playwright/lib/pages/proposalDiscussionPage.ts new file mode 100644 index 000000000..5c1b5dd5f --- /dev/null +++ b/tests/govtool-frontend/playwright/lib/pages/proposalDiscussionPage.ts @@ -0,0 +1,116 @@ +import { faker } from "@faker-js/faker"; +import { generateWalletAddress } from "@helpers/cardano"; +import { extractProposalIdFromUrl } from "@helpers/string"; +import { Page } from "@playwright/test"; +import { ProposalCreateRequest } from "@services/proposalDiscussion/types"; +import environments from "lib/constants/environments"; +import ProposalDiscussionDetailsPage from "./proposalDiscussionDetailsPage"; + +export default class ProposalDiscussionPage { + // Buttons + readonly proposalCreateBtn = this.page.getByRole("button", { + name: "Propose a Governance Action", + }); + readonly continueBtn = this.page.getByRole("button", { name: "Continue" }); // #BUG test-id missing + readonly filterBtn = this.page.locator("#filters-button"); // this.page.getByTestId("filters-button"); + readonly shareBtn = this.page + .locator(".MuiCardHeader-action > .MuiButtonBase-root") + .first(); //this.page.getByTestId("share-button"); + readonly sortBtn = this.page.locator("button:nth-child(2)").first(); //this.page.getByTestId("sort-button"); + readonly searchInput = this.page.getByPlaceholder("Search..."); // this.page.getByTestId("search-input"); + readonly showAllBtn = this.page + .getByRole("button", { name: "Show all" }) + .first(); //this.page.getByTestId("show-all-button"); + readonly showLessBtn = this.page.getByRole("button", { name: "Show less" }); + readonly infoRadio = this.page.getByLabel("Info"); + readonly treasuryRadio = this.page.getByLabel("Treasury"); + + constructor(private readonly page: Page) {} + + async goto() { + await this.page.goto( + `${environments.frontendUrl}/connected/proposal_pillar/proposal_discussion` + ); + await this.page.waitForTimeout(2_000); + } + + async closeUsernamePrompt() { + await this.page + .locator("div") + .filter({ hasText: /^Hey, setup your username$/ }) + .getByRole("button") + .click(); + } + + async viewFirstProposal(): Promise { + await this.page + .locator('[data-testid^="govaction-"][data-testid$="-view-detail"]') + .first() + .click(); + return new ProposalDiscussionDetailsPage(this.page); + } + + async getAllProposals() { + await this.page.waitForTimeout(2_000); + return this.page.locator('[data-testid$="-card"]').all(); // BUG + } + + async setUsername(name: string) { + await this.page.getByLabel("Username *").fill(name); + + const proceedBtn = this.page.getByRole("button", { + name: "Proceed with this username", + }); + await proceedBtn.click(); + await proceedBtn.click(); + + await this.page.getByRole("button", { name: "Close" }).click(); + } + + async createProposal(): Promise { + const receivingAddr = generateWalletAddress(); + const proposalRequest: ProposalCreateRequest = { + proposal_links: [ + { + prop_link: faker.internet.url(), + prop_link_text: faker.internet.displayName(), + }, + ], + gov_action_type_id: 1, + prop_name: faker.company.name(), + prop_abstract: faker.lorem.paragraph(2), + prop_motivation: faker.lorem.paragraph(2), + prop_rationale: faker.lorem.paragraph(2), + prop_receiving_address: receivingAddr, + prop_amount: faker.number.int({ min: 100, max: 1000 }).toString(), + is_draft: false, + }; + + await this.proposalCreateBtn.click(); + await this.continueBtn.click(); + + await this.fillForm(proposalRequest); + await this.continueBtn.click(); + await this.page.getByRole("button", { name: "Submit" }).click(); + + // Wait for redirection to `proposal-discussion-details` page + await this.page.waitForTimeout(2_000); + + const currentPageUrl = this.page.url(); + return extractProposalIdFromUrl(currentPageUrl); + } + + private async fillForm(data: ProposalCreateRequest) { + await this.page.getByLabel("Governance Action Type *").click(); + await this.page.getByRole("option", { name: "Info" }).click(); + await this.page.getByLabel("Title *").fill(data.prop_name); + await this.page.getByPlaceholder("Summary...").fill(data.prop_abstract); + await this.page.getByLabel("Motivation *").fill(data.prop_motivation); + await this.page.getByLabel("Rationale *").fill(data.prop_rationale); + await this.page + .getByLabel("Receiving address *") + .fill(data.prop_receiving_address); + + await this.page.getByPlaceholder("e.g.").fill(data.prop_amount); + } +} diff --git a/tests/govtool-frontend/playwright/lib/types.ts b/tests/govtool-frontend/playwright/lib/types.ts index 765c955e7..ea587206c 100644 --- a/tests/govtool-frontend/playwright/lib/types.ts +++ b/tests/govtool-frontend/playwright/lib/types.ts @@ -97,3 +97,28 @@ export type ProtocolParams = { dRepDeposit: number; govActionDeposit: number; }; + +type Comment = { + proposal_id: string; + comment_text: string; +}; + +export type StaticProposal = { + id: number; + comments?: Comment[]; + title: string; +}; + +export type CommentResponse = { + id: number; + attributes: { + proposal_id: string; + comment_parent_id: null | string; + user_id: string; + comment_text: string; + createdAt: string; + updatedAt: string; + user_govtool_username: string; + subcommens_number: number; + }; +}; diff --git a/tests/govtool-frontend/playwright/package.json b/tests/govtool-frontend/playwright/package.json index a6799bc66..52f83da01 100644 --- a/tests/govtool-frontend/playwright/package.json +++ b/tests/govtool-frontend/playwright/package.json @@ -25,7 +25,7 @@ "allure:serve": "npx allure serve", "test": "npx playwright test", "format": "prettier . --write", - "generate-wallets": "ts-node ./generate_wallets.ts 10" + "generate-wallets": "ts-node ./generate_wallets.ts 11" }, "dependencies": { "@cardanoapi/cardano-test-wallet": "^1.1.2", diff --git a/tests/govtool-frontend/playwright/playwright.config.ts b/tests/govtool-frontend/playwright/playwright.config.ts index 62e7c3287..ada004008 100644 --- a/tests/govtool-frontend/playwright/playwright.config.ts +++ b/tests/govtool-frontend/playwright/playwright.config.ts @@ -14,6 +14,10 @@ export default defineConfig({ testDir: "./tests", /* Run tests in files in parallel */ fullyParallel: true, + /**TODO: Remove this timeout * + * It has been intentionally used to slow loading of govtool. + */ + timeout: 90_000, /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: !!environments.ci, /* Retry on CI only */ 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 22f58d5da..2d3d1c46c 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,104 +1,141 @@ import environments from "@constants/environments"; import { user01Wallet } from "@constants/staticWallets"; +import { createTempUserAuth } 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 { createNewPageWithWallet } from "@helpers/page"; import DRepDirectoryPage from "@pages/dRepDirectoryPage"; import EditDRepPage from "@pages/editDRepPage"; +import ProposalDiscussionPage from "@pages/proposalDiscussionPage"; import { expect } from "@playwright/test"; test.beforeEach(async () => { await setAllureEpic("6. Miscellaneous"); }); -test.use({ - storageState: ".auth/user01.json", - wallet: user01Wallet, +test.describe("Logged in user", () => { + test.use({ + storageState: ".auth/user01.json", + wallet: user01Wallet, + }); + + test("6E. Should open Sanchonet docs in a new tab when clicking `Learn More` on dashboards in connected state.", async ({ + page, + context, + }) => { + await page.goto("/"); + + const [delegationLearnMorepage] = await Promise.all([ + context.waitForEvent("page"), + page.getByTestId("delegate-learn-more-button").click(), + ]); + + await expect(delegationLearnMorepage).toHaveURL( + `${environments.docsUrl}/faqs/ways-to-use-your-voting-power` + ); + + const [registerLearnMorepage] = await Promise.all([ + context.waitForEvent("page"), + page.getByTestId("register-learn-more-button").click(), + ]); + + await expect(registerLearnMorepage).toHaveURL( + `${environments.docsUrl}/faqs/what-does-it-mean-to-register-as-a-drep` + ); + + const [directVoterLearnMorepage] = await Promise.all([ + context.waitForEvent("page"), + page.getByTestId("learn-more-button").first().click(), // BUG should be unique test id + ]); + + await expect(directVoterLearnMorepage).toHaveURL( + `${environments.docsUrl}/faqs/what-does-it-mean-to-register-as-a-drep` + ); + + const [GA_LearnMorepage] = await Promise.all([ + context.waitForEvent("page"), + page.getByTestId("learn-more-governance-actions-button").click(), + ]); + + await expect(GA_LearnMorepage).toHaveURL("https://sancho.network/actions/"); + + const [proposed_GA_VoterLearnMorepage] = await Promise.all([ + context.waitForEvent("page"), + page + .locator("div") + .filter({ hasText: /^ProposeLearn more$/ }) + .getByTestId("learn-more-button") + .click(), + ]); // BUG should be unique test id + + await expect(proposed_GA_VoterLearnMorepage).toHaveURL( + `${environments.docsUrl}/faqs/what-is-a-governance-action` + ); + }); + + test("6F. Should open sanchonet docs in a new tab when clicking `info` button of abstain and signal-no-confidence card", async ({ + page, + context, + }) => { + const dRepDirectoryPage = new DRepDirectoryPage(page); + await dRepDirectoryPage.goto(); + + await dRepDirectoryPage.automaticDelegationOptionsDropdown.click(); + + const [abstain_Info_Page] = await Promise.all([ + context.waitForEvent("page"), + dRepDirectoryPage.abstainInfoButton.click(), + ]); + + await expect(abstain_Info_Page).toHaveURL(`${environments.docsUrl}`); + + const [signal_No_Confidence_Info_Page] = await Promise.all([ + context.waitForEvent("page"), + dRepDirectoryPage.signalNoConfidenceInfoButton.click(), + ]); + + await expect(signal_No_Confidence_Info_Page).toHaveURL( + `${environments.docsUrl}` + ); + }); + + test("6G. Should restrict edit dRep for non dRep", async ({ page }) => { + const editDrepPage = new EditDRepPage(page); + await editDrepPage.goto(); + + await page.waitForTimeout(2_000); + await expect(editDrepPage.nameInput).not.toBeVisible(); + }); + + test("6I. Should prompt for a username after clicking on proposal discussion link if username is not set", async ({ + page, + }) => { + await page.goto("/"); + await page.getByTestId("proposal-discussion-link").click(); + + await expect( + page.getByText( + "Hey, setup your usernameUsername cannot be changed in the Future. Some subtext" + ) + ).toBeVisible(); //BUG Add modal testid instead should be username-modal + + await expect(page.getByLabel("Username *")).toBeVisible(); // BUG use testid instead + }); }); -test("6E. Should open Sanchonet docs in a new tab when clicking `Learn More` on dashboards in connected state.", async ({ - page, - context, -}) => { - await page.goto("/"); - - const [delegationLearnMorepage] = await Promise.all([ - context.waitForEvent("page"), - page.getByTestId("delegate-learn-more-button").click(), - ]); - - await expect(delegationLearnMorepage).toHaveURL( - `${environments.docsUrl}/faqs/ways-to-use-your-voting-power` - ); - - const [registerLearnMorepage] = await Promise.all([ - context.waitForEvent("page"), - page.getByTestId("register-learn-more-button").click(), - ]); - - await expect(registerLearnMorepage).toHaveURL( - `${environments.docsUrl}/faqs/what-does-it-mean-to-register-as-a-drep` - ); - - const [directVoterLearnMorepage] = await Promise.all([ - context.waitForEvent("page"), - page.getByTestId("learn-more-button").first().click(), // BUG should be unique test id - ]); - - await expect(directVoterLearnMorepage).toHaveURL( - `${environments.docsUrl}/faqs/what-does-it-mean-to-register-as-a-drep` - ); - - const [GA_LearnMorepage] = await Promise.all([ - context.waitForEvent("page"), - page.getByTestId("learn-more-governance-actions-button").click(), - ]); - - await expect(GA_LearnMorepage).toHaveURL("https://sancho.network/actions/"); - - const [proposed_GA_VoterLearnMorepage] = await Promise.all([ - context.waitForEvent("page"), - page - .locator("div") - .filter({ hasText: /^ProposeLearn more$/ }) - .getByTestId("learn-more-button") - .click(), - ]); // BUG should be unique test id - - await expect(proposed_GA_VoterLearnMorepage).toHaveURL( - `${environments.docsUrl}/faqs/what-is-a-governance-action` - ); -}); - -test("6F. should open sanchonet docs in a new tab when clicking `info` button of abstain and signal-no-confidence card", async ({ - page, - context, -}) => { - const dRepDirectoryPage = new DRepDirectoryPage(page); - await dRepDirectoryPage.goto(); - - await dRepDirectoryPage.automaticDelegationOptionsDropdown.click(); - - const [abstain_Info_Page] = await Promise.all([ - context.waitForEvent("page"), - dRepDirectoryPage.abstainInfoButton.click(), - ]); - - await expect(abstain_Info_Page).toHaveURL(`${environments.docsUrl}`); - - const [signal_No_Confidence_Info_Page] = await Promise.all([ - context.waitForEvent("page"), - dRepDirectoryPage.signalNoConfidenceInfoButton.click(), - ]); - - await expect(signal_No_Confidence_Info_Page).toHaveURL( - `${environments.docsUrl}` - ); -}); - -test("6G. Should restrict edit dRep for non dRep", async ({ page }) => { - const editDrepPage = new EditDRepPage(page); - await editDrepPage.goto(); - - await page.waitForTimeout(2_000); - await expect(editDrepPage.nameInput).not.toBeVisible(); +test.describe("Temporary user", () => { + test("6J. Should add a username.", async ({ page, browser }) => { + const wallet = (await ShelleyWallet.generate()).json(); + const tempUserAuth = await createTempUserAuth(page, wallet); + const userPage = await createNewPageWithWallet(browser, { + storageState: tempUserAuth, + wallet, + }); + + const proposalDiscussionPage = new ProposalDiscussionPage(userPage); + await proposalDiscussionPage.goto(); + await proposalDiscussionPage.setUsername(faker.internet.userName()); + }); }); diff --git a/tests/govtool-frontend/playwright/tests/8-proposal-discussion/proposalDiscussion.loggedin.spec.ts b/tests/govtool-frontend/playwright/tests/8-proposal-discussion/proposalDiscussion.loggedin.spec.ts new file mode 100644 index 000000000..9851d247f --- /dev/null +++ b/tests/govtool-frontend/playwright/tests/8-proposal-discussion/proposalDiscussion.loggedin.spec.ts @@ -0,0 +1,206 @@ +import { proposal01Wallet, user01Wallet } from "@constants/staticWallets"; +import { createTempUserAuth } from "@datafactory/createAuth"; +import { faker } from "@faker-js/faker"; +import { test } from "@fixtures/proposal"; +import { ShelleyWallet } from "@helpers/crypto"; +import { createNewPageWithWallet } from "@helpers/page"; +import ProposalDiscussionDetailsPage from "@pages/proposalDiscussionDetailsPage"; +import ProposalDiscussionPage from "@pages/proposalDiscussionPage"; +import { Page, expect } from "@playwright/test"; + +test.describe("Proposal created logged in state", () => { + test.use({ + storageState: ".auth/user01.json", + wallet: user01Wallet, + }); + + let proposalDiscussionDetailsPage: ProposalDiscussionDetailsPage; + + test.beforeEach(async ({ page, proposalId }) => { + proposalDiscussionDetailsPage = new ProposalDiscussionDetailsPage(page); + await proposalDiscussionDetailsPage.goto(proposalId); + await proposalDiscussionDetailsPage.closeUsernamePrompt(); + }); + + test("8G. Should display the proper likes and dislikes count", async ({ + page, + }) => { + await proposalDiscussionDetailsPage.likeBtn.click(); + await page.waitForTimeout(2_000); + await expect(page.getByText("10")).toBeVisible(); + + await proposalDiscussionDetailsPage.dislikeBtn.click(); + await page.waitForTimeout(2_000); + await expect(page.getByText("01", { exact: true })).toBeVisible(); + }); + + test("8J. Should sort the proposed governance action comments.", async ({ + page, + }) => { + for (let i = 0; i < 4; i++) { + const comment = faker.lorem.paragraph(2); + await proposalDiscussionDetailsPage.addComment(comment); + await page.waitForTimeout(2_000); + } + + await proposalDiscussionDetailsPage.sortAndValidate( + "asc", + (date1, date2) => new Date(date1) <= new Date(date2) + ); + }); + + test("8M. Should comment anonymously if a username is not set", async ({ + page, + }) => { + const randComment = faker.lorem.paragraph(2); + await proposalDiscussionDetailsPage.addComment(randComment); + + await expect(page.getByText(randComment)).toBeVisible(); + }); + + test("8N. Should reply to comments", async ({ page }) => { + const randComment = faker.lorem.paragraph(2); + const randReply = faker.lorem.paragraph(2); + + await proposalDiscussionDetailsPage.addComment(randComment); + + await proposalDiscussionDetailsPage.replyComment(randReply); + await expect(page.getByText(randReply)).toBeVisible(); + }); +}); + +test.describe("Proposal created with poll enabled (user auth)", () => { + test.use({ + storageState: ".auth/user01.json", + wallet: user01Wallet, + pollEnabled: true, + }); + + let proposalDiscussionDetailsPage: ProposalDiscussionDetailsPage; + + test.beforeEach(async ({ page, proposalId, browser }) => { + const proposalPage = await createNewPageWithWallet(browser, { + storageState: ".auth/proposal01.json", + wallet: proposal01Wallet, + }); + + proposalDiscussionDetailsPage = new ProposalDiscussionDetailsPage(page); + await proposalDiscussionDetailsPage.goto(proposalId); + await proposalDiscussionDetailsPage.closeUsernamePrompt(); + }); + test("8Q. Should vote on poll.", async ({ page }) => { + const pollVotes = ["Yes", "No"]; + const choice = Math.floor(Math.random() * pollVotes.length); + const vote = pollVotes[choice]; + + await proposalDiscussionDetailsPage.voteOnPoll(vote); + + await expect(proposalDiscussionDetailsPage.pollYesBtn).not.toBeVisible(); + await expect(proposalDiscussionDetailsPage.pollNoBtn).not.toBeVisible(); + await expect(page.getByText(`${vote}: (100%)`)).toBeVisible(); + // opposite of random choice vote + const oppositeVote = pollVotes[pollVotes.length - 1 - choice]; + await expect(page.getByText(`${oppositeVote}: (0%)`)).toBeVisible(); + }); + + test("8T. Should change vote on poll.", async ({ page }) => { + const pollVotes = ["Yes", "No"]; + const choice = Math.floor(Math.random() * pollVotes.length); + const vote = pollVotes[choice]; + + await proposalDiscussionDetailsPage.voteOnPoll(vote); + + await proposalDiscussionDetailsPage.changeVoteBtn.click(); + await page + .getByRole("button", { name: "Yes, change my Poll Vote" }) + .click(); + + await expect(proposalDiscussionDetailsPage.pollYesBtn).not.toBeVisible(); + await expect(proposalDiscussionDetailsPage.pollNoBtn).not.toBeVisible(); + + // vote must be changed + await expect(page.getByText(`${vote}: (0%)`)).toBeVisible(); + // opposite of random choice vote + const oppositeVote = pollVotes[pollVotes.length - 1 - choice]; + await expect(page.getByText(`${oppositeVote}: (100%)`)).toBeVisible(); + }); +}); + +test.describe("Proposal created logged out state", () => { + let userPage: Page; + + test.beforeEach(async ({ page, browser }) => { + const wallet = (await ShelleyWallet.generate()).json(); + const tempUserAuth = await createTempUserAuth(page, wallet); + + userPage = await createNewPageWithWallet(browser, { + storageState: tempUserAuth, + wallet, + }); + }); + + test("8O. Should update anonymous username to set username in comments", async ({ + proposalId, + }) => { + test.slow(); + + const proposalDiscussionDetailsPage = new ProposalDiscussionDetailsPage( + userPage + ); + await proposalDiscussionDetailsPage.goto(proposalId); + await proposalDiscussionDetailsPage.closeUsernamePrompt(); + + const randComment = faker.lorem.paragraph(2); + await proposalDiscussionDetailsPage.addComment(randComment); + + await expect(userPage.getByText(/anonymous/i)).toBeVisible(); + + const proposalDiscussionPage = new ProposalDiscussionPage(userPage); + await proposalDiscussionPage.goto(); + + const userName = faker.internet.userName(); + await proposalDiscussionPage.setUsername(userName); + await proposalDiscussionDetailsPage.goto(proposalId); + + await expect(userPage.getByText(/anonymous/i)).not.toBeVisible(); + await expect(userPage.getByText(userName)).toBeVisible(); + }); +}); + +test.describe("Proposal created with poll enabled (proposal auth)", () => { + test.use({ + storageState: ".auth/user01.json", + wallet: user01Wallet, + pollEnabled: true, + }); + + let proposalDiscussionDetailsPage: ProposalDiscussionDetailsPage; + + test.beforeEach(async ({ browser, proposalId }) => { + const proposalPage = await createNewPageWithWallet(browser, { + storageState: ".auth/proposal01.json", + wallet: proposal01Wallet, + }); + proposalDiscussionDetailsPage = new ProposalDiscussionDetailsPage( + proposalPage + ); + proposalDiscussionDetailsPage.goto(proposalId); + }); + + test("8P. Should add poll on own proposal", async ({}) => { + await expect(proposalDiscussionDetailsPage.addPollBtn).not.toBeVisible(); + }); + + test("8R. Should disable voting after cancelling the poll with the current poll result.", async ({ + page, + }) => { + await proposalDiscussionDetailsPage.closePollBtn.click(); + await proposalDiscussionDetailsPage.closePollYesBtn.click(); + await expect(proposalDiscussionDetailsPage.closePollBtn).not.toBeVisible(); + + // user + const userProposalDetailsPage = new ProposalDiscussionDetailsPage(page); + await expect(userProposalDetailsPage.pollYesBtn).not.toBeVisible(); + await expect(userProposalDetailsPage.pollNoBtn).not.toBeVisible(); + }); +}); diff --git a/tests/govtool-frontend/playwright/tests/8-proposal-discussion/proposalDiscussion.spec.ts b/tests/govtool-frontend/playwright/tests/8-proposal-discussion/proposalDiscussion.spec.ts new file mode 100644 index 000000000..d00bb4f01 --- /dev/null +++ b/tests/govtool-frontend/playwright/tests/8-proposal-discussion/proposalDiscussion.spec.ts @@ -0,0 +1,145 @@ +import { faker } from "@faker-js/faker"; +import { test } from "@fixtures/proposal"; +import { setAllureEpic } from "@helpers/allure"; +import ProposalDiscussionDetailsPage from "@pages/proposalDiscussionDetailsPage"; +import ProposalDiscussionPage from "@pages/proposalDiscussionPage"; +import { expect } from "@playwright/test"; + +const mockProposal = require("../../lib/_mock/proposal.json"); +const mockPoll = require("../../lib/_mock/proposalPoll.json"); +const mockComments = require("../../lib/_mock/proposalComments.json"); + +test.beforeEach(() => { + setAllureEpic("Proposal Discussion Forum"); +}); + +test("8A. Should access proposed governance actions in disconnected state", async ({ + page, +}) => { + const proposalDiscussionPage = new ProposalDiscussionPage(page); + await proposalDiscussionPage.goto(); + + await expect(page.getByText(/Proposed Governance Actions/i)).toHaveCount(2); +}); + +test("8B. Should filter and sort the list of proposed governance actions.", async ({ + page, +}) => { + const proposalDiscussionPage = new ProposalDiscussionPage(page); + await proposalDiscussionPage.goto(); + + await proposalDiscussionPage.filterBtn.click(); + await proposalDiscussionPage.infoRadio.click(); + + await expect(page.getByText("Treasury")).toHaveCount(1); + + await proposalDiscussionPage.treasuryRadio.click(); +}); + +test("8C. Should search the list of proposed governance actions.", async ({ + page, +}) => { + const proposalName = "Labadie, Stehr and Rosenbaum"; + const proposalDiscussionPage = new ProposalDiscussionPage(page); + await proposalDiscussionPage.goto(); + + await proposalDiscussionPage.searchInput.fill(proposalName); + + const proposalCards = await proposalDiscussionPage.getAllProposals(); + + for (const proposalCard of proposalCards) { + await expect(proposalCard.getByText(proposalName)).toBeVisible(); + } +}); + +test("8D.Should show the view-all categorized proposed governance actions.", async ({ + page, +}) => { + const proposalDiscussionPage = new ProposalDiscussionPage(page); + await proposalDiscussionPage.goto(); + + await proposalDiscussionPage.showAllBtn.click(); + + await expect(proposalDiscussionPage.showLessBtn).toBeVisible(); +}); + +test("8H. Should disable proposal interaction on a disconnected state.", async ({ + page, +}) => { + const proposalDiscussionPage = new ProposalDiscussionPage(page); + await proposalDiscussionPage.goto(); + + const proposalDiscussionDetailsPage = + await proposalDiscussionPage.viewFirstProposal(); + + await proposalDiscussionDetailsPage.commentInput.fill( + faker.lorem.paragraph() + ); + + await expect(proposalDiscussionDetailsPage.likeBtn).toBeDisabled(); + await expect(proposalDiscussionDetailsPage.dislikeBtn).toBeDisabled(); + await expect(proposalDiscussionDetailsPage.commentBtn).toBeDisabled(); +}); + +test("8S. Should restrict proposal creation on disconnected state", async ({ + page, +}) => { + const proposalDiscussionPage = new ProposalDiscussionPage(page); + await proposalDiscussionPage.goto(); + + await expect(proposalDiscussionPage.proposalCreateBtn).not.toBeVisible(); +}); + +test.describe("Mocked proposal", () => { + let proposalDiscussionDetailsPage: ProposalDiscussionDetailsPage; + + test.beforeEach(async ({ page }) => { + await page.route("**/api/proposals/**", async (route) => + route.fulfill({ + body: JSON.stringify(mockProposal), + }) + ); + + await page.route("**/api/polls**", async (route) => + route.fulfill({ + body: JSON.stringify(mockPoll), + }) + ); + + await page.route("**/api/comments**", async (route) => + route.fulfill({ + body: JSON.stringify(mockComments), + }) + ); + + proposalDiscussionDetailsPage = new ProposalDiscussionDetailsPage(page); + await proposalDiscussionDetailsPage.goto(10); + }); + + test("8E. Should share proposed governance action", async ({ + page, + context, + }) => { + await context.grantPermissions(["clipboard-read", "clipboard-write"]); + + await page.getByTestId("share-button").click(); + await expect(page.getByText("Copied to clipboard")).toBeVisible(); + const copiedTextDRepDirectory = await page.evaluate(() => + navigator.clipboard.readText() + ); + expect(copiedTextDRepDirectory).toEqual(mockProposal.data.id); + }); + + test("8I. Should disable poll voting functionality.", async () => { + await expect(proposalDiscussionDetailsPage.pollVoteCard).not.toBeVisible(); + await expect(proposalDiscussionDetailsPage.pollYesBtn).not.toBeVisible(); + + await expect(proposalDiscussionDetailsPage.pollNoBtn).not.toBeVisible(); + }); + + test("8F. Should display all comments with count indication.", async () => { + await expect(proposalDiscussionDetailsPage.commentsCount).toHaveText( + mockProposal.data.attributes.prop_comments_number + ); + }); +}); diff --git a/tests/govtool-frontend/playwright/tests/auth.setup.ts b/tests/govtool-frontend/playwright/tests/auth.setup.ts index 0edd09915..1093c81b3 100644 --- a/tests/govtool-frontend/playwright/tests/auth.setup.ts +++ b/tests/govtool-frontend/playwright/tests/auth.setup.ts @@ -9,12 +9,15 @@ import { adaHolder06Wallet, dRep01Wallet, dRep02Wallet, + proposal01Wallet, user01Wallet, } from "@constants/staticWallets"; +import { faker } from "@faker-js/faker"; import { importWallet } from "@fixtures/importWallet"; import { test as setup } from "@fixtures/walletExtension"; import { setAllureEpic, setAllureStory } from "@helpers/allure"; import LoginPage from "@pages/loginPage"; +import ProposalDiscussionPage from "@pages/proposalDiscussionPage"; const dRep01AuthFile = ".auth/dRep01.json"; const dRep02AuthFile = ".auth/dRep02.json"; @@ -28,6 +31,8 @@ const adaHolder06AuthFile = ".auth/adaHolder06.json"; const user01AuthFile = ".auth/user01.json"; +const proposal01AuthFile = ".auth/proposal01.json"; + setup.beforeEach(async () => { await setAllureEpic("Setup"); await setAllureStory("Authentication"); @@ -122,3 +127,17 @@ setup("Create AdaHolder 06 auth", async ({ page, context }) => { await context.storageState({ path: adaHolder06AuthFile }); }); + +setup("Create Proposal 01 auth", async ({ page, context }) => { + await importWallet(page, proposal01Wallet); + + const loginPage = new LoginPage(page); + await loginPage.login(); + await loginPage.isLoggedIn(); + + const proposalDiscussionPage = new ProposalDiscussionPage(page); + await proposalDiscussionPage.goto(); + await proposalDiscussionPage.setUsername(faker.internet.userName()); + + await context.storageState({ path: proposal01AuthFile }); +}); diff --git a/tests/load-testing/README.md b/tests/load-testing/README.md index 8bbc1cca1..dd81fc547 100644 --- a/tests/load-testing/README.md +++ b/tests/load-testing/README.md @@ -17,25 +17,29 @@ Before you start, ensure you have the following prerequisites installed: ## Manual Run ```bash -export TARGET_USER_RATE= RAMP_DURATION= PEAK_USERS= STRESS_DURATION= API_URL=; ./mvnw gatling:test +export API_URL=https://govtool.cardanoapi.io/api +export PEAK_USERS=100 +export RAMP_DURATION=40 # in seconds +export STRESS_DURATION=40 # in seconds +./mvnw gatling:test ``` -## Docker Build and Run +## Run stress test with docker -1. Build the Docker image: ```bash -docker build -t load-testing . -``` -2. Run the Docker container: -```bash -docker run -e TARGET_USER_RATE= -e RAMP_DURATION= -e PEAK_USERS= -e STRESS_DURATION= -e API_URL= load-testing +docker build -t govtool/load-testing . # build the image +docker run \ + -e RAMP_DURATION=40 \ + -e PEAK_USERS=100 \ + -e STRESS_DURATION=40 \ + -e API_URL='https://govtool.cardanoapi.io/api'\ + govtool/load-testing ``` ## Environment Variables Explain the environment variables used in the project and their purpose. -- TARGET_USER_RATE: The target rate of users per second during the test. +- API_URL: The URL of the API being tested. +- PEAK_USERS: The number of users to be injected during the test for each scenario. - RAMP_DURATION:The duration over which the user rate gradually increases. -- PEAK_USERS: The number of users to be injected during the stress peak. - STRESS_DURATION: The duration over which the stress peak occurs. -- API_URL: The URL of the API being tested. diff --git a/tests/load-testing/src/test/java/org/cardano/govtool/ApiService.java b/tests/load-testing/src/test/java/org/cardano/govtool/ApiService.java index 20fed22a4..7caa530a1 100644 --- a/tests/load-testing/src/test/java/org/cardano/govtool/ApiService.java +++ b/tests/load-testing/src/test/java/org/cardano/govtool/ApiService.java @@ -4,32 +4,33 @@ import io.gatling.javaapi.core.ChainBuilder; import io.gatling.javaapi.http.HttpRequestActionBuilder; +import java.util.concurrent.ThreadLocalRandom; + import static io.gatling.javaapi.core.CoreDsl.*; import static io.gatling.javaapi.http.HttpDsl.http; -import static io.gatling.javaapi.http.internal.HttpCheckBuilders.status; public class ApiService { - + static String example_metadata_validate="{\"url\":\"https://metadata-govtool.cardanoapi.io/data/milanfile\",\"hash\":\"dcc25ca74534a1fa3b4c86447c1e28dd86e103e1ead5e5308b64b34338af00a9\"}"; + public static ChainBuilder validate_sample_metadata= exec(http("validate_metadata").post("/metadata/validate").body(StringBody(example_metadata_validate)).header("content-type","application/json")); public static ChainBuilder getCurrentDelegation = exec(http("get_current_delegation").get("/ada-holder/get-current-delegation/#{stakeKey}")); public static HttpRequestActionBuilder getAdaHolderVotingPower = http("get_AdaHolder_voting_power").get("/ada-holder/get-voting-power/#{stakeKey}"); - public static HttpRequestActionBuilder getDRepVotingPower = http("get_DRep_voting_power").get("/drep/get-voting-power/#{dRepId}"); + public static HttpRequestActionBuilder getDRepVotingPower = http("get_dRep_voting_power").get("/drep/get-voting-power/#{dRepId}"); + public static HttpRequestActionBuilder getDrepInfo = http("get_dRep_info").get("/drep/info/#{stakeKey}"); public static ChainBuilder getVotes = exec(http("get_votes").get("/drep/getVotes/#{dRepId}")); - public static ChainBuilder getAllDReps = exec(http("get_dReps").get("/drep/list")); + public static ChainBuilder getAllDReps = exec(http("get_dReps").get("/drep/list?page=0&size=10")); public static ChainBuilder getParams = exec(http("get_epoch_params").get("/epoch/params")); - public static ChainBuilder getProposal = exec(http("get_proposal_detail").get("/proposal/get/#{proposalId}").check(status().is(404))); - - public static ChainBuilder getAllProposals = exec(http("get_proposals").get("/proposal/list")); - public static ChainBuilder getTxStatus = exec(http("get_tx_status").get("/transaction/status/#{txId}")); - //TODO randomize the number of repeats, Range 2 to 6 - public static ChainBuilder pollTxStatus = repeat(5).on( - exec(http("get_tx_status").get("/transaction/status/#{txId}")).pause(4)); + public static ChainBuilder getMetrics = exec(http("get_metrics").get("/network/metrics")); + + public static ChainBuilder pollTxStatus = repeat(session -> ThreadLocalRandom.current() + .nextInt(3,6)).on( + exec(http("get_tx_status").get("/transaction/status/#{txId}")).pause(4)); } diff --git a/tests/load-testing/src/test/java/org/cardano/govtool/Scenario.java b/tests/load-testing/src/test/java/org/cardano/govtool/Scenario.java deleted file mode 100644 index 32fc360ab..000000000 --- a/tests/load-testing/src/test/java/org/cardano/govtool/Scenario.java +++ /dev/null @@ -1,53 +0,0 @@ -package org.cardano.govtool; - -import io.gatling.javaapi.core.ScenarioBuilder; -import org.cardano.govtool.actions.Action; -import org.cardano.govtool.actions.AdaHolderAction; -import org.cardano.govtool.actions.AuthenticationAction; -import org.cardano.govtool.actions.DRepAction; - -import static io.gatling.javaapi.core.CoreDsl.scenario; - -public class Scenario { - // User connects and leave - public static final ScenarioBuilder userConnectAndLeave = scenario("User connect and leave") - .exec(AuthenticationAction.connect); - - // User register as DRep - public static final ScenarioBuilder userRegisterAsDRep = scenario("User registers as DRep") - .exec(AuthenticationAction.connect) - .pause(2) - .exec(DRepAction.registerAsDRep); - - // User views proposal - public static final ScenarioBuilder userOnlyViewsProposal = scenario("User views proposal") - .exec(AuthenticationAction.connect) - .pause(2) - .exec(Action.viewProposals); - - // DRep votes on proposal - public static final ScenarioBuilder dRepVoteOnProposal = scenario("DRep vote on proposal") - .exec(AuthenticationAction.connect) - .pause(2) - .exec(DRepAction.vote); - - // DRep view votes - public static final ScenarioBuilder dRepViewVotes = scenario("DRep view votes") - .exec(AuthenticationAction.connect) - .pause(2) - .exec(Action.viewProposals) - .pause(2) - .exec(Action.viewVotes); - - // User retire as DRep - public static final ScenarioBuilder dRepRetires = scenario("DRep retirement") - .exec(AuthenticationAction.connect) - .pause(2) - .exec(DRepAction.retireAsDRep); - - // Ada holder delegate to DRep - public static final ScenarioBuilder adaHolderDelegateToDRep = scenario("DRep delegation") - .exec(AuthenticationAction.connect) - .pause(2) - .exec(AdaHolderAction.delegateToDRep); -} diff --git a/tests/load-testing/src/test/java/org/cardano/govtool/actions/Action.java b/tests/load-testing/src/test/java/org/cardano/govtool/actions/Action.java index a29f798fe..47760a9a2 100644 --- a/tests/load-testing/src/test/java/org/cardano/govtool/actions/Action.java +++ b/tests/load-testing/src/test/java/org/cardano/govtool/actions/Action.java @@ -2,13 +2,13 @@ import io.gatling.javaapi.core.ChainBuilder; import org.cardano.govtool.ApiService; +import org.cardano.govtool.feeders.PageVisits; import static io.gatling.javaapi.core.CoreDsl.*; // Common Actions public class Action { - public static ChainBuilder viewProposals = group("View Proposals").on(exec(ApiService.getAllProposals) + public static ChainBuilder viewProposals = group("View Proposals").on(exec(PageVisits.visitProposalPage()) .exec(ApiService.getVotes)); - public static ChainBuilder viewVotes = exec(ApiService.getVotes); } diff --git a/tests/load-testing/src/test/java/org/cardano/govtool/actions/AdaHolderAction.java b/tests/load-testing/src/test/java/org/cardano/govtool/actions/AdaHolderAction.java index 437202bcf..06ef1e1ea 100644 --- a/tests/load-testing/src/test/java/org/cardano/govtool/actions/AdaHolderAction.java +++ b/tests/load-testing/src/test/java/org/cardano/govtool/actions/AdaHolderAction.java @@ -1,19 +1,55 @@ package org.cardano.govtool.actions; +import static io.gatling.javaapi.http.HttpDsl.http; import org.cardano.govtool.ApiService; -import org.cardano.govtool.configs.HeaderConfig; import io.gatling.javaapi.core.ChainBuilder; import org.cardano.govtool.feeders.RandomDataFeeder; +import java.util.Collections; +import java.util.concurrent.ThreadLocalRandom; + import static io.gatling.javaapi.core.CoreDsl.*; +import static io.gatling.javaapi.http.HttpDsl.status; +import static org.cardano.govtool.feeders.PageVisits.listAndSelectDreps; public class AdaHolderAction { - public static ChainBuilder delegateToDRep = group("Delegation").on(exec(ApiService.getAllDReps) + + + public static ChainBuilder delegateToDRep = group("Delegation").on( + feed(RandomDataFeeder.stakeKey) + ) + .exec(adaHolderBasicApis()) + .exec( AdaHolderAction.exploreDrepsChain().exec(adaHolderBasicApis())) .pause(6) + .feed(RandomDataFeeder.txId) - .exec(ApiService.getTxStatus) - .pause(4) - .exec(ApiService.pollTxStatus) - .feed(RandomDataFeeder.stakeKey) - .exec(ApiService.getCurrentDelegation)); + .repeat(6).on(ApiService.getTxStatus.pause(4)) + .exec(adaHolderBasicApis()); + + public static ChainBuilder adaHolderBasicApis(){ + return exec(ApiService.getDrepInfo) + .exec(ApiService.getAdaHolderVotingPower) + .exec(ApiService.getMetrics) + .exec(ApiService.getCurrentDelegation); + } + + + /** + * + */ + public static ChainBuilder exploreDrepsChain () { + return exec(adaHolderBasicApis().exec(listAndSelectDreps(2,5))) + .pause(2).exitHereIf(session -> { + return session.get("drepIds") == null; + }) + .foreach("#{drepIds}", "drepId").on( + exec( + http("get_dRepDetails").get("/drep/info/#{drepId}") + .check(status().is(200)) + ).exec(adaHolderBasicApis().exec(ApiService.getAllDReps)) + .pause(ThreadLocalRandom.current().nextLong(1,4)) + ); + + } } + diff --git a/tests/load-testing/src/test/java/org/cardano/govtool/actions/AuthenticationAction.java b/tests/load-testing/src/test/java/org/cardano/govtool/actions/AuthenticationAction.java index fb46e53e4..37b31e49a 100644 --- a/tests/load-testing/src/test/java/org/cardano/govtool/actions/AuthenticationAction.java +++ b/tests/load-testing/src/test/java/org/cardano/govtool/actions/AuthenticationAction.java @@ -4,18 +4,31 @@ import org.cardano.govtool.ApiService; import org.cardano.govtool.feeders.RandomDataFeeder; -import java.util.UUID; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ThreadLocalRandom; +import java.util.function.Supplier; +import java.util.stream.Stream; import static io.gatling.javaapi.core.CoreDsl.*; -import static io.gatling.javaapi.http.HttpDsl.poll; public class AuthenticationAction { - public static ChainBuilder connect = group("Login") - .on(exec(ApiService.getAllDReps) - .feed(RandomDataFeeder.stakeKey) - .exec(ApiService.getCurrentDelegation) - .exec(ApiService.getParams) - .feed(RandomDataFeeder.dRepId) - .exec(ApiService.getDRepVotingPower) - .exec(ApiService.getAdaHolderVotingPower)); + public static ChainBuilder connect (List knownDrepIds) { + + var drepStream = Stream.generate((Supplier>) () -> { + var randomIndex=ThreadLocalRandom.current().nextInt(0,knownDrepIds.size()); + var dRepKey = knownDrepIds.get(randomIndex); + return Collections.singletonMap("dRepId", dRepKey); + } + ).iterator(); + + return group("Login") + .on(feed(RandomDataFeeder.stakeKey) + .feed(drepStream) + .exec(ApiService.getCurrentDelegation) + .exec(ApiService.getParams) + .exec(ApiService.getDRepVotingPower) + .exec(ApiService.getAdaHolderVotingPower)); + } } diff --git a/tests/load-testing/src/test/java/org/cardano/govtool/actions/DRepAction.java b/tests/load-testing/src/test/java/org/cardano/govtool/actions/DRepAction.java index 1860f6130..52f2fd88a 100644 --- a/tests/load-testing/src/test/java/org/cardano/govtool/actions/DRepAction.java +++ b/tests/load-testing/src/test/java/org/cardano/govtool/actions/DRepAction.java @@ -5,35 +5,35 @@ import io.gatling.javaapi.core.ChainBuilder; import static io.gatling.javaapi.core.CoreDsl.*; -import static io.gatling.javaapi.http.HttpDsl.http; +import static org.cardano.govtool.feeders.PageVisits.visitProposalPage; public class DRepAction { public static ChainBuilder registerAsDRep = group("RegisterAsDRep").on( feed(RandomDataFeeder.txId).exec(ApiService.getTxStatus) + .exec(ApiService.validate_sample_metadata) .pause(4) .exec(ApiService.pollTxStatus) .exec(ApiService.getAllDReps) ); + public static ChainBuilder retireAsDRep = group("RetireAsDRep").on( feed(RandomDataFeeder.txId).exec(ApiService.getTxStatus) .pause(4) .exec(ApiService.pollTxStatus) .exec(ApiService.getAllDReps)); - public static ChainBuilder vote = group("Voting").on( feed(RandomDataFeeder.txId) .feed(RandomDataFeeder.dRepId) .feed(RandomDataFeeder.proposalId) - .exec(ApiService.getAllProposals) - .exec(ApiService.getVotes) + .exec(visitProposalPage()) .pause(4) - .exec(ApiService.getProposal) + // viewing propsals refetches the entire proposal list + .exec(repeat(3).on(visitProposalPage())) .pause(4) .exec(ApiService.getTxStatus) .pause(4) .exec(ApiService.pollTxStatus) - .exec(ApiService.getAllProposals) - .exec(ApiService.getVotes)); + .exec(visitProposalPage())); } diff --git a/tests/load-testing/src/test/java/org/cardano/govtool/feeders/DrepListFetcher.java b/tests/load-testing/src/test/java/org/cardano/govtool/feeders/DrepListFetcher.java new file mode 100644 index 000000000..ae85fe931 --- /dev/null +++ b/tests/load-testing/src/test/java/org/cardano/govtool/feeders/DrepListFetcher.java @@ -0,0 +1,47 @@ +package org.cardano.govtool.feeders; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.ArrayList; +import java.util.List; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +public class DrepListFetcher { + + private static final int PAGE_SIZE = 20; + + public static List fetchDrepIds(String baseUrl) { + baseUrl = baseUrl.endsWith("/")?baseUrl : baseUrl + "/"; + try { + HttpClient client = HttpClient.newHttpClient(); + List allIds = new ArrayList<>(); + ObjectMapper mapper = new ObjectMapper(); + + // Fetch about 5 pages + for (int page = 0; page < 5; page++) { + String requestUrl = baseUrl + "/drep/list" + "?page=" + page + "&size=" + PAGE_SIZE; + HttpRequest request = HttpRequest.newBuilder() + .uri(new URI(requestUrl)) + .GET() + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + JsonNode rootNode = mapper.readTree(response.body()).get("elements"); + + // select the drepId field + List idList = rootNode.findValues("drepId").stream() + .map(JsonNode::asText) + .toList(); + allIds.addAll(idList); + } + return allIds; + + } catch (Exception e) { + throw new RuntimeException("Failed to fetch available dreps"); + } + } +} diff --git a/tests/load-testing/src/test/java/org/cardano/govtool/feeders/PageVisits.java b/tests/load-testing/src/test/java/org/cardano/govtool/feeders/PageVisits.java new file mode 100644 index 000000000..ec3afa34f --- /dev/null +++ b/tests/load-testing/src/test/java/org/cardano/govtool/feeders/PageVisits.java @@ -0,0 +1,81 @@ +package org.cardano.govtool.feeders; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.gatling.javaapi.core.ChainBuilder; +import io.gatling.javaapi.http.HttpRequestActionBuilder; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ThreadLocalRandom; +import java.util.stream.Collectors; + +import static io.gatling.javaapi.core.CoreDsl.*; +import static io.gatling.javaapi.http.HttpDsl.http; +import static io.gatling.javaapi.http.HttpDsl.status; + +public class PageVisits { + public static List proposalTypes = Arrays.asList( + "NoConfidence", + "NewCommittee", + "NewConstitution", + "HardForkInitiation", + "ParameterChange", + "TreasuryWithdrawals", + "InfoAction" + ); + public static ObjectMapper objectMapper=new ObjectMapper(); + public static HttpRequestActionBuilder listAndSelectDreps(int min, int max){ + return http("get_dReps").get("/drep/list?page=0&size=10") + .check(jmesPath("elements[].drepId").ofList().transform(dreps ->{ + Collections.shuffle(dreps); + int visited_dreps = ThreadLocalRandom.current().nextInt(min, max); + return dreps.subList(0, Math.min(visited_dreps, dreps.size())); + }).exists().saveAs("drepIds")); + } + + + public static ChainBuilder visitProposalPage(){ + + + return exec(proposalTypes.stream().map(pType ->exec(requestProposal(pType)) + .doIf(session -> session.get("metadataList"+pType)!=null) + .then(foreach("#{metadataList" +pType + "}","metadata"+pType) + .on(exec( + http("validate proposal metadata") + .post("/metadata/validate") + .body(StringBody(session1 -> session1.get("metadata"+pType) + )).header("content-type","application/json") + .check(status().is(200)) + ))) + + ).toList()); + } + public static HttpRequestActionBuilder requestProposal(String type){ + return http("list proposal type "+type) + .get("/proposal/list") + .queryParam("page","0") + .queryParam("size","7") + .multivaluedQueryParam("type",List.of(type)) + .check(status().is(200)) + .check(jmesPath("elements[*]").ofList().transform(rawElements -> { + + List> elements = rawElements.stream().map(v -> (Map) v).toList(); + + List metaDataList = elements.stream().map(proposal -> { + var metadataHash = proposal.get("metadataHash").toString(); + var url = proposal.get("url"); + var mp= Map.of("url",url,"hash",metadataHash); + try { + return objectMapper.writeValueAsString(mp); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + }) + .collect(Collectors.toList()); + return metaDataList; + }).exists().saveAs("metadataList"+type)); + } +} diff --git a/tests/load-testing/src/test/java/org/cardano/govtool/simulations/VvaSimulation.java b/tests/load-testing/src/test/java/org/cardano/govtool/simulations/VvaSimulation.java index ff61cf533..37c3a4586 100644 --- a/tests/load-testing/src/test/java/org/cardano/govtool/simulations/VvaSimulation.java +++ b/tests/load-testing/src/test/java/org/cardano/govtool/simulations/VvaSimulation.java @@ -1,9 +1,17 @@ package org.cardano.govtool.simulations; -import org.cardano.govtool.Scenario; +import io.gatling.javaapi.core.ChainBuilder; +import io.gatling.javaapi.core.PopulationBuilder; import io.gatling.javaapi.core.Simulation; import io.gatling.javaapi.http.HttpProtocolBuilder; +import org.cardano.govtool.actions.Action; +import org.cardano.govtool.actions.AdaHolderAction; +import org.cardano.govtool.actions.AuthenticationAction; +import org.cardano.govtool.actions.DRepAction; +import org.cardano.govtool.feeders.DrepListFetcher; +import org.cardano.govtool.feeders.PageVisits; +import java.util.List; import java.util.Optional; import static io.gatling.javaapi.core.CoreDsl.*; @@ -11,19 +19,27 @@ public class VvaSimulation extends Simulation { - private static final String API_URL = Optional.ofNullable(System.getenv("API_URL")).orElse("http://localhost:3000/api"); - private static final int TARGET_USER_RATE = Integer.parseInt(Optional.ofNullable(System.getenv("TARGET_USER_RATE")).orElse("5")); - private static final int PEAK_USERS = Integer.parseInt(Optional.ofNullable(System.getenv("PEAK_USERS")).orElse("10")); - private static final int STRESS_DURATION = Integer.parseInt(Optional.ofNullable(System.getenv("STRESS_DURATION")).orElse("10")); - private static final int RAMP_DURATION = Integer.parseInt(Optional.ofNullable(System.getenv("RAMP_DURATION")).orElse("10")); - + private static final String API_URL = Optional.ofNullable(System.getenv("API_URL")).orElse("https://govtool.cardanoapi.io/api2"); + private static final int PEAK_USERS = Integer.parseInt(Optional.ofNullable(System.getenv("PEAK_USERS")).orElse("600")); + private static final int STRESS_DURATION = Integer.parseInt(Optional.ofNullable(System.getenv("STRESS_DURATION")).orElse("20")); + private static final int RAMP_DURATION = Integer.parseInt(Optional.ofNullable(System.getenv("RAMP_DURATION")).orElse("20")); + private ListknownDreps; @Override public void before() { System.out.printf("Base API URL: %s%n", API_URL); - System.out.printf("Target user rate: %d users/sec%n", TARGET_USER_RATE); - System.out.printf("Ramping users over %d seconds%n", RAMP_DURATION); System.out.printf("Peak users count: %d%n", PEAK_USERS); + System.out.printf("Ramping users over %d seconds%n", RAMP_DURATION); System.out.printf("Stress interval %d seconds%n", STRESS_DURATION); + + } + + private PopulationBuilder makeScenario(String name, ChainBuilder chain, double userPercent) { + var rampUserRate = ((double) PEAK_USERS) * userPercent / (double) RAMP_DURATION; + return scenario(name).exec(AuthenticationAction.connect(knownDreps).pause(2).exec(chain)).injectOpen( + nothingFor(5), + constantUsersPerSec(rampUserRate).during(RAMP_DURATION), + stressPeakUsers(PEAK_USERS).during(STRESS_DURATION) + ); } private final HttpProtocolBuilder httpProtocol = http @@ -36,43 +52,25 @@ public void before() { // Load Simulation { + knownDreps= DrepListFetcher.fetchDrepIds(API_URL); + var DREP_USER_RATI0=0.3 setUp( - Scenario.userConnectAndLeave.injectOpen( - nothingFor(5), - rampUsersPerSec(1).to(TARGET_USER_RATE * 0.2).during(RAMP_DURATION), - stressPeakUsers(PEAK_USERS).during(STRESS_DURATION) - ), - Scenario.userRegisterAsDRep.injectOpen( - nothingFor(5), - rampUsersPerSec(1).to(TARGET_USER_RATE * 0.3).during(RAMP_DURATION), - stressPeakUsers(PEAK_USERS).during(STRESS_DURATION) - ), - Scenario.userOnlyViewsProposal.injectOpen( - nothingFor(5), - rampUsersPerSec(1).to(TARGET_USER_RATE * 0.1).during(RAMP_DURATION), - stressPeakUsers(PEAK_USERS).during(STRESS_DURATION) - ), - Scenario.adaHolderDelegateToDRep.injectOpen( - nothingFor(5), - rampUsersPerSec(1).to(TARGET_USER_RATE * 0.4).during(RAMP_DURATION), - stressPeakUsers(PEAK_USERS).during(STRESS_DURATION) - ), - // Further DRep scenarios - Scenario.dRepVoteOnProposal.injectOpen( - nothingFor(5), - rampUsersPerSec(1).to(TARGET_USER_RATE * 0.3 * 0.4).during(RAMP_DURATION), - stressPeakUsers(PEAK_USERS).during(STRESS_DURATION) - ), - Scenario.dRepViewVotes.injectOpen( - nothingFor(5), - rampUsersPerSec(1).to(TARGET_USER_RATE * 0.3 * 0.5).during(RAMP_DURATION), - stressPeakUsers(PEAK_USERS).during(STRESS_DURATION) - ), - Scenario.dRepRetires.injectOpen( - nothingFor(5), - rampUsersPerSec(1).to(TARGET_USER_RATE * 0.3 * 0.1).during(RAMP_DURATION), - stressPeakUsers(PEAK_USERS).during(STRESS_DURATION) - ) + makeScenario("User Connects and Leave", exec(), 0.1) + , makeScenario("User Registers as Drep", + exec(DRepAction.registerAsDRep), 0.1), + makeScenario("User Views Proposals", + exec(Action.viewProposals), 0.2) + , makeScenario("AdaHolder delegates to Drep", + exec(AdaHolderAction.delegateToDRep), 0.1) + , makeScenario("ListProposals", PageVisits.visitProposalPage(), 0.2) + + // Further split drep users on scenarios + , makeScenario("Drep view Votes", + exec(Action.viewProposals).pause(2).exec(Action.viewProposals), DREP_USER_RATI0 * 0.5) + , makeScenario("Drep votes on Proposal", + exec(DRepAction.vote), DREP_USER_RATI0 * 0.4) + , makeScenario("Drep Retirement", + exec(DRepAction.retireAsDRep), DREP_USER_RATI0 * 0.1) ).protocols(httpProtocol); } diff --git a/tests/test-infrastructure/.env.example b/tests/test-infrastructure/.env.example index ace81dd8f..9d3654b2f 100644 --- a/tests/test-infrastructure/.env.example +++ b/tests/test-infrastructure/.env.example @@ -4,4 +4,5 @@ SENTRY_DSN_FRONTEND= SENTRY_DSN_BACKEND= CARDANO_NETWORK=sanchonet BASE_DOMAIN=govtool.cardanoapi.io -GOVTOOL_TAG=test \ No newline at end of file +GOVTOOL_TAG=test +APP_ENV=test \ No newline at end of file diff --git a/tests/test-infrastructure/build-and-deploy.sh b/tests/test-infrastructure/build-and-deploy.sh index 919f64a15..4c6e6e166 100755 --- a/tests/test-infrastructure/build-and-deploy.sh +++ b/tests/test-infrastructure/build-and-deploy.sh @@ -24,6 +24,7 @@ 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 govtool_storybook "$BASE_IMAGE_NAME"/storybook:${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} diff --git a/tests/test-infrastructure/docker-compose-govtool.yml b/tests/test-infrastructure/docker-compose-govtool.yml index eebd897e0..c129fd247 100644 --- a/tests/test-infrastructure/docker-compose-govtool.yml +++ b/tests/test-infrastructure/docker-compose-govtool.yml @@ -20,9 +20,9 @@ services: - -c - vva-be -c /config.json start-app environment: + APP_ENV: ${APP_ENV:-test} VIRTUAL_HOST: https://${BASE_DOMAIN}/api/ -> :8080/ VIRTUAL_HOST_2: https://${BASE_DOMAIN}/swagger -> :8080/swagger - networks: - frontend - postgres @@ -42,7 +42,9 @@ services: VITE_BASE_URL: "/api" VITE_SENTRY_DSN: ${SENTRY_DSN_FRONTEND} NPMRC_TOKEN: ${NPMRC_TOKEN} + VITE_APP_ENV: ${APP_ENV:-test} VITE_IS_PROPOSAL_DISCUSSION_FORUM_ENABLED: "true" + GTM_ID: ${GTM_ID} environment: VIRTUAL_HOST: https://${BASE_DOMAIN} networks: @@ -59,7 +61,7 @@ services: context: ../../govtool/metadata-validation environment: VIRTUAL_HOST: https://${BASE_DOMAIN}/metadata-validation/ -> :3000 - PORT: '3000' + PORT: "3000" networks: - frontend deploy: @@ -68,3 +70,18 @@ services: placement: constraints: - node.labels.govtool==true + storybook: + image: govtool/storybook:${GOVTOOL_TAG} + build: + context: ../../govtool/frontend + dockerfile: storybook.Dockerfile + args: + NPMRC_TOKEN: ${NPMRC_TOKEN} + environment: + VIRTUAL_HOST: https://storybook-${BASE_DOMAIN} + deploy: + restart_policy: + delay: "30s" + placement: + constraints: + - node.labels.govtool==true diff --git a/tests/test-infrastructure/playbook.yml b/tests/test-infrastructure/playbook.yml index 6eef0d7c8..7a6337f3c 100644 --- a/tests/test-infrastructure/playbook.yml +++ b/tests/test-infrastructure/playbook.yml @@ -18,6 +18,8 @@ args: chdir: "/opt/govtool/tests/test-infrastructure" environment: + APP_ENV: :{{ lookup('env', 'APP_ENV') }} + GTM_ID: "{{ lookup ('env', 'GTM_ID') }}" GOVTOOL_TAG: "{{ lookup('env', 'GOVTOOL_TAG') }}" NPMRC_TOKEN: "{{ lookup('env','NPMRC_TOKEN') }}" SENTRY_DSN_FRONTEND: "{{ lookup ('env', 'SENTRY_DSN_FRONTEND') }}"