├── VERSION.txt ├── source ├── mcp-server │ ├── .gitignore │ ├── src │ │ ├── lib │ │ │ ├── errors.ts │ │ │ ├── metrics.ts │ │ │ ├── logger.ts │ │ │ └── common.ts │ │ └── tools │ │ │ ├── index.ts │ │ │ ├── list-scenarios.ts │ │ │ ├── get-baseline-test-run.ts │ │ │ ├── get-scenario-details.ts │ │ │ ├── get-latest-test-run.ts │ │ │ └── get-test-run.ts │ ├── vitest.config.ts │ ├── package.json │ ├── test │ │ └── lib │ │ │ └── errors.test.ts │ └── tsconfig.json ├── .prettierignore ├── webui │ ├── .prettierignore │ ├── src │ │ ├── models │ │ │ └── user.ts │ │ ├── utils │ │ │ ├── jsonValidator.ts │ │ │ ├── scenarioUtils.ts │ │ │ ├── generateUniqueId.ts │ │ │ ├── dateUtils.ts │ │ │ ├── __tests__ │ │ │ │ ├── jsonValidator.test.ts │ │ │ │ ├── dateUtils.test.ts │ │ │ │ └── errorUtils.test.ts │ │ │ ├── errorUtils.ts │ │ │ └── iotPolicy.ts │ │ ├── __tests__ │ │ │ ├── server.ts │ │ │ ├── pages │ │ │ │ ├── ScenarioDetailsPage.test.tsx │ │ │ │ └── CreateTestScenarioPage.test.ts │ │ │ ├── test-utils.tsx │ │ │ ├── test-data-random-utils.ts │ │ │ └── components │ │ │ │ └── TaskStatus.test.tsx │ │ ├── pages │ │ │ └── scenarios │ │ │ │ ├── ScenarioDetailsPage.css │ │ │ │ ├── ScenariosPage.tsx │ │ │ │ ├── hooks │ │ │ │ ├── useTagManagement.ts │ │ │ │ ├── useFormData.ts │ │ │ │ ├── useScenarioActions.ts │ │ │ │ └── useTestResultsData.ts │ │ │ │ ├── components │ │ │ │ ├── TestRunsBaselineContainer.tsx │ │ │ │ ├── GeneralSettingsStep.tsx │ │ │ │ ├── TestRunsDateFilter.tsx │ │ │ │ ├── TagsSection.tsx │ │ │ │ └── TestConfigurationSection.tsx │ │ │ │ ├── types │ │ │ │ ├── viewMode.ts │ │ │ │ └── createTest.ts │ │ │ │ └── constants.ts │ │ ├── App.tsx │ │ ├── store │ │ │ ├── stackInfoApiSlice.ts │ │ │ ├── types.ts │ │ │ ├── userThunks.ts │ │ │ ├── store.ts │ │ │ ├── regionsSlice.ts │ │ │ └── notificationsSlice.ts │ │ ├── styles.css │ │ ├── setupTests.ts │ │ ├── components │ │ │ └── navigation │ │ │ │ ├── TopNavigationBar.tsx │ │ │ │ ├── Breadcrumbs.tsx │ │ │ │ └── SideNavigationBar.tsx │ │ ├── AppRoutes.tsx │ │ ├── Layout.tsx │ │ ├── contexts │ │ │ └── NotificationContext.tsx │ │ ├── mocks │ │ │ └── browser.ts │ │ └── main.tsx │ ├── tsconfig.node.json │ ├── index.html │ ├── tsconfig.json │ ├── vite.config.ts │ ├── README.md │ └── eslint.config.js ├── infrastructure │ ├── .npmignore │ ├── .gitignore │ ├── jest.config.js │ ├── cdk.json │ ├── bin │ │ ├── solution.ts │ │ └── distributed-load-testing-on-aws.ts │ ├── tsconfig.json │ ├── test │ │ ├── auth.test.ts │ │ ├── console.test.ts │ │ ├── distributed-load-testing-on-aws-regional-stack.test.ts │ │ ├── distributed-load-testing-on-aws-stack.test.ts │ │ ├── scenarios-storage.test.ts │ │ ├── common-resources.test.ts │ │ ├── custom-resources-lambda.test.ts │ │ ├── add-cfn-guard-suppression.test.ts │ │ ├── snapshot_helpers.ts │ │ ├── step-functions.test.ts │ │ ├── custom-resources-infra.test.ts │ │ └── vpc.test.ts │ ├── lib │ │ ├── common-resources │ │ │ ├── add-cfn-guard-suppression.ts │ │ │ └── common-cfn-parameters.ts │ │ └── mcp │ │ │ ├── gateway-construct.ts │ │ │ └── gateway-target-construct.ts │ ├── package.json │ ├── README.md │ └── lambda │ │ └── aws-exports-handler │ │ └── index.ts ├── .eslintignore ├── integration-tests │ ├── src │ │ ├── assets │ │ │ └── ziptest.zip │ │ ├── cypress │ │ │ ├── e2e │ │ │ │ ├── home.cy.ts │ │ │ │ └── test-scenario-01.cy.ts │ │ │ └── support │ │ │ │ ├── e2e.ts │ │ │ │ └── commands.ts │ │ ├── scenario.ts │ │ └── utils.ts │ ├── cypress.config.ts │ ├── api.config.ts │ ├── tsconfig.json │ └── package.json ├── metrics-utils │ ├── index.ts │ ├── jest.config.js │ ├── tsconfig.json │ ├── lambda │ │ ├── helpers │ │ │ ├── client-helper.ts │ │ │ └── types.ts │ │ └── index.ts │ └── package.json ├── .prettierrc.yml ├── api-services │ ├── tsconfig.spec.json │ ├── babel.config.js │ ├── tsconfig.json │ ├── jest.config.js │ ├── lib │ │ └── validation │ │ │ └── index.ts │ └── package.json ├── task-runner │ ├── jest.config.js │ └── package.json ├── custom-resource │ ├── jest.config.js │ ├── lib │ │ ├── cfn │ │ │ ├── index.js │ │ │ └── index.spec.js │ │ ├── metrics │ │ │ ├── index.js │ │ │ └── index.spec.js │ │ ├── iot │ │ │ └── index.js │ │ ├── s3 │ │ │ └── index.js │ │ └── config-storage │ │ │ └── index.js │ ├── package.json │ └── regional-index.js ├── results-parser │ ├── jest.config.js │ ├── package.json │ └── lib │ │ └── mock.js ├── task-canceler │ ├── jest.config.js │ └── package.json ├── metric-filter-cleaner │ ├── jest.config.js │ └── package.json ├── task-status-checker │ ├── jest.config.js │ └── package.json ├── real-time-data-publisher │ ├── jest.config.js │ └── package.json ├── solution-utils │ ├── package.json │ └── utils.js ├── .eslintrc └── package.json ├── architecture.png ├── .github ├── ISSUE_TEMPLATE │ ├── general_question.md │ ├── feature_request.md │ └── bug_report.md └── PULL_REQUEST_TEMPLATE.md ├── CODE_OF_CONDUCT.md ├── solution-manifest.yaml ├── SECURITY.md ├── .gitignore └── deployment └── ecr └── distributed-load-testing-on-aws-load-tester ├── .bzt-rc ├── ecscontroller.py ├── Dockerfile └── ecslistener.py /VERSION.txt: -------------------------------------------------------------------------------- 1 | 4.0.2 2 | -------------------------------------------------------------------------------- /source/mcp-server/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ -------------------------------------------------------------------------------- /source/.prettierignore: -------------------------------------------------------------------------------- 1 | webui/public/mockServiceWorker.js 2 | -------------------------------------------------------------------------------- /source/webui/.prettierignore: -------------------------------------------------------------------------------- 1 | webui/public/mockServiceWorker.js 2 | -------------------------------------------------------------------------------- /architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/distributed-load-testing-on-aws/HEAD/architecture.png -------------------------------------------------------------------------------- /source/infrastructure/.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /source/infrastructure/.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !jest.config.js 3 | *.d.ts 4 | node_modules 5 | 6 | # CDK asset staging directory 7 | .cdk.staging 8 | cdk.out 9 | -------------------------------------------------------------------------------- /source/.eslintignore: -------------------------------------------------------------------------------- 1 | **/infrastructure/*.js 2 | *.d.ts 3 | node_modules 4 | coverage 5 | **/test/* 6 | **/build/* 7 | **/assets/* 8 | **/mcp-server/* 9 | **/mcp/** -------------------------------------------------------------------------------- /source/integration-tests/src/assets/ziptest.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/distributed-load-testing-on-aws/HEAD/source/integration-tests/src/assets/ziptest.zip -------------------------------------------------------------------------------- /source/webui/src/models/user.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | export interface User { 5 | alias: string; 6 | } 7 | -------------------------------------------------------------------------------- /source/metrics-utils/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | export { SolutionsMetrics } from "./lib/solutions-metrics"; 5 | export * from "./lambda/helpers/types"; 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/general_question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: General question 3 | about: Ask a general question 4 | title: '' 5 | labels: question 6 | assignees: '' 7 | 8 | --- 9 | 10 | **What is your question?** 11 | -------------------------------------------------------------------------------- /source/infrastructure/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ["/test"], 3 | testMatch: ["**/*.test.ts"], 4 | transform: { 5 | "^.+\\.tsx?$": "ts-jest", 6 | }, 7 | coverageReporters: ["text", "clover", "json", ["lcov", { projectRoot: "../.." }]], 8 | }; 9 | -------------------------------------------------------------------------------- /source/webui/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /source/.prettierrc.yml: -------------------------------------------------------------------------------- 1 | # .prettierrc or .prettierrc.yaml 2 | arrowParens: "always" 3 | bracketSpacing: true 4 | endOfLine: "lf" 5 | htmlWhitespaceSensitivity: "css" 6 | proseWrap: "preserve" 7 | trailingComma: "es5" 8 | tabWidth: 2 9 | semi: true 10 | quoteProps: "as-needed" 11 | printWidth: 120 -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /source/api-services/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": true 5 | }, 6 | "include": [ 7 | "**/*.spec.ts", 8 | "**/*.spec.js", 9 | "**/*.test.ts", 10 | "**/*.test.js" 11 | ], 12 | "exclude": [ 13 | "node_modules", 14 | "dist", 15 | "coverage" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /source/webui/src/utils/jsonValidator.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | export const isValidJSON = (str: string): boolean => { 5 | if (!str.trim()) return true; 6 | try { 7 | JSON.parse(str); 8 | return true; 9 | } catch { 10 | return false; 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /source/integration-tests/src/cypress/e2e/home.cy.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | describe("Home Page", () => { 5 | it("Loads successfully", () => { 6 | cy.visit("/"); 7 | cy.contains("Distributed Load Testing"); 8 | cy.contains("Test Scenarios"); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /source/mcp-server/src/lib/errors.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | export class AppError extends Error { 5 | public code: number; 6 | 7 | constructor(message: string, code: number) { 8 | super(message); 9 | this.name = "AppError"; 10 | this.code = code; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /source/task-runner/jest.config.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | module.exports = { 5 | roots: ["/lib"], 6 | testMatch: ["**/*.spec.js"], 7 | collectCoverageFrom: ["**/*.js"], 8 | coverageReporters: ["text", "clover", "json", ["lcov", { projectRoot: "../../" }]], 9 | }; 10 | -------------------------------------------------------------------------------- /source/custom-resource/jest.config.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | module.exports = { 5 | roots: ["/lib"], 6 | testMatch: ["**/*.spec.js"], 7 | collectCoverageFrom: ["**/*.js"], 8 | coverageReporters: ["text", "clover", "json", ["lcov", { projectRoot: "../../" }]], 9 | }; 10 | -------------------------------------------------------------------------------- /source/metrics-utils/jest.config.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | module.exports = { 5 | roots: ["/test"], 6 | testMatch: ["**/*.spec.ts"], 7 | transform: { 8 | "^.+\\.tsx?$": "ts-jest", 9 | }, 10 | coverageReporters: ["text", ["lcov", { projectRoot: "../" }]], 11 | }; 12 | -------------------------------------------------------------------------------- /source/results-parser/jest.config.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | module.exports = { 5 | roots: ["/lib"], 6 | testMatch: ["**/*.spec.js"], 7 | collectCoverageFrom: ["**/*.js"], 8 | coverageReporters: ["text", "clover", "json", ["lcov", { projectRoot: "../../" }]], 9 | }; 10 | -------------------------------------------------------------------------------- /source/task-canceler/jest.config.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | module.exports = { 5 | roots: ["/lib"], 6 | testMatch: ["**/*.spec.js"], 7 | collectCoverageFrom: ["**/*.js"], 8 | coverageReporters: ["text", "clover", "json", ["lcov", { projectRoot: "../../" }]], 9 | }; 10 | -------------------------------------------------------------------------------- /source/metric-filter-cleaner/jest.config.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | module.exports = { 5 | roots: ["/lib"], 6 | testMatch: ["**/*.spec.js"], 7 | collectCoverageFrom: ["**/*.js"], 8 | coverageReporters: ["text", "clover", "json", ["lcov", { projectRoot: "../../" }]], 9 | }; 10 | -------------------------------------------------------------------------------- /source/task-status-checker/jest.config.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | module.exports = { 5 | roots: ["/lib"], 6 | testMatch: ["**/*.spec.js"], 7 | collectCoverageFrom: ["**/*.js"], 8 | coverageReporters: ["text", "clover", "json", ["lcov", { projectRoot: "../.." }]], 9 | }; 10 | -------------------------------------------------------------------------------- /source/real-time-data-publisher/jest.config.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | module.exports = { 5 | roots: ["/lib"], 6 | testMatch: ["**/*.spec.js"], 7 | collectCoverageFrom: ["**/*.js"], 8 | coverageReporters: ["text", "clover", "json", ["lcov", { projectRoot: "../../" }]], 9 | }; 10 | -------------------------------------------------------------------------------- /source/api-services/babel.config.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | module.exports = { 5 | presets: [ 6 | ['@babel/preset-env', { 7 | targets: { 8 | node: 'current' 9 | } 10 | }], 11 | ['@babel/preset-typescript', { 12 | allowDeclareFields: true, 13 | }] 14 | ] 15 | }; 16 | -------------------------------------------------------------------------------- /source/infrastructure/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/distributed-load-testing-on-aws.ts", 3 | "context": { 4 | "@aws-cdk/core:stackRelativeExports": false, 5 | "@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": false, 6 | "solutionId": "SO0062", 7 | "solutionVersion": "custom-v4.0.2", 8 | "solutionName": "distributed-load-testing-on-aws" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /solution-manifest.yaml: -------------------------------------------------------------------------------- 1 | id: SO0062 2 | name: distributed-load-testing-on-aws 3 | version: v4.0.2 4 | cloudformation_templates: 5 | - template: distributed-load-testing-on-aws.template 6 | main_template: true 7 | - template: distributed-load-testing-on-aws-regional.template 8 | main_template: false 9 | build_environment: 10 | build_image: 'aws/codebuild/standard:7.0' 11 | container_images: 12 | - distributed-load-testing-on-aws-load-tester 13 | -------------------------------------------------------------------------------- /source/webui/src/__tests__/server.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { setupServer } from "msw/node"; 5 | import { handlers } from "../mocks/handlers.ts"; 6 | 7 | // configures a mock server for unit tests. 8 | // call server.use() in test to set up handlers. 9 | export const MOCK_SERVER_URL = "http://localhost:3001/"; 10 | export const server = setupServer(...handlers(MOCK_SERVER_URL)); 11 | -------------------------------------------------------------------------------- /source/webui/src/pages/scenarios/ScenarioDetailsPage.css: -------------------------------------------------------------------------------- 1 | /* Custom styling for the CloudScape ProgressBar component */ 2 | [class*="awsui_percentage"] { 3 | display: none !important; 4 | } 5 | [class*="awsui_progress"] { 6 | flex: 1 !important; 7 | margin-inline-end: 0 !important; 8 | vertical-align: top !important; 9 | margin-left: .25em !important; 10 | margin-right: .25em !important; 11 | } 12 | [class*="awsui_progress-container"] { 13 | align-items: start !important; 14 | } 15 | -------------------------------------------------------------------------------- /source/webui/src/App.tsx: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { withAuthenticator } from "@aws-amplify/ui-react"; 5 | import "@aws-amplify/ui-react/styles.css"; 6 | import { AppRoutes } from "./AppRoutes.tsx"; 7 | 8 | export const AppComponent = () => ; 9 | 10 | export const App = withAuthenticator(AppComponent, { 11 | loginMechanisms: ["username"], 12 | hideSignUp: true, 13 | }); 14 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Issue #, if available:** 2 | 3 | 4 | 5 | 6 | **Description of changes:** 7 | 8 | 9 | 10 | **Checklist** 11 | - [ ] :wave: I have run the unit tests, and all unit tests have passed. 12 | - [ ] :warning: This pull request might incur a breaking change. 13 | 14 | By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. 15 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting Security Issues 2 | 3 | We take all security reports seriously. 4 | When we receive such reports, 5 | we will investigate and subsequently address 6 | any potential vulnerabilities as quickly as possible. 7 | If you discover a potential security issue in this project, 8 | please notify AWS/Amazon Security via our 9 | [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/) 10 | or directly via email to [AWS Security](mailto:aws-security@amazon.com). 11 | Please do *not* create a public GitHub issue in this project. -------------------------------------------------------------------------------- /source/integration-tests/src/cypress/support/e2e.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import "./commands"; 5 | 6 | before(() => { 7 | cy.authenticate(`${Cypress.env("USERNAME")}`); 8 | }); 9 | 10 | declare global { 11 | // using Cypress namespace to extend with custom command 12 | // eslint-disable-next-line @typescript-eslint/no-namespace 13 | namespace Cypress { 14 | interface Chainable { 15 | authenticate(sessionName: string): Chainable; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /source/webui/src/utils/scenarioUtils.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { TestTypes } from "../pages/scenarios/constants"; 5 | 6 | export const isScriptTestType = (testType: string) => testType !== TestTypes.SIMPLE; 7 | 8 | export const getFileExtension = (filename: string) => filename.split(".").pop(); 9 | 10 | export const parseTimeUnit = (timeStr: string) => ({ 11 | value: timeStr?.replace(/[a-zA-Z]/g, "") || "1", 12 | unit: timeStr?.match(/[a-zA-Z]+/)?.[0] === "s" ? "seconds" : "minutes", 13 | }); -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this solution 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | 12 | 13 | **Describe the feature you'd like** 14 | 15 | 16 | **Additional context** 17 | 18 | -------------------------------------------------------------------------------- /source/webui/src/utils/generateUniqueId.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /** 5 | * Generates a unique ID based on the parameter length. 6 | * 7 | * @param length - The length of the unique ID (default: 10) 8 | * @returns The unique ID as a string 9 | */ 10 | export const generateUniqueId = (length = 10) => { 11 | const ALPHA_NUMERIC = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 12 | return Array.from({ length }, () => ALPHA_NUMERIC[Math.floor(Math.random() * ALPHA_NUMERIC.length)]).join(""); 13 | }; 14 | -------------------------------------------------------------------------------- /source/mcp-server/src/tools/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | export { handleGetBaselineTestRun } from "./get-baseline-test-run.js"; 5 | export { handleGetLatestTestRun } from "./get-latest-test-run.js"; 6 | export { handleGetScenarioDetails } from "./get-scenario-details.js"; 7 | export { handleGetTestRunArtifacts } from "./get-test-run-artifacts.js"; 8 | export { handleGetTestRun } from "./get-test-run.js"; 9 | export { handleListScenarios } from "./list-scenarios.js"; 10 | export { handleListTestRuns } from "./list-test-runs.js"; 11 | 12 | -------------------------------------------------------------------------------- /source/webui/index.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | Distributed Load Testing on AWS 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /source/infrastructure/bin/solution.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | export const SOLUTIONS_METRICS_ENDPOINT = "https://metrics.awssolutionsbuilder.com/generic"; 5 | 6 | export class Solution { 7 | public readonly id: string; 8 | public readonly name: string; 9 | public readonly version: string; 10 | public description: string; 11 | 12 | constructor(id: string, name: string, version: string, description: string) { 13 | this.id = id; 14 | this.name = name; 15 | this.version = version; 16 | this.description = description; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /source/mcp-server/vitest.config.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { defineConfig } from "vitest/config"; 5 | 6 | export default defineConfig({ 7 | test: { 8 | globals: true, 9 | environment: "node", 10 | coverage: { 11 | provider: "v8", 12 | reporter: ["text", "clover", "json", ["lcov", { projectRoot: "../../" }]], 13 | exclude: [ 14 | "node_modules/**", 15 | "test/**", 16 | "**/*.test.ts", 17 | "**/*.spec.ts", 18 | "dist/**", 19 | "coverage/**", 20 | ], 21 | }, 22 | }, 23 | }); 24 | -------------------------------------------------------------------------------- /source/integration-tests/cypress.config.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { defineConfig } from "cypress"; 5 | 6 | export default defineConfig({ 7 | e2e: { 8 | baseUrl: process.env.CONSOLE_URL, 9 | supportFile: "src/cypress/support/e2e.ts", 10 | specPattern: "src/cypress/e2e/*.cy.ts", 11 | viewportWidth: 1920, 12 | viewportHeight: 1080, 13 | defaultCommandTimeout: 10000, 14 | video: true, 15 | env: { 16 | USERNAME: process.env.USERNAME, 17 | PASSWORD: process.env.PASSWORD, 18 | }, 19 | testIsolation: false, // we want to use the same session across all tests 20 | }, 21 | }); 22 | -------------------------------------------------------------------------------- /source/webui/src/store/stackInfoApiSlice.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { ApiEndpoints, solutionApi } from "./solutionApi.ts"; 5 | 6 | interface StackInfo { 7 | created_time: string; 8 | region: string; 9 | version: string; 10 | mcp_endpoint: string | undefined; 11 | } 12 | 13 | export const stackInfoApiSlice = solutionApi.injectEndpoints({ 14 | endpoints: (builder) => ({ 15 | getStackInfo: builder.query({ 16 | query: () => ApiEndpoints.STACK_INFO, 17 | providesTags: [{ type: "StackInfo" }], 18 | }), 19 | }), 20 | }); 21 | 22 | export const { useGetStackInfoQuery } = stackInfoApiSlice; 23 | -------------------------------------------------------------------------------- /source/webui/src/utils/dateUtils.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /** 5 | * Formats a UTC date string to browser local time 6 | * @param utcDateString - UTC date string from backend (YYYY-MM-DD HH:MM:SS) 7 | * @param options - Intl.DateTimeFormatOptions for formatting 8 | * @returns Formatted date string in browser's local timezone 9 | */ 10 | export const formatToLocalTime = ( 11 | utcDateString?: string, 12 | options?: Intl.DateTimeFormatOptions 13 | ): string => { 14 | if (!utcDateString) return "-"; 15 | 16 | const date = new Date(utcDateString + 'Z'); 17 | return isNaN(date.getTime()) ? "-" : date.toLocaleString(undefined, options); 18 | }; -------------------------------------------------------------------------------- /source/integration-tests/src/cypress/support/commands.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import "@testing-library/cypress/add-commands"; 5 | 6 | Cypress.Commands.add("authenticate", (sessionName: string) => { 7 | cy.session( 8 | sessionName, 9 | () => { 10 | cy.visit("/"); 11 | cy.get('input[name="username"]').type(Cypress.env("USERNAME"), { log: false }); 12 | cy.get('input[name="password"]').type(Cypress.env("PASSWORD"), { log: false }); 13 | cy.get('button[type="submit"]').click(); 14 | }, 15 | { 16 | validate: () => { 17 | cy.contains("Distributed Load Testing").click(); 18 | }, 19 | } 20 | ); 21 | }); 22 | -------------------------------------------------------------------------------- /source/webui/src/styles.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | body, 7 | html { 8 | margin: 0; 9 | height: 100%; 10 | } 11 | .amplify-button--primary { 12 | background-color: #eb5f07; 13 | } 14 | 15 | .amplify-tabs-item[data-state="active"] { 16 | color: black; 17 | border-color: #eb5f07; 18 | } 19 | 20 | /* Keep a sticky top navigation bar at the top of the screen when scrolling down */ 21 | #top-nav { 22 | position: sticky; 23 | left: 0; 24 | top: 0; 25 | right: 0; 26 | z-index: 1000; 27 | } 28 | 29 | /* Align toggle component with other form controls */ 30 | .toggle-alignment { 31 | display: flex; 32 | align-items: center; 33 | height: 32px; 34 | } 35 | -------------------------------------------------------------------------------- /source/webui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["ESNext", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noFallthroughCasesInSwitch": true, 20 | "allowSyntheticDefaultImports": true 21 | }, 22 | "include": ["src", "node_modules/vitest/globals.d.ts"], 23 | "references": [ 24 | { 25 | "path": "./tsconfig.node.json" 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /source/solution-utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "solution-utils", 3 | "version": "4.0.2", 4 | "description": "Utilities package for Distributed Load Testing on AWS", 5 | "license": "Apache-2.0", 6 | "author": { 7 | "name": "Amazon Web Services", 8 | "url": "https://aws.amazon.com/solutions" 9 | }, 10 | "main": "utils", 11 | "scripts": { 12 | "clean": "rm -rf node_modules package-lock.json", 13 | "test": "jest --coverage --silent" 14 | }, 15 | "dependencies": { 16 | "nanoid": "^3.1.25", 17 | "axios": "^1.8.3" 18 | }, 19 | "devDependencies": { 20 | "jest": "29.7.0", 21 | "axios-mock-adapter": "1.19.0" 22 | }, 23 | "engines": { 24 | "node": "^20.x" 25 | }, 26 | "overrides": { 27 | "form-data": "4.0.4" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /source/metrics-utils/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "module": "commonjs", 5 | "lib": ["es2018"], 6 | "declaration": true, 7 | "strict": true, 8 | "noImplicitAny": true, 9 | "strictNullChecks": true, 10 | "noImplicitThis": true, 11 | "alwaysStrict": true, 12 | "noUnusedLocals": false, 13 | "noUnusedParameters": false, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": false, 16 | "inlineSourceMap": true, 17 | "inlineSources": true, 18 | "experimentalDecorators": true, 19 | "strictPropertyInitialization": false, 20 | "resolveJsonModule": true, 21 | "esModuleInterop": true, 22 | "typeRoots": ["./node_modules/@types"] 23 | }, 24 | "exclude": ["cdk.out"] 25 | } 26 | -------------------------------------------------------------------------------- /source/infrastructure/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "module": "commonjs", 5 | "lib": ["es2018"], 6 | "declaration": true, 7 | "strict": true, 8 | "noImplicitAny": true, 9 | "strictNullChecks": true, 10 | "noImplicitThis": true, 11 | "alwaysStrict": true, 12 | "noUnusedLocals": false, 13 | "noUnusedParameters": false, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": false, 16 | "inlineSourceMap": true, 17 | "inlineSources": true, 18 | "experimentalDecorators": true, 19 | "strictPropertyInitialization": false, 20 | "resolveJsonModule": true, 21 | "esModuleInterop": true, 22 | "typeRoots": ["./node_modules/@types"] 23 | }, 24 | "exclude": ["cdk.out"] 25 | } 26 | -------------------------------------------------------------------------------- /source/api-services/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "lib": ["ES2020"], 6 | "outDir": "./dist", 7 | "rootDir": "./", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "resolveJsonModule": true, 13 | "declaration": true, 14 | "declarationMap": true, 15 | "sourceMap": true, 16 | "moduleResolution": "node", 17 | "allowJs": true, 18 | "checkJs": false, 19 | "noEmit": false 20 | }, 21 | "include": [ 22 | "types/**/*", 23 | "lib/validation/**/*" 24 | ], 25 | "exclude": [ 26 | "node_modules", 27 | "dist", 28 | "coverage", 29 | "**/*.spec.ts", 30 | "**/*.spec.js" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /source/webui/src/store/types.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { EntityState } from "@reduxjs/toolkit"; 5 | 6 | /** 7 | * When using custom slices/thunks, we have to keep of the data loading status. 8 | * When using RTK Query instead, that's built in and we don't need the following types. 9 | */ 10 | export enum ApiDataStatus { 11 | IDLE = "IDLE", 12 | LOADING = "LOADING", 13 | SUCCEEDED = "SUCCEEDED", 14 | FAILED = "FAILED", 15 | } 16 | 17 | type StatusAndError = { 18 | status: ApiDataStatus; 19 | error: string | null; 20 | }; 21 | export type ApiDataState = EntityState & StatusAndError; 22 | 23 | export const DEFAULT_INITIAL_STATE: StatusAndError = { 24 | status: ApiDataStatus.IDLE, 25 | error: null, 26 | }; 27 | -------------------------------------------------------------------------------- /source/mcp-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mcp-server", 3 | "version": "4.0.0", 4 | "description": "MCP server for Distributed Load Testing on AWS", 5 | "main": "index.js", 6 | "directories": { 7 | "src": "src" 8 | }, 9 | "scripts": { 10 | "build": "tsc", 11 | "build:check": "tsc --noEmit", 12 | "test": "vitest run --coverage" 13 | }, 14 | "type": "module", 15 | "author": "", 16 | "license": "ISC", 17 | "dependencies": { 18 | "@aws-lambda-powertools/logger": "^2.27.0", 19 | "aws4": "^1.13.2", 20 | "zod": "^4.1.12" 21 | }, 22 | "devDependencies": { 23 | "@types/aws4": "^1.11.6", 24 | "@types/node": "^24.7.0", 25 | "@vitest/coverage-v8": "^4.0.8", 26 | "eslint": "^9.37.0", 27 | "prettier": "^3.6.2", 28 | "ts-node": "^10.9.2", 29 | "typescript": "^5.9.3", 30 | "vitest": "^4.0.8" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/aws-exports.json 2 | **/dist/ 3 | **/node_modules/ 4 | cdk.out 5 | coverage 6 | build/ 7 | dev/ 8 | global-s3-assets/ 9 | regional-s3-assets/ 10 | open-source/ 11 | deployment/ecr/**/*.jar 12 | .idea 13 | source/test/ 14 | **/__pycache__/ 15 | git-info 16 | .env 17 | build 18 | .pnp 19 | .pnp.js 20 | .DS_Store 21 | .env.local 22 | .env.development.local 23 | .env.test.local 24 | .env.production.local 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | yarn-lock.json 29 | script_deploy* 30 | cdk.context.json 31 | 32 | # cypress 33 | screenshots 34 | videos 35 | 36 | # solution metrics utils. 37 | source/metrics-utils/*.js 38 | !source/metrics-utils/jest.config.js 39 | source/metrics-utils/*.d.ts 40 | source/metrics-utils/**/*.d.ts 41 | source/metrics-utils/**/*.js 42 | source/metrics-utils/dist/ 43 | 44 | # webui public directory 45 | source/webui/public/ 46 | -------------------------------------------------------------------------------- /source/webui/src/pages/scenarios/ScenariosPage.tsx: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { useGetScenariosQuery } from "../../store/scenariosApiSlice.ts"; 5 | import { Alert, StatusIndicator } from "@cloudscape-design/components"; 6 | import ScenariosContent from "./ScenariosContent.tsx"; 7 | 8 | export default function ScenariosPage() { 9 | const { data, isLoading, error } = useGetScenariosQuery(); 10 | const scenariosArray = data?.Items || []; 11 | const scenariosContent = ; 12 | 13 | if (isLoading) { 14 | return Loading; 15 | } 16 | 17 | if (error) { 18 | return Failed to load test scenarios; 19 | } 20 | 21 | return scenariosContent; 22 | } 23 | -------------------------------------------------------------------------------- /source/integration-tests/api.config.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | export interface ApiConfig { 5 | readonly apiUrl: string; 6 | readonly accessKeyId: string; 7 | readonly secretAccessKey: string; 8 | readonly sessionToken: string; 9 | readonly region: string; 10 | readonly s3ScenarioBucket: string; 11 | } 12 | 13 | /** 14 | * Load the config from the environment 15 | * 16 | * @returns {ApiConfig} - environment config 17 | */ 18 | export function load(): ApiConfig { 19 | return { 20 | apiUrl: process.env.API_URL, 21 | accessKeyId: process.env.AWS_ACCESS_KEY_ID, 22 | secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, 23 | sessionToken: process.env.AWS_SESSION_TOKEN, 24 | region: process.env.AWS_REGION, 25 | s3ScenarioBucket: process.env.S3_SCENARIO_BUCKET, 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /source/metric-filter-cleaner/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "metric-filter-cleaner", 3 | "version": "4.0.0", 4 | "description": "Cleans up CloudWatch metric filters for completed/failed tests", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/aws-solutions/distributed-load-testing-on-aws" 8 | }, 9 | "license": "Apache-2.0", 10 | "author": { 11 | "name": "Amazon Web Services", 12 | "url": "https://aws.amazon.com/solutions" 13 | }, 14 | "main": "index.js", 15 | "scripts": { 16 | "clean": "rm -rf node_modules package-lock.json", 17 | "test": "jest lib/*.spec.js --coverage --silent" 18 | }, 19 | "dependencies": { 20 | "@aws-sdk/client-cloudwatch": "^3.758.0", 21 | "@aws-sdk/client-cloudwatch-logs": "^3.758.0" 22 | }, 23 | "devDependencies": { 24 | "jest": "29.7.0" 25 | }, 26 | "engines": { 27 | "node": "^20.x" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /source/api-services/jest.config.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | process.env.TZ = "UTC"; 5 | module.exports = { 6 | roots: ["/lib", ""], 7 | testMatch: ["**/*.spec.js", "**/*.spec.ts"], 8 | collectCoverageFrom: [ 9 | "**/*.js", 10 | "**/*.ts", 11 | "!**/*.d.ts", 12 | "!**/node_modules/**", 13 | "!**/coverage/**" 14 | ], 15 | coverageReporters: ["text", "clover", "json", ["lcov", { projectRoot: "../../" }]], 16 | coveragePathIgnorePatterns: ["/node_modules/", "/coverage/", "jest.config.js"], 17 | coverageThreshold: { 18 | global: { 19 | branches: 81, 20 | functions: 81, 21 | lines: 81, 22 | statements: 81 23 | } 24 | }, 25 | transform: { 26 | "^.+\\.(ts|tsx)$": ["babel-jest", { presets: ["@babel/preset-typescript"] }] 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /source/integration-tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Node 18", 4 | "compilerOptions": { 5 | "allowJs": true, 6 | "module": "commonjs", 7 | "outDir": "./dist", 8 | "sourceMap": false, 9 | "declaration": true, 10 | "removeComments": true, 11 | "emitDecoratorMetadata": true, 12 | "experimentalDecorators": true, 13 | "allowSyntheticDefaultImports": true, 14 | "target": "ES2022", 15 | "incremental": true, 16 | "skipLibCheck": true, 17 | "strictNullChecks": false, 18 | "noImplicitAny": false, 19 | "strictBindCallApply": false, 20 | "forceConsistentCasingInFileNames": false, 21 | "noFallthroughCasesInSwitch": false, 22 | "types": ["jest", "node", "@types/jest", "cypress", "@testing-library/cypress"] 23 | }, 24 | "exclude": ["node_modules", "dist"], 25 | "include": ["src/**/*"], 26 | "references": [] 27 | } 28 | -------------------------------------------------------------------------------- /source/webui/src/store/userThunks.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { createAsyncThunk } from "@reduxjs/toolkit"; 5 | import { get } from "aws-amplify/api"; 6 | import { ApiEndpoints } from "./solutionApi.ts"; 7 | import { addNotification } from "./notificationsSlice.ts"; 8 | import { v4 } from "uuid"; 9 | 10 | export const fetchUser = createAsyncThunk("user/fetchUser", async (_, thunkAPI): Promise => { 11 | try { 12 | const response = await get({ 13 | apiName: "solution-api", 14 | path: ApiEndpoints.USER, 15 | }).response; 16 | 17 | return await response.body.json(); 18 | } catch { 19 | thunkAPI.dispatch( 20 | addNotification({ 21 | id: v4(), 22 | content: "Failed to load user data", 23 | type: "error", 24 | }) 25 | ); 26 | return Promise.reject(new Error("Failed to load user data")); 27 | } 28 | }); 29 | -------------------------------------------------------------------------------- /source/real-time-data-publisher/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "real-time-data-publisher", 3 | "version": "4.0.2", 4 | "description": "Publishes real time test data to an IoT endpoint", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/aws-solutions/distributed-load-testing-on-aws" 8 | }, 9 | "license": "Apache-2.0", 10 | "author": { 11 | "name": "Amazon Web Services", 12 | "url": "https://aws.amazon.com/solutions" 13 | }, 14 | "main": "index.js", 15 | "scripts": { 16 | "clean": "rm -rf node_modules package-lock.json", 17 | "test": "jest --coverage --silent" 18 | }, 19 | "dependencies": { 20 | "@aws-sdk/client-iot-data-plane": "^3.758.0", 21 | "solution-utils": "file:../solution-utils" 22 | }, 23 | "devDependencies": { 24 | "aws-sdk-client-mock": "^4.1.0", 25 | "aws-sdk-client-mock-jest": "^4.1.0", 26 | "jest": "29.7.0" 27 | }, 28 | "engines": { 29 | "node": "^20.x" 30 | }, 31 | "readme": "./README.md" 32 | } 33 | -------------------------------------------------------------------------------- /source/task-status-checker/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "task-status-checker", 3 | "version": "4.0.2", 4 | "description": "checks if tasks are running or not", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/aws-solutions/distributed-load-testing-on-aws" 8 | }, 9 | "license": "Apache-2.0", 10 | "author": { 11 | "name": "Amazon Web Services", 12 | "url": "https://aws.amazon.com/solutions" 13 | }, 14 | "main": "index.js", 15 | "scripts": { 16 | "clean": "rm -rf node_modules package-lock.json", 17 | "test": "jest lib/*.spec.js --coverage --silent" 18 | }, 19 | "dependencies": { 20 | "@aws-sdk/client-dynamodb": "^3.758.0", 21 | "@aws-sdk/client-ecs": "^3.758.0", 22 | "@aws-sdk/client-lambda": "^3.758.0", 23 | "@aws-sdk/lib-dynamodb": "3.786.0", 24 | "solution-utils": "file:../solution-utils" 25 | }, 26 | "devDependencies": { 27 | "jest": "29.7.0" 28 | }, 29 | "engines": { 30 | "node": "^20.x" 31 | }, 32 | "readme": "./README.md" 33 | } 34 | -------------------------------------------------------------------------------- /source/metrics-utils/lambda/helpers/client-helper.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { CloudWatchClient } from "@aws-sdk/client-cloudwatch"; 5 | import { SQSClient } from "@aws-sdk/client-sqs"; 6 | import { CloudWatchLogsClient } from "@aws-sdk/client-cloudwatch-logs"; 7 | 8 | export class ClientHelper { 9 | private sqsClient: SQSClient; 10 | private cwClient: CloudWatchClient; 11 | private cwLogsClient: CloudWatchLogsClient; 12 | 13 | getSqsClient(): SQSClient { 14 | if (!this.sqsClient) { 15 | this.sqsClient = new SQSClient(); 16 | } 17 | return this.sqsClient; 18 | } 19 | 20 | getCwClient(): CloudWatchClient { 21 | if (!this.cwClient) { 22 | this.cwClient = new CloudWatchClient(); 23 | } 24 | return this.cwClient; 25 | } 26 | 27 | getCwLogsClient(): CloudWatchLogsClient { 28 | if (!this.cwLogsClient) { 29 | this.cwLogsClient = new CloudWatchLogsClient(); 30 | } 31 | return this.cwLogsClient; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /source/webui/src/utils/__tests__/jsonValidator.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { describe, it, expect } from "vitest"; 5 | import { isValidJSON } from "../jsonValidator"; 6 | 7 | describe("isValidJSON", () => { 8 | it("should return true for empty string", () => { 9 | expect(isValidJSON("")).toBe(true); 10 | }); 11 | 12 | it("should return true for whitespace-only string", () => { 13 | expect(isValidJSON(" ")).toBe(true); 14 | }); 15 | 16 | it("should return true for valid JSON object", () => { 17 | expect(isValidJSON('{"key": "value"}')).toBe(true); 18 | }); 19 | 20 | it("should return true for valid JSON array", () => { 21 | expect(isValidJSON("[1, 2, 3]")).toBe(true); 22 | }); 23 | 24 | it("should return false for invalid JSON", () => { 25 | expect(isValidJSON('{"key": value}')).toBe(false); 26 | }); 27 | 28 | it("should return false for malformed JSON", () => { 29 | expect(isValidJSON('{"key":')).toBe(false); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /source/task-canceler/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "task-canceler", 3 | "version": "4.0.2", 4 | "description": "Triggered by api-services lambda function, cancels ecs tasks", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/aws-solutions/distributed-load-testing-on-aws" 8 | }, 9 | "license": "Apache-2.0", 10 | "author": { 11 | "name": "Amazon Web Services", 12 | "url": "https://aws.amazon.com/solutions" 13 | }, 14 | "main": "index.js", 15 | "scripts": { 16 | "clean": "rm -rf node_modules package-lock.json", 17 | "test": "jest lib/*.spec.js --coverage --silent" 18 | }, 19 | "dependencies": { 20 | "@aws-sdk/client-cloudwatch": "^3.758.0", 21 | "@aws-sdk/client-cloudwatch-logs": "^3.758.0", 22 | "@aws-sdk/client-ecs": "^3.758.0", 23 | "@aws-sdk/client-dynamodb": "^3.758.0", 24 | "@aws-sdk/lib-dynamodb": "^3.758.0", 25 | "solution-utils": "file:../solution-utils" 26 | }, 27 | "devDependencies": { 28 | "jest": "29.7.0" 29 | }, 30 | "engines": { 31 | "node": "^20.x" 32 | }, 33 | "readme": "./README.md" 34 | } 35 | -------------------------------------------------------------------------------- /deployment/ecr/distributed-load-testing-on-aws-load-tester/.bzt-rc: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | modules: 5 | jmeter: 6 | path: ~/.bzt/jmeter-taurus/{version}/bin/jmeter 7 | download-link: https://archive.apache.org/dist/jmeter/binaries/apache-jmeter-{version}.zip 8 | version: 5.6.3 9 | force-ctg: true 10 | detect-plugins: true 11 | fix-jars: true 12 | plugins: 13 | - jpgc-json 14 | - jmeter-ftp 15 | - jpgc-casutg 16 | - jpgc-dummy 17 | - jpgc-ffw 18 | - jpgc-fifo 19 | - jpgc-functions 20 | - jpgc-perfmon 21 | - jpgc-prmctl 22 | - jpgc-tst 23 | - jpgc-udp 24 | - bzm-http2 25 | - bzm-random-csv 26 | plugins-manager: 27 | download-link: https://search.maven.org/remotecontent?filepath=kg/apc/jmeter-plugins-manager/{version}/jmeter-plugins-manager-{version}.jar 28 | version: '1.11' 29 | command-runner: 30 | download-link: https://search.maven.org/remotecontent?filepath=kg/apc/cmdrunner/{version}/cmdrunner-{version}.jar 31 | version: 2.3 -------------------------------------------------------------------------------- /source/webui/src/store/store.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { combineReducers, configureStore } from "@reduxjs/toolkit"; 5 | import { solutionApi } from "./solutionApi.ts"; 6 | import { notificationsSlice } from "./notificationsSlice.ts"; 7 | import { regionsSlice } from "./regionsSlice.ts"; 8 | import { setupListeners } from "@reduxjs/toolkit/query"; 9 | 10 | export const rootReducer = combineReducers({ 11 | [solutionApi.reducerPath]: solutionApi.reducer, 12 | notifications: notificationsSlice.reducer, 13 | regions: regionsSlice.reducer, 14 | }); 15 | 16 | // Infer the `RootState` types from the store itself 17 | export type RootState = ReturnType; 18 | 19 | export const setupStore = (preloadedState?: Partial) => { 20 | const store = configureStore({ 21 | reducer: rootReducer, 22 | preloadedState, 23 | middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(solutionApi.middleware), 24 | }); 25 | setupListeners(store.dispatch); 26 | return store; 27 | }; 28 | -------------------------------------------------------------------------------- /source/infrastructure/test/auth.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { App, DefaultStackSynthesizer, Stack, CfnCondition, Aws, Fn } from "aws-cdk-lib"; 5 | 6 | import { CognitoAuthConstruct } from "../lib/front-end/auth"; 7 | import { createTemplateWithoutS3Key } from "./snapshot_helpers"; 8 | 9 | test("DLT API Test", () => { 10 | const app = new App(); 11 | const stack = new Stack(app, "DLTStack", { 12 | synthesizer: new DefaultStackSynthesizer({ 13 | generateBootstrapVersionRule: false, 14 | }), 15 | }); 16 | 17 | const auth = new CognitoAuthConstruct(stack, "TestAuth", { 18 | adminEmail: "email", 19 | adminName: "testname", 20 | apiId: "apiId12345", 21 | webAppURL: "test.com", 22 | scenariosBucketArn: "arn:aws:s3:::DOC-EXAMPLE-BUCKET", 23 | }); 24 | 25 | expect(createTemplateWithoutS3Key(stack)).toMatchSnapshot(); 26 | expect(auth.cognitoIdentityPoolId).toBeDefined(); 27 | expect(auth.cognitoUserPoolClientId).toBeDefined(); 28 | expect(auth.cognitoUserPoolId).toBeDefined(); 29 | }); 30 | -------------------------------------------------------------------------------- /source/infrastructure/test/console.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { App, DefaultStackSynthesizer, Stack, CfnCondition, Aws, Fn } from "aws-cdk-lib"; 5 | import { DLTConsoleConstruct } from "../lib/front-end/console"; 6 | import { Bucket } from "aws-cdk-lib/aws-s3"; 7 | import { createTemplateWithoutS3Key } from "./snapshot_helpers"; 8 | 9 | test("DLT API Test", () => { 10 | const app = new App(); 11 | const stack = new Stack(app, "DLTStack", { 12 | synthesizer: new DefaultStackSynthesizer({ 13 | generateBootstrapVersionRule: false, 14 | }), 15 | }); 16 | const testSourceBucket = new Bucket(stack, "testSourceCodeBucket"); 17 | 18 | const console = new DLTConsoleConstruct(stack, "TestConsoleResources", { 19 | s3LogsBucket: testSourceBucket, 20 | solutionId: "SO0062", 21 | }); 22 | 23 | expect(createTemplateWithoutS3Key(stack)).toMatchSnapshot(); 24 | expect(console.webAppURL).toBeDefined(); 25 | expect(console.consoleBucket).toBeDefined(); 26 | expect(console.consoleBucketArn).toBeDefined(); 27 | }); 28 | -------------------------------------------------------------------------------- /source/task-runner/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "task-runner", 3 | "version": "4.0.2", 4 | "description": "Triggered by Step Functions, runs ecs task Definitions", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/aws-solutions/distributed-load-testing-on-aws" 8 | }, 9 | "license": "Apache-2.0", 10 | "author": { 11 | "name": "Amazon Web Services", 12 | "url": "https://aws.amazon.com/solutions" 13 | }, 14 | "main": "index.js", 15 | "scripts": { 16 | "clean": "rm -rf node_modules package-lock.json", 17 | "test": "jest lib/*.spec.js --coverage --silent" 18 | }, 19 | "dependencies": { 20 | "false": "^0.0.4", 21 | "@aws-sdk/client-ecs": "^3.758.0", 22 | "@aws-sdk/client-dynamodb": "^3.758.0", 23 | "@aws-sdk/client-s3": "^3.758.0", 24 | "@aws-sdk/client-cloudwatch": "^3.758.0", 25 | "@aws-sdk/client-cloudwatch-logs": "^3.758.0", 26 | "@aws-sdk/lib-dynamodb": "3.786.0", 27 | "solution-utils": "file:../solution-utils" 28 | }, 29 | "devDependencies": { 30 | "jest": "29.7.0" 31 | }, 32 | "engines": { 33 | "node": "^20.x" 34 | }, 35 | "readme": "./README.md" 36 | } 37 | -------------------------------------------------------------------------------- /source/mcp-server/test/lib/errors.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { describe, expect, it } from "vitest"; 5 | import { AppError } from "../../src/lib/errors.js"; 6 | 7 | describe("AppError", () => { 8 | it("should create an AppError with message and code", () => { 9 | const error = new AppError("Test error", 400); 10 | 11 | expect(error).toBeInstanceOf(Error); 12 | expect(error).toBeInstanceOf(AppError); 13 | expect(error.message).toBe("Test error"); 14 | expect(error.code).toBe(400); 15 | expect(error.name).toBe("AppError"); 16 | }); 17 | 18 | it("should handle different status codes", () => { 19 | const error404 = new AppError("Not found", 404); 20 | const error500 = new AppError("Internal error", 500); 21 | 22 | expect(error404.code).toBe(404); 23 | expect(error500.code).toBe(500); 24 | }); 25 | 26 | it("should preserve error stack trace", () => { 27 | const error = new AppError("Test error", 400); 28 | 29 | expect(error.stack).toBeDefined(); 30 | expect(error.stack).toContain("AppError"); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | 12 | 13 | **To Reproduce** 14 | 15 | 16 | **Expected behavior** 17 | 18 | 19 | **Please complete the following information about the solution:** 20 | - [ ] Version: [e.g. v1.1.0] 21 | - [ ] Region: [e.g. us-east-1] 22 | - [ ] Was the solution modified from the version published on this repository? 23 | - [ ] If the answer to the previous question was yes, are the changes available on GitHub? 24 | - [ ] Have you checked your [service quotas](https://docs.aws.amazon.com/general/latest/gr/aws_service_limits.html) for the services this solution uses? 25 | - [ ] Were there any errors in the CloudWatch Logs? 26 | 27 | **Screenshots** 28 | 29 | 30 | **Additional context** 31 | 32 | -------------------------------------------------------------------------------- /source/custom-resource/lib/cfn/index.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | const axios = require("axios"); 5 | 6 | const send = async (event, context, responseStatus, responseData, physicalResourceId) => { 7 | try { 8 | const responseBody = JSON.stringify({ 9 | Status: responseStatus, 10 | Reason: "See the details in CloudWatch Log Stream: " + context.logStreamName, 11 | PhysicalResourceId: physicalResourceId || context.logStreamName, 12 | StackId: event.StackId, 13 | RequestId: event.RequestId, 14 | LogicalResourceId: event.LogicalResourceId, 15 | Data: responseData, 16 | }); 17 | const params = { 18 | url: event.ResponseURL, 19 | port: 443, 20 | method: "put", 21 | headers: { 22 | "content-type": "", 23 | "content-length": responseBody.length, 24 | }, 25 | data: responseBody, 26 | }; 27 | await axios(params); 28 | } catch (err) { 29 | console.error(`There was an error sending the response to CloudFormation: ${err}`); 30 | throw err; 31 | } 32 | }; 33 | 34 | module.exports = { 35 | send: send, 36 | }; 37 | -------------------------------------------------------------------------------- /source/infrastructure/test/distributed-load-testing-on-aws-regional-stack.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { App, DefaultStackSynthesizer } from "aws-cdk-lib"; 5 | import { RegionalInfrastructureDLTStack } from "../lib/distributed-load-testing-on-aws-regional-stack"; 6 | import { Solution } from "../bin/solution"; 7 | import { createTemplateWithoutS3Key } from "./snapshot_helpers"; 8 | 9 | test("Distributed Load Testing Regional stack test", () => { 10 | const app = new App(); 11 | const solution = new Solution("testId", "DLT", "testVersion", "mainStackDescription"); 12 | process.env.PUBLIC_ECR_REGISTRY = "registry"; 13 | process.env.PUBLIC_ECR_TAG = "tag"; 14 | const stack = new RegionalInfrastructureDLTStack(app, "TestDLTRegionalStack", { 15 | synthesizer: new DefaultStackSynthesizer({ 16 | generateBootstrapVersionRule: false, 17 | imageAssetsRepositoryName: process.env.PUBLIC_ECR_REGISTRY, 18 | dockerTagPrefix: process.env.PUBLIC_ECR_TAG, 19 | }), 20 | solution, 21 | stackType: "regional", 22 | }); 23 | 24 | expect(createTemplateWithoutS3Key(stack)).toMatchSnapshot(); 25 | }); 26 | -------------------------------------------------------------------------------- /source/custom-resource/lib/metrics/index.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | const axios = require("axios"); 5 | 6 | const send = async (config, type) => { 7 | try { 8 | const metrics = { 9 | Solution: config.SolutionId, 10 | Version: config.Version, 11 | UUID: config.UUID, 12 | // Date and time instant in a java.sql.Timestamp compatible format 13 | TimeStamp: new Date().toISOString().replace("T", " ").replace("Z", ""), 14 | Data: { 15 | Type: type, 16 | Region: config.Region, 17 | ExistingVpc: config.existingVPC, 18 | AccountId: config.AccountId, 19 | }, 20 | }; 21 | const params = { 22 | method: "post", 23 | port: 443, 24 | url: process.env.METRIC_URL, 25 | headers: { 26 | "Content-Type": "application/json", 27 | }, 28 | data: metrics, 29 | }; 30 | //Send Metrics & return status code. 31 | await axios(params); 32 | } catch (err) { 33 | //Not returning an error to avoid Metrics affecting the Application 34 | console.error(err); 35 | } 36 | }; 37 | 38 | module.exports = { 39 | send: send, 40 | }; 41 | -------------------------------------------------------------------------------- /source/custom-resource/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "custom-resource", 3 | "version": "4.0.2", 4 | "description": "cfn custom resources for distributed load testing on AWS workflow", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/aws-solutions/distributed-load-testing-on-aws" 8 | }, 9 | "license": "Apache-2.0", 10 | "author": { 11 | "name": "Amazon Web Services", 12 | "url": "https://aws.amazon.com/solutions" 13 | }, 14 | "main": "index.js", 15 | "scripts": { 16 | "clean": "rm -rf node_modules package-lock.json", 17 | "test": "jest lib/**/*.spec.js --coverage --silent" 18 | }, 19 | "dependencies": { 20 | "axios": "^1.8.3", 21 | "js-yaml": "^4.1.0", 22 | "solution-utils": "file:../solution-utils", 23 | "@aws-sdk/client-dynamodb": "^3.758.0", 24 | "@aws-sdk/client-iot": "^3.758.0", 25 | "@aws-sdk/client-s3": "^3.758.0", 26 | "@aws-sdk/lib-dynamodb": "3.786.0", 27 | "uuid": "^8.3.1" 28 | }, 29 | "devDependencies": { 30 | "axios-mock-adapter": "1.19.0", 31 | "jest": "^29.7.0" 32 | }, 33 | "engines": { 34 | "node": "^20.x" 35 | }, 36 | "readme": "./README.md", 37 | "overrides": { 38 | "form-data": "4.0.4" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /source/results-parser/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "results-parser", 3 | "version": "4.0.2", 4 | "description": "result parser for indexing xml test results to DynamoDB", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/aws-solutions/distributed-load-testing-on-aws" 8 | }, 9 | "license": "Apache-2.0", 10 | "author": { 11 | "name": "Amazon Web Services", 12 | "url": "https://aws.amazon.com/solutions" 13 | }, 14 | "main": "index.js", 15 | "scripts": { 16 | "clean": "rm -rf node_modules package-lock.json", 17 | "test": "jest --coverage --silent" 18 | }, 19 | "dependencies": { 20 | "axios": "^1.8.3", 21 | "solution-utils": "file:../solution-utils", 22 | "xml-js": "^1.6.11", 23 | "@aws-sdk/lib-dynamodb": "^3.758.0", 24 | "@aws-sdk/client-dynamodb": "^3.758.0", 25 | "@aws-sdk/client-s3": "^3.758.0", 26 | "@aws-sdk/client-cloudwatch": "^3.758.0", 27 | "@aws-sdk/client-cloudwatch-logs": "^3.758.0" 28 | }, 29 | "devDependencies": { 30 | "axios-mock-adapter": "1.19.0", 31 | "jest": "29.7.0" 32 | }, 33 | "engines": { 34 | "node": "^20.x" 35 | }, 36 | "readme": "./README.md", 37 | "overrides": { 38 | "form-data": "4.0.4" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /source/infrastructure/test/distributed-load-testing-on-aws-stack.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { App, DefaultStackSynthesizer } from "aws-cdk-lib"; 5 | import { DLTStack } from "../lib/distributed-load-testing-on-aws-stack"; 6 | import { Solution } from "../bin/solution"; 7 | import { createTemplateWithoutS3Key } from "./snapshot_helpers"; 8 | 9 | test("Distributed Load Testing stack test", () => { 10 | const app = new App(); 11 | const solution = new Solution("testId", "DLT", "testVersion", "mainStackDescription"); 12 | process.env.PUBLIC_ECR_REGISTRY = "registry"; 13 | process.env.PUBLIC_ECR_TAG = "tag"; 14 | process.env.DIST_OUTPUT_BUCKET = "codeBucket"; 15 | process.env.SOLUTION_NAME = "DLT"; 16 | process.env.VERSION = "Version"; 17 | const stack = new DLTStack(app, "TestDLTStack", { 18 | synthesizer: new DefaultStackSynthesizer({ 19 | generateBootstrapVersionRule: false, 20 | imageAssetsRepositoryName: process.env.PUBLIC_ECR_REGISTRY, 21 | dockerTagPrefix: process.env.PUBLIC_ECR_TAG, 22 | }), 23 | solution, 24 | stackType: "main", 25 | }); 26 | expect(createTemplateWithoutS3Key(stack)).toMatchSnapshot(); 27 | }); 28 | -------------------------------------------------------------------------------- /source/webui/src/__tests__/pages/ScenarioDetailsPage.test.tsx: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { screen, waitFor } from "@testing-library/react"; 5 | import { renderAppContent } from "../test-utils"; 6 | 7 | describe("ScenarioDetailsPage", () => { 8 | it("shows loading spinner initially", () => { 9 | renderAppContent({ initialRoute: "/scenarios/Ic4PBihoJY" }); 10 | expect(screen.getByText("Loading")).toBeInTheDocument(); 11 | }); 12 | 13 | it("displays scenario details after loading", async () => { 14 | renderAppContent({ initialRoute: "/scenarios/Ic4PBihoJY" }); 15 | 16 | await waitFor(() => { 17 | const testNameElements = screen.getAllByText(/testname01/); 18 | expect(testNameElements.length).toBeGreaterThan(0); 19 | }); 20 | 21 | expect(screen.getByText(/Status/)).toBeInTheDocument(); 22 | }); 23 | 24 | it("displays testID in the header of the first container in Scenario Details tab", async () => { 25 | renderAppContent({ initialRoute: "/scenarios/Ic4PBihoJY" }); 26 | 27 | await waitFor(() => { 28 | const testIdElements = screen.getAllByText(/Ic4PBihoJY/); 29 | expect(testIdElements[0]).toBeInTheDocument(); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /source/custom-resource/lib/iot/index.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | const { IoT } = require("@aws-sdk/client-iot"); 5 | 6 | const utils = require("solution-utils"); 7 | 8 | let options = utils.getOptions({ region: process.env.MAIN_REGION }); 9 | const iot = new IoT(options); 10 | 11 | /** 12 | * Get the IoT endpoint 13 | */ 14 | const getIotEndpoint = async () => { 15 | let params = { 16 | endpointType: "iot:Data-ATS", 17 | }; 18 | const data = await iot.describeEndpoint(params); 19 | return data.endpointAddress; 20 | }; 21 | 22 | /** 23 | * Detach IoT policy on CloudFormation DELETE. 24 | */ 25 | const detachIotPolicy = async (iotPolicyName) => { 26 | const response = await iot.listTargetsForPolicy({ policyName: iotPolicyName }); 27 | const targets = response.targets; 28 | 29 | for (let target of targets) { 30 | const params = { 31 | policyName: iotPolicyName, 32 | principal: target, 33 | }; 34 | 35 | await iot.detachPrincipalPolicy(params); 36 | console.log(`${target} is detached from ${iotPolicyName}`); 37 | } 38 | 39 | return "success"; 40 | }; 41 | 42 | module.exports = { 43 | getIotEndpoint: getIotEndpoint, 44 | detachIotPolicy: detachIotPolicy, 45 | }; 46 | -------------------------------------------------------------------------------- /source/infrastructure/lib/common-resources/add-cfn-guard-suppression.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { CfnResource } from "aws-cdk-lib"; 5 | import { IConstruct } from "constructs"; 6 | 7 | /** 8 | * Adds a CFN Guard suppression to a resource. 9 | * 10 | * @param {IConstruct} resource - The resource to add suppression to 11 | * @param {string} suppression - The suppression rule to add 12 | */ 13 | export function addCfnGuardSuppression(resource: IConstruct, suppression: string): void { 14 | const cfnResource = resource.node.defaultChild as CfnResource; 15 | if (!cfnResource?.cfnOptions) { 16 | throw new Error(`Resource ${cfnResource?.logicalId} has no cfnOptions, unable to add CfnGuard suppression`); 17 | } 18 | const existingSuppressions: string[] = cfnResource.cfnOptions.metadata?.guard?.SuppressedRules; 19 | if (existingSuppressions) { 20 | existingSuppressions.push(suppression); 21 | } else if (cfnResource.cfnOptions.metadata) { 22 | cfnResource.cfnOptions.metadata.guard = { 23 | SuppressedRules: [suppression], 24 | }; 25 | } else { 26 | cfnResource.cfnOptions.metadata = { 27 | guard: { 28 | SuppressedRules: [suppression], 29 | }, 30 | }; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /source/webui/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import "@testing-library/jest-dom"; 5 | import { Amplify } from "aws-amplify"; 6 | import { afterAll, afterEach, beforeAll } from "vitest"; 7 | import { MOCK_SERVER_URL, server } from "./__tests__/server"; 8 | 9 | // The MSW (Mock Service Worker) can't intercept Amplify API calls with the native fetch. The following statement loads web APIs from undici in order to enable MSW to intercept Amplify API calls. 10 | import { fetch, Headers, Request, Response } from 'undici'; 11 | 12 | Object.assign(globalThis, { 13 | fetch, 14 | Headers, 15 | Request, 16 | Response, 17 | }); 18 | 19 | process.env.TZ = "UTC"; // fix environment timezone for tests to UTC 20 | 21 | beforeAll(() => { 22 | // Start MSW server before configuring Amplify 23 | server.listen({ onUnhandledRequest: "warn" }); 24 | 25 | Amplify.configure({ 26 | Auth: { 27 | Cognito: { 28 | userPoolId: "", 29 | userPoolClientId: "", 30 | }, 31 | }, 32 | API: { 33 | REST: { 34 | "solution-api": { 35 | endpoint: MOCK_SERVER_URL, 36 | }, 37 | }, 38 | }, 39 | }); 40 | }); 41 | afterAll(() => server.close()); 42 | afterEach(() => server.resetHandlers()); 43 | -------------------------------------------------------------------------------- /source/mcp-server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // Visit https://aka.ms/tsconfig to read more about this file 3 | "include": ["src/**/*"], 4 | "exclude": ["node_modules", "dist", "test"], 5 | "compilerOptions": { 6 | // File Layout 7 | "rootDir": "./src", 8 | "outDir": "./dist", 9 | 10 | // Environment Settings 11 | // See also https://aka.ms/tsconfig/module 12 | // "module": "nodenext", 13 | "moduleResolution": "bundler", 14 | "target": "esnext", 15 | "lib": ["esnext"], 16 | "types": ["node"], 17 | 18 | // Other Outputs 19 | "sourceMap": true, 20 | "declaration": true, 21 | "declarationMap": true, 22 | 23 | // Stricter Typechecking Options 24 | "noUncheckedIndexedAccess": true, 25 | "exactOptionalPropertyTypes": true, 26 | 27 | // Style Options 28 | "noImplicitReturns": true, 29 | "noImplicitOverride": true, 30 | "noUnusedLocals": true, 31 | "noUnusedParameters": true, 32 | "noFallthroughCasesInSwitch": true, 33 | "noPropertyAccessFromIndexSignature": true, 34 | 35 | // Recommended Options 36 | "strict": true, 37 | "jsx": "react-jsx", 38 | "verbatimModuleSyntax": true, 39 | "isolatedModules": true, 40 | "noUncheckedSideEffectImports": true, 41 | "moduleDetection": "force", 42 | "skipLibCheck": true, 43 | "esModuleInterop": true, 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /source/infrastructure/test/scenarios-storage.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { App, DefaultStackSynthesizer, Stack } from "aws-cdk-lib"; 5 | import { ScenarioTestRunnerStorageConstruct } from "../lib/back-end/scenarios-storage"; 6 | import { Bucket } from "aws-cdk-lib/aws-s3"; 7 | import { createTemplateWithoutS3Key } from "./snapshot_helpers"; 8 | 9 | test("DLT API Test", () => { 10 | const app = new App(); 11 | const stack = new Stack(app, "DLTStack", { 12 | synthesizer: new DefaultStackSynthesizer({ 13 | generateBootstrapVersionRule: false, 14 | }), 15 | }); 16 | const testLogsBucket = new Bucket(stack, "testLogsBucket"); 17 | 18 | const storage = new ScenarioTestRunnerStorageConstruct(stack, "TestScenarioStorage", { 19 | s3LogsBucket: testLogsBucket, 20 | webAppURL: "test.exampledomain.com", 21 | solutionId: "testId", 22 | }); 23 | 24 | expect(createTemplateWithoutS3Key(stack)).toMatchSnapshot(); 25 | expect(storage.scenariosBucket).toBeDefined(); 26 | expect(storage.scenariosS3Policy).toBeDefined(); 27 | expect(storage.scenariosTable).toBeDefined(); 28 | expect(storage.historyTable).toBeDefined(); 29 | expect(storage.scenarioDynamoDbPolicy).toBeDefined(); 30 | expect(storage.historyDynamoDbPolicy).toBeDefined(); 31 | }); 32 | -------------------------------------------------------------------------------- /source/mcp-server/src/tools/list-scenarios.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { z } from "zod"; 5 | import { parseEventWithSchema, type AgentCoreEvent } from "../lib/common"; 6 | import { AppError } from "../lib/errors"; 7 | import { IAMHttpClient, type HttpResponse } from "../lib/http-client"; 8 | 9 | // Zod schema for list_scenarios parameters (empty object) 10 | export const ListScenariosSchema = z.object({}); 11 | 12 | // TypeScript type derived from Zod schema 13 | export type ListScenariosParameters = z.infer; 14 | 15 | /** 16 | * Handle list_scenarios tool 17 | */ 18 | export async function handleListScenarios(httpClient: IAMHttpClient, apiEndpoint: string, event: AgentCoreEvent): Promise { 19 | parseEventWithSchema(ListScenariosSchema, event); 20 | 21 | let response: HttpResponse; 22 | try { 23 | response = await httpClient.get(`${apiEndpoint}/scenarios`); 24 | } catch (error) { 25 | throw new AppError("Internal request failed", 500); 26 | } 27 | 28 | if (response.statusCode !== 200) { 29 | throw new AppError(response.body, response.statusCode); 30 | } 31 | 32 | const data = JSON.parse(response.body); 33 | if (!data) { 34 | throw new AppError("No scenarios found", 404); 35 | } 36 | 37 | return data; 38 | } 39 | -------------------------------------------------------------------------------- /source/integration-tests/src/cypress/e2e/test-scenario-01.cy.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | describe("Test Scenario - Simple Test (no script)", () => { 5 | it("Create successfully", () => { 6 | cy.visit("/create"); 7 | 8 | // input all needed test scenario configurations 9 | cy.findByLabelText("Name").type("test-scenario-01"); 10 | cy.findByLabelText("Description").type("test scenario 01 description"); 11 | cy.get("div.regional-config-input-row").within(() => { 12 | cy.get("input#taskCount-0").type("1"); 13 | cy.get("input#concurrency-0").type("1"); 14 | cy.get("select#region-0").select(1); 15 | }); // getting DOM elements by selector as accessible name is not available in accessibility tree 16 | cy.findByLabelText("Ramp Up").type("1"); 17 | cy.findByLabelText("Hold For").type("1"); 18 | cy.findByLabelText("HTTP endpoint under test").type("https://example.com"); 19 | cy.findByRole("button", { name: "Run Now" }).click(); 20 | 21 | // verify details page is loaded after submitting test 22 | cy.url().should("include", "/details"); 23 | cy.findByRole("heading", { name: "Load Test Details" }); 24 | cy.findByRole("button", { name: "Refresh" }); 25 | cy.findByRole("button", { name: "Cancel" }); 26 | cy.screenshot(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /source/infrastructure/test/common-resources.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { App, DefaultStackSynthesizer, Stack } from "aws-cdk-lib"; 5 | import { CommonResources } from "../lib/common-resources/common-resources"; 6 | import { Solution } from "../bin/solution"; 7 | import { Bucket } from "aws-cdk-lib/aws-s3"; 8 | import { CustomResourceLambda } from "../lib/common-resources/custom-resource-lambda"; 9 | import { CfnApplication } from "aws-cdk-lib/aws-servicecatalogappregistry"; 10 | import { Policy } from "aws-cdk-lib/aws-iam"; 11 | import { createTemplateWithoutS3Key } from "./snapshot_helpers"; 12 | 13 | test("DLT API Test", () => { 14 | const app = new App(); 15 | const stack = new Stack(app, "DLTStack", { 16 | synthesizer: new DefaultStackSynthesizer({ 17 | generateBootstrapVersionRule: false, 18 | }), 19 | }); 20 | const solution = new Solution("testId", "DLT", "testVersion", "mainStackDescription"); 21 | const common = new CommonResources(stack, "TestCommonResources", solution, "regional"); 22 | 23 | expect(createTemplateWithoutS3Key(stack)).toMatchSnapshot(); 24 | expect(common.s3LogsBucket).toBeInstanceOf(Bucket); 25 | expect(common.customResourceLambda).toBeInstanceOf(CustomResourceLambda); 26 | expect(common.cloudWatchLogsPolicy).toBeInstanceOf(Policy); 27 | }); 28 | -------------------------------------------------------------------------------- /source/webui/src/store/regionsSlice.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { createSlice, PayloadAction } from "@reduxjs/toolkit"; 5 | import { solutionApi, ApiEndpoints } from "./solutionApi.ts"; 6 | 7 | interface RegionsState { 8 | data: string[] | null; 9 | } 10 | 11 | const initialState: RegionsState = { 12 | data: null, 13 | }; 14 | 15 | export const regionsSlice = createSlice({ 16 | name: "regions", 17 | initialState, 18 | reducers: { 19 | setRegionsData: (state, action: PayloadAction) => { 20 | state.data = action.payload; 21 | }, 22 | }, 23 | }); 24 | 25 | export const regionsApiSlice = solutionApi.injectEndpoints({ 26 | endpoints: (builder) => ({ 27 | getRegions: builder.query({ 28 | query: () => ApiEndpoints.REGIONS, 29 | async onQueryStarted(arg, { dispatch, queryFulfilled }) { 30 | try { 31 | const { data } = await queryFulfilled; 32 | const regionValues = data.regions?.map((region: any) => region.region) || []; 33 | dispatch(regionsSlice.actions.setRegionsData(regionValues)); 34 | } catch (error) { 35 | console.error("Failed to fetch regions:", error); 36 | } 37 | }, 38 | }), 39 | }), 40 | }); 41 | 42 | export const { setRegionsData } = regionsSlice.actions; 43 | export const { useGetRegionsQuery } = regionsApiSlice; 44 | -------------------------------------------------------------------------------- /source/webui/src/utils/__tests__/dateUtils.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { describe, it, expect } from "vitest"; 5 | import { formatToLocalTime } from "../dateUtils"; 6 | 7 | describe("formatToLocalTime", () => { 8 | it("should return '-' for undefined input", () => { 9 | expect(formatToLocalTime(undefined)).toBe("-"); 10 | }); 11 | 12 | it("should return '-' for empty string", () => { 13 | expect(formatToLocalTime("")).toBe("-"); 14 | }); 15 | 16 | it("should return '-' for invalid date string", () => { 17 | expect(formatToLocalTime("invalid-date")).toBe("-"); 18 | }); 19 | 20 | it("should format valid UTC date string", () => { 21 | const result = formatToLocalTime("2023-12-25 10:30:00"); 22 | expect(result).not.toBe("-"); 23 | expect(typeof result).toBe("string"); 24 | }); 25 | 26 | it("should format valid ISO date string", () => { 27 | const result = formatToLocalTime("2023-12-25T10:30:00"); 28 | expect(result).not.toBe("-"); 29 | expect(typeof result).toBe("string"); 30 | }); 31 | 32 | it("should apply custom formatting options", () => { 33 | const options: Intl.DateTimeFormatOptions = { 34 | year: "numeric", 35 | month: "short", 36 | day: "numeric" 37 | }; 38 | const result = formatToLocalTime("2023-12-25 10:30:00", options); 39 | expect(result).toMatch(/Dec.*25.*2023/); 40 | }); 41 | }); -------------------------------------------------------------------------------- /source/infrastructure/test/custom-resources-lambda.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { Template } from "aws-cdk-lib/assertions"; 5 | import { App, DefaultStackSynthesizer, Stack } from "aws-cdk-lib"; 6 | import { CustomResourceLambda } from "../lib/common-resources/custom-resource-lambda"; 7 | import { Solution, SOLUTIONS_METRICS_ENDPOINT } from "../bin/solution"; 8 | import { createTemplateWithoutS3Key } from "./snapshot_helpers"; 9 | 10 | test("DLT API Test", () => { 11 | const app = new App(); 12 | const stack = new Stack(app, "DLTStack", { 13 | synthesizer: new DefaultStackSynthesizer({ 14 | generateBootstrapVersionRule: false, 15 | }), 16 | }); 17 | 18 | const solution = new Solution("testId", "DLT", "testVersion", "mainStackDescription"); 19 | new CustomResourceLambda(stack, "TestCustomResourceInfra", solution, "main"); 20 | 21 | expect(createTemplateWithoutS3Key(stack)).toMatchSnapshot(); 22 | Template.fromStack(stack).hasResourceProperties("AWS::Lambda::Function", { 23 | Description: "CFN Lambda backed custom resource to deploy assets to s3", 24 | Environment: { 25 | Variables: { 26 | METRIC_URL: SOLUTIONS_METRICS_ENDPOINT, 27 | SOLUTION_ID: solution.id, 28 | VERSION: solution.version, 29 | }, 30 | }, 31 | Handler: "index.handler", 32 | Runtime: "nodejs20.x", 33 | Timeout: 120, 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /source/webui/src/pages/scenarios/hooks/useTagManagement.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Custom hook for managing tag addition and removal logic 5 | 6 | import { useState } from "react"; 7 | import { FormData } from "../types"; 8 | 9 | export const useTagManagement = (formData: FormData, updateFormData: (updates: Partial) => void) => { 10 | const [newTag, setNewTag] = useState(""); 11 | const [tagError, setTagError] = useState(""); 12 | 13 | const addTag = () => { 14 | const trimmedTag = newTag.trim(); 15 | const tagExists = formData.tags.some((tag) => tag.label.toLowerCase() === trimmedTag.toLowerCase()); 16 | 17 | if (!trimmedTag) { 18 | setTagError(""); 19 | return; 20 | } 21 | 22 | if (tagExists) { 23 | setTagError("This tag already exists."); 24 | return; 25 | } 26 | 27 | if (formData.tags.length >= 5) { 28 | setTagError("Maximum 5 tags allowed."); 29 | return; 30 | } 31 | 32 | updateFormData({ 33 | tags: [...formData.tags, { label: trimmedTag, dismissLabel: `Remove ${trimmedTag} tag` }], 34 | }); 35 | setNewTag(""); 36 | setTagError(""); 37 | }; 38 | 39 | const removeTag = (index: number) => { 40 | updateFormData({ 41 | tags: formData.tags.filter((_, i) => i !== index), 42 | }); 43 | }; 44 | 45 | return { newTag, setNewTag, tagError, setTagError, addTag, removeTag }; 46 | }; 47 | -------------------------------------------------------------------------------- /deployment/ecr/distributed-load-testing-on-aws-load-tester/ecscontroller.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | #!/usr/bin/python 5 | from multiprocessing import Pool 6 | import socket 7 | from functools import partial 8 | import sys 9 | 10 | def request_socket(ip_host, ip_net): 11 | """Create a socket and send a message over created socket""" 12 | msg = "" 13 | server_port = 50000 14 | server_name = ip_net + "." + ip_host 15 | 16 | #Create socket and connect 17 | client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 18 | client_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 19 | client_socket.connect((server_name, int(server_port))) 20 | 21 | #message to send 22 | msg="start" 23 | 24 | #Send Message 25 | client_socket.send(msg.encode()) 26 | 27 | #Close socket 28 | client_socket.close() 29 | 30 | if __name__ == "__main__": 31 | # Parse ip addresses 32 | ip_hosts = sys.argv[2] 33 | ip_network = sys.argv[1] 34 | 35 | ip_hosts_list = ip_hosts.split(',') 36 | 37 | print("Sending start message to IP Addresses...") 38 | 39 | #Create socket for each IP and send commands 40 | pool = Pool() 41 | request_socket_modified = partial(request_socket, ip_net=ip_network) 42 | pool.map(request_socket_modified, ip_hosts_list) 43 | 44 | print("Start messages sent successfuly.") 45 | 46 | -------------------------------------------------------------------------------- /source/infrastructure/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "distributed-load-testing-on-aws-infrastructure", 3 | "version": "4.0.2", 4 | "author": { 5 | "name": "Amazon Web Services", 6 | "url": "https://aws.amazon.com/solutions" 7 | }, 8 | "license": "Apache-2.0", 9 | "description": "distributed-load-testing-on-aws-infrastructure", 10 | "bin": { 11 | "distributed-load-testing-on-aws": "bin/distributed-load-testing-on-aws.ts" 12 | }, 13 | "scripts": { 14 | "clean": "rm -rf node_modules package-lock.json", 15 | "build": "tsc", 16 | "watch": "tsc -w", 17 | "test": "jest --coverage", 18 | "cdk": "cdk", 19 | "install:all": "cd ../ && npm run install:all && npm run build:webui && npm run build:metrics-utils" 20 | }, 21 | "devDependencies": { 22 | "@aws-cdk/aws-servicecatalogappregistry-alpha": "^2.190.0-alpha.0", 23 | "@aws-sdk/client-s3": "^3.0.0", 24 | "@aws-solutions-constructs/aws-cloudfront-s3": "2.86.0", 25 | "@types/jest": "^29.5.4", 26 | "@types/node": "^20.6.1", 27 | "aws-cdk": "^2.1029.0", 28 | "aws-cdk-lib": "^2.223.0", 29 | "constructs": "10.4.2", 30 | "esbuild": "^0.25.2", 31 | "jest": "^29.7.0", 32 | "ts-jest": "^29.1.1", 33 | "ts-node": "^10.0.0", 34 | "typescript": "^5.1.3" 35 | }, 36 | "dependencies": { 37 | "@aws-sdk/client-bedrock-agentcore-control": "^3.909.0", 38 | "source-map-support": "^0.5.16" 39 | }, 40 | "overrides": { 41 | "semver": "7.5.2" 42 | }, 43 | "resolutions": { 44 | "semver": "7.5.2" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /source/webui/src/utils/errorUtils.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /** 5 | * Extracts a user-friendly error message from API errors 6 | * Specifically handles AWS Amplify API error structures 7 | */ 8 | const ERROR_PATTERNS = [ 9 | { prefix: "InvalidParameter:", replacement: "" }, 10 | { prefix: "INVALID_REQUEST_BODY:", replacement: "Invalid request:" }, 11 | { prefix: "ValidationException:", replacement: "Validation error:" }, 12 | { prefix: "ResourceNotFoundException:", replacement: "Resource not found:" }, 13 | { prefix: "AccessDeniedException:", replacement: "Access denied:" }, 14 | { prefix: "InternalServerError:", replacement: "Server error:" }, 15 | { prefix: "BadRequestException:", replacement: "Bad request:" }, 16 | ]; 17 | 18 | export function extractErrorMessage(error: any): string { 19 | const message = 20 | error?.response?.body || 21 | error?.data?.message || 22 | error?.error || 23 | (error?.message !== "Unknown error" ? error?.message : null) || 24 | (typeof error === "string" ? error : null) || 25 | "An unexpected error occurred. Please try again."; 26 | 27 | // Format message by removing technical prefixes 28 | const pattern = ERROR_PATTERNS.find((p) => message.startsWith(p.prefix)); 29 | if (pattern) { 30 | const cleanMessage = message.replace(pattern.prefix, "").trim(); 31 | return pattern.replacement ? `${pattern.replacement} ${cleanMessage}` : cleanMessage; 32 | } 33 | 34 | return message; 35 | } 36 | -------------------------------------------------------------------------------- /source/integration-tests/src/scenario.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { AxiosResponse } from "axios"; 5 | 6 | interface TaskConfig { 7 | concurrency: string; 8 | taskCount: string; 9 | region: string; 10 | } 11 | 12 | interface ExecutionStep { 13 | "ramp-up": string; 14 | "hold-for": string; 15 | scenario: string; 16 | } 17 | 18 | interface Request { 19 | url: string; 20 | method: string; 21 | body: Record; 22 | headers: Record; 23 | } 24 | 25 | interface TestScenario { 26 | execution: ExecutionStep[]; 27 | scenarios: { 28 | [key: string]: { 29 | requests?: Request[]; 30 | script?: string; 31 | }; 32 | }; 33 | } 34 | 35 | interface RegionalTaskDetails { 36 | [key: string]: { 37 | vCPULimit: number; 38 | vCPUsPerTask: number; 39 | vCPUsInUse: number; 40 | dltTaskLimit: number; 41 | dltAvailableTasks: number; 42 | }; 43 | } 44 | 45 | export interface ScenarioRequest { 46 | testName: string; 47 | testDescription: string; 48 | showLive: boolean; 49 | testType: string; 50 | fileType: string; 51 | testTaskConfigs: TaskConfig[]; 52 | testScenario: TestScenario; 53 | regionalTaskDetails: RegionalTaskDetails; 54 | testId?: string; 55 | recurrence?: string; 56 | scheduleDate?: string; 57 | scheduleTime?: string; 58 | scheduleStep?: string; 59 | } 60 | 61 | export interface ScenarioResponse extends AxiosResponse { 62 | testId: string; 63 | testName: string; 64 | } 65 | -------------------------------------------------------------------------------- /source/webui/src/pages/scenarios/components/TestRunsBaselineContainer.tsx: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import React, { useMemo } from "react"; 5 | import { Container, Header, SpaceBetween, Button } from "@cloudscape-design/components"; 6 | import { TestRun } from "../types"; 7 | import { formatToLocalTime } from "../../../utils/dateUtils"; 8 | 9 | interface BaselineContainerProps { 10 | baselineTestRun: TestRun | null; 11 | onRemoveBaseline: () => void; 12 | isRemovingBaseline: boolean; 13 | } 14 | 15 | export const TestRunsBaselineContainer: React.FC = ({ 16 | baselineTestRun, 17 | onRemoveBaseline, 18 | isRemovingBaseline, 19 | }) => { 20 | const formattedDate = useMemo(() => 21 | baselineTestRun ? formatToLocalTime(baselineTestRun.startTime, { hour12: false }) : null, 22 | [baselineTestRun?.startTime] 23 | ); 24 | 25 | const removeButton = useMemo(() => ( 26 | 29 | ), [onRemoveBaseline, isRemovingBaseline]); 30 | 31 | if (!baselineTestRun) return null; 32 | 33 | return ( 34 | 37 | Baseline 38 | 39 | } 40 | > 41 | 42 |
Test Run
43 |
{formattedDate}
44 |
45 |
46 | ); 47 | }; -------------------------------------------------------------------------------- /source/api-services/lib/validation/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /** 5 | * Main entry point for validation module 6 | * Exports all validation functions, schemas, and utilities 7 | */ 8 | 9 | // Export all validation functions 10 | export { 11 | validateTestId, 12 | validateTestRunId, 13 | validatePathParameters, 14 | validateScenariosQuery, 15 | validateScenarioQuery, 16 | validateTestRunsQuery, 17 | validateBaselineQuery, 18 | validateCreateTestBody, 19 | validateSetBaselineBody, 20 | validateDeleteTestRunsBody, 21 | validateQueryForResource, 22 | validateBodyForResource, 23 | } from "./validators"; 24 | 25 | // Export schemas for advanced use cases 26 | export { 27 | testIdSchema, 28 | testRunIdSchema, 29 | pathParametersSchema, 30 | scenariosQuerySchema, 31 | scenarioQuerySchema, 32 | testRunsQuerySchema, 33 | baselineQuerySchema, 34 | createTestSchema, 35 | setBaselineSchema, 36 | deleteTestRunsSchema, 37 | } from "./schemas"; 38 | 39 | // Export error utilities 40 | export { formatZodError, getFirstZodError, groupZodIssuesByPath, zodIssuesToValidationErrors } from "./errors"; 41 | 42 | // Export TypeScript types 43 | export type { 44 | TestIdValidation, 45 | TestRunIdValidation, 46 | PathParametersValidation, 47 | ScenariosQueryValidation, 48 | ScenarioQueryValidation, 49 | TestRunsQueryValidation, 50 | BaselineQueryValidation, 51 | CreateTestValidation, 52 | SetBaselineValidation, 53 | DeleteTestRunsValidation, 54 | } from "./schemas"; 55 | -------------------------------------------------------------------------------- /source/webui/vite.config.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { defineConfig, UserConfig } from "vite"; 5 | import { CoverageV8Options, UserConfig as VitestUserConfig } from "vitest/node"; 6 | import react from "@vitejs/plugin-react-swc"; 7 | import { resolve } from "path"; 8 | 9 | const coverageConfig: { provider: "v8" } & CoverageV8Options = { 10 | provider: "v8", 11 | enabled: true, 12 | reportsDirectory: resolve(__dirname, "./coverage"), 13 | reporter: ["text", "html", "lcov"], 14 | exclude: [ 15 | "node_modules/**", 16 | "dist/**", 17 | "coverage/**", 18 | "**/mockServiceWorker.js", 19 | "vite.config.ts", 20 | "src/mocks/**", 21 | "src/__tests__/**", 22 | ], 23 | }; 24 | 25 | // https://vitejs.dev/config/ 26 | const config: VitestUserConfig & UserConfig = { 27 | test: { 28 | globals: true, // makes describe, it, expect available without import 29 | environment: "jsdom", 30 | setupFiles: ["./src/setupTests.ts"], // runs this file before all tests 31 | include: ["./src/__tests__/**/*.test.ts?(x)"], 32 | coverage: coverageConfig, 33 | maxConcurrency: 1, // set to 1 to run tests serially, one file at a time 34 | testTimeout: 25000, // 25s test timeout unless specified otherwise in the test suite 35 | }, 36 | plugins: [react()], 37 | server: { 38 | port: 3000, 39 | }, 40 | build: { 41 | outDir: resolve(__dirname, "./dist"), 42 | }, 43 | define: { 44 | global: "globalThis", 45 | }, 46 | }; 47 | export default defineConfig(config); 48 | -------------------------------------------------------------------------------- /source/webui/src/components/navigation/TopNavigationBar.tsx: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { TopNavigation, TopNavigationProps } from "@cloudscape-design/components"; 5 | import { useAuthenticator } from "@aws-amplify/ui-react"; 6 | 7 | export default function TopNavigationBar() { 8 | const { user, signOut } = useAuthenticator(); 9 | 10 | const solutionIdentity: TopNavigationProps.Identity = { 11 | href: "/", 12 | logo: { src: "/aws-logo.svg", alt: "AWS" }, 13 | }; 14 | 15 | const i18nStrings: TopNavigationProps.I18nStrings = { 16 | overflowMenuTitleText: "All", 17 | overflowMenuTriggerText: "More", 18 | }; 19 | 20 | const utilities: TopNavigationProps.Utility[] = [ 21 | { 22 | type: "menu-dropdown", 23 | text: user.username ?? "User", 24 | iconName: "user-profile", 25 | items: [ 26 | { 27 | id: "documentation", 28 | text: "Documentation", 29 | href: "https://docs.aws.amazon.com/solutions/latest/distributed-load-testing-on-aws/solution-overview.html", 30 | external: true, 31 | externalIconAriaLabel: " (opens in new tab)", 32 | }, 33 | { 34 | id: "signout", 35 | text: "Sign Out", 36 | }, 37 | ], 38 | onItemClick: async (event) => { 39 | if (event.detail.id === "signout") { 40 | await signOut(); 41 | } 42 | }, 43 | }, 44 | ]; 45 | 46 | return ; 47 | } 48 | -------------------------------------------------------------------------------- /source/webui/src/pages/scenarios/types/viewMode.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { TableRow } from "./testResults"; 5 | 6 | export enum ViewMode { 7 | Overall = 'overall', 8 | ByEndpoint = 'byEndpoint', 9 | ByRegion = 'byRegion', 10 | } 11 | 12 | export interface AggregateMetrics { 13 | requests: number; 14 | success: number; 15 | successRate: number; 16 | avgRespTime: number; 17 | p95RespTime: number; 18 | // Additional metrics 19 | errors: number; 20 | requestsPerSecond: number; 21 | avgLatency: number; 22 | avgConnectionTime: number; 23 | avgBandwidth: number; 24 | // Additional percentiles 25 | p0RespTime: number; 26 | p50RespTime: number; 27 | p90RespTime: number; 28 | p99RespTime: number; 29 | p99_9RespTime: number; 30 | p100RespTime: number; 31 | } 32 | 33 | export interface BaselineComparison { 34 | requests: number; 35 | success: number; 36 | successRate: number; 37 | avgRespTime: number; 38 | p95RespTime: number; 39 | // Additional metrics 40 | errors: number; 41 | requestsPerSecond: number; 42 | avgLatency: number; 43 | avgConnectionTime: number; 44 | avgBandwidth: number; 45 | // Additional percentiles 46 | p0RespTime: number; 47 | p50RespTime: number; 48 | p90RespTime: number; 49 | p99RespTime: number; 50 | p99_9RespTime: number; 51 | p100RespTime: number; 52 | } 53 | 54 | export interface ProcessedTableData { 55 | rows: TableRow[]; 56 | baselineComparisons: Map; 57 | aggregatedBaseline: AggregateMetrics | null; 58 | } 59 | -------------------------------------------------------------------------------- /source/custom-resource/lib/s3/index.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | const { S3 } = require("@aws-sdk/client-s3"); 5 | 6 | const utils = require("solution-utils"); 7 | 8 | let options = utils.getOptions({}); 9 | const s3 = new S3(options); 10 | 11 | /** 12 | * Copy regional template from source to destination bucket and modify mappings 13 | */ 14 | const putRegionalTemplate = async (config) => { 15 | try { 16 | //get file from S3 and convert from yaml 17 | const getParams = { 18 | Bucket: config.SrcBucket, 19 | Key: `${config.SrcPath}/distributed-load-testing-on-aws-regional.template`, 20 | }; 21 | 22 | const templateJson = await s3.getObject(getParams); 23 | const template = JSON.parse(await templateJson.Body.transformToString()); 24 | 25 | template.Mappings.Solution.Config.MainRegionLambdaTaskRoleArn = config.MainRegionLambdaTaskRoleArn; 26 | template.Mappings.Solution.Config.ScenariosTable = config.ScenariosTable; 27 | template.Mappings.Solution.Config.MainRegionStack = config.MainRegionStack; 28 | template.Mappings.Solution.Config.ScenariosBucket = config.DestBucket; 29 | 30 | const putParams = { 31 | Bucket: config.DestBucket, 32 | Key: "regional-template/distributed-load-testing-on-aws-regional.template", 33 | Body: JSON.stringify(template), 34 | }; 35 | await s3.putObject(putParams); 36 | } catch (err) { 37 | console.error(err); 38 | throw err; 39 | } 40 | return "success"; 41 | }; 42 | 43 | module.exports = { 44 | putRegionalTemplate: putRegionalTemplate, 45 | }; 46 | -------------------------------------------------------------------------------- /source/webui/src/pages/scenarios/components/GeneralSettingsStep.tsx: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // First wizard step for test scenario general settings 5 | 6 | import { SpaceBetween } from "@cloudscape-design/components"; 7 | import { FormData } from "../types"; 8 | import { useTagManagement } from "../hooks/useTagManagement"; 9 | import { TestConfigurationSection } from "./TestConfigurationSection"; 10 | import { TagsSection } from "./TagsSection"; 11 | import { ScheduleSection } from "./ScheduleSection"; 12 | 13 | interface Props { 14 | formData: FormData; 15 | updateFormData: (updates: Partial) => void; 16 | showValidationErrors?: boolean; 17 | } 18 | 19 | export const GeneralSettingsStep = ({ formData, updateFormData, showValidationErrors }: Props) => { 20 | const { newTag, setNewTag, tagError, setTagError, addTag, removeTag } = useTagManagement(formData, updateFormData); 21 | 22 | return ( 23 | 24 | 29 | 38 | 39 | 40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /source/infrastructure/lib/mcp/gateway-construct.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { AuthorizerType, GatewayProtocolType } from "@aws-sdk/client-bedrock-agentcore-control"; 5 | import { CfnGateway } from "aws-cdk-lib/aws-bedrockagentcore"; 6 | import { Role } from "aws-cdk-lib/aws-iam"; 7 | import { Construct } from "constructs"; 8 | 9 | export interface AgentCoreGatewayProps { 10 | readonly name: string; 11 | readonly description: string; 12 | readonly executionRole: Role; 13 | readonly discoveryUrl: string; 14 | readonly allowedClients: string[]; 15 | } 16 | 17 | export class AgentCoreGateway extends Construct { 18 | public readonly gatewayId: string; 19 | public readonly gatewayArn: string; 20 | public readonly gatewayUrl: string; 21 | 22 | constructor(scope: Construct, id: string, props: AgentCoreGatewayProps) { 23 | super(scope, id); 24 | 25 | const gateway = new CfnGateway(this, "DltAgentCoreGateway", { 26 | name: props.name, 27 | description: props.description, 28 | roleArn: props.executionRole.roleArn, 29 | protocolType: GatewayProtocolType.MCP, 30 | authorizerType: AuthorizerType.CUSTOM_JWT, 31 | authorizerConfiguration: { 32 | customJwtAuthorizer: { 33 | discoveryUrl: props.discoveryUrl, 34 | allowedClients: props.allowedClients, 35 | }, 36 | }, 37 | }); 38 | 39 | // Expose gateway attributes 40 | this.gatewayId = gateway.attrGatewayIdentifier; 41 | this.gatewayArn = gateway.attrGatewayArn; 42 | this.gatewayUrl = gateway.attrGatewayUrl; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /source/api-services/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api-services", 3 | "version": "4.0.2", 4 | "description": "REST API micro services", 5 | "repository": { 6 | "type": "git", 7 | "url": "na" 8 | }, 9 | "license": "Apache-2.0", 10 | "author": { 11 | "name": "Amazon Web Services", 12 | "url": "https://aws.amazon.com/solutions" 13 | }, 14 | "main": "na", 15 | "scripts": { 16 | "clean": "rm -rf node_modules package-lock.json", 17 | "test": "jest --coverage --silent" 18 | }, 19 | "dependencies": { 20 | "@aws-sdk/client-cloudformation": "^3.758.0", 21 | "@aws-sdk/client-cloudwatch": "^3.758.0", 22 | "@aws-sdk/client-cloudwatch-events": "^3.758.0", 23 | "@aws-sdk/client-cloudwatch-logs": "^3.758.0", 24 | "@aws-sdk/client-dynamodb": "^3.758.0", 25 | "@aws-sdk/client-ecs": "^3.758.0", 26 | "@aws-sdk/client-lambda": "^3.758.0", 27 | "@aws-sdk/client-s3": "^3.758.0", 28 | "@aws-sdk/client-service-quotas": "^3.758.0", 29 | "@aws-sdk/client-sfn": "^3.758.0", 30 | "@aws-sdk/lib-dynamodb": "^3.758.0", 31 | "axios": "^1.8.3", 32 | "cron-parser": "^4.9.0", 33 | "luxon": "^3.5.0", 34 | "solution-utils": "file:../solution-utils", 35 | "zod": "^3.23.8" 36 | }, 37 | "devDependencies": { 38 | "@babel/preset-env": "^7.28.5", 39 | "@babel/preset-typescript": "^7.28.5", 40 | "@types/jest": "^29.5.12", 41 | "@types/node": "^20.12.7", 42 | "axios-mock-adapter": "1.18.2", 43 | "jest": "^29.7.0", 44 | "typescript": "^5.4.5" 45 | }, 46 | "engines": { 47 | "node": "^20.x" 48 | }, 49 | "readme": "./README.md", 50 | "overrides": { 51 | "form-data": "4.0.4" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /source/webui/src/store/notificationsSlice.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { CaseReducer, createSlice, Slice } from "@reduxjs/toolkit"; 5 | import React from "react"; 6 | import { RootState } from "./store.ts"; 7 | 8 | export type NotificationPayload = { 9 | id: string; 10 | header?: React.ReactNode; 11 | content?: React.ReactNode; 12 | type: "success" | "warning" | "info" | "error" | "in-progress"; 13 | }; 14 | 15 | export type NotificationState = { 16 | notifications: Array; 17 | }; 18 | 19 | export type NotificationReducers = { 20 | addNotification: CaseReducer; 21 | deleteNotification: CaseReducer; 22 | }; 23 | 24 | export const notificationsSlice: Slice = createSlice({ 25 | name: "notifications", 26 | initialState: { 27 | notifications: [] as Array, 28 | }, 29 | reducers: { 30 | addNotification: (state, action) => { 31 | const notification = action.payload; 32 | if (!state.notifications.find((it) => it.id === notification.id)) state.notifications.push(notification); 33 | }, 34 | deleteNotification: (state, action) => { 35 | state.notifications = state.notifications.filter((it) => it.id !== action.payload.id); 36 | }, 37 | }, 38 | }); 39 | 40 | export const selectNotifications = (state: RootState) => state.notifications.notifications; 41 | export const { addNotification, deleteNotification } = notificationsSlice.actions; 42 | -------------------------------------------------------------------------------- /source/infrastructure/test/add-cfn-guard-suppression.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { CfnResource, Stack } from "aws-cdk-lib"; 5 | import { Bucket } from "aws-cdk-lib/aws-s3"; 6 | import { addCfnGuardSuppression } from "../lib/common-resources/add-cfn-guard-suppression"; 7 | 8 | describe("add cfn guard suppression", function () { 9 | it("adds suppression when none present", function () { 10 | const stack = new Stack(); 11 | const bucket = new Bucket(stack, "Bucket"); 12 | const ruleName = "IAM_NO_INLINE_POLICY_CHECK"; 13 | addCfnGuardSuppression(bucket, "IAM_NO_INLINE_POLICY_CHECK"); 14 | expect((bucket.node.defaultChild as CfnResource).cfnOptions.metadata?.guard?.SuppressedRules).toStrictEqual( 15 | expect.arrayContaining([ruleName]) 16 | ); 17 | }); 18 | 19 | it("adds suppression when metadata already exists", function () { 20 | const stack = new Stack(); 21 | const bucket = new Bucket(stack, "Bucket"); 22 | const firstSuppression = { id: "my id", reason: "my reason" }; 23 | (bucket.node.defaultChild as CfnResource).cfnOptions.metadata = { 24 | cfn_nag: { rules_to_suppress: [firstSuppression] }, 25 | }; 26 | addCfnGuardSuppression(bucket, "IAM_NO_INLINE_POLICY_CHECK"); 27 | expect((bucket.node.defaultChild as CfnResource).cfnOptions.metadata?.cfn_nag?.rules_to_suppress).toStrictEqual( 28 | expect.arrayContaining([firstSuppression]) 29 | ); 30 | expect((bucket.node.defaultChild as CfnResource).cfnOptions.metadata?.guard?.SuppressedRules).toStrictEqual( 31 | expect.arrayContaining(["IAM_NO_INLINE_POLICY_CHECK"]) 32 | ); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /source/mcp-server/src/tools/get-baseline-test-run.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { z } from "zod"; 5 | import { parseEventWithSchema, TEST_SCENARIO_ID_LENGTH, TEST_SCENARIO_ID_REGEX, type AgentCoreEvent } from "../lib/common"; 6 | import { AppError } from "../lib/errors"; 7 | import { IAMHttpClient, type HttpResponse } from "../lib/http-client"; 8 | 9 | // Zod schema for get_baseline_test_run parameters 10 | export const GetBaselineTestRunSchema = z.object({ 11 | test_id: z.string() 12 | .length(TEST_SCENARIO_ID_LENGTH, `test_id should be the ${TEST_SCENARIO_ID_LENGTH} character unique id for a test scenario`) 13 | .regex(TEST_SCENARIO_ID_REGEX, "Invalid test_id") 14 | }); 15 | 16 | // TypeScript type derived from Zod schema 17 | export type GetBaselineTestRunParameters = z.infer; 18 | 19 | /** 20 | * Handle get_baseline_test_run tool 21 | */ 22 | export async function handleGetBaselineTestRun(httpClient: IAMHttpClient, apiEndpoint: string, event: AgentCoreEvent): Promise { 23 | const { test_id } = parseEventWithSchema(GetBaselineTestRunSchema, event); 24 | 25 | let response: HttpResponse; 26 | try { 27 | response = await httpClient.get(`${apiEndpoint}/scenarios/${test_id}/baseline`); 28 | } catch (error) { 29 | throw new AppError("Internal request failed", 500); 30 | } 31 | 32 | if (response.statusCode !== 200) { 33 | throw new AppError(response.body, response.statusCode); 34 | } 35 | 36 | const data = JSON.parse(response.body); 37 | if (!data) { 38 | throw new AppError(`Baseline test run not found: ${test_id}`, 404); 39 | } 40 | 41 | return data; 42 | } 43 | -------------------------------------------------------------------------------- /source/integration-tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "integration-tests", 3 | "version": "0.1.0", 4 | "description": "Amazon Web Services - Distributed Load Testing Integration Tests", 5 | "author": { 6 | "name": "Amazon Web Services", 7 | "url": "https://aws.amazon.com/solutions" 8 | }, 9 | "scripts": { 10 | "format": "npx prettier --write \"src/**/*.ts\"", 11 | "lint": "npx eslint \"src/**/*.ts\" --quiet", 12 | "clean": "rm -rf dist && rm -rf node_modules && rm -rf coverage" 13 | }, 14 | "license": "Apache-2.0", 15 | "dependencies": { 16 | "@aws-sdk/client-s3": "^3.658.1", 17 | "ajv": "^8.16.0", 18 | "aws4-axios": "^3.3.0", 19 | "axios": "^1.8.3", 20 | "cypress": "^13.9.0" 21 | }, 22 | "devDependencies": { 23 | "@testing-library/cypress": "^10.0.1", 24 | "@types/jest": "^29.5.2", 25 | "@types/node": "^20.3.1", 26 | "@typescript-eslint/eslint-plugin": "^5.59.11", 27 | "@typescript-eslint/parser": "^5.59.11", 28 | "eslint": "^8.42.0", 29 | "eslint-config-prettier": "^8.8.0", 30 | "eslint-plugin-import": "^2.28.1", 31 | "eslint-plugin-prettier": "^4.2.1", 32 | "jest": "^29.5.0", 33 | "prettier": "^2.8.8", 34 | "source-map-support": "^0.5.21", 35 | "ts-jest": "^29.1.0", 36 | "ts-node": "^10.9.2", 37 | "typescript": "^5.1.3" 38 | }, 39 | "overrides": { 40 | "fast-xml-parser": "4.4.1", 41 | "form-data": "4.0.4" 42 | }, 43 | "jest": { 44 | "moduleFileExtensions": [ 45 | "js", 46 | "json", 47 | "ts" 48 | ], 49 | "rootDir": "src", 50 | "testRegex": ".*\\.spec\\.ts$", 51 | "transform": { 52 | "^.+\\.(t|j)s$": "ts-jest" 53 | }, 54 | "testEnvironment": "node" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /source/infrastructure/test/snapshot_helpers.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { Template } from "aws-cdk-lib/assertions"; 5 | import { Stack } from "aws-cdk-lib"; 6 | 7 | export function createTemplateWithoutS3Key(stack: Stack): Template { 8 | const templateJson = Template.fromStack(stack).toJSON(); 9 | 10 | Object.keys(templateJson.Resources).forEach((key) => { 11 | if (templateJson.Resources[key].Properties?.Code?.S3Key) { 12 | templateJson.Resources[key].Properties.Code.S3Key = "Omitted to remove snapshot dependency on hash"; 13 | } 14 | if (templateJson.Resources[key].Properties?.Content?.S3Key) { 15 | templateJson.Resources[key].Properties.Content.S3Key = "Omitted to remove snapshot dependency on hash"; 16 | } 17 | if (templateJson.Resources[key].Properties?.SourceObjectKeys) { 18 | templateJson.Resources[key].Properties.SourceObjectKeys = [ 19 | "Omitted to remove snapshot dependency on demo ui module hash", 20 | ]; 21 | } 22 | if (templateJson.Resources[key].Properties?.Environment?.Variables?.SOLUTION_VERSION) { 23 | templateJson.Resources[key].Properties.Environment.Variables.SOLUTION_VERSION = 24 | "Omitted to remove snapshot dependency on solution version"; 25 | } 26 | if (templateJson.Resources[key].Properties?.Timestamp) { 27 | templateJson.Resources[key].Properties.Timestamp = 28 | "Omitted to remove snapshot dependency on timestamp"; 29 | } 30 | }); 31 | 32 | // Create a new Template instance with the modified JSON 33 | return { 34 | ...Template.fromJSON(templateJson), 35 | toJSON: () => templateJson, 36 | } as Template; 37 | } 38 | -------------------------------------------------------------------------------- /source/mcp-server/src/tools/get-scenario-details.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { z } from "zod"; 5 | import { parseEventWithSchema, TEST_SCENARIO_ID_LENGTH, TEST_SCENARIO_ID_REGEX, type AgentCoreEvent } from "../lib/common"; 6 | import { AppError } from "../lib/errors"; 7 | import { IAMHttpClient, type HttpResponse } from "../lib/http-client"; 8 | 9 | // Zod schema for get_scenario_details parameters 10 | export const GetScenarioDetailsSchema = z.object({ 11 | test_id: z.string() 12 | .length(TEST_SCENARIO_ID_LENGTH, `test_id should be the ${TEST_SCENARIO_ID_LENGTH} character unique id for a test scenario`) 13 | .regex(TEST_SCENARIO_ID_REGEX, "Invalid test_id") 14 | }); 15 | 16 | // TypeScript type derived from Zod schema 17 | export type GetScenarioDetailsParameters = z.infer; 18 | 19 | /** 20 | * Handle get_scenario_details tool 21 | */ 22 | export async function handleGetScenarioDetails(httpClient: IAMHttpClient, apiEndpoint: string, event: AgentCoreEvent): Promise { 23 | const { test_id } = parseEventWithSchema(GetScenarioDetailsSchema, event); 24 | 25 | let response: HttpResponse; 26 | try { 27 | response = await httpClient.get(`${apiEndpoint}/scenarios/${test_id}?history=false&latest=false`); 28 | } catch (error) { 29 | throw new AppError("Internal request failed", 500); 30 | } 31 | 32 | if (response.statusCode !== 200) { 33 | throw new AppError(response.body, response.statusCode); 34 | } 35 | 36 | const data = JSON.parse(response.body); 37 | if (!data) { 38 | throw new AppError(`Scenario not found: ${test_id}`, 404); 39 | } 40 | 41 | return data; 42 | } 43 | -------------------------------------------------------------------------------- /source/custom-resource/lib/cfn/index.spec.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | const axios = require("axios"); 5 | const MockAdapter = require("axios-mock-adapter"); 6 | 7 | const lambda = require("./index.js"); 8 | 9 | const _event = { 10 | RequestType: "Create", 11 | ServiceToken: "arn:aws:lambda", 12 | ResponseURL: "https://cloudformation", 13 | StackId: "arn:aws:cloudformation", 14 | RequestId: "1111111", 15 | LogicalResourceId: "Uuid", 16 | ResourceType: "Custom::UUID", 17 | }; 18 | 19 | const _context = { 20 | logStreamName: "cloudwatch", 21 | }; 22 | 23 | const _responseStatus = "ok"; 24 | 25 | const _responseData = { 26 | test: "testing", 27 | }; 28 | 29 | describe("#CFN RESPONSE::", () => { 30 | it("should succeed on send cfn", async () => { 31 | let mock = new MockAdapter(axios); 32 | mock.onPut().reply(200, {}); 33 | 34 | lambda.send(_event, _context, _responseStatus, _responseData, () => { 35 | expect(mock).toHaveBeenCalledTimes(1); 36 | expect(mock).toHaveBeenCalledWith(_event); 37 | expect("responseBody").toBeDefined(); 38 | expect("responseBody").toHaveProperty("Status", "ok"); 39 | expect("responseBody").toHaveProperty("StackId", "StackId"); 40 | expect("responseBody").toHaveProperty("Data", { test: "testing" }); 41 | }); 42 | }); 43 | 44 | it("should return error on connection timeout", async () => { 45 | let mock = new MockAdapter(axios); 46 | mock.onPut().networkError(); 47 | 48 | await lambda.send(_event, _context, _responseStatus, _responseData).catch((err) => { 49 | expect(err.toString()).toEqual("Error: Network Error"); 50 | }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /source/webui/README.md: -------------------------------------------------------------------------------- 1 | # Front-end Overview 2 | 3 | This directory contains sources for the DLT front-end console. This front-end consists of the following functional components: 4 | 5 | - **React**, for dynamic manipulation of front-end content, structure, and styling 6 | - **Cloudscape**, for AWS styling 7 | - **Redux**, for data fetching and caching 8 | - **Amplify**, for authenticating and authorizing requests using Cognito and API Gateway 9 | - **Vite**, for locally running a dev server with hot reloading 10 | - **MockServer**, for end-to-end testing with a simulated REST API back-end. 11 | 12 | ## Distribution 13 | 14 | The front-end is packaged and deployed as follows: 15 | 16 | 1. The build script (`build-s3-dist.sh`) runs `npm run build` in `source/webui/`, creating a `source/webui/dist/` folder with the compiled front-end assets. 17 | 18 | 2. CDK bundles the front-end assets in `source/webui/dist` using the Console construct in `source/infrastructure/lib/front-end/console.ts`. 19 | 20 | 3. The `build-s3-dist.sh` script creates zip files for all CDK assets, including the front-end assets, and copies them to `deployment/regional-s3-assets/`. 21 | 22 | 4. Front-end files are deployed directly to the S3 bucket created by the `CloudFrontToS3` construct. 23 | 24 | 5. The custom resource in `source/infrastructure/lib/front-end/webUIConfigConstruct.ts` generates `aws-exports.json` with values for the deployed Cognito and API Gateway resources and saves it to the S3 bucket hosting the front-end. 25 | 26 | ## Testing 27 | 28 | Use these commands to run the front-end locally. 29 | 30 | ```bash 31 | cd source/webui/ 32 | npm install 33 | npm run test 34 | # Update public/aws-exports.json with bindings to a deployed stack 35 | npm run dev 36 | ``` 37 | 38 | -------------------------------------------------------------------------------- /source/webui/eslint.config.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import js from "@eslint/js"; 5 | import tseslint from "@typescript-eslint/eslint-plugin"; 6 | import tsparser from "@typescript-eslint/parser"; 7 | import globals from "globals"; 8 | 9 | export default [ 10 | js.configs.recommended, 11 | { 12 | files: ["**/*.ts", "**/*.tsx"], 13 | languageOptions: { 14 | parser: tsparser, 15 | parserOptions: { 16 | ecmaVersion: "latest", 17 | sourceType: "module", 18 | project: "./tsconfig.json", 19 | }, 20 | globals: { 21 | ...globals.browser, 22 | ...globals.node, 23 | ...globals.es2021, 24 | }, 25 | }, 26 | plugins: { 27 | "@typescript-eslint": tseslint, 28 | }, 29 | rules: { 30 | "@typescript-eslint/no-inferrable-types": "off", 31 | "@typescript-eslint/no-useless-constructor": "off", 32 | "@typescript-eslint/no-unused-vars": [ 33 | "error", 34 | { args: "none", argsIgnorePattern: "^_", varsIgnorePattern: "^_" }, 35 | ], 36 | "no-unused-vars": "off", 37 | }, 38 | }, 39 | { 40 | files: ["**/*.test.ts", "**/*.test.tsx", "**/setupTests.ts"], 41 | languageOptions: { 42 | globals: { 43 | ...globals.browser, 44 | ...globals.node, 45 | ...globals.es2021, 46 | describe: "readonly", 47 | it: "readonly", 48 | expect: "readonly", 49 | vi: "readonly", 50 | global: "readonly", 51 | }, 52 | }, 53 | }, 54 | { 55 | ignores: ["**/node_modules/**", "**/*.config.ts", "**/dist/**", "**/cdk.out/**", "**/metrics-utils/**/*.js"], 56 | }, 57 | ]; 58 | -------------------------------------------------------------------------------- /source/webui/src/AppRoutes.tsx: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { Container, ContentLayout, Header } from "@cloudscape-design/components"; 5 | import { Route, Routes } from "react-router-dom"; 6 | import Layout from "./Layout.tsx"; 7 | import IntroductionPage from "./pages/introduction/IntroductionPage.tsx"; 8 | import McpServerPage from "./pages/mcp-server/McpServerPage.tsx"; 9 | import CreateTestScenarioPage from "./pages/scenarios/CreateTestScenarioPage.tsx"; 10 | import ScenarioDetailsPage from "./pages/scenarios/ScenarioDetailsPage.tsx"; 11 | import ScenariosPage from "./pages/scenarios/ScenariosPage.tsx"; 12 | import TestRunDetailsPage from "./pages/scenarios/TestResultsDetailsPage.tsx"; 13 | 14 | export const AppRoutes = () => ( 15 | 16 | }> 17 | } /> 18 | } /> 19 | } /> 20 | } /> 21 | } /> 22 | } /> 23 | {/* Add more child routes that use the same Layout here */} 24 | Error}> 28 | Page not found 😿}> 29 | 30 | } 31 | /> 32 | 33 | {/* Add another set of routes with a different layout here */} 34 | 35 | ); 36 | -------------------------------------------------------------------------------- /source/webui/src/Layout.tsx: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { useContext } from "react"; 5 | import { AppLayout, Flashbar } from "@cloudscape-design/components"; 6 | import SideNavigationBar from "./components/navigation/SideNavigationBar.tsx"; 7 | import { NotificationContext } from "./contexts/NotificationContext.tsx"; 8 | import { Outlet } from "react-router-dom"; 9 | import { Breadcrumbs } from "./components/navigation/Breadcrumbs.tsx"; 10 | import TopNavigationBar from "./components/navigation/TopNavigationBar.tsx"; 11 | 12 | export default function Layout() { 13 | const { notifications } = useContext(NotificationContext); 14 | 15 | return ( 16 | <> 17 |
18 | 19 |
20 |
21 | 25 | 26 |
27 | } 28 | contentType={"dashboard"} 29 | breadcrumbs={} 30 | navigation={} 31 | notifications={} 32 | stickyNotifications={true} 33 | ariaLabels={{ 34 | navigation: "Navigation drawer", 35 | navigationClose: "Close navigation drawer", 36 | navigationToggle: "Open navigation drawer", 37 | notifications: "Notifications", 38 | tools: "Help panel", 39 | toolsClose: "Close help panel", 40 | toolsToggle: "Open help panel", 41 | }} 42 | /> 43 | 44 | 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /source/webui/src/utils/iotPolicy.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { fetchAuthSession } from "aws-amplify/auth"; 5 | 6 | export const attachIoTPolicy = async (policyName: string): Promise => { 7 | try { 8 | let session; 9 | let retries = 0; 10 | const maxRetries = 5; 11 | 12 | // Retry with exponential backoff to handle the case where 13 | // the identity provider hasn't yet issued an identity 14 | // ID immediately after login. 15 | while (retries < maxRetries) { 16 | session = await fetchAuthSession({ forceRefresh: retries > 0 }); 17 | console.log(`IoT policy retry ${retries + 1}/${maxRetries}: identityId=${session.identityId ? 'found' : 'not found'}`); 18 | 19 | 20 | if (session.identityId) { 21 | break; 22 | } 23 | 24 | retries++; 25 | if (retries < maxRetries) { 26 | await new Promise((resolve) => setTimeout(resolve, 1000 * (2 ** retries))); 27 | } 28 | } 29 | 30 | if (!session || !session.identityId) { 31 | throw new Error("No identity ID found after retries"); 32 | } 33 | 34 | const response = await fetch("/aws-exports.json"); 35 | const config = await response.json(); 36 | 37 | const iotClient = await import("@aws-sdk/client-iot"); 38 | const { IoTClient, AttachPolicyCommand } = iotClient; 39 | 40 | const client = new IoTClient({ 41 | region: config.UserFilesBucketRegion, 42 | credentials: session.credentials, 43 | }); 44 | 45 | await client.send( 46 | new AttachPolicyCommand({ 47 | policyName, 48 | target: session.identityId, 49 | }) 50 | ); 51 | } catch (error) { 52 | console.error("Failed to attach IoT policy:", error); 53 | throw error; 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /source/metrics-utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "metrics-utils", 3 | "version": "4.0.2", 4 | "main": "index.ts", 5 | "license": "Apache-2.0", 6 | "description": "Distributed Load Testing on AWS Ops Metrics", 7 | "author": { 8 | "name": "Amazon Web Services", 9 | "url": "https://aws.amazon.com/solutions" 10 | }, 11 | "scripts": { 12 | "cleanup": "tsc --build ./ --clean && rm -rf node_modules", 13 | "cleanup:tsc": "tsc --build ./ --clean", 14 | "cleanup:dist": "rm -rf dist", 15 | "build:tsc": "npm ci && tsc", 16 | "build-init": "npm run cleanup:dist && rm -rf coverage && mkdir dist && mkdir dist/helpers", 17 | "build:copy": "cd lambda && for file in `find . -name '*.js' | egrep -v '__tests__|node_modules|lib|test'`;do echo \"Copying $file\"; cp -pr $file ../dist/$file; done", 18 | "build:install": "cp package.json dist/ && cp package-lock.json dist/ && cd dist && ls -ltRr && npm ci --production", 19 | "build": "npm run build:tsc && npm run build-init && npm run build:copy && npm run build:install", 20 | "watch": "tsc -w", 21 | "test": "jest --coverage", 22 | "zip": "cd dist && zip -rq metrics-utils.zip ." 23 | }, 24 | "devDependencies": { 25 | "@types/jest": "^29.5.12", 26 | "@types/node": "^20.6.1", 27 | "jest": "^29.6.2", 28 | "ts-jest": "^29.2.0" 29 | }, 30 | "dependencies": { 31 | "@aws-sdk/client-cloudwatch": "^3.637.0", 32 | "@aws-sdk/client-cloudwatch-logs": "^3.637.0", 33 | "@aws-sdk/client-sqs": "^3.637.0", 34 | "@aws-solutions-constructs/aws-eventbridge-lambda": "2.86.0", 35 | "@aws-solutions-constructs/aws-lambda-sqs-lambda": "2.86.0", 36 | "@types/aws-lambda": "^8.10.143", 37 | "axios": "^1.8.3", 38 | "esbuild": "^0.25.5" 39 | }, 40 | "overrides": { 41 | "form-data": "4.0.4", 42 | "aws-cdk-lib": "2.223.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /source/infrastructure/README.md: -------------------------------------------------------------------------------- 1 | # Guide to deploying Distributed Load Testing on AWS (DLT) using cdk 2 | 3 | To support custom deployments of DLT, solution's IaC is developed with AWS CDK. 4 | 5 | #### Pre-requisites 6 | Following instructions for `cdk deploy` require docker engine running 7 | Make sure to run the docker login command for public.aws.ecr/v2 8 | `aws ecr-public get-login-password --region us-east-1 | docker login --username AWS --password-stdin public.ecr.aws/v2` 9 | 10 | #### Deploy primary stack 11 | Time to deploy: ~ 10 minutes 12 | 13 | ```shell 14 | # install dependencies 15 | npm ci 16 | npm run install:all 17 | 18 | # bootstrap cdk environment 19 | npx cdk bootstrap --profile {myProfile} 20 | 21 | # deploy stack 22 | npx cdk deploy DLTStack --profile {myProfile} --parameters AdminName={myAdmin} --parameters AdminEmail={myEmail} 23 | ``` 24 | - myProfile - aws profile with required permissions to deploy in your account 25 | - myAdmin - username to login to DLT web portal 26 | - myEmail - email address to receive temporary login credentials 27 | 28 | #### Deploy regional stack 29 | Time to deploy: ~ 5 minutes 30 | 31 | ```shell 32 | # bootstrap cdk environment 33 | AWS_REGION={myRegion} npx cdk bootstrap --profile {myProfile} 34 | 35 | AWS_REGION={myRegion} npx cdk deploy RegionalDLTStack --profile {myProfile} --parameters ScenariosBucket={myBucket} \ 36 | --parameters ScenariosTable={myTable} --parameters PrimaryStackRegion={myPrimaryRegion} 37 | ``` 38 | - myRegion - aws region for deploying regional DLT stack, to perform load test from that region 39 | - myBucket - s3 bucket from primary stack output 40 | - myTable - dynamodb table from primary stack output 41 | - myPrimaryRegion - region of deployment for primary stack 42 | 43 | _Note: AWS_REGION={myRegion} can be prepended to `cdk deploy` to change region of deployment_ 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /source/webui/src/contexts/NotificationContext.tsx: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { createContext, ReactNode, useEffect, useState } from "react"; 5 | import { FlashbarProps } from "@cloudscape-design/components"; 6 | import { useDispatch, useSelector } from "react-redux"; 7 | import { deleteNotification, selectNotifications } from "../store/notificationsSlice.ts"; 8 | 9 | /** 10 | * NotificationContext provides the notifications to the global FlashBar 11 | * and any component that needs to use them. 12 | * 13 | * The notifications are stored in the redux store, 14 | * but NotificationContext adds the onDismiss method to each notification object 15 | * which is not serializable and cannot be stored in redux. 16 | */ 17 | export type NotificationContextType = { 18 | notifications: ReadonlyArray; 19 | }; 20 | 21 | export const NotificationContext = createContext(null as unknown as NotificationContextType); 22 | export const NotificationContextProvider = (props: { children: ReactNode }) => { 23 | const storeNotifications = useSelector(selectNotifications); 24 | const dispatch = useDispatch(); 25 | 26 | const initialState: ReadonlyArray = []; 27 | const [notifications, setNotifications] = useState(initialState); 28 | 29 | useEffect(() => { 30 | setNotifications( 31 | storeNotifications.map((it) => ({ 32 | dismissible: true, 33 | onDismiss: () => dispatch(deleteNotification({ id: it.id })), 34 | ...it, 35 | })) 36 | ); 37 | }, [storeNotifications]); 38 | 39 | return ( 40 | <> 41 | {props.children} 42 | 43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /source/webui/src/components/navigation/Breadcrumbs.tsx: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { BreadcrumbGroup } from "@cloudscape-design/components"; 5 | import { useLocation, useNavigate, useParams } from "react-router-dom"; 6 | import { useGetScenarioDetailsQuery, useGetTestRunDetailsQuery } from "../../store/scenariosApiSlice"; 7 | import { createBreadcrumbs } from "./create-breadcrumbs.ts"; 8 | 9 | export const Breadcrumbs = () => { 10 | const location = useLocation(); 11 | const navigate = useNavigate(); 12 | const path = location.pathname; 13 | const { testId, testRunId } = useParams<{ testId: string; testRunId: string }>(); 14 | 15 | // Fetch scenario name for /scenario/{testId} endpoints in order to display scenario name in breadcrumb 16 | const { data: scenario } = useGetScenarioDetailsQuery( 17 | { testId: testId || "" }, 18 | { skip: !testId } 19 | ); 20 | 21 | // Fetch test run for /scenario/{testId}/testruns/{testRunId} endpoints in order to display test run date in breadcrumb 22 | const { data: testRun } = useGetTestRunDetailsQuery( 23 | { testId: testId || "", testRunId: testRunId || "" }, 24 | { skip: !testId || !testRunId } 25 | ); 26 | 27 | // Only use scenario name when we're actually on a scenario route 28 | const isScenarioRoute = path.includes('/scenarios/'); 29 | const scenarioName = isScenarioRoute ? scenario?.testName : undefined; 30 | 31 | const breadCrumbItems = createBreadcrumbs( 32 | path, 33 | scenarioName, 34 | testRunId, 35 | testId, 36 | testRun?.startTime 37 | ); 38 | 39 | return ( 40 | 47 | ); 48 | }; 49 | -------------------------------------------------------------------------------- /source/webui/src/__tests__/pages/CreateTestScenarioPage.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { describe, expect, test } from "vitest"; 5 | import { TestTypes } from "../../pages/scenarios/constants"; 6 | 7 | const getFileExtension = (testType: string) => { 8 | switch (testType) { 9 | case TestTypes.JMETER: 10 | return ".jmx"; 11 | case TestTypes.K6: 12 | return ".js"; 13 | case TestTypes.LOCUST: 14 | return ".py"; 15 | default: 16 | return ""; 17 | } 18 | }; 19 | 20 | const validateFileSize = (files: File[]) => { 21 | const maxSize = 100 * 1024 * 1024; // 100MB 22 | const validFiles = files.filter((file) => file.size <= maxSize); 23 | return { 24 | validFiles, 25 | hasError: validFiles.length !== files.length, 26 | errorMessage: "File size must be 100MB or less", 27 | }; 28 | }; 29 | 30 | describe("getFileExtension", () => { 31 | test("returns correct extensions", () => { 32 | expect(getFileExtension("jmeter")).toBe(".jmx"); 33 | expect(getFileExtension("k6")).toBe(".js"); 34 | expect(getFileExtension("locust")).toBe(".py"); 35 | expect(getFileExtension("unknown")).toBe(""); 36 | }); 37 | }); 38 | 39 | describe("validateFileSize", () => { 40 | test("validates file size correctly", () => { 41 | const validFile = new File(["content"], "test.jmx", { type: "text/xml" }); 42 | const largeFile = Object.defineProperty(new File(["content"], "large.jmx"), "size", { value: 200 * 1024 * 1024 }); 43 | 44 | const validResult = validateFileSize([validFile]); 45 | expect(validResult.hasError).toBe(false); 46 | 47 | const invalidResult = validateFileSize([largeFile]); 48 | expect(invalidResult.hasError).toBe(true); 49 | expect(invalidResult.errorMessage).toBe("File size must be 100MB or less"); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /source/webui/src/mocks/browser.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { Amplify } from "aws-amplify"; 5 | 6 | /** 7 | * This function enables mock-service-worker (msw) in the browser, so you can do local frontend development against the mock handlers. 8 | * 9 | * Only if aws-exports.json file is NOT present or does NOT contain the API endpoint config, msw will be enabled. 10 | * If the API config is present, requests will be sent to the API. 11 | * 12 | * @param apiEndpoint 13 | */ 14 | export async function startMockServer(apiEndpoint: string) { 15 | const config = Amplify.getConfig(); 16 | 17 | // if aws-exports.json is present and contains an API endpoint, do not enable mocking 18 | const isBackendConfigured = !!config.API?.REST?.["solution-api"]?.endpoint; 19 | 20 | console.log("🔧 Mock Server Debug:", { 21 | apiEndpoint, 22 | config: config.API?.REST, 23 | isBackendConfigured, 24 | willEnableMocking: !isBackendConfigured, 25 | }); 26 | 27 | if (isBackendConfigured) { 28 | console.log("✅ Backend configured - MSW disabled"); 29 | return Promise.resolve(); 30 | } 31 | 32 | console.log("🎭 Starting Mock Service Worker..."); 33 | const { setupWorker } = await import("msw/browser"); 34 | const { handlers } = await import("./handlers"); 35 | 36 | const worker = setupWorker(...handlers(apiEndpoint)); 37 | // `worker.start()` returns a Promise that resolves 38 | // once the Service Worker is up and ready to intercept requests. 39 | return worker 40 | .start({ 41 | onUnhandledRequest(request, print) { 42 | // Print MSW unhandled request warning, to detect requests that are not handled by MSW 43 | print.warning(); 44 | }, 45 | }) 46 | .then(() => { 47 | console.log("🎭 Mock Service Worker started successfully"); 48 | }); 49 | } 50 | -------------------------------------------------------------------------------- /source/mcp-server/src/tools/get-latest-test-run.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { z } from "zod"; 5 | import { parseEventWithSchema, TEST_SCENARIO_ID_LENGTH, TEST_SCENARIO_ID_REGEX, type AgentCoreEvent } from "../lib/common"; 6 | import { AppError } from "../lib/errors"; 7 | import { IAMHttpClient, type HttpResponse } from "../lib/http-client"; 8 | 9 | // Zod schema for get_latest_test_run parameters 10 | export const GetLatestTestRunSchema = z.object({ 11 | test_id: z.string() 12 | .length(TEST_SCENARIO_ID_LENGTH, `test_id should be the ${TEST_SCENARIO_ID_LENGTH} character unique id for a test scenario`) 13 | .regex(TEST_SCENARIO_ID_REGEX, "Invalid test_id") 14 | }); 15 | 16 | // TypeScript type derived from Zod schema 17 | export type GetLatestTestRunParameters = z.infer; 18 | 19 | /** 20 | * Handle get_latest_test_run tool 21 | */ 22 | export async function handleGetLatestTestRun(httpClient: IAMHttpClient, apiEndpoint: string, event: AgentCoreEvent): Promise { 23 | const { test_id } = parseEventWithSchema(GetLatestTestRunSchema, event); 24 | 25 | let response: HttpResponse; 26 | try { 27 | response = await httpClient.get(`${apiEndpoint}/scenarios/${test_id}/testruns?limit=1`); 28 | } catch (error) { 29 | throw new AppError("Internal request failed", 500); 30 | } 31 | 32 | if (response.statusCode !== 200) { 33 | throw new AppError(response.body, response.statusCode); 34 | } 35 | 36 | const data = JSON.parse(response.body); 37 | if (!data) { 38 | throw new AppError(`Test run data not found: ${test_id}`, 404); 39 | } 40 | 41 | const testRuns = data["testRuns"]; 42 | if (!testRuns || testRuns.length === 0) { 43 | throw new AppError(`No test runs found for test_id: ${test_id}`, 404); 44 | } 45 | 46 | return testRuns[0]; 47 | } 48 | -------------------------------------------------------------------------------- /source/webui/src/pages/scenarios/types/createTest.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | export type CreateScenarioRequest = { 5 | testId: string | undefined; 6 | testName: string; 7 | testDescription: string; 8 | testTaskConfigs: TestTaskConfig[]; 9 | testScenario: TestScenario; 10 | testType: "simple" | "jmeter" | "k6" | "locust"; 11 | fileType: string | undefined; 12 | showLive: boolean; 13 | regionalTaskDetails: Record; 14 | tags: string[]; 15 | // Scheduling Options 16 | scheduleDate?: string; 17 | scheduleTime?: string; 18 | scheduleStep?: string; 19 | cronValue?: string; 20 | cronExpiryDate?: string; 21 | recurrence?: string; 22 | }; 23 | 24 | export type TestTaskConfig = { 25 | concurrency: number; 26 | taskCount: number; 27 | region: string; 28 | }; 29 | 30 | export type TestScenario = { 31 | execution: TestScenarioExecution[]; 32 | scenarios: Record; 33 | }; 34 | 35 | export type RegionalTaskDetail = { 36 | vCPULimit: number; 37 | vCPUsPerTask: number; 38 | vCPUsInUse: number; 39 | dltTaskLimit: number; 40 | dltAvailableTasks: number; 41 | }; 42 | 43 | export type TestScenarioExecution = { 44 | // number value appended with time unit (e.g. 30s or 2m) 45 | "ramp-up": string; 46 | // number value appended with time unit (e.g. 30s or 2m) 47 | "hold-for": string; 48 | scenario: string; 49 | executor: "jmeter" | "k6" | "locust" | undefined; 50 | }; 51 | 52 | export type TestScenarioSimpleDefinition = { 53 | requests: TestScenariosSimpleRequest[]; 54 | }; 55 | 56 | export type TestScenarioScriptDefinition = { 57 | script: string; 58 | }; 59 | 60 | export type TestScenariosSimpleRequest = { 61 | url: string; 62 | method: string; 63 | headers: any; 64 | body?: string; 65 | }; 66 | -------------------------------------------------------------------------------- /source/webui/src/pages/scenarios/hooks/useFormData.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Custom hook for managing test scenario form state 5 | 6 | import { useCallback, useState } from "react"; 7 | import { TestTypes } from "../constants"; 8 | import { FormData } from "../types"; 9 | 10 | const INITIAL_FORM_DATA: FormData = { 11 | testName: "", 12 | testDescription: "", 13 | testId: "", 14 | testType: TestTypes.SIMPLE, 15 | executionTiming: "run-now", 16 | showLive: false, 17 | scriptFile: [], 18 | fileError: "", 19 | tags: [], 20 | httpEndpoint: "", 21 | httpMethod: { label: "GET", value: "GET" }, 22 | requestHeaders: "", 23 | bodyPayload: "", 24 | scheduleTime: "", 25 | scheduleDate: "", 26 | cronMinutes: "", 27 | cronHours: "", 28 | cronDayOfMonth: "", 29 | cronMonth: "", 30 | cronDayOfWeek: "", 31 | cronExpiryDate: "", 32 | regions: [], 33 | rampUpValue: "", 34 | rampUpUnit: "minutes", 35 | holdForValue: "", 36 | holdForUnit: "minutes" 37 | }; 38 | 39 | export const useFormData = () => { 40 | const [formData, setFormData] = useState(() => ({ ...INITIAL_FORM_DATA })); 41 | 42 | const updateFormData = useCallback((updates: Partial) => { 43 | setFormData(prev => { 44 | const newData = { ...prev, ...updates }; 45 | try { 46 | const dataToSave = { ...newData, scriptFile: [] }; 47 | localStorage.setItem('dlt-current-draft', JSON.stringify(dataToSave)); 48 | } catch (error) { 49 | console.warn('Failed to save form data:', error); 50 | } 51 | return newData; 52 | }); 53 | }, []); 54 | 55 | const resetFormData = useCallback(() => { 56 | localStorage.removeItem('dlt-current-draft'); 57 | setFormData({ ...INITIAL_FORM_DATA }); 58 | }, []); 59 | 60 | return { formData, setFormData, updateFormData, resetFormData }; 61 | }; 62 | -------------------------------------------------------------------------------- /source/mcp-server/src/tools/get-test-run.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { z } from "zod"; 5 | import { parseEventWithSchema, TEST_RUN_ID_LENGTH, TEST_RUN_ID_REGEX, TEST_SCENARIO_ID_LENGTH, TEST_SCENARIO_ID_REGEX, type AgentCoreEvent } from "../lib/common"; 6 | import { AppError } from "../lib/errors"; 7 | import { IAMHttpClient, type HttpResponse } from "../lib/http-client"; 8 | 9 | // Zod schema for get_test_run parameters 10 | export const GetTestRunSchema = z.object({ 11 | test_id: z.string() 12 | .length(TEST_SCENARIO_ID_LENGTH, `test_id should be the ${TEST_SCENARIO_ID_LENGTH} character unique id for a test scenario`) 13 | .regex(TEST_SCENARIO_ID_REGEX, "Invalid test_id"), 14 | test_run_id: z.string() 15 | .length(TEST_RUN_ID_LENGTH, `test_run_id should be the ${TEST_RUN_ID_LENGTH} character unique id for a test run`) 16 | .regex(TEST_RUN_ID_REGEX, "Invalid test_run_id") 17 | }); 18 | 19 | // TypeScript type derived from Zod schema 20 | export type GetTestRunParameters = z.infer; 21 | 22 | /** 23 | * Handle get_test_run tool 24 | */ 25 | export async function handleGetTestRun(httpClient: IAMHttpClient, apiEndpoint: string, event: AgentCoreEvent): Promise { 26 | const { test_id, test_run_id } = parseEventWithSchema(GetTestRunSchema, event); 27 | 28 | let response: HttpResponse; 29 | try { 30 | response = await httpClient.get(`${apiEndpoint}/scenarios/${test_id}/testruns/${test_run_id}`); 31 | } catch (error) { 32 | throw new AppError("Internal request failed", 500); 33 | } 34 | 35 | if (response.statusCode !== 200) { 36 | throw new AppError(response.body, response.statusCode); 37 | } 38 | 39 | const data = JSON.parse(response.body); 40 | if (!data) { 41 | throw new AppError(`Test run not found: ${test_id}/${test_run_id}`, 404); 42 | } 43 | 44 | return data; 45 | } 46 | -------------------------------------------------------------------------------- /source/custom-resource/lib/metrics/index.spec.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | const axios = require("axios"); 5 | const MockAdapter = require("axios-mock-adapter"); 6 | 7 | const lambda = require("./index.js"); 8 | 9 | const _config = { 10 | SolutionId: "SO00XX", 11 | Version: "testVersion", 12 | UUID: "999-999", 13 | Region: "testRegion", 14 | existingVPC: "testTest", 15 | AccountId: "123456789012", 16 | }; 17 | 18 | describe("#SEND METRICS", () => { 19 | beforeEach(() => { 20 | process.env.METRIC_URL = "TestEndpoint"; 21 | }); 22 | 23 | afterEach(() => { 24 | delete process.env.METRIC_URL; 25 | }); 26 | 27 | it("send metrics success", async () => { 28 | // Arrange 29 | const expected_metric_object = { 30 | Solution: _config.SolutionId, 31 | Version: _config.Version, 32 | UUID: _config.UUID, 33 | Data: { 34 | Type: "Create", 35 | Region: _config.Region, 36 | ExistingVpc: _config.existingVPC, 37 | AccountId: _config.AccountId, 38 | }, 39 | }; 40 | const mock = new MockAdapter(axios); 41 | mock.onPost().reply(200); 42 | 43 | // Act 44 | await lambda.send(_config, "Create"); 45 | 46 | // Assert 47 | expect(mock.history.post.length).toEqual(1); // called once 48 | expect(mock.history.post[0].url).toEqual(process.env.METRIC_URL); 49 | expect(typeof Date.parse(JSON.parse(mock.history.post[0].data).TimeStamp)).toEqual("number"); // epoch time 50 | expect(JSON.parse(mock.history.post[0].data)).toMatchObject(expected_metric_object); 51 | }); 52 | 53 | it("should not throw error, when metric send fails", async () => { 54 | // Arrange 55 | let mock = new MockAdapter(axios); 56 | mock.onPost().networkError(); 57 | 58 | // Act 59 | await lambda.send(_config, "Create"); 60 | 61 | // Assert 62 | expect(mock.history.post.length).toBe(1); // called once 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /source/integration-tests/src/utils.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { aws4Interceptor } from "aws4-axios"; 5 | import Ajv from "ajv"; 6 | import axios, { AxiosError, AxiosResponse } from "axios"; 7 | import { load } from "../api.config"; 8 | import { ScenarioResponse } from "./scenario"; 9 | 10 | const config = load(); 11 | 12 | export interface ErrorResponse { 13 | status: number; 14 | code: string; 15 | data: string; 16 | } 17 | 18 | const setupAxiosInterceptors = () => { 19 | const interceptor = aws4Interceptor({ 20 | options: { 21 | region: config.region, 22 | service: "execute-api", 23 | }, 24 | credentials: { 25 | accessKeyId: config.accessKeyId, 26 | secretAccessKey: config.secretAccessKey, 27 | sessionToken: config.sessionToken, 28 | }, 29 | }); 30 | 31 | axios.interceptors.request.use(interceptor); 32 | axios.interceptors.response.use( 33 | (response: AxiosResponse): AxiosResponse => response, 34 | (error: AxiosError): ErrorResponse => ({ 35 | status: error.response.status, 36 | code: error.response.statusText.toUpperCase().replace(/ /g, "_"), 37 | data: error.response.data, 38 | }) 39 | ); 40 | }; 41 | 42 | const teardownAxiosInterceptors = () => { 43 | axios.interceptors.request.clear(); 44 | axios.interceptors.response.clear(); 45 | }; 46 | 47 | const validateScenario = (item: ScenarioResponse): boolean => { 48 | const ajv = new Ajv(); 49 | const schema = { 50 | type: "object", 51 | properties: { 52 | testId: { type: "string" }, 53 | testName: { type: "string" }, 54 | status: { type: "string" }, 55 | }, 56 | required: ["testId", "testName", "status"], 57 | additionalProperties: true, 58 | }; 59 | 60 | const validate = ajv.compile(schema); 61 | return validate(item); 62 | }; 63 | 64 | export { setupAxiosInterceptors, teardownAxiosInterceptors, validateScenario }; 65 | -------------------------------------------------------------------------------- /source/infrastructure/test/step-functions.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { App, DefaultStackSynthesizer, Stack } from "aws-cdk-lib"; 5 | import { TaskRunnerStepFunctionConstruct } from "../lib/back-end/step-functions"; 6 | import { Code, Runtime, Function } from "aws-cdk-lib/aws-lambda"; 7 | import { Bucket } from "aws-cdk-lib/aws-s3"; 8 | import { Effect, PolicyDocument, PolicyStatement, Role, ServicePrincipal } from "aws-cdk-lib/aws-iam"; 9 | import { createTemplateWithoutS3Key } from "./snapshot_helpers"; 10 | 11 | test("DLT API Test", () => { 12 | const app = new App(); 13 | const stack = new Stack(app, "DLTStack", { 14 | synthesizer: new DefaultStackSynthesizer({ 15 | generateBootstrapVersionRule: false, 16 | }), 17 | }); 18 | 19 | const testRole = new Role(stack, "TestRole", { 20 | assumedBy: new ServicePrincipal("lambda.amazonaws.com"), 21 | inlinePolicies: { 22 | DenyPolicy: new PolicyDocument({ 23 | statements: [ 24 | new PolicyStatement({ 25 | effect: Effect.DENY, 26 | actions: ["*"], 27 | resources: ["*"], 28 | }), 29 | ], 30 | }), 31 | }, 32 | }); 33 | 34 | const codeBucket = Bucket.fromBucketName(stack, "SourceCodeBucket", "testbucket"); 35 | const testLambda = new Function(stack, "TestFunction", { 36 | code: Code.fromBucket(codeBucket, "custom-resource.zip"), 37 | handler: "index.handler", 38 | runtime: Runtime.NODEJS_20_X, 39 | role: testRole, 40 | }); 41 | 42 | const testStateMachine = new TaskRunnerStepFunctionConstruct(stack, "TaskRunnerStepFunction", { 43 | taskStatusChecker: testLambda, 44 | taskRunner: testLambda, 45 | resultsParser: testLambda, 46 | taskCanceler: testLambda, 47 | metricFilterCleaner: testLambda, 48 | suffix: "abc-def-xyz", 49 | }); 50 | 51 | expect(createTemplateWithoutS3Key(stack)).toMatchSnapshot(); 52 | expect(testStateMachine.taskRunnerStepFunctions).toBeDefined(); 53 | }); 54 | -------------------------------------------------------------------------------- /deployment/ecr/distributed-load-testing-on-aws-load-tester/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/amazonlinux/amazonlinux:2023-minimal@sha256:3f6c5a2858113e9bb6710dfccdace7dc698e83f7a012240a1d07b3a46d273999 2 | 3 | RUN dnf upgrade -y --refresh && \ 4 | dnf install -y python3.11 python3.11-pip java-21-amazon-corretto bc procps jq findutils unzip && \ 5 | dnf clean all 6 | 7 | ENV PIP_INSTALL="pip3.11 install --no-cache-dir" 8 | 9 | 10 | # install bzt 11 | RUN $PIP_INSTALL --upgrade pip && \ 12 | $PIP_INSTALL --upgrade bzt awscli setuptools==78.1.1 h11 && \ 13 | $PIP_INSTALL --upgrade bzt 14 | COPY ./.bzt-rc /root/.bzt-rc 15 | RUN chmod 755 /root/.bzt-rc 16 | 17 | # install bzt tools 18 | RUN bzt -install-tools -o modules.install-checker.exclude=selenium,gatling,tsung,siege,ab,k6,external-results-loader,junit,testng,rspec,mocha,nunit,xunit,wdio,robot,newman,playwright 19 | RUN rm -rf /root/.bzt/selenium-taurus 20 | RUN mkdir /bzt-configs /tmp/artifacts 21 | ADD ./load-test.sh /bzt-configs/ 22 | ADD ./*.jar /bzt-configs/ 23 | ADD ./*.py /bzt-configs/ 24 | 25 | RUN chmod 755 /bzt-configs/load-test.sh 26 | RUN chmod 755 /bzt-configs/ecslistener.py 27 | RUN chmod 755 /bzt-configs/ecscontroller.py 28 | RUN chmod 755 /bzt-configs/jar_updater.py 29 | RUN python3.11 /bzt-configs/jar_updater.py 30 | 31 | # Remove jar files from /tmp 32 | RUN rm -rf /tmp/jmeter-plugins-manager-1* && \ 33 | rm -rf /usr/local/lib/python3.11/site-packages/setuptools-65.5.0.dist-info && \ 34 | rm -rf /usr/local/lib/python3.11/site-packages/urllib3-1.26.17.dist-info 35 | 36 | # Add settings file to capture the output logs from bzt cli 37 | RUN mkdir -p /etc/bzt.d && echo '{"settings": {"artifacts-dir": "/tmp/artifacts"}}' > /etc/bzt.d/90-artifacts-dir.json 38 | 39 | # Temporary fix for CVE-2025-66418 and CVE-2025-66471 in urllib3 2.5.0 40 | # This must be upgraded after bzt setup to satisfy bzt requirements.txt 41 | # https://github.com/Blazemeter/taurus/blob/647dd34ab318b5d3060a8c6ce2e5b047a0efddd2/requirements.txt 42 | RUN $PIP_INSTALL 'urllib3>=2.6.0' 43 | 44 | WORKDIR /bzt-configs 45 | ENTRYPOINT ["./load-test.sh"] 46 | -------------------------------------------------------------------------------- /source/webui/src/__tests__/test-utils.tsx: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /* global vi */ 5 | 6 | import { configureStore } from "@reduxjs/toolkit"; 7 | import { Provider } from "react-redux"; 8 | import { NotificationContextProvider } from "../contexts/NotificationContext.tsx"; 9 | import { MemoryRouter } from "react-router-dom"; 10 | import { AppRoutes } from "../AppRoutes.tsx"; 11 | import { render } from "@testing-library/react"; 12 | import { rootReducer, RootState } from "../store/store.ts"; 13 | import { solutionApi } from "../store/solutionApi.ts"; 14 | 15 | // Mock the useAuthenticator hook to return a mock user 16 | vi.mock("@aws-amplify/ui-react", () => ({ 17 | useAuthenticator: () => ({ 18 | user: { 19 | username: "test-user", 20 | userId: "test-user-id", 21 | signInDetails: { 22 | loginId: "test-user@example.com", 23 | authFlowType: "USER_SRP_AUTH", 24 | }, 25 | }, 26 | signOut: vi.fn(), 27 | }), 28 | })); 29 | 30 | /* 31 | * Render a page within the context of a Router, redux store, and NotificationContext. 32 | * 33 | * This function provides setup for component tests that 34 | * - interact with the store state, 35 | * - navigate between pages 36 | * - emit notifications 37 | * - use mocked authentication 38 | */ 39 | export function renderAppContent(props?: { preloadedState?: Partial; initialRoute: string }) { 40 | const store = configureStore({ 41 | reducer: rootReducer, 42 | preloadedState: props?.preloadedState ?? {}, 43 | middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(solutionApi.middleware), 44 | }); 45 | 46 | const renderResult = render( 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | ); 55 | return { 56 | renderResult, 57 | store, 58 | }; 59 | } 60 | -------------------------------------------------------------------------------- /source/mcp-server/src/lib/metrics.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { getMetricUrl, getSolutionId, getUuid, getVersion } from './config'; 5 | 6 | export type ToolUsageMetric = { 7 | Type: string; 8 | MetricSchemaVersion: number; 9 | UserAgent: string; 10 | ToolName: string; 11 | TokenCount: number; 12 | DurationMs: number; 13 | Status: "success" | "failure"; 14 | StatusCode: number; 15 | }; 16 | 17 | export const toolUsageMetricType = "ToolUsage"; 18 | export const toolUsageMetricSchemaVersion = 1; 19 | export const toolUsageUserAgent = "dlt-mcp-server"; 20 | 21 | /** 22 | * Approximates token count using 1 token = 4 characters rule. 23 | * Actual token count is dependent on the LLM which we are not aware of during tool invocations. 24 | * @param text - the text to calculate tokens for 25 | * @returns estimated token count 26 | */ 27 | export function approximateTokenCount(text: string): number { 28 | return Math.ceil(text.length / 4); 29 | } 30 | 31 | /** 32 | * Sends anonymized tool usage metrics 33 | * @param metric - the tool usage data 34 | * @returns HTTP status code or undefined if failed 35 | */ 36 | export async function sendToolUsageMetric(metric: ToolUsageMetric): Promise { 37 | try { 38 | const metrics = { 39 | Solution: getSolutionId(), 40 | UUID: getUuid(), 41 | // Date and time instant in a java.sql.Timestamp compatible format 42 | TimeStamp: new Date().toISOString().replace("T", " ").replace("Z", ""), 43 | Version: getVersion(), 44 | Data: metric, 45 | }; 46 | 47 | const response = await fetch(getMetricUrl(), { 48 | method: "POST", 49 | headers: { 50 | "Content-Type": "application/json", 51 | }, 52 | body: JSON.stringify(metrics), 53 | }); 54 | 55 | if (response.status !== 200) { 56 | console.error(`Failed to send tool usage metrics: ${response.status} ${response.statusText}`) 57 | } 58 | 59 | } catch (err) { 60 | // silently catch errors 61 | console.error("Failed to send tool usage metrics:", err); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /source/custom-resource/regional-index.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | const uuid = require("uuid"); 5 | const cfn = require("./lib/cfn"); 6 | const metrics = require("./lib/metrics"); 7 | const storeConfig = require("./lib/config-storage"); 8 | const iot = require("./lib/iot"); 9 | 10 | exports.handler = async (event, context) => { 11 | console.log(`Custom resource: ${event.ResourceProperties?.Resource}, RequestType: ${event.RequestType}`); 12 | 13 | const resource = event.ResourceProperties.Resource; 14 | const config = event.ResourceProperties; 15 | const requestType = event.RequestType; 16 | let responseData = {}; 17 | 18 | try { 19 | switch (resource) { 20 | case "TestingResourcesConfigFile": 21 | if (requestType === "Delete") { 22 | await storeConfig.delTestingResourcesConfigFile(config.TestingResourcesConfig); 23 | } else { 24 | await storeConfig.testingResourcesConfigFile(config.TestingResourcesConfig); 25 | } 26 | break; 27 | case "UUID": 28 | if (requestType === "Create") { 29 | responseData = { 30 | UUID: uuid.v4(), 31 | SUFFIX: uuid.v4().slice(-10), 32 | }; 33 | } 34 | break; 35 | case "GetIotEndpoint": 36 | if (requestType !== "Delete") { 37 | const iotEndpoint = await iot.getIotEndpoint(); 38 | responseData = { 39 | IOT_ENDPOINT: iotEndpoint, 40 | }; 41 | } 42 | break; 43 | case "Metric": 44 | await metrics.send(config, requestType); 45 | break; 46 | default: 47 | throw Error(`${resource} not supported`); 48 | } 49 | await cfn.send(event, context, "SUCCESS", responseData, resource); 50 | } catch (err) { 51 | console.error(`Error in custom resource ${resource}: ${err.message}, Code: ${err.code || 'N/A'}, RequestType: ${requestType}`); 52 | await cfn.send(event, context, "FAILED", {}, resource); 53 | throw new Error(`Custom resource ${resource} failed: ${err.message || 'Unknown error'}`); 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /source/webui/src/pages/scenarios/components/TestRunsDateFilter.tsx: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import React, { useCallback } from "react"; 5 | import { DateRangePicker } from "@cloudscape-design/components"; 6 | 7 | const RELATIVE_DATE_OPTIONS = [ 8 | { key: "last-3-days", amount: 3, unit: "day" as const, type: "relative" as const }, 9 | { key: "last-7-days", amount: 7, unit: "day" as const, type: "relative" as const }, 10 | { key: "last-2-weeks", amount: 14, unit: "day" as const, type: "relative" as const }, 11 | { key: "last-month", amount: 30, unit: "day" as const, type: "relative" as const }, 12 | ]; 13 | 14 | const DATE_PICKER_I18N = { 15 | relativeRangeSelectionHeading: "Choose a range", 16 | clearButtonLabel: "Clear and dismiss", 17 | cancelButtonLabel: "Cancel", 18 | applyButtonLabel: "Apply", 19 | customRelativeRangeOptionLabel: "Custom range", 20 | customRelativeRangeOptionDescription: "Set a custom range in the past", 21 | customRelativeRangeDurationLabel: "Duration", 22 | customRelativeRangeUnitLabel: "Unit of time", 23 | }; 24 | 25 | interface DateFilterProps { 26 | dateFilter: any; 27 | onChange: (dateRange: any) => void; 28 | } 29 | 30 | const formatRelativeRange = (range: any) => { 31 | if (!range?.amount || !range.unit) return "Custom range"; 32 | const unit = range.amount === 1 ? range.unit : `${range.unit}s`; 33 | return `Last ${range.amount} ${unit}`; 34 | }; 35 | 36 | export const TestRunsDateFilter: React.FC = ({ dateFilter, onChange }) => { 37 | const handleChange = useCallback(({ detail }: any) => onChange(detail.value), [onChange]); 38 | 39 | return ( 40 | ({ valid: true })} 47 | placeholder="Filter by date range" 48 | expandToViewport 49 | i18nStrings={{ 50 | ...DATE_PICKER_I18N, 51 | formatRelativeRange, 52 | }} 53 | /> 54 | ); 55 | }; -------------------------------------------------------------------------------- /source/webui/src/components/navigation/SideNavigationBar.tsx: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { SideNavigation, SideNavigationProps } from "@cloudscape-design/components"; 5 | import { useCallback, useEffect, useState } from "react"; 6 | import { NavigateFunction, useLocation, useNavigate } from "react-router-dom"; 7 | 8 | export default function SideNavigationBar() { 9 | const navigate: NavigateFunction = useNavigate(); 10 | const [activeHref, setActiveHref] = useState("/"); 11 | 12 | const navigationItems: SideNavigationProps["items"] = [ 13 | { type: "link", text: "Dashboard", href: "/" }, 14 | { type: "link", text: "Test Scenarios", href: "/scenarios" }, 15 | { type: "link", text: "MCP Server", href: "/mcp-server" }, 16 | { type: "divider" }, 17 | { 18 | type: "link", 19 | external: true, 20 | href: "https://docs.aws.amazon.com/solutions/latest/distributed-load-testing-on-aws/solution-overview.html", 21 | text: "Documentation", 22 | }, 23 | { 24 | type: "link", 25 | external: true, 26 | href: "https://amazonmr.au1.qualtrics.com/jfe/form/SV_6mwYmfThkyrd7sq", 27 | text: "Give Feedback", 28 | }, 29 | ]; 30 | 31 | // follow the given router link and update the store with active path 32 | const handleFollow = useCallback( 33 | (event: Readonly): void => { 34 | if (event.detail.external || !event.detail.href) return; 35 | 36 | event.preventDefault(); 37 | 38 | const path = event.detail.href; 39 | navigate(path); 40 | }, 41 | [navigate] 42 | ); 43 | 44 | const location = useLocation(); 45 | useEffect(() => { 46 | const pathParts = location.pathname.split("/"); 47 | const topLevelPath = pathParts.length > 1 ? `/${pathParts[1]}` : "/"; 48 | setActiveHref(topLevelPath); 49 | }, [location]); 50 | 51 | const navHeader: SideNavigationProps.Header = { 52 | href: "/", 53 | text: "Distributed Load Testing on AWS", 54 | }; 55 | 56 | return ; 57 | } 58 | -------------------------------------------------------------------------------- /source/infrastructure/bin/distributed-load-testing-on-aws.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | import { App, DefaultStackSynthesizer } from "aws-cdk-lib"; 6 | import { DLTStack } from "../lib/distributed-load-testing-on-aws-stack"; 7 | import { Solution } from "./solution"; 8 | import { RegionalInfrastructureDLTStack } from "../lib/distributed-load-testing-on-aws-regional-stack"; 9 | 10 | // CDK and default deployment 11 | let synthesizer = new DefaultStackSynthesizer({ generateBootstrapVersionRule: false }); 12 | 13 | // Solutions pipeline deployment 14 | const { DIST_OUTPUT_BUCKET, SOLUTION_ID, SOLUTION_NAME, VERSION, PUBLIC_ECR_REGISTRY, PUBLIC_ECR_TAG } = process.env; 15 | if (DIST_OUTPUT_BUCKET && SOLUTION_NAME && VERSION && PUBLIC_ECR_REGISTRY && PUBLIC_ECR_TAG) 16 | synthesizer = new DefaultStackSynthesizer({ 17 | generateBootstrapVersionRule: false, 18 | fileAssetsBucketName: `${DIST_OUTPUT_BUCKET}-\${AWS::Region}`, 19 | bucketPrefix: `${SOLUTION_NAME}/${VERSION}/`, 20 | imageAssetsRepositoryName: PUBLIC_ECR_REGISTRY, 21 | dockerTagPrefix: PUBLIC_ECR_TAG, 22 | }); 23 | 24 | const app = new App(); 25 | const solutionName = app.node.tryGetContext("solutionName"); 26 | const solutionVersion = VERSION ?? app.node.tryGetContext("solutionVersion"); 27 | const solutionId = SOLUTION_ID ?? app.node.tryGetContext("solutionId"); 28 | const mainStackDescription = `(${solutionId}) - ${solutionName}. Version ${solutionVersion}`; 29 | const regionsStackDescription = `(${solutionId}-regional) - Distributed Load Testing on AWS testing resources regional deployment. Version ${solutionVersion}`; 30 | 31 | const solution = new Solution(solutionId, solutionName, solutionVersion, mainStackDescription); 32 | 33 | // main stack 34 | new DLTStack(app, "distributed-load-testing-on-aws", { 35 | synthesizer, 36 | solution, 37 | stackType: "main", 38 | }); 39 | 40 | // regional stack 41 | solution.description = regionsStackDescription; 42 | new RegionalInfrastructureDLTStack(app, "distributed-load-testing-on-aws-regional", { 43 | synthesizer, 44 | solution, 45 | stackType: "regional", 46 | }); 47 | -------------------------------------------------------------------------------- /source/webui/src/pages/scenarios/components/TagsSection.tsx: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Component for managing test scenario tags 5 | 6 | import { 7 | Container, 8 | Header, 9 | SpaceBetween, 10 | Box, 11 | TokenGroup, 12 | FormField, 13 | Input, 14 | Button, 15 | } from "@cloudscape-design/components"; 16 | import { FormData } from "../types"; 17 | 18 | interface Props { 19 | formData: FormData; 20 | newTag: string; 21 | setNewTag: (tag: string) => void; 22 | tagError: string; 23 | setTagError: (error: string) => void; 24 | addTag: () => void; 25 | removeTag: (index: number) => void; 26 | } 27 | 28 | export const TagsSection = ({ formData, newTag, setNewTag, tagError, setTagError, addTag, removeTag }: Props) => ( 29 | Tags }> 30 | 31 | 32 | Tags are labels you assign to test scenarios that allow you to manage, identify, organize, search for, and 33 | filter Distributed Load Testing scenarios. 34 | 35 | 36 | removeTag(detail.itemIndex)} /> 37 | 38 | 39 | 40 | { 43 | if (detail.value.length <= 50) { 44 | setNewTag(detail.value); 45 | setTagError(""); 46 | } 47 | }} 48 | placeholder="Enter tag name" 49 | invalid={!!tagError} 50 | /> 51 | 54 | 55 | 56 | 57 | 58 | You can add {5 - formData.tags.length} more {5 - formData.tags.length === 1 ? "tag" : "tags"}. 59 | 60 | 61 | 62 | ); 63 | -------------------------------------------------------------------------------- /source/webui/src/utils/__tests__/errorUtils.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { extractErrorMessage } from '../errorUtils'; 5 | 6 | describe('extractErrorMessage', () => { 7 | it('should extract message from AWS Amplify error with JSON string body', () => { 8 | const error = { 9 | response: { 10 | body: '{"message": "INVALID_REQUEST_BODY: regionalTaskDetails.us-west-2.dltAvailableTasks: Number must be greater than 0"}' 11 | } 12 | }; 13 | expect(extractErrorMessage(error)).toBe('INVALID_REQUEST_BODY: regionalTaskDetails.us-west-2.dltAvailableTasks: Number must be greater than 0'); 14 | }); 15 | 16 | it('should extract message from AWS Amplify error with object body', () => { 17 | const error = { 18 | response: { 19 | body: { 20 | message: 'Resource not found' 21 | } 22 | } 23 | }; 24 | expect(extractErrorMessage(error)).toBe('Resource not found'); 25 | }); 26 | 27 | it('should return raw string body when JSON parsing fails', () => { 28 | const error = { 29 | response: { 30 | body: 'Plain error message' 31 | } 32 | }; 33 | expect(extractErrorMessage(error)).toBe('Plain error message'); 34 | }); 35 | 36 | it('should extract message from RTK Query error', () => { 37 | const error = { 38 | data: { 39 | message: 'Invalid request parameters' 40 | } 41 | }; 42 | expect(extractErrorMessage(error)).toBe('Invalid request parameters'); 43 | }); 44 | 45 | it('should extract direct message property', () => { 46 | const error = { 47 | message: 'Network error' 48 | }; 49 | expect(extractErrorMessage(error)).toBe('Network error'); 50 | }); 51 | 52 | it('should handle string errors', () => { 53 | const error = 'Something went wrong'; 54 | expect(extractErrorMessage(error)).toBe('Something went wrong'); 55 | }); 56 | 57 | it('should return default message for unknown error structure', () => { 58 | const error = { someProperty: 'value' }; 59 | expect(extractErrorMessage(error)).toBe('An unexpected error occurred. Please try again.'); 60 | }); 61 | }); -------------------------------------------------------------------------------- /source/webui/src/__tests__/test-data-random-utils.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /* 5 | * Collection of functions to generate domain-independent quasi random data. 6 | */ 7 | const alphabet = "abcdefghijklmnopqrstuvwxyz"; 8 | const getRandomLetter = () => alphabet[randomInteger(alphabet.length)]; 9 | 10 | /** 11 | * 12 | * @param targetWordCount 13 | */ 14 | export function blindText(targetWordCount: number): string { 15 | return Array.from({ length: targetWordCount }, randomWord).join(" "); 16 | } 17 | 18 | /** 19 | * 20 | * @param minLength 21 | * @param maxLength 22 | */ 23 | export function randomWord(minLength = 5, maxLength = 10): string { 24 | const difference = Math.abs(maxLength - minLength); 25 | const wordLength = randomInteger(difference) + minLength; 26 | 27 | const word = Array.from({ length: wordLength }, getRandomLetter).join(""); 28 | return word.charAt(0).toUpperCase() + word.slice(1); 29 | } 30 | 31 | /** 32 | * 33 | */ 34 | export function randomAlias() { 35 | return randomWord().toLowerCase() + "@"; 36 | } 37 | 38 | /** 39 | * 40 | * @param minLength 41 | * @param maxLength 42 | */ 43 | export function randomSentence(minLength = 1, maxLength = 5): string { 44 | const difference = Math.abs(maxLength - minLength); 45 | const numberOfWords = randomInteger(difference) + minLength; 46 | const words = Array.from({ length: numberOfWords }, randomWord); 47 | return words.join(" "); 48 | } 49 | 50 | /** 51 | * 52 | * @param max 53 | */ 54 | export function randomInteger(max: number) { 55 | return Math.floor(Math.random() * max); 56 | } 57 | 58 | /** 59 | * 60 | * @param max 61 | */ 62 | export function randomDigit(max = 10) { 63 | return randomInteger(max); 64 | } 65 | 66 | /** 67 | * 68 | */ 69 | export function randomAccountId() { 70 | const safeTestAccountIds = ["111111111111", "222222222222", "333333333333", "123456789012"]; 71 | return safeTestAccountIds[Math.floor(Math.random() * safeTestAccountIds.length)]; 72 | } 73 | 74 | /** 75 | * 76 | * @param array 77 | */ 78 | export function shuffle(array: T[]): T[] { 79 | return array.sort(() => 0.5 - Math.random()); 80 | } 81 | -------------------------------------------------------------------------------- /source/infrastructure/lib/mcp/gateway-target-construct.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { CredentialProviderType } from "@aws-sdk/client-bedrock-agentcore-control"; 5 | import { CfnGatewayTarget } from "aws-cdk-lib/aws-bedrockagentcore"; 6 | import { Construct } from "constructs"; 7 | 8 | export interface AgentCoreGatewayTargetProps { 9 | readonly gatewayId: string; 10 | readonly lambdaArn: string; 11 | readonly targetName: string; 12 | readonly targetDescription: string; 13 | readonly toolSchema: unknown; 14 | } 15 | 16 | /** 17 | * Helper function to convert raw JSON tool schema to CfnGatewayTarget.ToolDefinitionProperty[] 18 | * 19 | * @param {unknown} rawSchema - The raw JSON tool schema 20 | * @returns {CfnGatewayTarget.ToolDefinitionProperty[]} Converted tool schema as ToolDefinitionProperty array 21 | */ 22 | function convertToolSchemaToDefinitionProperties(rawSchema: unknown): CfnGatewayTarget.ToolDefinitionProperty[] { 23 | return rawSchema as CfnGatewayTarget.ToolDefinitionProperty[]; 24 | } 25 | 26 | export class AgentCoreGatewayTarget extends Construct { 27 | public readonly targetId: string; 28 | 29 | constructor(scope: Construct, id: string, props: AgentCoreGatewayTargetProps) { 30 | super(scope, id); 31 | 32 | // Convert raw JSON tool schema to typed definition properties 33 | const convertedToolSchema = convertToolSchemaToDefinitionProperties(props.toolSchema); 34 | 35 | const target = new CfnGatewayTarget(this, "DltAgentCoreGatewayTarget", { 36 | gatewayIdentifier: props.gatewayId, 37 | name: props.targetName, 38 | description: props.targetDescription, 39 | targetConfiguration: { 40 | mcp: { 41 | lambda: { 42 | lambdaArn: props.lambdaArn, 43 | toolSchema: { 44 | inlinePayload: convertedToolSchema, 45 | }, 46 | }, 47 | }, 48 | }, 49 | credentialProviderConfigurations: [ 50 | { 51 | credentialProviderType: CredentialProviderType.GATEWAY_IAM_ROLE, 52 | }, 53 | ], 54 | }); 55 | 56 | // Expose target ID 57 | this.targetId = target.attrTargetId; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /source/webui/src/pages/scenarios/hooks/useScenarioActions.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { useNavigate } from "react-router-dom"; 5 | import { get, del } from "aws-amplify/api"; 6 | import { ScenarioDefinition } from "../types"; 7 | import { useDeleteScenarioMutation } from "../../../store/scenariosApiSlice"; 8 | 9 | export const useScenarioActions = () => { 10 | const navigate = useNavigate(); 11 | const [deleteScenario] = useDeleteScenarioMutation(); 12 | 13 | const editScenario = async (testId: string) => { 14 | try { 15 | const response = await get({ apiName: "solution-api", path: `/scenarios/${testId}` }).response; 16 | const scenarioDetails = await response.body.json() as ScenarioDefinition; 17 | const encodedData = encodeURIComponent(JSON.stringify(scenarioDetails)); 18 | navigate(`/create-scenario?step=0&editData=${encodedData}`); 19 | } catch (error) { 20 | console.error('Failed to fetch scenario for editing:', error); 21 | throw error; 22 | } 23 | }; 24 | 25 | const copyScenario = async (testId: string) => { 26 | try { 27 | const response = await get({ apiName: "solution-api", path: `/scenarios/${testId}` }).response; 28 | const scenarioDetails = await response.body.json() as ScenarioDefinition; 29 | const { testId: _, ...scenarioWithoutId } = scenarioDetails; 30 | const encodedData = encodeURIComponent(JSON.stringify(scenarioWithoutId)); 31 | navigate(`/create-scenario?step=0©Data=${encodedData}`); 32 | } catch (error) { 33 | console.error('Failed to fetch scenario for copying:', error); 34 | throw error; 35 | } 36 | }; 37 | 38 | const cancelTestRun = async (testId: string) => { 39 | try { 40 | await del({ apiName: "solution-api", path: `/scenarios/${testId}` }).response; 41 | // Return success to allow parent component to handle refresh 42 | return { success: true }; 43 | } catch (error) { 44 | console.error('Failed to cancel test run:', error); 45 | throw error; 46 | } 47 | }; 48 | 49 | return { 50 | editScenario, 51 | copyScenario, 52 | cancelTestRun, 53 | deleteScenario 54 | }; 55 | }; -------------------------------------------------------------------------------- /source/webui/src/pages/scenarios/components/TestConfigurationSection.tsx: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Component for test scenario name and description configuration 5 | 6 | import { Container, Header, SpaceBetween, FormField, Input, Textarea } from "@cloudscape-design/components"; 7 | import { FormData } from "../types"; 8 | 9 | interface Props { 10 | formData: FormData; 11 | updateFormData: (updates: Partial) => void; 12 | showValidationErrors?: boolean; 13 | } 14 | 15 | export const TestConfigurationSection = ({ formData, updateFormData, showValidationErrors = false }: Props) => ( 16 | Test Configuration}> 17 | 18 | 24 | { 27 | if (detail.value.length <= 200) { 28 | updateFormData({ testName: detail.value }); 29 | } 30 | }} 31 | invalid={showValidationErrors && !formData.testName?.trim()} 32 | /> 33 | 34 | 35 | 41 |