From ff2d32008e96988d017d9a43f561a92ba9d5b4d0 Mon Sep 17 00:00:00 2001 From: kamyar ziabari Date: Wed, 6 Mar 2024 16:41:10 -0500 Subject: [PATCH] Update to version v3.2.6 --- CHANGELOG.md | 17 ++ NOTICE.txt => NOTICE | 10 +- VERSION.txt | 1 + deployment/build-s3-dist.sh | 18 +- .../Dockerfile | 32 ++- .../jar_updater.py | 4 +- .../load-test.sh | 19 +- deployment/run-unit-tests.sh | 7 +- source/api-services/index.js | 12 +- source/api-services/jest.config.js | 1 + source/api-services/lib/scenarios/index.js | 87 ++++--- .../api-services/lib/scenarios/index.spec.js | 17 +- source/api-services/package.json | 3 +- source/console/package.json | 32 ++- source/console/src/App.js | 50 ++-- .../console/src/Components/Create/Create.js | 221 +++++++++++------- .../src/Components/Create/Create.spec.js | 157 +++++++++++++ .../src/Components/Dashboard/Dashboard.js | 8 +- .../console/src/Components/Details/Details.js | 21 +- .../Components/RegionalModal/RegionalModal.js | 8 +- .../console/src/Components/Results/Results.js | 6 +- .../console/src/Components/Running/Running.js | 8 +- .../Shared/Buttons/CancelButtons.js | 4 +- .../Shared/Buttons/TestControlButtons.js | 7 +- source/console/src/index.js | 7 +- source/console/src/pubsub.js | 10 + source/custom-resource/lib/metrics/index.js | 4 +- .../custom-resource/lib/metrics/index.spec.js | 56 +++-- source/custom-resource/package.json | 3 +- .../lib/back-end/step-functions.ts | 4 +- .../lib/back-end/test-task-lambdas.ts | 4 +- .../lib/common-resources/common-resources.ts | 2 +- .../distributed-load-testing-on-aws-stack.ts | 1 + source/infrastructure/lib/front-end/api.ts | 2 +- .../testing-resources/regional-permissions.ts | 2 +- source/infrastructure/package.json | 2 +- .../test/__snapshots__/api.test.ts.snap | 1 + ...-load-testing-on-aws-regional.test.ts.snap | 35 +-- ...ted-load-testing-on-aws-stack.test.ts.snap | 71 +++--- .../regional-permissions.test.ts.snap | 1 + .../__snapshots__/step-functions.test.ts.snap | 32 +-- .../test-task-lambdas.test.ts.snap | 2 + .../test/regional-permissions.test.ts | 2 +- .../test/test-task-lambdas.test.ts | 1 + source/package.json | 28 +-- source/real-time-data-publisher/index.js | 8 +- source/real-time-data-publisher/package.json | 6 +- source/results-parser/index.js | 22 +- source/results-parser/lib/index.spec.js | 107 +++++++++ source/results-parser/lib/mock.js | 83 +++++++ source/results-parser/package.json | 6 +- source/solution-utils/package.json | 8 +- source/solution-utils/test/index.spec.js | 75 ++++++ source/solution-utils/utils.js | 34 +++ source/task-canceler/package.json | 2 +- source/task-runner/index.js | 1 + source/task-runner/lib/index.spec.js | 4 +- source/task-runner/package.json | 2 +- source/task-status-checker/package.json | 2 +- 59 files changed, 1023 insertions(+), 357 deletions(-) rename NOTICE.txt => NOTICE (87%) create mode 100644 VERSION.txt create mode 100644 source/console/src/Components/Create/Create.spec.js create mode 100644 source/console/src/pubsub.js create mode 100644 source/results-parser/lib/index.spec.js create mode 100644 source/results-parser/lib/mock.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 48b49b4..71aa054 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [3.2.6] - 2024-03-06 + +### Changed + +- Updated version of chartjs fromv v3.0.0 to v4 +- Updated version of react from v17 to v18 +- Updated version of react-dom from v17 to v18 +- Removed moment.js as the library was in maintenance mode. Replaced with built-in javascript date and time +- Updated Jmeter dependencies and taurus dependencis within the docker image to enhance the security of the docker image +- Updated taurus version from v1.16.27 to v1.16.29 + +### Fixed + +- Bug fix to resolve issue with graph not showing on scheduled tests [#158](https://github.com/aws-solutions/distributed-load-testing-on-aws/issues/158) +- Bug fix created by changes of the ECS account setting and enabling Tag Resource Authorization as default settings [#162](https://github.com/aws-solutions/distributed-load-testing-on-aws/issues/162) +- Bug fix to resolve issue with running the tests on OPT-IN regions [#163](https://github.com/aws-solutions/distributed-load-testing-on-aws/issues/163) + ## [3.2.5] - 2024-01-11 ### Changed diff --git a/NOTICE.txt b/NOTICE similarity index 87% rename from NOTICE.txt rename to NOTICE index 9e46c96..8e2bb8a 100644 --- a/NOTICE.txt +++ b/NOTICE @@ -1,5 +1,11 @@ -Distributed Load Testing Reference Architecture +Distributed Load Testing + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +Licensed under the Apache License Version 2.0 (the "License"). You may not use this file except +in compliance with the License. A copy of the License is located at http://www.apache.org/licenses/ +or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express or implied. See the License for the +specific language governing permissions and limitations under the License. ********************** THIRD PARTY COMPONENTS @@ -25,7 +31,7 @@ bootstrap under the Massachusetts Institute of Technology (MIT) license bootstrap-icons under the Massachusetts Institute of Technology (MIT) license brace under the Massachusetts Institute of Technology (MIT) license chart.js under the Massachusetts Institute of Technology (MIT) license -chartjs-adapter-moment under the Massachusetts Institute of Technology (MIT) license +chartjs-adapter-date-fns under the Massachusetts Institute of Technology (MIT) license constructs under the Apache License 2.0 eslint under the Massachusetts Institute of Technology (MIT) license eslint-config-prettier under the Massachusetts Institute of Technology (MIT) license diff --git a/VERSION.txt b/VERSION.txt new file mode 100644 index 0000000..c4a602d --- /dev/null +++ b/VERSION.txt @@ -0,0 +1 @@ +3.2.6 \ No newline at end of file diff --git a/deployment/build-s3-dist.sh b/deployment/build-s3-dist.sh index 067afab..d39326c 100755 --- a/deployment/build-s3-dist.sh +++ b/deployment/build-s3-dist.sh @@ -58,8 +58,7 @@ declare -A templates=( ) cd ${source_dir}/infrastructure -npm run clean -npm install +npm ci for template in "${!templates[@]}"; do node_modules/aws-cdk/bin/cdk synth --asset-metadata false --path-metadata false -a "npx ts-node --prefer-ts-exts bin/${template}.ts" > ${templates[$template]}/${template}.template @@ -76,9 +75,7 @@ done # Setup solution utils package cd ${source_dir}/solution-utils -rm -rf node_modules -npm install --production -rm -rf package-lock.json +npm ci --production # Creating custom resource resources for both stacks main_stack_custom_resource_files="index.js node_modules lib/*" @@ -90,9 +87,7 @@ declare -a stacks=( ) cd ${source_dir}/custom-resource -rm -rf node_modules/ -npm install --production -rm package-lock.json +npm ci --production for stack in "${stacks[@]}"; do cp ${stack}-index.js index.js files_to_zip=${stack}_stack_custom_resource_files @@ -124,9 +119,7 @@ for package in "${packages[@]}"; do echo "Creating $package deployment package" echo "------------------------------------------------------------------------------" cd ${source_dir}/${package} - rm -rf node_modules/ - npm install --production - rm package-lock.json + npm ci --production zip -q -r9 ${build_dist_dir}/${package}.zip * if [ $? -eq 0 ] then @@ -156,8 +149,7 @@ echo "Building console" echo "------------------------------------------------------------------------------" cd ${source_dir}/console [ -e build ] && rm -r build -[ -e node_modules ] && rm -rf node_modules -npm install +npm ci npm run build mkdir ${build_dist_dir}/console cp -r ./build/* ${build_dist_dir}/console/ diff --git a/deployment/ecr/distributed-load-testing-on-aws-load-tester/Dockerfile b/deployment/ecr/distributed-load-testing-on-aws-load-tester/Dockerfile index 2680a5c..7cee5a1 100644 --- a/deployment/ecr/distributed-load-testing-on-aws-load-tester/Dockerfile +++ b/deployment/ecr/distributed-load-testing-on-aws-load-tester/Dockerfile @@ -1,8 +1,8 @@ -FROM blazemeter/taurus:1.16.27 +FROM blazemeter/taurus:1.16.29 # taurus includes python and pip RUN /usr/bin/python3 -m pip install --upgrade pip RUN pip install --no-cache-dir awscli -RUN apt-get -y install xmlstarlet bc procps +RUN apt-get -y install --no-install-recommends xmlstarlet bc procps # Removing selenium and gatling from our image as they are not supported in DLT RUN rm -rf /root/.bzt/selenium-taurus @@ -25,16 +25,34 @@ WORKDIR /usr/local/rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/rbs-2.8.2/steep RUN sed -i 's/7.0.4/7.0.7.1/g' Gemfile.lock RUN gem install activesupport -v 7.0.7.1 -# Replacing urllib3, Werkzeug and cryptography with more stable Versions to fix vulnerabilities +# Fixing CVE-2023-36617 +WORKDIR /usr/local/rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/specifications/default +RUN sed -i 's/0.12.1/0.12.2.0/g' uri-0.12.1.gemspec +RUN mv uri-0.12.1.gemspec uri-0.12.2.gemspec +RUN gem install --default uri -v 0.12.2.0 + +# Replacing urllib3 with more stable Versions to resolve vulnerabilities RUN pip install urllib3==2.0.7 -RUN pip install Werkzeug==3.0.1 -RUN pip install cryptography==41.0.6 RUN rm -rf /root/.bzt/python-packages/3.10.12/urllib3* -RUN rm -rf /root/.bzt/python-packages/3.10.12/werkzeug* -RUN rm -rf /root/.bzt/python-packages/3.10.12/cryptography* RUN cp -r /usr/local/lib/python3.10/dist-packages/urllib3* /root/.bzt/python-packages/3.10.12/ + +# Replacing Werkzeug with more stable version to resolve vulnerabilities +RUN pip install Werkzeug==3.0.1 +RUN rm -rf /root/.bzt/python-packages/3.10.12/werkzeug* RUN cp -r /usr/local/lib/python3.10/dist-packages/werkzeug* /root/.bzt/python-packages/3.10.12/ + +# Replacing cryptography with more stable version to resolve vulnerabilities +RUN pip install cryptography==42.0.5 +RUN rm -rf /root/.bzt/python-packages/3.10.12/cryptography* RUN cp -r /usr/local/lib/python3.10/dist-packages/cryptography* /root/.bzt/python-packages/3.10.12/ +# Replacing Pillow with more stable version resolve CVE-2023-50447 +RUN rm -rf /root/.local/lib/python3.10/site-packages/Pillow* +RUN pip install --upgrade pillow --target /root/.local/lib/python3.10/site-packages/ + +# Replaing aiohttp with more stable version to resolve CVE-2024-23334 +RUN rm -rf /usr/local/lib/python3.10/dist-packages/aiohttp* +RUN pip install --upgrade aiohttp + WORKDIR /bzt-configs/ ENTRYPOINT ["./load-test.sh"] diff --git a/deployment/ecr/distributed-load-testing-on-aws-load-tester/jar_updater.py b/deployment/ecr/distributed-load-testing-on-aws-load-tester/jar_updater.py index 944eb5c..6c34f61 100644 --- a/deployment/ecr/distributed-load-testing-on-aws-load-tester/jar_updater.py +++ b/deployment/ecr/distributed-load-testing-on-aws-load-tester/jar_updater.py @@ -16,6 +16,7 @@ * batik-bridge v1.14 will be replaced with v1.17 * batik-transcoder v1.14 will be replaced with v1.17 * lets-plot-batik v2.2.1 will be replaced with 4.2.0 + * commons-net v3.8.0 will be replaced with v3.9.0 Also jmeter plugins manager will be updated to v1.10 to address CVEs and cmdrunner will be updated to v2.3 to accomodate with plugins manager. """ @@ -29,7 +30,8 @@ "batik-script": "org/apache/xmlgraphics/batik-script/1.17/batik-script-1.17.jar", "batik-bridge": "org/apache/xmlgraphics/batik-bridge/1.17/batik-bridge-1.17.jar", "batik-transcoder": "org/apache/xmlgraphics/batik-transcoder/1.17/batik-transcoder-1.17.jar", - "lets-plot-batik": "org/jetbrains/lets-plot/lets-plot-batik/4.2.0/lets-plot-batik-4.2.0.jar" + "lets-plot-batik": "org/jetbrains/lets-plot/lets-plot-batik/4.2.0/lets-plot-batik-4.2.0.jar", + "commons-net": "commons-net/commons-net/3.9.0/commons-net-3.9.0.jar" } JMETER_VERSION = "5.5" JMETER_PLUGINS_MANAGER_VERSION = "1.10" diff --git a/deployment/ecr/distributed-load-testing-on-aws-load-tester/load-test.sh b/deployment/ecr/distributed-load-testing-on-aws-load-tester/load-test.sh index f04c173..6695101 100644 --- a/deployment/ecr/distributed-load-testing-on-aws-load-tester/load-test.sh +++ b/deployment/ecr/distributed-load-testing-on-aws-load-tester/load-test.sh @@ -10,6 +10,7 @@ echo "FILE_TYPE:: ${FILE_TYPE}" echo "PREFIX:: ${PREFIX}" echo "UUID:: ${UUID}" echo "LIVE_DATA_ENABLED:: ${LIVE_DATA_ENABLED}" +echo "MAIN_STACK_REGION:: ${MAIN_STACK_REGION}" sigterm_handler() { if [ $pypid -ne 0 ]; then @@ -22,7 +23,7 @@ sigterm_handler() { trap 'sigterm_handler' SIGTERM echo "Download test scenario" -aws s3 cp s3://$S3_BUCKET/test-scenarios/$TEST_ID-$AWS_REGION.json test.json +aws s3 cp s3://$S3_BUCKET/test-scenarios/$TEST_ID-$AWS_REGION.json test.json --region $MAIN_STACK_REGION # download JMeter jmx file if [ "$TEST_TYPE" != "simple" ]; then @@ -32,9 +33,9 @@ if [ "$TEST_TYPE" != "simple" ]; then cp $PWD/*.jar $JMETER_LIB_PATH if [ "$FILE_TYPE" != "zip" ]; then - aws s3 cp s3://$S3_BUCKET/public/test-scenarios/$TEST_TYPE/$TEST_ID.jmx ./ + aws s3 cp s3://$S3_BUCKET/public/test-scenarios/$TEST_TYPE/$TEST_ID.jmx ./ --region $MAIN_STACK_REGION else - aws s3 cp s3://$S3_BUCKET/public/test-scenarios/$TEST_TYPE/$TEST_ID.zip ./ + aws s3 cp s3://$S3_BUCKET/public/test-scenarios/$TEST_TYPE/$TEST_ID.zip ./ --region $MAIN_STACK_REGION unzip $TEST_ID.zip # only looks for the first jmx file. JMETER_SCRIPT=`find . -name "*.jmx" | head -n 1` @@ -100,7 +101,7 @@ if [ "$TEST_TYPE" != "simple" ]; then fi echo "Uploading $p" - aws s3 cp $f $p + aws s3 cp $f $p --region $MAIN_STACK_REGION done fi @@ -114,11 +115,11 @@ if [ -f /tmp/artifacts/results.xml ]; then fi echo "Uploading results, bzt log, and JMeter log, out, and err files" - aws s3 cp /tmp/artifacts/results.xml s3://$S3_BUCKET/results/${TEST_ID}/${PREFIX}-${UUID}-${AWS_REGION}.xml - aws s3 cp /tmp/artifacts/bzt.log s3://$S3_BUCKET/results/${TEST_ID}/bzt-${PREFIX}-${UUID}-${AWS_REGION}.log - aws s3 cp /tmp/artifacts/jmeter.log s3://$S3_BUCKET/results/${TEST_ID}/jmeter-${PREFIX}-${UUID}-${AWS_REGION}.log - aws s3 cp /tmp/artifacts/jmeter.out s3://$S3_BUCKET/results/${TEST_ID}/jmeter-${PREFIX}-${UUID}-${AWS_REGION}.out - aws s3 cp /tmp/artifacts/jmeter.err s3://$S3_BUCKET/results/${TEST_ID}/jmeter-${PREFIX}-${UUID}-${AWS_REGION}.err + aws s3 cp /tmp/artifacts/results.xml s3://$S3_BUCKET/results/${TEST_ID}/${PREFIX}-${UUID}-${AWS_REGION}.xml --region $MAIN_STACK_REGION + aws s3 cp /tmp/artifacts/bzt.log s3://$S3_BUCKET/results/${TEST_ID}/bzt-${PREFIX}-${UUID}-${AWS_REGION}.log --region $MAIN_STACK_REGION + aws s3 cp /tmp/artifacts/jmeter.log s3://$S3_BUCKET/results/${TEST_ID}/jmeter-${PREFIX}-${UUID}-${AWS_REGION}.log --region $MAIN_STACK_REGION + aws s3 cp /tmp/artifacts/jmeter.out s3://$S3_BUCKET/results/${TEST_ID}/jmeter-${PREFIX}-${UUID}-${AWS_REGION}.out --region $MAIN_STACK_REGION + aws s3 cp /tmp/artifacts/jmeter.err s3://$S3_BUCKET/results/${TEST_ID}/jmeter-${PREFIX}-${UUID}-${AWS_REGION}.err --region $MAIN_STACK_REGION else echo "An error occurred while the test was running." fi \ No newline at end of file diff --git a/deployment/run-unit-tests.sh b/deployment/run-unit-tests.sh index 1813d54..64b7639 100755 --- a/deployment/run-unit-tests.sh +++ b/deployment/run-unit-tests.sh @@ -45,9 +45,9 @@ run_tests() { if [ $component_name = "solution-utils" ] then - rm -rf coverage package-lock.json + rm -rf coverage else - rm -rf coverage node_modules package-lock.json + rm -rf coverage node_modules fi } @@ -57,7 +57,6 @@ coverage_reports_top_path=$source_dir/test/coverage-reports #install dependencies cd $source_dir -npm run clean:all npm run install:all #run prettier @@ -85,7 +84,6 @@ else echo "******************************************************************************" exit 1 fi -npm run clean # Run unit tests echo "Running unit tests" @@ -101,6 +99,7 @@ declare -a packages=( "task-canceler" "task-runner" "task-status-checker" + "console" ) for package in "${packages[@]}"; do diff --git a/source/api-services/index.js b/source/api-services/index.js index 4a1deeb..44aa2c1 100644 --- a/source/api-services/index.js +++ b/source/api-services/index.js @@ -2,8 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 const scenarios = require("./lib/scenarios/"); -const metrics = require("./lib/metrics/"); - +const utils = require("solution-utils"); /** * API Manager Class that gets API path and their method * and calls the appropriate handler function to handle the request @@ -51,10 +50,11 @@ class APIHandler { // Handle creating test else data = await scenarios.createTest(config); if (process.env.SEND_METRIC === "Yes") { - await metrics.send({ - testTaskConfigs: config.testTaskConfigs, - testType: config.testType, - fileType: config.fileType, + await utils.sendMetric({ + Type: "TaskCreate", + TestType: config.testType, + FileType: config.fileType || (config.testType === "simple" ? "none" : "script"), + TaskCount: config.taskCount, }); } return data; diff --git a/source/api-services/jest.config.js b/source/api-services/jest.config.js index f6940dc..1aedbde 100644 --- a/source/api-services/jest.config.js +++ b/source/api-services/jest.config.js @@ -1,6 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +process.env.TZ = "UTC"; module.exports = { roots: ["/lib"], testMatch: ["**/*.spec.js"], diff --git a/source/api-services/lib/scenarios/index.js b/source/api-services/lib/scenarios/index.js index a5fd21d..a4e4a84 100644 --- a/source/api-services/lib/scenarios/index.js +++ b/source/api-services/lib/scenarios/index.js @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 const AWS = require("aws-sdk"); -const moment = require("moment"); const utils = require("solution-utils"); const { HISTORY_TABLE, SCENARIOS_TABLE, SCENARIOS_BUCKET, STATE_MACHINE_ARN, TASK_CANCELER_ARN, STACK_ID } = process.env; @@ -252,14 +251,14 @@ const scheduleTest = async (event, context) => { } if (config.scheduleStep === "create") { - //Schedule for 1 min prior to account for time it takes to create rule - const createRun = moment([year, parseInt(month, 10) - 1, day, hour, minute]) - .subtract(1, "minute") - .format("YYYY-MM-DD HH:mm:ss"); - let [createDate, createTime] = createRun.split(" "); - const [createHour, createMin] = createTime.split(":"); - const [createYear, createMonth, createDay] = createDate.split("-"); - const cronStart = `cron(${createMin} ${createHour} ${createDay} ${createMonth} ? ${createYear})`; + const createRun = new Date(year, parseInt(month, 10) - 1, day, hour, minute); + + // Schedule for 1 min prior to account for time it takes to create rule + // getMonth() returns Jan with index Zero that is why months need a +1 + // refrence https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getMonth + const cronStart = `cron(${createRun.getMinutes() - 1} ${createRun.getHours()} ${createRun.getDate()} ${ + createRun.getMonth() + 1 + } ? ${createRun.getFullYear()})`; scheduleRecurrence = config.recurrence; //Create rule to create schedule @@ -447,33 +446,35 @@ const setFileType = (testType, fileType) => { const setTestId = (testId) => // When accessing API directly and no testId testId || utils.generateUniqueId(10); + /** * Sets the next schedule test run - * @param {string} scheduledTime + * @param {Date} scheduledTime * @param {string} scheduleRecurrence - * @returns next run, scheduleRecurrence + * @returns nextRun */ const setNextRun = (scheduledTime, scheduleRecurrence = "") => { - let nextRun = ""; - if (scheduleRecurrence) { - switch (scheduleRecurrence) { - case "daily": - nextRun = scheduledTime.add(1, "d").format("YYYY-MM-DD HH:mm:ss"); - break; - case "weekly": - nextRun = scheduledTime.add(7, "d").format("YYYY-MM-DD HH:mm:ss"); - break; - case "biweekly": - nextRun = scheduledTime.add(14, "d").format("YYYY-MM-DD HH:mm:ss"); - break; - case "monthly": - nextRun = scheduledTime.add(1, "M").format("YYYY-MM-DD HH:mm:ss"); - break; - default: - throw new ErrorException("InvalidParameter", "Invalid recurrence value."); - } + let newDate = new Date(scheduledTime.getTime()); + if (!scheduleRecurrence) { + return ""; } - return nextRun; + switch (scheduleRecurrence) { + case "daily": + newDate.setDate(newDate.getDate() + 1); + break; + case "weekly": + newDate.setDate(newDate.getDate() + 7); + break; + case "biweekly": + newDate.setDate(newDate.getDate() + 14); + break; + case "monthly": + newDate.setMonth(newDate.getMonth() + 1); + break; + default: + throw new ErrorException("InvalidParameter", "Invalid recurrence value."); + } + return convertDateToString(newDate); }; /** * Validates the setting for task count and task concurrency @@ -751,6 +752,18 @@ const updateTestDBEntry = async (updateTestConfigs) => { } }; +/** + * @function convertDateToString + * Description: Formats the date to a YYYY-MM-DD HH:MM:SS format + * @config {string} a formatted string date + * */ +const convertDateToString = (date) => { + return date + .toISOString() + .replace("T", " ") + .replace(/\.\d+Z$/, ""); +}; + /** * @function createTest * Description: returns a consolidated list of test scenarios @@ -766,20 +779,21 @@ const createTest = async (config) => { testId = setTestId(testId); const testEntry = await getTestEntry(testId); - if (testEntry && testEntry.nextRun) nextRun = moment.utc(testEntry.nextRun, "YYYY-MM-DD HH:mm:ss"); + if (testEntry && testEntry.nextRun) nextRun = new Date(testEntry.nextRun); + + let startTime = new Date(); - let startTime = moment().utc(); if (eventBridge) { - const startDate = moment().format("YYYY-MM-DD"); + const startDate = new Date().toISOString().slice(0, 10); // If it is eventBridge triggered definitely has scheduleTime - startTime = moment.utc(`${startDate} ${scheduleTime}:00`, "YYYY-MM-DD HH:mm:ss"); + startTime = new Date(`${startDate} ${scheduleTime}:00`); } - if (nextRun && startTime.isBefore(nextRun)) nextRun = nextRun.format("YYYY-MM-DD HH:mm:ss"); + if (nextRun && startTime < nextRun) nextRun = convertDateToString(nextRun); else nextRun = setNextRun(startTime, recurrence); const scheduleRecurrence = recurrence ? recurrence : ""; - startTime = startTime.format("YYYY-MM-DD HH:mm:ss"); + startTime = convertDateToString(startTime); testTaskConfigs = validateTaskCountConcurrency(testTaskConfigs, regionalTaskDetails); @@ -841,6 +855,7 @@ const createTest = async (config) => { testType, fileType, }; + const data = await updateTestDBEntry(updateDBData); console.log(`Create test complete: ${JSON.stringify(data)}`); diff --git a/source/api-services/lib/scenarios/index.spec.js b/source/api-services/lib/scenarios/index.spec.js index 00ad2ca..18b1846 100644 --- a/source/api-services/lib/scenarios/index.spec.js +++ b/source/api-services/lib/scenarios/index.spec.js @@ -13,7 +13,6 @@ const mockLambda = jest.fn(); const mockCloudFormation = jest.fn(); const mockServiceQuotas = jest.fn(); const mockAWS = require("aws-sdk"); -const moment = require("moment"); mockAWS.S3 = jest.fn(() => ({ putObject: mockS3, })); @@ -65,8 +64,6 @@ mockAWS.ServiceQuotas = jest.fn(() => ({ getServiceQuota: mockServiceQuotas, })); -Date.now = jest.fn(() => new Date("2017-04-22T02:28:37.000Z")); - const testId = "1234"; const listData = { Items: [{ testId: "1234" }, { testId: "5678" }], @@ -729,8 +726,13 @@ describe("#SCENARIOS API:: ", () => { mockCloudFormation.mockReset(); mockServiceQuotas.mockReset(); getData = { ...origData }; + jest.useFakeTimers("modern"); + jest.setSystemTime(new Date(Date.UTC(2017, 3, 22, 2, 28, 37))); // Note: Month is 0-indexed }); + beforeAll(() => { + process.env.TZ = "UTC"; + }); //Positive tests it('should return "SUCCESS" when "LISTTESTS" returns success', async () => { mockDynamoDB.mockImplementationOnce(() => ({ @@ -1822,9 +1824,8 @@ describe("#SCENARIOS API:: ", () => { }); it("should use the right nextRun value for manually triggered recurring tests", async () => { - const nextRun = moment().utc().add(1, "days").format("YYYY-MM-DD HH:mm:ss"); config.recurrence = "daily"; - getData.Item.nextRun = nextRun; + getData.Item.nextRun = "2017-04-23 02:28:37"; mockS3.mockImplementation(() => ({ promise() { // putObject @@ -1865,11 +1866,11 @@ describe("#SCENARIOS API:: ", () => { expect(mockDynamoDB).toHaveBeenCalledWith( expect.objectContaining({ ExpressionAttributeValues: expect.objectContaining({ - ":nr": nextRun, + ":nr": "2017-04-23 02:28:37", }), }) ); - //reset config + // reset config delete config.recurrence; delete getData.Item.nextRun; }); @@ -2074,7 +2075,7 @@ describe("#SCENARIOS API:: ", () => { delete config.recurrence; }); - it('should record proper date when "CREATETEST" with daily recurrence', async () => { + it('should record proper date when "CREATETEST" with monthly recurrence', async () => { config.recurrence = "monthly"; mockDynamoDB.mockImplementationOnce(() => ({ promise() { diff --git a/source/api-services/package.json b/source/api-services/package.json index 8bed7ed..3ca2529 100644 --- a/source/api-services/package.json +++ b/source/api-services/package.json @@ -1,6 +1,6 @@ { "name": "api-services", - "version": "3.2.5", + "version": "3.2.6", "description": "REST API micro services", "repository": { "type": "git", @@ -19,7 +19,6 @@ "dependencies": { "aws-sdk": "^2.1001.0", "axios": "^1.6.0", - "moment": "^2.27.0", "solution-utils": "file:../solution-utils" }, "devDependencies": { diff --git a/source/console/package.json b/source/console/package.json index 678f4cb..b24feb1 100644 --- a/source/console/package.json +++ b/source/console/package.json @@ -1,6 +1,6 @@ { "name": "distributed-load-testing-on-aws-ui", - "version": "3.2.5", + "version": "3.2.6", "private": true, "license": "Apache-2.0", "author": { @@ -13,7 +13,7 @@ "clean": "rm -rf node_modules package-lock.json", "eject": "react-scripts eject", "start": "react-scripts start", - "test": "react-scripts test" + "test": "react-scripts test --watchAll=false --coverage --silent" }, "browserslist": { "production": [ @@ -31,19 +31,19 @@ "extends": "react-app" }, "dependencies": { - "@aws-amplify/pubsub": "4.4.10", - "@aws-amplify/ui-react": "^1.0.2", - "aws-amplify": "4.3.31", + "@aws-amplify/pubsub": "^6.0.16", + "@aws-amplify/ui-react": "^6.1.3", + "aws-amplify": "^6.0.16", "aws-sdk": "^2.1457.0", "bootstrap": "^5.1.0", "bootstrap-icons": "^1.8.1", "brace": "^0.11.1", - "chart.js": "^3.7.1", - "chartjs-adapter-moment": "^1.0.0", - "moment": "^2.29.1", - "react": "^17.0.1", + "chart.js": "^4.0.0", + "chartjs-adapter-date-fns": "^3.0.0", + "jest": "^29.7.0", + "react": "^18.2.0", "react-ace": "^10.1.0", - "react-dom": "^17.0.1", + "react-dom": "^18.2.0", "react-router-dom": "^5.2.0", "react-scripts": "^5.0.1", "reactstrap": "^9.0.0", @@ -54,10 +54,18 @@ "@aws-sdk/client-s3": "3.414.0", "@aws-sdk/client-sts": "3.414.0", "resolve-url-loader": "5.0.0", - "axios": "^1.6.0" + "axios": "^1.6.0", + "react-refresh": "0.14.0" }, "readme": "./README.md", "devDependencies": { - "@babel/plugin-proposal-private-property-in-object": "^7.21.11" + "@babel/plugin-proposal-private-property-in-object": "^7.21.11", + "@testing-library/jest-dom": "^6.4.2", + "@testing-library/react": "^14.2.1" + }, + "jest": { + "transformIgnorePatterns": [ + "/node_modules/(?!(axios)/)" + ] } } diff --git a/source/console/src/App.js b/source/console/src/App.js index 9790f87..75409b3 100644 --- a/source/console/src/App.js +++ b/source/console/src/App.js @@ -6,9 +6,10 @@ import { BrowserRouter as Router, Route, Switch, Link } from "react-router-dom"; import { Collapse, Navbar, NavbarToggler, NavbarBrand, Nav, NavItem } from "reactstrap"; //Amplify -import { Amplify, Auth } from "aws-amplify"; +import { Amplify } from "aws-amplify"; +import { getCurrentUser, signOut, fetchAuthSession } from "aws-amplify/auth"; import { withAuthenticator } from "@aws-amplify/ui-react"; -import { PubSub, AWSIoTProvider } from "@aws-amplify/pubsub"; +import "@aws-amplify/ui-react/styles.css"; import AWS from "aws-sdk"; //Components @@ -16,16 +17,33 @@ import Dashboard from "./Components/Dashboard/Dashboard.js"; import Create from "./Components/Create/Create.js"; import Details from "./Components/Details/Details.js"; import RegionalModal from "./Components/RegionalModal/RegionalModal.js"; - declare var awsConfig; -Amplify.addPluggable( - new AWSIoTProvider({ - aws_pubsub_region: awsConfig.aws_project_region, - aws_pubsub_endpoint: "wss://" + awsConfig.aws_iot_endpoint + "/mqtt", - }) -); -PubSub.configure(awsConfig); -Amplify.configure(awsConfig); + +const ResourcesConfig = { + Auth: { + Cognito: { + userPoolId: awsConfig.aws_user_pools_id, + userPoolClientId: awsConfig.aws_user_pools_web_client_id, + identityPoolId: awsConfig.aws_cognito_identity_pool_id, + }, + }, + API: { + REST: { + dlts: { + endpoint: awsConfig.aws_cloud_logic_custom[0].endpoint, + region: awsConfig.aws_cloud_logic_custom[0].region, + }, + }, + }, + Storage: { + S3: { + bucket: awsConfig.aws_user_files_s3_bucket, // Optional + region: awsConfig.aws_user_files_s3_bucket_region, // Optional + }, + }, +}; + +Amplify.configure({ ...ResourcesConfig }); class App extends React.Component { constructor(props) { @@ -45,15 +63,15 @@ class App extends React.Component { */ async componentDidMount() { try { - await Auth.currentAuthenticatedUser(); + await getCurrentUser(); } catch (error) { console.log("User is not signed in"); } - const credentials = await Auth.currentCredentials(); + const credentials = await fetchAuthSession(); const identityId = credentials.identityId; AWS.config.update({ region: awsConfig.aws_project_region, - credentials: Auth.essentialCredentials(credentials), + credentials: credentials.credentials, }); const params = { policyName: awsConfig.aws_iot_policy_name, @@ -89,7 +107,7 @@ class App extends React.Component { } async signOut() { - await Auth.signOut(); + await signOut(); window.location.reload(); } @@ -168,4 +186,4 @@ class App extends React.Component { } } -export default withAuthenticator(App, false); +export default withAuthenticator(App); diff --git a/source/console/src/Components/Create/Create.js b/source/console/src/Components/Create/Create.js index 665e0e6..8419bf6 100644 --- a/source/console/src/Components/Create/Create.js +++ b/source/console/src/Components/Create/Create.js @@ -2,8 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 import React from "react"; -import { API, Storage } from "aws-amplify"; +import { uploadData } from "aws-amplify/storage"; +import { get, post } from "aws-amplify/api"; import "brace"; +import { v4 as uuidv4 } from "uuid"; + import AceEditor from "react-ace"; import { Card, @@ -40,14 +43,10 @@ class Create extends React.Component { constructor(props) { super(props); if (this.props.location.state && this.props.location.state.data.testId) { - let fileType = ""; - if (this.props.location.state.data.testType && this.props.location.state.data.testType !== "simple") { - if (this.props.location.state.data.fileType) { - fileType = this.props.location.state.data.fileType; - } else { - fileType = "script"; - } - } + let fileType = this.setInitialFileType(); + this.props.location.state.data.testTaskConfigs.forEach((config) => { + config.id = uuidv4(); + }); this.state = { isLoading: false, isUploading: false, @@ -105,7 +104,7 @@ class Create extends React.Component { formValues: { testName: "", testDescription: "", - testTaskConfigs: [{ concurrency: 0, taskCount: 0, region: "" }], + testTaskConfigs: [{ concurrency: 0, taskCount: 0, region: "", id: uuidv4() }], holdFor: 0, holdForUnits: "m", rampUp: 0, @@ -141,6 +140,15 @@ class Create extends React.Component { this.checkForTaskCountWarning = this.checkForTaskCountWarning.bind(this); } + setInitialFileType() { + if (this.props.location.state.data.testType && this.props.location.state.data.testType !== "simple") { + if (this.props.location.state.data.fileType) { + return this.props.location.state.data.fileType; + } + return "script"; + } + } + parseJson(str) { try { return JSON.parse(str); @@ -150,107 +158,140 @@ class Create extends React.Component { } handleSubmit = async () => { - const values = this.state.formValues; + const { + testName, + testDescription, + testTaskConfigs, + rampUp, + rampUpUnits, + holdFor, + holdForUnits, + onSchedule, + scheduleDate, + scheduleTime, + recurrence, + testType, + fileType, + showLive, + headers, + body, + endpoint, + method, + } = this.state.formValues; if (!this.form.current.reportValidity()) { this.setState({ isLoading: false }); return false; } - const testId = this.state.testId || generateUniqueId(10); let payload = { testId, - testName: values.testName, - testDescription: values.testDescription, - testTaskConfigs: values.testTaskConfigs, + testName: testName, + testDescription: testDescription, + testTaskConfigs: testTaskConfigs.map(({ id, ...rest }) => rest), testScenario: { execution: [ { - "ramp-up": String(parseInt(values.rampUp)).concat(values.rampUpUnits), - "hold-for": String(parseInt(values.holdFor)).concat(values.holdForUnits), - scenario: values.testName, + "ramp-up": String(parseInt(rampUp)).concat(rampUpUnits), + "hold-for": String(parseInt(holdFor)).concat(holdForUnits), + scenario: testName, }, ], scenarios: { - [values.testName]: {}, + [testName]: {}, }, }, - showLive: values.showLive, - testType: values.testType, - fileType: values.fileType, + showLive: showLive, + testType: testType, + fileType: fileType, regionalTaskDetails: this.state.regionalTaskDetails, }; - if (!!parseInt(values.onSchedule)) { - payload.scheduleDate = values.scheduleDate; - payload.scheduleTime = values.scheduleTime; + if (!!parseInt(onSchedule)) { + payload.scheduleDate = scheduleDate; + payload.scheduleTime = scheduleTime; payload.scheduleStep = "start"; if (this.state.activeTab === "2") { payload.scheduleStep = "create"; - payload.recurrence = values.recurrence; + payload.recurrence = recurrence; } } + await this.setPayloadTestScenario({ testType, testName, endpoint, method, headers, body, payload, testId }); - if (values.testType === "simple") { - if (!values.headers) { - values.headers = "{}"; - } - if (!values.body) { - values.body = "{}"; - } - if (!this.parseJson(values.headers.trim())) { + this.setState({ isLoading: true }); + this.setState({ isUploading: false }); + try { + const _response = await post({ + apiName: "dlts", + path: "/scenarios", + options: { + body: payload, + }, + }).response; + const response = await _response.body.json(); + console.log("Scenario created successfully", response.testId); + this.props.history.push({ pathname: `/details/${response.testId}`, state: { testId: response.testId } }); + } catch (err) { + console.error("Failed to create scenario", err); + this.setState({ isLoading: false }); + } + }; + + async setPayloadTestScenario(props) { + let { testType, testName, endpoint, method, headers, body, payload, testId } = props; + if (testType === "simple") { + headers = headers || "{}"; + body = body || "{}"; + + if (!this.parseJson(headers.trim())) { return alert("WARNING: headers text is not valid JSON"); } - if (!this.parseJson(values.body.trim())) { + if (!this.parseJson(body.trim())) { return alert("WARNING: body text is not valid JSON"); } - payload.testScenario.scenarios[values.testName] = { + payload.testScenario.scenarios[testName] = { requests: [ { - url: values.endpoint, - method: values.method, - body: this.parseJson(values.body.trim()), - headers: this.parseJson(values.headers.trim()), + url: endpoint, + method: method, + body: this.parseJson(body.trim()), + headers: this.parseJson(headers.trim()), }, ], }; + console.log(payload); } else { - payload.testScenario.scenarios[values.testName] = { + payload.testScenario.scenarios[testName] = { script: `${testId}.jmx`, }; if (this.state.file) { - try { - const file = this.state.file; - let filename = `${testId}.jmx`; - - if (file.type && file.type.includes("zip")) { - payload.fileType = "zip"; - filename = `${testId}.zip`; - } else { - payload.fileType = "script"; - } - this.setState({ isUploading: true }); - await Storage.put(`test-scenarios/jmeter/${filename}`, file); - console.log("Script uploaded successfully"); - } catch (error) { - console.error("Error", error); - } + payload.fileType = await this.uploadFileToScenarioBucket(testId); } } + } - this.setState({ isLoading: true }); - this.setState({ isUploading: false }); + async uploadFileToScenarioBucket(testId) { + const file = this.state.file; + let filename; + let fileType; + if (file.type && file.type.includes("zip")) { + fileType = "zip"; + filename = `${testId}.zip`; + } else { + fileType = "script"; + filename = `${testId}.jmx`; + } try { - const response = await API.post("dlts", "/scenarios", { body: payload }); - console.log("Scenario created successfully", response.testId); - this.props.history.push({ pathname: `/details/${response.testId}`, state: { testId: response.testId } }); - } catch (err) { - console.error("Failed to create scenario", err); - this.setState({ isLoading: false }); + this.setState({ isUploading: true }); + await uploadData({ key: `test-scenarios/jmeter/${filename}`, data: file }).result; + console.log("Script uploaded successfully"); + } catch (error) { + console.error("Error", error); } - }; + return fileType; + } setFormValue(key, value, id) { const formValues = this.state.formValues; @@ -363,7 +404,11 @@ class Create extends React.Component { listRegions = async () => { try { const regions = {}; - const data = await API.get("dlts", "/regions"); + const _data = await get({ + apiName: "dlts", + path: "/regions", + }).response; + const data = await _data.body.json(); for (const item of data.regions) { regions[item.region] = item.region; } @@ -377,8 +422,11 @@ class Create extends React.Component { try { this.setState({ vCPUDetailsLoading: true }); - const vCPUDetails = await API.get("dlts", "/vCPUDetails"); - + const _vCPUDetails = await get({ + apiName: "dlts", + path: "/vCPUDetails", + }).response; + const vCPUDetails = await _vCPUDetails.body.json(); const regions = {}; for (const region in vCPUDetails) { const regionDetails = vCPUDetails[region]; @@ -421,17 +469,17 @@ class Create extends React.Component { await this.getvCPUDetails(); } - render() { - const getTableIcon = () => { - if (this.state.vCPUDetailsLoading) { - return ; - } else if (this.state.showResourceTable) { - return ; - } else { - return ; - } - }; + getTableIcon = () => { + if (this.state.vCPUDetailsLoading) { + return ; + } else if (this.state.showResourceTable) { + return ; + } else { + return ; + } + }; + render() { const cancel = () => { return this.state.testId === null ? this.props.history.push("/") @@ -479,7 +527,9 @@ class Create extends React.Component { required onChange={this.handleInputChange} /> - Short description of the test scenario. + + Short description of the test scenario. + @@ -494,7 +544,7 @@ class Create extends React.Component { {this.state.formValues.testTaskConfigs.map((value, index) => ( - + { const formValues = this.state.formValues; index < maxRegions - 1 && - formValues.testTaskConfigs.push({ concurrency: 0, taskCount: 0, region: "" }); + formValues.testTaskConfigs.push({ + concurrency: 0, + taskCount: 0, + region: "", + id: uuidv4(), + }); this.setState({ formValues }); }} > @@ -619,7 +674,7 @@ class Create extends React.Component { }} >
- {getTableIcon()} + {this.getTableIcon()}
{"Currently Available Tasks"} diff --git a/source/console/src/Components/Create/Create.spec.js b/source/console/src/Components/Create/Create.spec.js new file mode 100644 index 0000000..61dc1a9 --- /dev/null +++ b/source/console/src/Components/Create/Create.spec.js @@ -0,0 +1,157 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import Create from "./Create"; +import { uploadData } from "aws-amplify/storage"; + +jest.mock("aws-amplify/api", () => ({ + get: jest.fn(), + post: jest.fn(), +})); +jest.mock("aws-amplify/storage", () => ({ + uploadData: jest.fn(), +})); + +const commonProps = { + // Example prop structure based on your component's needs + location: { + state: { + data: { + testId: "123", + testType: "jmeter", + fileType: "script", + holdFor: "11", + rampUp: "12", + testTaskConfigs: [ + { + region: "us-east-1", + concurrency: "5", + taskCount: "5", + }, + ], + }, + }, + }, +}; + +describe("Component Testing", () => { + test("setInitialFileType see if Run Now is not disabled", () => { + // Example of Componenet Testings + render(); + expect(screen.getByRole("button", { name: "Run Now" })).not.toBeDisabled(); + }); +}); + +describe("Functions Testing", () => { + test("setInitialFileType returns script type", () => { + const createInstance = new Create(commonProps); + const result = createInstance.setInitialFileType(/* arguments */); + expect(result).toBe("script"); + }); + test("setInitialFileType returns type", () => { + const props = { + // Example prop structure based on your component's needs + location: { + state: { + data: { + testId: "123", + testType: "jmeter", + fileType: "fileType", + holdFor: "11", + rampUp: "12", + testTaskConfigs: [ + { + region: "us-east-1", + concurrency: "5", + taskCount: "5", + }, + ], + }, + }, + }, + }; + const createInstance = new Create(props); + const result = createInstance.setInitialFileType(); + expect(result).toBe("fileType"); + }); + + test("setPayloadTestScenario for simple test type", async () => { + const createInstance = new Create(commonProps); + // Mock the internal method and state as needed + createInstance.state = { file: undefined }; + + const payload = { testScenario: { scenarios: {} } }; // Initial empty payload structure + const props = { + testType: "simple", + testName: "testName", + endpoint: "http://example.com", + method: "GET", + headers: "{}", + body: "{}", + payload, + testId: "123", + }; + + await createInstance.setPayloadTestScenario(props); + + expect(payload).toEqual({ + testScenario: { + scenarios: { + testName: { + requests: [ + { + url: "http://example.com", + method: "GET", + body: {}, + headers: {}, + }, + ], + }, + }, + }, + }); + }); + + test("setPayloadTestScenario for NOT simple test type", async () => { + const createInstance = new Create(commonProps); + // Mock the internal method and state as needed + createInstance.uploadFileToScenarioBucket = jest.fn(() => Promise.resolve()); + createInstance.state = { file: undefined }; + + const payload = { testScenario: { scenarios: {} } }; // Initial empty payload structure + const props = { + testType: "NOT SIMPLE", + testName: "testName", + endpoint: "http://example.com", + method: "GET", + headers: "{}", + body: "{}", + payload, + testId: "123", + }; + + await createInstance.setPayloadTestScenario(props); + + expect(payload).toEqual({ + testScenario: { + scenarios: { + testName: { + script: "123.jmx", + }, + }, + }, + }); + }); + + describe("uploadFileToScenarioBucket", () => { + test.each([ + ["zip", "test.zip"], + ["script", "test.jmx"], + ])("uploads file successfully for fileType %s", async (initialFileType) => { + let createInstance = new Create(commonProps); + createInstance.state = { file: { type: initialFileType === "zip" ? "application/zip" : "text/plain" } }; + await createInstance.uploadFileToScenarioBucket("test"); + expect(uploadData).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/source/console/src/Components/Dashboard/Dashboard.js b/source/console/src/Components/Dashboard/Dashboard.js index 49588d2..7009fdb 100644 --- a/source/console/src/Components/Dashboard/Dashboard.js +++ b/source/console/src/Components/Dashboard/Dashboard.js @@ -4,7 +4,7 @@ import React from "react"; import { Table, Spinner } from "reactstrap"; import { Link } from "react-router-dom"; -import { API } from "aws-amplify"; +import { get } from "aws-amplify/api"; import PageHeader from "../Shared/PageHeader/PageHeader"; import RefreshButtons from "../Shared/Buttons/RefreshButtons"; @@ -25,7 +25,11 @@ class Dashboard extends React.Component { }); try { - const data = await API.get("dlts", "/scenarios"); + const _data = await get({ + apiName: "dlts", + path: "/scenarios", + }).response; + const data = await _data.body.json(); data.Items.sort((a, b) => { if (!a.startTime) a.startTime = ""; if (!b.startTime) b.startTime = ""; diff --git a/source/console/src/Components/Details/Details.js b/source/console/src/Components/Details/Details.js index ab4f3eb..5694223 100644 --- a/source/console/src/Components/Details/Details.js +++ b/source/console/src/Components/Details/Details.js @@ -3,7 +3,9 @@ import React from "react"; import { Spinner } from "reactstrap"; -import { API, Storage } from "aws-amplify"; +import { get } from "aws-amplify/api"; +import { getUrl } from "aws-amplify/storage"; +import { Amplify } from "aws-amplify"; import PageHeader from "../Shared/PageHeader/PageHeader.js"; import RefreshButtons from "../Shared/Buttons/RefreshButtons.js"; @@ -96,7 +98,11 @@ class Details extends React.Component { getTest = async () => { const testId = this.state.testId; try { - const data = await API.get("dlts", `/scenarios/${testId}`); + const _data = await get({ + apiName: "dlts", + path: `/scenarios/${testId}`, + }).response; + const data = await _data.body.json(); if (data.nextRun) { const [scheduleDate, scheduleTime] = data.nextRun.split(" "); data.scheduleDate = scheduleDate; @@ -104,7 +110,8 @@ class Details extends React.Component { data.recurrence = data.scheduleRecurrence; delete data.nextRun; } - data.regionalTaskDetails = await API.get("dlts", "/vCPUDetails"); + const _vCPUDetails = await get({ apiName: "dlts", path: "/vCPUDetails" }).response; + data.regionalTaskDetails = await _vCPUDetails.body.json(); this.setTestData(data); } catch (err) { console.error(err); @@ -132,8 +139,8 @@ class Details extends React.Component { const testId = this.state.testId; const { testType } = this.state.data; let filename = this.state.data.fileType === "zip" ? `${testId}.zip` : `${testId}.jmx`; - const url = await Storage.get(`test-scenarios/${testType}/${filename}`, { expires: 10 }); - window.open(url, "_blank"); + const url = await getUrl({ key: `test-scenarios/${testType}/${filename}` }); + window.open(url.url, "_blank"); } catch (error) { console.error("Error", error); } @@ -142,7 +149,9 @@ class Details extends React.Component { async handleFullTestDataLocation() { try { const testId = this.state.testId; - const url = `https://console.aws.amazon.com/s3/buckets/${Storage._config.AWSS3.bucket}?prefix=results/${testId}/`; + const url = `https://console.aws.amazon.com/s3/buckets/${ + Amplify.getConfig().Storage.S3.bucket + }?prefix=results/${testId}/`; window.open(url, "_blank"); } catch (error) { console.error("Failed to open S3 location for test run results", error); diff --git a/source/console/src/Components/RegionalModal/RegionalModal.js b/source/console/src/Components/RegionalModal/RegionalModal.js index e4cc852..7c5878c 100644 --- a/source/console/src/Components/RegionalModal/RegionalModal.js +++ b/source/console/src/Components/RegionalModal/RegionalModal.js @@ -3,7 +3,7 @@ import React from "react"; import { Modal, ModalHeader, ModalBody, Row, Col, Button, ListGroup, ListGroupItem, Tooltip } from "reactstrap"; -import { API } from "aws-amplify"; +import { get } from "aws-amplify/api"; class RegionalModal extends React.Component { constructor(props) { @@ -37,7 +37,11 @@ class RegionalModal extends React.Component { listRegions = async () => { try { const regions = []; - const data = await API.get("dlts", "/regions"); + const _data = await get({ + apiName: "dlts", + path: "/regions", + }).response; + const data = await _data.body.json(); for (const item of data.regions) { regions.push(item.region); } diff --git a/source/console/src/Components/Results/Results.js b/source/console/src/Components/Results/Results.js index 7251c20..0fdbaca 100644 --- a/source/console/src/Components/Results/Results.js +++ b/source/console/src/Components/Results/Results.js @@ -20,7 +20,7 @@ import { ModalHeader, ModalBody, } from "reactstrap"; -import { Storage } from "aws-amplify"; +import { downloadData } from "aws-amplify/storage"; import AceEditor from "react-ace"; class Results extends React.Component { @@ -101,8 +101,8 @@ class Results extends React.Component { */ retrieveImage = async (metricS3ImageLocation) => { try { - const image = await Storage.get(metricS3ImageLocation, { contentType: "data:image/jpeg;base64", download: true }); - const imageBodyText = await image.Body.text(); + const { body } = await downloadData({ key: metricS3ImageLocation }).result; + const imageBodyText = await body.text(); this.setState({ metricImage: imageBodyText }); } catch (error) { console.error("There was an error trying to retrieve the CloudWatch widget image from S3: ", error); diff --git a/source/console/src/Components/Running/Running.js b/source/console/src/Components/Running/Running.js index 11c9244..86358e5 100644 --- a/source/console/src/Components/Running/Running.js +++ b/source/console/src/Components/Running/Running.js @@ -3,9 +3,9 @@ import React from "react"; import { Button, Table, Col, Row } from "reactstrap"; -import { PubSub } from "@aws-amplify/pubsub"; +import { pubsub } from "../../pubsub"; import Chart from "chart.js/auto"; -import "chartjs-adapter-moment"; +import "chartjs-adapter-date-fns"; declare var awsConfig; class Running extends React.Component { @@ -85,9 +85,9 @@ class Running extends React.Component { //set regions for graph data this.setGraphRegions(); //subscribe to iot topic, handle incoming messages - this.iotSubscription = PubSub.subscribe(`dlt/${this.props.testId}`).subscribe({ + this.iotSubscription = pubsub.subscribe({ topics: `dlt/${this.props.testId}` }).subscribe({ next: (data) => { - this.handleMessage(data.value); + this.handleMessage(data); }, error: (error) => console.error(error), complete: () => console.log("closing connection"), diff --git a/source/console/src/Components/Shared/Buttons/CancelButtons.js b/source/console/src/Components/Shared/Buttons/CancelButtons.js index 8752e4a..1a9cada 100644 --- a/source/console/src/Components/Shared/Buttons/CancelButtons.js +++ b/source/console/src/Components/Shared/Buttons/CancelButtons.js @@ -4,7 +4,7 @@ import React from "react"; import { withRouter } from "react-router-dom"; import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from "reactstrap"; -import { API } from "aws-amplify"; +import { post } from "aws-amplify/api"; class CancelButtons extends React.Component { constructor(props) { @@ -25,7 +25,7 @@ class CancelButtons extends React.Component { cancelTest = async () => { const testId = this.props.testId; try { - await API.post("dlts", `/scenarios/${testId}`); + await post({ apiName: "dlts", path: `/scenarios/${testId}` }).response; } catch (err) { alert(err); } diff --git a/source/console/src/Components/Shared/Buttons/TestControlButtons.js b/source/console/src/Components/Shared/Buttons/TestControlButtons.js index 5b0b04e..53b459a 100644 --- a/source/console/src/Components/Shared/Buttons/TestControlButtons.js +++ b/source/console/src/Components/Shared/Buttons/TestControlButtons.js @@ -4,7 +4,7 @@ import React from "react"; import { withRouter, Link } from "react-router-dom"; import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from "reactstrap"; -import { API } from "aws-amplify"; +import { del, post } from "aws-amplify/api"; import "brace"; import "brace/theme/github"; @@ -30,7 +30,7 @@ class TestControlButtons extends React.Component { deleteTest = async () => { const testId = this.props.testId; try { - await API.del("dlts", `/scenarios/${testId}`); + await del({ apiName: "dlts", path: `/scenarios/${testId}` }).response; } catch (err) { alert(err); } @@ -99,7 +99,8 @@ class TestControlButtons extends React.Component { try { hasEmptyRegion && this.emptyRegionError(); - const response = await API.post("dlts", "/scenarios", { body: payload }); + const _response = await post({ apiName: "dlts", path: "/scenarios", options: { body: payload } }).response; + const response = await _response.body.json(); console.log("Scenario started successfully", response.testId); await this.props.refreshFunction(); } catch (err) { diff --git a/source/console/src/index.js b/source/console/src/index.js index b56bee4..570f547 100644 --- a/source/console/src/index.js +++ b/source/console/src/index.js @@ -2,7 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 import React from "react"; -import ReactDOM from "react-dom"; +import { createRoot } from "react-dom/client"; + import "bootstrap/dist/css/bootstrap.min.css"; import "bootstrap-icons/font/bootstrap-icons.css"; import "./index.css"; @@ -10,8 +11,8 @@ import App from "./App"; import * as serviceWorker from "./serviceWorker"; -ReactDOM.render(, document.getElementById("root")); - +const root = createRoot(document.getElementById("root")); +root.render(); // If you want your app to work offline and load faster, you can change // unregister() to register() below. Note this comes with some pitfalls. // Learn more about service workers: https://bit.ly/CRA-PWA diff --git a/source/console/src/pubsub.js b/source/console/src/pubsub.js new file mode 100644 index 0000000..78427c3 --- /dev/null +++ b/source/console/src/pubsub.js @@ -0,0 +1,10 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { PubSub } from "@aws-amplify/pubsub"; +declare var awsConfig; + +export const pubsub = new PubSub({ + region: awsConfig.aws_project_region, + endpoint: `wss://${awsConfig.aws_iot_endpoint}/mqtt`, +}); diff --git a/source/custom-resource/lib/metrics/index.js b/source/custom-resource/lib/metrics/index.js index 51902fb..d866cbd 100644 --- a/source/custom-resource/lib/metrics/index.js +++ b/source/custom-resource/lib/metrics/index.js @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 const axios = require("axios"); -const moment = require("moment"); const send = async (config, type) => { try { @@ -10,7 +9,8 @@ const send = async (config, type) => { Solution: config.SolutionId, Version: config.Version, UUID: config.UUID, - TimeStamp: moment().utc().format("YYYY-MM-DD HH:mm:ss.S"), + // Date and time instant in a java.sql.Timestamp compatible format + TimeStamp: new Date().toISOString().replace("T", " ").replace("Z", ""), Data: { Type: type, Region: config.Region, diff --git a/source/custom-resource/lib/metrics/index.spec.js b/source/custom-resource/lib/metrics/index.spec.js index cfb4835..438bcc9 100644 --- a/source/custom-resource/lib/metrics/index.spec.js +++ b/source/custom-resource/lib/metrics/index.spec.js @@ -11,32 +11,52 @@ const _config = { Version: "testVersion", UUID: "999-999", Region: "testRegion", - ExistingVpc: "testTest", + existingVPC: "testTest", }; describe("#SEND METRICS", () => { - it("Send metrics success", async () => { + beforeEach(() => { + process.env.METRIC_URL = "TestEndpoint"; + }); + + afterEach(() => { + delete process.env.METRIC_URL; + }); + + it("send metrics success", async () => { + // Arrange + const expected_metric_object = { + Solution: _config.SolutionId, + Version: _config.Version, + UUID: _config.UUID, + Data: { + Type: "Create", + Region: _config.Region, + ExistingVpc: _config.existingVPC, + }, + }; const mock = new MockAdapter(axios); + mock.onPost().reply(200); + + // Act + await lambda.send(_config, "Create"); - lambda.send(_config, "Create", () => { - expect(mock).toHaveBeenCalledTimes(1); - expect(mock).toHaveBeenCalledWith(_config); - expect("metrics").toBeDefined(); - expect("metrics").toHaveProperty("Solution", "SO00XX"); - expect("metrics").toHaveProperty("Version", "testVersion"); - expect("metrics").toHaveProperty("UUID", "999-999"); - expect("metrics").toHaveProperty("Data.Region", "testRegion"); - expect("metrics").toHaveProperty("Data.ExistingVpc", "testTest"); - }); + // Assert + expect(mock.history.post.length).toEqual(1); // called once + expect(mock.history.post[0].url).toEqual(process.env.METRIC_URL); + expect(typeof Date.parse(JSON.parse(mock.history.post[0].data).TimeStamp)).toEqual("number"); // epoch time + expect(JSON.parse(mock.history.post[0].data)).toMatchObject(expected_metric_object); }); - it("should return error", async () => { + it("should not throw error, when metric send fails", async () => { + // Arrange let mock = new MockAdapter(axios); - mock.onPut().networkError(); + mock.onPost().networkError(); + + // Act + await lambda.send(_config, "Create"); - await lambda.send(_config, "Create").catch((err) => { - expect(mock).toHaveBeenCalledTimes(1); - expect(err.toString()).toEqual("Error: Network Error"); - }); + // Assert + expect(mock.history.post.length).toBe(1); // called once }); }); diff --git a/source/custom-resource/package.json b/source/custom-resource/package.json index 64fb2c3..608e36d 100644 --- a/source/custom-resource/package.json +++ b/source/custom-resource/package.json @@ -1,6 +1,6 @@ { "name": "custom-resource", - "version": "3.2.5", + "version": "3.2.6", "description": "cfn custom resources for distributed load testing on AWS workflow", "repository": { "type": "git", @@ -19,7 +19,6 @@ "dependencies": { "axios": "^1.6.0", "js-yaml": "^4.1.0", - "moment": "^2.29.1", "solution-utils": "file:../solution-utils", "aws-sdk": "^2.1001.0", "uuid": "^8.3.1" diff --git a/source/infrastructure/lib/back-end/step-functions.ts b/source/infrastructure/lib/back-end/step-functions.ts index d457dfb..c986078 100644 --- a/source/infrastructure/lib/back-end/step-functions.ts +++ b/source/infrastructure/lib/back-end/step-functions.ts @@ -110,7 +110,7 @@ export class TaskRunnerStepFunctionConstruct extends Construct { inputPath: "$", resultPath: JsonPath.DISCARD, itemsPath: "$.testTaskConfig", - parameters: { + itemSelector: { "testTaskConfig.$": "$$.Map.Item.Value", "testId.$": "$.testId", "testType.$": "$.testType", @@ -166,7 +166,7 @@ export class TaskRunnerStepFunctionConstruct extends Construct { }); checkRunningTests.next(noRunningTests); - const definition = Chain.start(regionConfigsForTest.iterator(checkRunningTests)).next(parseResult); + const definition = Chain.start(regionConfigsForTest.itemProcessor(checkRunningTests)).next(parseResult); this.taskRunnerStepFunctions = new StateMachine(this, "TaskRunnerStepFunctions", { definitionBody: DefinitionBody.fromChainable(definition), diff --git a/source/infrastructure/lib/back-end/test-task-lambdas.ts b/source/infrastructure/lib/back-end/test-task-lambdas.ts index 8a32bed..315e816 100644 --- a/source/infrastructure/lib/back-end/test-task-lambdas.ts +++ b/source/infrastructure/lib/back-end/test-task-lambdas.ts @@ -48,6 +48,7 @@ export interface TestRunnerLambdasConstructProps { readonly sourceCodeBucket: IBucket; readonly sourceCodePrefix: string; readonly uuid: string; + readonly mainStackRegion: string; } /** @@ -164,7 +165,7 @@ export class TestRunnerLambdasConstruct extends Construct { }), new PolicyStatement({ effect: Effect.ALLOW, - actions: ["ecs:RunTask", "ecs:DescribeTasks"], + actions: ["ecs:RunTask", "ecs:DescribeTasks", "ecs:TagResource"], resources: [taskArn, taskDefArn], }), new PolicyStatement({ @@ -209,6 +210,7 @@ export class TestRunnerLambdasConstruct extends Construct { SCENARIOS_TABLE: props.scenariosTable.tableName, SOLUTION_ID: props.solutionId, VERSION: props.solutionVersion, + MAIN_STACK_REGION: props.mainStackRegion, }, runtime: Runtime.NODEJS_18_X, timeout: Duration.seconds(900), diff --git a/source/infrastructure/lib/common-resources/common-resources.ts b/source/infrastructure/lib/common-resources/common-resources.ts index 7d03453..c84e3ba 100644 --- a/source/infrastructure/lib/common-resources/common-resources.ts +++ b/source/infrastructure/lib/common-resources/common-resources.ts @@ -122,6 +122,6 @@ export class CommonResourcesConstruct extends Construct { solutionName, }, }); - application.associateAttributeGroup(attributeGroup); + attributeGroup.associateWith(application); } } diff --git a/source/infrastructure/lib/distributed-load-testing-on-aws-stack.ts b/source/infrastructure/lib/distributed-load-testing-on-aws-stack.ts index 1e1e437..0e5826c 100644 --- a/source/infrastructure/lib/distributed-load-testing-on-aws-stack.ts +++ b/source/infrastructure/lib/distributed-load-testing-on-aws-stack.ts @@ -373,6 +373,7 @@ export class DLTStack extends Stack { scenariosBucket: dltStorage.scenariosBucket.bucketName, scenariosTable: dltStorage.scenariosTable, uuid, + mainStackRegion, }); const taskRunnerStepFunctions = new TaskRunnerStepFunctionConstruct(this, "DLTStepFunction", { diff --git a/source/infrastructure/lib/front-end/api.ts b/source/infrastructure/lib/front-end/api.ts index 0278bfe..4970f1a 100644 --- a/source/infrastructure/lib/front-end/api.ts +++ b/source/infrastructure/lib/front-end/api.ts @@ -98,7 +98,7 @@ export class DLTAPI extends Construct { }), new PolicyStatement({ effect: Effect.ALLOW, - actions: ["ecs:RunTask", "ecs:DescribeTasks"], + actions: ["ecs:RunTask", "ecs:DescribeTasks", "ecs:TagResource"], resources: [taskArn, taskDefArn], }), new PolicyStatement({ diff --git a/source/infrastructure/lib/testing-resources/regional-permissions.ts b/source/infrastructure/lib/testing-resources/regional-permissions.ts index f384c37..18c8a9d 100644 --- a/source/infrastructure/lib/testing-resources/regional-permissions.ts +++ b/source/infrastructure/lib/testing-resources/regional-permissions.ts @@ -68,7 +68,7 @@ export class RegionalPermissionsConstruct extends Construct { statements: [ new PolicyStatement({ effect: Effect.ALLOW, - actions: ["ecs:RunTask", "ecs:DescribeTasks"], + actions: ["ecs:RunTask", "ecs:DescribeTasks", "ecs:TagResource"], resources: [taskArn, taskDefArn], }), new PolicyStatement({ diff --git a/source/infrastructure/package.json b/source/infrastructure/package.json index 6a85440..2ef5a3e 100644 --- a/source/infrastructure/package.json +++ b/source/infrastructure/package.json @@ -1,6 +1,6 @@ { "name": "distributed-load-testing-on-aws-infrastructure", - "version": "3.2.5", + "version": "3.2.6", "author": { "name": "Amazon Web Services", "url": "https://aws.amazon.com/solutions" diff --git a/source/infrastructure/test/__snapshots__/api.test.ts.snap b/source/infrastructure/test/__snapshots__/api.test.ts.snap index 016d5a1..a9232d7 100644 --- a/source/infrastructure/test/__snapshots__/api.test.ts.snap +++ b/source/infrastructure/test/__snapshots__/api.test.ts.snap @@ -263,6 +263,7 @@ exports[`DLT API Test 1`] = ` "Action": [ "ecs:RunTask", "ecs:DescribeTasks", + "ecs:TagResource", ], "Effect": "Allow", "Resource": [ diff --git a/source/infrastructure/test/__snapshots__/distributed-load-testing-on-aws-regional.test.ts.snap b/source/infrastructure/test/__snapshots__/distributed-load-testing-on-aws-regional.test.ts.snap index 0ec74eb..4cc8cc7 100644 --- a/source/infrastructure/test/__snapshots__/distributed-load-testing-on-aws-regional.test.ts.snap +++ b/source/infrastructure/test/__snapshots__/distributed-load-testing-on-aws-regional.test.ts.snap @@ -226,23 +226,6 @@ exports[`Distributed Load Testing Regional stack test 1`] = ` }, "Type": "AWS::ServiceCatalogAppRegistry::ResourceAssociation", }, - "AppRegistryAttributeGroupAssociationa7177d48be75C139509C": { - "Properties": { - "Application": { - "Fn::GetAtt": [ - "AppRegistry968496A3", - "Id", - ], - }, - "AttributeGroup": { - "Fn::GetAtt": [ - "DefaultApplicationAttributesFC1CC26B", - "Id", - ], - }, - }, - "Type": "AWS::ServiceCatalogAppRegistry::AttributeGroupAssociation", - }, "CommonResourcesCloudWatchLogsPolicyB8257A4C": { "Properties": { "PolicyDocument": { @@ -958,6 +941,23 @@ exports[`Distributed Load Testing Regional stack test 1`] = ` }, "Type": "AWS::EC2::Subnet", }, + "DefaultApplicationAttributesApplicationAttributeGroupAssociationbd111f642bde61D21AEC": { + "Properties": { + "Application": { + "Fn::GetAtt": [ + "AppRegistry968496A3", + "Id", + ], + }, + "AttributeGroup": { + "Fn::GetAtt": [ + "DefaultApplicationAttributesFC1CC26B", + "Id", + ], + }, + }, + "Type": "AWS::ServiceCatalogAppRegistry::AttributeGroupAssociation", + }, "DefaultApplicationAttributesFC1CC26B": { "Properties": { "Attributes": { @@ -1719,6 +1719,7 @@ exports[`Distributed Load Testing Regional stack test 1`] = ` "Action": [ "ecs:RunTask", "ecs:DescribeTasks", + "ecs:TagResource", ], "Effect": "Allow", "Resource": [ diff --git a/source/infrastructure/test/__snapshots__/distributed-load-testing-on-aws-stack.test.ts.snap b/source/infrastructure/test/__snapshots__/distributed-load-testing-on-aws-stack.test.ts.snap index 3553515..a4e1b24 100644 --- a/source/infrastructure/test/__snapshots__/distributed-load-testing-on-aws-stack.test.ts.snap +++ b/source/infrastructure/test/__snapshots__/distributed-load-testing-on-aws-stack.test.ts.snap @@ -422,23 +422,6 @@ exports[`Distributed Load Testing stack test 1`] = ` }, "Type": "AWS::ServiceCatalogAppRegistry::ResourceAssociation", }, - "AppRegistryAttributeGroupAssociation17c9944e720456F5A644": { - "Properties": { - "Application": { - "Fn::GetAtt": [ - "AppRegistry968496A3", - "Id", - ], - }, - "AttributeGroup": { - "Fn::GetAtt": [ - "DefaultApplicationAttributesFC1CC26B", - "Id", - ], - }, - }, - "Type": "AWS::ServiceCatalogAppRegistry::AttributeGroupAssociation", - }, "DLTApi0C903EB5": { "Properties": { "Description": { @@ -878,6 +861,7 @@ exports[`Distributed Load Testing stack test 1`] = ` "Action": [ "ecs:RunTask", "ecs:DescribeTasks", + "ecs:TagResource", ], "Effect": "Allow", "Resource": [ @@ -3953,6 +3937,7 @@ exports[`Distributed Load Testing stack test 1`] = ` "Action": [ "ecs:RunTask", "ecs:DescribeTasks", + "ecs:TagResource", ], "Effect": "Allow", "Resource": [ @@ -4577,6 +4562,9 @@ exports[`Distributed Load Testing stack test 1`] = ` "Description": "Task runner for ECS task definitions", "Environment": { "Variables": { + "MAIN_STACK_REGION": { + "Ref": "AWS::Region", + }, "SCENARIOS_BUCKET": { "Ref": "DLTTestRunnerStorageDLTScenariosBucketA9290D21", }, @@ -4868,7 +4856,7 @@ exports[`Distributed Load Testing stack test 1`] = ` "Fn::Join": [ "", [ - "{"StartAt":"Regions for testing","States":{"Regions for testing":{"Type":"Map","ResultPath":null,"Next":"Parse result","InputPath":"$","Parameters":{"testTaskConfig.$":"$$.Map.Item.Value","testId.$":"$.testId","testType.$":"$.testType","fileType.$":"$.fileType","showLive.$":"$.showLive","testDuration.$":"$.testDuration","prefix.$":"$.prefix"},"Iterator":{"StartAt":"Check running tests","States":{"Check running tests":{"Next":"No running tests","Retry":[{"ErrorEquals":["Lambda.ClientExecutionTimeoutException","Lambda.ServiceException","Lambda.AWSLambdaException","Lambda.SdkClientException"],"IntervalSeconds":2,"MaxAttempts":6,"BackoffRate":2}],"Type":"Task","InputPath":"$","OutputPath":"$.Payload","Resource":"arn:", + "{"StartAt":"Regions for testing","States":{"Regions for testing":{"Type":"Map","ResultPath":null,"Next":"Parse result","InputPath":"$","ItemsPath":"$.testTaskConfig","ItemSelector":{"testTaskConfig.$":"$$.Map.Item.Value","testId.$":"$.testId","testType.$":"$.testType","fileType.$":"$.fileType","showLive.$":"$.showLive","testDuration.$":"$.testDuration","prefix.$":"$.prefix"},"ItemProcessor":{"ProcessorConfig":{"Mode":"INLINE"},"StartAt":"Check running tests","States":{"Check running tests":{"Next":"No running tests","Retry":[{"ErrorEquals":["Lambda.ClientExecutionTimeoutException","Lambda.ServiceException","Lambda.AWSLambdaException","Lambda.SdkClientException"],"IntervalSeconds":2,"MaxAttempts":6,"BackoffRate":2}],"Type":"Task","InputPath":"$","OutputPath":"$.Payload","Resource":"arn:", { "Ref": "AWS::Partition", }, @@ -4934,7 +4922,7 @@ exports[`Distributed Load Testing stack test 1`] = ` "Arn", ], }, - "","Payload.$":"$"}},"Wait 1 minute - task status":{"Type":"Wait","Comment":"Wait 1 minute to check task status again","Seconds":60,"Next":"Check task status"},"Are all tasks done?":{"Type":"Choice","Choices":[{"Variable":"$.isRunning","BooleanEquals":false,"Next":"Map End"}],"Default":"Wait 1 minute - task status"},"Map End":{"Type":"Pass","End":true}}},"ItemsPath":"$.testTaskConfig"},"Parse result":{"Next":"Done","Retry":[{"ErrorEquals":["Lambda.ClientExecutionTimeoutException","Lambda.ServiceException","Lambda.AWSLambdaException","Lambda.SdkClientException"],"IntervalSeconds":2,"MaxAttempts":6,"BackoffRate":2}],"Type":"Task","Resource":"arn:", + "","Payload.$":"$"}},"Wait 1 minute - task status":{"Type":"Wait","Comment":"Wait 1 minute to check task status again","Seconds":60,"Next":"Check task status"},"Are all tasks done?":{"Type":"Choice","Choices":[{"Variable":"$.isRunning","BooleanEquals":false,"Next":"Map End"}],"Default":"Wait 1 minute - task status"},"Map End":{"Type":"Pass","End":true}}}},"Parse result":{"Next":"Done","Retry":[{"ErrorEquals":["Lambda.ClientExecutionTimeoutException","Lambda.ServiceException","Lambda.AWSLambdaException","Lambda.SdkClientException"],"IntervalSeconds":2,"MaxAttempts":6,"BackoffRate":2}],"Type":"Task","Resource":"arn:", { "Ref": "AWS::Partition", }, @@ -5056,20 +5044,6 @@ exports[`Distributed Load Testing stack test 1`] = ` "Properties": { "PolicyDocument": { "Statement": [ - { - "Action": [ - "logs:CreateLogDelivery", - "logs:GetLogDelivery", - "logs:UpdateLogDelivery", - "logs:DeleteLogDelivery", - "logs:ListLogDeliveries", - "logs:PutResourcePolicy", - "logs:DescribeResourcePolicies", - "logs:DescribeLogGroups", - ], - "Effect": "Allow", - "Resource": "*", - }, { "Action": "lambda:InvokeFunction", "Effect": "Allow", @@ -5174,6 +5148,20 @@ exports[`Distributed Load Testing stack test 1`] = ` }, ], }, + { + "Action": [ + "logs:CreateLogDelivery", + "logs:GetLogDelivery", + "logs:UpdateLogDelivery", + "logs:DeleteLogDelivery", + "logs:ListLogDeliveries", + "logs:PutResourcePolicy", + "logs:DescribeResourcePolicies", + "logs:DescribeLogGroups", + ], + "Effect": "Allow", + "Resource": "*", + }, ], "Version": "2012-10-17", }, @@ -5745,6 +5733,23 @@ exports[`Distributed Load Testing stack test 1`] = ` }, "Type": "AWS::EC2::Subnet", }, + "DefaultApplicationAttributesApplicationAttributeGroupAssociation137261565f10E63E4638": { + "Properties": { + "Application": { + "Fn::GetAtt": [ + "AppRegistry968496A3", + "Id", + ], + }, + "AttributeGroup": { + "Fn::GetAtt": [ + "DefaultApplicationAttributesFC1CC26B", + "Id", + ], + }, + }, + "Type": "AWS::ServiceCatalogAppRegistry::AttributeGroupAssociation", + }, "DefaultApplicationAttributesFC1CC26B": { "Properties": { "Attributes": { diff --git a/source/infrastructure/test/__snapshots__/regional-permissions.test.ts.snap b/source/infrastructure/test/__snapshots__/regional-permissions.test.ts.snap index d03fa3f..7c17b2e 100644 --- a/source/infrastructure/test/__snapshots__/regional-permissions.test.ts.snap +++ b/source/infrastructure/test/__snapshots__/regional-permissions.test.ts.snap @@ -200,6 +200,7 @@ exports[`DLT Regional Permission Test 1`] = ` "Action": [ "ecs:RunTask", "ecs:DescribeTasks", + "ecs:TagResource", ], "Effect": "Allow", "Resource": [ diff --git a/source/infrastructure/test/__snapshots__/step-functions.test.ts.snap b/source/infrastructure/test/__snapshots__/step-functions.test.ts.snap index 06b1d09..95e2b97 100644 --- a/source/infrastructure/test/__snapshots__/step-functions.test.ts.snap +++ b/source/infrastructure/test/__snapshots__/step-functions.test.ts.snap @@ -153,7 +153,7 @@ exports[`DLT API Test 1`] = ` "Fn::Join": [ "", [ - "{"StartAt":"Regions for testing","States":{"Regions for testing":{"Type":"Map","ResultPath":null,"Next":"Parse result","InputPath":"$","Parameters":{"testTaskConfig.$":"$$.Map.Item.Value","testId.$":"$.testId","testType.$":"$.testType","fileType.$":"$.fileType","showLive.$":"$.showLive","testDuration.$":"$.testDuration","prefix.$":"$.prefix"},"Iterator":{"StartAt":"Check running tests","States":{"Check running tests":{"Next":"No running tests","Retry":[{"ErrorEquals":["Lambda.ClientExecutionTimeoutException","Lambda.ServiceException","Lambda.AWSLambdaException","Lambda.SdkClientException"],"IntervalSeconds":2,"MaxAttempts":6,"BackoffRate":2}],"Type":"Task","InputPath":"$","OutputPath":"$.Payload","Resource":"arn:", + "{"StartAt":"Regions for testing","States":{"Regions for testing":{"Type":"Map","ResultPath":null,"Next":"Parse result","InputPath":"$","ItemsPath":"$.testTaskConfig","ItemSelector":{"testTaskConfig.$":"$$.Map.Item.Value","testId.$":"$.testId","testType.$":"$.testType","fileType.$":"$.fileType","showLive.$":"$.showLive","testDuration.$":"$.testDuration","prefix.$":"$.prefix"},"ItemProcessor":{"ProcessorConfig":{"Mode":"INLINE"},"StartAt":"Check running tests","States":{"Check running tests":{"Next":"No running tests","Retry":[{"ErrorEquals":["Lambda.ClientExecutionTimeoutException","Lambda.ServiceException","Lambda.AWSLambdaException","Lambda.SdkClientException"],"IntervalSeconds":2,"MaxAttempts":6,"BackoffRate":2}],"Type":"Task","InputPath":"$","OutputPath":"$.Payload","Resource":"arn:", { "Ref": "AWS::Partition", }, @@ -219,7 +219,7 @@ exports[`DLT API Test 1`] = ` "Arn", ], }, - "","Payload.$":"$"}},"Wait 1 minute - task status":{"Type":"Wait","Comment":"Wait 1 minute to check task status again","Seconds":60,"Next":"Check task status"},"Are all tasks done?":{"Type":"Choice","Choices":[{"Variable":"$.isRunning","BooleanEquals":false,"Next":"Map End"}],"Default":"Wait 1 minute - task status"},"Map End":{"Type":"Pass","End":true}}},"ItemsPath":"$.testTaskConfig"},"Parse result":{"Next":"Done","Retry":[{"ErrorEquals":["Lambda.ClientExecutionTimeoutException","Lambda.ServiceException","Lambda.AWSLambdaException","Lambda.SdkClientException"],"IntervalSeconds":2,"MaxAttempts":6,"BackoffRate":2}],"Type":"Task","Resource":"arn:", + "","Payload.$":"$"}},"Wait 1 minute - task status":{"Type":"Wait","Comment":"Wait 1 minute to check task status again","Seconds":60,"Next":"Check task status"},"Are all tasks done?":{"Type":"Choice","Choices":[{"Variable":"$.isRunning","BooleanEquals":false,"Next":"Map End"}],"Default":"Wait 1 minute - task status"},"Map End":{"Type":"Pass","End":true}}}},"Parse result":{"Next":"Done","Retry":[{"ErrorEquals":["Lambda.ClientExecutionTimeoutException","Lambda.ServiceException","Lambda.AWSLambdaException","Lambda.SdkClientException"],"IntervalSeconds":2,"MaxAttempts":6,"BackoffRate":2}],"Type":"Task","Resource":"arn:", { "Ref": "AWS::Partition", }, @@ -317,20 +317,6 @@ exports[`DLT API Test 1`] = ` "Properties": { "PolicyDocument": { "Statement": [ - { - "Action": [ - "logs:CreateLogDelivery", - "logs:GetLogDelivery", - "logs:UpdateLogDelivery", - "logs:DeleteLogDelivery", - "logs:ListLogDeliveries", - "logs:PutResourcePolicy", - "logs:DescribeResourcePolicies", - "logs:DescribeLogGroups", - ], - "Effect": "Allow", - "Resource": "*", - }, { "Action": "lambda:InvokeFunction", "Effect": "Allow", @@ -357,6 +343,20 @@ exports[`DLT API Test 1`] = ` }, ], }, + { + "Action": [ + "logs:CreateLogDelivery", + "logs:GetLogDelivery", + "logs:UpdateLogDelivery", + "logs:DeleteLogDelivery", + "logs:ListLogDeliveries", + "logs:PutResourcePolicy", + "logs:DescribeResourcePolicies", + "logs:DescribeLogGroups", + ], + "Effect": "Allow", + "Resource": "*", + }, ], "Version": "2012-10-17", }, diff --git a/source/infrastructure/test/__snapshots__/test-task-lambdas.test.ts.snap b/source/infrastructure/test/__snapshots__/test-task-lambdas.test.ts.snap index b012da9..a63751f 100644 --- a/source/infrastructure/test/__snapshots__/test-task-lambdas.test.ts.snap +++ b/source/infrastructure/test/__snapshots__/test-task-lambdas.test.ts.snap @@ -40,6 +40,7 @@ exports[`DLT Task Lambda Test 1`] = ` "Action": [ "ecs:RunTask", "ecs:DescribeTasks", + "ecs:TagResource", ], "Effect": "Allow", "Resource": [ @@ -457,6 +458,7 @@ exports[`DLT Task Lambda Test 1`] = ` "Description": "Task runner for ECS task definitions", "Environment": { "Variables": { + "MAIN_STACK_REGION": "us-east-1", "SCENARIOS_BUCKET": "testBucket", "SCENARIOS_TABLE": { "Ref": "TestTable5769773A", diff --git a/source/infrastructure/test/regional-permissions.test.ts b/source/infrastructure/test/regional-permissions.test.ts index a583f84..82f8531 100644 --- a/source/infrastructure/test/regional-permissions.test.ts +++ b/source/infrastructure/test/regional-permissions.test.ts @@ -27,7 +27,7 @@ test("DLT Regional Permission Test", () => { PolicyDocument: { Statement: [ { - Action: ["ecs:RunTask", "ecs:DescribeTasks"], + Action: ["ecs:RunTask", "ecs:DescribeTasks", "ecs:TagResource"], Effect: "Allow", Resource: [ { diff --git a/source/infrastructure/test/test-task-lambdas.test.ts b/source/infrastructure/test/test-task-lambdas.test.ts index fb3b35e..5295c4f 100644 --- a/source/infrastructure/test/test-task-lambdas.test.ts +++ b/source/infrastructure/test/test-task-lambdas.test.ts @@ -78,6 +78,7 @@ test("DLT Task Lambda Test", () => { scenariosBucket: "testBucket", scenariosTable: testTable, uuid: "testId", + mainStackRegion: "us-east-1", }); expect(Template.fromStack(stack)).toMatchSnapshot(); diff --git a/source/package.json b/source/package.json index 5e14bd2..372dfff 100644 --- a/source/package.json +++ b/source/package.json @@ -1,6 +1,6 @@ { "name": "source", - "version": "3.2.5", + "version": "3.2.6", "private": true, "description": "ESLint and prettier dependencies to be used within the solution", "license": "Apache-2.0", @@ -10,19 +10,19 @@ }, "scripts": { "lint": "npx eslint . --ext .ts,.js", - "prettier-format": "npx prettier --config .prettierrc.yml '**/*.{js,ts}' --write", - "prettier-check": "npx prettier --config .prettierrc.yml '**/*.{js,ts}' --check", - "install:api-services": "cd ./api-services && npm run clean && npm install", - "install:console": "cd ./console && npm run clean && npm install", - "install:custom-resource": "cd ./custom-resource && npm run clean && npm install", - "install:infrastructure": "cd ./infrastructure && npm run clean && npm install", - "install:real-time-data-publisher": "cd ./real-time-data-publisher && npm run clean && npm install", - "install:results-parser": "cd ./results-parser && npm run clean && npm install", - "install:solution-utils": "cd ./solution-utils && npm run clean && npm install", - "install:task-canceler": "cd ./task-canceler && npm run clean && npm install", - "install:task-runner": "cd ./task-runner && npm run clean && npm install", - "install:task-status-checker": "cd ./task-status-checker && npm run clean && npm install", - "install:all": "find . -maxdepth 2 -name package.json -execdir npm run clean \\; -execdir npm install \\;", + "prettier-format": "npx prettier --config .prettierrc.yml '**/*.{js,ts}' --ignore-path '../.gitignore' --write", + "prettier-check": "prettier --config .prettierrc.yml '**/*.{js,ts}' --check --ignore-path '../.gitignore'", + "install:api-services": "cd ./api-services && npm ci", + "install:console": "cd ./console && npm ci", + "install:custom-resource": "cd ./custom-resource && npm ci", + "install:infrastructure": "cd ./infrastructure && npm ci", + "install:real-time-data-publisher": "cd ./real-time-data-publisher && npm ci", + "install:results-parser": "cd ./results-parser && npm run clean && ci", + "install:solution-utils": "cd ./solution-utils && npm run clean && ci", + "install:task-canceler": "cd ./task-canceler && npm run clean && ci", + "install:task-runner": "cd ./task-runner && npm run clean && ci", + "install:task-status-checker": "cd ./task-status-checker && npm ci", + "install:all": "find . -maxdepth 2 -name package.json -execdir npm ci \\;", "clean:all": "find . -maxdepth 2 -name package.json -execdir npm run clean \\;", "clean": "rm -rf node_modules package-lock.json" }, diff --git a/source/real-time-data-publisher/index.js b/source/real-time-data-publisher/index.js index 85007f9..fa8c711 100644 --- a/source/real-time-data-publisher/index.js +++ b/source/real-time-data-publisher/index.js @@ -48,10 +48,10 @@ exports.handler = async function (event) { const logString = logItem.message; // Define individual regex patterns for each condition const wordPattern = /^\w+/; - const vuPattern = /\d+(\.\d+)?(?=\svu)/; - const succPattern = /\d+(\.\d+)?(?=\ssucc)/; - const failPattern = /\d+(\.\d+)?(?=\sfail)/; - const avgRTPattern = /\d+(\.\d+)?(?=\savg rt\s)/; + const vuPattern = /\d{1,6}(?=\svu)/; + const succPattern = /\d{1,6}(?=\ssucc)/; + const failPattern = /\d{1,6}(?=\sfail)/; + const avgRTPattern = /\d{1,3}(\.\d{1,3})?(?=\savg rt\s)/; // Combine the patterns using the | (or) operator const regex = new RegExp( diff --git a/source/real-time-data-publisher/package.json b/source/real-time-data-publisher/package.json index 4c57831..1f609d8 100644 --- a/source/real-time-data-publisher/package.json +++ b/source/real-time-data-publisher/package.json @@ -1,6 +1,6 @@ { "name": "real-time-data-publisher", - "version": "3.2.5", + "version": "3.2.6", "description": "Publishes real time test data to an IoT endpoint", "repository": { "type": "git", @@ -14,11 +14,11 @@ "main": "index.js", "scripts": { "clean": "rm -rf node_modules package-lock.json", - "test": "jest lib/*.spec.js --coverage --silent" + "test": "jest --coverage --silent" }, "dependencies": { "solution-utils": "file:../solution-utils", - "aws-sdk": "^2.1001.0" + "aws-sdk": "^2.1001.0" }, "devDependencies": { "jest": "29.7.0" diff --git a/source/results-parser/index.js b/source/results-parser/index.js index efea882..6af6997 100644 --- a/source/results-parser/index.js +++ b/source/results-parser/index.js @@ -1,9 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -const moment = require("moment"); const parser = require("./lib/parser/"); -const metrics = require("./lib/metrics/"); const AWS = require("aws-sdk"); const utils = require("solution-utils"); let options = {}; @@ -158,7 +156,7 @@ const getFilesByRegion = async (resultList) => { //get all results files from test sorted by region for (const content of resultList) { //extract region from file name - const regex = /\w+-\w+-\w+(?=.xml)/g; + const regex = /[a-z]{1,3}-[a-z]+-\d(?=.xml)/g; // Check if logString exceeds character limit if (content.Key.length > 250) throw new Error("Log message exceeds character limit."); @@ -196,7 +194,10 @@ const getFilesByRegion = async (resultList) => { exports.handler = async (event) => { console.log(JSON.stringify(event, null, 2)); const { testId, fileType, prefix, testTaskConfig: eventConfigs } = event; - const endTime = moment().utc().format("YYYY-MM-DD HH:mm:ss"); + const endTime = new Date() + .toISOString() + .replace("T", " ") + .replace(/\.\d+Z$/, ""); try { const scenariosTableItems = await getScenariosTableItems(testId); @@ -226,7 +227,14 @@ exports.handler = async (event) => { } // Send anonymized metrics - if (process.env.SEND_METRIC === "Yes") await metrics.send({ testType, totalDuration, fileType, testResult }); + if (process.env.SEND_METRIC === "Yes") + await utils.sendMetric({ + Type: "TaskCompletion", + TestType: testType, + FileType: fileType || (testType === "simple" ? "none" : "script"), + TestResult: testResult, + Duration: totalDuration, + }); return "success"; } catch (error) { console.error(error); @@ -253,3 +261,7 @@ const updateScenariosTable = async (testId, errorReason) => { }) .promise(); }; + +if (process.env.RUNNING_UNIT_TESTS === "True") { + exports._getFilesByRegion = getFilesByRegion; +} diff --git a/source/results-parser/lib/index.spec.js b/source/results-parser/lib/index.spec.js new file mode 100644 index 0000000..ea5bad0 --- /dev/null +++ b/source/results-parser/lib/index.spec.js @@ -0,0 +1,107 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +const { + mockS3, + mockDDBDocumentClient, + mockResultParserEvent, + mockS3ListObjectResponse, + mockParser, + mockSolutionUtils, +} = require("./mock.js"); + +process.env = { + RUNNING_UNIT_TESTS: "True", + SCENARIOS_BUCKET: "MyBucket", + SCENARIOS_TABLE: "MyTable", +}; + +const { handler, _getFilesByRegion } = require("../index.js"); + +describe("Test getFilesByRegion()", () => { + beforeEach(() => { + mockS3.getObject.mockReset(); + }); + + it("test regex for aws region matches correctly in bucket object key", async () => { + // Arrange + const validRegion = "us-east-2"; + const validBucketObjectKey = `114.44:25:71T20-20-4202-a3677174-a062-4a50-bbe2-50b995a536b5-${validRegion}.xml`; + const invalidBucketObjectKey = "114.44:25:71T20-20-4202-a3677174-a062-4a50-bbe2-50b995a536b5-my-test-region.xml"; + mockS3.getObject.mockImplementation(() => ({ + promise() { + return Promise.resolve(""); + }, + })); + + // Act & Assert + const successResult = await _getFilesByRegion([{ Key: validBucketObjectKey }]); + expect(successResult).toHaveProperty(validRegion); + + const failureResult = await _getFilesByRegion([{ Key: invalidBucketObjectKey }]); + expect(failureResult).toEqual({}); // no matched region + }); +}); + +describe("Handler", () => { + beforeEach(() => { + mockDDBDocumentClient.get.mockReset(); + mockDDBDocumentClient.update.mockReset(); + mockS3.listObjectsV2.mockReset(); + mockS3.getObject.mockReset(); + }); + const successfulMocks = () => { + mockDDBDocumentClient.update.mockImplementation(() => ({ + promise() { + return Promise.resolve(""); + }, + })); + mockDDBDocumentClient.get.mockImplementation(() => ({ + promise() { + return Promise.resolve({ Item: {} }); + }, + })); + mockS3.listObjectsV2.mockImplementation(() => ({ + promise() { + return Promise.resolve(mockS3ListObjectResponse); + }, + })); + mockS3.getObject.mockImplementation(() => ({ + promise() { + return Promise.resolve({ Body: "STREAMING_BLOB_VALUE" }); + }, + })); + mockParser.results.mockReturnValue({}); + mockParser.finalResults.mockReturnValue({ metricLocation: "" }); + mockParser.createWidget.mockReturnValue({}); + }; + + it("test handler for successful invocation", async () => { + // Arrange + successfulMocks(); + + // Act + const response = await handler(mockResultParserEvent); + + // Assert + expect(response).toEqual("success"); + }); + + it("metric sent successfully", async () => { + // Arrange + successfulMocks(); + process.env.SEND_METRIC = "Yes"; + mockSolutionUtils.sendMetric.mockResolvedValue("metric sent"); + + // Act + await handler(mockResultParserEvent); + + // Assert + expect(mockSolutionUtils.sendMetric.mock.calls).toHaveLength(1); + expect(mockSolutionUtils.sendMetric.mock.calls[0][0]).toHaveProperty("Type", "TaskCompletion"); + expect(mockSolutionUtils.sendMetric.mock.calls[0][0]).toHaveProperty("FileType"); + expect(mockSolutionUtils.sendMetric.mock.calls[0][0]).toHaveProperty("TestType"); + expect(mockSolutionUtils.sendMetric.mock.calls[0][0]).toHaveProperty("Duration"); + expect(mockSolutionUtils.sendMetric.mock.calls[0][0]).toHaveProperty("TestResult"); + }); +}); diff --git a/source/results-parser/lib/mock.js b/source/results-parser/lib/mock.js new file mode 100644 index 0000000..41c8c6c --- /dev/null +++ b/source/results-parser/lib/mock.js @@ -0,0 +1,83 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +const mockS3 = { + getObject: jest.fn(), + listObjectsV2: jest.fn(), +}; +const mockDDBDocumentClient = { + get: jest.fn(), + update: jest.fn(), +}; + +const mockParser = { + results: jest.fn(), + finalResults: jest.fn(), + createWidget: jest.fn(), + deleteRegionalMetricFilter: jest.fn(), + putTestHistory: jest.fn(), + updateTable: jest.fn(), +}; + +const mockSolutionUtils = { + getOptions: jest.fn(), + sendMetric: jest.fn(), +}; + +jest.mock("aws-sdk", () => ({ + S3: jest.fn(() => ({ + ...mockS3, + })), + DynamoDB: { + DocumentClient: jest.fn(() => ({ + ...mockDDBDocumentClient, + })), + }, +})); + +jest.mock("./parser", () => ({ ...mockParser })); + +jest.mock("solution-utils", () => ({ ...mockSolutionUtils })); + +const mockResultParserEvent = { + testTaskConfig: [ + { + concurrency: 2, + taskCount: 5, + region: "my-region-1", + ecsCloudWatchLogGroup: "myLogGroup", + taskCluster: "myTaskCluster", + testId: "myTestId", + taskDefinition: "myTaskDefinition", + subnetB: "mySubnetB", + taskImage: "myImage", + subnetA: "mySubnetA", + taskSecurityGroup: "mySecurityGroup", + }, + ], + testId: "Q9Isyy5DIK", + testType: "simple", + fileType: "none", + showLive: true, + testDuration: 60, + prefix: "613.75:44:12T80-20-4202", +}; + +const mockS3ListObjectResponse = { + Contents: [ + { + ETag: '"70ee1738b6b21e2c8a43f3a5ab0eee71"', + Key: "114.44:25:71T20-20-4202-a3677174-a062-4a50-bbe2-50b995a536b5-my-region-1.xml", + LastModified: "", + Size: 11, + StorageClass: "STANDARD", + }, + ], +}; + +exports.mockS3 = mockS3; +exports.mockDDBDocumentClient = mockDDBDocumentClient; +exports.mockResultParserEvent = mockResultParserEvent; +exports.mockS3ListObjectResponse = mockS3ListObjectResponse; +exports.mockParser = mockParser; +exports.mockSolutionUtils = mockSolutionUtils; diff --git a/source/results-parser/package.json b/source/results-parser/package.json index cac6747..d02c7d2 100644 --- a/source/results-parser/package.json +++ b/source/results-parser/package.json @@ -1,6 +1,6 @@ { "name": "results-parser", - "version": "3.2.5", + "version": "3.2.6", "description": "result parser for indexing xml test results to DynamoDB", "repository": { "type": "git", @@ -14,15 +14,13 @@ "main": "index.js", "scripts": { "clean": "rm -rf node_modules package-lock.json", - "test": "jest lib/**/*.spec.js --coverage --silent" + "test": "jest --coverage --silent" }, "dependencies": { "axios": "^1.6.0", - "moment": "^2.29.1", "solution-utils": "file:../solution-utils", "xml-js": "^1.6.11", "aws-sdk": "^2.1001.0" - }, "devDependencies": { "axios-mock-adapter": "1.19.0", diff --git a/source/solution-utils/package.json b/source/solution-utils/package.json index 0d2470a..0dda68a 100644 --- a/source/solution-utils/package.json +++ b/source/solution-utils/package.json @@ -1,6 +1,6 @@ { "name": "solution-utils", - "version": "3.2.5", + "version": "3.2.6", "description": "Utilities package for Distributed Load Testing on AWS", "license": "Apache-2.0", "author": { @@ -13,10 +13,12 @@ "test": "jest --coverage --silent" }, "dependencies": { - "nanoid": "^3.1.25" + "nanoid": "^3.1.25", + "axios": "^1.6.0" }, "devDependencies": { - "jest": "29.7.0" + "jest": "29.7.0", + "axios-mock-adapter": "1.19.0" }, "engines": { "node": "^18.x" diff --git a/source/solution-utils/test/index.spec.js b/source/solution-utils/test/index.spec.js index bfda4d7..5f0aa94 100644 --- a/source/solution-utils/test/index.spec.js +++ b/source/solution-utils/test/index.spec.js @@ -2,6 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 const utils = require("../utils"); +const axios = require("axios"); +const MockAdapter = require("axios-mock-adapter"); describe("#GET OPTIONS:: ", () => { const OLD_ENV = process.env; @@ -73,3 +75,76 @@ describe("#GENERATE UNIQUE ID:: ", () => { expect(uniqueId).toHaveLength(20); }); }); + +describe("#SEND METRICS", () => { + beforeEach(() => { + process.env.UUID = "MyUUID"; + process.env.SOLUTION_ID = "MySolutionID"; + process.env.VERSION = "MyVersion"; + process.env.METRIC_URL = "MyEndpoint"; + }); + + afterEach(() => { + delete process.env.UUID; + delete process.env.SOLUTION_ID; + delete process.env.VERSION; + delete process.env.METRIC_URL; + }); + + it("should return 200 status code on success", async () => { + // Arrange + let mock = new MockAdapter(axios); + mock.onPost().reply(200, {}); + const metricData = { + Type: "TaskCompletion", + Duration: 300.0, + TestType: "simple", + TestResult: "completed", + }; + + // Act + let response = await utils.sendMetric(metricData); + + // Assert + expect(response).toEqual(200); + }); + + it("should send metric in correct format", async () => { + // Arrange + let mock = new MockAdapter(axios); + mock.onPost().reply(200, {}); + const metricData = { + Type: "TaskCompletion", + Duration: 300.0, + TestType: "jmeter", + FileType: "zip", + TestResult: "failed", + }; + const expectedMetricObject = { + Solution: process.env.SOLUTION_ID, + UUID: process.env.UUID, + Version: process.env.VERSION, + Data: metricData, + }; + + // Act + await utils.sendMetric(metricData); + + // Assert + expect(mock.history.post[0].url).toEqual(process.env.METRIC_URL); + expect(JSON.parse(mock.history.post[0].data)).toMatchObject(expectedMetricObject); + expect(typeof Date.parse(JSON.parse(mock.history.post[0].data).TimeStamp)).toEqual("number"); // epoch time + }); + + it("should not throw error, when metric send fails", async () => { + // Arrange + let mock = new MockAdapter(axios); + mock.onPost().networkError(); + + // Act + await utils.sendMetric({}); + + // Assert + expect(mock.history.post.length).toBe(1); // called once + }); +}); diff --git a/source/solution-utils/utils.js b/source/solution-utils/utils.js index 0433e96..f36818d 100644 --- a/source/solution-utils/utils.js +++ b/source/solution-utils/utils.js @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 const { customAlphabet } = require("nanoid"); +const axios = require("axios"); /** * Generates an unique ID based on the parameter length. @@ -31,7 +32,40 @@ const getOptions = (options) => { return options; }; +/** + * Sends anonymized metrics. + * @param {{ taskCount: number, testType: string, fileType: string|undefined }} - the number of containers used for the test, the test type, and the file type + */ +const sendMetric = async (metricData) => { + let data; + + try { + const metrics = { + Solution: process.env.SOLUTION_ID, + UUID: process.env.UUID, + // Date and time instant in a java.sql.Timestamp compatible format + TimeStamp: new Date().toISOString().replace("T", " ").replace("Z", ""), + Version: process.env.VERSION, + Data: metricData, + }; + const params = { + method: "post", + port: 443, + url: process.env.METRIC_URL, + headers: { + "Content-Type": "application/json", + }, + data: metrics, + }; + data = await axios(params); + return data.status; + } catch (err) { + console.error(err); + } +}; + module.exports = { generateUniqueId: generateUniqueId, getOptions: getOptions, + sendMetric: sendMetric, }; diff --git a/source/task-canceler/package.json b/source/task-canceler/package.json index b040500..0f469ba 100644 --- a/source/task-canceler/package.json +++ b/source/task-canceler/package.json @@ -1,6 +1,6 @@ { "name": "task-canceler", - "version": "3.2.5", + "version": "3.2.6", "description": "Triggered by api-services lambda function, cancels ecs tasks", "repository": { "type": "git", diff --git a/source/task-runner/index.js b/source/task-runner/index.js index 9124b4f..0e5db81 100644 --- a/source/task-runner/index.js +++ b/source/task-runner/index.js @@ -264,6 +264,7 @@ exports.handler = async (event, context) => { { name: taskImage, environment: [ + { name: "MAIN_STACK_REGION", value: process.env.MAIN_STACK_REGION }, { name: "S3_BUCKET", value: process.env.SCENARIOS_BUCKET }, { name: "TEST_ID", value: testId }, { name: "TEST_TYPE", value: testType }, diff --git a/source/task-runner/lib/index.spec.js b/source/task-runner/lib/index.spec.js index 236babf..0925a8f 100644 --- a/source/task-runner/lib/index.spec.js +++ b/source/task-runner/lib/index.spec.js @@ -46,6 +46,7 @@ process.env = { SCENARIOS_BUCKET: "mock-bucket", SOLUTION_ID: "SO0062", VERSION: "2.0.1", + MAIN_STACK_REGION: "us-west-2", }; const lambda = require("../index.js"); @@ -91,6 +92,7 @@ let mockParam = { { name: event.testTaskConfig.taskImage, environment: [ + { name: "MAIN_STACK_REGION", value: process.env.MAIN_STACK_REGION }, { name: "S3_BUCKET", value: process.env.SCENARIOS_BUCKET }, { name: "TEST_ID", value: event.testId }, { name: "TEST_TYPE", value: "simple" }, @@ -346,7 +348,7 @@ describe("#TASK RUNNER:: ", () => { expect(response).toEqual(expect.objectContaining(expectedResponse)); }); it("should return when launching leader task is successful", async () => { - mockParam.overrides.containerOverrides[0].environment[7].value = "ecscontroller.py"; + mockParam.overrides.containerOverrides[0].environment[8].value = "ecscontroller.py"; mockParam.overrides.containerOverrides[0].environment.push({ name: "IPNETWORK", value: "" }); mockParam.overrides.containerOverrides[0].environment.push({ name: "IPHOSTS", value: "" }); let taskIds = getTaskIds(5); diff --git a/source/task-runner/package.json b/source/task-runner/package.json index 0d982d4..ab34cf2 100644 --- a/source/task-runner/package.json +++ b/source/task-runner/package.json @@ -1,6 +1,6 @@ { "name": "task-runner", - "version": "3.2.5", + "version": "3.2.6", "description": "Triggered by Step Functions, runs ecs task Definitions", "repository": { "type": "git", diff --git a/source/task-status-checker/package.json b/source/task-status-checker/package.json index 114fba2..51baab7 100644 --- a/source/task-status-checker/package.json +++ b/source/task-status-checker/package.json @@ -1,6 +1,6 @@ { "name": "task-status-checker", - "version": "3.2.5", + "version": "3.2.6", "description": "checks if tasks are running or not", "repository": { "type": "git",