diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index 0fdb8c379..eae056eca 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -7,13 +7,17 @@ on: push: branches: - 'dev' + - 'release/*' + +permissions: + contents: read jobs: - testrun_package: + create_package: permissions: {} name: Package runs-on: ubuntu-22.04 - timeout-minutes: 5 + timeout-minutes: 10 steps: - name: Checkout source uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 @@ -24,4 +28,106 @@ jobs: uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0 with: name: testrun_package - path: testrun*.deb \ No newline at end of file + path: testrun*.deb + + install_package_20: + permissions: {} + needs: create_package + name: Install on Ubuntu 20.04 + runs-on: ubuntu-20.04 + timeout-minutes: 15 + steps: + - name: Checkout source + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Download package + uses: actions/download-artifact@v4 + with: + name: testrun_package + - name: Install dependencies + shell: bash {0} + run: sudo cmd/prepare + - name: Install package + shell: bash {0} + run: sudo apt install ./testrun*.deb + - name: Start testrun + shell: bash {0} + run: sudo testrun > >(tee testrun_output.log) 2>&1 & + - name: Verify testrun started + shell: bash {0} + run: | + sleep 5 + if grep -q "API waiting for requests" testrun_output.log; then + echo "Testrun started successfully." + else + echo "Testrun did not start correctly." + cat testrun_output.log + exit 1 + fi + + install_package_22: + permissions: {} + needs: create_package + name: Install on Ubuntu 22.04 + runs-on: ubuntu-22.04 + timeout-minutes: 15 + steps: + - name: Checkout source + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Download package + uses: actions/download-artifact@v4 + with: + name: testrun_package + - name: Install dependencies + shell: bash {0} + run: sudo cmd/prepare + - name: Install package + shell: bash {0} + run: sudo apt install ./testrun*.deb + - name: Start testrun + shell: bash {0} + run: sudo testrun > >(tee testrun_output.log) 2>&1 & + - name: Verify testrun started + shell: bash {0} + run: | + sleep 5 + if grep -q "API waiting for requests" testrun_output.log; then + echo "Testrun started successfully." + else + echo "Testrun did not start correctly." + cat testrun_output.log + exit 1 + fi + + install_package_24: + permissions: {} + needs: create_package + name: Install on Ubuntu 24.04 + runs-on: ubuntu-24.04 + timeout-minutes: 15 + steps: + - name: Checkout source + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Download package + uses: actions/download-artifact@v4 + with: + name: testrun_package + - name: Install dependencies + shell: bash {0} + run: sudo cmd/prepare + - name: Install package + shell: bash {0} + run: sudo apt install ./testrun*.deb + - name: Start testrun + shell: bash {0} + run: sudo testrun > >(tee testrun_output.log) 2>&1 & + - name: Verify testrun started + shell: bash {0} + run: | + sleep 5 + if grep -q "API waiting for requests" testrun_output.log; then + echo "Testrun started successfully." + else + echo "Testrun did not start correctly." + cat testrun_output.log + exit 1 + fi diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 12884c718..f0f89a631 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -7,10 +7,6 @@ on: # For Branch-Protection check. Only the default branch is supported. See # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection branch_protection_rule: - # To guarantee Maintained check is occasionally updated. See - # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained - schedule: - - cron: '20 6 * * 4' push: branches: [ "main" ] @@ -70,4 +66,4 @@ jobs: - name: "Upload to code-scanning" uses: github/codeql-action/upload-sarif@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9 with: - sarif_file: results.sarif + sarif_file: results.sarif \ No newline at end of file diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 0556a2189..b14f52c9f 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -10,7 +10,7 @@ jobs: testrun_baseline: permissions: {} name: Baseline - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 timeout-minutes: 20 steps: - name: Checkout source @@ -29,7 +29,7 @@ jobs: testrun_api: permissions: {} name: API - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 timeout-minutes: 60 steps: - name: Checkout source @@ -39,7 +39,7 @@ jobs: run: cmd/prepare - name: Install Testrun shell: bash {0} - run: TESTRUN_DIR=. cmd/install + run: cmd/install -l timeout-minutes: 30 - name: Run tests shell: bash {0} @@ -55,6 +55,59 @@ jobs: name: runtime_api_${{ github.run_id }} path: runtime.tgz + testrun_unit: + permissions: {} + name: Unit + runs-on: ubuntu-22.04 + timeout-minutes: 15 + steps: + - name: Checkout source + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Install dependencies + shell: bash {0} + run: cmd/prepare + - name: Install Testrun + shell: bash {0} + run: cmd/install -l + - name: Build Testrun + shell: bash {0} + run: cmd/build + timeout-minutes: 10 + - name: Run tests for conn module + shell: bash {0} + run: bash testing/unit/run_test_module.sh conn captures ethtool output + - name: Run tests for dns module + shell: bash {0} + run: bash testing/unit/run_test_module.sh dns captures reports output + - name: Run tests for ntp module + shell: bash {0} + run: bash testing/unit/run_test_module.sh ntp captures reports output + - name: Run tests for protocol module + shell: bash {0} + run: bash testing/unit/run_test_module.sh protocol captures output + - name: Run tests for services module + shell: bash {0} + run: bash testing/unit/run_test_module.sh services reports results output + - name: Run tests for tls module + shell: bash {0} + run: bash testing/unit/run_test_module.sh tls captures certAuth certs reports root_certs output + - name: Run tests for risk profiles + shell: bash {0} + run: bash testing/unit/run_report_test.sh testing/unit/risk_profile/risk_profile_test.py + - name: Run tests for reports + shell: bash {0} + run: bash testing/unit/run_report_test.sh testing/unit/report/report_test.py + - name: Archive HTML reports for modules + if: ${{ always() }} + run: sudo tar --exclude-vcs -czf html_reports.tgz testing/unit/report/output/ + - name: Upload HTML reports + uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0 + if: ${{ always() }} + with: + if-no-files-found: error + name: html-reports_${{ github.run_id }} + path: html_reports.tgz + pylint: permissions: {} name: Pylint @@ -76,7 +129,7 @@ jobs: - name: Install Node uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1 with: - node-version: 18.18.0 + node-version: 18.19.0 - name: Install Chromium Browser run: sudo apt install chromium-browser - name: Install dependencies @@ -85,7 +138,7 @@ jobs: - name: Run tests run: | export CHROME_BIN=/usr/bin/chromium-browser - CI=true npm run test-headless + CI=true npm run test-ci env: CI: true working-directory: ./modules/ui @@ -99,7 +152,7 @@ jobs: - name: Install Node uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1 with: - node-version: 18.18.0 + node-version: 18.19.0 - name: Install dependencies run: npm install && npm ci working-directory: ./modules/ui diff --git a/.gitignore b/.gitignore index 82b6bbf64..f1c4ea203 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ build/ # Ignore generated files from unit tests testing/unit_test/temp/ +testing/unit/conn/output/ testing/unit/dns/output/ testing/unit/nmap/output/ testing/unit/ntp/output/ @@ -15,6 +16,12 @@ testing/unit/tls/output/ testing/unit/tls/tmp/ testing/unit/report/output/ testing/unit/risk_profile/output/ +testing/unit/services/output/ + +# Ignore generated files from requirements generation +*requirements_freeze.txt +*unique_freeze.txt +*requirements_gen.txt *.deb make/DEBIAN/postinst diff --git a/README.md b/README.md index 4a04e8885..cc5f0f893 100644 --- a/README.md +++ b/README.md @@ -4,84 +4,93 @@ [![CodeQL](https://github.com/google/testrun/actions/workflows/github-code-scanning/codeql/badge.svg?branch=main)](https://github.com/google/testrun/actions/workflows/github-code-scanning/codeql) [![Testrun test suite](https://github.com/google/testrun/actions/workflows/testing.yml/badge.svg?branch=main&event=push)](https://github.com/google/testrun/actions/workflows/testing.yml) -## Introduction :wave: -Testrun automates specific test cases to verify network and security functionality in IoT devices. It is an open source tool which allows manufacturers of IP capable devices to test their devices for the purposes of Device Qualification within the BOS program. +# Introduction :wave: -## Motivation :bulb: -Without tools like Testrun, test labs and engineers may need to maintain a large and complex network coupled with dynamic configuration files and constant software updates. The major issues which can and should be solved are: - 1) The complexity of managing a testing network - 2) The time required to perform testing of network functionality - 3) The accuracy and consistency of testing network functionality +Testrun automates specific test cases to verify network and security functionality in IoT devices. It's an open-source tool that manufacturers use to test their IP-capable devices for the purpose of device qualification within Google's Building Operating System (BOS) program. -## How it works :triangular_ruler: -Testrun creates an isolated and controlled network environment on a linux machine. This removes the necessity for complex hardware, advanced knowledge and networking experience whilst enabling test engineers to validate device behaviour against Google’s Building Operating System requirements. +# Motivation :bulb: -Two modes are supported by Testrun: +Test labs and engineers often need to maintain a large and complex network coupled with dynamic configuration files and constant software updates. Testrun helps address major issues like: -
- - Automated testing - +- The complexity of managing a testing network +- The time required to perform testing of network functionality +- The accuracy and consistency of testing network functionality -Once the device has become operational (steady state), automated testing of the DUT (device under test) will begin. Containerized test modules will then execute against the device, one module at a time. Once all test modules have been executed, a report will be produced - presenting the results. -
+# How it works :triangular_ruler: -
+Testrun creates an isolated and controlled network environment on a Linux machine. This removes the necessity for complex hardware, advanced knowledge, and networking experience while enabling test engineers to validate device behavior against Google's BOS requirements. - - Lab network - +Testrun supports two modes: automated testing and lab network. -When manual testing or configuration changes are required, Testrun will provide the network and some tools to assist an engineer performing the additional testing. This reduces the need to maintain a separate but identical lab network. Testrun will take care of packet captures and logs for each network service for further debugging. +## Automated testing -
+Automated testing of the device under test (DUT) begins once the device is operational (steady state). Containerized test modules execute against the device one module at a time. Testrun produces a report with the results after all modules are executed. -## Minimum requirements :computer: -### Hardware - - PC running Ubuntu LTS 20.04, 22.04 or 24.04 (laptop or desktop) - - 2x USB ethernet adapter (One may be built in ethernet) - - Internet connection -### Software -- Docker - installation guide: [https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository](https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository) -### Device under test (DUT) - - DHCP client - The device must be able to obtain an IP address via DHCP +## Lab network -## Get started ▶️ -Once you have met the hardware and software requirements, you can get started with Testrun by following the [Get started guide](docs/get_started.md). +Testrun provides the network and assistive tools for engineers when manual testing or configuration changes are required, reducing the need to maintain a separate but identical lab network. Testrun handles packet captures and logs for each network service for further debugging. -## Roadmap :chart_with_upwards_trend: -Testrun will constantly evolve to further support end-users by automating device network behaviour against industry standards. For further information on upcoming features, check out the [Roadmap](docs/roadmap.pdf). +# Minimum requirements :computer: -## Accessibility :busts_in_silhouette: -We are proud of our tool and strive to provide an enjoyable experience for all of our users. Testrun goes through rigorous accessibility testing at each release. You can read more about [Google and Accessibility here](https://www.google.co.uk/accessibility). You are welcome to submit a new issue and provide feedback on our implementations. To find out how Testrun implements accessibility features, you can view a [short video here](docs/ui/accessibility.mp4). +## Hardware -## Issue reporting :triangular_flag_on_post: -If the application has come across a problem at any point during setup or use, please raise an issue under the [issues tab](https://github.com/google/testrun/issues). Issue templates exist for both bug reports and feature requests. If neither of these are appropriate for your issue, raise a blank issue instead. +- PC running Ubuntu LTS 20.04, 22.04, or 24.04 (laptop or desktop) +- 2x USB Ethernet adapter (one may be built-in Ethernet) +- Internet connection -## Contributing :keyboard: -The contributing requirements can be found in [CONTRIBUTING.md](CONTRIBUTING.md). In short, checkout the [Google CLA](https://cla.developers.google.com/) site to get started. +## Software -## FAQ :raising_hand: -1) I have an issue whilst installing/upgrading Testrun, what do I do? +Testrun requires Docker. Refer to the [installation guide](https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository) for more information. - Sometimes, issues may arise when installing or upgrading Testrun - this may happen due to one of many reasons due to the nature of the application. However, most of the time, it can be resolved by following a full Testrun re-install by using these commands: - - ```sudo docker system prune -a``` - - ```sudo apt install ./testrun-*.deb``` +## Device under test (DUT) -2) What device networking functionality is validated by Testrun? +The DUT must be able to obtain an IP address via DHCP. - Best practices and requirements for IoT devices are constantly changing due to technological advances and discovery of vulnerabilities. - The current expectations for IoT devices on Google deployments can be found in the [Application Security Requirements for IoT Devices](https://partner-security.withgoogle.com/docs/iot_requirements). - Testrun aims to automate as much of the Application Security Requirements as possible. +# Get started :arrow_forward: -3) What services are provided on the virtual network? +Once you meet the hardware and software requirements, follow the Testrun [Get started guide](/docs/get_started.md). Additional guidance is available in the [docs directory](/docs). - The following are network services that are containerized and accessible to the device under test though are likely to change over time: - - DHCP in failover configuration with internet connectivity - - IPv6 SLAAC - - DNS - - NTPv4 +# Roadmap :chart_with_upwards_trend: -4) Can I run Testrun on a virtual machine? +Testrun continually evolves to further support end users by automating device network behavior against industry standards. For information on upcoming features, check out the [Roadmap](/docs/roadmap.pdf). - Testrun can be virtualized if the 2x ethernet adapters are passed through to a VirtualBox VM as a USB device rather than managed network adapters. You can view the guide to working on a [virtual machine here](docs/virtual_machine.md). +# Accessibility :busts_in_silhouette: + +We're proud of our tool and strive to provide an enjoyable experience for everyone. Testrun goes through rigorous accessibility testing at each release. Download the [Testrun: Accessible features](https://github.com/google/testrun/raw/refs/heads/main/docs/ui/accessibility.mp4) video to learn more.You're welcome to [submit a new issue](https://github.com/google/testrun/issues) and provide feedback on our implementations. To learn more about Google's [Belonging initiative](https://www.google.co.uk/accessibility) and their approach to accessibility, visit their site. + +# Issue reporting :triangular_flag_on_post: + +If you encounter a problem during setup or use, raise an issue under the [Issues tab](https://github.com/google/testrun/issues). Issue templates exist for both bug reports and feature requests. If neither of these apply, raise a blank issue instead. + +# Contributing :keyboard: + +We strongly encourage contributions from the community. Review the requirements on the ["How to Contribute" page](CONTRIBUTING.md), then follow the [developer guidelines](/docs/dev/README.md). + +# FAQ :raising_hand: + +#### 1. What should I do if I have an issue while installing or upgrading Testrun? + + You can resolve most issues by reinstalling Testrun using these commands: +- `sudo docker system prune -a` +- `sudo apt install ./testrun*.deb` + +If this doesn't resolve the problem, [raise an issue](https://github.com/google/testrun/issues). + +#### 2. What device networking functionality does Testrun validate? + +Best practices and requirements for IoT devices change often due to technological advances and discovery of vulnerabilities. You can find the current expectations for IoT devices on Google deployments in the [Application Security Requirements for IoT Devices](https://partner-security.withgoogle.com/docs/iot_requirements). Testrun aims to automate as much of the Application Security Requirements as possible. + +#### 3. What services are provided on the virtual network? + +The following network services are containerized and accessible to the DUT: + +- DHCP in failover configuration with internet connectivity +- IPv6 SLAAC +- DNS +- NTPv4 + +Note that this list is likely to change over time. + +#### 4. Can I run Testrun on a virtual machine? + +Testrun can be virtualized if the 2x Ethernet adapters are passed through to a VirtualBox VM as a USB device rather than managed network adapters. Visit the [virtual machine guide](/docs/virtual_machine.md) for additional details. \ No newline at end of file diff --git a/cmd/build b/cmd/build index d15171f31..8ecccb5ef 100755 --- a/cmd/build +++ b/cmd/build @@ -36,22 +36,34 @@ fi # Builds all docker images echo Building docker images -# Build user interface -echo Building user interface -if docker build -t test-run/ui -f modules/ui/ui.Dockerfile . ; then +# Check if UI has already been built (if -l was used during install) +if [ ! -d "modules/ui/dist" ]; then + cmd/build_ui +fi + +# Build UI image +if docker build -t testrun/ui -f modules/ui/ui.Dockerfile . ; then echo Successully built the user interface else echo An error occured whilst building the user interface exit 1 fi +# Build websockets server +echo Building websockets server +if docker build -t testrun/ws -f modules/ws/ws.Dockerfile . ; then + echo Successully built the web sockets server +else + echo An error occured whilst building the websockets server + exit 1 +fi + # Build network modules echo Building network modules -mkdir -p build/network for dir in modules/network/* ; do module=$(basename $dir) echo Building network module $module... - if docker build -f modules/network/$module/$module.Dockerfile -t test-run/$module . ; then + if docker build -f modules/network/$module/$module.Dockerfile -t testrun/$module . ; then echo Successfully built container for network $module else echo An error occured whilst building container for network module $module @@ -61,11 +73,10 @@ done # Build validators echo Building network validators -mkdir -p build/devices for dir in modules/devices/* ; do module=$(basename $dir) echo Building validator module $module... - if docker build -f modules/devices/$module/$module.Dockerfile -t test-run/$module . ; then + if docker build -f modules/devices/$module/$module.Dockerfile -t testrun/$module . ; then echo Successfully built container for device module $module else echo An error occured whilst building container for device module $module @@ -75,11 +86,10 @@ done # Build test modules echo Building test modules -mkdir -p build/test for dir in modules/test/* ; do module=$(basename $dir) echo Building test module $module... - if docker build -f modules/test/$module/$module.Dockerfile -t test-run/$module-test . ; then + if docker build -f modules/test/$module/$module.Dockerfile -t testrun/$module-test . ; then echo Successfully built container for test module $module else echo An error occured whilst building container for test module $module diff --git a/cmd/build_ui b/cmd/build_ui new file mode 100755 index 000000000..afb0d8827 --- /dev/null +++ b/cmd/build_ui @@ -0,0 +1,37 @@ +#!/bin/bash -e + +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Build the UI +echo Building the ui builder + +# Build UI builder image +if docker build -t testrun/build-ui -f modules/ui/build.Dockerfile . ; then + echo Successully built the ui builder +else + echo An error occured whilst building the ui builder + exit 1 +fi + +# Check that the container is not already running +docker kill tr-ui-build 2> /dev/null || true + +echo "Building the user interface" + +# Start build container and build the ui dist +docker run --rm -v $PWD/modules/ui:/modules/ui testrun/build-ui /bin/sh -c "npm install && npm run build" + +# Kill the container (Should not be running anymore) +docker kill tr-ui-build 2> /dev/null || true diff --git a/cmd/install b/cmd/install index 53d12b324..baf0f5469 100755 --- a/cmd/install +++ b/cmd/install @@ -20,15 +20,29 @@ echo Installing application dependencies while getopts ":l" option; do case $option in l) # Install Testrun in local directory - TESTRUN_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")"/.. && pwd) + TESTRUN_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")"/.. && pwd) esac done # Check if TESTRUN_DIR has been set, otherwise install in /usr/local/testrun if [[ -z "${TESTRUN_DIR}" ]]; then TESTRUN_DIR=/usr/local/testrun + + # Check that user is sudo + if [[ "$EUID" -ne 0 ]]; then + echo "Installing Testrun in the default location requires sudo. Run using sudo cmd/install" + exit 1 + fi + else TESTRUN_DIR="${TESTRUN_DIR}" + + # Check that user is in docker group + if ! (id -nGz "$USER" | grep -qzxF "docker"); then + echo User is not in docker group. Follow https://docs.docker.com/engine/install/linux-postinstall/ to finish setting up docker. + exit 1 + fi + fi echo Installing Testrun at $TESTRUN_DIR @@ -51,18 +65,16 @@ cp -n local/system.json.example local/system.json deactivate # Build docker images -sudo cmd/build +cmd/build # Create local folders -mkdir -p local/devices -mkdir -p local/root_certs -mkdir -p local/risk_profiles +mkdir -p local/{devices,root_certs,risk_profiles} # Set file permissions on local # This does not work on GitHub actions if logname ; then USER_NAME=$(logname) - sudo chown -R "$USER_NAME" local + sudo chown -R "$USER_NAME" local resources || true fi echo Finished installing Testrun diff --git a/cmd/package b/cmd/package index fc418ab05..719258a83 100755 --- a/cmd/package +++ b/cmd/package @@ -16,6 +16,12 @@ # Creates a package for Testrun +# Check that user is not root +if [[ "$EUID" == 0 ]]; then + echo "Must not run as root. Use cmd/package as regular user" + exit 1 +fi + MAKE_SRC_DIR=make MAKE_CONTROL_DIR=make/DEBIAN/control @@ -25,10 +31,10 @@ version=$(grep -R "Version: " $MAKE_CONTROL_DIR | awk '{print $2}') # Replace invalid characters version="${version//./_}" -# Delete existing make files -rm -rf $MAKE_SRC_DIR/usr +echo Building package for testrun v${version} # Delete existing make files +echo Cleaning up previous build files rm -rf $MAKE_SRC_DIR/usr # Copy testrun script to /bin @@ -60,6 +66,9 @@ mkdir -p $MAKE_SRC_DIR/usr/local/testrun/local/risk_profiles mkdir -p local/root_certs cp -r local/root_certs $MAKE_SRC_DIR/usr/local/testrun/local/ +# Build the UI +cmd/build_ui + # Copy framework and modules into testrun folder cp -r {framework,modules} $MAKE_SRC_DIR/usr/local/testrun diff --git a/cmd/prune b/cmd/prune index 9f471897d..4c2796460 100755 --- a/cmd/prune +++ b/cmd/prune @@ -25,17 +25,16 @@ fi # Remove docker images echo Removing docker images -docker_images=$(sudo docker images --filter=reference="test-run/*" -q) +docker_images=$(sudo docker images --filter=reference="testrun/*" -q) if [ -z "$docker_images" ]; then echo No docker images to delete else - sudo docker rmi $docker_images > /dev/null + sudo docker rmi $docker_images fi # Remove docker networks echo Removing docker networks -sudo docker network rm endev0 > /dev/null -# Private network not used, add cleanup -# back in if/when implemented -#sudo docker network rm tr-private-net > /dev/null \ No newline at end of file +sudo docker network rm endev0 || true + +echo Successfully pruned Testrun resources diff --git a/cmd/update_requirements b/cmd/update_requirements new file mode 100755 index 000000000..f185f87d9 --- /dev/null +++ b/cmd/update_requirements @@ -0,0 +1,115 @@ +# #!/bin/bash -e + +# # Copyright 2023 Google LLC +# # +# # Licensed under the Apache License, Version 2.0 (the "License"); +# # you may not use this file except in compliance with the License. +# # You may obtain a copy of the License at +# # +# # https://www.apache.org/licenses/LICENSE-2.0 +# # +# # Unless required by applicable law or agreed to in writing, software +# # distributed under the License is distributed on an "AS IS" BASIS, +# # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# # See the License for the specific language governing permissions and +# # limitations under the License. + +# update_requirements(){ +# modules_dir="$1" +# for dir in $modules_dir/* ; do +# echo "dir: $dir" +# module=$(basename "$dir") +# MODULE_DIR="$PWD/$modules_dir/$module/python" +# IMAGE=testrun/$module +# # Check if updating the test modules +# if [[ $modules_dir == *test* ]]; then +# # Append '-test' to the variable +# IMAGE="${IMAGE}-test" +# fi +# echo "Module dir: $MODULE_DIR" +# echo "Image: $IMAGE" +# echo Updating requirements for module $modules_dir/$module... + +# if [ -e "$MODULE_DIR/requirements.txt" ]; then +# if docker run --rm -v "$PWD/$modules_dir/$module/python/:/testrun/python/" --entrypoint /bin/bash $IMAGE -c "pip3 freeze > /testrun/python/requirements_freeze.txt" ; then +# echo Successfully built requirements file for module $modules_dir/$module + +# # Normalize line endings and remove extra spaces +# dos2unix "$MODULE_DIR/requirements.txt" "$MODULE_DIR/requirements_freeze.txt" +# sed -i 's/^[ \t]*//;s/[ \t]*$//' "$MODULE_DIR/requirements.txt" "$MODULE_DIR/requirements_freeze.txt" + +# # Temporary file to store unique packages +# > "$MODULE_DIR/unique_freeze.txt" + +# # Find unique packages in requirements_freeze.txt that are not in requirements.txt +# while IFS= read -r freeze_line; do +# # Extract the package name from freeze_line +# freeze_package=$(echo "$freeze_line" | cut -d'=' -f1 | xargs) + +# echo "Frozen package: $freeze_package" +# # Search for the package name in requirements.txt, ignoring case and whitespace +# if ! grep -iq "^${freeze_package}$" "$MODULE_DIR/requirements.txt"; then +# echo "$freeze_line" >> "$MODULE_DIR/unique_freeze.txt" +# fi +# done < "$MODULE_DIR/requirements_freeze.txt" + +# # Temporary file to store generated requirements +# > "$MODULE_DIR/requirements_gen.txt" + +# # Add the downstream packages at the top of requirements_gen.txt +# # so we pull in package dependencies before the defined dependency +# # to prevent auto-upgrades in the package pipeline +# echo "# Dependencies to user defined packages" > "$MODULE_DIR/requirements_gen.txt" +# echo -e "# Package dependencies should always be defined before the user defined" >> "$MODULE_DIR/requirements_gen.txt" +# echo -e "# packages to prevent auto-upgrades of stable dependencies" >> "$MODULE_DIR/requirements_gen.txt" +# cat "$MODULE_DIR/unique_freeze.txt" >> "$MODULE_DIR/requirements_gen.txt" + +# # Create the requirements_gen.txt file +# echo -e "\n# User defined packages" >> "$MODULE_DIR/requirements_gen.txt" + +# # Loop through each package in requirements.txt +# while IFS= read -r package; do +# # Trim leading and trailing whitespace from package +# package=$(echo "$package" | xargs) +# echo "Package: $package" + +# # Extract the base package name (without version or any comparison operator) +# base_package=$(echo "$package" | sed -E 's/[<>=!].*//') +# echo "Base package: $base_package" + +# # Check if the base package is a comment and non-empty +# if [[ -n "$base_package" && "$base_package" != \#* ]]; then +# # Perform the grep only if it is not a comment +# versioned_package=$(grep -i "^${base_package}==" "$MODULE_DIR/requirements_freeze.txt") +# else +# # Set versioned_package to empty if it's a comment +# versioned_package="" +# fi + +# # Debug output: Print the result of matching +# echo "Versioned Package: '$versioned_package'" + +# if [ -n "$versioned_package" ]; then +# # If the package with the version is found, add it to requirements_gen.txt +# echo "$versioned_package" >> "$MODULE_DIR/requirements_gen.txt" +# else +# # If not found, just add the package as is from requirements.txt +# echo "$package" >> "$MODULE_DIR/requirements_gen.txt" +# fi +# done < "$MODULE_DIR/requirements.txt" +# echo "Module done" + +# else +# echo An error occurred while building requirements file for network module $module +# exit 1 +# fi +# else +# echo No requirements.txt file defined for this module +# fi +# done +# } +# echo Updating python requirements in network modules +# update_requirements modules/network + +# echo Updating python requirements in test modules +# update_requirements modules/test diff --git a/docs/README.md b/docs/README.md index 96eb32223..efea0d413 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,18 +1,19 @@ Testrun logo +# Contents -## Contents +- [Get started](/docs/get_started.md) + - [Run on a virtual machine](/docs/virtual_machine.md) +- [Network](/docs/network/README.md) + - [Network addresses](/docs/network/addresses.md) + - [Add a new network service](/docs/network/add_new_service.md) +- [Testing](/docs/test/README.md) + - [Test modules](/docs/test/modules.md) + - [Test results](/docs/test/statuses.md) +- [Developer guidelines](/docs/dev/README.md) +- [Accessibility](/docs/ui/accessibility.md) +- [Roadmap](/docs/roadmap.pdf) - - [Get Started](get_started.md) - - [Network](network/README.md) - - [Network Overview](network/README.md) - - [How to identify network interfaces](network/identify_interfaces.md) - - [Addresses](network/addresses.md) - - [Add a new network service](network/add_new_service.md) - - [Testing](test/README.md) - - [Test modules](test/modules.md) - - [Test statuses](test/statuses.md) - - [Development](dev/README.md) - - [Running on a virtual machine](virtual_machine.md) - - [Accessibility](ui/accessibility.mp4) - - [Roadmap](roadmap.pdf) +# Something missing? + +To request additional documentation or report an issue with existing resources, visit [the Issues tab](https://github.com/google/testrun/issues/new/choose). diff --git a/docs/configure_device.md b/docs/configure_device.md deleted file mode 100644 index 1db7155be..000000000 --- a/docs/configure_device.md +++ /dev/null @@ -1,31 +0,0 @@ -Testrun logo - -## Device Configuration (Deprecated) - -The device configuration file allows you to customize the testing behavior for a specific device. This file is located at `local/devices/{Device Name}/device_config.json`. Below is an overview of how to configure the device tests. - -## Device Information - -The device information section includes the manufacturer, model, and MAC address of the device. These details help identify the specific device being tested. - -## Test Modules - -Test modules are groups of tests that can be enabled or disabled as needed. You can choose which test modules to run on your device. - -### Enabling and Disabling Test Modules - -To enable or disable a test module, modify the `enabled` field within the respective module. Setting it to `true` enables the module, while setting it to `false` disables the module. - -## Customizing the Device Configuration - -To customize the device configuration for your specific device, follow these steps: - -1. Copy the default configuration file provided in the `resources/devices/template` folder. - - Create a new folder for your device under `local/devices` directory. - - Copy the `device_config.json` file from `resources/devices/template` to the newly created device folder. - -This ensures that you have a copy of the default configuration file, which you can then modify for your specific device. - -> Note: Ensure that the device configuration file is properly formatted, and the changes made align with the intended test behavior. Incorrect settings or syntax may lead to unexpected results during testing. - -If you encounter any issues or need assistance with the device configuration, refer to the Testrun documentation or ask a question on the Issues page. diff --git a/docs/dev/README.md b/docs/dev/README.md new file mode 100644 index 000000000..076cb827c --- /dev/null +++ b/docs/dev/README.md @@ -0,0 +1,35 @@ +Testrun logo + +# Developer guidelines + +## How to contribute + +As an open source project, we encourage contributions from the community to help Testrun remain an expanding but stable product. To contribute, follow the steps below: + +1. Sign the [Google Contributor License Agreement (CLA)](https://cla.developers.google.com/). + - Whether you're an individual or contributing on behalf of your organization, you must be covered by a Google CLA. + +1. Determine the scope of your contribution. + - Keep it simple. Your contribution is more likely to be accepted if you change fewer files. + - Ensure your pull request addresses one thing, such as a bug fix, dependency issue, or new framework capability. + +1. Reach out to the core maintainers at [testrun-team@googlegroups.com](mailto:testrun-team@googlegroups.com). + - They can provide confirmation that your proposed changes meet our objectives and align with Testrun principles, making them more likely to be accepted. + +1. Fork Testrun and get developing. + - We aim to provide thorough and clear developer documentation to help you contribute successfully. + +## Code quality + +To ensure code quality, use the appropriate style guide when developing code for Testrun: + +- [Python](https://google.github.io/styleguide/pyguide.html) +- [Angular](https://google.github.io/styleguide/angularjs-google-style.html) +- [Shell](https://google.github.io/styleguide/shellguide.html) +- [HTML/CSS](https://google.github.io/styleguide/htmlcssguide.html) +- [JSON](https://google.github.io/styleguide/jsoncstyleguide.xml) +- [Markdown](https://google.github.io/styleguide/docguide/style.html) + +## Automated actions + +The current code base has zero code lint issues. To maintain this, all lint checks are enforced on pull requests to dev and main. You should ensure these lint checks pass before marking your pull requests as Ready for review. diff --git a/docs/dev/postman.json b/docs/dev/postman.json index 39e4529b3..642090dd4 100644 --- a/docs/dev/postman.json +++ b/docs/dev/postman.json @@ -1,9 +1,10 @@ { "info": { - "_postman_id": "3d270980-478e-4243-9130-ee5cab278ed5", + "_postman_id": "f42dd4c6-e1a3-4ae3-a991-24cceb4d2627", "name": "Testrun", + "description": "API endpoints for the Testrun API", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", - "_exporter_id": "28312403" + "_exporter_id": "37950585" }, "item": [ { @@ -21,11 +22,12 @@ "system", "interfaces" ] - } + }, + "description": "Obtain a list of applicable and available network adapters for use in testing" }, "response": [ { - "name": "Interfaces", + "name": "Interfaces Available (200)", "originalRequest": { "method": "GET", "header": [], @@ -43,10 +45,100 @@ }, "status": "OK", "code": 200, - "_postman_previewlanguage": null, - "header": null, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], "cookie": [], "body": "{\n \"enx00e04c020fa8\": \"00:e0:4c:02:0f:a8\",\n \"enx207bd26205e9\": \"20:7b:d2:62:05:e9\"\n}" + }, + { + "name": "No interfaces (200)", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "localhost:8000/system/interfaces", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "system", + "interfaces" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{}" + } + ] + }, + { + "name": "Get Configuration", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "localhost:8000/system/config", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "system", + "config" + ] + }, + "description": "Get the current system configuration" + }, + "response": [ + { + "name": "Get Configuration (200)", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "localhost:8000/system/config", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "system", + "config" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"network\": {\n \"device_intf\": \"enx207bd2620617\",\n \"internet_intf\": \"enx207bd26205e9\"\n },\n \"log_level\": \"INFO\",\n \"startup_timeout\": 60,\n \"monitor_period\": 60,\n \"runtime\": 120\n}" } ] }, @@ -57,7 +149,7 @@ "header": [], "body": { "mode": "raw", - "raw": "{\n \"network\": {\n \"device_intf\": \"enx207bd2620617\",\n \"internet_intf\": \"enx207bd26205e9\"\n },\n \"log_level\": \"DEBUG\",\n \"startup_timeout\": 60,\n \"monitor_period\": 20,\n \"runtime\": 120\n}", + "raw": "{\n \"network\": {\n \"device_intf\": \"enx207bd2620617\",\n \"internet_intf\": \"enx207bd26205e9\"\n },\n \"log_level\": \"DEBUG\",\n \"startup_timeout\": 60,\n \"monitor_period\": 20,\n \"runtime\": 120,\n \"org_name\": \"Google\"\n}", "options": { "raw": { "language": "json" @@ -74,17 +166,18 @@ "system", "config" ] - } + }, + "description": "Update the current system configuration" }, "response": [ { - "name": "Configuration Set", + "name": "Configuration Set (200)", "originalRequest": { "method": "POST", "header": [], "body": { "mode": "raw", - "raw": "{\n \"network\": {\n \"device_intf\": \"enx00e04c020fa8\",\n \"internet_intf\": \"enx207bd26205e9\"\n },\n \"log_level\": \"DEBUG\",\n \"runtime\": 120,\n \"startup_timeout\": 60,\n \"monitor_period\": 60 \n}", + "raw": "{\n \"network\": {\n \"device_intf\": \"enx00e04c020fa8\",\n \"internet_intf\": \"enx207bd26205e9\"\n },\n \"log_level\": \"DEBUG\",\n \"runtime\": 120, // Optional\n \"startup_timeout\": 60, // Optional\n \"monitor_period\": 60 // Optional\n}", "options": { "raw": { "language": "json" @@ -105,13 +198,20 @@ }, "status": "OK", "code": 200, - "_postman_previewlanguage": null, - "header": null, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], "cookie": [], - "body": "{\n \"network\": {\n \"device_intf\": \"enx00e04c020fa8\",\n \"internet_intf\": \"enx207bd26205e9\"\n },\n \"log_level\": \"DEBUG\",\n \"runtime\": 120,\n \"startup_timeout\": 60,\n \"monitor_period\": 60 \n}" + "body": "{\n \"network\": {\n \"device_intf\": \"enx00e04c020fa8\",\n \"internet_intf\": \"enx207bd26205e9\"\n },\n \"log_level\": \"DEBUG\",\n \"runtime\": 120,\n \"startup_timeout\": 60,\n \"monitor_period\": 60\n}" }, { - "name": "Invalid JSON", + "name": "Invalid JSON (400)", "originalRequest": { "method": "POST", "header": [], @@ -138,8 +238,15 @@ }, "status": "Bad Request", "code": 400, - "_postman_previewlanguage": null, - "header": null, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], "cookie": [], "body": "{\n \"error\": \"Invalid JSON received\"\n}" } @@ -152,7 +259,7 @@ "header": [], "body": { "mode": "raw", - "raw": "{\n \"device\": {\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"firmware\": \"1.2.2\"\n }\n}", + "raw": "{\n \"device\": {\n \"mac_addr\": \"aa:bb:cc:dd:ee:ff\",\n \"firmware\": \"1.2.2\",\n \"test_modules\": {\n \"protocol\": {\n \"enabled\": true\n },\n \"services\": {\n \"enabled\": false\n },\n \"ntp\": {\n \"enabled\": true\n },\n \"tls\": {\n \"enabled\": false\n },\n \"connection\": {\n \"enabled\": true\n },\n \"dns\": {\n \"enabled\": true\n }\n }\n }\n}", "options": { "raw": { "language": "json" @@ -169,11 +276,52 @@ "system", "start" ] - } + }, + "description": "Start Testrun against a target device" }, "response": [ { - "name": "Invalid request", + "name": "Starting (200)", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"device\": {\n \"mac_addr\": \"aa:bb:cc:dd:ee:ff\",\n \"firmware\": \"1.2.2\",\n \"test_modules\": {\n \"protocol\": {\n \"enabled\": true\n },\n \"services\": {\n \"enabled\": false\n },\n \"ntp\": {\n \"enabled\": true\n },\n \"tls\": {\n \"enabled\": false\n },\n \"connection\": {\n \"enabled\": true\n },\n \"dns\": {\n \"enabled\": true\n }\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:8000/system/start", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "system", + "start" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"status\": \"Waiting for Device\",\n \"device\": {\n \"mac_addr\": \"aa:bb:cc:dd:ee:ff\",\n \"manufacturer\": \"Teltonika\",\n \"model\": \"TRB140\",\n \"firmware\": \"1.2.2\"\n },\n \"started\": \"2023-07-18T11:14:42.917670\",\n \"finished\": null, \n \"report\": null,\n \"tests\": {\n \"total\": 0,\n \"results\": []\n }\n}" + }, + { + "name": "Invalid Request (400)", "originalRequest": { "method": "POST", "header": [], @@ -200,19 +348,26 @@ }, "status": "Bad Request", "code": 400, - "_postman_previewlanguage": null, - "header": null, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], "cookie": [], "body": "{\n \"error\": \"Invalid request received\"\n}" }, { - "name": "Starting", + "name": "Invalid JSON (400)", "originalRequest": { "method": "POST", "header": [], "body": { "mode": "raw", - "raw": "{\n \"device\": {\n \"mac_addr\": \"aa:bb:cc:dd:ee:ff\",\n \"firmware\": \"1.2.2\"\n }\n}", + "raw": "{\n \"device\": {\n \"mac_addr\": \"aa:bb:cc:dd:ee:ff,\n \"firmware\": \"1.2.2\"\n }\n}", "options": { "raw": { "language": "json" @@ -231,15 +386,22 @@ ] } }, - "status": "OK", - "code": 200, - "_postman_previewlanguage": null, - "header": null, + "status": "Bad Request", + "code": 400, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], "cookie": [], - "body": "{\n \"status\": \"Starting\",\n \"device\": {\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"manufacturer\": \"Teltonika\",\n \"model\": \"TRB 140\",\n \"test_modules\": {\n \"dns\": {\n \"enabled\": false\n },\n \"connection\": {\n \"enabled\": true\n },\n \"ntp\": {\n \"enabled\": false\n },\n \"baseline\": {\n \"enabled\": false\n },\n \"nmap\": {\n \"enabled\": false\n }\n }\n }\n \"started\": \"2023-07-18T11:14:42.917670\",\n \"finished\": null,\n \"tests\": {\n \"total\": 0,\n \"results\": []\n }\n}" + "body": "{\n \"error\": \"Invalid JSON received\"\n}" }, { - "name": "Device Not Found", + "name": "Device Not Found (404)", "originalRequest": { "method": "POST", "header": [], @@ -266,19 +428,26 @@ }, "status": "Not Found", "code": 404, - "_postman_previewlanguage": null, - "header": null, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], "cookie": [], - "body": "{\n \"error\": \"Device not found\"\n}" + "body": "{\n \"error\": \"A device with that MAC address could not be found\"\n}" }, { - "name": "Invalid JSON", + "name": "Testrun Already in Progress (409)", "originalRequest": { "method": "POST", "header": [], "body": { "mode": "raw", - "raw": "{\n \"device\": {\n \"mac_addr\": \"aa:bb:cc:dd:ee:ff,\n \"firmware\": \"1.2.2\"\n }\n}", + "raw": "{\n \"device\": {\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"firmware\": \"1.2.2\",\n \"test_modules\": {\n \"dns\": {\n \"enabled\": true\n },\n \"ntp\": {\n \"enabled\": false\n }\n }\n }\n}", "options": { "raw": { "language": "json" @@ -297,12 +466,19 @@ ] } }, - "status": "Bad Request", - "code": 400, - "_postman_previewlanguage": null, - "header": null, + "status": "Conflict", + "code": 409, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], "cookie": [], - "body": "{\n \"error\": \"Invalid JSON received\"\n}" + "body": "{\n \"error\": \"Testrun cannot be started whilst a test is running on another device\"\n}" } ] }, @@ -330,11 +506,12 @@ "system", "stop" ] - } + }, + "description": "Stop Testrun from running against a device" }, "response": [ { - "name": "Not Running", + "name": "Stopped (200)", "originalRequest": { "method": "POST", "header": [], @@ -359,15 +536,22 @@ ] } }, - "status": "Bad Request", - "code": 400, - "_postman_previewlanguage": null, - "header": null, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], "cookie": [], - "body": "{\n \"error\": \"Test Run is not running\"\n}" + "body": "{\n \"success\": \"Testrun stopped\"\n}" }, { - "name": "Stopped", + "name": "Not Running (404)", "originalRequest": { "method": "POST", "header": [], @@ -392,168 +576,151 @@ ] } }, - "status": "OK", - "code": 200, - "_postman_previewlanguage": null, - "header": null, + "status": "Not Found", + "code": 404, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], "cookie": [], - "body": "{\n \"success\": \"Test Run has stopped\"\n}" + "body": "{\n \"error\": \"Testrun is not currently running\"\n}" } ] }, { - "name": "Get Devices", + "name": "Get System Status", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, "request": { "method": "GET", "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, "url": { - "raw": "localhost:8000/devices", + "raw": "localhost:8000/system/status", "host": [ "localhost" ], "port": "8000", "path": [ - "devices" + "system", + "status" ] - } + }, + "description": "Get the current status of Testrun. Suggested that this is called every 5 seconds to capture most events" }, "response": [ { - "name": "Devices", + "name": "Monitoring (200)", "originalRequest": { "method": "GET", "header": [], "url": { - "raw": "localhost:8000/devices", + "raw": "localhost:8000/system/status", "host": [ "localhost" ], "port": "8000", "path": [ - "devices" + "system", + "status" ] } }, "status": "OK", "code": 200, - "_postman_previewlanguage": null, - "header": null, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], "cookie": [], - "body": "[\n {\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"manufacturer\": \"Teltonika\",\n \"model\": \"TRB 140\",\n \"test_modules\": {\n \"dns\": {\n \"enabled\": false\n },\n \"connection\": {\n \"enabled\": true\n },\n \"ntp\": {\n \"enabled\": false\n },\n \"baseline\": {\n \"enabled\": false\n },\n \"nmap\": {\n \"enabled\": false\n }\n }\n },\n {\n \"mac_addr\": \"aa:bb:cc:dd:ee:ff\",\n \"manufacturer\": \"Manufacturer X\",\n \"model\": \"Device X\",\n \"test_modules\": {\n \"dns\": {\n \"enabled\": true\n },\n \"connection\": {\n \"enabled\": true\n },\n \"ntp\": {\n \"enabled\": true\n },\n \"baseline\": {\n \"enabled\": false\n },\n \"nmap\": {\n \"enabled\": true\n }\n }\n }\n]" + "body": "{\n \"status\": \"Monitoring\",\n \"device\": {\n \"manufacturer\": \"Delta\",\n \"model\": \"03-DIN-CPU\",\n \"mac_addr\": \"01:02:03:04:05:06\",\n \"firmware\": \"1.2.2\"\n },\n \"started\": \"2023-06-22T09:20:00.123Z\",\n \"finished\": null,\n \"report\": null,\n \"tests\": {\n \"total\": 0,\n \"results\": []\n }\n}" }, { - "name": "No Devices", + "name": "Test in Progress (200)", "originalRequest": { "method": "GET", "header": [], "url": { - "raw": "localhost:8000/devices", + "raw": "localhost:8000/system/status", "host": [ "localhost" ], "port": "8000", "path": [ - "devices" + "system", + "status" ] } }, "status": "OK", "code": 200, - "_postman_previewlanguage": null, - "header": null, - "cookie": [], - "body": "[\n\n]" - } - ] - }, - { - "name": "Get Configuration", - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "localhost:8000/system/config", - "host": [ - "localhost" + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } ], - "port": "8000", - "path": [ - "system", - "config" - ] - } - }, - "response": [ + "cookie": [], + "body": "{\n \"status\": \"In Progress\",\n \"device\": {\n \"manufacturer\": \"Delta\",\n \"model\": \"03-DIN-CPU\",\n \"mac_addr\": \"01:02:03:04:05:06\",\n \"firmware\": \"1.2.2\"\n },\n \"started\": \"2023-06-22T09:20:00.123Z\",\n \"finished\": null,\n \"tests\": {\n \"total\": 26,\n \"results\": [\n {\n \"name\": \"dns.network.hostname_resolution\",\n \"description\": \"The device should resolve hostnames\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"dns.network.from_dhcp\",\n \"description\": \"The device should use the DNS server provided by the DHCP server\",\n \"result\": \"Non-Compliant\"\n }\n ]\n }\n}" + }, { - "name": "Configuration", + "name": "Cancelled (200)", "originalRequest": { "method": "GET", "header": [], "url": { - "raw": "localhost:8000/system/config", + "raw": "localhost:8000/system/status", "host": [ "localhost" ], "port": "8000", "path": [ "system", - "config" + "status" ] } }, "status": "OK", "code": 200, - "_postman_previewlanguage": null, - "header": null, - "cookie": [], - "body": "{\n \"network\": {\n \"device_intf\": \"enx207bd2620617\",\n \"internet_intf\": \"enx207bd26205e9\"\n },\n \"log_level\": \"INFO\",\n \"startup_timeout\": 60,\n \"monitor_period\": 60,\n \"runtime\": 120\n}" - } - ] - }, - { - "name": "Get System Status", - "protocolProfileBehavior": { - "disableBodyPruning": true - }, - "request": { - "method": "GET", - "header": [], - "body": { - "mode": "raw", - "raw": "", - "options": { - "raw": { - "language": "json" + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" } - } - }, - "url": { - "raw": "localhost:8000/system/status", - "host": [ - "localhost" ], - "port": "8000", - "path": [ - "system", - "status" - ] - } - }, - "response": [ + "cookie": [], + "body": "{\n \"status\": \"Cancelled\",\n \"device\": {\n \"manufacturer\": \"Delta\",\n \"model\": \"03-DIN-CPU\",\n \"mac_addr\": \"01:02:03:04:05:06\",\n \"firmware\": \"1.2.2\"\n },\n \"started\": \"2023-06-22T09:20:00.123Z\",\n \"finished\": \"2023-06-22T09:24:00.123Z\",\n \"report\": null,\n \"tests\": {\n \"total\": 22,\n \"results\": [\n {\n \"name\": \"dns.network.hostname_resolution\",\n \"description\": \"The device should resolve hostnames\",\n \"result\": \"Compliant\",\n \"required_result\": \"Required\"\n },\n {\n \"name\": \"dns.network.from_dhcp\",\n \"description\": \"The device should use the DNS server provided by the DHCP server\",\n \"result\": \"Feature Not Present\",\n \"required_result\": \"Roadmap\"\n }\n ]\n }\n}" + }, { - "name": "Test In Progress", + "name": "Waiting for Device (200)", "originalRequest": { "method": "GET", "header": [], - "body": { - "mode": "raw", - "raw": "", - "options": { - "raw": { - "language": "json" - } - } - }, "url": { "raw": "localhost:8000/system/status", "host": [ @@ -568,25 +735,23 @@ }, "status": "OK", "code": 200, - "_postman_previewlanguage": null, - "header": null, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], "cookie": [], - "body": "{\n \"status\": \"In Progress\",\n \"device\": {\n \"manufacturer\": \"Delta\",\n \"model\": \"03-DIN-CPU\",\n \"mac_addr\": \"01:02:03:04:05:06\",\n \"firmware\": \"1.2.2\"\n },\n \"started\": \"2023-06-22T09:20:00.123Z\",\n \"finished\": null,\n \"tests\": {\n \"total\": 26,\n \"results\": [\n {\n \"name\": \"dns.network.hostname_resolution\",\n \"description\": \"The device should resolve hostnames\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"dns.network.from_dhcp\",\n \"description\": \"The device should use the DNS server provided by the DHCP server\",\n \"result\": \"Non-Compliant\"\n } \n ]\n }\n}" + "body": "{\n \"status\": \"Waiting for Device\",\n \"device\": {\n \"manufacturer\": \"Delta\",\n \"model\": \"03-DIN-CPU\",\n \"mac_addr\": \"01:02:03:04:05:06\",\n \"firmware\": \"1.2.2\"\n },\n \"started\": \"2023-06-22T09:20:00.123Z\",\n \"finished\": null,\n \"report\": null,\n \"tests\": {\n \"total\": 0,\n \"results\": []\n }\n}" }, { - "name": "Not Running", + "name": "Non-Compliant (200)", "originalRequest": { "method": "GET", "header": [], - "body": { - "mode": "raw", - "raw": "", - "options": { - "raw": { - "language": "json" - } - } - }, "url": { "raw": "localhost:8000/system/status", "host": [ @@ -601,83 +766,148 @@ }, "status": "OK", "code": 200, - "_postman_previewlanguage": null, - "header": null, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"status\": \"Non-Compliant\",\n \"device\": {\n \"manufacturer\": \"Delta\",\n \"model\": \"03-DIN-CPU\",\n \"mac_addr\": \"01:02:03:04:05:06\",\n \"firmware\": \"1.2.2\"\n },\n \"started\": \"2023-06-22T09:20:00.123Z\",\n \"finished\": \"2023-06-22T09:26:00.123Z\",\n \"report\": \"https://api.testrun.io/report.pdf\",\n \"tests\": {\n \"total\": 26,\n \"results\": [\n {\n \"name\": \"dns.network.hostname_resolution\",\n \"description\": \"The device should resolve hostnames\",\n \"result\": \"Non-Compliant\",\n \"required_result\": \"Compliant\"\n \"recommendations\": [\n \"An example of a step to resolve\",\n \"Disable any running NTP server\"\n ]\n },\n {\n \"name\": \"dns.network.from_dhcp\",\n \"description\": \"The device should use the DNS server provided by the DHCP server\",\n \"result\": \"Compliant\",\n \"required_result\": \"Roadmap\"\n },\n {\n \"name\": \"security.services.ftp\",\n \"description\": \"FTP server should not be available\",\n \"result\": \"Compliant\",\n \"required_result\": \"Required\"\n }\n ]\n }\n}" + }, + { + "name": "Compliant (200)", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "localhost:8000/system/status", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "system", + "status" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"status\": \"Compliant\",\n \"device\": {\n \"manufacturer\": \"Delta\",\n \"model\": \"03-DIN-CPU\",\n \"mac_addr\": \"01:02:03:04:05:06\",\n \"firmware\": \"1.2.2\"\n },\n \"started\": \"2023-06-22T09:20:00.123Z\",\n \"finished\": \"2023-06-22T09:26:00.123Z\",\n \"report\": \"https://api.testrun.io/report.pdf\",\n \"tests\": {\n \"total\": 3,\n \"results\": [\n {\n \"name\": \"dns.network.hostname_resolution\",\n \"description\": \"The device should resolve hostnames\",\n \"result\": \"Compliant\",\n \"required_result\": \"Required\"\n },\n {\n \"name\": \"dns.network.from_dhcp\",\n \"description\": \"The device should use the DNS server provided by the DHCP server\",\n \"result\": \"Feature Not Present\",\n \"required_result\": \"Roadmap\"\n },\n {\n \"name\": \"security.services.ftp\",\n \"description\": \"FTP server should not be available\",\n \"result\": \"Compliant\",\n \"required_result\": \"Required\"\n }\n ]\n }\n}" + }, + { + "name": "Not Running - Idle (200)", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "localhost:8000/system/status", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "system", + "status" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], "cookie": [], - "body": "{\n \"status\": \"Idle\",\n \"device\": null,\n \"started\": null,\n \"finished\": null,\n \"tests\": {\n \"total\": 0,\n \"results\": [\n ]\n }\n}" + "body": "{\n \"status\": \"Idle\",\n \"device\": null,\n \"started\": null,\n \"finished\": null,\n \"tests\": {\n \"total\": 0,\n \"results\": []\n }\n}" } ] }, { - "name": "Get Test Reports", - "protocolProfileBehavior": { - "disableBodyPruning": true - }, + "name": "Shutdown Testrun", "request": { - "method": "GET", + "method": "POST", "header": [], - "body": { - "mode": "raw", - "raw": "", - "options": { - "raw": { - "language": "json" - } - } - }, "url": { - "raw": "localhost:8000/reports", + "raw": "http://localhost:8000/system/shutdown", + "protocol": "http", "host": [ "localhost" ], "port": "8000", "path": [ - "reports" + "system", + "shutdown" ] - } + }, + "description": "Stop the Testrun framework" }, "response": [ { - "name": "Test In Progress", + "name": "Shutdown Testrun (200)", "originalRequest": { - "method": "GET", + "method": "POST", "header": [], - "body": { - "mode": "raw", - "raw": "", - "options": { - "raw": { - "language": "json" - } - } - }, "url": { - "raw": "localhost:8000/system/status", + "raw": "http://localhost:8000/system/shutdown", + "protocol": "http", "host": [ "localhost" ], "port": "8000", "path": [ "system", - "status" + "shutdown" ] } }, "status": "OK", "code": 200, - "_postman_previewlanguage": null, - "header": null, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], "cookie": [], - "body": "{\n \"status\": \"In Progress\",\n \"device\": {\n \"manufacturer\": \"Delta\",\n \"model\": \"03-DIN-CPU\",\n \"mac_addr\": \"01:02:03:04:05:06\",\n \"firmware\": \"1.2.2\"\n },\n \"started\": \"2023-06-22T09:20:00.123Z\",\n \"finished\": null,\n \"tests\": {\n \"total\": 26,\n \"results\": [\n {\n \"name\": \"dns.network.hostname_resolution\",\n \"description\": \"The device should resolve hostnames\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"dns.network.from_dhcp\",\n \"description\": \"The device should use the DNS server provided by the DHCP server\",\n \"result\": \"Non-Compliant\"\n } \n ]\n }\n}" + "body": "null" }, { - "name": "Not Running", + "name": "Test in Progress (400)", "originalRequest": { - "method": "GET", - "header": [], + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], "body": { "mode": "raw", - "raw": "", + "raw": "{\n \"device\": {\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"firmware\": \"1.2.2\",\n \"test_modules\": {\n \"dns\": {\n \"enabled\": true\n },\n \"ntp\": {\n \"enabled\": false\n }\n }\n }\n}", "options": { "raw": { "language": "json" @@ -685,34 +915,99 @@ } }, "url": { - "raw": "localhost:8000/system/status", + "raw": "http://localhost:8000/system/shutdown", + "protocol": "http", "host": [ "localhost" ], "port": "8000", "path": [ "system", - "status" + "shutdown" + ] + } + }, + "status": "Bad Request", + "code": 400, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"Unable to shutdown. A test is currently in progress.\"\n}" + } + ] + }, + { + "name": "Get Version", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8000/system/version", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "system", + "version" + ] + }, + "description": "Get the current Testrun version and check if a software update is available" + }, + "response": [ + { + "name": "Get Version (200)", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8000/system/version", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "system", + "version" ] } }, "status": "OK", "code": 200, - "_postman_previewlanguage": null, - "header": null, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], "cookie": [], - "body": "{\n \"status\": \"Idle\",\n \"device\": null,\n \"started\": null,\n \"finished\": null,\n \"tests\": {\n \"total\": 0,\n \"results\": [\n ]\n }\n}" + "body": "{\n \"installed_version\": \"v2.0\",\n \"update_available\": false,\n \"latest_version\": \"v2.0\",\n \"latest_version_url\": \"https://github.com/google/testrun/releases/tag/v2.0\"\n}" } ] }, { - "name": "Create Device", + "name": "Get Test Reports", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, "request": { - "method": "POST", + "method": "GET", "header": [], "body": { "mode": "raw", - "raw": "{\n \"manufacturer\": \"Delta\",\n \"mac_addr\": \"00:1e:42:35:73:c1\",\n \"model\": \"O3-DIN-CPU\",\n \"test_modules\": {\n \"connection\": {\n \"enabled\": true\n },\n \"nmap\": {\n \"enabled\": false\n },\n \"dns\": {\n \"enabled\": false\n },\n \"ntp\": {\n \"enabled\": false\n },\n \"baseline\": {\n \"enabled\": false\n },\n \"tls\": {\n \"enabled\": false\n }\n }\n}", + "raw": "", "options": { "raw": { "language": "json" @@ -720,91 +1015,107 @@ } }, "url": { - "raw": "localhost:8000/device", + "raw": "localhost:8000/reports", "host": [ "localhost" ], "port": "8000", "path": [ - "device" + "reports" ] - } + }, + "description": "Get all previous Testrun reports" }, "response": [ { - "name": "Create Device", + "name": "Get Test Reports (200)", "originalRequest": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"manufacturer\": \"Delta\",\n \"model\": \"O3-DIN-CPU\",\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"test_modules\": {\n \"dns\": {\n \"enabled\": false\n },\n \"nmap\": {\n \"enabled\": true\n },\n \"dhcp\": {\n \"enabled\": false\n }\n }\n}", - "options": { - "raw": { - "language": "json" - } + "method": "GET", + "header": [ + { + "key": "Origin", + "value": "http://localhost:8000", + "type": "text" } - }, + ], "url": { - "raw": "localhost:8000/device", + "raw": "localhost:8000/reports", "host": [ "localhost" ], "port": "8000", "path": [ - "device" + "reports" + ], + "query": [ + { + "key": "", + "value": null, + "disabled": true + } ] } }, "status": "OK", "code": 200, - "_postman_previewlanguage": null, - "header": null, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], "cookie": [], - "body": "{\n \"manufacturer\": \"Delta\",\n \"model\": \"O3-DIN-CPU\",\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"test_modules\": [\n \"dns\": {\n \"enabled\": false\n },\n \"nmap\": {\n \"enabled\": true\n },\n \"dhcp\": {\n \"enabled\": false\n }\n ]\n}" + "body": "[\n {\n \"testrun\": {\n \"version\": \"2.0\"\n },\n \"mac_addr\": \"00:1e:42:28:9e:4a\",\n \"device\": {\n \"mac_addr\": \"00:1e:42:28:9e:4a\",\n \"manufacturer\": \"Teltonika\",\n \"model\": \"TRB140\",\n \"firmware\": \"test\",\n \"test_modules\": {\n \"protocol\": {\n \"enabled\": true\n },\n \"services\": {\n \"enabled\": true\n },\n \"ntp\": {\n \"enabled\": true\n },\n \"tls\": {\n \"enabled\": true\n },\n \"connection\": {\n \"enabled\": true\n },\n \"dns\": {\n \"enabled\": true\n }\n }\n },\n \"status\": \"Non-Compliant\",\n \"started\": \"2000-01-01 00:00:00\",\n \"finished\": \"2000-01-01 00:30:00\",\n \"tests\": {\n \"total\": 40,\n \"results\": [\n {\n \"name\": \"protocol.valid_bacnet\",\n \"description\": \"BACnet device could not be discovered\",\n \"expected_behavior\": \"BACnet traffic can be seen on the network and packets are valid and not malformed\",\n \"required_result\": \"Recommended\",\n \"result\": \"Feature Not Detected\"\n },\n {\n \"name\": \"protocol.bacnet.version\",\n \"description\": \"Device did not respond to BACnet discovery\",\n \"expected_behavior\": \"The BACnet client implements an up to date version of BACnet\",\n \"required_result\": \"Recommended\",\n \"result\": \"Feature Not Detected\"\n },\n {\n \"name\": \"protocol.valid_modbus\",\n \"description\": \"Device did not respond to Modbus connection\",\n \"expected_behavior\": \"Any Modbus functionality works as expected and valid Modbus traffic can be observed\",\n \"required_result\": \"Recommended\",\n \"result\": \"Feature Not Detected\"\n },\n {\n \"name\": \"security.services.ftp\",\n \"description\": \"No FTP server found\",\n \"expected_behavior\": \"There is no FTP service running on any port\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"security.ssh.version\",\n \"description\": \"SSH server found running protocol 2.0\",\n \"expected_behavior\": \"SSH server is not running or server is SSHv2\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"security.services.telnet\",\n \"description\": \"No telnet server found\",\n \"expected_behavior\": \"There is no Telnet service running on any port\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"security.services.smtp\",\n \"description\": \"No SMTP server found\",\n \"expected_behavior\": \"There is no SMTP service running on any port\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"security.services.http\",\n \"description\": \"Found HTTP server running on port 80/tcp\",\n \"expected_behavior\": \"Device is unreachable on port 80 (or any other port) and only responds to HTTPS requests on port 443 (or any other port if HTTP is used at all)\",\n \"required_result\": \"Required\",\n \"result\": \"Non-Compliant\",\n \"recommendations\": [\n \"Disable all unsecure HTTP servers\",\n \"Setup TLS on the web server\"\n ]\n },\n {\n \"name\": \"security.services.pop\",\n \"description\": \"No POP server found\",\n \"expected_behavior\": \"There is no POP service running on any port\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"security.services.imap\",\n \"description\": \"No IMAP server found\",\n \"expected_behavior\": \"There is no IMAP service running on any port\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"security.services.snmpv3\",\n \"description\": \"No SNMP server found\",\n \"expected_behavior\": \"Device is unreachable on port 161 (or any other port) and device is unreachable on port 162 (or any other port) unless SNMP is essential in which case it is SNMPv3 is used.\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"security.services.vnc\",\n \"description\": \"No VNC server found\",\n \"expected_behavior\": \"Device cannot be accessed / connected to via VNC on any port\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"security.services.tftp\",\n \"description\": \"No TFTP server found\",\n \"expected_behavior\": \"There is no TFTP service running on any port\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"ntp.network.ntp_server\",\n \"description\": \"No NTP server found\",\n \"expected_behavior\": \"The device does not respond to NTP requests when it's IP is set as the NTP server on another device\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"connection.port_link\",\n \"description\": \"No port errors detected\",\n \"expected_behavior\": \"When the etherent cable is connected to the port, the device triggers the port to its enabled \\\"Link UP\\\" (LEDs illuminate on device and switch ports if present) state, and the switch shows no errors with the LEDs and when interrogated with a \\\"show interface\\\" command on most network switches.\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"connection.port_speed\",\n \"description\": \"Succesfully auto-negotiated speeds above 10 Mbps\",\n \"expected_behavior\": \"When the ethernet cable is connected to the port, the device autonegotiates a speed that can be checked with the \\\"show interface\\\" command on most network switches. The output of this command must also show that the \\\"configured speed\\\" is set to \\\"auto\\\".\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"connection.port_duplex\",\n \"description\": \"Succesfully auto-negotiated full duplex\",\n \"expected_behavior\": \"When the ethernet cable is connected to the port, the device autonegotiates a full-duplex connection.\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"connection.switch.arp_inspection\",\n \"description\": \"Device uses ARP\",\n \"expected_behavior\": \"Device continues to operate correctly when ARP inspection is enabled on the switch. No functionality is lost with ARP inspection enabled.\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"connection.switch.dhcp_snooping\",\n \"description\": \"Device does not act as a DHCP server\",\n \"expected_behavior\": \"Device continues to operate correctly when DHCP snooping is enabled on the switch.\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"connection.dhcp_address\",\n \"description\": \"Device responded to leased ip address\",\n \"expected_behavior\": \"The device is not setup with a static IP address. The device accepts an IP address from a DHCP server (RFC 2131) and responds succesfully to an ICMP echo (ping) request.\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"connection.mac_address\",\n \"description\": \"MAC address found: 00:1e:42:28:9e:4a\",\n \"expected_behavior\": \"N/A\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"connection.mac_oui\",\n \"description\": \"OUI Manufacturer found: Teltonika\",\n \"expected_behavior\": \"The MAC address prefix is registered in the IEEE Organizationally Unique Identifier database.\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"connection.private_address\",\n \"description\": \"All subnets are supported\",\n \"expected_behavior\": \"The device under test accepts IP addresses within all ranges specified in RFC 1918 and communicates using these addresses. The Internet Assigned Numbers Authority (IANA) has reserved the following three blocks of the IP address space for private internets. 10.0.0.0 - 10.255.255.255.255 (10/8 prefix). 172.16.0.0 - 172.31.255.255 (172.16/12 prefix). 192.168.0.0 - 192.168.255.255 (192.168/16 prefix)\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"connection.shared_address\",\n \"description\": \"All subnets are supported\",\n \"expected_behavior\": \"The device under test accepts IP addresses within the ranges specified in RFC 6598 and communicates using these addresses\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"connection.dhcp_disconnect\",\n \"description\": \"An error occured whilst running this test\",\n \"expected_behavior\": \"A client SHOULD use DHCP to reacquire or verify its IP address and network parameters whenever the local network parameters may have changed; e.g., at system boot time or after a disconnection from the local network, as the local network configuration may change without the client's or user's knowledge. If a client has knowledge ofa previous network address and is unable to contact a local DHCP server, the client may continue to use the previous network addres until the lease for that address expires. If the lease expires before the client can contact a DHCP server, the client must immediately discontinue use of the previous network address and may inform local users of the problem.\",\n \"required_result\": \"Required\",\n \"result\": \"Error\"\n },\n {\n \"name\": \"connection.single_ip\",\n \"description\": \"Device is using multiple IP addresses\",\n \"expected_behavior\": \"The device under test does not behave as a network switch and only requets one IP address. This test is to avoid that devices implement network switches that allow connecting strings of daisy chained devices to one single network port, as this would not make 802.1x port based authentication possible.\",\n \"required_result\": \"Required\",\n \"result\": \"Non-Compliant\",\n \"recommendations\": [\n \"Ensure that all ports on the device are isolated\",\n \"Ensure only one DHCP client is running\"\n ]\n },\n {\n \"name\": \"connection.target_ping\",\n \"description\": \"Device responds to ping\",\n \"expected_behavior\": \"The device under test responds to an ICMP echo (ping) request.\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"connection.ipaddr.ip_change\",\n \"description\": \"Device has accepted an IP address change\",\n \"expected_behavior\": \"If the lease expires before the client receiveds a DHCPACK, the client moves to INIT state, MUST immediately stop any other network processing and requires network initialization parameters as if the client were uninitialized. If the client then receives a DHCPACK allocating the client its previous network addres, the client SHOULD continue network processing. If the client is given a new network address, it MUST NOT continue using the previous network address and SHOULD notify the local users of the problem.\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"connection.ipaddr.dhcp_failover\",\n \"description\": \"Secondary DHCP server lease confirmed active in device\",\n \"expected_behavior\": \"\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"connection.ipv6_slaac\",\n \"description\": \"Device does not support IPv6 SLAAC\",\n \"expected_behavior\": \"The device under test complies with RFC4862 and forms a valid IPv6 SLAAC address\",\n \"required_result\": \"Required\",\n \"result\": \"Non-Compliant\",\n \"recommendations\": [\n \"Install a network manager that supports IPv6\",\n \"Disable DHCPv6\"\n ]\n },\n {\n \"name\": \"connection.ipv6_ping\",\n \"description\": \"No IPv6 SLAAC address found. Cannot ping\",\n \"expected_behavior\": \"The device responds to the ping as per RFC4443\",\n \"required_result\": \"Required\",\n \"result\": \"Non-Compliant\",\n \"recommendations\": [\n \"Enable ping response to IPv6 ICMP requests in network manager settings\",\n \"Create a firewall exception to allow ICMPv6 via LAN\"\n ]\n },\n {\n \"name\": \"security.tls.v1_2_server\",\n \"description\": \"TLS 1.2 certificate is invalid\",\n \"expected_behavior\": \"TLS 1.2 certificate is issued to the web browser client when accessed\",\n \"required_result\": \"Required if Applicable\",\n \"result\": \"Non-Compliant\",\n \"recommendations\": [\n \"Enable TLS 1.2 support in the web server configuration\",\n \"Disable TLS 1.0 and 1.1\",\n \"Sign the certificate used by the web server\"\n ]\n },\n {\n \"name\": \"security.tls.v1_2_client\",\n \"description\": \"No outbound connections were found\",\n \"expected_behavior\": \"The packet indicates a TLS connection with at least TLS 1.2 and support for ECDH and ECDSA ciphers\",\n \"required_result\": \"Required if Applicable\",\n \"result\": \"Feature Not Detected\"\n },\n {\n \"name\": \"security.tls.v1_3_server\",\n \"description\": \"TLS 1.3 certificate is invalid\",\n \"expected_behavior\": \"TLS 1.3 certificate is issued to the web browser client when accessed\",\n \"required_result\": \"Informational\",\n \"result\": \"Informational\"\n },\n {\n \"name\": \"security.tls.v1_3_client\",\n \"description\": \"No outbound connections were found\",\n \"expected_behavior\": \"The packet indicates a TLS connection with at least TLS 1.3\",\n \"required_result\": \"Informational\",\n \"result\": \"Informational\"\n },\n {\n \"name\": \"ntp.network.ntp_support\",\n \"description\": \"Device has not sent any NTP requests\",\n \"expected_behavior\": \"The device sends an NTPv4 request to the configured NTP server.\",\n \"required_result\": \"Required\",\n \"result\": \"Non-Compliant\",\n \"recommendations\": [\n \"Set the NTP version to v4 in the NTP client\",\n \"Install an NTP client that supports NTPv4\"\n ]\n },\n {\n \"name\": \"ntp.network.ntp_dhcp\",\n \"description\": \"Device has not sent any NTP requests\",\n \"expected_behavior\": \"Device can accept NTP server address, provided by the DHCP server (DHCP OFFER PACKET)\",\n \"required_result\": \"Roadmap\",\n \"result\": \"Feature Not Detected\"\n },\n {\n \"name\": \"dns.network.hostname_resolution\",\n \"description\": \"DNS traffic detected from device\",\n \"expected_behavior\": \"The device sends DNS requests.\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\"\n },\n {\n \"name\": \"dns.network.from_dhcp\",\n \"description\": \"DNS traffic detected only to DHCP provided server\",\n \"expected_behavior\": \"The device sends DNS requests to the DNS server provided by the DHCP server\",\n \"required_result\": \"Informational\",\n \"result\": \"Informational\"\n },\n {\n \"name\": \"dns.mdns\",\n \"description\": \"No MDNS traffic detected from the device\",\n \"expected_behavior\": \"Device may send MDNS requests\",\n \"required_result\": \"Informational\",\n \"result\": \"Informational\"\n }\n ]\n },\n \"report\": \"http://localhost:8000/report/Teltonika TRB140/2024-09-10T13:19:24\"\n }\n]" }, { - "name": "Invalid JSON", + "name": "No Test Reports (200)", "originalRequest": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"manufacturer\": \"Delta\",\n \"model\": \"O3-DIN-CPU\",\n \"mac_addr\": \"00:1e:42:35:73:c4\"\n \"test_modules\": {\n \"dns\": {\n \"enabled\": false\n },\n \"nmap\": {\n \"enabled\": true\n },\n \"dhcp\": {\n \"enabled\": false\n }\n }\n}", - "options": { - "raw": { - "language": "json" - } + "method": "GET", + "header": [ + { + "key": "Origin", + "value": "http://localhost:8000", + "type": "text" } - }, + ], "url": { - "raw": "localhost:8000/device", + "raw": "localhost:8000/reports", "host": [ "localhost" ], "port": "8000", "path": [ - "device" + "reports" ] } }, - "status": "Bad Request", - "code": 400, - "_postman_previewlanguage": null, - "header": null, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], "cookie": [], - "body": "{\n \"error\": \"Invalid JSON received\"\n}" + "body": "[]" } ] }, { - "name": "Edit Device", + "name": "Delete Report", "request": { - "method": "POST", + "method": "DELETE", "header": [], "body": { "mode": "raw", - "raw": "{\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"device\": {\n \"mac_addr\": \"00:1e:42:35:73:c1\",\n \"manufacturer\": \"Teltonika\",\n \"model\": \"TRB140\",\n \"test_modules\": {\n \"connection\": {\n \"enabled\": true\n },\n \"nmap\": {\n \"enabled\": false\n },\n \"dns\": {\n \"enabled\": false\n },\n \"ntp\": {\n \"enabled\": false\n },\n \"baseline\": {\n \"enabled\": false\n },\n \"tls\": {\n \"enabled\": false\n }\n }\n }\n}", + "raw": "{\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"timestamp\": \"2023-10-10 16:53:47\"\n}", "options": { "raw": { "language": "json" @@ -812,26 +1123,27 @@ } }, "url": { - "raw": "localhost:8000/device/edit", + "raw": "http://localhost:8000/report", + "protocol": "http", "host": [ "localhost" ], "port": "8000", "path": [ - "device", - "edit" + "report" ] - } + }, + "description": "Delete a previous Testrun report" }, "response": [ { - "name": "Create Device", + "name": "Report Deleted (200)", "originalRequest": { - "method": "POST", + "method": "DELETE", "header": [], "body": { "mode": "raw", - "raw": "{\n \"manufacturer\": \"Delta\",\n \"model\": \"O3-DIN-CPU\",\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"test_modules\": {\n \"dns\": {\n \"enabled\": false\n },\n \"nmap\": {\n \"enabled\": true\n },\n \"dhcp\": {\n \"enabled\": false\n }\n }\n}", + "raw": "{\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"timestamp\": \"2024-08-05 13:37:53\"\n}", "options": { "raw": { "language": "json" @@ -839,31 +1151,39 @@ } }, "url": { - "raw": "localhost:8000/device", + "raw": "http://localhost:8000/report", + "protocol": "http", "host": [ "localhost" ], "port": "8000", "path": [ - "device" + "report" ] } }, "status": "OK", "code": 200, - "_postman_previewlanguage": null, - "header": null, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], "cookie": [], - "body": "{\n \"manufacturer\": \"Delta\",\n \"model\": \"O3-DIN-CPU\",\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"test_modules\": [\n \"dns\": {\n \"enabled\": false\n },\n \"nmap\": {\n \"enabled\": true\n },\n \"dhcp\": {\n \"enabled\": false\n }\n ]\n}" + "body": "{\n \"success\": \"Deleted report\"\n}" }, { - "name": "Invalid JSON", + "name": "Invalid JSON (400)", "originalRequest": { - "method": "POST", + "method": "DELETE", "header": [], "body": { "mode": "raw", - "raw": "{\n \"manufacturer\": \"Delta\",\n \"model\": \"O3-DIN-CPU\",\n \"mac_addr\": \"00:1e:42:35:73:c4\"\n \"test_modules\": {\n \"dns\": {\n \"enabled\": false\n },\n \"nmap\": {\n \"enabled\": true\n },\n \"dhcp\": {\n \"enabled\": false\n }\n }\n}", + "raw": "{\n \n}", "options": { "raw": { "language": "json" @@ -871,60 +1191,1343 @@ } }, "url": { - "raw": "localhost:8000/device", + "raw": "http://localhost:8000/report", + "protocol": "http", "host": [ "localhost" ], "port": "8000", "path": [ - "device" + "report" ] } }, "status": "Bad Request", "code": 400, - "_postman_previewlanguage": null, - "header": null, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], "cookie": [], - "body": "{\n \"error\": \"Invalid JSON received\"\n}" + "body": "{\n \"error\": \"Invalid request received\"\n}" + }, + { + "name": "Report Not Found (404))", + "originalRequest": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"timestamp\": \"2023-10-10 16:53:47\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8000/report", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "report" + ] + } + }, + "status": "Not Found", + "code": 404, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"Report not found\"\n}" } ] }, { - "name": "Delete Report", + "name": "Get Report", "request": { - "method": "DELETE", + "method": "GET", "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"timestamp\": \"2023-10-10 16:53:47\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, "url": { - "raw": "http://localhost:8000/report", + "raw": "http://localhost:8000/report/{device_name}/{timestamp}", "protocol": "http", "host": [ "localhost" ], "port": "8000", "path": [ - "report" + "report", + "{device_name}", + "{timestamp}" ] - } + }, + "description": "Get the PDF report for a specific device and timestamp" }, - "response": [] - }, - { - "name": "Get Version", + "response": [ + { + "name": "Get Report (200)", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8000/report/Teltonika TRB140/2000-01-01T00:00:00", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "report", + "Teltonika TRB140", + "2000-01-01T00:00:00" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "text", + "header": [ + { + "key": "Content-Type", + "value": "application/pdf", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "report.pdf" + }, + { + "name": "Report Not Found (404)", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8000/report/Teltonika TRB140/2024-05-05 13:37:53", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "report", + "Teltonika TRB140", + "2024-05-05 13:37:53" + ] + } + }, + "status": "Not Found", + "code": 404, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"Report could not be found\"\n}" + } + ] + }, + { + "name": "Export", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"profile\": \"Primary profile\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8000/export/{device_name}/{timestamp}", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "export", + "{device_name}", + "{timestamp}" + ], + "query": [ + { + "key": "", + "value": null, + "disabled": true + } + ] + }, + "description": "Export the ZIP file of a specific device testing report" + }, + "response": [ + { + "name": "Export without profile (200)", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8000/export/Teltonika TRB140/2024-08-05T13:37:53", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "export", + "Teltonika TRB140", + "2024-08-05T13:37:53" + ], + "query": [ + { + "key": "", + "value": null, + "disabled": true + } + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "text", + "header": [ + { + "key": "Content-Type", + "value": "application/zip", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "report.zip" + }, + { + "name": "Export with profile (200)", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8000/export/Teltonika TRB140/2024-08-05T13:37:53", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "export", + "Teltonika TRB140", + "2024-08-05T13:37:53" + ], + "query": [ + { + "key": "", + "value": null, + "disabled": true + } + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "Text", + "header": [ + { + "key": "Content-Type", + "value": "application/zip", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "report.zip" + }, + { + "name": "Profile Not Found (404)", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"profile\": \"Non-existing profile\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8000/export/Teltonika TRB140/2024-06-13T11:31:28", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "export", + "Teltonika TRB140", + "2024-06-13T11:31:28" + ], + "query": [ + { + "key": "", + "value": null, + "disabled": true + } + ] + } + }, + "status": "Not Found", + "code": 404, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"A profile with that name could not be found\"\n}" + }, + { + "name": "Report Not Found (404)", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8000/export/Teltonika TRB140/2020-08-05T13:37:53", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "export", + "Teltonika TRB140", + "2020-08-05T13:37:53" + ], + "query": [ + { + "key": "", + "value": null, + "disabled": true + } + ] + } + }, + "status": "Not Found", + "code": 404, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"Report not found\"\n}" + } + ] + }, + { + "name": "Get Devices", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "localhost:8000/devices", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "devices" + ] + }, + "description": "Obtain the list of devices from the device repository" + }, + "response": [ + { + "name": "Get Devices (200)", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "localhost:8000/devices", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "devices" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "[\n {\n \"folder_url\": \"local/devices/Teltonika TRB140\",\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"manufacturer\": \"Teltonika\",\n \"model\": \"TRB140\",\n \"test_modules\": {\n \"protocol\": {\n \"enabled\": true\n },\n \"services\": {\n \"enabled\": false\n },\n \"connection\": {\n \"enabled\": false\n },\n \"tls\": {\n \"enabled\": true\n },\n \"ntp\": {\n \"enabled\": true\n },\n \"dns\": {\n \"enabled\": true\n }\n },\n \"ip_addr\": null,\n \"firmware\": null,\n \"device_folder\": \"Teltonika TRB140\",\n \"reports\": [\n {\n \"_device\": {\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"manufacturer\": \"Teltonika\",\n \"model\": \"TRB140\",\n \"firmware\": \"1.2.3\",\n \"test_modules\": {\n \"protocol\": {\n \"enabled\": true\n },\n \"services\": {\n \"enabled\": false\n },\n \"connection\": {\n \"enabled\": false\n },\n \"tls\": {\n \"enabled\": true\n },\n \"ntp\": {\n \"enabled\": true\n },\n \"dns\": {\n \"enabled\": true\n }\n }\n },\n \"_mac_addr\": \"00:1e:42:35:73:c4\",\n \"_status\": \"Non-Compliant\",\n \"_started\": \"2024-08-05T13:37:53\",\n \"_finished\": \"2024-08-05T13:39:35\",\n \"_total_tests\": 12,\n \"_results\": [\n {\n \"name\": \"protocol.valid_bacnet\",\n \"description\": \"BACnet device could not be discovered\",\n \"expected_behavior\": \"BACnet traffic can be seen on the network and packets are valid and not malformed\",\n \"required_result\": \"Recommended\",\n \"result\": \"Feature Not Detected\",\n \"recommendations\": []\n },\n {\n \"name\": \"protocol.bacnet.version\",\n \"description\": \"Device did not respond to BACnet discovery\",\n \"expected_behavior\": \"The BACnet client implements an up to date version of BACnet\",\n \"required_result\": \"Recommended\",\n \"result\": \"Feature Not Detected\",\n \"recommendations\": []\n },\n {\n \"name\": \"protocol.valid_modbus\",\n \"description\": \"Device did not respond to Modbus connection\",\n \"expected_behavior\": \"Any Modbus functionality works as expected and valid Modbus traffic can be observed\",\n \"required_result\": \"Recommended\",\n \"result\": \"Feature Not Detected\",\n \"recommendations\": []\n },\n {\n \"name\": \"security.tls.v1_2_server\",\n \"description\": \"TLS 1.2 certificate is invalid\",\n \"expected_behavior\": \"TLS 1.2 certificate is issued to the web browser client when accessed\",\n \"required_result\": \"Required if Applicable\",\n \"result\": \"Non-Compliant\",\n \"recommendations\": [\n \"Enable TLS 1.2 support in the web server configuration\",\n \"Disable TLS 1.0 and 1.1\",\n \"Sign the certificate used by the web server\"\n ]\n },\n {\n \"name\": \"security.tls.v1_2_client\",\n \"description\": \"TLS 1.2 client connections valid\",\n \"expected_behavior\": \"The packet indicates a TLS connection with at least TLS 1.2 and support for ECDH and ECDSA ciphers\",\n \"required_result\": \"Required if Applicable\",\n \"result\": \"Compliant\",\n \"recommendations\": []\n },\n {\n \"name\": \"security.tls.v1_3_server\",\n \"description\": \"TLS 1.3 certificate is invalid\",\n \"expected_behavior\": \"TLS 1.3 certificate is issued to the web browser client when accessed\",\n \"required_result\": \"Informational\",\n \"result\": \"Informational\",\n \"recommendations\": []\n },\n {\n \"name\": \"security.tls.v1_3_client\",\n \"description\": \"TLS 1.3 client connections valid\",\n \"expected_behavior\": \"The packet indicates a TLS connection with at least TLS 1.3\",\n \"required_result\": \"Informational\",\n \"result\": \"Informational\",\n \"recommendations\": []\n },\n {\n \"name\": \"ntp.network.ntp_support\",\n \"description\": \"Device sent NTPv3 packets. NTPv3 is not allowed\",\n \"expected_behavior\": \"The device sends an NTPv4 request to the configured NTP server.\",\n \"required_result\": \"Required\",\n \"result\": \"Non-Compliant\",\n \"recommendations\": [\n \"Set the NTP version to v4 in the NTP client\",\n \"Install an NTP client that supports NTPv4\"\n ]\n },\n {\n \"name\": \"ntp.network.ntp_dhcp\",\n \"description\": \"Device sent NTP request to non-DHCP provided server\",\n \"expected_behavior\": \"Device can accept NTP server address, provided by the DHCP server (DHCP OFFER PACKET)\",\n \"required_result\": \"Roadmap\",\n \"result\": \"Feature Not Detected\",\n \"recommendations\": []\n },\n {\n \"name\": \"dns.network.hostname_resolution\",\n \"description\": \"DNS traffic detected from device\",\n \"expected_behavior\": \"The device sends DNS requests.\",\n \"required_result\": \"Required\",\n \"result\": \"Compliant\",\n \"recommendations\": []\n },\n {\n \"name\": \"dns.network.from_dhcp\",\n \"description\": \"DNS traffic detected only to DHCP provided server\",\n \"expected_behavior\": \"The device sends DNS requests to the DNS server provided by the DHCP server\",\n \"required_result\": \"Informational\",\n \"result\": \"Informational\",\n \"recommendations\": []\n },\n {\n \"name\": \"dns.mdns\",\n \"description\": \"No MDNS traffic detected from the device\",\n \"expected_behavior\": \"Device may send MDNS requests\",\n \"required_result\": \"Informational\",\n \"result\": \"Informational\",\n \"recommendations\": []\n }\n ],\n \"_module_reports\": [],\n \"_report_url\": \"http://localhost:8000/report/Teltonika TRB140/2024-08-05T13:37:53\",\n \"_cur_page\": 0,\n \"_version\": \"1.3.1\"\n }\n ],\n \"max_device_reports\": null\n },\n {\n \"folder_url\": \"local/devices/Google First\",\n \"mac_addr\": \"00:0e:12:15:13:c8\",\n \"manufacturer\": \"Google\",\n \"model\": \"First\",\n \"test_modules\": null,\n \"ip_addr\": null,\n \"firmware\": null,\n \"device_folder\": \"Google First\",\n \"reports\": [],\n \"max_device_reports\": null\n },\n {\n \"folder_url\": \"local/devices/Delta O3-DIN-CPU\",\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"manufacturer\": \"Delta\",\n \"model\": \"O3-DIN-CPU\",\n \"test_modules\": {\n \"dns\": {\n \"enabled\": true\n },\n \"connection\": {\n \"enabled\": true\n },\n \"ntp\": {\n \"enabled\": false\n },\n \"baseline\": {\n \"enabled\": true\n },\n \"nmap\": {\n \"enabled\": false\n }\n },\n \"ip_addr\": null,\n \"firmware\": null,\n \"device_folder\": \"Delta O3-DIN-CPU\",\n \"reports\": [],\n \"max_device_reports\": null\n }\n]" + }, + { + "name": "No Devices (200)", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "localhost:8000/devices", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "devices" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "[]" + } + ] + }, + { + "name": "Create Device", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"mac_addr\": \"00:1e:42:35:73:55\",\n \"manufacturer\": \"TEST\",\n \"model\": \"Stest2\",\n \"type\": \"IoT Gateway\",\n \"technology\": \"Hardware - Access Control\",\n \"test_pack\": \"Device Qualification\",\n \"additional_info\": [\n {\n \"question\": \"What type of device is this?\",\n \"answer\": \"IoT Gateway\"\n },\n {\n \"question\": \"Please select the technology this device falls into\",\n \"answer\": \"Hardware - Access Control\"\n },\n {\n \"question\": \"Does your device process any sensitive information? \",\n \"answer\": \"Yes\"\n },\n {\n \"question\": \"Can all non-essential services be disabled on your device?\",\n \"answer\": \"Yes\"\n },\n {\n \"question\": \"Is there a second IP port on the device?\",\n \"answer\": \"Yes\"\n },\n {\n \"question\": \"Can the second IP port on your device be disabled?\",\n \"answer\": \"Yes\"\n }\n ],\n \"test_modules\": {\n \"protocol\": { \"enabled\": true },\n \"services\": { \"enabled\": true },\n \"ntp\": { \"enabled\": true },\n \"tls\": { \"enabled\": true },\n \"connection\": { \"enabled\": true },\n \"dns\": { \"enabled\": true }\n }}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:8000/device", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "device" + ] + }, + "description": "Create a new device to test" + }, + "response": [ + { + "name": "Create Device (201)", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"manufacturer\": \"Delta\",\n \"model\": \"O3-DIN-CPU\",\n \"mac_addr\": \"00:1e:42:35:73:c4\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:8000/device", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "device" + ] + } + }, + "status": "Created", + "code": 201, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"manufacturer\": \"Delta\",\n \"model\": \"O3-DIN-CPU\",\n \"test_modules\": null\n}" + }, + { + "name": "Already Exists (409)", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"manufacturer\": \"Delta\",\n \"model\": \"O3-DIN-CPU\",\n \"mac_addr\": \"00:1e:42:35:73:c4\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:8000/device", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "device" + ] + } + }, + "status": "Conflict", + "code": 409, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"A device with that MAC address already exists\"\n}" + }, + { + "name": "Invalid JSON (400)", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"manufacturer\": \"Delta\",\n \"model\": \"O3-DIN-CPU\"\n \"mac_addr\": \"00:1e:42:35:73:c4\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:8000/device", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "device" + ] + } + }, + "status": "Bad Request", + "code": 400, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"Invalid JSON received\"\n}" + }, + { + "name": "Invalid Request (400)", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:8000/device", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "device" + ] + } + }, + "status": "Bad Request", + "code": 400, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"Invalid JSON received\"\n}" + } + ] + }, + { + "name": "Delete device", + "request": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"mac_addr\": \"00:1e:42:35:73:c4\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:8000/device", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "device" + ] + }, + "description": "Create a new device to test" + }, + "response": [ + { + "name": "Delete device (200)", + "originalRequest": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"mac_addr\": \"00:1e:42:35:73:c4\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:8000/device", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "device" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"success\": \"Successfully deleted the device\"\n}" + }, + { + "name": "Not Found (404)", + "originalRequest": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"mac_addr\": \"non-existing\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:8000/device", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "device" + ] + } + }, + "status": "Not Found", + "code": 404, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"Device not found\"\n}" + }, + { + "name": "No Mac (400)", + "originalRequest": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:8000/device", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "device" + ] + } + }, + "status": "Bad Request", + "code": 400, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"Invalid request received\"\n}" + }, + { + "name": "Testrun in Progress (403)", + "originalRequest": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"mac_addr\": \"00:1e:42:35:73:c4\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:8000/device", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "device" + ] + } + }, + "status": "Forbidden", + "code": 403, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"Cannot delete this device whilst it is being tested\"\n}" + } + ] + }, + { + "name": "Edit Device", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"device\": {\n \"mac_addr\": \"00:1e:42:35:73:c1\",\n \"manufacturer\": \"Teltonika\",\n \"model\": \"TRB140\",\n \"test_modules\": {\n \"connection\": {\n \"enabled\": true\n },\n \"nmap\": {\n \"enabled\": false\n },\n \"dns\": {\n \"enabled\": false\n },\n \"ntp\": {\n \"enabled\": false\n },\n \"baseline\": {\n \"enabled\": false\n },\n \"tls\": {\n \"enabled\": false\n }\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:8000/device/edit", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "device", + "edit" + ] + }, + "description": "Update the configuration for a device" + }, + "response": [ + { + "name": "Edit Device (200)", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"device\": {\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"manufacturer\": \"Delta\",\n \"model\": \"O3-DIN-CPU\",\n \"test_modules\": {\n \"dns\": {\"enabled\": true},\n \"connection\": {\"enabled\": true},\n \"ntp\": {\"enabled\": false},\n \"baseline\": {\"enabled\": true},\n \"nmap\": {\"enabled\": false}\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:8000/device/edit", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "device", + "edit" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"manufacturer\": \"Delta\",\n \"model\": \"O3-DIN-CPU\",\n \"test_modules\": {\n \"dns\": {\n \"enabled\": true\n },\n \"connection\": {\n \"enabled\": true\n },\n \"ntp\": {\n \"enabled\": false\n },\n \"baseline\": {\n \"enabled\": true\n },\n \"nmap\": {\n \"enabled\": false\n }\n }\n}" + }, + { + "name": "Not Found (404)", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"mac_addr\": \"non-existing\",\n \"device\": {\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"manufacturer\": \"Delta\",\n \"model\": \"O3-DIN-CPU\",\n \"test_modules\": {\n \"dns\": {\"enabled\": true},\n \"connection\": {\"enabled\": true},\n \"ntp\": {\"enabled\": false},\n \"baseline\": {\"enabled\": true},\n \"nmap\": {\"enabled\": false}\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:8000/device/edit", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "device", + "edit" + ] + } + }, + "status": "Not Found", + "code": 404, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"A device with that MAC address could not be found\"\n}" + }, + { + "name": "Mac Address in Use (409)", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"device\": {\n \"mac_addr\": \"00:0e:12:15:13:c8\",\n \"manufacturer\": \"Delta\",\n \"model\": \"O3-DIN-CPU\",\n \"test_modules\": {\n \"dns\": {\"enabled\": true},\n \"connection\": {\"enabled\": true},\n \"ntp\": {\"enabled\": false},\n \"baseline\": {\"enabled\": true},\n \"nmap\": {\"enabled\": false}\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:8000/device/edit", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "device", + "edit" + ] + } + }, + "status": "Conflict", + "code": 409, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"A device with that MAC address already exists\"\n}" + }, + { + "name": "Device is Being Tested (403)", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"device\": {\n \"mac_addr\": \"00:0e:12:15:13:c8\",\n \"manufacturer\": \"Delta\",\n \"model\": \"O3-DIN-CPU\",\n \"test_modules\": {\n \"dns\": {\"enabled\": true},\n \"connection\": {\"enabled\": true},\n \"ntp\": {\"enabled\": false},\n \"baseline\": {\"enabled\": true},\n \"nmap\": {\"enabled\": false}\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:8000/device/edit", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "device", + "edit" + ] + } + }, + "status": "Forbidden", + "code": 403, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"Cannot edit this device whilst it is being tested\"\n}" + }, + { + "name": "Invalid JSON (400)", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"mac_addr\": \"00:1e:42:35:73:c4\",\n \"device\": {\n \"mac_addr\": \"00:1e:42:35:73:c4\"\n \"model\": \"O3-DIN-CPU\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:8000/device/edit", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "device", + "edit" + ] + } + }, + "status": "Bad Request", + "code": 400, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"Invalid request received\"\n}" + } + ] + }, + { + "name": "Get Devices Format", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "localhost:8000/devices/format", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "devices", + "format" + ] + }, + "description": "Obtain the list of devices from the device repository" + }, + "response": [ + { + "name": "Get Devices Format (200)", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "localhost:8000/devices/format", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "devices", + "format" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "name": "Content-Type", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "[\n {\n \"step\": 1,\n \"title\": \"Select device type & technology\",\n \"description\": \"Before your device can go through testing, tell us more about your device and its functionality. It is important that we fully understand your device before a thorough assessment can be made.\",\n \"questions\": [\n {\n \"id\": 1,\n \"question\": \"What type of device is this?\",\n \"validation\": {\n \"required\": true\n },\n \"type\": \"select\",\n \"options\": [\n {\n \"text\": \"Building Automation Gateway\",\n \"risk\": \"High\",\n \"id\": 1\n },\n {\n \"text\": \"IoT Gateway\",\n \"risk\": \"High\",\n \"id\": 2\n },\n {\n \"text\": \"Controller - AHU\",\n \"risk\": \"High\",\n \"id\": 3\n },\n {\n \"text\": \"Controller - Boiler\",\n \"risk\": \"High\",\n \"id\": 4\n },\n {\n \"text\": \"Controller - Chiller\",\n \"risk\": \"High\",\n \"id\": 5\n },\n {\n \"text\": \"Controller - FCU\",\n \"risk\": \"Limited\",\n \"id\": 6\n },\n {\n \"text\": \"Controller - Pump\",\n \"risk\": \"Limited\",\n \"id\": 7\n },\n {\n \"text\": \"Controller - CRAC\",\n \"risk\": \"High\",\n \"id\": 8\n },\n {\n \"text\": \"Controller - VAV\",\n \"risk\": \"Limited\",\n \"id\": 9\n },\n {\n \"text\": \"Controller - VRF\",\n \"risk\": \"Limited\",\n \"id\": 10\n },\n {\n \"text\": \"Controller - Multiple\",\n \"risk\": \"High\",\n \"id\": 11\n },\n {\n \"text\": \"Controller - Other\",\n \"risk\": \"High\",\n \"id\": 12\n },\n {\n \"text\": \"Controller - Lighting\",\n \"risk\": \"Limited\",\n \"id\": 13\n },\n {\n \"text\": \"Controller - Blinds/Facades\",\n \"risk\": \"High\",\n \"id\": 14\n },\n {\n \"text\": \"Controller - Lifts/Elevators\",\n \"risk\": \"High\",\n \"id\": 15\n },\n {\n \"text\": \"Controller - UPS\",\n \"risk\": \"High\",\n \"id\": 16\n },\n {\n \"text\": \"Sensor - Air Quality\",\n \"risk\": \"Limited\",\n \"id\": 17\n },\n {\n \"text\": \"Sensor - Vibration\",\n \"risk\": \"Limited\",\n \"id\": 18\n },\n {\n \"text\": \"Sensor - Humidity\",\n \"risk\": \"Limited\",\n \"id\": 19\n },\n {\n \"text\": \"Sensor - Water\",\n \"risk\": \"Limited\",\n \"id\": 20\n },\n {\n \"text\": \"Sensor - Occupancy\",\n \"risk\": \"High\",\n \"id\": 21\n },\n {\n \"text\": \"Sensor - Volume\",\n \"risk\": \"Limited\",\n \"id\": 22\n },\n {\n \"text\": \"Sensor - Weight\",\n \"risk\": \"Limited\",\n \"id\": 23\n },\n {\n \"text\": \"Sensor - Weather\",\n \"risk\": \"Limited\",\n \"id\": 24\n },\n {\n \"text\": \"Sensor - Steam\",\n \"risk\": \"High\",\n \"id\": 25\n },\n {\n \"text\": \"Sensor - Air Flow\",\n \"risk\": \"Limited\",\n \"id\": 26\n },\n {\n \"text\": \"Sensor - Lighting\",\n \"risk\": \"Limited\",\n \"id\": 27\n },\n {\n \"text\": \"Sensor - Other\",\n \"risk\": \"High\",\n \"id\": 28\n },\n {\n \"text\": \"Sensor - Air Quality\",\n \"risk\": \"Limited\",\n \"id\": 29\n },\n {\n \"text\": \"Monitoring - Fire System\",\n \"risk\": \"Limited\",\n \"id\": 30\n },\n {\n \"text\": \"Monitoring - Emergency Lighting\",\n \"risk\": \"Limited\",\n \"id\": 31\n },\n {\n \"text\": \"Monitoring - Other\",\n \"risk\": \"High\",\n \"id\": 32\n },\n {\n \"text\": \"Monitoring - UPS\",\n \"risk\": \"Limited\",\n \"id\": 33\n },\n {\n \"text\": \"Meter - Water\",\n \"risk\": \"Limited\",\n \"id\": 34\n },\n {\n \"text\": \"Meter - Gas\",\n \"risk\": \"Limited\",\n \"id\": 35\n },\n {\n \"text\": \"Meter - Electricity\",\n \"risk\": \"Limited\",\n \"id\": 36\n },\n {\n \"text\": \"Meter - Other\",\n \"risk\": \"High\",\n \"id\": 37\n },\n {\n \"text\": \"Other\",\n \"risk\": \"High\",\n \"id\": 38\n },\n {\n \"text\": \"Data - Storage\",\n \"risk\": \"High\",\n \"id\": 39\n },\n {\n \"text\": \"Data - Processing\",\n \"risk\": \"High\",\n \"id\": 40\n },\n {\n \"text\": \"Tablet\",\n \"risk\": \"High\",\n \"id\": 41\n }\n ]\n },\n {\n \"id\": 2,\n \"question\": \"Please select the technology this device falls into\",\n \"validation\": {\n \"required\": true\n },\n \"type\": \"select\",\n \"options\": [\n {\n \"id\": 1,\n \"text\": \"Hardware - Access Control\"\n },\n {\n \"id\": 2,\n \"text\": \"Hardware - Air quality\"\n },\n {\n \"id\": 3,\n \"text\": \"Hardware - Asset location tracking\"\n },\n {\n \"id\": 4,\n \"text\": \"Hardware - Audio Visual\"\n },\n {\n \"id\": 5,\n \"text\": \"Hardware - Blinds/Facade\"\n },\n {\n \"id\": 6,\n \"text\": \"Hardware - Cameras\"\n },\n {\n \"id\": 7,\n \"text\": \"Hardware - Catering\"\n },\n {\n \"id\": 8,\n \"text\": \"Hardware - Data Ingestion/Managment\"\n },\n {\n \"id\": 9,\n \"text\": \"Hardware - EV Charging\"\n },\n {\n \"id\": 10,\n \"text\": \"Hardware - Fitness\"\n },\n {\n \"id\": 11,\n \"text\": \"Hardware - HVAC\"\n },\n {\n \"id\": 12,\n \"text\": \"Hardware - Irrigation\"\n },\n {\n \"id\": 13,\n \"text\": \"Hardware - Leak Detection\"\n },\n {\n \"id\": 14,\n \"text\": \"Hardware - Lifts/Elevators\"\n },\n {\n \"id\": 15,\n \"text\": \"Hardware - Lighting\"\n },\n {\n \"id\": 16,\n \"text\": \"Hardware - Metering\"\n },\n {\n \"id\": 17,\n \"text\": \"Hardware - Monitoring\"\n },\n {\n \"id\": 18,\n \"text\": \"Hardware - Occupancy\"\n },\n {\n \"id\": 19,\n \"text\": \"Hardware - System Integration\"\n },\n {\n \"id\": 20,\n \"text\": \"Hardware - Time Management\"\n },\n {\n \"id\": 21,\n \"text\": \"Hardware - UPS\"\n },\n {\n \"id\": 22,\n \"text\": \"Hardware - Waste Management\"\n },\n {\n \"id\": 23,\n \"text\": \"Building Automation\"\n },\n {\n \"id\": 24,\n \"text\": \"Other\"\n }\n ]\n },\n {\n \"id\": 3,\n \"question\": \"Does your device process any sensitive information? \",\n \"validation\": {\n \"required\": true\n },\n \"type\": \"select\",\n \"options\": [\n {\n \"id\": 1,\n \"text\": \"Yes\",\n \"risk\": \"Limited\"\n },\n {\n \"id\": 2,\n \"text\": \"No\",\n \"risk\": \"High\"\n },\n {\n \"id\": 3,\n \"text\": \"I don't know\",\n \"risk\": \"High\"\n }\n ]\n }\n ]\n },\n {\n \"step\": 2,\n \"title\": \"Tell us more about your device\",\n \"description\": \"Before your device can go through testing, tell us more about your device and its functionality. It is important that we fully understand your device before a thorough assessment can be made.\",\n \"questions\": [\n {\n \"id\": 1,\n \"question\": \"Can all non-essential services be disabled on your device?\",\n \"validation\": {\n \"required\": true\n },\n \"type\": \"select\",\n \"options\": [\n {\n \"text\": \"Yes\",\n \"id\": 1\n },\n {\n \"text\": \"No\",\n \"id\": 2\n }\n ]\n },\n {\n \"id\": 2,\n \"question\": \"Is there a second IP port on the device?\",\n \"validation\": {\n \"required\": true\n },\n \"type\": \"select\",\n \"options\": [\n {\n \"text\": \"Yes\",\n \"id\": 1\n },\n {\n \"text\": \"No\",\n \"id\": 2\n }\n ]\n },\n {\n \"id\": 3,\n \"question\": \"Can the second IP port on your device be disabled?\",\n \"validation\": {\n \"required\": true\n },\n \"type\": \"select\",\n \"options\": [\n {\n \"text\": \"Yes\",\n \"id\": 1\n },\n {\n \"text\": \"No\",\n \"id\": 2\n },\n {\n \"text\": \"N/A\",\n \"id\": 3\n }\n ]\n }\n ]\n }\n]" + } + ] + }, + { + "name": "Get System Modules", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8000/system/modules", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "system", + "modules" + ] + }, + "description": "Get a list of all root CA certificates that have been loaded into Testrun" + }, + "response": [ + { + "name": "Get System Modules (200)", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8000/system/modules", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "system", + "modules" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "[\n \"Protocol\",\n \"Services\",\n \"Connection\",\n \"TLS\",\n \"NTP\",\n \"DNS\"\n]" + } + ] + }, + { + "name": "Get System Testpacks", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "localhost:8000/system/testpacks", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "system", + "testpacks" + ] + }, + "description": "Obtain a list of applicable and available network adapters for use in testing" + }, + "response": [ + { + "name": "System Testpacks (200)", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "localhost:8000/system/testpacks", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "system", + "testpacks" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "[\n \"Pilot Assessment\",\n \"Device Qualification\"\n]" + } + ] + }, + { + "name": "List Certificates", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8000/system/config/certs", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "system", + "config", + "certs" + ] + }, + "description": "Get a list of all root CA certificates that have been loaded into Testrun" + }, + "response": [ + { + "name": "List Certificates (200)", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8000/system/config/certs", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "system", + "config", + "certs" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "[\n {\n \"name\": \"GTS Root R1\",\n \"status\": \"Valid\",\n \"organisation\": \"Google Trust Services LLC\",\n \"expires\": \"2036-06-22T00:00:00+00:00\",\n \"filename\": \"crt.pem\"\n },\n {\n \"name\": \"WR2\",\n \"status\": \"Valid\",\n \"organisation\": \"Google Trust Services LLC\",\n \"expires\": \"2029-02-20T14:00:00+00:00\",\n \"filename\": \"WR2.pem\"\n }\n]" + }, + { + "name": "No Certificates (200)", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8000/system/config/certs", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "system", + "config", + "certs" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "name": "Content-Type", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "[]" + } + ] + }, + { + "name": "Upload Certificate", "request": { - "method": "GET", + "method": "POST", "header": [], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "file", + "type": "file", + "src": "NH1mNzqEQ/1c3.pem" + } + ] + }, "url": { - "raw": "http://localhost:8000/system/version", + "raw": "http://localhost:8000/system/config/certs", "protocol": "http", "host": [ "localhost" @@ -932,110 +2535,643 @@ "port": "8000", "path": [ "system", - "version" + "config", + "certs" ] - } + }, + "description": "Upload a new root CA certificate into Testrun" }, - "response": [] + "response": [ + { + "name": "Upload Certificate (201)", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "file", + "type": "file", + "src": "NH1mNzqEQ/1c3.pem" + } + ] + }, + "url": { + "raw": "http://localhost:8000/system/config/certs", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "system", + "config", + "certs" + ] + } + }, + "status": "Created", + "code": 201, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "name": "Content-Type", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"name\": \"GTS Root R1\",\n \"status\": \"Valid\",\n \"organisation\": \"Google Trust Services LLC\",\n \"expires\": \"2036-06-22T00:00:00+00:00\",\n \"filename\": \"crt.pem\"\n}" + }, + { + "name": "Duplicate (409)", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "file", + "type": "file", + "src": "NH1mNzqEQ/1c3.pem" + } + ] + }, + "url": { + "raw": "http://localhost:8000/system/config/certs", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "system", + "config", + "certs" + ] + } + }, + "status": "Conflict", + "code": 409, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "name": "Content-Type", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"A certificate with that file name already exists.\"\n}" + }, + { + "name": "Invalid Cert (400)", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "file", + "type": "file", + "src": "postman-cloud:///1ef5fb1a-6342-44e0-b35f-700d496e0cc5" + } + ] + }, + "url": { + "raw": "http://localhost:8000/system/config/certs", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "system", + "config", + "certs" + ] + } + }, + "status": "Bad Request", + "code": 400, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "name": "Content-Type", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"Failed to upload certificate. Is it in the correct format?\"\n}" + } + ] }, { - "name": "Get Report", + "name": "Delete Certificate", "request": { - "method": "GET", + "method": "DELETE", "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"GTS CA 1C3\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, "url": { - "raw": "http://localhost:8000/report/{device_name}/{timestamp}", + "raw": "http://localhost:8000/system/config/certs", "protocol": "http", "host": [ "localhost" ], "port": "8000", "path": [ - "report", - "{device_name}", - "{timestamp}" + "system", + "config", + "certs" ] - } + }, + "description": "Delete a root CA certificate" }, - "response": [] + "response": [ + { + "name": "Delete Certificate (200)", + "originalRequest": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"GTS Root R1\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8000/system/config/certs", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "system", + "config", + "certs" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "name": "Content-Type", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"success\": \"Successfully deleted the certificate\"\n}" + }, + { + "name": "Not Found (404)", + "originalRequest": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"non-existing name\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8000/system/config/certs", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "system", + "config", + "certs" + ] + } + }, + "status": "Not Found", + "code": 404, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "name": "Content-Type", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"A certificate with that name could not be found\"\n}" + }, + { + "name": "Invalid JSON (400)", + "originalRequest": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8000/system/config/certs", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "system", + "config", + "certs" + ] + } + }, + "status": "Bad Request", + "code": 400, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "name": "Content-Type", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"Received a bad request\"\n}" + } + ] }, { - "name": "Get Export", + "name": "Get Profile Format", "request": { "method": "GET", "header": [], "url": { - "raw": "http://localhost:8000/export/{device_name}/{timestamp}", + "raw": "http://localhost:8000/profiles/format", "protocol": "http", "host": [ "localhost" ], "port": "8000", "path": [ - "export", - "{device_name}", - "{timestamp}" + "profiles", + "format" ] - } + }, + "description": "Obtain the current format of the risk assessment questionnaire" }, - "response": [] + "response": [ + { + "name": "Get Profile Format (200)", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8000/profiles/format", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "profiles", + "format" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "" + } + ], + "cookie": [], + "body": "[\n {\n \"question\": \"How will this device be used at Google?\",\n \"description\": \"Desribe your use case. Add links to user journey diagrams and TDD if available.\",\n \"type\": \"text-long\",\n \"validation\": {\n \"max\": \"512\",\n \"required\": true\n }\n },\n {\n \"question\": \"Is this device going to be managed by Google or a third party?\",\n \"description\": \"A manufacturer or supplier is considered third party in this case\",\n \"type\": \"select\",\n \"options\": [\n \"Google\",\n \"Third Party\"\n ],\n \"validation\": {\n \"required\": true\n }\n },\n {\n \"question\": \"Will the third-party device administrator be able to grant access to authorized Google personnel upon request?\",\n \"type\": \"select\",\n \"options\": [\n \"Yes\",\n \"No\",\n \"N/A\"\n ],\n \"default\": \"N/A\",\n \"validation\": {\n \"required\": true\n }\n },\n {\n \"category\": \"Data Transmission\",\n \"question\": \"Which of the following statements are true about this device?\",\n \"description\": \"This tells us about the types of data that are transmitted from this device and how the transmission is performed from a technical standpoint.\",\n \"type\": \"select-multiple\",\n \"options\": [\n \"PII/PHI, confidential/sensitive business data, Intellectual Property and Trade Secrets, Critical Infrastructure and Identity Assets to a domain outside Alphabet's ownership\",\n \"Data transmission occurs across less-trusted networks (e.g. the internet).\",\n \"A failure in data transmission would likely have a substantial negative impact (https://www.rra.rocks/docs/standard_levels#levels-definitions)\",\n \"A confidentiality breach during transmission would have a substantial negative impact\",\n \"The device does not encrypt data during transmission\",\n \"None of the above\"\n ],\n \"validation\": {\n \"required\": true\n }\n },\n {\n \"category\": \"Data Transmission\",\n \"question\": \"Does the network protocol assure server-to-client identity verification?\",\n \"type\": \"select\",\n \"options\": [\n \"Yes\",\n \"No\",\n \"I don't know\"\n ],\n \"validation\": {\n \"required\": true\n }\n },\n {\n \"category\": \"Remote Operation\",\n \"question\": \"Click the statements that best describe the characteristics of this device.\",\n \"description\": \"This tells us about how this device is managed remotely.\",\n \"type\": \"select-multiple\",\n \"options\": [\n \"PII/PHI, or confidential business data is accessible from the device without authentication\",\n \"Unrecoverable actions (e.g. disk wipe) can be performed remotely\",\n \"Authentication is not required for remote access\",\n \"The management interface is accessible from the public internet\",\n \"Static credentials are used for administration\",\n \"None of the above\"\n ],\n \"validation\": {\n \"required\": true\n }\n },\n {\n \"category\": \"Operating Environment\",\n \"question\": \"Are any of the following statements true about this device?\",\n \"description\": \"This informs us about what other systems and processes this device is a part of.\",\n \"type\": \"select-multiple\",\n \"options\": [\n \"The device monitors an environment for active risks to human life.\",\n \"The device is used to convey people, or critical property.\",\n \"The device controls robotics in human-accessible spaces.\",\n \"The device controls physical access systems.\",\n \"The device is involved in processes required by regulations, or compliance. (ex. privacy, security, safety regulations)\",\n \"The device's failure would cause faults in other high-criticality processes.\",\n \"None of the above\"\n ],\n \"validation\": {\n \"required\": true\n }\n },\n {\n \"question\": \"Comments\",\n \"description\": \"Anything else to share?\",\n \"type\": \"text-long\",\n \"validation\": {\n \"max\": \"512\"\n }\n }\n]" + } + ] }, { - "name": "List Certificates", + "name": "List Profiles", "request": { "method": "GET", "header": [], "url": { - "raw": "http://localhost:8000/system/config/certs/list", + "raw": "http://localhost:8000/profiles", "protocol": "http", "host": [ "localhost" ], - "port": "8000", - "path": [ - "system", - "config", - "certs", - "list" - ] + "port": "8000", + "path": [ + "profiles" + ] + }, + "description": "Get a list of risk assessment profiles saved by the user" + }, + "response": [ + { + "name": "List Profiles (200)", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8000/profiles", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "profiles" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "" + } + ], + "cookie": [], + "body": "[\n {\n \"name\": \"New Profile\",\n \"version\": \"2.0\",\n \"created\": \"2024-10-08\",\n \"status\": \"Valid\",\n \"risk\": \"High\",\n \"questions\": [\n {\n \"question\": \"How will this device be used at Google?\",\n \"answer\": \"Yes\"\n },\n {\n \"question\": \"Is this device going to be managed by Google or a third party?\",\n \"answer\": \"Google\",\n \"risk\": \"Limited\"\n },\n {\n \"question\": \"Will the third-party device administrator be able to grant access to authorized Google personnel upon request?\",\n \"answer\": \"No\",\n \"risk\": \"Limited\"\n },\n {\n \"question\": \"Which of the following statements are true about this device?\",\n \"answer\": [\n 0,\n 1,\n 2\n ],\n \"risk\": \"High\"\n },\n {\n \"question\": \"Does the network protocol assure server-to-client identity verification?\",\n \"answer\": \"Yes\",\n \"risk\": \"Limited\"\n },\n {\n \"question\": \"Click the statements that best describe the characteristics of this device.\",\n \"answer\": [\n 0\n ],\n \"risk\": \"High\"\n },\n {\n \"question\": \"Are any of the following statements true about this device?\",\n \"answer\": [\n 0\n ],\n \"risk\": \"High\"\n },\n {\n \"question\": \"Comments\",\n \"answer\": \"\"\n }\n ]\n }\n]" + }, + { + "name": "No Profiles (200)", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8000/profiles", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "profiles" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "" + } + ], + "cookie": [], + "body": "[\n \n]" } - }, - "response": [] + ] }, { - "name": "Upload Certificate", + "name": "Update Profile", "request": { "method": "POST", "header": [], "body": { - "mode": "file", - "file": {} + "mode": "raw", + "raw": "{\n \"name\": \"Testing\",\n \"status\": \"Valid\",\n \"questions\": [\n {\n \"question\": \"What type of device is this?\",\n \"answer\": \"IoT Gateway\"\n },\n {\n \"question\": \"How will this device be used at Google?\",\n \"answer\": \"asdasd\"\n },\n {\n \"question\": \"Is this device going to be managed by Google or a third party?\",\n \"answer\": \"Google\"\n },\n {\n \"question\": \"Will the third-party device administrator be able to grant access to authorized Google personnel upon request?\",\n \"answer\": \"N/A\"\n },\n {\n \"question\": \"Are any of the following statements true about your device?\",\n \"answer\": [\n 3\n ]\n },\n {\n \"question\": \"Which of the following statements are true about this device?\",\n \"answer\": [\n 5\n ]\n },\n {\n \"question\": \"Does the network protocol assure server-to-client identity verification?\",\n \"answer\": \"Yes\"\n },\n {\n \"question\": \"Click the statements that best describe the characteristics of this device.\",\n \"answer\": [\n 5\n ]\n },\n {\n \"question\": \"Are any of the following statements true about this device?\",\n \"answer\": [\n 6\n ]\n },\n {\n \"question\": \"Comments\",\n \"answer\": \"\"\n }\n ]\n}", + "options": { + "raw": { + "language": "json" + } + } }, "url": { - "raw": "http://localhost:8000/system/config/certs/upload", + "raw": "http://localhost:8000/profiles", "protocol": "http", "host": [ "localhost" ], "port": "8000", "path": [ - "system", - "config", - "certs", - "upload" + "profiles" ] - } + }, + "description": "Create or update a risk assessment questionnaire response" }, - "response": [] + "response": [ + { + "name": "Update Profile (200)", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "[\n {\n \"name\": \"New Profile\",\n \"version\": \"2.0\",\n \"created\": \"2024-10-08\",\n \"status\": \"Valid\",\n \"risk\": \"High\",\n \"questions\": [\n {\n \"question\": \"How will this device be used at Google?\",\n \"answer\": \"Yes\"\n },\n {\n \"question\": \"Is this device going to be managed by Google or a third party?\",\n \"answer\": \"Google\",\n \"risk\": \"Limited\"\n },\n {\n \"question\": \"Will the third-party device administrator be able to grant access to authorized Google personnel upon request?\",\n \"answer\": \"No\",\n \"risk\": \"Limited\"\n },\n {\n \"question\": \"Which of the following statements are true about this device?\",\n \"answer\": [\n 0,\n 1,\n 2\n ],\n \"risk\": \"High\"\n },\n {\n \"question\": \"Does the network protocol assure server-to-client identity verification?\",\n \"answer\": \"Yes\",\n \"risk\": \"Limited\"\n },\n {\n \"question\": \"Click the statements that best describe the characteristics of this device.\",\n \"answer\": [\n 0\n ],\n \"risk\": \"High\"\n },\n {\n \"question\": \"Are any of the following statements true about this device?\",\n \"answer\": [\n 0\n ],\n \"risk\": \"High\"\n },\n {\n \"question\": \"Comments\",\n \"answer\": \"\"\n }\n ]\n }\n]", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8000/profiles", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "profiles" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "name": "Content-Type", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"success\": \"Successfully updated that profile\"\n}" + }, + { + "name": "Create Profile (201)", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "[\n {\n \"name\": \"New Profile\",\n \"version\": \"2.0\",\n \"created\": \"2024-10-08\",\n \"status\": \"Valid\",\n \"risk\": \"High\",\n \"questions\": [\n {\n \"question\": \"How will this device be used at Google?\",\n \"answer\": \"Yes\"\n },\n {\n \"question\": \"Is this device going to be managed by Google or a third party?\",\n \"answer\": \"Google\",\n \"risk\": \"Limited\"\n },\n {\n \"question\": \"Will the third-party device administrator be able to grant access to authorized Google personnel upon request?\",\n \"answer\": \"No\",\n \"risk\": \"Limited\"\n },\n {\n \"question\": \"Which of the following statements are true about this device?\",\n \"answer\": [\n 0,\n 1,\n 2\n ],\n \"risk\": \"High\"\n },\n {\n \"question\": \"Does the network protocol assure server-to-client identity verification?\",\n \"answer\": \"Yes\",\n \"risk\": \"Limited\"\n },\n {\n \"question\": \"Click the statements that best describe the characteristics of this device.\",\n \"answer\": [\n 0\n ],\n \"risk\": \"High\"\n },\n {\n \"question\": \"Are any of the following statements true about this device?\",\n \"answer\": [\n 0\n ],\n \"risk\": \"High\"\n },\n {\n \"question\": \"Comments\",\n \"answer\": \"\"\n }\n ]\n }\n]", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8000/profiles", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "profiles" + ] + } + }, + "status": "Created", + "code": 201, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "name": "Content-Type", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"success\": \"Successfully created a new profile\"\n}" + }, + { + "name": "Invalid JSON (400)", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8000/profiles", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "profiles" + ] + } + }, + "status": "Bad Request", + "code": 400, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "name": "Content-Type", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"Invalid request received\"\n}" + }, + { + "name": "Not Implemented (501)", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "[\n {\n \"name\": \"New Profile\",\n \"version\": \"2.0\",\n \"created\": \"2024-10-08\",\n \"status\": \"Valid\",\n \"risk\": \"High\",\n \"questions\": [\n {\n \"question\": \"How will this device be used at Google?\",\n \"answer\": \"Yes\"\n },\n {\n \"question\": \"Is this device going to be managed by Google or a third party?\",\n \"answer\": \"Google\",\n \"risk\": \"Limited\"\n },\n {\n \"question\": \"Will the third-party device administrator be able to grant access to authorized Google personnel upon request?\",\n \"answer\": \"No\",\n \"risk\": \"Limited\"\n },\n {\n \"question\": \"Which of the following statements are true about this device?\",\n \"answer\": [\n 0,\n 1,\n 2\n ],\n \"risk\": \"High\"\n },\n {\n \"question\": \"Does the network protocol assure server-to-client identity verification?\",\n \"answer\": \"Yes\",\n \"risk\": \"Limited\"\n },\n {\n \"question\": \"Click the statements that best describe the characteristics of this device.\",\n \"answer\": [\n 0\n ],\n \"risk\": \"High\"\n },\n {\n \"question\": \"Are any of the following statements true about this device?\",\n \"answer\": [\n 0\n ],\n \"risk\": \"High\"\n },\n {\n \"question\": \"Comments\",\n \"answer\": \"\"\n }\n ]\n }\n]", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8000/profiles", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "profiles" + ] + } + }, + "status": "Not Implemented", + "code": 501, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"Risk profiles are not available right now\"\n}" + } + ] }, { - "name": "Delete Certificate", + "name": "Delete Profile", "request": { "method": "DELETE", "header": [], "body": { "mode": "raw", - "raw": "{\n \"name\": \"iot.bms.google.com\"\n}", + "raw": "{\n \"name\": \"New Profile\"\n}", "options": { "raw": { "language": "json" @@ -1043,21 +3179,184 @@ } }, "url": { - "raw": "http://localhost:8000/system/config/certs/delete", + "raw": "http://localhost:8000/profiles", "protocol": "http", "host": [ "localhost" ], "port": "8000", "path": [ - "system", - "config", - "certs", - "delete" + "profiles" ] - } + }, + "description": "Delete an existing risk assessment questionaire response" }, - "response": [] + "response": [ + { + "name": "Delete Profile (200)", + "originalRequest": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"New Profile\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8000/profiles", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "profiles" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "name": "Content-Type", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"success\": \"Successfully deleted that profile\"\n}" + }, + { + "name": "Not Found (404)", + "originalRequest": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"non-existing profile\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8000/profiles", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "profiles" + ] + } + }, + "status": "Not Found", + "code": 404, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "name": "Content-Type", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"A profile with that name could not be found\"\n}" + }, + { + "name": "Internal Serves Error (500)", + "originalRequest": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"non-existing profile\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8000/profiles", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "profiles" + ] + } + }, + "status": "Internal Server Error", + "code": 500, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "name": "Content-Type", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"An error occurred whilst deleting that profile\"\n}" + }, + { + "name": "Invalid JSON (400)", + "originalRequest": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8000/profiles", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8000", + "path": [ + "profiles" + ] + } + }, + "status": "Bad Request", + "code": 400, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "name": "Content-Type", + "description": "", + "type": "text" + } + ], + "cookie": [], + "body": "{\n \"error\": \"Invalid request received\"\n}" + } + ] } ] } \ No newline at end of file diff --git a/docs/get_started.md b/docs/get_started.md index f9a2aa2f8..2b0cdf98f 100644 --- a/docs/get_started.md +++ b/docs/get_started.md @@ -1,123 +1,186 @@ Testrun logo +# Get started -## Getting Started +This page covers the following topics: -It is recommended that you run Testrun on a standalone machine running a fresh install of Ubuntu 20.04, 22.04 or 24.04 LTS (laptop or desktop). +- [Prerequisites](#prerequisites) +- [Installation](#installation) +- [Testing](#testing) +- [Additional Configuration Options](#additional-configuration-options) +- [Troubleshooting](#troubleshooting) +- [Review the report](#review-the-report) +- [Uninstall](#uninstall) -## Prerequisites +# Prerequisites -### Hardware +We recommend that you run Testrun on a stand-alone machine that has a fresh install of Ubuntu 20.04, 22.04, or 24.04 LTS (laptop or desktop). -Before starting with Testrun, ensure you have the following hardware: +## Hardware -- PC running Ubuntu LTS (laptop or desktop) -- 2x USB Ethernet adapter (one may be a built-in Ethernet port) -- Internet connection +Before you start, ensure you have the following hardware: -![Visual representation of setup](setup/visual.png) +- PC running Ubuntu LTS (laptop or desktop) +- 2x USB Ethernet adapter (one may be a built-in Ethernet port) +- Internet connection -**NOTE: Running in a virtual machine? Checkout the virtual machine documentation [here](/docs/virtual_machine.md).** +![Required hardware for Testrun](/docs/ui/getstarted--2dn8vrzsspe.png) -### Software +Note: If you're using Testrun in a virtual machine, follow the steps on the [Virtual machine page](/docs/virtual_machine.md). -Ensure the following software is installed on your Ubuntu LTS PC: -- Docker - installation guide: [https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository](https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository) -- System dependencies (These will be installed automatically when installing Testrun if not already installed): - - Python3-dev - - Python3-venv - - Openvswitch Common - - Openvswitch Switch - - Build Essential - - Net Tools - - Ethtool +## Software -### Device -Any device with an ethernet connection, and support for IPv4 DHCP can be tested. +Install Docker on your Ubuntu LTS PC using [the installation guide](https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository). The following system dependencies install automatically when you install Testrun: -However, to achieve a compliant test outcome, your device must be configured correctly and implement the required security features. These standards are outlined in the [Application Security Requirements for IoT Devices](https://partner-security.withgoogle.com/docs/iot_requirements). but further detail is available in [documentation for each test module](/docs/test/modules.md). +- Python3-dev +- Python3-venv +- Openvswitch Common +- Openvswitch Switch +- Build Essential +- Net Tools +- Ethtool -## Installation +## Device -1. Download the latest version of the Testrun installer from the [releases page](https://github.com/google/testrun/releases) +You can test any device with an Ethernet connection and support for IPv4 DHCP. However, to achieve a compliant test outcome, you must configure your device correctly and implement the required security features. The [Application Security Requirements for IoT Devices](https://partner-security.withgoogle.com/docs/iot_requirements) outlines these standards. Additional details are available on the [Test modules page](/docs/test/modules.md). -2. Open a terminal and navigate to location of the Testrun installer (most likely your downloads folder) +# Installation -3. Install the package using ``sudo apt install ./testrun*.deb`` +Follow these steps to install Testrun: - - Testrun will be installed under the /usr/local/testrun directory. - - Testing data will be available in the ``local/devices/{device}/reports`` folders +1. Download the latest version of the Testrun installer from the [Releases page](https://github.com/google/testrun/releases). +2. Open a terminal and navigate to the location of the Testrun installer (most likely your Downloads folder). +3. Install the package using `sudo apt install ./testrun*.deb` - **NOTE: Local CA certificates should be uploaded within Testrun to run TLS server testing** +Testrun installs under the `/usr/local/testrun` directory. Testing data is available in the `local/devices/{device}/reports` folders. - ![Installing Testrun in terminal window](setup/install.gif) +Note: Local CA certificates should be uploaded within Testrun to run TLS server testing. -## Start Testrun - -1. Attach network interfaces: - - Connect one USB Ethernet adapter to the internet source (e.g., router or switch) using an ethernet cable. - - Connect the other USB Ethernet adapter directly to the IoT device you want to test using an ethernet cable. - - Some things to remember: - - Both adapters should be disabled in the host system (IPv4, IPv6 and general). You can do this by going to Settings > Network - - The device under test should be powered off until prompted - - Struggling to identify the correct interfaces? See [this guide](network/identify_interfaces.md). - -2. Start Testrun. - -Start Testrun with the command `sudo testrun` - - - To run Testrun in network-only mode (without running any tests), use the `--net-only` option. - - - To run Testrun with just one interface (connected to the device), use the ``--single-intf`` option. - -## Test Your Device - -1. Once Testrun has started, open your browser to http://localhost:8080. - -2. Configure your network interfaces under the settings menu - located in the top right corner of the application. Settings can be changed at any time. - - ![](/docs/ui/settings_icon.png) - -3. Navigate to the device repository icon to add a new device for testing. +![Terminal during install](/docs/setup/install.gif) - ![](/docs/ui/device_icon.png) +# Testing -4. Click the button 'Add Device'. - -5. Enter the MAC address, manufacturer name and model number. - -6. Select the test modules you wish to enable for this device (Hint: All are required for qualification purposes) and click save. - -7. Navigate to the Testrun progress icon and click the button 'Start New Testrun'. - - ![](/docs/ui/progress_icon.png) - -8. Select the device you would like to test. - -9. Enter the version number of the firmware running on the device. - -10. Click 'Start Testrun' - - - During testing, if you would like to stop Testrun, click 'Stop' next to the test name. +## Start Testrun -11. Once the notification 'Waiting for Device' appears, power on the device under test. +Follow these steps to start Testrun: +1. Attach the network interfaces. + - Connect one USB Ethernet adapter to the internet source (e.g., router or switch) using an Ethernet cable. + - Connect the other USB Ethernet adapter directly to the IoT device you want to test using an Ethernet cable. + +Notes: + +- Disable both adapters in the host system (IPv4, IPv6, and general) by opening **Settings**, then **Network**. +- Keep the DUT powered off until prompted. + +1. Start Testrun with the command `sudo testrun` + - To run Testrun in network-only mode (without running any tests), use the `--net-only` option. + - To run Testrun with just one interface (connected to the device), use the `--single-intf` option. + + +## Test your device + +Follow these steps to test your IoT device: + +1. Open Testrun by navigating to [http://localhost:8080](http://localhost:8080/) in your browser. +2. Select the **Settings** menu in the top-right corner, then select your network interfaces. You can change the settings at any time. + ![Settings menu button](/docs/ui/getstarted--7cfvdpdnc5o.png) + +3. Select the **device repository** icon on the left panel to add a new device for testing. + ![Device repository button](/docs/ui/getstarted--q5uw26tfod.png) + +4. Select the **Add Device** button. +5. Enter the MAC address, manufacturer name, and model number. +6. Select the test modules you want to enable for this device. +Note: For qualification purposes, you must select all. +7. Select **Save**. +8. Select the Testrun progress icon, then select the **Testing** button.![Testing button](/docs/ui/getstarted--w09wecsry3.png) + +9. Select the device you want to test. +10. Enter the version number of the firmware running on the device. +11. Select **Start Testrun**. +- If you need to stop Testrun during testing, select **Stop** next to the test name. +12. Once the Waiting for Device notification appears, power on the device under test. A report appears under the Reports icon once the test sequence is complete. + ![Reports button](/docs/ui/getstarted--m4si1otdu5d.png) + +# Additional Configuration Options + +Some configuration options are available but not exposed through the user interface and requires direct access. +Modification of various configuration files is necessary to access these options. + +## Override test module timeout at the system level + +Testrun attempts to set reasonable timeouts for test modules to prevent overly long test times but sometimes +a device or series of device may require longer than these default values. These can be overridden at +the test module configuration level but is not preferred since these changes will be undone during every +version upgrade. To modify these values: + +1. Navigate to the testrun installation directory. By default, this will be at: + `/usr/local/testrun` + +2. Open the system.json file and add the following section: + `"test_modules":{}` + +3. Add the module name(s) and timeout property into this test_modules section you wish to +set the timeout property for: + ``` + "test_modules":{ + "connection":{ + "timeout": 500 + } + } + ``` + +Before timeout options: +``` +{ + "network": { + "device_intf": "ens0", + "internet_intf": "ens1" + }, + "log_level": "DEBUG", + "startup_timeout": 60, + "monitor_period": 60, + "max_device_reports": 5, + "org_name": "", + "single_intf": false + } +``` + +After timeout options: +``` +{ + "network": { + "device_intf": "ens0", + "internet_intf": "ens1" + }, + "log_level": "DEBUG", + "startup_timeout": 60, + "monitor_period": 60, + "max_device_reports": 5, + "org_name": "", + "single_intf": false, + "test_modules":{ + "connection":{ + "timeout": 500 + } + } +``` -12. On completion of the test sequence, a report will appear under the history icon. +# Troubleshooting - ![](/docs/ui/history_icon.png) +If you encounter any issues, try the following: -# Troubleshooting +- Ensure that your computer meets all hardware and software prerequisites. +- Verify that the network interfaces are connected correctly. +- Check the configuration settings. +- Refer to the [Testrun documentation](/docs). -If you encounter any issues or need assistance, consider the following: +If you still need assistance, ask a question on the [Issues page](https://github.com/google/testrun/issues). -- Ensure that all hardware and software prerequisites are met. -- Verify that the network interfaces are connected correctly. -- Check the configuration settings. -- Refer to the Testrun documentation or ask for assistance in the issues page: https://github.com/google/testrun/issues +# Review the report -# Reviewing -Once you have completed a test attempt, you may want to review the test report provided by Testrun. For more information about what Testrun looks for when testing, and what the output means, take a look at the testing documentation: [Testing](/docs/test/README.md). +Once you complete a test attempt, you can review the test report provided by Testrun. For more information on Testrun requirements and outputs, refer to the [Testing documentation](/docs/test/README.md). # Uninstall -To uninstall Testrun, use the built-in dpkg uninstall command to remove Testrun correctly. For Testrun, this would be: ```sudo apt-get remove testrun```. + +To uninstall Testrun correctly, use the built-in dpkg uninstall command: `sudo apt-get remove testrun` \ No newline at end of file diff --git a/docs/network/README.md b/docs/network/README.md index b5536c30c..0efb5e3ff 100644 --- a/docs/network/README.md +++ b/docs/network/README.md @@ -1,45 +1,41 @@ Testrun logo +# Network -## Network Overview +This page provides an overview of Testrun's network services. Visit these pages for additional information: -## Table of Contents -1) Network Overview (this page) -2) [How to identify network interfaces](identify_interfaces.md) -3) [Addresses](addresses.md) -4) [Add a new network service](add_new_service.md) +- [Network addresses](/docs/network/addresses.md) +- [Add a new network service](/docs/network/add_new_service.md) -Testrun provides several built-in network services that can be utilized for testing purposes. These services are already available and can be used without any additional configuration. +Testrun provides several built-in network services you can use for testing purposes. These services don't require any additional configuration. Below is a list and brief description of the network services provided. -The following network services are provided: - -### Internet Connectivity (Gateway Service) +# Internet connectivity (gateway service) The gateway service provides internet connectivity to the test network. It allows devices in the network to access external resources and communicate with the internet. -### DHCPv4 Service +# DHCPv4 service The DHCPv4 service provides Dynamic Host Configuration Protocol (DHCP) functionality for IPv4 addressing. It includes the following components: -- Primary DHCP Server: A primary DHCP server is available to assign IPv4 addresses to DHCP clients in the network. -- Secondary DHCP Server (Failover Configuration): A secondary DHCP server operates in failover configuration with the primary server to provide high availability and redundancy. +- Primary DHCP server: Assigns IPv4 addresses to DHCP clients in the network. +- Secondary DHCP server (failover configuration): Operates in failover configuration with the primary server to provide high availability and redundancy. -#### Configuration +## Configuration -The configuration of the DHCPv4 service can be modified using the provided GRPC (gRPC Remote Procedure Call) service. +You can modify the configuration of the DHCPv4 service using the provided Remote Procedure Call (GRPC) service. -### IPv6 SLAAC Addressing +# IPv6 SLAAC addressing -The primary DHCP server also provides IPv6 Stateless Address Autoconfiguration (SLAAC) addressing for devices in the network. IPv6 addresses are automatically assigned to devices using SLAAC where test devices support it. +The primary DHCP server provides IPv6 Stateless Address Autoconfiguration (SLAAC) addressing for devices on the network. It automatically assigns IPv6 addresses to devices using SLAAC where test devices support it. -### NTP Service +# NTP service -The Network Time Protocol (NTP) service provides time synchronization for devices in the network. It ensures that all devices have accurate and synchronized time information. +The Network Time Protocol (NTP) service provides time synchronization for devices on the network. It ensures that all devices have accurate and synchronized time information. -### DNS Service +# DNS service -The DNS (Domain Name System) service resolves domain names to their corresponding IP addresses. It allows devices in the network to access external resources using domain names. +The Domain Name System (DNS) service resolves domain names to their corresponding IP addresses. It allows devices on the network to access external resources using domain names. -### 802.1x Authentication (Radius Module) +# 802.1x authentication (radius module) -The radius module provides 802.1x authentication for devices in the network. It ensures secure and authenticated access to the network. The issuing CA (Certificate Authority) certificate can be specified by the user if required. \ No newline at end of file +The radius module provides 802.1x authentication for devices on the network. It ensures secure and authenticated access to the network. The user can specify the issuing Certificate Authority (CA) certificate if required. \ No newline at end of file diff --git a/docs/network/add_new_service.md b/docs/network/add_new_service.md index 7a07e43be..e6b01e102 100644 --- a/docs/network/add_new_service.md +++ b/docs/network/add_new_service.md @@ -1,21 +1,44 @@ Testrun logo -## Adding a New Network Service +# Add a new network service -The Testrun framework allows users to add their own network services with ease. A template network service can be used to get started quickly, this can be found at [modules/network/template](../../modules/network/template). Otherwise, see below for details of the requirements for new network services. +The Testrun framework allows you to easily add your own network services. You can use the template network service at [modules/network/template](/modules/network/template). To add a new network service, follow these steps: -To add a new network service to Testrun, follow the procedure below: +1. Create a folder under `modules/network/` with the name of the network service in lowercase using only alphanumeric characters and hyphens (-). +1. Include the following items in the created folder: + - `{module}.Dockerfile`: Dockerfile builds the network service image. Replace `{module}` with the name of the module. + - `conf/`: Folder containing the module configuration files. + - `bin/`: Folder containing the start-up script for the network service. + - Place any additional application code in its own folder. -1. Create a folder under `modules/network/` with the name of the network service in lowercase, using only alphanumeric characters and hyphens (`-`). -2. Inside the created folder, include the following files and folders: - - `{module}.Dockerfile`: Dockerfile for building the network service image. Replace `{module}` with the name of the module. - - `conf/`: Folder containing the module configuration files. - - `bin/`: Folder containing the startup script for the network service. - - Any additional application code can be placed in its own folder. +Here are some examples: -### Example `module_config.json` +## {module}.Dockerfile -```json +``` +# Image name: test-run/{module} +FROM test-run/base:latest + +ARG MODULE_NAME={module} +ARG MODULE_DIR=modules/network/$MODULE_NAME + +# Install network service dependencies +# ... + +# Copy over all configuration files +COPY $MODULE_DIR/conf /testrun/conf + +# Copy over all binary files +COPY $MODULE_DIR/bin /testrun/bin + +# Copy over all python files +COPY $MODULE_DIR/python /testrun/python + +# Do not specify a CMD or Entrypoint as Testrun will automatically start your service as required by calling the start_network_service script in the bin folder +``` + +## module_config.json +``` { "config": { "meta": { @@ -42,35 +65,11 @@ To add a new network service to Testrun, follow the procedure below: } } } -``` - -### Example of {module}.Dockerfile -```Dockerfile -# Image name: test-run/{module} -FROM test-run/base:latest - -ARG MODULE_NAME={module} -ARG MODULE_DIR=modules/network/$MODULE_NAME - -# Install network service dependencies -# ... - -# Copy over all configuration files -COPY $MODULE_DIR/conf /testrun/conf - -# Copy over all binary files -COPY $MODULE_DIR/bin /testrun/bin - -# Copy over all python files -COPY $MODULE_DIR/python /testrun/python - -# Do not specify a CMD or Entrypoint as Test Run will automatically start your service as required ``` -### Example of start_network_service script - -```bash +## start_network_service script +``` #!/bin/bash CONFIG_FILE=/etc/network_service/config.conf @@ -89,8 +88,4 @@ echo "Starting Network Service..." # Restart the network service when the config changes # ... -``` - - - - +``` \ No newline at end of file diff --git a/docs/network/addresses.md b/docs/network/addresses.md index 7fa71d716..261242687 100644 --- a/docs/network/addresses.md +++ b/docs/network/addresses.md @@ -1,20 +1,19 @@ Testrun logo -## Network Addresses +# Network addresses -Each network service is configured with an IPv4 and IPv6 address. For IPv4 addressing, the last number in the IPv4 address is fixed (ensuring the IP is unique). See below for a table of network addresses: +Each network service is configured with an IPv4 and IPv6 address. For IPv4 addressing, the last number in the IPv4 address is fixed, ensuring the IP is unique. The table below lists network addresses you might need. -| Name | Mac address | IPv4 address | IPv6 address | -|---------------------|----------------------|--------------|------------------------------| -| Internet gateway | 9a:02:57:1e:8f:01 | 10.10.10.1 | fd10:77be:4186::1 | -| DHCP primary | 9a:02:57:1e:8f:02 | 10.10.10.2 | fd10:77be:4186::2 | -| DHCP secondary | 9a:02:57:1e:8f:03 | 10.10.10.3 | fd10:77be:4186::3 | -| DNS server | 9a:02:57:1e:8f:04 | 10.10.10.4 | fd10:77be:4186::4 | -| NTP server | 9a:02:57:1e:8f:05 | 10.10.10.5 | fd10:77be:4186::5 | -| Radius authenticator| 9a:02:57:1e:8f:07 | 10.10.10.7 | fd10:77be:4186::7 | -| Active test module | 9a:02:57:1e:8f:09 | 10.10.10.9 | fd10:77be:4186::9 | +| Name | MAC address | IPv4 address | IPv6 address | +| ----------------- | ----------------- | ------------- | ------------------- | +| Internet gateway | 9a\:02\:57\:1e\:8f\:01 | 10.10.10.1 | fd10\:77be\:4186\:\:1 | +| DHCP primary | 9a\:02\:57\:1e\:8f\:02 | 10.10.10.2 | fd10\:77be\:4186\:\:2 | +| DHCP secondary | 9a\:02\:57\:1e\:8f\:03 | 10.10.10.3 | fd10\:77be\:4186\:\:3 | +| DNS server | 9a\:02\:57\:1e\:8f\:04 | 10.10.10.4 | fd10\:77be\:4186\:\:4 | +| NTP server | 9a\:02\:57\:1e\:8f\:05 | 10.10.10.5 | fd10\:77be\:4186\:\:5 | +| Radius authenticator | 9a\:02\:57\:1e\:8f\:07 | 10.10.10.7 | fd10\:77be\:4186\:\:7 | +| Active test module | 9a\:02\:57\:1e\:8f\:09 | 10.10.10.9 | fd10\:77be\:4186\:\:9 | +The default network range is 10.10.10.0/24 and devices are assigned addresses in that range via DHCP. The range may change when requested by a test module. In that case, network services restart and are accessible on the new range with the same final host ID. The default IPv6 network is fd10:77be:4186::/64 and addresses are assigned to devices on the network using IPv6 SLAAC. -The default network range is 10.10.10.0/24 and devices will be assigned addresses in that range via DHCP. The range may change when requested by a test module. In which case, network services will be restarted and accessible on the new range, with the same final host ID. The default IPv6 network is fd10:77be:4186::/64 and addresses will be assigned to devices on the network using IPv6 SLAAC. - -When creating a new network module, please ensure that the ip_index value in the module_config.json is unique otherwise unexpected behaviour will occur. \ No newline at end of file +When creating a new network module, ensure that the ip_index value in the module_config.json is unique to prevent unexpected behavior. \ No newline at end of file diff --git a/docs/network/identify_interfaces.md b/docs/network/identify_interfaces.md deleted file mode 100644 index 50e62acd3..000000000 --- a/docs/network/identify_interfaces.md +++ /dev/null @@ -1,20 +0,0 @@ -Testrun logo - -## Identifying network interfaces - -For Testrun to operate correctly, you must select the correct network interfaces within the settings panel of the user interface. There are 2 methods to identify the correct network interfaces: - -A) Find the printed MAC address on your interface - -Some USB network interfaces will have the MAC address printed on the interface itself. This will look something like: ```00:e0:4c:02:0f:a8```. - -Compare this printed MAC address against the MAC address provided in the settings panel in the user interface. - -B) Connect your interfaces one at a time - - 1) Ensure both interfaces are disconnected from your PC and open the settings panel in the user interface. - - 2) Connect your internet interface to your PC and refresh the settings panel. One interface, which was not previously present, should now be visibile. - - 3) Repeat the previous step for the devices interface. - diff --git a/docs/roadmap.pdf b/docs/roadmap.pdf index 9f4599ee1..0566598e0 100644 Binary files a/docs/roadmap.pdf and b/docs/roadmap.pdf differ diff --git a/docs/test/README.md b/docs/test/README.md index 19aa691d8..19fcf3fd5 100644 --- a/docs/test/README.md +++ b/docs/test/README.md @@ -1,8 +1,5 @@ Testrun logo +# Testing -## Testing - -The test requirements that are investigated by Testrun can be found in the [test modules documentation](/docs/test/modules.md). - -To understand the testing results, various definitions of test results and requirements are specified in the [statuses documentation](/docs/test/statuses.md). \ No newline at end of file +Testrun provides modules for you to test your own device. You can learn more about the requirements for each on the [Test modules page](/docs/test/modules.md). The [Test results page](/docs/test/statuses.md) outlines possible results and how to interpret them. diff --git a/docs/test/modules.md b/docs/test/modules.md index 7c5851ba4..ff6bb5c53 100644 --- a/docs/test/modules.md +++ b/docs/test/modules.md @@ -1,16 +1,26 @@ Testrun logo -## Test Modules +# Test modules -Testrun provides some pre-built test modules for you to use when testing your own device. These test modules are listed below: +Testrun provides some pre-built modules you can use when testing your own device. The table below lists the test module, its purpose, and a link to additional information. -| Name | Description | Read more | -|---|---|---| -| Base | Template for all test modules | [Base module](/modules/test/base/README.md) | -| Baseline | A sample test module | [Baseline module](/modules/test/baseline/README.md) | -| Connection | Verify IP and DHCP based behavior | [Connection module](/modules/test/conn/README.md) | -| DNS | Verify DNS functionality | [DNS module](/modules/test/dns/README.md) | -| NMAP | Ensure unsecure services are disabled | [NMAP module](/modules/test/nmap/README.md) | -| NTP | Verify NTP functionality | [NTP module](/modules/test/ntp/README.md) | -| Protocol | Inspect BMS protocol implementation | [Protocol Module](/modules/test/protocol/README.md) | -| TLS | Determine TLS client and server behavior | [TLS module](/modules/test/tls/README.md) | +| Module name | Purpose | Additional documentation | +| ------------ | ---------------------------- | ----------------------------- | +| Base | Template for all test modules | [Base module] | +| Baseline | Sample test module | [Baseline module] | +| Connection | Verify IP and DHCP-based behavior | [Connection module] | +| DNS | Verify DNS functionality | [DNS module] | +| Services | Ensure unsecure services are disabled | [Services module] | +| NTP | Verify NTP functionality | [NTP module] | +| Protocol | Inspect BMS protocol implementation | [Protocol Module] | +| TLS | Determine TLS client and server behavior | [TLS module] | + + +[Base module]: /modules/test/base/README.md +[Baseline module]: /modules/test/baseline/README.md +[Connection module]: /modules/test/conn/README.md +[DNS module]: /modules/test/dns/README.md +[Services module]: /modules/test/services/README.md +[NTP module]: /modules/test/ntp/README.md +[Protocol Module]: /modules/test/protocol/README.md +[TLS module]: /modules/test/tls/README.md diff --git a/docs/test/statuses.md b/docs/test/statuses.md index d196fa4af..8268b3e88 100644 --- a/docs/test/statuses.md +++ b/docs/test/statuses.md @@ -1,33 +1,33 @@ Testrun logo -## Test Statuses -Testrun will output the result and description of each automated test. The test results will be one of the following: +# Test results -| Name | Description | What next? | -|---|---|---| -| Compliant | The device implements the required feature correctly | Nothing | -| Non-Compliant | The device does not support the specified requirements for the test | Modify or implement the required functionality on the device | -| Feature Not Detected | The device does not implement a feature covered by the test | You may implement the functionality (not required) | -| Error | An error occured whilst running the test | Create a bug report requesting additional support to diagnose the issue | +Testrun outputs the result and a description of each automated test. The table below includes the result name, its description, and what your next step should be. -## Test Requirement -Testrun also determines whether each test is required for the device to receive an overall compliant result. These rules are: +| Result name | Description | What next? | +| --------------------- | ------------------------ | ------------------------ | +| Compliant | The device implements the required feature correctly. | Nothing. | +| Non-Compliant | The device doesn’t support the specified requirements for the test. | Modify or implement the required functionality on the device. | +| Informational | Extra information about the device under test | Nothing. | +| Feature Not Detected | The device doesn’t implement a feature covered by the test. | You may implement the functionality but it’s not required. | +| Error | An error occurred while running the test. | Create a bug report requesting additional support to diagnose the issue. | -| Name | Description | -|---|---| -| Required | The device must implement the feature | -| Recommended | The device should implement the feature, but will not receive an overall Non-Compliant if not implemented | -| Roadmap | The device should implement this feature in the future, but is not required at the moment | -| Required If Applicable | If the device implements this feature, it must be implemented correctly (as per the test requirements) | -## Testrun Statuses -Once testing is completed, an overall result for the test attempt will be produced. This is calculated by comparing the result of all tests, and whether they are required or not required. +# Test requirements -### Compliant -All required tests are implemented correctly, and all required if applicable tests are implemented correctly (where the feature has been implemented). +Testrun determines whether the device needs each test to receive an overall compliant result. Here are the rules and what they mean: -### Non-Compliant -One or more of the required tests (or required if applicable tests) have produced a non-compliant result. +- Required: The device must implement the feature. +- Recommended: The device should implement the feature but won't receive an overall Non-Compliant if it's not implemented. +- Roadmap: The device should implement this feature in the future, but it's not required at the moment. +- Required If Applicable: If the device implements this feature, it must be implemented correctly (per the test requirements). +- Informational: Regardless whether the device implements the feature or not, the test will receive an Informational result. -### Error -One of more of the required tests (or required if applicable tests) have not executed correctly. This does not necessarily indicate that the device is compliant or non-compliant. +# Testrun statuses + +Once testing is complete, the program produces an overall status for the test attempt. It's calculated by comparing the results of all tests and whether they're required or not. The possible statuses are: + +- Compliant: All required tests are implemented correctly, and all required if applicable tests are implemented correctly (where the feature is implemented). +- Non-Compliant: One or more of the required tests (or Required If Applicable tests) produced a Non-Compliant result. +- Error: One or more of the required tests (or Required If Applicable tests) didn't execute correctly. This doesn't necessarily indicate that the device is Compliant or Non-Compliant. +- Cancelled: Either the device was disconnected during testing or the user requested to cancel the test attempt. \ No newline at end of file diff --git a/docs/ui/accessibility.md b/docs/ui/accessibility.md new file mode 100644 index 000000000..58d948522 --- /dev/null +++ b/docs/ui/accessibility.md @@ -0,0 +1,12 @@ +Testrun logo + +# Accessibility + +We designed Testrun with accessibility at its core. The application provides full support for: + +- Screen readers +- Keyboard navigation +- Responsive resizing and scaling +- [Helperbird](https://www.helperbird.com/) + +For a more thorough explanation of these features, download the [accessibility video](https://github.com/google/testrun/raw/main/docs/ui/accessibility.mp4). If you require further accommodations when using Testrun, please [raise an issue](https://github.com/google/testrun/issues/new/choose). \ No newline at end of file diff --git a/docs/ui/device_icon.png b/docs/ui/device_icon.png deleted file mode 100644 index 2472f7da2..000000000 Binary files a/docs/ui/device_icon.png and /dev/null differ diff --git a/docs/ui/getstarted--2dn8vrzsspe.png b/docs/ui/getstarted--2dn8vrzsspe.png new file mode 100644 index 000000000..302e55302 Binary files /dev/null and b/docs/ui/getstarted--2dn8vrzsspe.png differ diff --git a/docs/ui/getstarted--3d9k3si3ul1.png b/docs/ui/getstarted--3d9k3si3ul1.png new file mode 100644 index 000000000..5c762156d Binary files /dev/null and b/docs/ui/getstarted--3d9k3si3ul1.png differ diff --git a/docs/ui/getstarted--7cfvdpdnc5o.png b/docs/ui/getstarted--7cfvdpdnc5o.png new file mode 100644 index 000000000..288e5f876 Binary files /dev/null and b/docs/ui/getstarted--7cfvdpdnc5o.png differ diff --git a/docs/ui/getstarted--m4si1otdu5d.png b/docs/ui/getstarted--m4si1otdu5d.png new file mode 100644 index 000000000..f6b7eddc9 Binary files /dev/null and b/docs/ui/getstarted--m4si1otdu5d.png differ diff --git a/docs/ui/getstarted--q5uw26tfod.png b/docs/ui/getstarted--q5uw26tfod.png new file mode 100644 index 000000000..4b8b2e847 Binary files /dev/null and b/docs/ui/getstarted--q5uw26tfod.png differ diff --git a/docs/ui/getstarted--w09wecsry3.png b/docs/ui/getstarted--w09wecsry3.png new file mode 100644 index 000000000..e379040b5 Binary files /dev/null and b/docs/ui/getstarted--w09wecsry3.png differ diff --git a/docs/ui/history_icon.png b/docs/ui/history_icon.png deleted file mode 100644 index eb95a8663..000000000 Binary files a/docs/ui/history_icon.png and /dev/null differ diff --git a/docs/ui/progress_icon.png b/docs/ui/progress_icon.png deleted file mode 100644 index c326d185e..000000000 Binary files a/docs/ui/progress_icon.png and /dev/null differ diff --git a/docs/ui/settings_icon.png b/docs/ui/settings_icon.png deleted file mode 100644 index 8fc83b9bb..000000000 Binary files a/docs/ui/settings_icon.png and /dev/null differ diff --git a/docs/ui/settings_menu.png b/docs/ui/settings_menu.png deleted file mode 100644 index 046526b25..000000000 Binary files a/docs/ui/settings_menu.png and /dev/null differ diff --git a/docs/ui/test_name.png b/docs/ui/test_name.png deleted file mode 100644 index 3d18df19d..000000000 Binary files a/docs/ui/test_name.png and /dev/null differ diff --git a/docs/virtual_machine.md b/docs/virtual_machine.md index 2f029b296..579a40c93 100644 --- a/docs/virtual_machine.md +++ b/docs/virtual_machine.md @@ -1,38 +1,41 @@ Testrun logo -## Virtual Machine +# Run on a virtual machine -This guide will provide steps to use Testrun within a virtual machine in virtual Box (VMWare and other providers have not yet been tested). You should use this guide alongside the [Get Started guide](/docs/get_started.md) - only differences will be outlined in this guide. +This page provides steps to use Testrun within a virtual machine in VirtualBox. VMWare and other providers haven't been tested yet. You should use these instructions alongside the [Get started guide](/docs/get_started.md). -## Prerequisites +# Prerequisites -### Hardware +## Hardware -Before starting with Testrun, ensure you have the following hardware: -- PC running any OS that supports Virtual Box -- 2x USB Ethernet adapter (built in ethernet connections are not supported) -- Internet connection +Before you start with Testrun, ensure you have the following hardware: -### Software +- PC running any OS that supports VirtualBox +- 2x USB Ethernet adapter (built-in Ethernet connections aren't supported) +- Internet connection -Ensure the following software is installed on the host PC: - - Virtual Box +## Software -Ensure the following software is installed on your virtual machine: -- Ubuntu LTS (22.04 or 20.04) -- Docker - installation guide: [https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository](https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository) +Ensure you have VirtualBox installed on the host PC. Then, install the following software on your virtual machine: -## Installation +- Ubuntu LTS (22.04 or 20.04) +- Docker + - Refer to the [installation guide](https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository) as needed. -In addition to the install steps provided in the Get Started guide, the default user must be added to the sudo group. -1. Open a terminal and run ```sudo su``` to login as root (you will be prompted for your password). -2. Add the default user to the sudo group by running ```adduser {username} sudo```. -3. Restart the virtual machine. -4. Continue the installation as per the Get Started guide. +# Installation -## Start Testrun +As part of installation, you must add the default user to the sudo group: + +1. Open a terminal and run `sudo su` to log in as root. +1. Enter your password when prompted. +1. Add the default user to the sudo group by running `adduser {username} sudo`. +1. Restart the virtual machine. +1. Follow the steps in the [Get started guide](/docs/get_started.md) to complete the installation. + +# Start Testrun + +Follow these steps to start Testrun. Keep in mind that attaching USB Ethernet adapters is different when working in a virtual machine. -Attaching USB ethernet adapters is different when working in a Virtual Machine. 1. Ensure the 2x adapters are attached to the host PC. -2. With the virtual machine running, right click the USB icon in the bottom right of the window. -3. Select the 2x ethernet adapter names and check that these two adapters have now appeared in the virtual machine. \ No newline at end of file +1. With the virtual machine running, right-click the **USB** icon in the bottom-right of the window. +1. Select the 2x Ethernet adapter names. The two adapters should now appear in the virtual machine. \ No newline at end of file diff --git a/framework/python/src/api/api.py b/framework/python/src/api/api.py index aed663ab8..0ae7becd4 100644 --- a/framework/python/src/api/api.py +++ b/framework/python/src/api/api.py @@ -26,8 +26,10 @@ import uvicorn from urllib.parse import urlparse +from core import tasks from common import logger from common.device import Device +from common.statuses import TestrunStatus LOGGER = logger.get_logger("api") @@ -35,8 +37,16 @@ DEVICE_MANUFACTURER_KEY = "manufacturer" DEVICE_MODEL_KEY = "model" DEVICE_TEST_MODULES_KEY = "test_modules" +DEVICE_TEST_PACK_KEY = "test_pack" +DEVICE_TYPE_KEY = "type" +DEVICE_TECH_KEY = "technology" +DEVICE_ADDITIONAL_INFO_KEY = "additional_info" + DEVICES_PATH = "local/devices" -DEFAULT_DEVICE_INTF = "enx123456789123" + +RESOURCES_PATH = "resources" +DEVICE_FOLDER_PATH = "devices" +DEVICE_QUESTIONS_FILE_NAME = "device_profile.json" LATEST_RELEASE_CHECK = ("https://api.github.com/repos/google/" + "testrun/releases/latest") @@ -45,32 +55,45 @@ class Api: """Provide REST endpoints to manage Testrun""" - def __init__(self, test_run): + def __init__(self, testrun): - self._test_run = test_run + self._testrun = testrun self._name = "Testrun API" self._router = APIRouter() - self._session = self._test_run.get_session() + # Load static JSON resources + device_resources = os.path.join(self._testrun.get_root_dir(), + RESOURCES_PATH, + DEVICE_FOLDER_PATH) + + # Load device profile questions + self._device_profile = self._load_json(device_resources, + DEVICE_QUESTIONS_FILE_NAME) + # Fetch Testrun session + self._session = self._testrun.get_session() + + # System endpoints self._router.add_api_route("/system/interfaces", self.get_sys_interfaces) self._router.add_api_route("/system/config", self.post_sys_config, methods=["POST"]) self._router.add_api_route("/system/config", self.get_sys_config) self._router.add_api_route("/system/start", - self.start_test_run, + self.start_testrun, methods=["POST"]) self._router.add_api_route("/system/stop", - self.stop_test_run, + self.stop_testrun, methods=["POST"]) self._router.add_api_route("/system/status", self.get_status) self._router.add_api_route("/system/shutdown", self.shutdown, methods=["POST"]) - self._router.add_api_route("/system/version", self.get_version) + self._router.add_api_route("/system/modules", self.get_test_modules) + self._router.add_api_route("/system/testpacks", self.get_test_packs) + # Report endpoints self._router.add_api_route("/reports", self.get_reports) self._router.add_api_route("/report", self.delete_report, @@ -81,6 +104,7 @@ def __init__(self, test_run): self.get_results, methods=["POST"]) + # Device endpoints self._router.add_api_route("/devices", self.get_devices) self._router.add_api_route("/device", self.delete_device, @@ -89,10 +113,9 @@ def __init__(self, test_run): self._router.add_api_route("/device/edit", self.edit_device, methods=["POST"]) + self._router.add_api_route("/devices/format", self.get_devices_profile) - # Load modules - self._router.add_api_route("/system/modules", self.get_test_modules) - + # Certificate endpoints self._router.add_api_route("/system/config/certs", self.get_certs) self._router.add_api_route("/system/config/certs", self.upload_cert, @@ -114,8 +137,16 @@ def __init__(self, test_run): # Allow all origins to access the API origins = ["*"] - self._app = FastAPI() + # Scheduler for background periodic tasks + self._scheduler = tasks.PeriodicTasks(self._testrun) + + # Init FastAPI + self._app = FastAPI(lifespan=self._scheduler.start) + + # Attach router to FastAPI self._app.include_router(self._router) + + # Attach CORS middleware self._app.add_middleware( CORSMiddleware, allow_origins=origins, @@ -124,10 +155,27 @@ def __init__(self, test_run): allow_headers=["*"], ) + # Use separate thread for API self._api_thread = threading.Thread(target=self._start, name="Testrun API", daemon=True) + def _load_json(self, directory, file_name): + """Utility method to load json files' """ + # Construct the base path relative to the main folder + root_dir = self._testrun.get_root_dir() + + # Construct the full file path + file_path = os.path.join(root_dir, directory, file_name) + + # Open the file in read mode + with open(file_path, "r", encoding="utf-8") as file: + # Return the file content + return json.load(file) + + def _get_testrun(self): + return self._testrun + def start(self): LOGGER.info("Starting API") self._api_thread.start() @@ -165,7 +213,19 @@ async def post_sys_config(self, request: Request, response: Response): try: config = (await request.body()).decode("UTF-8") config_json = json.loads(config) + + # Validate req fields + if ("network" not in config_json or + "device_intf" not in config_json.get("network") or + "internet_intf" not in config_json.get("network") or + "log_level" not in config_json): + response.status_code = status.HTTP_400_BAD_REQUEST + return self._generate_msg( + False, + "Configuration is missing required fields") + self._session.set_config(config_json) + # Catch JSON Decode error etc except JSONDecodeError: response.status_code = status.HTTP_400_BAD_REQUEST @@ -176,9 +236,12 @@ async def get_sys_config(self): return self._session.get_config() async def get_devices(self): - return self._session.get_device_repository() + devices = [] + for device in self._session.get_device_repository(): + devices.append(device.to_dict()) + return devices - async def start_test_run(self, request: Request, response: Response): + async def start_testrun(self, request: Request, response: Response): LOGGER.debug("Received start command") @@ -199,9 +262,22 @@ async def start_test_run(self, request: Request, response: Response): device = self._session.get_device(body_json["device"]["mac_addr"]) + # Check if requested device is known in the device repository + if device is None: + response.status_code = status.HTTP_404_NOT_FOUND + return self._generate_msg( + False, "A device with that MAC address could not be found") + + # Check if device is fully configured + if device.status != "Valid": + response.status_code = status.HTTP_400_BAD_REQUEST + return self._generate_msg(False, "Device configuration is not complete") + # Check Testrun is not already running - if self._test_run.get_session().get_status() in [ - "In Progress", "Waiting for Device", "Monitoring" + if self._testrun.get_session().get_status() in [ + TestrunStatus.IN_PROGRESS, + TestrunStatus.WAITING_FOR_DEVICE, + TestrunStatus.MONITORING ]: LOGGER.debug("Testrun is already running. Cannot start another instance") response.status_code = status.HTTP_409_CONFLICT @@ -209,40 +285,42 @@ async def start_test_run(self, request: Request, response: Response): False, "Testrun cannot be started " + "whilst a test is running on another device") - # Check if requested device is known in the device repository - if device is None: - response.status_code = status.HTTP_404_NOT_FOUND - return self._generate_msg( - False, "A device with that MAC address could not be found") - device.firmware = body_json["device"]["firmware"] # Check if config has been updated (device interface not default) - if (self._test_run.get_session().get_device_interface() == - DEFAULT_DEVICE_INTF): + if (self._testrun.get_session().get_device_interface() == + ""): response.status_code = status.HTTP_400_BAD_REQUEST return self._generate_msg( False, "Testrun configuration has not yet " + "been completed.") # Check Testrun is able to start - if self._test_run.get_net_orc().check_config() is False: + if self._testrun.get_net_orc().check_config() is False: response.status_code = status.HTTP_500_INTERNAL_SERVER_ERROR return self._generate_msg( False, "Configured interfaces are not " + "ready for use. Ensure required interfaces " + "are connected.") - device.test_modules = body_json["device"]["test_modules"] + # UI doesn't send individual test configs so we need to + # merge these manually until the UI is updated to handle + # the full config file + for module_name, module_config in device.test_modules.items(): + # Check if the module exists in UI test modules + if module_name in body_json["device"]["test_modules"]: + # Merge the enabled state + module_config["enabled"] = body_json[ + "device"]["test_modules"][module_name]["enabled"] LOGGER.info("Starting Testrun with device target " + f"{device.manufacturer} {device.model} with " + f"MAC address {device.mac_addr}") - thread = threading.Thread(target=self._start_test_run, name="Testrun") + thread = threading.Thread(target=self._start_testrun, name="Testrun") thread.start() - self._test_run.get_session().set_target_device(device) + self._testrun.get_session().set_target_device(device) - return self._test_run.get_session().to_json() + return self._testrun.get_session().to_json() def _generate_msg(self, success, message): msg_type = "success" @@ -250,24 +328,26 @@ def _generate_msg(self, success, message): msg_type = "error" return json.loads('{"' + msg_type + '": "' + message + '"}') - def _start_test_run(self): - self._test_run.start() + def _start_testrun(self): + self._testrun.start() - async def stop_test_run(self, response: Response): + async def stop_testrun(self, response: Response): LOGGER.debug("Received stop command") # Check if Testrun is running - if (self._test_run.get_session().get_status() - not in ["In Progress", "Waiting for Device", "Monitoring"]): + if (self._testrun.get_session().get_status() + not in [TestrunStatus.IN_PROGRESS, + TestrunStatus.WAITING_FOR_DEVICE, + TestrunStatus.MONITORING]): response.status_code = 404 return self._generate_msg(False, "Testrun is not currently running") - self._test_run.stop() + self._testrun.stop() return self._generate_msg(True, "Testrun stopped") async def get_status(self): - return self._test_run.get_session().to_json() + return self._testrun.get_session().to_json() def shutdown(self, response: Response): @@ -275,20 +355,24 @@ def shutdown(self, response: Response): # Check that Testrun is not currently running if (self._session.get_status() - not in ["Cancelled", "Compliant", "Non-Compliant", "Idle"]): + not in [TestrunStatus.CANCELLED, + TestrunStatus.COMPLIANT, + TestrunStatus.NON_COMPLIANT, + TestrunStatus.IDLE + ]): LOGGER.debug("Unable to shutdown Testrun as Testrun is in progress") response.status_code = 400 return self._generate_msg( False, "Unable to shutdown. A test is currently in progress.") - self._test_run.shutdown() + self._testrun.shutdown() os.kill(os.getpid(), signal.SIGTERM) async def get_version(self, response: Response): # Add defaults json_response = {} - json_response["installed_version"] = "v" + self._test_run.get_version() + json_response["installed_version"] = "v" + self._testrun.get_version() json_response["update_available"] = False json_response["latest_version"] = None json_response["latest_version_url"] = ( @@ -360,7 +444,7 @@ async def delete_report(self, request: Request, response: Response): if len(body_raw) == 0: response.status_code = 400 - return self._generate_msg(False, "Invalid request received") + return self._generate_msg(False, "Invalid request received, missing body") try: body_json = json.loads(body_raw) @@ -372,12 +456,18 @@ async def delete_report(self, request: Request, response: Response): if "mac_addr" not in body_json or "timestamp" not in body_json: response.status_code = 400 - return self._generate_msg(False, "Invalid request received") + return self._generate_msg(False, "Missing mac address or timestamp") mac_addr = body_json.get("mac_addr").lower() timestamp = body_json.get("timestamp") - parsed_timestamp = datetime.strptime(timestamp, "%Y-%m-%d %H:%M:%S") - timestamp_formatted = parsed_timestamp.strftime("%Y-%m-%dT%H:%M:%S") + + try: + parsed_timestamp = datetime.strptime(timestamp, "%Y-%m-%d %H:%M:%S") + timestamp_formatted = parsed_timestamp.strftime("%Y-%m-%dT%H:%M:%S") + + except ValueError: + response.status_code = 400 + return self._generate_msg(False, "Incorrect timestamp format") # Get device from MAC address device = self._session.get_device(mac_addr) @@ -386,7 +476,15 @@ async def delete_report(self, request: Request, response: Response): response.status_code = 404 return self._generate_msg(False, "Could not find device") - if self._test_run.delete_report(device, timestamp_formatted): + # Assign the reports folder path from testrun + reports_folder = self._testrun.get_reports_folder(device) + + # Check if reports folder exists + if not os.path.exists(reports_folder): + response.status_code = 404 + return self._generate_msg(False, "Report not found") + + if self._testrun.delete_report(device, timestamp_formatted): return self._generate_msg(True, "Deleted report") response.status_code = 500 @@ -410,7 +508,7 @@ async def delete_device(self, request: Request, response: Response): mac_addr = device_json.get("mac_addr").lower() # Check that device exists - device = self._test_run.get_session().get_device(mac_addr) + device = self._testrun.get_session().get_device(mac_addr) if device is None: response.status_code = 404 @@ -419,13 +517,16 @@ async def delete_device(self, request: Request, response: Response): # Check that Testrun is not currently running against this device if (self._session.get_target_device() == device and self._session.get_status() - not in ["Cancelled", "Compliant", "Non-Compliant"]): + not in [TestrunStatus.CANCELLED, + TestrunStatus.COMPLIANT, + TestrunStatus.NON_COMPLIANT + ]): response.status_code = 403 return self._generate_msg( False, "Cannot delete this device whilst " + "it is being tested") # Delete device - self._test_run.delete_device(device) + self._testrun.delete_device(device) # Return success response response.status_code = 200 @@ -464,6 +565,19 @@ async def save_device(self, request: Request, response: Response): device_json.get(DEVICE_MODEL_KEY) ) + # Check if device folder exists + device_folder = os.path.join(self._testrun.get_root_dir(), + DEVICES_PATH, + device_json.get(DEVICE_MANUFACTURER_KEY) + + " " + + device_json.get(DEVICE_MODEL_KEY)) + + if os.path.exists(device_folder): + response.status_code = status.HTTP_409_CONFLICT + return self._generate_msg( + False, "A folder with that name already exists, " \ + "please rename the device or folder") + if device is None: # Create new device @@ -471,10 +585,15 @@ async def save_device(self, request: Request, response: Response): device.mac_addr = device_json.get(DEVICE_MAC_ADDR_KEY).lower() device.manufacturer = device_json.get(DEVICE_MANUFACTURER_KEY) device.model = device_json.get(DEVICE_MODEL_KEY) + device.test_pack = device_json.get(DEVICE_TEST_PACK_KEY) + device.type = device_json.get(DEVICE_TYPE_KEY) + device.technology = device_json.get(DEVICE_TECH_KEY) + device.additional_info = device_json.get(DEVICE_ADDITIONAL_INFO_KEY) + device.device_folder = device.manufacturer + " " + device.model device.test_modules = device_json.get(DEVICE_TEST_MODULES_KEY) - self._test_run.create_device(device) + self._testrun.create_device(device) response.status_code = status.HTTP_201_CREATED else: @@ -517,14 +636,17 @@ async def edit_device(self, request: Request, response: Response): if device is None: response.status_code = status.HTTP_404_NOT_FOUND return self._generate_msg( - False, "A device with that MAC " + "address could not be found") + False, "A device with that MAC address could not be found") if (self._session.get_target_device() == device and self._session.get_status() - not in ["Cancelled", "Compliant", "Non-Compliant"]): + not in [TestrunStatus.CANCELLED, + TestrunStatus.COMPLIANT, + TestrunStatus.NON_COMPLIANT + ]): response.status_code = 403 return self._generate_msg( - False, "Cannot edit this device whilst " + "it is being tested") + False, "Cannot edit this device whilst it is being tested") # Check if a device exists with the new MAC address check_new_device = self._session.get_device( @@ -534,15 +656,22 @@ async def edit_device(self, request: Request, response: Response): != check_new_device.mac_addr): response.status_code = status.HTTP_409_CONFLICT return self._generate_msg( - False, "A device with that MAC address " + "already exists") + False, "A device with that MAC address already exists") # Update the device device.mac_addr = device_json.get(DEVICE_MAC_ADDR_KEY).lower() device.manufacturer = device_json.get(DEVICE_MANUFACTURER_KEY) device.model = device_json.get(DEVICE_MODEL_KEY) + device.test_pack = device_json.get(DEVICE_TEST_PACK_KEY) + device.type = device_json.get(DEVICE_TYPE_KEY) + device.technology = device_json.get(DEVICE_TECH_KEY) + device.additional_info = device_json.get(DEVICE_ADDITIONAL_INFO_KEY) device.test_modules = device_json.get(DEVICE_TEST_MODULES_KEY) - self._test_run.save_device(device, device_json) + # Update device status to valid now that configuration is complete + device.status = "Valid" + + self._testrun.save_device(device) response.status_code = status.HTTP_200_OK return device.to_config_json() @@ -555,6 +684,12 @@ async def edit_device(self, request: Request, response: Response): async def get_report(self, response: Response, device_name, timestamp): device = self._session.get_device_by_name(device_name) + # If the device not found + if device is None: + LOGGER.info("Device not found, returning 404") + response.status_code = 404 + return self._generate_msg(False, "Device not found") + # 1.3 file path file_path = os.path.join( DEVICES_PATH, @@ -608,47 +743,77 @@ async def get_results(self, request: Request, response: Response, device_name, return self._generate_msg(False, "A device with that name could not be found") - file_path = self._get_test_run().get_test_orc().zip_results( + # Check if report exists (1.3 file path) + report_file_path = os.path.join( + DEVICES_PATH, + device_name, + "reports", + timestamp,"test", + device.mac_addr.replace(":","")) + + if not os.path.isdir(report_file_path): + # pre 1.3 file path + report_file_path = os.path.join(DEVICES_PATH, device_name, "reports", + timestamp) + + if not os.path.isdir(report_file_path): + LOGGER.info("Report could not be found, returning 404") + response.status_code = 404 + return self._generate_msg(False, "Report could not be found") + + zip_file_path = self._get_testrun().get_test_orc().zip_results( device, timestamp, profile) - if file_path is None: + if zip_file_path is None: response.status_code = status.HTTP_500_INTERNAL_SERVER_ERROR return self._generate_msg( False, "An error occurred whilst archiving test results") - if os.path.isfile(file_path): - return FileResponse(file_path) + if os.path.isfile(zip_file_path): + return FileResponse(zip_file_path) else: LOGGER.info("Test results could not be found, returning 404") response.status_code = 404 return self._generate_msg(False, "Test results could not be found") + async def get_devices_profile(self): + """Device profile questions""" + return self._device_profile + def _validate_device_json(self, json_obj): # Check all required properties are present - if not (DEVICE_MAC_ADDR_KEY in json_obj and DEVICE_MANUFACTURER_KEY - in json_obj and DEVICE_MODEL_KEY in json_obj): - return False + for string in [ + DEVICE_MAC_ADDR_KEY, + DEVICE_MANUFACTURER_KEY, + DEVICE_MODEL_KEY, + DEVICE_TYPE_KEY, + DEVICE_TECH_KEY, + DEVICE_ADDITIONAL_INFO_KEY + ]: + if string not in json_obj: + LOGGER.error(f"Missing required key {string} in device configuration") + return False # Check length of strings if len(json_obj.get(DEVICE_MANUFACTURER_KEY)) > 28 or len( json_obj.get(DEVICE_MODEL_KEY)) > 28: + LOGGER.error("Device manufacturer or model are longer than 28 characters") return False disallowed_chars = ["/", "\\", "\'", "\"", ";"] for char in json_obj.get(DEVICE_MANUFACTURER_KEY): if char in disallowed_chars: + LOGGER.error("Disallowed character in device manufacturer") return False for char in json_obj.get(DEVICE_MODEL_KEY): if char in disallowed_chars: + LOGGER.error("Disallowed character in device model") return False return True - def _get_test_run(self): - return self._test_run - # Profiles def get_profiles_format(self, response: Response): @@ -670,6 +835,12 @@ async def update_profile(self, request: Request, response: Response): LOGGER.debug("Received profile update request") + # Check if the profiles format was loaded correctly + if self.get_session().get_profiles_format() is None: + response.status_code = status.HTTP_501_NOT_IMPLEMENTED + return self._generate_msg(False, + "Risk profiles are not available right now") + try: req_raw = (await request.body()).decode("UTF-8") req_json = json.loads(req_raw) @@ -679,12 +850,18 @@ async def update_profile(self, request: Request, response: Response): response.status_code = status.HTTP_400_BAD_REQUEST return self._generate_msg(False, "Invalid request received") + # Validate json profile + if not self.get_session().validate_profile_json(req_json): + response.status_code = status.HTTP_400_BAD_REQUEST + return self._generate_msg(False, "Invalid request received") + profile_name = req_json.get("name") # Check if profile exists profile = self.get_session().get_profile(profile_name) if profile is None: + # Create new profile profile = self.get_session().update_profile(req_json) @@ -798,6 +975,20 @@ async def upload_cert(self, file: UploadFile, response: Response): False, "A certificate with that common name already exists." ) + # Returned when organization name is missing + elif str(e) == "Certificate is missing the organization name": + response.status_code = status.HTTP_400_BAD_REQUEST + return self._generate_msg( + False, "The certificate must contain the organization name" + ) + + # Returned when common name is missing + elif str(e) == "Certificate is missing the common name": + response.status_code = status.HTTP_400_BAD_REQUEST + return self._generate_msg( + False, "The certificate must contain the common name" + ) + # Returned when unable to load PEM file else: response.status_code = status.HTTP_400_BAD_REQUEST @@ -845,7 +1036,13 @@ async def delete_cert(self, request: Request, response: Response): def get_test_modules(self): modules = [] - for module in self._test_run.get_test_orc().get_test_modules(): + for module in self._testrun.get_test_orc().get_test_modules(): if module.enabled and module.enable_container: modules.append(module.display_name) return modules + + def get_test_packs(self): + test_packs: list[str] = [] + for test_pack in self._testrun.get_test_orc().get_test_packs(): + test_packs.append(test_pack.name) + return test_packs diff --git a/framework/python/src/common/device.py b/framework/python/src/common/device.py index c6a289d2c..d90720d90 100644 --- a/framework/python/src/common/device.py +++ b/framework/python/src/common/device.py @@ -14,7 +14,7 @@ """Track device object information.""" -from typing import Dict, List +from typing import List, Dict from dataclasses import dataclass, field from common.testreport import TestReport from datetime import datetime @@ -23,17 +23,21 @@ class Device(): """Represents a physical device and it's configuration.""" + status: str = 'Valid' folder_url: str = None mac_addr: str = None manufacturer: str = None model: str = None + type: str = None + technology: str = None + test_pack: str = 'Device Qualification' + additional_info: List[dict] = field(default_factory=list) test_modules: Dict = field(default_factory=dict) ip_addr: str = None firmware: str = None device_folder: str = None reports: List[TestReport] = field(default_factory=list) max_device_reports: int = None - reports: List[TestReport] = field(default_factory=list) def add_report(self, report): self.reports.append(report) @@ -54,11 +58,18 @@ def to_dict(self): """Returns the device as a python dictionary. This is used for the system status API endpoint and in the report.""" device_json = {} + device_json['status'] = self.status device_json['mac_addr'] = self.mac_addr device_json['manufacturer'] = self.manufacturer device_json['model'] = self.model + device_json['type'] = self.type + device_json['technology'] = self.technology + device_json['test_pack'] = self.test_pack + device_json['additional_info'] = self.additional_info + if self.firmware is not None: device_json['firmware'] = self.firmware + device_json['test_modules'] = self.test_modules return device_json @@ -69,5 +80,9 @@ def to_config_json(self): device_json['mac_addr'] = self.mac_addr device_json['manufacturer'] = self.manufacturer device_json['model'] = self.model + device_json['type'] = self.type + device_json['technology'] = self.technology + device_json['test_pack'] = self.test_pack device_json['test_modules'] = self.test_modules + device_json['additional_info'] = self.additional_info return device_json diff --git a/framework/python/src/common/docker_util.py b/framework/python/src/common/docker_util.py new file mode 100644 index 000000000..06b030419 --- /dev/null +++ b/framework/python/src/common/docker_util.py @@ -0,0 +1,35 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Utility for common docker methods""" +import docker + +def create_private_net(network_name): + client = docker.from_env() + try: + network = client.networks.get(network_name) + network.remove() + except docker.errors.NotFound: + pass + + # TODO: These should be made into variables + ipam_pool = docker.types.IPAMPool(subnet='100.100.0.0/16', + iprange='100.100.100.0/24') + + ipam_config = docker.types.IPAMConfig(pool_configs=[ipam_pool]) + + client.networks.create(network_name, + ipam=ipam_config, + internal=True, + check_duplicate=True, + driver='macvlan') diff --git a/framework/python/src/common/mqtt.py b/framework/python/src/common/mqtt.py new file mode 100644 index 000000000..b98b4ab1b --- /dev/null +++ b/framework/python/src/common/mqtt.py @@ -0,0 +1,60 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""MQTT client""" +import json +import typing as t +import paho.mqtt.client as mqtt_client +from common import logger + +LOGGER = logger.get_logger("mqtt") +WEBSOCKETS_HOST = "localhost" +WEBSOCKETS_PORT = 1883 + +class MQTTException(Exception): + def __init__(self, message: str) -> None: + super().__init__(message) + + +class MQTT: + """ MQTT client class""" + def __init__(self) -> None: + self._host = WEBSOCKETS_HOST + self._client = mqtt_client.Client(mqtt_client.CallbackAPIVersion.VERSION2) + + def _connect(self): + """Establish connection to MQTT broker""" + if not self._client.is_connected(): + try: + self._client.connect(self._host, WEBSOCKETS_PORT, 60) + except (ValueError, ConnectionRefusedError): + LOGGER.error("Cannot connect to MQTT broker") + + def disconnect(self): + """Disconnect the local client from the MQTT broker""" + if self._client.is_connected(): + LOGGER.debug("Disconnecting from broker") + self._client.disconnect() + + def send_message(self, topic: str, message: t.Union[str, dict]) -> None: + """Send message to specific topic + + Args: + topic (str): mqtt topic + message (t.Union[str, dict]): message + """ + self._connect() + if isinstance(message, dict): + message = json.dumps(message) + self._client.publish(topic, str(message)) diff --git a/framework/python/src/common/risk_profile.py b/framework/python/src/common/risk_profile.py index 6afb229ac..559117aec 100644 --- a/framework/python/src/common/risk_profile.py +++ b/framework/python/src/common/risk_profile.py @@ -20,10 +20,16 @@ from common import logger import json import os +from jinja2 import Template +from copy import deepcopy +import math PROFILES_PATH = 'local/risk_profiles' LOGGER = logger.get_logger('risk_profile') RESOURCES_DIR = 'resources/report' +TEMPLATE_FILE = 'risk_report_template.html' +TEMPLATE_STYLES = 'risk_report_styles.css' +DEVICE_FORMAT_PATH = 'resources/devices/device_profile.json' # Locate parent directory current_dir = os.path.dirname(os.path.realpath(__file__)) @@ -43,6 +49,34 @@ class RiskProfile(): def __init__(self, profile_json=None, profile_format=None): + # Jinja template + with open(os.path.join(report_resource_dir, TEMPLATE_FILE), + 'r', + encoding='UTF-8' + ) as template_file: + self._template = Template(template_file.read()) + with open(os.path.join(report_resource_dir, + TEMPLATE_STYLES), + 'r', + encoding='UTF-8' + ) as style_file: + self._template_styles = style_file.read() + + # Device profile format + self._device_format = [] + try: + with open(os.path.join(root_dir, DEVICE_FORMAT_PATH), + 'r', + encoding='utf-8') as device_format_file: + device_format_json = json.load(device_format_file) + for step in device_format_json: + self._device_format.extend(step['questions']) + except (IOError, ValueError) as e: + LOGGER.error( + 'An error occurred whilst loading the device profile format') + LOGGER.debug(e) + + if profile_json is None or profile_format is None: return @@ -92,15 +126,16 @@ def update(self, profile_json, profile_format): self.risk = new_profile.risk def get_file_path(self): + """Returns the file path for the current risk profile json""" return os.path.join(PROFILES_PATH, self.name + '.json') def _validate(self, profile_json, profile_format): - if self._valid(profile_json, profile_format): - if self._expired(): - self.status = 'Expired' + if self._expired(): + self.status = 'Expired' + elif self._valid(profile_json, profile_format): # User only wants to save a draft - elif 'status' in profile_json and profile_json['status'] == 'Draft': + if 'status' in profile_json and profile_json['status'] == 'Draft': self.status = 'Draft' else: self.status = 'Valid' @@ -108,6 +143,7 @@ def _validate(self, profile_json, profile_format): self.status = 'Draft' def update_risk(self, profile_format): + """Update the calculated risk for the risk profile""" if self.status == 'Valid': @@ -176,6 +212,15 @@ def update_risk(self, profile_format): self.risk = risk + def _update_risk_by_device(self): + risk = self.risk + if self._device and self.status == 'Valid': + for question in self._device.additional_info: + if 'risk' in question and question['risk'] == 'High': + risk = 'High' + break + return risk + def _get_format_question(self, question: str, profile_format: dict): for q in profile_format: @@ -281,6 +326,7 @@ def _expired(self): return today > expiry_date def to_json(self, pretty=False): + """Returns the current risk profile in JSON format""" json_dict = { 'name': self.name, 'version': self.version, @@ -293,351 +339,114 @@ def to_json(self, pretty=False): return json.dumps(json_dict, indent=indent) def to_html(self, device): - - self._device = device - - return f''' - - - {self._generate_head()} - -
- {self._generate_header()} - {self._generate_risk_banner()} - {self._generate_risk_questions()} - {self._generate_footer()} -
- - - ''' - - def _generate_head(self): - - return f''' - - - - Risk Assessment - - - ''' - - def _generate_header(self): + """Returns the current risk profile in HTML format""" + + high_risk_message = '''The device has been assessed to be high + risk due to the nature of the answers provided + about the device functionality.''' + limited_risk_message = '''The device has been assessed to be limited risk + due to the nature of the answers provided about + the device functionality.''' with open(test_run_img_file, 'rb') as f: - tr_img_b64 = base64.b64encode(f.read()).decode('utf-8') - header = f''' -
-

Risk assessment

-

- {self._device.manufacturer} - {self._device.model} -

''' - header += f'''Testrun -
- ''' - return header - - def _generate_risk_banner(self): - return f''' -
-
-

{'high' if self.risk == 'High' else 'limited'} Risk

-
-
- { - 'The device has been assessed to be high risk due to the nature of the answers provided about the device functionality.' - if self.risk == 'High' else - 'The device has been assessed to be limited risk due to the nature of the answers provided about the device functionality.' - } -
-
- ''' - - def _generate_risk_questions(self): - - max_page_height = 350 - content = '' - - content += self._generate_table_head() + logo_img_b64 = base64.b64encode(f.read()).decode('utf-8') + + self._device = self._format_device_profile(device) + pages = self._generate_report_pages() + return self._template.render( + styles=self._template_styles, + manufacturer=self._device.manufacturer, + model=self._device.model, + logo=logo_img_b64, + risk=self._update_risk_by_device(), + high_risk_message=high_risk_message, + limited_risk_message=limited_risk_message, + pages=pages, + total_pages=len(pages), + version=self.version, + created_at=self.created.strftime('%d.%m.%Y') + ) + + def _generate_report_pages(self): + + # Text block heght + block_height = 18 + # Table row padding + block_padding = 30 + # Margin bottom list answer + margin_list = 14 + # margin after table row + margin_row = 8 + + height_first_page = 760 + height_page = 980 + + # Average text block width in characters + # for a 14px font size (average width of one character is 8px). + letters_in_line_str = 38 + letters_in_line_q = 40 + letters_in_line_list = 36 - index = 1 height = 0 + pages = [] + current_page = [] + index = 1 - for question in self.questions: - - if height > max_page_height: - content += self._generate_new_page() - height = 0 - - content += f''' -
-
{index}.
-
{question['question']}
-
''' - - # String answers (one line) - if isinstance(question['answer'], str): - content += question['answer'] - - if len(question['answer']) > 400: - height += 160 - elif len(question['answer']) > 300: - height += 140 - elif len(question['answer']) > 200: - height += 120 - elif len(question['answer']) > 100: - height += 70 - else: - height += 53 - - # Select multiple answers - elif isinstance(question['answer'], list): - content += '' - - content += '''
''' - + text_answers.append(options_dict[answer_index]['text']) + page_item['answer'] = text_answers + # Answer height for list + for answer in options: + answer_height += math.ceil(len(answer) + / letters_in_line_list + ) * block_height + answer_height += block_padding + margin_row + margin_list + page_item['index'] = index + row_height = max(question_height, answer_height) + + if ( + (len(pages) == 0 and row_height + height > height_first_page) + or (len(pages) > 0 and row_height + height > height_page) + ): + pages.append(current_page) + height = 0 + current_page = [page_item] + else: + height += row_height + current_page.append(page_item) index += 1 + pages.append(current_page) - return content - - def _generate_table_head(self): - return ''' -
-
-
Question
-
Answer
-
''' - - def _generate_new_page(self): - - # End the current table - content = ''' -
''' - - # End the page - content += self._generate_footer() - content += '' - - # Start a new page - content += ''' -
- ''' - - content += self._generate_header() - - content += self._generate_table_head() - - return content - - def _generate_footer(self): - footer = f''' - - ''' - return footer - - def _generate_css(self): - return ''' - /* Set some global variables */ - :root { - --header-height: .75in; - --header-width: 8.5in; - --header-pos-x: 0in; - --header-pos-y: 0in; - --page-width: 8.5in; - } - - @font-face { - font-family: 'Google Sans'; - font-style: normal; - src: url(https://fonts.gstatic.com/s/googlesans/v58/4Ua_rENHsxJlGDuGo1OIlJfC6l_24rlCK1Yo_Iqcsih3SAyH6cAwhX9RFD48TE63OOYKtrwEIJllpyk.woff2) format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; - } - - @font-face { - font-family: 'Roboto Mono'; - font-style: normal; - src: url(https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap) format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; - } - - /* Define some common body formatting*/ - body { - font-family: 'Google Sans', sans-serif; - margin: 0px; - padding: 0px; - } - - /* Sets proper page size during print to pdf for weasyprint */ - @page { - size: Letter; - width: 8.5in; - height: 11in; - } - - .page { - position: relative; - margin: 0 20px; - width: 8.5in; - height: 11in; - } - - /* Define the header related css elements*/ - .header { - position: relative; - } - - h1 { - margin: 0 0 8px 0; - font-size: 20px; - font-weight: 400; - } - - h2 { - margin: 0px; - font-size: 48px; - font-weight: 700; - } - - h3 { - font-size: 24px; - margin-bottom: 10px; - margin-top: 15px; - } - - h4 { - font-size: 12px; - font-weight: 500; - color: #5F6368; - margin-bottom: 0; - margin-top: 0; - } - - /* CSS for the footer */ - .footer { - position: absolute; - height: 30px; - width: 8.5in; - bottom: 0in; - border-top: 1px solid #D3D3D3; - } - - .footer-label { - color: #3C4043; - position: absolute; - top: 5px; - font-size: 12px; - } + return pages - @media print { - @page { - size: Letter; - width: 8.5in; - height: 11in; - } - } - - .risk-banner { - min-height: 120px; - padding: 5px 40px 0 40px; - margin-top: 30px; - } - - .risk-banner-limited { - background-color: #E4F7FB; - color: #007B83; - } - - .risk-banner-high { - background-color: #FCE8E6; - color: #C5221F; - } - - .risk-banner-title { - text-transform: uppercase; - font-weight: bold; - } - - .risk-table { - width: 100%; - margin-top: 40px; - text-align: left; - color: #3C4043; - font-size: 14px; - } - - .risk-table-head { - margin-bottom: 15px; - } - - .risk-table-head-question { - display: inline-block; - margin-left: 70px; - font-weight: bold; - } - - .risk-table-head-answer { - display: inline-block; - margin-left: 325px; - font-weight: bold; - } - - .risk-table-row { - margin-bottom: 8px; - background-color: #F8F9FA; - display: flex; - align-items: stretch; - overflow: hidden; - } - - .risk-question-no { - padding: 15px 20px; - width: 10px; - display: inline-block; - vertical-align: top; - position: relative; - } - - .risk-question { - padding: 15px 20px; - display: inline-block; - width: 350px; - vertical-align: top; - position: relative; - height: 100%; - } - - .risk-answer { - background-color: #E8F0FE; - padding: 15px 20px; - display: inline-block; - width: 340px; - position: relative; - height: 100%; - } - - ul { - margin-top: 0; - } - ''' def to_pdf(self, device): + """Returns the current risk profile in PDF format""" # Resolve the data as html first html = self.to_html(device) @@ -646,3 +455,21 @@ def to_pdf(self, device): pdf_bytes = BytesIO() HTML(string=html).write_pdf(pdf_bytes) return pdf_bytes + + # Adding risks to device profile questions + def _format_device_profile(self, device): + device_copy = deepcopy(device) + risk_map = { + question['question']: { + option['text']: option.get('risk', None) + for option in question['options'] if 'risk' in option + } + for question in self._device_format + } + for question in device_copy.additional_info: + risk = risk_map.get( + question['question'], {} + ).get(question['answer'], None) + if risk: + question['risk'] = risk + return device_copy diff --git a/framework/python/src/common/statuses.py b/framework/python/src/common/statuses.py new file mode 100644 index 000000000..4817d7cf8 --- /dev/null +++ b/framework/python/src/common/statuses.py @@ -0,0 +1,36 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Enums for Testrun""" + + +class TestrunStatus: + IDLE = "Idle" + WAITING_FOR_DEVICE = "Waiting for Device" + MONITORING = "Monitoring" + IN_PROGRESS = "In Progress" + CANCELLED = "Cancelled" + COMPLIANT = "Compliant" + NON_COMPLIANT = "Non-Compliant" + STOPPING = "Stopping" + + +class TestResult: + IN_PROGRESS = "In Progress" + COMPLIANT = "Compliant" + NON_COMPLIANT = "Non-Compliant" + ERROR = "Error" + FEATURE_NOT_DETECTED = "Feature Not Detected" + INFORMATIONAL = "Informational" + NOT_STARTED = "Not Started" + DISABLED = "Disabled" diff --git a/framework/python/src/common/tasks.py b/framework/python/src/common/tasks.py new file mode 100644 index 000000000..5da0b40c9 --- /dev/null +++ b/framework/python/src/common/tasks.py @@ -0,0 +1,78 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Periodic background tasks""" + +from contextlib import asynccontextmanager +import datetime +import logging + +from apscheduler.schedulers.asyncio import AsyncIOScheduler +from fastapi import FastAPI + +from common import logger + +# Check adapters period seconds +# Check adapters period seconds +CHECK_NETWORK_ADAPTERS_PERIOD = 5 +CHECK_INTERNET_PERIOD = 2 +INTERNET_CONNECTION_TOPIC = 'events/internet' +NETWORK_ADAPTERS_TOPIC = 'events/adapter' + +LOGGER = logger.get_logger('tasks') + + +class PeriodicTasks: + """Background periodic tasks + """ + def __init__( + self, testrun_obj, + ) -> None: + self._testrun = testrun_obj + self._mqtt_client = self._testrun.get_mqtt_client() + local_tz = datetime.datetime.now().astimezone().tzinfo + self._scheduler = AsyncIOScheduler(timezone=local_tz) + # Prevent scheduler warnings + self._scheduler._logger.setLevel(logging.ERROR) + + self.adapters_checker_job = self._scheduler.add_job( + func=self._testrun.get_net_orc().network_adapters_checker, + kwargs={ + 'mqtt_client': self._mqtt_client, + 'topic': NETWORK_ADAPTERS_TOPIC + }, + trigger='interval', + seconds=CHECK_NETWORK_ADAPTERS_PERIOD, + ) + # add internet connection cheking job only in single-intf mode + if 'single_intf' not in self._testrun.get_session().get_runtime_params(): + self.internet_shecker = self._scheduler.add_job( + func=self._testrun.get_net_orc().internet_conn_checker, + kwargs={ + 'mqtt_client': self._mqtt_client, + 'topic': INTERNET_CONNECTION_TOPIC + }, + trigger='interval', + seconds=CHECK_INTERNET_PERIOD, + ) + + @asynccontextmanager + async def start(self, app: FastAPI): # pylint: disable=unused-argument + """Start background tasks + + Args: + app (FastAPI): app instance + """ + # Job that checks for changes in network adapters + self._scheduler.start() + yield diff --git a/framework/python/src/common/testreport.py b/framework/python/src/common/testreport.py index 88a25a2b1..f9401fe80 100644 --- a/framework/python/src/common/testreport.py +++ b/framework/python/src/common/testreport.py @@ -17,14 +17,19 @@ from weasyprint import HTML from io import BytesIO from common import util +from common.statuses import TestrunStatus import base64 import os from test_orc.test_case import TestCase +from jinja2 import Environment, FileSystemLoader +from collections import OrderedDict DATE_TIME_FORMAT = '%Y-%m-%d %H:%M:%S' RESOURCES_DIR = 'resources/report' TESTS_FIRST_PAGE = 11 TESTS_PER_PAGE = 20 +TEST_REPORT_STYLES = 'test_report_styles.css' +TEST_REPORT_TEMPLATE = 'test_report_template.html' # Locate parent directory current_dir = os.path.dirname(os.path.realpath(__file__)) @@ -37,13 +42,15 @@ report_resource_dir = os.path.join(root_dir, RESOURCES_DIR) test_run_img_file = os.path.join(report_resource_dir, 'testrun.png') +qualification_icon = os.path.join(report_resource_dir, 'qualification-icon.png') +pilot_icon = os.path.join(report_resource_dir, 'pilot-icon.png') class TestReport(): """Represents a previous Testrun report.""" def __init__(self, - status='Non-Compliant', + status=TestrunStatus.NON_COMPLIANT, started=None, finished=None, total_tests=0): @@ -115,6 +122,10 @@ def to_json(self): if test.recommendations is not None and len(test.recommendations) > 0: test_dict['recommendations'] = test.recommendations + if (test.optional_recommendations is not None + and len(test.optional_recommendations) > 0): + test_dict['optional_recommendations'] = test.optional_recommendations + test_results.append(test_dict) report_json['tests'] = {'total': self._total_tests, @@ -141,6 +152,12 @@ def from_json(self, json_file): if 'test_modules' in json_file['device']: self._device['test_modules'] = json_file['device']['test_modules'] + if 'test_pack' in json_file['device']: + self._device['test_pack'] = json_file['device']['test_pack'] + + if 'additional_info' in json_file['device']: + self._device['device_profile'] = json_file['device']['additional_info'] + self._status = json_file['status'] self._started = datetime.strptime(json_file['started'], DATE_TIME_FORMAT) self._finished = datetime.strptime(json_file['finished'], DATE_TIME_FORMAT) @@ -157,8 +174,16 @@ def from_json(self, json_file): expected_behavior=test_result['expected_behavior'], required_result=test_result['required_result'], result=test_result['result']) + + # Add test recommendations if 'recommendations' in test_result: test_case.recommendations = test_result['recommendations'] + + # Add optional test recommendations + if 'optional_recommendations' in test_result: + test_case.optional_recommendations = test_result[ + 'optional_recommendations'] + self.add_test(test_case) # Create a pdf file in memory and return the bytes @@ -172,35 +197,81 @@ def to_pdf(self): return pdf_bytes def to_html(self): - json_data = self.to_json() - return f''' - - - {self.generate_head()} - - {self.generate_body(json_data)} - - - ''' - - def generate_test_sections(self, json_data): - results = json_data['tests']['results'] - sections = '' - for result in results: - sections += self.generate_test_section(result) - return sections - - def generate_test_section(self, result): - section_content = '
\n' - for key, value in result.items(): - if value is not None: # Check if the value is not None - # Replace underscores and capitalize - formatted_key = key.replace('_', ' ').title() - section_content += f'

{formatted_key}: {value}

\n' - section_content += '
\n
\n' - return section_content - - def generate_pages(self, json_data): + + # Jinja template + template_env = Environment(loader=FileSystemLoader(report_resource_dir)) + template = template_env.get_template(TEST_REPORT_TEMPLATE) + with open(os.path.join(report_resource_dir, + TEST_REPORT_STYLES), + 'r', + encoding='UTF-8' + ) as style_file: + styles = style_file.read() + + # Load Testrun logo to base64 + with open(test_run_img_file, 'rb') as f: + logo = base64.b64encode(f.read()).decode('utf-8') + + json_data=self.to_json() + + # Icons + with open(qualification_icon, 'rb') as f: + icon_qualification = base64.b64encode(f.read()).decode('utf-8') + with open(pilot_icon, 'rb') as f: + icon_pilot = base64.b64encode(f.read()).decode('utf-8') + + # Convert the timestamp strings to datetime objects + start_time = datetime.strptime(json_data['started'], '%Y-%m-%d %H:%M:%S') + end_time = datetime.strptime(json_data['finished'], '%Y-%m-%d %H:%M:%S') + + # Calculate the duration + duration = end_time - start_time + + # Calculate number of successful tests + successful_tests = 0 + for test in json_data['tests']['results']: + if test['result'] != 'Error': + successful_tests += 1 + + # Obtain the steps to resolve + steps_to_resolve = self._get_steps_to_resolve(json_data) + + # Obtain optional recommendations + optional_steps_to_resolve = self._get_optional_steps_to_resolve(json_data) + + module_reports = self._get_module_pages() + pages_num = self._pages_num(json_data) + total_pages = pages_num + len(module_reports) + 1 + if len(steps_to_resolve) > 0: + total_pages += 1 + if (len(optional_steps_to_resolve) > 0 + and json_data['device']['test_pack'] == 'Pilot Assessment' + ): + total_pages += 1 + + return template.render(styles=styles, + logo=logo, + icon_qualification=icon_qualification, + icon_pilot=icon_pilot, + version=self._version, + json_data=json_data, + device=json_data['device'], + modules=self._device_modules(json_data['device']), + test_status=json_data['status'], + duration=duration, + successful_tests=successful_tests, + total_tests=self._total_tests, + test_results=json_data['tests']['results'], + steps_to_resolve=steps_to_resolve, + optional_steps_to_resolve=optional_steps_to_resolve, + module_reports=module_reports, + pages_num=pages_num, + total_pages=total_pages, + tests_first_page=TESTS_FIRST_PAGE, + tests_per_page=TESTS_PER_PAGE, + ) + + def _pages_num(self, json_data): # Calculate pages test_count = len(json_data['tests']['results']) @@ -208,136 +279,62 @@ def generate_pages(self, json_data): # Multiple pages required if test_count > TESTS_FIRST_PAGE: # First page - full_page = 1 + pages = 1 - # Remaining tests + # Remaining testsgenerate test_count -= TESTS_FIRST_PAGE - full_page += (int)(test_count / TESTS_PER_PAGE) - partial_page = 1 if test_count % TESTS_PER_PAGE > 0 else 0 + pages += (int)(test_count / TESTS_PER_PAGE) + pages = pages + 1 if test_count % TESTS_PER_PAGE > 0 else pages # 1 page required - elif test_count == TESTS_FIRST_PAGE: - full_page = 1 - partial_page = 0 - # Less than 1 page required else: - full_page = 0 - partial_page = 1 + pages = 1 - num_pages = full_page + partial_page - - pages = '' - for _ in range(num_pages): - self._cur_page += 1 - pages += self.generate_results_page(json_data=json_data, - page_num=self._cur_page) return pages - def generate_results_page(self, json_data, page_num): - page = '
' - page += self.generate_header(json_data, (page_num == 1)) - if page_num == 1: - page += self.generate_summary(json_data) - page += self.generate_results(json_data, page_num) - page += self.generate_footer(page_num) - page += '
' - page += '
' - return page - - def generate_module_page(self, json_data, module_report): - self._cur_page += 1 - page = '
' - page += self.generate_header(json_data, False) - page += f''' -
- {module_report} -
''' - page += self.generate_footer(self._cur_page) - page += '
' # Page end - page += '
' - return page - - def generate_steps_to_resolve(self, json_data): - - steps_so_far = 0 + def _device_modules(self, device): + sorted_modules = {} + + if 'test_modules' in device: + + for test_module in device['test_modules']: + if 'enabled' in device['test_modules'][test_module]: + sorted_modules[ + util.get_module_display_name(test_module)] = device['test_modules'][ + test_module]['enabled'] + + # Sort the modules by enabled first + sorted_modules = OrderedDict(sorted(sorted_modules.items(), + key=lambda x:x[1], + reverse=True) + ) + return sorted_modules + + def _get_steps_to_resolve(self, json_data): tests_with_recommendations = [] - index = 1 # Collect all tests with recommendations for test in json_data['tests']['results']: if 'recommendations' in test: tests_with_recommendations.append(test) - # Check if test has recommendations - if len(tests_with_recommendations) == 0: - return '' - - # Start new page - self._cur_page += 1 - page = '
' - page += self.generate_header(json_data, False) - - # Add title - page += '

Steps to Resolve

' - - for test in tests_with_recommendations: - - # Generate new page - if steps_so_far == 4 and ( - len(tests_with_recommendations) - (index-1) > 0): - - # Reset steps counter - steps_so_far = 0 - - # Render footer - page += self.generate_footer(self._cur_page) - page += '
' # Page end - page += '
' - - # Render new header - self._cur_page += 1 - page += '
' - page += self.generate_header(json_data, False) - - # Render test recommendations - page += f''' -
-
- {index}. -
- Name
{test["name"]} -
-
- Description
{test["description"]} -
-
-
- Steps to resolve - ''' - - step_number = 1 - for recommendation in test['recommendations']: - page += f''' -
{ - step_number}. {recommendation}''' - step_number += 1 - - page += '
' - - index += 1 - steps_so_far += 1 - - # Render final footer - page += self.generate_footer(self._cur_page) - page += '
' # Page end - page += '
' - - return page - - def generate_module_pages(self, json_data): - pages = '' + return tests_with_recommendations + + def _get_optional_steps_to_resolve(self, json_data): + tests_with_recommendations = [] + + # Collect all tests with recommendations + for test in json_data['tests']['results']: + if 'optional_recommendations' in test: + tests_with_recommendations.append(test) + + return tests_with_recommendations + + def _get_module_pages(self): content_max_size = 913 + reports = [] + for module_reports in self._module_reports: # ToDo: Figure out how to make this dynamic # Padding values from CSS @@ -391,8 +388,7 @@ def generate_module_pages(self, json_data): # If in the middle of a table, close the table if data_rows_active: page_content += '' - page = self.generate_module_page(json_data, page_content) - pages += page + '\n' + reports.append(page_content) content_size = 0 # If in the middle of a data table, restart # it for the rest of the rows @@ -400,727 +396,5 @@ def generate_module_pages(self, json_data): if data_rows_active else '') page_content += line + '\n' if len(page_content) > 0: - page = self.generate_module_page(json_data, page_content) - pages += page + '\n' - return pages - - def generate_body(self, json_data): - self._num_pages = 0 - self._cur_page = 0 - body = f''' - - {self.generate_pages(json_data)} - {self.generate_steps_to_resolve(json_data)} - {self.generate_module_pages(json_data)} - - ''' - # Set the max pages after all pages have been generated - return body.replace('MAX_PAGE', str(self._cur_page)) - - def generate_footer(self, page_num): - footer = f''' - - ''' - return footer - - def generate_results(self, json_data, page_num): - - successful_tests = 0 - for test in json_data['tests']['results']: - if test['result'] != 'Error': - successful_tests += 1 - - result_list = f''' -
-

Results List ({successful_tests}/{self._total_tests})

-
-
Name
-
Description
-
Result
-
''' - if page_num == 1: - start = 0 - elif page_num == 2: - start = TESTS_FIRST_PAGE - else: - start = (page_num - 2) * TESTS_PER_PAGE + TESTS_FIRST_PAGE - results_on_page = TESTS_FIRST_PAGE if page_num == 1 else TESTS_PER_PAGE - result_end = min(start + results_on_page, - len(json_data['tests']['results'])) - for ix in range(result_end - start): - result = json_data['tests']['results'][ix + start] - result_list += self.generate_result(result) - result_list += '
' - return result_list - - def generate_result(self, result): - if result['result'] == 'Non-Compliant': - result_class = 'result-test-result-non-compliant' - elif result['result'] == 'Compliant': - result_class = 'result-test-result-compliant' - elif result['result'] == 'Error': - result_class = 'result-test-result-error' - elif result['result'] == 'Feature Not Detected': - result_class = 'result-test-result-feature-not-detected' - elif result['result'] == 'Informational': - result_class = 'result-test-result-informational' - else: - result_class = 'result-test-result-skipped' - - result_html = f''' -
-
{result['name']}
-
{result['description']}
-
{result['result']}
-
- ''' - return result_html - - def generate_header(self, json_data, first_page): - with open(test_run_img_file, 'rb') as f: - tr_img_b64 = base64.b64encode(f.read()).decode('utf-8') - header = '' - - if first_page: - header += f''' -
-

Testrun report

-

- {json_data["device"]["manufacturer"]} - {json_data["device"]["model"]} -

''' - else: - header += f''' -
-

Testrun report

-

- {json_data["device"]["manufacturer"]} - {json_data["device"]["model"]} -

''' - header += f'''Testrun -
- ''' - return header - - def generate_summary(self, json_data): - # Generate the basic content section layout - summary = ''' -
- ''' - # Add the device information - manufacturer = (json_data['device']['manufacturer'] - if 'manufacturer' in json_data['device'] else 'Undefined') - model = (json_data['device']['model'] - if 'model' in json_data['device'] else 'Undefined') - fw = (json_data['device']['firmware'] - if 'firmware' in json_data['device'] else 'Undefined') - mac = (json_data['device']['mac_addr'] - if 'mac_addr' in json_data['device'] else 'Undefined') - - summary += '''
-
''' - - summary += self.generate_device_summary_label('Manufacturer', manufacturer) - summary += self.generate_device_summary_label('Model', model) - summary += self.generate_device_summary_label('Firmware', fw) - summary += self.generate_device_summary_label('MAC Address', - mac, - trailing_space=False) - - summary += '
' - - # Add device configuration - summary += ''' -
-
-

Device Configuration

-
- ''' - - if 'test_modules' in json_data['device']: - - sorted_modules = {} - - for test_module in json_data['device']['test_modules']: - if 'enabled' in json_data['device']['test_modules'][test_module]: - sorted_modules[test_module] = json_data['device']['test_modules'][ - test_module]['enabled'] - - # Sort the modules by enabled first - sorted_modules = sorted(sorted_modules.items(), - key=lambda x:x[1], - reverse=True) - - for module in sorted_modules: - summary += self.generate_device_module_label( - module[0], - module[1] - ) - - summary += '
' - - # Add device configuration - summary += ''' -
-
-

Device Configuration

-
- ''' - - if 'test_modules' in json_data['device']: - - sorted_modules = {} - - for test_module in json_data['device']['test_modules']: - if 'enabled' in json_data['device']['test_modules'][test_module]: - sorted_modules[test_module] = json_data['device']['test_modules'][ - test_module]['enabled'] - - # Sort the modules by enabled first - sorted_modules = sorted(sorted_modules.items(), - key=lambda x:x[1], - reverse=True) - - for module in sorted_modules: - summary += self.generate_device_module_label( - module[0], - module[1] - ) - - summary += '
' - - # Add the result summary - summary += self.generate_result_summary(json_data) - - summary += '\n
' - return summary - - def generate_device_module_label(self, module, enabled): - - # Do not render deleted modules - if module == 'nmap': - return '' - - label = '
' - if enabled: - label += '' - else: - label += '' - label += util.get_module_display_name(module) - label += '
' - return label - - def generate_result_summary(self, json_data): - if json_data['status'] == 'Compliant': - result_summary = '''
''' - else: - result_summary = '''
''' - result_summary += self.generate_result_summary_item('Test status', - 'Complete') - result_summary += self.generate_result_summary_item( - 'Test result', - json_data['status'], - style='color: white; font-size:24px; font-weight: 700;') - result_summary += self.generate_result_summary_item('Started', - json_data['started']) - - # Convert the timestamp strings to datetime objects - start_time = datetime.strptime(json_data['started'], '%Y-%m-%d %H:%M:%S') - end_time = datetime.strptime(json_data['finished'], '%Y-%m-%d %H:%M:%S') - # Calculate the duration - duration = end_time - start_time - result_summary += self.generate_result_summary_item('Duration', - str(duration)) - - result_summary += '\n
' - return result_summary - - def generate_result_summary_item(self, key, value, style=None): - summary_item = f'''
{key}
''' - if style is not None: - summary_item += f'''
{value}
''' - else: - summary_item += f'''
{value}
''' - return summary_item - - def generate_device_summary_label(self, key, value, trailing_space=True): - label = f''' -

{key}

-
{value}
- ''' - if trailing_space: - label += '''
''' - return label - - def generate_head(self): - return f''' - - - - Testrun Report - - - ''' - - def generate_css(self): - return ''' - /* Set some global variables */ - :root { - --header-height: .75in; - --header-width: 8.5in; - --header-pos-x: 0in; - --header-pos-y: 0in; - --page-width: 8.5in; - --summary-height: 2.8in; - --vertical-line-height: calc(var(--summary-height)-.2in); - --vertical-line-pos-x: 25%; - } - - @font-face { - font-family: 'Google Sans'; - font-style: normal; - src: url(https://fonts.gstatic.com/s/googlesans/v58/4Ua_rENHsxJlGDuGo1OIlJfC6l_24rlCK1Yo_Iqcsih3SAyH6cAwhX9RFD48TE63OOYKtrwEIJllpyk.woff2) format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; - } - - @font-face { - font-family: 'Roboto Mono'; - font-style: normal; - src: url(https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap) format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; - } - - /* Define some common body formatting*/ - body { - font-family: 'Google Sans', sans-serif; - margin: 0px; - padding: 0px; - } - - /* Use this for various section breaks*/ - .gradient-line { - position: relative; - background-image: linear-gradient(to right, red, blue, green, yellow, orange); - height: 1px; - /* Adjust the height as needed */ - width: 100%; - /* To span the entire width */ - display: block; - /* Ensures it's a block-level element */ - } - - /* Sets proper page size during print to pdf for weasyprint */ - @page { - size: Letter; - width: 8.5in; - height: 11in; - } - - .page { - position: relative; - margin: 0 20px; - width: 8.5in; - height: 11in; - } - - /* Define the header related css elements*/ - .header { - position: relative; - } - - h1 { - margin: 0 0 8px 0; - font-size: 20px; - font-weight: 400; - } - - h2 { - margin: 0px; - font-size: 48px; - font-weight: 700; - } - - h3 { - font-size: 24px; - } - - h4 { - font-size: 12px; - font-weight: 500; - color: #5F6368; - margin-bottom: 0; - margin-top: 0; - } - - .module-summary { - background-color: #F8F9FA; - width: 100%; - margin-bottom: 25px; - margin-top: 25px; - } - - .module-summary thead tr th { - text-align: left; - padding-top: 15px; - padding-left: 15px; - font-weight: 500; - color: #5F6368; - font-size: 14px; - } - - .module-summary tbody tr td { - padding-bottom: 15px; - padding-left: 15px; - font-size: 24px; - } - - .module-data { - border: 1px solid #DADCE0; - border-radius: 3px; - border-spacing: 0; - } - - .module-data thead tr th { - text-align: left; - padding: 12px 25px; - color: #3C4043; - font-size: 14px; - font-weight: 700; - } - - .module-data tbody tr td { - text-align: left; - padding: 12px 25px; - color: #3C4043; - font-size: 14px; - font-weight: 400; - border-top: 1px solid #DADCE0; - font-family: 'Roboto Mono', monospace; - } - - div.steps-to-resolve { - background-color: #F8F9FA; - margin-bottom: 30px; - width: 756px; - padding: 20px 30px; - vertical-align: top; - } - - .steps-to-resolve-row { - vertical-align: top; - } - - .steps-to-resolve-test-name { - display: inline-block; - margin-left: 70px; - margin-bottom: 20px; - width: 250px; - vertical-align: top; - } - - .steps-to-resolve-description { - display: inline-block; - } - - .steps-to-resolve.subtitle { - text-align: left; - padding-top: 15px; - font-weight: 500; - color: #5F6368; - font-size: 14px; - } - - .steps-to-resolve-index { - font-size: 40px; - position: absolute; - } - - .callout-container.info { - background-color: #e8f0fe; - } - - .callout-container.info .icon { - width: 22px; - height: 22px; - margin-right: 5px; - background-size: contain; - background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEwAAABOCAYAAACKX/AgAAAABHNCSVQICAgIfAhkiAAACYVJREFUeF7tXGtsVEUUPi0t0NIHli5Uni1I5KVYiCgPtQV8BcSIBkVUjFI0GiNGhR9KiIEfIqIkRlSqRlBQAVEREx9AqwIqClV5imILCBT6gHZLW2gLnm+xZHM5d2fm7t1tN9kv2R+dO3fmzHfncV7TmNKTZ89RFNoMxGrXjFb0MRAlzHAiRAmLEmbIgGH16AyLEmbIgGH16AyLEmbIgGH1OMP6rlVvZH1518E62nO4jkrKz9CBstNU4W2kU6fP8q/J10+Hdm34F0udkuOol6cdZXnaUr+uCTSwZwLFxca4JotJQzHh1PS9dU307Y4q2rjTS0XFp6j2zFkTWS/UTWwbS9m9O9CYgck09spUSm7fxlE7Tl4KC2F/H6un/PVlVLC7mhoa3bXE4uNiKHdACk0f66E+Xdo74cDonZASdryqgV7/5jit23aCQm2xtuElOn5IR3rsps7UOTXeiASTyiEhDEvv3cJyWrG5nM40uDujVINrFxdLk0d1oody0ik5wf2l6jphW/+uoZnLD1FV7fmNWzVA6Xnzfh7MrOzYIY7mT+lOw/okSV04LnOVsI+3VNDLX5QSTkAdJPEJOLJfCg3JSvAtI08y/1LjKC3p/OFdWdNIZVX88zYQlve24lrastdLNXyS6gAn6bMTMmjS8E461bXquEJYQ9M5mv/5Ufrk50plpyBjzKAUyuETbljvJIrjTdsEjXxobP2nhgp3eWnDzmoCqSrcdU0azbz9UopvY9aX1G7QhFXz0ntq2UHazmpCIECfmnpDOt1/fTq1j3fHwKhjteT978tpGf+gvwXCUFZDXrm/J6UkBrevBUUYZtaj+SUBycJXnchf+JExHrrk/6UWaGBOnmGWLdlQRp/8VBlwOwBpb0zLDGqmBUXYvDVHAi7DjI7xtGhqL7q8a+j1IxC990gdzXjvIB3j/c4OWJ7PTexq91hZ7nhtfLS5IiBZV2Um0vIn+oSNLIwUZhP6HMx922E177Mrf6ywe6wsd0TYz3/V0MJ1pbaNTxh6CeXnZV047WwrhuAB7M63uW/IYIcFa0tp6/4au8cBy40Jg1I6a8Uh270Cgr4wqZvx6RdQSsOHOHkhgx1pUHtmLf+XMBZTGBP2TkG5rVKKpTA7iP0Bpx4Mcv8fypwCstgtz5OnGn3WiCmMNn1sphMW7BPNHWzw2D+alU5TQVB/xOzdZCUogT0TW+YOcNKc7x24jKa8tl88CGBGrZ3Z18j2NJphi78+LpIF1QGnYTBkOWZE8SL2tEUP9hT9Z6cbz9Jidg6YQJuwv0rrad32E2Lbd16bFtbTUBQiQCFOT8goYd32k7Sf3U+60CYsnxVDyUSEBj99tEe3vxarN50VZ8hqRRMPagn76nRxcQvCmzhNCtn5JwHmTqg0eKk/p2XYLh5gs0wCHJveer0TU4uwr36vEj2lEAK2YaQAskr7LLzA6/+o0hqGVhCkYJc8u8ZekeqaIQ1pgzkNdUaLExeeklVsc1qxgb0fdwyT9zn/usoZBgO7iP1QEnIGJEvFrboMbiUJRf+cslXGjQjbeaiW6hsuVh7h/Luarf9IA3xwkN0KKMsI+6lw8ZuWNxA3lDCqf0qLmj+STDplMJtG9JNnGbwdKigJO1Amu0qyMxNUbbfa50OzZG9GcdkZpcxKwko4Ii0BplCkwi4Mh+i7CspTEraYBE+K+4SNnbeXai2u5kTeb9Y/308SwXEZgi0S7MbqX1dJWHOeg7WDdJtOrfVM/gZZVuPb5H3duohMSVDFBfCOcklK+Q+IG6YlBRdMkAQOVxmUVymXxW5y+MulJOycEGKMiQk+XBUuctzuR0mYncFaWaNne7ktsBvtIcokOxLUq0aDMLmRco5GRyoQTZcgTQ5rPSVhcMBJKKuOYMJsPrbdWP3HryQskzP/JByz+UpS3dZWhjwNCchyVEFJWC+PrLMUlcgGuarD1vB8e7FsAmWmt1WKpySsfzfZBNq0x0vwZEQakMyyea/srrIbq/8YlYRd0T1R9HnBQ7mNXSKRBmT+SOlSyJtFsrEKSsJg3SPsL6GAnW6RBqRJScjO6iBGlqx1lYThhdHspZSwnjOiJV+ZVLc1lEFW5JRJGD1IdvlY62oRdsvgVNH3BQXwgx/Mo8dWIcL1N3LJpAQ8ZGLfyO52HWgRhuTaHHYYSljK4XaE3Vs7TvDHXfqd/HGRtq6bQKxFGMhAHrxksGIDXbJRP67XUsS+xXFVyRuBMeXx2HShTVjfjPY0boicQrT6x0rad1Q/eqwrnFv1/jxST2ts8m/Hc7bRZQYXIrQJg/C4NID1bgX0sRnvHRD3B2vdcP+NPWvG0gOiztg2PoYe5zGZwCh7Bw0v+rKUlvLmKQHqBxLpTDOjm9uC89CqCuPzIJ7oBFBS8/KL6Tcbq+TBHA89eWsXo6aNJXko12ObiQzB5n56xEgA/8ogBgqk/88pWWh3Lufg2pGVytnUuC1iCmPCkLY9/94ehLs9Etb+eoLmrDpM+LotBfQ9Z+VhWst3nCTgwsNLU3pon4z+bRgThpev7ZtET4/LkGTxlYE0LAVJ57F9yaUH6HMa921HFrp55rYMGnaZsys1jghDp7gANTFALgKWwn2c+RfO0xOnIbINf7fZsyD3nZx2fvcI51dpjDd9/4mge7HhruFpvhwyXJgKBaCUQhfExYZAHpQhbC++mdeCFxsweN2rM8hnmMqb7H3XuXd1BrYhzB1o8JJS6v9xQNarD7Tw1ZlmgfBVX/zsKK3ZenEakXVGIcSFNKlczqLBVRbTC1PY0H9ht1Lhbi/B+NfZJ7EMZ7WWy1n+hHy4qYIWsp6GNEgd4K72qP7JlM36WxcOriKajgBxc8wTkSkEWxA/KD3ZQEUldbRpT7Xoz5L6w2n49PgMumek8z3L2m5Qe5i1Mfz9E98SwcUHLFWngMpyjgOimryL3UDPgvpzDZ/obsJ1wiAcyHq3oIxW8IVTty/FqwYPc2fyiHR6ODdCrjD7DwjLCHnwX3K6ejCzRUUSnkOPHs/Ogcdu7szLWw7c6LSjqhOSGWbtFDn+SO0u5P3HbQsAzoAc9mflcVo5PCqhRlgIax4E0teRkb2R3cRQbJ26t3GjN5uT4nIHphC8wbrOPzfIDCth/gJjpu34t9b3r2SQ5YjEvfP/SqbJp1Mh3wVGOP6dDCLSCCgjRopQ2KAeicbqiBtkoY0WI8ytAYS7Hce2ZLgFbS39RQkz/BJRwqKEGTJgWD06w6KEGTJgWD06w6KEGTJgWP0/nqir/+GPk3oAAAAASUVORK5CYII='); - } - - .callout-container { - display: flex; - box-sizing: border-box; - height: auto; - min-height: 48px; - padding: 6px 24px; - border-radius: 8px; - align-items: center; - gap: 10px; - color: #3c4043; - font-size: 14px; - } - - .device-information { - padding-top: 0.2in; - padding-left: 0.2in; - background-color: #F8F9FA; - width: 250px; - height: 100.4%; - } - - /* Define the summary related css elements*/ - .summary-content { - position: relative; - width: var(--page-width); - height: var(--summary-height); - margin-top: 19px; - margin-bottom: 19px; - background-color: #E8EAED; - padding-bottom: 20px; - } - - .summary-item-label { - position: relative; - } - - .summary-item-value { - position: relative; - font-size: 20px; - font-weight: 400; - color: #202124; - } - - .summary-item-space { - position: relative; - padding-bottom: 15px; - margin: 0; - } - - .summary-device-modules { - position: absolute; - left: 3.2in; - top: .3in; - } - - .summary-device-module-label { - font-size: 16px; - font-weight: 500; - color: #202124; - width: fit-content; - margin-bottom: 0.1in; - } - - .summary-vertical-line { - width: 1px; - height: var(--vertical-line-height); - background-color: #80868B; - position: absolute; - top: .3in; - bottom: .1in; - left: 3in; - } - - /* CSS for the color box */ - .summary-color-box { - position: absolute; - right: 0in; - top: 0in; - width: 2.6in; - height: 100%; - } - - .summary-box-compliant { - background-color: rgb(24, 128, 56); - } - - .summary-box-non-compliant { - background-color: #b31412; - } - - .summary-box-label { - font-size: 14px; - margin-top: 5px; - color: #DADCE0; - position: relative; - top: 10px; - left: 20px; - font-weight: 500; - } - - .summary-box-value { - font-size: 18px; - margin: 0 0 10px 0; - color: #ffffff; - position: relative; - top: 10px; - left: 20px; - } - - .result-list-title { - font-size: 24px; - } - - .result-list { - position: relative; - margin-top: .2in; - font-size: 18px; - } - - .result-line { - border: 1px solid #D3D3D3; - /* Light Gray border*/ - height: .4in; - width: 8.5in; - } - - .result-line-result { - border-top: 0px; - } - - .result-list-header-label { - font-weight: 500; - position: absolute; - font-size: 12px; - font-weight: bold; - height: 40px; - display: flex; - align-items: center; - } - - .result-test-label { - position: absolute; - font-size: 12px; - margin-top: 12px; - max-width: 300px; - font-weight: normal; - align-items: center; - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - } - - .result-test-description { - max-width: 380px; - } - - .result-test-result-error { - background-color: #FCE8E6; - color: #C5221F; - left: 7.3in; - } - - .result-test-result-feature-not-detected { - background-color: #e3e3e3; - left: 6.85in; - } - - .result-test-result-informational { - background-color: #d9f0ff; - color: #0b5c8d; - left: 7.08in; - } - - .result-test-result-non-compliant { - background-color: #FCE8E6; - color: #C5221F; - left: 7.01in; - } - - .result-test-result { - position: absolute; - font-size: 12px; - width: fit-content; - height: 12px; - margin-top: 8px; - padding: 4px 4px 7px 5px; - border-radius: 2px; - } - - .result-test-result-compliant { - background-color: #E6F4EA; - color: #137333; - left: 7.16in; - } - - .result-test-result-skipped { - background-color: #e3e3e3; - color: #393939; - left: 7.24in; - } - - /* CSS for the footer */ - .footer { - position: absolute; - height: 30px; - width: 8.5in; - bottom: 0in; - border-top: 1px solid #D3D3D3; - } - - .footer-label { - color: #3C4043; - position: absolute; - top: 5px; - font-size: 12px; - } - - /*CSS for the markdown tables */ - .markdown-table { - border-collapse: collapse; - margin-left: 20px; - background-color: #F8F9FA; - } - - .markdown-table th, .markdown-table td { - border: none; - text-align: left; - padding: 8px; - } - - .markdown-header-h1 { - margin-top:20px; - margin-bottom:20px; - margin-right:0px; - font-size: 2em; - } - - .markdown-header-h2 { - margin-top:20px; - margin-bottom:20px; - margin-right:0px; - font-size: 1.5em; - } - - .module-page-content { - /*Page height minus header(93px), footer(30px), - and a 20px bottom padding.*/ - height: calc(11in - 93px - 30px - 20px); - - /* In case we mess something up in our calculations - we'll cut off the content of the page so - the header, footer and line break work - as expected - */ - overflow: hidden; - } - - .module-page-content h1 { - font-size: 32px; - } - - @media print { - @page { - size: Letter; - width: 8.5in; - height: 11in; - } - }''' + reports.append(page_content) + return reports diff --git a/framework/python/src/common/util.py b/framework/python/src/common/util.py index 096aaf4df..ba1b23e81 100644 --- a/framework/python/src/common/util.py +++ b/framework/python/src/common/util.py @@ -12,18 +12,19 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Provides basic utilities for the network orchestrator.""" +"""Provides basic utilities for Testrun.""" import getpass import os import subprocess import shlex -from common import logger +import typing as t import netifaces +from common import logger LOGGER = logger.get_logger('util') -def run_command(cmd, output=True): +def run_command(cmd, output=True, timeout=None): """Runs a process at the os level By default, returns the standard output and error output If the caller sets optional output parameter to False, @@ -35,7 +36,7 @@ def run_command(cmd, output=True): with subprocess.Popen(shlex.split(cmd), stdout=subprocess.PIPE, stderr=subprocess.PIPE) as process: - stdout, stderr = process.communicate() + stdout, stderr = process.communicate(timeout) if process.returncode != 0 and output: err_msg = f'{stderr.strip()}. Code: {process.returncode}' @@ -51,12 +52,15 @@ def run_command(cmd, output=True): def interface_exists(interface): + """Checks whether an interface is available""" return interface in netifaces.interfaces() def prettify(mac_string): + """Formats a MAC address with colons""" return ':'.join([f'{ord(b):02x}' for b in mac_string]) def get_host_user(): + """Returns the username of the host user""" user = get_os_user() # If primary method failed, try secondary @@ -66,6 +70,7 @@ def get_host_user(): return user def get_os_user(): + """Attempts to get the username using os library""" user = None try: user = os.getlogin() @@ -78,6 +83,7 @@ def get_os_user(): return user def get_user(): + """Attempts to get the host user using the getpass library""" user = None try: user = getpass.getuser() @@ -96,9 +102,11 @@ def get_user(): return user def set_file_owner(path, owner): + """Change the owner of a file""" run_command(f'chown -R {owner} {path}') def get_module_display_name(search): + """Returns the display name of a test module""" modules = { 'ntp': 'NTP', 'dns': 'DNS', @@ -113,3 +121,32 @@ def get_module_display_name(search): return module[1] return 'Unknown' + + +def diff_dicts(d1: t.Dict[t.Any, t.Any], d2: t.Dict[t.Any, t.Any]) -> t.Dict: + """Compares two dictionaries by keys + + Args: + d1 (t.Dict[t.Any, t.Any]): first dict to compare + d2 (t.Dict[t.Any, t.Any]): second dict to compare + + Returns: + t.Dict[t.Any, t.Any]: Returns an empty dictionary + if the compared dictionaries are equal, + otherwise returns a dictionary that contains + the removed items(if available) + and the added items(if available). + """ + diff = {} + if d1 != d2: + s1 = set(d1) + s2 = set(d2) + keys_removed = s1 - s2 + keys_added = s2 - s1 + items_removed = {k:d1[k] for k in keys_removed} + items_added = {k:d2[k] for k in keys_added} + if items_removed: + diff['items_removed'] = items_removed + if items_added: + diff['items_added'] = items_added + return diff diff --git a/framework/python/src/core/docker/docker_module.py b/framework/python/src/core/docker/docker_module.py new file mode 100644 index 000000000..21dabdc16 --- /dev/null +++ b/framework/python/src/core/docker/docker_module.py @@ -0,0 +1,163 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Represents the base module.""" +import docker +from docker.models.containers import Container +import os +from common import logger +import json + +IMAGE_PREFIX = 'testrun/' +CONTAINER_PREFIX = 'tr-ct' +DEFAULT_NETWORK = 'bridge' + + +class Module: + """Represents the base module.""" + + def __init__(self, + module_config_file, + session, + docker_network=DEFAULT_NETWORK, + extra_hosts=None): + self._session = session + self.extra_hosts = extra_hosts + + # Read the config file into a json object + with open(module_config_file, encoding='UTF-8') as config_file: + module_json = json.load(config_file) + + self.docker_network = docker_network + # General module information + self.name = module_json['config']['meta']['name'] + self.display_name = module_json['config']['meta']['display_name'] + self.description = module_json['config']['meta']['description'] + self.enabled = module_json['config'].get('enabled', True) + self.depends_on = module_json['config']['docker'].get('depends_on', None) + + # Absolute path + # Store the root directory of Testrun based on the expected locatoin + # Testrun/modules///conf -> 5 levels + self.root_path = os.path.abspath( + os.path.join(module_config_file, '../../../../..')) + self.dir = os.path.dirname(os.path.dirname(module_config_file)) + self.dir_name = os.path.basename(self.dir) + + # Docker settings + self.build_file = self.dir_name + '.Dockerfile' + self.image_name = f'{IMAGE_PREFIX}{self.dir_name}' + self.container_name = f'{CONTAINER_PREFIX}-{self.dir_name}' + if 'tests' in module_json['config']: + # Append Test module + self.image_name += '-test' + self.container_name += '-test' + self.enable_container = module_json['config']['docker'].get( + 'enable_container', True) + self.container: Container = None + + self._add_logger(log_name=self.name, module_name=self.name) + self.setup_module(module_json) + + def _add_logger(self, log_name, module_name, log_dir=None): + self.logger = logger.get_logger( + name=f'{log_name}_module', # pylint: disable=E1123 + log_file=f'{module_name}_module', + log_dir=log_dir) + + def build(self): + self.logger.debug('Building module ' + self.dir_name) + client = docker.from_env() + client.images.build( + dockerfile=os.path.join(self.dir, self.build_file), + path=self._path, + forcerm=True, # Cleans up intermediate containers during build + tag=self.image_name) + + def get_container(self): + container = None + try: + client = docker.from_env() + container = client.containers.get(self.container_name) + except docker.errors.NotFound: + self.logger.debug('Container ' + self.container_name + ' not found') + except docker.errors.APIError as error: + self.logger.error('Failed to resolve container') + self.logger.error(error) + return container + + def get_session(self): + return self._session + + def get_status(self): + self.container = self.get_container() + if self.container is not None: + return self.container.status + return None + + def get_network(self): + return self.docker_network + + def get_mounts(self): + return [] + + def get_environment(self, device=None): # pylint: disable=W0613 + return {} + + def setup_module(self, module_json): + pass + + def _setup_runtime(self, device=None): + pass + + def start(self, device=None): + self._setup_runtime(device) + + self.logger.debug('Starting module ' + self.display_name) + network = self.get_network() + self.logger.debug(f"""Network: {network}, image name: {self.image_name}, + container name: {self.container_name}""") + + try: + client = docker.from_env() + self.container = client.containers.run( + self.image_name, + auto_remove=True, + cap_add=['NET_ADMIN'], + name=self.container_name, + hostname=self.container_name, + network_mode=network, + privileged=True, + detach=True, + mounts=self.get_mounts(), + environment=self.get_environment(device), + extra_hosts=self.extra_hosts if self.extra_hosts is not None else {}) + except docker.errors.ContainerError as error: + self.logger.error('Container run error') + self.logger.error(error) + + def stop(self, kill=False): + self.logger.debug('Stopping module ' + self.container_name) + try: + container = self.get_container() + if container is not None: + if kill: + self.logger.debug('Killing container: ' + self.container_name) + container.kill() + else: + self.logger.debug('Stopping container: ' + self.container_name) + container.stop() + self.logger.debug('Container stopped: ' + self.container_name) + except Exception as error: # pylint: disable=W0703 + self.logger.error('Container stop error') + self.logger.error(error) diff --git a/framework/python/src/core/docker/network_docker_module.py b/framework/python/src/core/docker/network_docker_module.py new file mode 100644 index 000000000..6c892092a --- /dev/null +++ b/framework/python/src/core/docker/network_docker_module.py @@ -0,0 +1,98 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Represents a test module.""" +from core.docker.docker_module import Module +import os +from docker.types import Mount + +RUNTIME_DIR = 'runtime' +RUNTIME_TEST_DIR = os.path.join(RUNTIME_DIR, 'test') +DEFAULT_TIMEOUT = 60 # time in seconds +DEFAULT_DOCKER_NETWORK = 'none' + + +class NetworkModule(Module): + """Represents a test module.""" + + def __init__(self, module_config_file, session): + super().__init__(module_config_file=module_config_file, + docker_network=DEFAULT_DOCKER_NETWORK, + session=session) + + def setup_module(self, module_json): + self.template = module_json['config']['docker'].get('template', False) + self.net_config = NetworkModuleNetConfig() + if self.enable_container: + self.net_config.enable_wan = module_json['config']['network'].get( + 'enable_wan', False) + self.net_config.host = module_json['config']['network'].get('host', False) + # Override default network if host is requested + if self.net_config.host: + self.docker_network = 'host' + + if not self.net_config.host: + self.net_config.ip_index = module_json['config']['network'].get( + 'ip_index') + + self.net_config.ipv4_address = self.get_session().get_ipv4_subnet()[ + self.net_config.ip_index] + self.net_config.ipv4_network = self.get_session().get_ipv4_subnet() + + self.net_config.ipv6_address = self.get_session().get_ipv6_subnet()[ + self.net_config.ip_index] + + self.net_config.ipv6_network = self.get_session().get_ipv6_subnet() + + self._mounts = [] + if 'mounts' in module_json['config']['docker']: + for mount_point in module_json['config']['docker']['mounts']: + self._mounts.append( + Mount(target=mount_point['target'], + source=os.path.join(os.getcwd(), mount_point['source']), + type='bind')) + + def _setup_runtime(self, device): + pass + + def get_environment(self, device=None): # pylint: disable=W0613 + environment = { + 'TZ': self.get_session().get_timezone(), + 'HOST_USER': self.get_session().get_host_user() + } + return environment + + def get_mounts(self): + return self._mounts + + +class NetworkModuleNetConfig: + """Define all the properties of the network config for a network module""" + + def __init__(self): + + self.enable_wan = False + + self.ip_index = 0 + self.ipv4_address = None + self.ipv4_network = None + self.ipv6_address = None + self.ipv6_network = None + + self.host = False + + def get_ipv4_addr_with_prefix(self): + return format(self.ipv4_address) + '/' + str(self.ipv4_network.prefixlen) + + def get_ipv6_addr_with_prefix(self): + return format(self.ipv6_address) + '/' + str(self.ipv6_network.prefixlen) diff --git a/framework/python/src/core/docker/test_docker_module.py b/framework/python/src/core/docker/test_docker_module.py new file mode 100644 index 000000000..3198ef1ba --- /dev/null +++ b/framework/python/src/core/docker/test_docker_module.py @@ -0,0 +1,157 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Represents a test module.""" +from core.docker.docker_module import Module +from test_orc.test_case import TestCase +import os +import json +from common import util +from docker.types import Mount + +RUNTIME_DIR = 'runtime' +RUNTIME_TEST_DIR = os.path.join(RUNTIME_DIR, 'test') +DEFAULT_TIMEOUT = 60 # time in seconds + + +class TestModule(Module): + """Represents a test module.""" + + def __init__(self, module_config_file, test_orc, session, extra_hosts): + super().__init__(module_config_file=module_config_file, + session=session, + extra_hosts=extra_hosts) + + self._test_orc = test_orc + + # Set IP Index for all test modules + self.ip_index = 9 + + def setup_module(self, module_json): + # Set the defaults + self.network = True + self.total_tests = 0 + self.tests: list = [] + + self.timeout = self._get_module_timeout(module_json) + + # Determine if this module needs network access + if 'network' in module_json['config']: + self.network = module_json['config']['network'] + + # Load test cases + if 'tests' in module_json['config']: + self.total_tests = len(module_json['config']['tests']) + for test_case_json in module_json['config']['tests']: + try: + test_case = TestCase( + name=test_case_json['name'], + description=test_case_json['test_description'], + expected_behavior=test_case_json['expected_behavior']) + + # Check if steps to resolve have been specified + if 'recommendations' in test_case_json: + test_case.recommendations = test_case_json['recommendations'] + + self.tests.append(test_case) + except Exception as error: # pylint: disable=W0718 + self.logger.error('Failed to load test case. See error for details') + self.logger.error(error) + + def _setup_runtime(self, device): + self.device_test_dir = os.path.join(self.root_path, RUNTIME_TEST_DIR, + device.mac_addr.replace(':', '')) + + self.container_runtime_dir = os.path.join(self.device_test_dir, self.name) + os.makedirs(self.container_runtime_dir, exist_ok=True) + + self.container_log_file = os.path.join(self.container_runtime_dir, + 'module.log') + + self.config_file = os.path.join(self.root_path, 'local/system.json') + self.root_certs_dir = os.path.join(self.root_path, 'local/root_certs') + + self.network_runtime_dir = os.path.join(self.root_path, 'runtime/network') + + self.device_startup_capture = os.path.join(self.device_test_dir, + 'startup.pcap') + host_user = self.get_session().get_host_user() + util.run_command(f'chown -R {host_user} {self.device_startup_capture}') + + self.device_monitor_capture = os.path.join(self.device_test_dir, + 'monitor.pcap') + util.run_command(f'chown -R {host_user} {self.device_monitor_capture}') + + def get_environment(self, device): + + # Obtain the test pack + test_pack = self._test_orc.get_test_pack(device.test_pack) + + environment = { + 'TZ': self.get_session().get_timezone(), + 'HOST_USER': self.get_session().get_host_user(), + 'DEVICE_MAC': device.mac_addr, + 'IPV4_ADDR': device.ip_addr, + 'DEVICE_TEST_MODULES': json.dumps(device.test_modules), + 'DEVICE_TEST_PACK': json.dumps(test_pack.to_dict()), + 'IPV4_SUBNET': self.get_session().get_ipv4_subnet(), + 'IPV6_SUBNET': self.get_session().get_ipv6_subnet(), + 'DEV_IFACE': self.get_session().get_device_interface(), + 'DEV_IFACE_MAC': self.get_session().get_device_interface_mac_addr() + } + return environment + + def get_mounts(self): + mounts = [ + Mount(target='/testrun/system.json', + source=self.config_file, + type='bind', + read_only=True), + Mount(target='/testrun/root_certs', + source=self.root_certs_dir, + type='bind', + read_only=True), + Mount(target='/runtime/output', + source=self.container_runtime_dir, + type='bind'), + Mount(target='/runtime/network', + source=self.network_runtime_dir, + type='bind', + read_only=True), + Mount(target='/runtime/device/startup.pcap', + source=self.device_startup_capture, + type='bind', + read_only=True), + Mount(target='/runtime/device/monitor.pcap', + source=self.device_monitor_capture, + type='bind', + read_only=True) + ] + return mounts + + def _get_module_timeout(self, module_json): + timeout = DEFAULT_TIMEOUT + try: + timeout = DEFAULT_TIMEOUT + test_modules = self.get_session().get_config().get('test_modules', {}) + test_config = test_modules.get(self.name, {}) + sys_timeout = test_config.get('timeout', None) + + if sys_timeout is not None: + timeout = sys_timeout + elif 'timeout' in module_json['config']['docker']: + timeout = module_json['config']['docker']['timeout'] + except Exception: # pylint: disable=W0718 + # Ignore errors, just use default + timeout = DEFAULT_TIMEOUT + return timeout # pylint: disable=W0150 diff --git a/framework/python/src/common/session.py b/framework/python/src/core/session.py similarity index 55% rename from framework/python/src/common/session.py rename to framework/python/src/core/session.py index f555a9732..f2e5466d3 100644 --- a/framework/python/src/common/session.py +++ b/framework/python/src/core/session.py @@ -17,8 +17,11 @@ import pytz import json import os -from common import util, logger +from fastapi.encoders import jsonable_encoder +from common import util, logger, mqtt from common.risk_profile import RiskProfile +from common.statuses import TestrunStatus, TestResult +from net_orc.ip_control import IPControl # Certificate dependencies from cryptography import x509 @@ -34,9 +37,13 @@ API_URL_KEY = 'api_url' API_PORT_KEY = 'api_port' MAX_DEVICE_REPORTS_KEY = 'max_device_reports' +ORG_NAME_KEY = 'org_name' +TEST_CONFIG_KEY = 'test_modules' CERTS_PATH = 'local/root_certs' CONFIG_FILE_PATH = 'local/system.json' -SECONDS_IN_YEAR = 31536000 +STATUS_TOPIC = 'status' + +MAKE_CONTROL_DIR = 'make/DEBIAN/control' PROFILE_FORMAT_PATH = 'resources/risk_assessment.json' PROFILES_DIR = 'local/risk_profiles' @@ -44,13 +51,41 @@ LOGGER = logger.get_logger('session') +def session_tracker(method): + """Session changes tracker.""" + def wrapper(self, *args, **kwargs): + + result = method(self, *args, **kwargs) + + if self.get_status() != TestrunStatus.IDLE: + self.get_mqtt_client().send_message( + STATUS_TOPIC, + jsonable_encoder(self.to_json()) + ) + + return result + return wrapper + +def apply_session_tracker(cls): + """Applies tracker decorator to class methods""" + for attr in dir(cls): + if (callable(getattr(cls, attr)) + and not attr.startswith('_') + and not attr.startswith('get') + and not attr == 'to_json' + ): + setattr(cls, attr, session_tracker(getattr(cls, attr))) + return cls + +@apply_session_tracker class TestrunSession(): - """Represents the current session of Test Run.""" + """Represents the current session of Testrun.""" def __init__(self, root_dir): self._root_dir = root_dir - self._status = 'Idle' + self._status = TestrunStatus.IDLE + self._description = None # Target test device self._device = None @@ -93,11 +128,20 @@ def __init__(self, root_dir): self._config_file = os.path.join(root_dir, CONFIG_FILE_PATH) self._config = self._get_default_config() + # System network interfaces + self._ifaces = {} # Loading methods self._load_version() self._load_config() self._load_profiles() + # Network information + self._ipv4_subnet = None + self._ipv6_subnet = None + + # Store host user for permissions use + self._host_user = util.get_host_user() + self._certs = [] self.load_certs() @@ -107,9 +151,12 @@ def __init__(self, root_dir): self._timezone = tz[0] LOGGER.debug(f'System timezone is {self._timezone}') + # MQTT client + self._mqtt_client = mqtt.MQTT() + def start(self): self.reset() - self._status = 'Waiting for Device' + self._status = TestrunStatus.WAITING_FOR_DEVICE self._started = datetime.datetime.now() def get_started(self): @@ -119,14 +166,14 @@ def get_finished(self): return self._finished def stop(self): - self.set_status('Stopping') + self.set_status(TestrunStatus.STOPPING) self.finish() def finish(self): # Set any in progress test results to Error for test_result in self._results: - if test_result.result == 'In Progress': - test_result.result = 'Error' + if test_result.result == TestResult.IN_PROGRESS: + test_result.result = TestResult.ERROR self._finished = datetime.datetime.now() @@ -141,7 +188,9 @@ def _get_default_config(self): 'monitor_period': 30, 'max_device_reports': 0, 'api_url': 'http://localhost', - 'api_port': 8000 + 'api_port': 8000, + 'org_name': '', + 'single_intf': False, } def get_config(self): @@ -161,7 +210,7 @@ def _load_config(self): # Network interfaces if (NETWORK_KEY in config_file_json and DEVICE_INTF_KEY in config_file_json.get(NETWORK_KEY) - and INTERNET_INTF_KEY in config_file_json.get(NETWORK_KEY)): + and INTERNET_INTF_KEY in config_file_json.get(NETWORK_KEY)): self._config[NETWORK_KEY][DEVICE_INTF_KEY] = config_file_json.get( NETWORK_KEY, {}).get(DEVICE_INTF_KEY) self._config[NETWORK_KEY][INTERNET_INTF_KEY] = config_file_json.get( @@ -188,13 +237,21 @@ def _load_config(self): self._config[MAX_DEVICE_REPORTS_KEY] = config_file_json.get( MAX_DEVICE_REPORTS_KEY) - LOGGER.debug(self._config) + if ORG_NAME_KEY in config_file_json: + self._config[ORG_NAME_KEY] = config_file_json.get( + ORG_NAME_KEY + ) + + if TEST_CONFIG_KEY in config_file_json: + self._config[TEST_CONFIG_KEY] = config_file_json.get( + TEST_CONFIG_KEY + ) def _load_version(self): version_cmd = util.run_command( 'dpkg-query --showformat=\'${Version}\' --show testrun') # index 1 of response is the stderr byte stream so if - # it has any data in it, there was an error and we + # it has any data in it, there was an error and wen # did not resolve the version and we'll use the fallback if len(version_cmd[1]) == 0: version = version_cmd[0] @@ -202,14 +259,37 @@ def _load_version(self): else: LOGGER.debug('Failed getting the version from dpkg-query') # Try getting the version from the make control file + + # Check if MAKE_CONTROL_DIR exists + if not os.path.exists(MAKE_CONTROL_DIR): + LOGGER.error('make/DEBIAN/control file path not found') + self._version = 'Unknown' + return + try: - version = util.run_command( - '$(grep -R "Version: " $MAKE_CONTROL_DIR | awk "{print $2}"') - except Exception as e: + # Run the grep command to find the version line + grep_cmd = util.run_command(f'grep -R "Version: " {MAKE_CONTROL_DIR}') + + if grep_cmd[0] and len(grep_cmd[1]) == 0: + # Extract the version number from grep + version = grep_cmd[0].split()[1] + self._version = version + LOGGER.debug(f'Testrun version is: {self._version}') + + else: + # Error handling if grep can't find the version line + self._version = 'Unknown' + LOGGER.debug(f'Testrun version is {self._version}') + raise Exception('Version line not found in make control file') + + except Exception as e: # pylint: disable=W0703 LOGGER.debug('Failed getting the version from make control file') LOGGER.error(e) self._version = 'Unknown' + def get_host_user(self): + return self._host_user + def get_version(self): return self._version @@ -225,11 +305,17 @@ def get_runtime_params(self): return self._runtime_params def add_runtime_param(self, param): + if param == 'single_intf': + self._config['single_intf'] = True self._runtime_params.append(param) def get_device_interface(self): return self._config.get(NETWORK_KEY, {}).get(DEVICE_INTF_KEY) + def get_device_interface_mac_addr(self): + iface = self.get_device_interface() + return IPControl.get_iface_mac_address(iface=iface) + def get_internet_interface(self): return self._config.get(NETWORK_KEY, {}).get(INTERNET_INTF_KEY) @@ -253,7 +339,7 @@ def set_config(self, config_json): self._save_config() # Update log level - LOGGER.debug(f'Setting log level to {config_json["log_level"]}') + LOGGER.debug(f'Setting log level to {config_json["log_level"]}') # pylint: disable=W1405 logger.set_log_level(config_json['log_level']) def set_target_device(self, device): @@ -291,12 +377,21 @@ def get_device(self, mac_addr): def remove_device(self, device): self._device_repository.remove(device) + def get_ipv4_subnet(self): + return self._ipv4_subnet + + def get_ipv6_subnet(self): + return self._ipv6_subnet + def get_status(self): return self._status def set_status(self, status): self._status = status + def set_description(self, desc: str): + self._description = desc + def get_test_results(self): return self._results @@ -322,16 +417,51 @@ def add_test_result(self, result): # result type is TestCase object if test_result.name == result.name: - # Just update the result and description - test_result.result = result.result - test_result.description = result.description - test_result.recommendations = result.recommendations + # Just update the result, description and recommendations + if len(result.description) != 0: + test_result.description = result.description + + # Add recommendations if provided + if result.recommendations is not None: + test_result.recommendations = result.recommendations + + if len(result.recommendations) == 0: + test_result.recommendations = None + + if result.result is not None: + + # Any informational test should always report informational + if test_result.required_result == 'Informational': + + # Set test result to informational + if result.result in [ + TestResult.NON_COMPLIANT, + TestResult.COMPLIANT, + TestResult.INFORMATIONAL + ]: + test_result.result = TestResult.INFORMATIONAL + else: + test_result.result = result.result + + # Copy any test recommendations to optional + test_result.optional_recommendations = result.recommendations + + # Remove recommendations from informational tests + test_result.recommendations = None + else: + test_result.result = result.result + updated = True if not updated: - result.result = 'In Progress' self._results.append(result) + def set_test_result_error(self, result): + """Set test result error""" + result.result = TestResult.ERROR + result.recommendations = None + self._results.append(result) + def add_module_report(self, module_report): self._module_reports.append(module_report) @@ -357,15 +487,18 @@ def get_report_url(self): def set_report_url(self, url): self._report_url = url + def set_subnets(self, ipv4_subnet, ipv6_subnet): + self._ipv4_subnet = ipv4_subnet + self._ipv6_subnet = ipv6_subnet + def _load_profiles(self): # Load format of questionnaire LOGGER.debug('Loading risk assessment format') try: - with open(os.path.join( - self._root_dir, PROFILE_FORMAT_PATH - ), encoding='utf-8') as profile_format_file: + with open(os.path.join(self._root_dir, PROFILE_FORMAT_PATH), + encoding='utf-8') as profile_format_file: format_json = json.load(profile_format_file) # Save original profile format for internal validation self._profile_format = format_json @@ -374,6 +507,10 @@ def _load_profiles(self): 'An error occurred whilst loading the risk assessment format') LOGGER.debug(e) + # If the format JSON fails to load, skip loading profiles + LOGGER.error('Profiles will not be loaded') + return + profile_format_array = [] # Remove internal properties @@ -398,21 +535,39 @@ def _load_profiles(self): try: for risk_profile_file in os.listdir( - os.path.join(self._root_dir, PROFILES_DIR)): + os.path.join(self._root_dir, PROFILES_DIR)): + LOGGER.debug(f'Discovered profile {risk_profile_file}') + # Open the risk profile file with open(os.path.join(self._root_dir, PROFILES_DIR, risk_profile_file), encoding='utf-8') as f: - json_data = json.load(f) - risk_profile = RiskProfile() - risk_profile.load( - profile_json=json_data, - profile_format=self._profile_format - ) - risk_profile.status = self.check_profile_status(risk_profile) + + # Parse risk profile json + json_data: dict = json.load(f) + + # Validate profile JSON + if not self.validate_profile_json(json_data): + LOGGER.error('Profile failed validation') + continue + + # Instantiate a new risk profile + risk_profile: RiskProfile = RiskProfile() + + # Assign the profile questions + questions: list[dict] = json_data.get('questions') + + # Pass only the valid questions to the risk profile + json_data['questions'] = self._remove_invalid_questions(questions) + + # Pass JSON to populate risk profile + risk_profile.load(profile_json=json_data, + profile_format=self._profile_format) + + # Add risk profile to session self._profiles.append(risk_profile) - except Exception as e: + except Exception as e: # pylint: disable=W0703 LOGGER.error('An error occurred whilst loading risk profiles') LOGGER.debug(e) @@ -428,25 +583,6 @@ def get_profile(self, name): return profile return None - def validate_profile(self, profile_json): - - # Check name field is present - if 'name' not in profile_json: - return False - - # Check questions field is present - if 'questions' not in profile_json: - return False - - # Check all questions are present - for format_q in self.get_profiles_format(): - if self._get_profile_question(profile_json, - format_q.get('question')) is None: - LOGGER.error('Missing question: ' + format_q.get('question')) - return False - - return True - def _get_profile_question(self, profile_json, question): for q in profile_json.get('questions'): @@ -455,7 +591,14 @@ def _get_profile_question(self, profile_json, question): return None + def get_profile_format_question(self, question): + for q in self.get_profiles_format(): + if q.get('question') == question: + return q + def update_profile(self, profile_json): + """Update the risk profile with the provided JSON. + The content has already been validated in the API""" profile_name = profile_json['name'] @@ -463,45 +606,19 @@ def update_profile(self, profile_json): profile_json['version'] = self.get_version() profile_json['created'] = datetime.datetime.now().strftime('%Y-%m-%d') - if 'status' in profile_json and profile_json.get('status') == 'Valid': - # Attempting to submit a risk profile, we need to check it - - # Check all questions have been answered - all_questions_answered = True - - for question in self.get_profiles_format(): - - # Check question is present - profile_question = self._get_profile_question(profile_json, - question.get('question')) - - if profile_question is not None: - - # Check answer is present - if 'answer' not in profile_question: - LOGGER.error('Missing answer for question: ' + - question.get('question')) - all_questions_answered = False + # Assign the profile questions + questions: list[dict] = profile_json.get('questions') - else: - LOGGER.error('Missing question: ' + question.get('question')) - all_questions_answered = False - - if not all_questions_answered: - LOGGER.error('Not all questions answered') - return None - - else: - profile_json['status'] = 'Draft' + # Pass only the valid questions to the risk profile + profile_json['questions'] = self._remove_invalid_questions(questions) + # Check if profile already exists risk_profile = self.get_profile(profile_name) - if risk_profile is None: # Create a new risk profile - risk_profile = RiskProfile( - profile_json=profile_json, - profile_format=self._profile_format) + risk_profile = RiskProfile(profile_json=profile_json, + profile_format=self._profile_format) self._profiles.append(risk_profile) else: @@ -524,19 +641,128 @@ def update_profile(self, profile_json): return risk_profile - def check_profile_status(self, profile): + def _remove_invalid_questions(self, questions): + """Remove unrecognised questions from the profile""" + + # Store valid questions + valid_questions = [] + + # Remove any additional (outdated questions from the profile) + for question in questions: - if profile.status == 'Valid': + # Check if question exists in the profile format + if self.get_profile_format_question( + question=question['question']) is not None: - # Check expiry - created_date = profile.created.timestamp() + # Add the question to the valid_questions + valid_questions.append(question) - today = datetime.datetime.now().timestamp() + else: + LOGGER.debug(f'Removed unrecognised question: {question["question"]}') - if created_date < (today - SECONDS_IN_YEAR): - profile.status = 'Expired' + # Return the list of valid questions + return valid_questions - return profile.status + def validate_profile_json(self, profile_json): + """Validate properties in profile update requests""" + + # Get the status field + valid = False + if 'status' in profile_json and profile_json.get('status') == 'Valid': + valid = True + + # Check if 'name' exists in profile + if 'name' not in profile_json: + LOGGER.error('Missing "name" in profile') + return False + + # Check if 'name' field not empty + elif len(profile_json.get('name').strip()) == 0: + LOGGER.error('Name field left empty') + return False + + # Error handling if 'questions' not in request + if 'questions' not in profile_json and valid: + LOGGER.error('Missing "questions" field in profile') + return False + + # Validating the questions section + for question in profile_json.get('questions'): + + # Check if the question field is present + if 'question' not in question: + LOGGER.error('The "question" field is missing') + return False + + # Check if 'question' field not empty + elif len(question.get('question').strip()) == 0: + LOGGER.error('A question is missing from "question" field') + return False + + # Check if question is a recognised question + format_q = self.get_profile_format_question( + question.get('question')) + + if format_q is None: + LOGGER.error(f'Unrecognised question: {question.get("question")}') + # Just ignore additional questions + continue + + # Error handling if 'answer' is missing + if 'answer' not in question and valid: + LOGGER.error('The answer field is missing') + return False + + # If answer is present, check the validation rules + else: + + # Extract the answer out of the profile + answer = question.get('answer') + + # Get the validation rules + field_type = format_q.get('type') + + # Check if type is string or single select, answer should be a string + if ((field_type in ['string', 'select']) + and not isinstance(answer, str)): + LOGGER.error(f'''Answer for question \ +{question.get('question')} is incorrect data type''') + return False + + # Check if type is select, answer must be from list + if field_type == 'select' and valid: + possible_answers = format_q.get('options') + if answer not in possible_answers: + LOGGER.error(f'''Answer for question \ +{question.get('question')} is not valid''') + return False + + # Validate select multiple field types + if field_type == 'select-multiple': + + if not isinstance(answer, list): + LOGGER.error(f'''Answer for question \ +{question.get('question')} is incorrect data type''') + return False + + question_options_len = len(format_q.get('options')) + + # We know it is a list, now check the indexes + for index in answer: + + # Check if the index is an integer + if not isinstance(index, int): + LOGGER.error(f'''Answer for question \ +{question.get('question')} is incorrect data type''') + return False + + # Check if index is 0 or above and less than the num of options + if index < 0 or index >= question_options_len: + LOGGER.error(f'''Invalid index provided as answer for \ +question {question.get('question')}''') + return False + + return True def delete_profile(self, profile): @@ -551,13 +777,14 @@ def delete_profile(self, profile): return True - except Exception as e: + except Exception as e: # pylint: disable=W0703 LOGGER.error('An error occurred whilst deleting a profile') LOGGER.debug(e) return False def reset(self): - self.set_status('Idle') + self.set_status(TestrunStatus.IDLE) + self.set_description(None) self.set_target_device(None) self._report_url = None self._total_tests = 0 @@ -565,6 +792,7 @@ def reset(self): self._results = [] self._started = None self._finished = None + self._ifaces = IPControl.get_sys_interfaces() def to_json(self): @@ -589,6 +817,9 @@ def to_json(self): if self._report_url is not None: session_json['report'] = self.get_report_url() + if self._description is not None: + session_json['description'] = self._description + return session_json def get_timezone(self): @@ -601,17 +832,30 @@ def upload_cert(self, filename, content): # Parse bytes into x509 object cert = x509.load_pem_x509_certificate(content, default_backend()) - # Extract required properties - common_name = cert.subject.get_attributes_for_oid( - NameOID.COMMON_NAME)[0].value + # Retrieve the common name attributes from the subject + common_name_attr = cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME) + + # Raise an error if the common name attribute is missing + if not common_name_attr: + raise ValueError('Certificate is missing the common name') + + # Extract the organization name value + common_name = common_name_attr[0].value # Check if any existing certificates have the same common name for cur_cert in self._certs: if common_name == cur_cert['name']: raise ValueError('A certificate with that name already exists') - issuer = cert.issuer.get_attributes_for_oid( - NameOID.ORGANIZATION_NAME)[0].value + # Retrieve the organization name attributes from issuer + issuer_attr = cert.issuer.get_attributes_for_oid(NameOID.ORGANIZATION_NAME) + + # Raise an error if the organization name attribute is missing + if not issuer_attr: + raise ValueError('Certificate is missing the organization name') + + # Extract the organization name value + issuer = issuer_attr[0].value status = 'Valid' if now > cert.not_valid_after_utc: @@ -650,6 +894,11 @@ def load_certs(self): self._certs = [] for cert_file in os.listdir(CERTS_PATH): + + # Ignore directories + if os.path.isdir(os.path.join(CERTS_PATH, cert_file)): + continue + LOGGER.debug(f'Loading certificate {cert_file}') try: @@ -685,7 +934,7 @@ def load_certs(self): self._certs.append(cert_obj) LOGGER.debug(f'Successfully loaded {cert_file}') - except Exception as e: + except Exception as e: # pylint: disable=W0703 LOGGER.error(f'An error occurred whilst loading {cert_file}') LOGGER.debug(e) @@ -705,10 +954,32 @@ def delete_cert(self, filename): self._certs.remove(cert) return True - except Exception as e: + except Exception as e: # pylint: disable=W0703 LOGGER.error('An error occurred whilst deleting the certificate') LOGGER.debug(e) return False def get_certs(self): return self._certs + + def detect_network_adapters_change(self) -> dict: + adapters = {} + ifaces_new = IPControl.get_sys_interfaces() + + # Difference between stored and newly received network interfaces + diff = util.diff_dicts(self._ifaces, ifaces_new) + if diff: + if 'items_added' in diff: + adapters['adapters_added'] = diff['items_added'] + if 'items_removed' in diff: + adapters['adapters_removed'] = diff['items_removed'] + # Save new network interfaces to session + LOGGER.debug(f'Network adapters change detected: {adapters}') + self._ifaces = ifaces_new + return adapters + + def get_mqtt_client(self): + return self._mqtt_client + + def get_ifaces(self): + return self._ifaces diff --git a/framework/python/src/core/tasks.py b/framework/python/src/core/tasks.py new file mode 100644 index 000000000..5da0b40c9 --- /dev/null +++ b/framework/python/src/core/tasks.py @@ -0,0 +1,78 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Periodic background tasks""" + +from contextlib import asynccontextmanager +import datetime +import logging + +from apscheduler.schedulers.asyncio import AsyncIOScheduler +from fastapi import FastAPI + +from common import logger + +# Check adapters period seconds +# Check adapters period seconds +CHECK_NETWORK_ADAPTERS_PERIOD = 5 +CHECK_INTERNET_PERIOD = 2 +INTERNET_CONNECTION_TOPIC = 'events/internet' +NETWORK_ADAPTERS_TOPIC = 'events/adapter' + +LOGGER = logger.get_logger('tasks') + + +class PeriodicTasks: + """Background periodic tasks + """ + def __init__( + self, testrun_obj, + ) -> None: + self._testrun = testrun_obj + self._mqtt_client = self._testrun.get_mqtt_client() + local_tz = datetime.datetime.now().astimezone().tzinfo + self._scheduler = AsyncIOScheduler(timezone=local_tz) + # Prevent scheduler warnings + self._scheduler._logger.setLevel(logging.ERROR) + + self.adapters_checker_job = self._scheduler.add_job( + func=self._testrun.get_net_orc().network_adapters_checker, + kwargs={ + 'mqtt_client': self._mqtt_client, + 'topic': NETWORK_ADAPTERS_TOPIC + }, + trigger='interval', + seconds=CHECK_NETWORK_ADAPTERS_PERIOD, + ) + # add internet connection cheking job only in single-intf mode + if 'single_intf' not in self._testrun.get_session().get_runtime_params(): + self.internet_shecker = self._scheduler.add_job( + func=self._testrun.get_net_orc().internet_conn_checker, + kwargs={ + 'mqtt_client': self._mqtt_client, + 'topic': INTERNET_CONNECTION_TOPIC + }, + trigger='interval', + seconds=CHECK_INTERNET_PERIOD, + ) + + @asynccontextmanager + async def start(self, app: FastAPI): # pylint: disable=unused-argument + """Start background tasks + + Args: + app (FastAPI): app instance + """ + # Job that checks for changes in network adapters + self._scheduler.start() + yield diff --git a/framework/python/src/core/test_runner.py b/framework/python/src/core/test_runner.py index 870e97752..0a8f69e43 100644 --- a/framework/python/src/core/test_runner.py +++ b/framework/python/src/core/test_runner.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Wrapper for the Testrun that simplifies virtual testing procedure by allowing direct calling from the command line. @@ -25,6 +24,7 @@ from testrun import Testrun from common import logger import signal +import io LOGGER = logger.get_logger("runner") @@ -37,13 +37,17 @@ def __init__(self, validate=False, net_only=False, single_intf=False, - no_ui=False): + no_ui=False, + target=None, + firmware=None): self._register_exits() - self.test_run = Testrun(config_file=config_file, + self._testrun = Testrun(config_file=config_file, validate=validate, net_only=net_only, single_intf=single_intf, - no_ui=no_ui) + no_ui=no_ui, + target_mac=target, + firmware=firmware) def _register_exits(self): signal.signal(signal.SIGINT, self._exit_handler) @@ -62,7 +66,7 @@ def _exit_handler(self, signum, arg): # pylint: disable=unused-argument sys.exit(1) def stop(self): - self.test_run.stop() + self._testrun.stop() def parse_args(): @@ -73,8 +77,7 @@ def parse_args(): "-f", "--config-file", default=None, - help="Define the configuration file for Testrun and Network Orchestrator" - ) + help="Define the configuration file for Testrun and Network Orchestrator") parser.add_argument( "--validate", default=False, @@ -91,7 +94,38 @@ def parse_args(): default=False, action="store_true", help="Do not launch the user interface") + parser.add_argument("--target", + default=None, + type=str, + help="MAC address of the target device") + parser.add_argument("-fw", + "--firmware", + default=None, + type=str, + help="Firmware version to be tested") + parsed_args = parser.parse_known_args()[0] + + if (parsed_args.no_ui and not parsed_args.net_only + and (parsed_args.target is None or parsed_args.firmware is None)): + # Capture help text + help_text = io.StringIO() + parser.print_help(file=help_text) + + # Get help text as lines and find where "Testrun" starts (skip usage) + help_lines = help_text.getvalue().splitlines() + start_index = next( + (i for i, line in enumerate(help_lines) if "Testrun" in line), 0) + + # Join only lines starting from "Testrun" and print without extra newlines + help_message = "\n".join(line.rstrip() for line in help_lines[start_index:]) + print(help_message) + + print( + "Error: --target and --firmware are required when --no-ui is specified", + file=sys.stderr) + sys.exit(1) + return parsed_args @@ -101,4 +135,6 @@ def parse_args(): validate=args.validate, net_only=args.net_only, single_intf=args.single_intf, - no_ui=args.no_ui) + no_ui=args.no_ui, + target=args.target, + firmware=args.firmware) diff --git a/framework/python/src/core/testrun.py b/framework/python/src/core/testrun.py index 5b43cfd65..5d4e78e9c 100644 --- a/framework/python/src/core/testrun.py +++ b/framework/python/src/core/testrun.py @@ -11,14 +11,9 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - -"""The overall control of the Test Run application. - +"""The overall control of the Testrun application. This file provides the integration between all of the -Test Run components, such as net_orc, test_orc and test_ui. - -Run using the provided command scripts in the cmd folder. -E.g sudo cmd/start +Testrun components, such as net_orc, test_orc and test_ui. """ import docker import json @@ -27,10 +22,11 @@ import signal import sys import time -from common import logger, util +from common import logger, util, mqtt from common.device import Device -from common.session import TestrunSession from common.testreport import TestReport +from common.statuses import TestrunStatus +from session import TestrunSession from api.api import Api from net_orc.listener import NetworkEvent from net_orc import network_orchestrator as net_orc @@ -38,14 +34,7 @@ from docker.errors import ImageNotFound -# Locate parent directory -current_dir = os.path.dirname(os.path.realpath(__file__)) - -# Locate the test-run root directory, 4 levels, src->python->framework->test-run -root_dir = os.path.dirname(os.path.dirname( - os.path.dirname(os.path.dirname(current_dir)))) - -LOGGER = logger.get_logger('test_run') +LOGGER = logger.get_logger('testrun') DEFAULT_CONFIG_FILE = 'local/system.json' EXAMPLE_CONFIG_FILE = 'local/system.json.example' @@ -58,10 +47,16 @@ DEVICE_MODEL = 'model' DEVICE_MAC_ADDR = 'mac_addr' DEVICE_TEST_MODULES = 'test_modules' +DEVICE_TYPE_KEY = 'type' +DEVICE_TECHNOLOGY_KEY = 'technology' +DEVICE_TEST_PACK_KEY = 'test_pack' +DEVICE_ADDITIONAL_INFO_KEY = 'additional_info' + MAX_DEVICE_REPORTS_KEY = 'max_device_reports' + class Testrun: # pylint: disable=too-few-public-methods - """Test Run controller. + """Testrun controller. Creates an instance of the network orchestrator, test orchestrator and user interface. @@ -72,8 +67,19 @@ def __init__(self, validate=False, net_only=False, single_intf=False, - no_ui=False): + no_ui=False, + target_mac=None, + firmware=None): + + # Locate parent directory + current_dir = os.path.dirname(os.path.realpath(__file__)) + # Locate the test-run root directory, 4 levels, + # src->python->framework->test-run + self._root_dir = os.path.dirname( + os.path.dirname(os.path.dirname(os.path.dirname(current_dir)))) + + # Determine config file if config_file is None: self._config_file = self._get_config_abs(DEFAULT_CONFIG_FILE) else: @@ -81,13 +87,15 @@ def __init__(self, self._net_only = net_only self._single_intf = single_intf - self._no_ui = no_ui + # Network only option only works if UI is also + # disbled so need to set no_ui if net_only is selected + self._no_ui = no_ui or net_only # Catch any exit signals self._register_exits() # Create session - self._session = TestrunSession(root_dir=root_dir) + self._session = TestrunSession(root_dir=self._root_dir) # Register runtime parameters if single_intf: @@ -97,18 +105,35 @@ def __init__(self, if validate: self._session.add_runtime_param('validate') - self._net_orc = net_orc.NetworkOrchestrator( - session=self._session) - self._test_orc = test_orc.TestOrchestrator( - self._session, - self._net_orc) + self._net_orc = net_orc.NetworkOrchestrator(session=self._session) + self._test_orc = test_orc.TestOrchestrator(self._session, self._net_orc) # Load device repository self.load_all_devices() + # If no_ui selected and not network only mode, + # load the target device into the session + if self._no_ui and not net_only: + target_device = self._session.get_device(target_mac) + if target_device is not None: + target_device.firmware = firmware + self._session.set_target_device(target_device) + else: + print( + f'Target device specified does not exist in device registry: ' + f'{target_mac}', + file=sys.stderr) + sys.exit(1) + # Load test modules self._test_orc.start() + # Start websockets server + self.start_ws() + + # Init MQTT client + self._mqtt_client = mqtt.MQTT() + if self._no_ui: # Check Testrun is able to start @@ -131,6 +156,9 @@ def __init__(self, while True: time.sleep(1) + def get_root_dir(self): + return self._root_dir + def get_version(self): return self.get_session().get_version() @@ -150,25 +178,33 @@ def _load_devices(self, device_dir): for device_folder in os.listdir(device_dir): - device_config_file_path = os.path.join(device_dir, - device_folder, + device_config_file_path = os.path.join(device_dir, device_folder, DEVICE_CONFIG) # Check if device config file exists before loading if not os.path.exists(device_config_file_path): LOGGER.error('Device configuration file missing ' + - f'from device {device_folder}') + f'for device {device_folder}') continue # Open device config file with open(device_config_file_path, encoding='utf-8') as device_config_file: - device_config_json = json.load(device_config_file) + + try: + device_config_json = json.load(device_config_file) + except json.decoder.JSONDecodeError as e: + LOGGER.error('Invalid JSON found in ' + + f'device configuration {device_config_file_path}') + LOGGER.debug(e) + continue device_manufacturer = device_config_json.get(DEVICE_MANUFACTURER) device_model = device_config_json.get(DEVICE_MODEL) mac_addr = device_config_json.get(DEVICE_MAC_ADDR) test_modules = device_config_json.get(DEVICE_TEST_MODULES) + + # Load max device reports max_device_reports = None if 'max_device_reports' in device_config_json: max_device_reports = device_config_json.get(MAX_DEVICE_REPORTS_KEY) @@ -183,6 +219,25 @@ def _load_devices(self, device_dir): max_device_reports=max_device_reports, device_folder=device_folder) + # Load in the additional fields + if DEVICE_TYPE_KEY in device_config_json: + device.type = device_config_json.get(DEVICE_TYPE_KEY) + + if DEVICE_TECHNOLOGY_KEY in device_config_json: + device.technology = device_config_json.get(DEVICE_TECHNOLOGY_KEY) + + if DEVICE_TEST_PACK_KEY in device_config_json: + device.test_pack = device_config_json.get(DEVICE_TEST_PACK_KEY) + + if DEVICE_ADDITIONAL_INFO_KEY in device_config_json: + device.additional_info = device_config_json.get( + DEVICE_ADDITIONAL_INFO_KEY) + + if None in [device.type, device.technology, device.test_pack]: + LOGGER.warning( + 'Device is outdated and requires further configuration') + device.status = 'Invalid' + self._load_test_reports(device) # Add device to device repository @@ -192,37 +247,35 @@ def _load_devices(self, device_dir): def _load_test_reports(self, device): - LOGGER.debug(f'Loading test reports for device {device.model}') + LOGGER.debug('Loading test reports for device ' + + f'{device.manufacturer} {device.model}') # Remove the existing reports in memory device.clear_reports() # Locate reports folder - reports_folder = os.path.join(root_dir, - LOCAL_DEVICES_DIR, - device.device_folder, 'reports') + reports_folder = self.get_reports_folder(device) # Check if reports folder exists (device may have no reports) if not os.path.exists(reports_folder): return - LOGGER.info(f'Loading reports from {reports_folder}') - for report_folder in os.listdir(reports_folder): # 1.3 file path - report_json_file_path = os.path.join( - reports_folder, - report_folder, - 'test', - device.mac_addr.replace(':',''), - 'report.json') - + report_json_file_path = os.path.join(reports_folder, report_folder, + 'test', + device.mac_addr.replace(':', ''), + 'report.json') + if not os.path.isfile(report_json_file_path): # Revert to pre 1.3 file path - report_json_file_path = os.path.join( - reports_folder, - report_folder, - 'report.json') + report_json_file_path = os.path.join(reports_folder, report_folder, + 'report.json') + + if not os.path.isfile(report_json_file_path): + # Revert to pre 1.3 file path + report_json_file_path = os.path.join(reports_folder, report_folder, + 'report.json') # Check if the report.json file exists if not os.path.isfile(report_json_file_path): @@ -236,18 +289,17 @@ def _load_test_reports(self, device): test_report.set_mac_addr(device.mac_addr) device.add_report(test_report) + def get_reports_folder(self, device): + """Return the reports folder path for the device""" + return os.path.join(self._root_dir, LOCAL_DEVICES_DIR, device.device_folder, + 'reports') + def delete_report(self, device: Device, timestamp): LOGGER.debug(f'Deleting test report for device {device.model} ' + f'at {timestamp}') # Locate reports folder - reports_folder = os.path.join(root_dir, - LOCAL_DEVICES_DIR, - device.device_folder, 'reports') - - # Check if reports folder exists (device may have no reports) - if not os.path.exists(reports_folder): - return False + reports_folder = self.get_reports_folder(device) for report_folder in os.listdir(reports_folder): if report_folder == timestamp: @@ -261,15 +313,13 @@ def delete_report(self, device: Device, timestamp): def create_device(self, device: Device): # Define the device folder location - device_folder_path = os.path.join(root_dir, - LOCAL_DEVICES_DIR, + device_folder_path = os.path.join(self._root_dir, LOCAL_DEVICES_DIR, device.device_folder) # Create the directory os.makedirs(device_folder_path) - config_file_path = os.path.join(device_folder_path, - DEVICE_CONFIG) + config_file_path = os.path.join(device_folder_path, DEVICE_CONFIG) with open(config_file_path, 'w', encoding='utf-8') as config_file: config_file.writelines(json.dumps(device.to_config_json(), indent=4)) @@ -282,23 +332,12 @@ def create_device(self, device: Device): return device.to_config_json() - def save_device(self, device: Device, device_json): + def save_device(self, device: Device): """Edit and save an existing device config.""" - # Update device properties - device.manufacturer = device_json['manufacturer'] - device.model = device_json['model'] - - if 'test_modules' in device_json: - device.test_modules = device_json['test_modules'] - else: - device.test_modules = {} - # Obtain the config file path - config_file_path = os.path.join(root_dir, - LOCAL_DEVICES_DIR, - device.device_folder, - DEVICE_CONFIG) + config_file_path = os.path.join(self._root_dir, LOCAL_DEVICES_DIR, + device.device_folder, DEVICE_CONFIG) with open(config_file_path, 'w+', encoding='utf-8') as config_file: config_file.writelines(json.dumps(device.to_config_json(), indent=4)) @@ -311,9 +350,8 @@ def save_device(self, device: Device, device_json): def delete_device(self, device: Device): # Obtain the config file path - device_folder = os.path.join(root_dir, - LOCAL_DEVICES_DIR, - device.device_folder) + device_folder = os.path.join(self._root_dir, LOCAL_DEVICES_DIR, + device.device_folder) # Delete the device directory shutil.rmtree(device_folder) @@ -328,17 +366,13 @@ def start(self): self._start_network() self.get_net_orc().get_listener().register_callback( - self._device_discovered, - [NetworkEvent.DEVICE_DISCOVERED] - ) + self._device_discovered, [NetworkEvent.DEVICE_DISCOVERED]) if self._net_only: LOGGER.info('Network only option configured, no tests will be run') else: self.get_net_orc().get_listener().register_callback( - self._device_stable, - [NetworkEvent.DEVICE_STABLE] - ) + self._device_stable, [NetworkEvent.DEVICE_STABLE]) self.get_net_orc().start_listener() LOGGER.info('Waiting for devices on the network...') @@ -349,15 +383,21 @@ def start(self): def stop(self): + # First, change the status to stopping + self.get_session().stop() + # Prevent discovering new devices whilst stopping if self.get_net_orc().get_listener() is not None: self.get_net_orc().get_listener().stop_listener() - self.get_session().stop() - self._stop_tests() + + self.get_session().set_status(TestrunStatus.CANCELLED) + + # Disconnect before WS server stops to prevent error + self._mqtt_client.disconnect() + self._stop_network(kill=True) - self.get_session().set_status('Cancelled') def _register_exits(self): signal.signal(signal.SIGINT, self._exit_handler) @@ -369,6 +409,7 @@ def shutdown(self): LOGGER.info('Shutting down Testrun') self.stop() self._stop_ui() + self._stop_ws() def _exit_handler(self, signum, arg): # pylint: disable=unused-argument LOGGER.debug('Exit signal received: ' + str(signum)) @@ -380,7 +421,7 @@ def _exit_handler(self, signum, arg): # pylint: disable=unused-argument def _get_config_abs(self, config_file=None): if config_file is None: # If not defined, use relative pathing to local file - config_file = os.path.join(root_dir, self._config_file) + config_file = os.path.join(self._root_dir, self._config_file) # Expand the config file to absolute pathing return os.path.abspath(config_file) @@ -406,6 +447,9 @@ def _stop_network(self, kill=True): def _stop_tests(self): self._test_orc.stop() + def get_mqtt_client(self): + return self._mqtt_client + def get_device(self, mac_addr): """Returns a loaded device object from the device mac address.""" for device in self.get_session().get_device_repository(): @@ -435,16 +479,17 @@ def _device_discovered(self, mac_addr): def _device_stable(self, mac_addr): # Do not continue testing if Testrun has cancelled during monitor phase - if self.get_session().get_status() == 'Cancelled': + if self.get_session().get_status() == TestrunStatus.CANCELLED: self._stop_network() return LOGGER.info(f'Device with mac address {mac_addr} is ready for testing.') - self._set_status('In Progress') + self._set_status(TestrunStatus.IN_PROGRESS) result = self._test_orc.run_test_modules() if result is not None: self._set_status(result) + self._stop_network() def get_session(self): @@ -462,16 +507,12 @@ def start_ui(self): client = docker.from_env() try: - client.containers.run( - image='test-run/ui', - auto_remove=True, - name='tr-ui', - hostname='testrun.io', - detach=True, - ports={ - '80': 8080 - } - ) + client.containers.run(image='testrun/ui', + auto_remove=True, + name='tr-ui', + hostname='testrun.io', + detach=True, + ports={'80': 8080}) except ImageNotFound as ie: LOGGER.error('An error occured whilst starting the UI. ' + 'Please investigate and try again.') @@ -489,4 +530,37 @@ def _stop_ui(self): if container is not None: container.kill() except docker.errors.NotFound: - return + pass + + def start_ws(self): + + self._stop_ws() + + LOGGER.info('Starting WS server') + + client = docker.from_env() + + try: + client.containers.run(image='testrun/ws', + auto_remove=True, + name='tr-ws', + detach=True, + ports={ + '9001': 9001, + '1883': 1883 + }) + except ImageNotFound as ie: + LOGGER.error('An error occured whilst starting the websockets server. ' + + 'Please investigate and try again.') + LOGGER.error(ie) + sys.exit(1) + + def _stop_ws(self): + LOGGER.info('Stopping websockets server') + client = docker.from_env() + try: + container = client.containers.get('tr-ws') + if container is not None: + container.kill() + except docker.errors.NotFound: + pass diff --git a/framework/python/src/net_orc/ip_control.py b/framework/python/src/net_orc/ip_control.py index 506b23a95..aa07283af 100644 --- a/framework/python/src/net_orc/ip_control.py +++ b/framework/python/src/net_orc/ip_control.py @@ -12,9 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. """IP Control Module""" +import psutil +import typing as t from common import logger from common import util import re +import socket LOGGER = logger.get_logger('ip_ctrl') @@ -43,10 +46,7 @@ def add_namespace(self, namespace): def check_interface_status(self, interface_name): output = util.run_command(cmd=f'ip link show {interface_name}', output=True) - if 'state DOWN ' in output[0]: - return False - else: - return True + return 'state UP ' in output[0] def delete_link(self, interface_name): """Delete an ip link""" @@ -89,6 +89,16 @@ def get_iface_connection_stats(self, iface): else: return None + @staticmethod + def get_iface_mac_address(iface): + net_if_addrs = psutil.net_if_addrs() + if iface in net_if_addrs: + for addr_info in net_if_addrs[iface]: + # AF_LINK corresponds to the MAC address + if addr_info.family == psutil.AF_LINK: + return addr_info.address + return None + def get_iface_port_stats(self, iface): """Extract information about packets connection""" response = util.run_command(f'ethtool -S {iface}') @@ -97,9 +107,17 @@ def get_iface_port_stats(self, iface): else: return None + def get_ip_address(self, iface): + addrs = psutil.net_if_addrs() + if iface in addrs: + for addr in addrs[iface]: + if addr.family == socket.AF_INET: + return addr.address + return None + def get_namespaces(self): result = util.run_command('ip netns list') - #Strip ID's from the namespace results + # Strip ID's from the namespace results namespaces = re.findall(r'(\S+)(?:\s+\(id: \d+\))?', result[0]) return namespaces @@ -237,3 +255,30 @@ def configure_container_interface(self, LOGGER.error(f'Failed to set interface up {namespace_intf}') return False return True + + def ping_via_gateway(self, host): + """Ping the host trough the gateway container""" + command = f'timeout 3 docker exec tr-ct-gateway ping -W 1 -c 1 {host}' + output = util.run_command(command) + if '0% packet loss' in output[0]: + return True + return False + + @staticmethod + def get_sys_interfaces() -> t.Dict[str, t.Dict[str, str]]: + """ Retrieves all Ethernet network interfaces from the host system + Returns: + t.Dict[str, str] + """ + addrs = psutil.net_if_addrs() + ifaces = {} + + for key in addrs: + nic = addrs[key] + # Ignore any interfaces that are not ethernet + if not (key.startswith('en') or key.startswith('eth')): + continue + + ifaces[key] = nic[0].address + + return ifaces diff --git a/framework/python/src/net_orc/network_orchestrator.py b/framework/python/src/net_orc/network_orchestrator.py index f20093a28..b8e7befd2 100644 --- a/framework/python/src/net_orc/network_orchestrator.py +++ b/framework/python/src/net_orc/network_orchestrator.py @@ -17,18 +17,20 @@ import json import os from scapy.all import sniff, wrpcap, BOOTP, AsyncSniffer +from scapy.error import Scapy_Exception import shutil import subprocess import sys -import docker import time -from docker.types import Mount -from common import logger, util +import traceback +from common import logger, util, mqtt +from common.statuses import TestrunStatus from net_orc.listener import Listener from net_orc.network_event import NetworkEvent from net_orc.network_validator import NetworkValidator from net_orc.ovs_control import OVSControl from net_orc.ip_control import IPControl +from core.docker.network_docker_module import NetworkModule LOGGER = logger.get_logger('net_orc') RUNTIME_DIR = 'runtime' @@ -66,6 +68,10 @@ def __init__(self, session): self._ovs = OVSControl(self._session) self._ip_ctrl = IPControl() + # Load subnet information into the session + self._session.set_subnets(self.network_config.ipv4_network, + self.network_config.ipv6_network) + def start(self): """Start the network orchestrator.""" @@ -137,6 +143,9 @@ def start_network(self): # Get network ready (via Network orchestrator) LOGGER.debug('Network is ready') + def get_ip_address(self, iface): + return self._ip_ctrl.get_ip_address(iface) + def get_listener(self): return self._listener @@ -190,7 +199,7 @@ def _device_discovered(self, mac_addr): test_dir = os.path.join(RUNTIME_DIR, TEST_DIR) device_tests = os.listdir(test_dir) for device_test in device_tests: - device_test_path = os.path.join(RUNTIME_DIR,TEST_DIR,device_test) + device_test_path = os.path.join(RUNTIME_DIR, TEST_DIR, device_test) if os.path.isdir(device_test_path): shutil.rmtree(device_test_path, ignore_errors=True) @@ -215,7 +224,7 @@ def _device_discovered(self, mac_addr): if device.ip_addr is None: LOGGER.info( f'Timed out whilst waiting for {mac_addr} to obtain an IP address') - self._session.set_status('Cancelled') + self._session.set_status(TestrunStatus.CANCELLED) return LOGGER.info( f'Device with mac addr {device.mac_addr} has obtained IP address ' @@ -223,7 +232,9 @@ def _device_discovered(self, mac_addr): #self._ovs.add_arp_inspection_filter(ip_address=device.ip_addr, # mac_address=device.mac_addr) - self._start_device_monitor(device) + # Don't monitor devices when in network only mode + if 'net_only' not in self._session.get_runtime_params(): + self._start_device_monitor(device) def _get_conn_stats(self): """ Extract information about the physical connection @@ -273,7 +284,7 @@ def _dhcp_lease_ack(self, packet): def _start_device_monitor(self, device): """Start a timer until the steady state has been reached and callback the steady state method for this device.""" - self.get_session().set_status('Monitoring') + self.get_session().set_status(TestrunStatus.MONITORING) self._monitor_packets = [] LOGGER.info(f'Monitoring device with mac addr {device.mac_addr} ' f'for {str(self._session.get_monitor_period())} seconds') @@ -290,15 +301,22 @@ def _start_device_monitor(self, device): time.sleep(1) # Check Testrun hasn't been cancelled - if self._session.get_status() == 'Cancelled': + if self._session.get_status() in ( + TestrunStatus.STOPPING, + TestrunStatus.CANCELLED + ): sniffer.stop() return if not self._ip_ctrl.check_interface_status( self._session.get_device_interface()): - sniffer.stop() - self._session.set_status('Cancelled') - LOGGER.error('Device interface disconnected, cancelling Testrun') + try: + sniffer.stop() + except Scapy_Exception: + LOGGER.error('Device adapter disconnected whilst monitoring.') + finally: + self._session.set_status(TestrunStatus.CANCELLED) + LOGGER.error('Device interface disconnected, cancelling Testrun') LOGGER.debug('Writing packets to monitor.pcap') wrpcap(os.path.join(device_runtime_dir, 'monitor.pcap'), @@ -330,26 +348,6 @@ def _ping(self, net_module): success = util.run_command(cmd, output=False) return success - def _create_private_net(self): - client = docker.from_env() - try: - network = client.networks.get(PRIVATE_DOCKER_NET) - network.remove() - except docker.errors.NotFound: - pass - - # TODO: These should be made into variables - ipam_pool = docker.types.IPAMPool(subnet='100.100.0.0/16', - iprange='100.100.100.0/24') - - ipam_config = docker.types.IPAMConfig(pool_configs=[ipam_pool]) - - client.networks.create(PRIVATE_DOCKER_NET, - ipam=ipam_config, - internal=True, - check_duplicate=True, - driver='macvlan') - def _ci_pre_network_create(self): """ Stores network properties to restore network after network creation and flushes internet interface @@ -436,79 +434,33 @@ def load_network_modules(self): for module_dir in os.listdir(net_modules_dir): - if self._get_network_module(module_dir) is None: + if (self._get_network_module(module_dir) is None and + module_dir != 'template'): loaded_module = self._load_network_module(module_dir) loaded_modules += loaded_module.dir_name + ' ' LOGGER.info(loaded_modules) def _load_network_module(self, module_dir): + """Import module configuration from module_config.json.""" - net_modules_dir = os.path.join(self._path, NETWORK_MODULES_DIR) + # Make sure we only load each module once since some modules will + # depend on the same module + if not any(m.dir_name == module_dir for m in self._net_modules): - net_module = NetworkModule() - - # Load module information - with open(os.path.join(self._path, net_modules_dir, module_dir, - NETWORK_MODULE_METADATA), - 'r', - encoding='UTF-8') as module_file_open: - net_module_json = json.load(module_file_open) - - net_module.name = net_module_json['config']['meta']['name'] - net_module.display_name = net_module_json['config']['meta']['display_name'] - net_module.description = net_module_json['config']['meta']['description'] - net_module.dir = os.path.join(self._path, net_modules_dir, module_dir) - net_module.dir_name = module_dir - net_module.build_file = module_dir + '.Dockerfile' - net_module.container_name = 'tr-ct-' + net_module.dir_name - net_module.image_name = 'test-run/' + net_module.dir_name - - # Attach folder mounts to network module - if 'docker' in net_module_json['config']: - - if 'mounts' in net_module_json['config']['docker']: - for mount_point in net_module_json['config']['docker']['mounts']: - net_module.mounts.append( - Mount(target=mount_point['target'], - source=os.path.join(os.getcwd(), mount_point['source']), - type='bind')) - - if 'depends_on' in net_module_json['config']['docker']: - depends_on_module = net_module_json['config']['docker']['depends_on'] - if self._get_network_module(depends_on_module) is None: - self._load_network_module(depends_on_module) - - # Determine if this is a container or just an image/template - if 'enable_container' in net_module_json['config']['docker']: - net_module.enable_container = net_module_json['config']['docker'][ - 'enable_container'] - - # Determine if this is a template - if 'template' in net_module_json['config']['docker']: - net_module.template = net_module_json['config']['docker']['template'] - - # Load network service networking configuration - if net_module.enable_container: - - net_module.net_config.enable_wan = net_module_json['config']['network'][ - 'enable_wan'] - net_module.net_config.ip_index = net_module_json['config']['network'][ - 'ip_index'] - - net_module.net_config.host = False if not 'host' in net_module_json[ - 'config']['network'] else net_module_json['config']['network']['host'] - - net_module.net_config.ipv4_address = self.network_config.ipv4_network[ - net_module.net_config.ip_index] - net_module.net_config.ipv4_network = self.network_config.ipv4_network - - net_module.net_config.ipv6_address = self.network_config.ipv6_network[ - net_module.net_config.ip_index] - net_module.net_config.ipv6_network = self.network_config.ipv6_network - - self._net_modules.append(net_module) - return net_module + LOGGER.debug(f'Loading network module {module_dir}') + + modules_dir = os.path.join(self._path, NETWORK_MODULES_DIR) + + module_conf_file = os.path.join(self._path, modules_dir, module_dir, + NETWORK_MODULE_METADATA) + + module = NetworkModule(module_conf_file, self._session) + if module.depends_on is not None: + self._load_network_module(module.depends_on) + self._net_modules.append(module) + + return module def build_network_modules(self): LOGGER.info('Building network modules...') @@ -518,12 +470,7 @@ def build_network_modules(self): def _build_module(self, net_module): LOGGER.debug('Building network module ' + net_module.dir_name) - client = docker.from_env() - client.images.build(dockerfile=os.path.join(net_module.dir, - net_module.build_file), - path=self._path, - forcerm=True, - tag='test-run/' + net_module.dir_name) + net_module.build() def _get_network_module(self, name): for net_module in self._net_modules: @@ -535,65 +482,17 @@ def _get_network_module(self, name): def _start_network_service(self, net_module): LOGGER.debug('Starting network service ' + net_module.display_name) - network = 'host' if net_module.net_config.host else PRIVATE_DOCKER_NET + network = 'host' if net_module.net_config.host else 'bridge' LOGGER.debug(f"""Network: {network}, image name: {net_module.image_name}, container name: {net_module.container_name}""") - try: - client = docker.from_env() - net_module.container = client.containers.run( - net_module.image_name, - auto_remove=True, - cap_add=['NET_ADMIN'], - name=net_module.container_name, - hostname=net_module.container_name, - # Undetermined version of docker seems to have broken - # DNS configuration (/etc/resolv.conf) Re-add when/if - # this network is utilized and DNS issue is resolved - #network=PRIVATE_DOCKER_NET, - network_mode='none', - privileged=True, - detach=True, - mounts=net_module.mounts, - environment={ - 'TZ': self.get_session().get_timezone(), - 'HOST_USER': util.get_host_user() - }) - except docker.errors.ContainerError as error: - LOGGER.error('Container run error') - LOGGER.error(error) - - if network != 'host': + net_module.start() + if net_module.get_network() != 'host': self._attach_service_to_network(net_module) def _stop_service_module(self, net_module, kill=False): LOGGER.debug('Stopping network container ' + net_module.container_name) - try: - container = self._get_service_container(net_module) - if container is not None: - if kill: - LOGGER.debug('Killing container: ' + net_module.container_name) - container.kill() - else: - LOGGER.debug('Stopping container: ' + net_module.container_name) - container.stop() - LOGGER.debug('Container stopped: ' + net_module.container_name) - except Exception as error: # pylint: disable=W0703 - LOGGER.error('Container stop error') - LOGGER.error(error) - - def _get_service_container(self, net_module): - LOGGER.debug('Resolving service container: ' + net_module.container_name) - container = None - try: - client = docker.from_env() - container = client.containers.get(net_module.container_name) - except docker.errors.NotFound: - LOGGER.debug('Container ' + net_module.container_name + ' not found') - except Exception as e: # pylint: disable=W0703 - LOGGER.error('Failed to resolve container') - LOGGER.error(e) - return container + net_module.stop(kill=kill) def stop_networking_services(self, kill=False): LOGGER.info('Stopping network services') @@ -758,13 +657,10 @@ def restore_net(self): if self.get_listener() is not None and self.get_listener().is_running(): self.get_listener().stop_listener() - client = docker.from_env() - # Stop all network containers if still running for net_module in self._net_modules: try: - container = client.containers.get('tr-ct-' + net_module.dir_name) - container.kill() + net_module.stop(kill=True) except Exception: # pylint: disable=W0703 continue @@ -786,53 +682,52 @@ def restore_net(self): def get_session(self): return self._session - -class NetworkModule: - """Define all the properties of a Network Module""" - - def __init__(self): - self.name = None - self.display_name = None - self.description = None - - self.container = None - self.container_name = None - self.image_name = None - self.template = False - - # Absolute path - self.dir = None - self.dir_name = None - self.build_file = None - self.mounts = [] - - self.enable_container = True - - self.net_config = NetworkModuleNetConfig() - - -class NetworkModuleNetConfig: - """Define all the properties of the network config - for a network module""" - - def __init__(self): - - self.enable_wan = False - - self.ip_index = 0 - self.ipv4_address = None - self.ipv4_network = None - self.ipv6_address = None - self.ipv6_network = None - - self.host = False - - def get_ipv4_addr_with_prefix(self): - return format(self.ipv4_address) + '/' + str(self.ipv4_network.prefixlen) - - def get_ipv6_addr_with_prefix(self): - return format(self.ipv6_address) + '/' + str(self.ipv6_network.prefixlen) - + def network_adapters_checker(self, mqtt_client: mqtt.MQTT, topic: str): + """Checks for changes in network adapters + and sends a message to the frontend + """ + try: + adapters = self._session.detect_network_adapters_change() + if adapters: + mqtt_client.send_message(topic, adapters) + except Exception: # pylint: disable=W0703 + LOGGER.error(traceback.format_exc()) + + def is_device_connected(self): + """Check if device connected""" + return self._ip_ctrl.check_interface_status( + self._session.get_device_interface() + ) + + def internet_conn_checker(self, mqtt_client: mqtt.MQTT, topic: str): + """Checks internet connection and sends a status to frontend""" + + # Default message + message = {'connection': False} + + # Only check if Testrun is running + if self.get_session().get_status() not in [ + TestrunStatus.WAITING_FOR_DEVICE, + TestrunStatus.MONITORING, + TestrunStatus.IN_PROGRESS + ]: + message['connection'] = None + + # Only run if single intf mode not used + elif 'single_intf' not in self._session.get_runtime_params(): + iface = self._session.get_internet_interface() + + # Check that an internet intf has been selected + if iface and iface in self._ip_ctrl.get_sys_interfaces(): + + # Ping google.com from gateway container + internet_connection = self._ip_ctrl.ping_via_gateway('google.com') + + if internet_connection: + message['connection'] = True + + # Broadcast via MQTT client + mqtt_client.send_message(topic, message) class NetworkConfig: """Define all the properties of the network configuration""" diff --git a/framework/python/src/net_orc/network_validator.py b/framework/python/src/net_orc/network_validator.py index df9b96b1d..d760970a3 100644 --- a/framework/python/src/net_orc/network_validator.py +++ b/framework/python/src/net_orc/network_validator.py @@ -106,7 +106,7 @@ def _load_devices(self): device.dir_name = module_dir device.build_file = module_dir + '.Dockerfile' device.container_name = 'tr-ct-' + device.dir_name - device.image_name = 'test-run/' + device.dir_name + device.image_name = 'testrun/' + device.dir_name runtime_source = os.path.join(os.getcwd(), OUTPUT_DIR, device.name) conf_source = os.path.join(os.getcwd(), CONF_DIR) diff --git a/framework/python/src/test_orc/test_case.py b/framework/python/src/test_orc/test_case.py index cf0d6593a..6f4e3434b 100644 --- a/framework/python/src/test_orc/test_case.py +++ b/framework/python/src/test_orc/test_case.py @@ -14,6 +14,7 @@ """Represents an individual test case.""" from dataclasses import dataclass, field +from common.statuses import TestResult @dataclass @@ -24,25 +25,25 @@ class TestCase: # pylint: disable=too-few-public-methods,too-many-instance-attr description: str = "" expected_behavior: str = "" required_result: str = "Recommended" - result: str = "Non-Compliant" + result: str = TestResult.NON_COMPLIANT recommendations: list = field(default_factory=lambda: []) + optional_recommendations: list = field(default_factory=lambda: []) def to_dict(self): + test_dict = { + "name": self.name, + "description": self.description, + "expected_behavior": self.expected_behavior, + "required_result": self.required_result, + "result": self.result + } + if self.recommendations is not None and len(self.recommendations) > 0: - return { - "name": self.name, - "description": self.description, - "expected_behavior": self.expected_behavior, - "required_result": self.required_result, - "result": self.result, - "recommendations": self.recommendations - } - - return { - "name": self.name, - "description": self.description, - "expected_behavior": self.expected_behavior, - "required_result": self.required_result, - "result": self.result - } + test_dict["recommendations"] = self.recommendations + + if (self.optional_recommendations is not None + and len(self.optional_recommendations) > 0): + test_dict["optional_recommendations"] = self.optional_recommendations + + return test_dict diff --git a/framework/python/src/test_orc/test_orchestrator.py b/framework/python/src/test_orc/test_orchestrator.py index d38f888a1..d133fefd9 100644 --- a/framework/python/src/test_orc/test_orchestrator.py +++ b/framework/python/src/test_orc/test_orchestrator.py @@ -20,23 +20,32 @@ import shutil import docker from datetime import datetime -from docker.types import Mount from common import logger, util from common.testreport import TestReport -from test_orc.module import TestModule +from common.statuses import TestrunStatus, TestResult +from core.docker.test_docker_module import TestModule from test_orc.test_case import TestCase +from test_orc.test_pack import TestPack import threading +from typing import List LOG_NAME = "test_orc" LOGGER = logger.get_logger("test_orc") + RUNTIME_DIR = "runtime" -RUNTIME_TEST_DIR = os.path.join(RUNTIME_DIR,"test") +RESOURCES_DIR = "resources" + +RUNTIME_TEST_DIR = os.path.join(RUNTIME_DIR, "test") +TEST_PACKS_DIR = os.path.join(RESOURCES_DIR, "test_packs") + TEST_MODULES_DIR = "modules/test" MODULE_CONFIG = "conf/module_config.json" -LOG_REGEX = r"^[A-Z][a-z]{2} [0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2} test_" + SAVED_DEVICE_REPORTS = "report/{device_folder}/" LOCAL_DEVICE_REPORTS = "local/devices/{device_folder}/reports" DEVICE_ROOT_CERTS = "local/root_certs" + +LOG_REGEX = r"^[A-Z][a-z]{2} [0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2} test_" API_URL = "http://localhost:8000" @@ -44,22 +53,25 @@ class TestOrchestrator: """Manages and controls the test modules.""" def __init__(self, session, net_orc): - self._test_modules = [] + + self._test_modules: List[TestModule] = [] + self._test_packs: List[TestPack] = [] + self._container_logs = [] self._session = session - self._api_url = (self._session.get_api_url() + ":" + - str(self._session.get_api_port())) + + self._api_url = (self.get_session().get_api_url() + ":" + + str(self.get_session().get_api_port())) + self._net_orc = net_orc self._test_in_progress = False - self._path = os.path.dirname( - os.path.dirname( - os.path.dirname( - os.path.dirname(os.path.dirname(os.path.realpath(__file__)))))) self._root_path = os.path.dirname( os.path.dirname( os.path.dirname( os.path.dirname(os.path.dirname(os.path.realpath(__file__)))))) + self._test_modules_running = [] + self._current_module = 0 def start(self): LOGGER.debug("Starting test orchestrator") @@ -73,6 +85,7 @@ def start(self): os.makedirs(DEVICE_ROOT_CERTS, exist_ok=True) self._load_test_modules() + self._load_test_packs() def stop(self): """Stop any running tests""" @@ -82,39 +95,87 @@ def run_test_modules(self): """Iterates through each test module and starts the container.""" # Do not start test modules if status is not in progress, e.g. Stopping - if self.get_session().get_status() != "In Progress": + if self.get_session().get_status() != TestrunStatus.IN_PROGRESS: return - device = self._session.get_target_device() + device = self.get_session().get_target_device() + test_pack_name = device.test_pack + test_pack = self.get_test_pack(test_pack_name) + LOGGER.debug("Using test pack " + test_pack.name) + self._test_in_progress = True + LOGGER.info( f"Running test modules on device with mac addr {device.mac_addr}") test_modules = [] + for module in self._test_modules: + # Ignore test modules that are just base images etc if module is None or not module.enable_container: continue + # Ignore test modules that are disabled for this device if not self._is_module_enabled(module, device): continue + num_tests = 0 + + # Add module to list of modules to run test_modules.append(module) - self.get_session().add_total_tests(len(module.tests)) - for module in test_modules: + for test in module.tests: + + # Duplicate test obj so we don't alter the source + test_copy = copy.deepcopy(test) + + # Do not add test if it is not enabled + if not self._is_test_enabled(test_copy.name, device): + continue + + # Set result to Not Started + test_copy.result = TestResult.NOT_STARTED + + # We don't want steps to resolve for not started tests + if hasattr(test_copy, "recommendations"): + test_copy.recommendations = None + + # Set the required result from the correct test pack + required_result = test_pack.get_required_result(test.name) + + test_copy.required_result = required_result + + # Add test result to the session + self.get_session().add_test_result(test_copy) + + # Increment number of tests being run by this module + num_tests += 1 + + # Increment number of tests that will be run + self.get_session().add_total_tests(num_tests) + + # Store enabled test modules in the TestOrchectrator object + self._test_modules_running = test_modules + self._current_module = 0 + + for index, module in enumerate(test_modules): + + self._current_module = index self._run_test_module(module) LOGGER.info("All tests complete") - self._session.finish() + self.get_session().finish() # Do not carry on (generating a report) if Testrun has been stopped - if self.get_session().get_status() != "In Progress": - return "Cancelled" + if self.get_session().get_status() != TestrunStatus.IN_PROGRESS: + return TestrunStatus.CANCELLED report = TestReport() - report.from_json(self._generate_report()) + + generated_report_json = self._generate_report() + report.from_json(generated_report_json) report.add_module_reports(self.get_session().get_module_reports()) device.add_report(report) @@ -122,6 +183,19 @@ def run_test_modules(self): self._test_in_progress = False self.get_session().set_report_url(report.get_report_url()) + # Set testing description + test_pack: TestPack = self.get_test_pack(device.test_pack) + + # Default message is empty (better than an error message). + # This should never be shown + message: str = "" + if report.get_status() == TestrunStatus.COMPLIANT: + message = test_pack.get_message("compliant_description") + elif report.get_status() == TestrunStatus.NON_COMPLIANT: + message = test_pack.get_message("non_compliant_description") + + self.get_session().set_description(message) + # Move testing output from runtime to local device folder self._timestamp_results(device) @@ -136,7 +210,7 @@ def _write_reports(self, test_report): out_dir = os.path.join( self._root_path, RUNTIME_TEST_DIR, - self._session.get_target_device().mac_addr.replace(":", "")) + self.get_session().get_target_device().mac_addr.replace(":", "")) LOGGER.debug(f"Writing reports to {out_dir}") @@ -157,9 +231,7 @@ def _write_reports(self, test_report): def _generate_report(self): report = {} - report["testrun"] = { - "version": self.get_session().get_version() - } + report["testrun"] = {"version": self.get_session().get_version()} report["mac_addr"] = self.get_session().get_target_device().mac_addr report["device"] = self.get_session().get_target_device().to_dict() @@ -178,16 +250,22 @@ def _generate_report(self): return report def _calculate_result(self): - result = "Compliant" - for test_result in self._session.get_test_results(): + result = TestResult.COMPLIANT + for test_result in self.get_session().get_test_results(): + # Check Required tests if (test_result.required_result.lower() == "required" - and test_result.result.lower() != "compliant"): - result = "Non-Compliant" + and test_result.result not in [ + TestResult.COMPLIANT, + TestResult.ERROR + ]): + result = TestResult.NON_COMPLIANT + # Check Required if Applicable tests elif (test_result.required_result.lower() == "required if applicable" - and test_result.result.lower() == "non-compliant"): - result = "Non-Compliant" + and test_result.result == TestResult.NON_COMPLIANT): + result = TestResult.NON_COMPLIANT + return result def _cleanup_old_test_results(self, device): @@ -195,7 +273,7 @@ def _cleanup_old_test_results(self, device): if device.max_device_reports is not None: max_device_reports = device.max_device_reports else: - max_device_reports = self._session.get_max_device_reports() + max_device_reports = self.get_session().get_max_device_reports() if max_device_reports > 0: completed_results_dir = os.path.join( @@ -270,25 +348,20 @@ def _timestamp_results(self, device): return completed_results_dir - def zip_results(self, - device, - timestamp, - profile): + def zip_results(self, device, timestamp, profile): try: LOGGER.debug("Archiving test results") - src_path = os.path.join(LOCAL_DEVICE_REPORTS.replace( - "{device_folder}", - device.device_folder), - timestamp) + src_path = os.path.join( + LOCAL_DEVICE_REPORTS.replace("{device_folder}", device.device_folder), + timestamp) # Define temp directory to store files before zipping results_dir = os.path.join(f"/tmp/testrun/{time.time()}") # Define where to save the zip file - zip_location = os.path.join("/tmp/testrun", - timestamp) + zip_location = os.path.join("/tmp/testrun", timestamp) # Delete zip_temp if it already exists if os.path.exists(results_dir): @@ -298,16 +371,13 @@ def zip_results(self, if os.path.exists(zip_location + ".zip"): os.remove(zip_location + ".zip") - shutil.copytree(src_path,results_dir) + shutil.copytree(src_path, results_dir) # Include profile if specified if profile is not None: - LOGGER.debug( - f"Copying profile {profile.name} to results directory") + LOGGER.debug(f"Copying profile {profile.name} to results directory") shutil.copy(profile.get_file_path(), - os.path.join( - results_dir, - "profile.json")) + os.path.join(results_dir, "profile.json")) with open(os.path.join(results_dir, "profile.pdf"), "wb") as f: f.write(profile.to_pdf(device).getvalue()) @@ -320,14 +390,13 @@ def zip_results(self, # Check that the ZIP was successfully created zip_file = zip_location + ".zip" - LOGGER.info(f'''Archive {'created at ' + zip_file + LOGGER.info(f"""Archive {"created at " + zip_file if os.path.exists(zip_file) - else'creation failed'}''') - + else "creation failed"}""") return zip_file - except Exception as error: # pylint: disable=W0703 + except Exception as error: # pylint: disable=W0703 LOGGER.error("Failed to create zip file") LOGGER.debug(error) return None @@ -339,6 +408,7 @@ def _is_module_enabled(self, module, device): # Enable module as fallback enabled = True + if device.test_modules is not None: test_modules = device.test_modules if module.name in test_modules: @@ -350,99 +420,50 @@ def _is_module_enabled(self, module, device): return enabled + def _is_test_enabled(self, test, device): + + test_pack_name = device.test_pack + test_pack = self.get_test_pack(test_pack_name) + + return test_pack.get_test(test) is not None + def _run_test_module(self, module): """Start the test container and extract the results.""" # Check that Testrun is not stopping - if self.get_session().get_status() != "In Progress": + if self.get_session().get_status() != TestrunStatus.IN_PROGRESS: return - device = self._session.get_target_device() + device = self.get_session().get_target_device() LOGGER.info(f"Running test module {module.name}") # Get all tests to be executed and set to in progress - for test in module.tests: + for current_test, test in enumerate(module.tests): + # Check that device is connected + if not self._net_orc.is_device_connected(): + LOGGER.error("Device was disconnected") + self._set_test_modules_error(current_test) + self.get_session().set_status(TestrunStatus.CANCELLED) + return + + # Copy the test so we don't alter the source test_copy = copy.deepcopy(test) - test_copy.result = "In Progress" + + # Update test status to in progress + test_copy.result = TestResult.IN_PROGRESS # We don't want steps to resolve for in progress tests if hasattr(test_copy, "recommendations"): test_copy.recommendations = None - self.get_session().add_test_result(test_copy) + # Only add/update the test if it is enabled + if self._is_test_enabled(test_copy.name, device): + self.get_session().add_test_result(test_copy) - try: - - device_test_dir = os.path.join(self._root_path, RUNTIME_TEST_DIR, - device.mac_addr.replace(":", "")) - - container_runtime_dir = os.path.join(device_test_dir, module.name) - os.makedirs(container_runtime_dir, exist_ok=True) - - config_file = os.path.join(self._root_path, "local/system.json") - root_certs_dir = os.path.join(self._root_path, "local/root_certs") - - container_log_file = os.path.join(container_runtime_dir, "module.log") - - network_runtime_dir = os.path.join(self._root_path, "runtime/network") - - device_startup_capture = os.path.join(device_test_dir, "startup.pcap") - util.run_command(f"chown -R {self._host_user} {device_startup_capture}") - - device_monitor_capture = os.path.join(device_test_dir, "monitor.pcap") - util.run_command(f"chown -R {self._host_user} {device_monitor_capture}") - - client = docker.from_env() - - module.container = client.containers.run( - module.image_name, - auto_remove=True, - cap_add=["NET_ADMIN"], - name=module.container_name, - hostname=module.container_name, - privileged=True, - detach=True, - mounts=[ - Mount(target="/testrun/system.json", - source=config_file, - type="bind", - read_only=True), - Mount(target="/testrun/root_certs", - source=root_certs_dir, - type="bind", - read_only=True), - Mount(target="/runtime/output", - source=container_runtime_dir, - type="bind"), - Mount(target="/runtime/network", - source=network_runtime_dir, - type="bind", - read_only=True), - Mount(target="/runtime/device/startup.pcap", - source=device_startup_capture, - type="bind", - read_only=True), - Mount(target="/runtime/device/monitor.pcap", - source=device_monitor_capture, - type="bind", - read_only=True) - ], - environment={ - "TZ": self.get_session().get_timezone(), - "HOST_USER": self._host_user, - "DEVICE_MAC": device.mac_addr, - "IPV4_ADDR": device.ip_addr, - "DEVICE_TEST_MODULES": json.dumps(device.test_modules), - "IPV4_SUBNET": self._net_orc.network_config.ipv4_network, - "IPV6_SUBNET": self._net_orc.network_config.ipv6_network - }) - except (docker.errors.APIError, - docker.errors.ContainerError) as container_error: - LOGGER.error("Test module " + module.name + " has failed to start") - LOGGER.debug(container_error) - return + # Start the test module + module.start(device) # Mount the test container to the virtual network if requried if module.network: @@ -451,7 +472,6 @@ def _run_test_module(self, module): # Determine the module timeout time test_module_timeout = time.time() + module.timeout - status = self._get_module_status(module) # Resolving container logs is blocking so we need to spawn a new thread log_stream = module.container.logs(stream=True, stdout=True, stderr=True) @@ -460,87 +480,86 @@ def _run_test_module(self, module): log_thread.daemon = True log_thread.start() - while (status == "running" and self._session.get_status() == "In Progress"): + while (module.get_status() == "running" + and self.get_session().get_status() == TestrunStatus.IN_PROGRESS): + + # Check that timeout has not exceeded if time.time() > test_module_timeout: LOGGER.error("Module timeout exceeded, killing module: " + module.name) - self._stop_module(module=module, kill=True) + module.stop(kill=True) break - status = self._get_module_status(module) # Save all container logs to file - with open(container_log_file, "w", encoding="utf-8") as f: + with open(module.container_log_file, "w", encoding="utf-8") as f: for line in self._container_logs: f.write(line + "\n") # Check that Testrun has not been stopped whilst this module was running - if self.get_session().get_status() == "Stopping": + if self.get_session().get_status() == TestrunStatus.STOPPING: # Discard results for this module LOGGER.info(f"Test module {module.name} has forcefully quit") return - # Get test results from module - container_runtime_dir = os.path.join( - self._root_path, - "runtime/test/" + device.mac_addr.replace(":", "") + "/" + module.name) - results_file = f"{container_runtime_dir}/{module.name}-result.json" + results_file = f"{module.container_runtime_dir}/{module.name}-result.json" try: with open(results_file, "r", encoding="utf-8-sig") as f: + + # Load results from JSON file module_results_json = json.load(f) module_results = module_results_json["results"] for test_result in module_results: - # Convert dict into TestCase object + # Convert dict from json into TestCase object test_case = TestCase( - name=test_result["name"], - description=test_result["description"], - expected_behavior=test_result["expected_behavior"], - required_result=test_result["required_result"], - result=test_result["result"]) - test_case.result=test_result["result"] - - if (test_case.result == "Non-Compliant" and + name=test_result["name"], + result=test_result["result"], + description=test_result["description"]) + + # Add steps to resolve if test is non-compliant + if (test_case.result == TestResult.NON_COMPLIANT and "recommendations" in test_result): test_case.recommendations = test_result["recommendations"] else: - test_case.recommendations = None + test_case.recommendations = [] - self._session.add_test_result(test_case) + self.get_session().add_test_result(test_case) except (FileNotFoundError, PermissionError, json.JSONDecodeError) as results_error: LOGGER.error( - f"Error occurred whilst obtaining results for module {module.name}") + f"Error occurred whilst obtaining results for module {module.name}") LOGGER.error(results_error) # Get the markdown report from the module if generated - markdown_file = f"{container_runtime_dir}/{module.name}_report.md" + markdown_file = f"{module.container_runtime_dir}/{module.name}_report.md" try: with open(markdown_file, "r", encoding="utf-8") as f: module_report = f.read() - self._session.add_module_report(module_report) + self.get_session().add_module_report(module_report) except (FileNotFoundError, PermissionError): LOGGER.debug("Test module did not produce a markdown module report") # Get the HTML report from the module if generated - html_file = f"{container_runtime_dir}/{module.name}_report.html" + html_file = f"{module.container_runtime_dir}/{module.name}_report.html" try: with open(html_file, "r", encoding="utf-8") as f: module_report = f.read() LOGGER.debug(f"Adding module report for module {module.name}") - self._session.add_module_report(module_report) + self.get_session().add_module_report(module_report) except (FileNotFoundError, PermissionError): LOGGER.debug("Test module did not produce a html module report") LOGGER.info(f"Test module {module.name} has finished") - # Resolve all current log data in the containers log_stream - # this method is blocking so should be called in - # a thread or within a proper blocking context def _get_container_logs(self, log_stream): + """Resolve all current log data in the containers log_stream + this method is blocking so should be called in + a thread or within a proper blocking context""" self._container_logs = [] for log_chunk in log_stream: lines = log_chunk.decode("utf-8").splitlines() + # Process each line and strip blank space processed_lines = [line.strip() for line in lines if line.strip()] self._container_logs.extend(processed_lines) @@ -574,12 +593,32 @@ def _get_module_container(self, module): LOGGER.error(error) return container + def _load_test_packs(self): + + for test_pack_file in os.listdir(TEST_PACKS_DIR): + + LOGGER.debug(f"Loading test pack {test_pack_file}") + + with open(os.path.join( + self._root_path, + TEST_PACKS_DIR, + test_pack_file), encoding="utf-8") as f: + test_pack_json = json.load(f) + + test_pack: TestPack = TestPack( + name = test_pack_json["name"], + tests = test_pack_json["tests"], + language = test_pack_json["language"] + ) + + self._test_packs.append(test_pack) + def _load_test_modules(self): """Load network modules from module_config.json.""" LOGGER.debug("Loading test modules from /" + TEST_MODULES_DIR) loaded_modules = "Loaded the following test modules: " - test_modules_dir = os.path.join(self._path, TEST_MODULES_DIR) + test_modules_dir = os.path.join(self._root_path, TEST_MODULES_DIR) module_dirs = os.listdir(test_modules_dir) # Check if the directory protocol exists and move it to the beginning @@ -599,87 +638,42 @@ def _load_test_modules(self): def _load_test_module(self, module_dir): """Import module configuration from module_config.json.""" - LOGGER.debug(f"Loading test module {module_dir}") - - modules_dir = os.path.join(self._path, TEST_MODULES_DIR) - - # Load basic module information - module = TestModule() - with open(os.path.join(self._path, modules_dir, module_dir, MODULE_CONFIG), - encoding="UTF-8") as module_config_file: - module_json = json.load(module_config_file) - - module.name = module_json["config"]["meta"]["name"] - module.display_name = module_json["config"]["meta"]["display_name"] - module.description = module_json["config"]["meta"]["description"] - - if "enabled" in module_json["config"]: - module.enabled = module_json["config"]["enabled"] - - module.dir = os.path.join(self._path, modules_dir, module_dir) - module.dir_name = module_dir - module.build_file = module_dir + ".Dockerfile" - module.container_name = "tr-ct-" + module.dir_name + "-test" - module.image_name = "test-run/" + module.dir_name + "-test" - - # Load test cases - if "tests" in module_json["config"]: - module.total_tests = len(module_json["config"]["tests"]) - for test_case_json in module_json["config"]["tests"]: - try: - test_case = TestCase( - name=test_case_json["name"], - description=test_case_json["test_description"], - expected_behavior=test_case_json["expected_behavior"], - required_result=test_case_json["required_result"] - ) - - if "recommendations" in test_case_json: - test_case.recommendations = test_case_json["recommendations"] - module.tests.append(test_case) - except Exception as error: # pylint: disable=W0718 - LOGGER.error("Failed to load test case. See error for details") - LOGGER.error(error) - - if "timeout" in module_json["config"]["docker"]: - module.timeout = module_json["config"]["docker"]["timeout"] - - # Determine if this is a container or just an image/template - if "enable_container" in module_json["config"]["docker"]: - module.enable_container = module_json["config"]["docker"][ - "enable_container"] - - # Determine if this module needs network access - if "network" in module_json["config"]: - module.network = module_json["config"]["network"] - - # Ensure container is built after any dependencies - if "depends_on" in module_json["config"]["docker"]: - depends_on_module = module_json["config"]["docker"]["depends_on"] - if self._get_test_module(depends_on_module) is None: - self._load_test_module(depends_on_module) - - self._test_modules.append(module) - return module - - def build_test_modules(self): - """Build all test modules.""" - LOGGER.info("Building test modules...") - for module in self._test_modules: - self._build_test_module(module) - - def _build_test_module(self, module): - LOGGER.debug("Building docker image for module " + module.dir_name) - - client = docker.from_env() - try: - client.images.build( - dockerfile=os.path.join(module.dir, module.build_file), - path=self._path, - forcerm=True, # Cleans up intermediate containers during build - tag=module.image_name) - except docker.errors.BuildError as error: - LOGGER.error(error) + # Resolve the main docker interface (docker0) for host interaction + # Can't use device or internet iface since these are not in a stable + # state for this type of communication during testing but docker0 has + # to exist and should always be available + external_ip = self._net_orc.get_ip_address("docker0") + extra_hosts = { + "external.localhost": external_ip + } if external_ip is not None else {} + + # Make sure we only load each module once since some modules will + # depend on the same module + if not any(m.dir_name == module_dir for m in self._test_modules): + + modules_dir = os.path.join(self._root_path, TEST_MODULES_DIR) + + module_conf_file = os.path.join(self._root_path, modules_dir, module_dir, + MODULE_CONFIG) + + module = TestModule(module_conf_file, + self, + self.get_session(), + extra_hosts) + if module.depends_on is not None: + self._load_test_module(module.depends_on) + self._test_modules.append(module) + + return module + + def get_test_packs(self) -> List[TestPack]: + return self._test_packs + + def get_test_pack(self, name: str) -> TestPack: + for test_pack in self._test_packs: + if test_pack.name.lower() == name.lower(): + return test_pack + return None def _stop_modules(self, kill=False): LOGGER.info("Stopping test modules") @@ -692,18 +686,7 @@ def _stop_modules(self, kill=False): def _stop_module(self, module, kill=False): LOGGER.debug("Stopping test module " + module.container_name) - try: - container = module.container - if container is not None: - if kill: - LOGGER.debug("Killing container:" + module.container_name) - container.kill() - else: - LOGGER.debug("Stopping container:" + module.container_name) - container.stop() - LOGGER.debug("Container stopped:" + module.container_name) - except docker.errors.NotFound: - pass + module.stop(kill=kill) def get_test_modules(self): return self._test_modules @@ -729,3 +712,12 @@ def get_test_case(self, name): def get_session(self): return self._session + + def _set_test_modules_error(self, current_test): + """Set all remaining tests to error""" + for i in range(self._current_module, len(self._test_modules_running)): + start_idx = current_test if i == self._current_module else 0 + for j in range(start_idx, len(self._test_modules_running[i].tests)): + self.get_session().set_test_result_error( + self._test_modules_running[i].tests[j] + ) diff --git a/framework/python/src/test_orc/test_pack.py b/framework/python/src/test_orc/test_pack.py new file mode 100644 index 000000000..a2e7c5f97 --- /dev/null +++ b/framework/python/src/test_orc/test_pack.py @@ -0,0 +1,58 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Represents a testing pack.""" +from typing import List, Dict +from dataclasses import dataclass, field +from collections import defaultdict + + +@dataclass +class TestPack: # pylint: disable=too-few-public-methods,too-many-instance-attributes + """Represents a test pack.""" + + name: str = "undefined" + description: str = "" + tests: List[dict] = field(default_factory=lambda: []) + language: Dict = field(default_factory=lambda: defaultdict(dict)) + + def get_test(self, test_name: str) -> str: + """Get details of a test from the test pack""" + + for test in self.tests: + if "name" in test and test["name"].lower() == test_name.lower(): + return test + + def get_required_result(self, test_name: str) -> str: + """Fetch the required result of the test""" + + test = self.get_test(test_name) + + if test is not None and "required_result" in test: + return test["required_result"] + + return "Informational" + + def get_message(self, name: str) -> str: + if name in self.language: + return self.language[name] + return "Message not found" + + def to_dict(self): + return { + "name": self.name, + "description": self.description, + "tests": self.tests, + "language": self.language + } diff --git a/framework/requirements.txt b/framework/requirements.txt index c31978d99..6f54d3a99 100644 --- a/framework/requirements.txt +++ b/framework/requirements.txt @@ -1,11 +1,11 @@ # Requirements for the core module -requests<2.32.0 +requests==2.32.3 # Requirements for the net_orc module -docker==7.0.0 +docker==7.1.0 ipaddress==1.0.23 netifaces==0.11.0 -scapy==2.5.0 +scapy==2.6.0 # Requirments for the test_orc module weasyprint==61.2 @@ -21,13 +21,24 @@ pydantic==2.7.1 # Requirements for testing pytest==7.4.4 pytest-timeout==2.2.0 +responses==0.25.3 + # Requirements for the report markdown==3.5.2 # Requirements for the session -cryptography==42.0.7 +cryptography==43.0.1 pytz==2024.1 # Requirements for the risk profile python-dateutil==2.9.0 + +# Requirements for MQTT client +paho-mqtt==2.1.0 + +# Requirements for background tasks +APScheduler==3.10.4 + +# Requirements for reports generation +Jinja2==3.1.4 diff --git a/local/system.json.example b/local/system.json.example index 23023bead..df89b502f 100644 --- a/local/system.json.example +++ b/local/system.json.example @@ -1,10 +1,11 @@ { "network": { - "device_intf": "enx123456789123", - "internet_intf": "enx123456789124" + "device_intf": "", + "internet_intf": "" }, "log_level": "INFO", "startup_timeout": 60, "monitor_period": 300, - "max_device_reports": 0 + "max_device_reports": 0, + "org_name": "" } diff --git a/make/DEBIAN/control b/make/DEBIAN/control index 488f69458..c822e65fd 100644 --- a/make/DEBIAN/control +++ b/make/DEBIAN/control @@ -1,5 +1,5 @@ Package: Testrun -Version: 1.3.1 +Version: 2.0.1 Architecture: amd64 Maintainer: Google Homepage: https://github.com/google/testrun diff --git a/modules/devices/faux-dev/bin/start_network_service b/modules/devices/faux-dev/bin/start_network_service index d4bb8a92d..7d689f9dd 100644 --- a/modules/devices/faux-dev/bin/start_network_service +++ b/modules/devices/faux-dev/bin/start_network_service @@ -35,7 +35,7 @@ else INTF=$DEFINED_IFACE fi -#Create and set permissions on the output files +# Create and set permissions on the output files OUTPUT_DIR=/runtime/validation/ LOG_FILE=$OUTPUT_DIR/$MODULE_NAME.log RESULT_FILE=$OUTPUT_DIR/result.json diff --git a/modules/devices/faux-dev/faux-dev.Dockerfile b/modules/devices/faux-dev/faux-dev.Dockerfile index ecfdfc5c2..18901a2a1 100644 --- a/modules/devices/faux-dev/faux-dev.Dockerfile +++ b/modules/devices/faux-dev/faux-dev.Dockerfile @@ -12,13 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Image name: test-run/faux-dev -FROM test-run/base:latest +# Image name: testrun/faux-dev +FROM testrun/base:latest ARG MODULE_NAME=faux-dev ARG MODULE_DIR=modules/devices/$MODULE_NAME +ARG COMMON_DIR=framework/python/src/common -#Update and get all additional requirements not contained in the base image +# Update and get all additional requirements not contained in the base image RUN apt-get update --fix-missing # NTP requireds interactive installation so we're going to turn that off @@ -34,4 +35,4 @@ COPY $MODULE_DIR/conf /testrun/conf COPY $MODULE_DIR/bin /testrun/bin # Copy over all python files -COPY $MODULE_DIR/python /testrun/python \ No newline at end of file +COPY $MODULE_DIR/python /testrun/python diff --git a/modules/devices/faux-dev/python/src/logger.py b/modules/devices/faux-dev/python/src/logger.py deleted file mode 100644 index a727ad7bb..000000000 --- a/modules/devices/faux-dev/python/src/logger.py +++ /dev/null @@ -1,61 +0,0 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Sets up the logger to be used for the faux-device.""" - -import json -import logging -import os - -LOGGERS = {} -_LOG_FORMAT = '%(asctime)s %(name)-8s %(levelname)-7s %(message)s' -_DATE_FORMAT = '%b %02d %H:%M:%S' -_CONF_DIR = 'conf' -_CONF_FILE_NAME = 'system.json' -_LOG_DIR = '/runtime/validation' - -# Set log level -with open(os.path.join(_CONF_DIR, _CONF_FILE_NAME), - encoding='utf-8') as conf_file: - system_conf_json = json.load(conf_file) - -log_level_str = system_conf_json['log_level'] -log_level = logging.getLevelName(log_level_str) - -log_format = logging.Formatter(fmt=_LOG_FORMAT, datefmt=_DATE_FORMAT) - - -def add_file_handler(log, log_file): - """Add file handler to existing log.""" - handler = logging.FileHandler(os.path.join(_LOG_DIR, log_file + '.log')) - handler.setFormatter(log_format) - log.addHandler(handler) - - -def add_stream_handler(log): - """Add stream handler to existing log.""" - handler = logging.StreamHandler() - handler.setFormatter(log_format) - log.addHandler(handler) - - -def get_logger(name, log_file=None): - """Return logger for requesting class.""" - if name not in LOGGERS: - LOGGERS[name] = logging.getLogger(name) - LOGGERS[name].setLevel(log_level) - add_stream_handler(LOGGERS[name]) - if log_file is not None: - add_file_handler(LOGGERS[name], log_file) - return LOGGERS[name] diff --git a/modules/devices/faux-dev/python/src/util.py b/modules/devices/faux-dev/python/src/util.py deleted file mode 100644 index 81f9d2ced..000000000 --- a/modules/devices/faux-dev/python/src/util.py +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Provides basic utilities for the faux-device.""" -import subprocess -import shlex - - -def run_command(cmd, logger, output=True): - """Runs a process at the os level - By default, returns the standard output and error output - If the caller sets optional output parameter to False, - will only return a boolean result indicating if it was - successful in running the command. Failure is indicated - by any return code from the process other than zero.""" - - success = False - with subprocess.Popen( - shlex.split(cmd), - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) as process: - - stdout, stderr = process.communicate() - - if process.returncode != 0: - err_msg = f'{stderr.strip()}. Code: {process.returncode}' - logger.error('Command Failed: ' + cmd) - logger.error('Error: ' + err_msg) - else: - success = True - logger.debug('Command succeeded: ' + cmd) - if output: - out = stdout.strip().decode('utf-8') - logger.debug('Command output: ' + out) - return success, out - else: - return success, None diff --git a/modules/network/base/base.Dockerfile b/modules/network/base/base.Dockerfile index b30f6a7d9..7f6edb409 100644 --- a/modules/network/base/base.Dockerfile +++ b/modules/network/base/base.Dockerfile @@ -12,8 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Image name: test-run/base -FROM ubuntu@sha256:e6173d4dc55e76b87c4af8db8821b1feae4146dd47341e4d431118c7dd060a74 +# Image name: testrun/base +FROM ubuntu@sha256:77d57fd89366f7d16615794a5b53e124d742404e20f035c22032233f1826bd6a RUN apt-get update @@ -30,8 +30,9 @@ COPY $COMMON_DIR/ /testrun/python/src/common # Setup the base python requirements COPY $MODULE_DIR/python /testrun/python -# Install all python requirements for the module -RUN pip3 install -r /testrun/python/requirements.txt +# Install all python requirements for the module +# --break-system-packages flag used to bypass PEP668 +RUN pip3 install --break-system-packages -r /testrun/python/requirements.txt # Add the bin files COPY $MODULE_DIR/bin /testrun/bin @@ -42,5 +43,5 @@ RUN dos2unix /testrun/bin/* # Make sure all the bin files are executable RUN chmod u+x /testrun/bin/* -#Start the network module +# Start the network module ENTRYPOINT [ "/testrun/bin/start_module" ] \ No newline at end of file diff --git a/modules/network/base/bin/start_module b/modules/network/base/bin/start_module index 8e8cb5e4b..fb2823afd 100644 --- a/modules/network/base/bin/start_module +++ b/modules/network/base/bin/start_module @@ -22,7 +22,7 @@ DEFAULT_IFACE=veth0 # Create a local user that matches the same as the host # to be used for correct file ownership for various logs -# HOST_USER mapped in via docker container environemnt variables +# HOST_USER mapped in via docker container environment variables useradd $HOST_USER # Enable IPv6 for all containers @@ -42,6 +42,7 @@ fi # Extract the necessary config parameters MODULE_NAME=$(echo "$CONF" | jq -r '.config.meta.name') DEFINED_IFACE=$(echo "$CONF" | jq -r '.config.network.interface') +HOST=$(echo "$CONF" | jq -r '.config.network.host') GRPC=$(echo "$CONF" | jq -r '.config.grpc') # Validate the module name is present @@ -70,14 +71,19 @@ $BIN_DIR/setup_binaries $BIN_DIR echo "Starting module $MODULE_NAME on local interface $INTF..." -# Wait for interface to become ready -$BIN_DIR/wait_for_interface $INTF +# Only non-host containers will have a specific +# interface for capturing +if [[ "$HOST" != "true" ]]; then -# Small pause to let the interface stabalize before starting the capture -#sleep 1 + # Wait for interface to become ready + $BIN_DIR/wait_for_interface $INTF -# Start network capture -$BIN_DIR/capture $MODULE_NAME $INTF + # Small pause to let the interface stabalize before starting the capture + #sleep 1 + + # Start network capture + $BIN_DIR/capture $MODULE_NAME $INTF +fi # Start the grpc server if [[ ! -z $GRPC && ! $GRPC == "null" ]] @@ -96,4 +102,4 @@ fi sleep 3 # Start the networking service -$BIN_DIR/start_network_service $MODULE_NAME $INTF \ No newline at end of file +$BIN_DIR/start_network_service $MODULE_NAME $INTF diff --git a/modules/network/base/python/requirements.txt b/modules/network/base/python/requirements.txt index 9d9473d74..01abf05ce 100644 --- a/modules/network/base/python/requirements.txt +++ b/modules/network/base/python/requirements.txt @@ -1,3 +1,10 @@ -grpcio -grpcio-tools -netifaces \ No newline at end of file +# Dependencies to user defined packages +# Package dependencies should always be defined before the user defined +# packages to prevent auto-upgrades of stable dependencies +protobuf==5.28.3 + +# User defined packages +grpcio==1.67.1 +grpcio-tools==1.67.1 +netifaces==0.11.0 + diff --git a/modules/network/base/python/src/grpc_server/start_server.py b/modules/network/base/python/src/grpc_server/start_server.py index d372949e5..9c34ec736 100644 --- a/modules/network/base/python/src/grpc_server/start_server.py +++ b/modules/network/base/python/src/grpc_server/start_server.py @@ -46,6 +46,5 @@ def run(): print('gRPC server starting on port ' + port) serve(port) - if __name__ == '__main__': run() diff --git a/modules/network/dhcp-1/dhcp-1.Dockerfile b/modules/network/dhcp-1/dhcp-1.Dockerfile index e50ed9a95..7df94b4fd 100644 --- a/modules/network/dhcp-1/dhcp-1.Dockerfile +++ b/modules/network/dhcp-1/dhcp-1.Dockerfile @@ -12,8 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Image name: test-run/dhcp-primary -FROM test-run/base:latest +# Image name: testrun/dhcp-primary +FROM testrun/base:latest ARG MODULE_NAME=dhcp-1 ARG MODULE_DIR=modules/network/$MODULE_NAME diff --git a/modules/network/dhcp-1/python/src/grpc_server/dhcp_leases.py b/modules/network/dhcp-1/python/src/grpc_server/dhcp_leases.py index aa2945759..e2318ac02 100644 --- a/modules/network/dhcp-1/python/src/grpc_server/dhcp_leases.py +++ b/modules/network/dhcp-1/python/src/grpc_server/dhcp_leases.py @@ -68,8 +68,8 @@ def get_leases(self): leases.append(lease) except Exception as e: # pylint: disable=W0718 # Let non lease lines file without extra checks - LOGGER.error('Making Lease Error: ' + str(e)) - LOGGER.error('Not a valid lease line: ' + line) + LOGGER.info('Not a valid lease line: ' + line) + LOGGER.error('Get lease error: ' + str(e)) return leases def delete_lease(self, ip_addr): diff --git a/modules/network/dhcp-2/dhcp-2.Dockerfile b/modules/network/dhcp-2/dhcp-2.Dockerfile index 66ea857c3..4dcd7a819 100644 --- a/modules/network/dhcp-2/dhcp-2.Dockerfile +++ b/modules/network/dhcp-2/dhcp-2.Dockerfile @@ -12,8 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Image name: test-run/dhcp-primary -FROM test-run/base:latest +# Image name: testrun/dhcp-primary +FROM testrun/base:latest ARG MODULE_NAME=dhcp-2 ARG MODULE_DIR=modules/network/$MODULE_NAME diff --git a/modules/network/dhcp-2/python/src/grpc_server/dhcp_leases.py b/modules/network/dhcp-2/python/src/grpc_server/dhcp_leases.py index 08e6feabe..f6db83094 100644 --- a/modules/network/dhcp-2/python/src/grpc_server/dhcp_leases.py +++ b/modules/network/dhcp-2/python/src/grpc_server/dhcp_leases.py @@ -58,9 +58,9 @@ def get_leases(self): leases = [] lease_list_raw = self._get_lease_list() LOGGER.info('Raw Leases:\n' + str(lease_list_raw) + '\n') - lease_list_start = lease_list_raw.find('=========',0) - lease_list_start = lease_list_raw.find('\n',lease_list_start) - lease_list = lease_list_raw[lease_list_start+1:] + lease_list_start = lease_list_raw.find('=========', 0) + lease_list_start = lease_list_raw.find('\n', lease_list_start) + lease_list = lease_list_raw[lease_list_start + 1:] lines = lease_list.split('\n') for line in lines: try: @@ -68,8 +68,8 @@ def get_leases(self): leases.append(lease) except Exception as e: # pylint: disable=W0718 # Let non lease lines file without extra checks - LOGGER.error('Making Lease Error: ' + str(e)) - LOGGER.error('Not a valid lease line: ' + line) + LOGGER.info('Not a valid lease line: ' + line) + LOGGER.error('Get lease error: ' + str(e)) return leases def delete_lease(self, ip_addr): diff --git a/modules/network/dns/dns.Dockerfile b/modules/network/dns/dns.Dockerfile index d59b8a391..2b46dfb4a 100644 --- a/modules/network/dns/dns.Dockerfile +++ b/modules/network/dns/dns.Dockerfile @@ -12,8 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Image name: test-run/dns -FROM test-run/base:latest +# Image name: testrun/dns +FROM testrun/base:latest ARG MODULE_NAME=dns ARG MODULE_DIR=modules/network/$MODULE_NAME diff --git a/modules/network/gateway/gateway.Dockerfile b/modules/network/gateway/gateway.Dockerfile index 885e4a9f0..2b72174ab 100644 --- a/modules/network/gateway/gateway.Dockerfile +++ b/modules/network/gateway/gateway.Dockerfile @@ -12,8 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Image name: test-run/gateway -FROM test-run/base:latest +# Image name: testrun/gateway +FROM testrun/base:latest ARG MODULE_NAME=gateway ARG MODULE_DIR=modules/network/$MODULE_NAME diff --git a/testing/unit/build.sh b/modules/network/host/bin/start_network_service similarity index 82% rename from testing/unit/build.sh rename to modules/network/host/bin/start_network_service index db84e0299..b94b6ff7c 100644 --- a/testing/unit/build.sh +++ b/modules/network/host/bin/start_network_service @@ -1,4 +1,4 @@ -#!/bin/bash -e +#!/bin/bash # Copyright 2023 Google LLC # @@ -14,4 +14,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -sudo docker build -f testing/unit/unit_test.Dockerfile -t testrun/unit-test . \ No newline at end of file +echo "Starting host service..." + +# Keep host container running until stopped +while true; do + sleep 3 +done diff --git a/modules/network/host/conf/module_config.json b/modules/network/host/conf/module_config.json new file mode 100644 index 000000000..87ec39a35 --- /dev/null +++ b/modules/network/host/conf/module_config.json @@ -0,0 +1,24 @@ +{ + "config": { + "meta": { + "name": "host", + "display_name": "Host", + "description": "Used to access host level networking operations" + }, + "network": { + "host": true + }, + "grpc":{ + "port": 5001 + }, + "docker": { + "depends_on": "base", + "mounts": [ + { + "source": "runtime/network", + "target": "/runtime/network" + } + ] + } + } +} \ No newline at end of file diff --git a/modules/network/host/host.Dockerfile b/modules/network/host/host.Dockerfile new file mode 100644 index 000000000..60c8bf59a --- /dev/null +++ b/modules/network/host/host.Dockerfile @@ -0,0 +1,34 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Image name: testrun/host +FROM testrun/base:latest + +ARG MODULE_NAME=host +ARG MODULE_DIR=modules/network/$MODULE_NAME + +#Update and get all additional requirements not contained in the base image +RUN apt-get update --fix-missing + +# Install all necessary packages +RUN apt-get install -y net-tools ethtool + +# Copy over all configuration files +COPY $MODULE_DIR/conf /testrun/conf + +# Copy over all binary files +COPY $MODULE_DIR/bin /testrun/bin + +# Copy over all python files +COPY $MODULE_DIR/python /testrun/python diff --git a/modules/network/host/python/src/grpc_server/network_service.py b/modules/network/host/python/src/grpc_server/network_service.py new file mode 100644 index 000000000..cbb3a1b7a --- /dev/null +++ b/modules/network/host/python/src/grpc_server/network_service.py @@ -0,0 +1,120 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""gRPC Network Service for the Host network module""" +import proto.grpc_pb2_grpc as pb2_grpc +import proto.grpc_pb2 as pb2 + +import traceback +from common import logger +from common import util + +LOG_NAME = 'network_service' +LOGGER = None + + +class NetworkService(pb2_grpc.HostNetworkModule): + """gRPC endpoints for the Host container""" + + def __init__(self): + global LOGGER + LOGGER = logger.get_logger(LOG_NAME, 'host') + + def CheckInterfaceStatus(self, request, context): # pylint: disable=W0613 + try: + status = self.check_interface_status(request.iface_name) + return pb2.CheckInterfaceStatusResponse(code=200, status=status) + except Exception as e: # pylint: disable=W0718 + fail_message = 'Failed to read iface status: ' + str(e) + LOGGER.error(fail_message) + LOGGER.error(traceback.format_exc()) + return pb2.CheckInterfaceStatusResponse(code=500, status=False) + + def GetIfaceConnectionStats(self, request, context): # pylint: disable=W0613 + try: + stats = self.get_iface_connection_stats(request.iface_name) + return pb2.GetIfaceStatsResponse(code=200, stats=stats) + except Exception as e: # pylint: disable=W0718 + fail_message = 'Failed to read connection stats: ' + str(e) + LOGGER.error(fail_message) + LOGGER.error(traceback.format_exc()) + return pb2.GetIfaceStatsResponse(code=500, stats=False) + + def GetIfacePortStats(self, request, context): # pylint: disable=W0613 + try: + stats = self.get_iface_port_stats(request.iface_name) + return pb2.GetIfaceStatsResponse(code=200, stats=stats) + except Exception as e: # pylint: disable=W0718 + fail_message = 'Failed to read port stats: ' + str(e) + LOGGER.error(fail_message) + LOGGER.error(traceback.format_exc()) + return pb2.GetIfaceStatsResponse(code=500, stats=False) + + def SetIfaceDown(self, request, context): # pylint: disable=W0613 + try: + success = self.set_interface_down(request.iface_name) + return pb2.SetIfaceResponse(code=200, success=success) + except Exception as e: # pylint: disable=W0718 + fail_message = 'Failed to set interface down: ' + str(e) + LOGGER.error(fail_message) + LOGGER.error(traceback.format_exc()) + return pb2.SetIfaceResponse(code=500, success=False) + + def SetIfaceUp(self, request, context): # pylint: disable=W0613 + try: + success = self.set_interface_up(request.iface_name) + return pb2.SetIfaceResponse(code=200, success=success) + except Exception as e: # pylint: disable=W0718 + fail_message = 'Failed to set interface up: ' + str(e) + LOGGER.error(fail_message) + LOGGER.error(traceback.format_exc()) + return pb2.SetIfaceResponse(code=500, success=False) + + def check_interface_status(self, interface_name): + output = util.run_command(cmd=f'ip link show {interface_name}', output=True) + if 'state DOWN ' in output[0]: + return False + else: + return True + + def get_iface_connection_stats(self, iface): + """Extract information about the physical connection""" + response = util.run_command(f'ethtool {iface}') + if len(response[1]) == 0: + return response[0] + else: + return None + + def get_iface_port_stats(self, iface): + """Extract information about packets connection""" + response = util.run_command(f'ethtool -S {iface}') + if len(response[1]) == 0: + return response[0] + else: + return None + + def set_interface_up(self, interface_name): + """Set the interface to the up state""" + response = util.run_command('ip link set dev ' + interface_name + ' up') + if len(response[1]) == 0: + return response[0] + else: + return None + + def set_interface_down(self, interface_name): + """Set the interface to the up state""" + response = util.run_command('ip link set dev ' + interface_name + ' down') + if len(response[1]) == 0: + return response[0] + else: + return None diff --git a/modules/network/host/python/src/grpc_server/proto/grpc.proto b/modules/network/host/python/src/grpc_server/proto/grpc.proto new file mode 100644 index 000000000..c881b13f7 --- /dev/null +++ b/modules/network/host/python/src/grpc_server/proto/grpc.proto @@ -0,0 +1,37 @@ +syntax = "proto3"; + +service HostNetworkModule { + + rpc CheckInterfaceStatus(CheckInterfaceStatusRequest) returns (CheckInterfaceStatusResponse) {}; + rpc GetIfaceConnectionStats(GetIfaceStatsRequest) returns (GetIfaceStatsResponse) {}; + rpc SetIfaceDown(SetIfaceRequest) returns (SetIfaceResponse) {}; + rpc SetIfaceUp(SetIfaceRequest) returns (SetIfaceResponse) {}; +} + +message CheckInterfaceStatusRequest { + string iface_name = 1; +} + +message CheckInterfaceStatusResponse { + int32 code = 1; + bool status = 2; +} + +message GetIfaceStatsRequest { + string iface_name = 1; +} + +message GetIfaceStatsResponse { + int32 code = 1; + string stats = 2; +} + +message SetIfaceRequest { + string iface_name = 1; +} + +message SetIfaceResponse { + int32 code = 1; + bool success = 2; +} + diff --git a/modules/network/host/python/src/grpc_server/start_server.py b/modules/network/host/python/src/grpc_server/start_server.py new file mode 100644 index 000000000..962277188 --- /dev/null +++ b/modules/network/host/python/src/grpc_server/start_server.py @@ -0,0 +1,50 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Base class for starting the gRPC server for a network module.""" +from concurrent import futures +import grpc +import proto.grpc_pb2_grpc as pb2_grpc +from network_service import NetworkService +import argparse + +DEFAULT_PORT = '5001' + + +def serve(port): + server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) + pb2_grpc.add_HostNetworkModuleServicer_to_server(NetworkService(), server) + server.add_insecure_port('[::]:' + port) + server.start() + server.wait_for_termination() + + +def run(): + parser = argparse.ArgumentParser( + description='GRPC Server for Network Module', + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + parser.add_argument('-p', + '--port', + default=DEFAULT_PORT, + help='Define the default port to run the server on.') + + args = parser.parse_args() + + port = args.port + + print('gRPC server starting on port ' + port) + serve(port) + +if __name__ == '__main__': + run() diff --git a/modules/network/ntp/ntp.Dockerfile b/modules/network/ntp/ntp.Dockerfile index aa6f63e3f..d047770ef 100644 --- a/modules/network/ntp/ntp.Dockerfile +++ b/modules/network/ntp/ntp.Dockerfile @@ -12,8 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Image name: test-run/ntp -FROM test-run/base:latest +# Image name: testrun/ntp +FROM testrun/base:latest ARG MODULE_NAME=ntp ARG MODULE_DIR=modules/network/$MODULE_NAME @@ -21,6 +21,8 @@ ARG MODULE_DIR=modules/network/$MODULE_NAME # Set DEBIAN_FRONTEND to noninteractive mode ENV DEBIAN_FRONTEND=noninteractive +RUN apt-get update + # Install all necessary packages RUN apt-get install -y chrony diff --git a/modules/network/ntp/python/src/ntp_server.py b/modules/network/ntp/python/src/ntp_server.py index 42fe21e77..fbe3ac17e 100644 --- a/modules/network/ntp/python/src/ntp_server.py +++ b/modules/network/ntp/python/src/ntp_server.py @@ -38,7 +38,7 @@ def is_running(self): if __name__ == '__main__': ntp = NTPServer() ntp.start() - # give some time for the server to start + # Give some time for the server to start running = False for _ in range(10): running = ntp.is_running() diff --git a/modules/network/radius/bin/start_network_service b/modules/network/radius/bin/start_network_service index d285c20d9..aad840c3a 100644 --- a/modules/network/radius/bin/start_network_service +++ b/modules/network/radius/bin/start_network_service @@ -27,7 +27,7 @@ cp $CONF_DIR/ca.crt /etc/ssl/certs/ca-certificates.crt python3 -u $PYTHON_SRC_DIR/authenticator.py & -#Create and set permissions on the log file +# Create and set permissions on the log file touch $LOG_FILE chown $HOST_USER $LOG_FILE diff --git a/modules/network/radius/python/requirements.txt b/modules/network/radius/python/requirements.txt index 37d126cb1..c814da515 100644 --- a/modules/network/radius/python/requirements.txt +++ b/modules/network/radius/python/requirements.txt @@ -1,3 +1,11 @@ -eventlet -pbr -transitions \ No newline at end of file +# Dependencies to user defined packages +# Package dependencies should always be defined before the user defined +# packages to prevent auto-upgrades of stable dependencies +dnspython==2.6.1 +greenlet==3.0.3 +six==1.16.0 + +# User defined packages +eventlet==0.36.1 +pbr==6.1.0 +transitions==0.9.2 diff --git a/modules/network/radius/radius.Dockerfile b/modules/network/radius/radius.Dockerfile index 4c8f8fac5..802480ce9 100644 --- a/modules/network/radius/radius.Dockerfile +++ b/modules/network/radius/radius.Dockerfile @@ -12,8 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Image name: test-run/radius -FROM test-run/base:latest +# Image name: testrun/radius +FROM testrun/base:latest ARG MODULE_NAME=radius ARG MODULE_DIR=modules/network/$MODULE_NAME @@ -25,7 +25,8 @@ RUN apt-get update && apt-get install -y openssl freeradius git RUN git clone --branch 0.0.25 https://github.com/faucetsdn/chewie # Install chewie as Python module -RUN pip3 install chewie/ +# --break-system-packages flag used to bypass PEP668 +RUN pip3 install --break-system-packages chewie/ EXPOSE 1812/udp EXPOSE 1813/udp @@ -40,4 +41,5 @@ COPY $MODULE_DIR/bin /testrun/bin COPY $MODULE_DIR/python /testrun/python # Install all python requirements for the module -RUN pip3 install -r /testrun/python/requirements.txt \ No newline at end of file +# --break-system-packages flag used to bypass PEP668 +RUN pip3 install --break-system-packages -r /testrun/python/requirements.txt \ No newline at end of file diff --git a/modules/network/template/template.Dockerfile b/modules/network/template/template.Dockerfile index 1c3060496..265725258 100644 --- a/modules/network/template/template.Dockerfile +++ b/modules/network/template/template.Dockerfile @@ -12,8 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Image name: test-run/template -FROM test-run/base:latest +# Image name: testrun/template +FROM testrun/base:latest ARG MODULE_NAME=template ARG MODULE_DIR=modules/network/$MODULE_NAME diff --git a/modules/test/base/README.md b/modules/test/base/README.md index e7f05d80e..24a725607 100644 --- a/modules/test/base/README.md +++ b/modules/test/base/README.md @@ -14,6 +14,13 @@ The ```config/module_config.json``` provides the name and description of the mod Within the ```python/src``` directory, basic logging and environment variables are provided to the test module. +Within the ```usr/local/etc``` directory there is a local copy of the MAC OUI database. This is just in case a new copy is unable to be downloaded during the install or update process. + +## GRPC server +Within the python directory, GRPC client code is provided to allow test modules to programmatically modify the various network services provided by Testrun. + +These currently include obtaining information about and controlling the DHCP servers in failover configuration. + ## Tests covered No tests are run by this module \ No newline at end of file diff --git a/modules/test/base/base.Dockerfile b/modules/test/base/base.Dockerfile index 4d8c0399a..253270ea9 100644 --- a/modules/test/base/base.Dockerfile +++ b/modules/test/base/base.Dockerfile @@ -12,17 +12,23 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Image name: test-run/base-test -FROM ubuntu@sha256:e6173d4dc55e76b87c4af8db8821b1feae4146dd47341e4d431118c7dd060a74 +# Builder stage +# Image name: testrun/base-test +FROM python:3.10-slim AS builder ARG MODULE_NAME=base ARG MODULE_DIR=modules/test/$MODULE_NAME ARG COMMON_DIR=framework/python/src/common -RUN apt-get update +# Install additional requirements needed to build python packages +RUN apt-get update && \ + apt-get install -y gcc dos2unix -# Install common software -RUN DEBIAN_FRONTEND=noninteractive apt-get install -yq net-tools iputils-ping tzdata tcpdump iproute2 jq python3 python3-pip dos2unix nmap wget --fix-missing +# Create the virtual environment +RUN python -m venv /opt/venv + +# Activate the virtual environment +ENV PATH="/opt/venv/bin:$PATH" # Install common python modules COPY $COMMON_DIR/ /testrun/python/src/common @@ -31,7 +37,7 @@ COPY $COMMON_DIR/ /testrun/python/src/common COPY $MODULE_DIR/python /testrun/python # Install all python requirements for the module -RUN pip3 install -r /testrun/python/requirements.txt +RUN pip install -r /testrun/python/requirements.txt # Copy over all binary files COPY $MODULE_DIR/bin /testrun/bin @@ -49,6 +55,7 @@ ARG CONTAINER_PROTO_DIR=testrun/python/src/grpc_server/proto COPY $NET_MODULE_DIR/dhcp-1/$NET_MODULE_PROTO_DIR $CONTAINER_PROTO_DIR/dhcp1/ COPY $NET_MODULE_DIR/dhcp-2/$NET_MODULE_PROTO_DIR $CONTAINER_PROTO_DIR/dhcp2/ +COPY $NET_MODULE_DIR/host/$NET_MODULE_PROTO_DIR $CONTAINER_PROTO_DIR/host/ # Copy the cached version of oui.txt incase the download fails RUN mkdir -p /usr/local/etc @@ -57,5 +64,21 @@ COPY $MODULE_DIR/usr/local/etc/oui.txt /usr/local/etc/oui.txt # Update the oui.txt file from ieee RUN wget https://standards-oui.ieee.org/oui.txt -O /usr/local/etc/oui.txt || echo "Unable to update the MAC OUI database" +# Operational stage +FROM python:3.10-slim + +# Install common software +RUN DEBIAN_FRONTEND=noninteractive apt-get update && apt-get install -yq net-tools iputils-ping tzdata tcpdump iproute2 jq dos2unix nmap wget procps --fix-missing + +# Get the virtual environment from builder stage +COPY --from=builder /opt/venv /opt/venv + +# Copy over all testrun files from the builder stage +COPY --from=builder /testrun /testrun +COPY --from=builder /usr/local/etc/oui.txt /usr/local/etc/oui.txt + +# Activate the virtual environment by setting the PATH +ENV PATH="/opt/venv/bin:$PATH" + # Start the test module ENTRYPOINT [ "/testrun/bin/start" ] \ No newline at end of file diff --git a/modules/test/base/bin/setup b/modules/test/base/bin/setup new file mode 100644 index 000000000..514466548 --- /dev/null +++ b/modules/test/base/bin/setup @@ -0,0 +1,81 @@ +#!/bin/bash + +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Define the local mount point to store local files to +export OUTPUT_DIR="/runtime/output" + +# Directory where all binaries will be loaded +export BIN_DIR="/testrun/bin" + +# Default interface should be veth0 for all containers +export IFACE=veth0 + +# Assign the current host user +export HOST_USER=$(whoami) + +# Create a local user that matches the same as the host +# to be used for correct file ownership for various logs +# HOST_USER mapped in via docker container environment variables +if ! id "$HOST_USER" &>/dev/null; then + useradd "$HOST_USER" +else + echo User $HOST_USER already exists +fi + +# Create the output directory +mkdir -p "$OUTPUT_DIR" + +# Set permissions on the output files +chown -R $HOST_USER $OUTPUT_DIR + +# Enable IPv6 for all containers +sysctl net.ipv6.conf.all.disable_ipv6=0 +sysctl -p + +# Read in the config file +CONF_FILE="/testrun/conf/module_config.json" +CONF=`cat $CONF_FILE` + +if [[ -z $CONF ]] +then + echo "No config file present at $CONF_FILE. Exiting startup." + exit 1 +fi + +# Extract the necessary config parameters +export MODULE_NAME=$(echo "$CONF" | jq -r '.config.meta.name') +export NETWORK_REQUIRED=$(echo "$CONF" | jq -r '.config.network') +export GRPC=$(echo "$CONF" | jq -r '.config.grpc') + +# Validate the module name is present +if [[ -z "$MODULE_NAME" || "$MODULE_NAME" == "null" ]] +then + echo "No module name present in $CONF_FILE. Exiting startup." + exit 1 +fi + +# Setup the PYTHONPATH so all imports work as expected +echo "Setting up PYTHONPATH..." +export PYTHONPATH=$($BIN_DIR/setup_python_path) +echo "PYTHONPATH: $PYTHONPATH" + +echo "Configuring binary files..." +$BIN_DIR/setup_binaries $BIN_DIR + +# Build all gRPC files from the proto for use in +# gRPC clients for communications to network modules +echo "Building gRPC files from available proto files..." +$BIN_DIR/setup_grpc_clients \ No newline at end of file diff --git a/modules/test/base/bin/start b/modules/test/base/bin/start index 37902b868..d1f29989f 100755 --- a/modules/test/base/bin/start +++ b/modules/test/base/bin/start @@ -14,4 +14,15 @@ # See the License for the specific language governing permissions and # limitations under the License. -/testrun/bin/start_module \ No newline at end of file +# Allow one argument which is the unit test file to run +# instead of running the test module +UNIT_TEST_FILE=$1 + +source /testrun/bin/setup + +# Conditionally run start_module based on RUN +if [[ -z "$UNIT_TEST_FILE" ]];then + /testrun/bin/start_module +else + python3 $UNIT_TEST_FILE +fi diff --git a/modules/test/base/bin/start_module b/modules/test/base/bin/start_module index 0ee68fa6a..fb79cc018 100644 --- a/modules/test/base/bin/start_module +++ b/modules/test/base/bin/start_module @@ -1,102 +1,46 @@ -#!/bin/bash - -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Define the local mount point to store local files to -OUTPUT_DIR="/runtime/output" - -# Directory where all binaries will be loaded -BIN_DIR="/testrun/bin" - -# Default interface should be veth0 for all containers -IFACE=veth0 - -# Create a local user that matches the same as the host -# to be used for correct file ownership for various logs -# HOST_USER mapped in via docker container environemnt variables -useradd $HOST_USER - -# Set permissions on the output files -chown -R $HOST_USER $OUTPUT_DIR - -# Enable IPv6 for all containers -sysctl net.ipv6.conf.all.disable_ipv6=0 -sysctl -p - -# Read in the config file -CONF_FILE="/testrun/conf/module_config.json" -CONF=`cat $CONF_FILE` - -if [[ -z $CONF ]] -then - echo "No config file present at $CONF_FILE. Exiting startup." - exit 1 -fi - -# Extract the necessary config parameters -MODULE_NAME=$(echo "$CONF" | jq -r '.config.meta.name') -NETWORK_REQUIRED=$(echo "$CONF" | jq -r '.config.network') -GRPC=$(echo "$CONF" | jq -r '.config.grpc') - -# Validate the module name is present -if [[ -z "$MODULE_NAME" || "$MODULE_NAME" == "null" ]] -then - echo "No module name present in $CONF_FILE. Exiting startup." - exit 1 -fi - -# Setup the PYTHONPATH so all imports work as expected -echo "Setting up PYTHONPATH..." -export PYTHONPATH=$($BIN_DIR/setup_python_path) -echo "PYTHONPATH: $PYTHONPATH" - -# Build all gRPC files from the proto for use in -# gRPC clients for communications to network modules -echo "Building gRPC files from available proto files..." -$BIN_DIR/setup_grpc_clients - -echo "Configuring binary files..." -$BIN_DIR/setup_binaries $BIN_DIR - -echo "Starting module $MODULE_NAME..." - -# Only start network services if the test container needs -# a network connection to run its tests -if [ $NETWORK_REQUIRED == "true" ];then - # Wait for interface to become ready - $BIN_DIR/wait_for_interface $IFACE - - # Start network capture - $BIN_DIR/capture $MODULE_NAME $IFACE -fi - -# Start the grpc server -if [[ ! -z $GRPC && ! $GRPC == "null" ]] -then - GRPC_PORT=$(echo "$GRPC" | jq -r '.port') - if [[ ! -z $GRPC_PORT && ! $GRPC_PORT == "null" ]] - then - echo "gRPC port resolved from config: $GRPC_PORT" - $BIN_DIR/start_grpc "-p $GRPC_PORT" - else - $BIN_DIR/start_grpc - fi -fi - -# Small pause to let all core services stabalize -sleep 3 - -# Start the test module +#!/bin/bash + +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +echo "Starting module $MODULE_NAME..." + +# Only start network services if the test container needs +# a network connection to run its tests +if [ $NETWORK_REQUIRED == "true" ];then + # Wait for interface to become ready + $BIN_DIR/wait_for_interface $IFACE + + # Start network capture + $BIN_DIR/capture $MODULE_NAME $IFACE +fi + +# Start the grpc server +if [[ ! -z $GRPC && ! $GRPC == "null" ]] +then + GRPC_PORT=$(echo "$GRPC" | jq -r '.port') + if [[ ! -z $GRPC_PORT && ! $GRPC_PORT == "null" ]] + then + echo "gRPC port resolved from config: $GRPC_PORT" + $BIN_DIR/start_grpc "-p $GRPC_PORT" + else + $BIN_DIR/start_grpc + fi +fi + +# Small pause to let all core services stabalize +sleep 3 + +# Start the test module $BIN_DIR/start_test_module $MODULE_NAME $IFACE \ No newline at end of file diff --git a/modules/test/base/python/requirements.txt b/modules/test/base/python/requirements.txt index 9d9473d74..0ed8a792d 100644 --- a/modules/test/base/python/requirements.txt +++ b/modules/test/base/python/requirements.txt @@ -1,3 +1,9 @@ -grpcio -grpcio-tools -netifaces \ No newline at end of file +# Dependencies to user defined packages +# Package dependencies should always be defined before the user defined +# packages to prevent auto-upgrades of stable dependencies +protobuf==5.28.0 + +# User defined packages +grpcio==1.67.1 +grpcio-tools==1.67.1 +netifaces==0.11.0 \ No newline at end of file diff --git a/modules/test/base/python/src/grpc/proto/host/client.py b/modules/test/base/python/src/grpc/proto/host/client.py new file mode 100644 index 000000000..e08d3376a --- /dev/null +++ b/modules/test/base/python/src/grpc/proto/host/client.py @@ -0,0 +1,63 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License +"""gRPC client module for the secondary DHCP Server""" +import grpc +import host.grpc_pb2_grpc as pb2_grpc +import host.grpc_pb2 as pb2 + +DEFAULT_PORT = '5001' +DEFAULT_HOST = 'external.localhost' # Default DHCP2 server + + +class Client(): + """gRPC Client for the secondary DHCP server""" + def __init__(self, port=DEFAULT_PORT, host=DEFAULT_HOST): + self._port = port + self._host = host + + # Create a gRPC channel to connect to the server + self._channel = grpc.insecure_channel(self._host + ':' + self._port) + + # Create a gRPC stub + self._stub = pb2_grpc.HostNetworkModuleStub(self._channel) + + def check_interface_status(self, iface_name): + # Create a request message + request = pb2.CheckInterfaceStatusRequest() + request.iface_name = iface_name + + # Make the RPC call + response = self._stub.CheckInterfaceStatus(request) + + return response + + def set_iface_down(self, iface_name): + # Create a request message + request = pb2.SetIfaceRequest() + request.iface_name = iface_name + + # Make the RPC call + response = self._stub.SetIfaceDown(request) + + return response + + def set_iface_up(self, iface_name): + # Create a request message + request = pb2.SetIfaceRequest() + request.iface_name = iface_name + + # Make the RPC call + response = self._stub.SetIfaceUp(request) + + return response diff --git a/modules/test/base/python/src/logger.py b/modules/test/base/python/src/logger.py deleted file mode 100644 index e6a2b004c..000000000 --- a/modules/test/base/python/src/logger.py +++ /dev/null @@ -1,61 +0,0 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Sets up the logger to be used for the test modules.""" -import json -import logging - -LOGGERS = {} -_LOG_FORMAT = '%(asctime)s %(name)-8s %(levelname)-7s %(message)s' -_DATE_FORMAT = '%b %02d %H:%M:%S' -_DEFAULT_LEVEL = logging.INFO -_CONF_FILE_NAME = 'testrun/system.json' -_LOG_DIR = '/runtime/output/' - -# Set log level -try: - with open(_CONF_FILE_NAME, - encoding='UTF-8') as config_json_file: - system_conf_json = json.load(config_json_file) - - log_level_str = system_conf_json['log_level'] - log_level = logging.getLevelName(log_level_str) -except OSError: - # TODO: Print out warning that log level is incorrect or missing - log_level = _DEFAULT_LEVEL - -log_format = logging.Formatter(fmt=_LOG_FORMAT, datefmt=_DATE_FORMAT) - - -def add_file_handler(log, log_file, log_dir=_LOG_DIR): - handler = logging.FileHandler(log_dir + log_file + '.log') - handler.setFormatter(log_format) - log.addHandler(handler) - - -def add_stream_handler(log): - handler = logging.StreamHandler() - handler.setFormatter(log_format) - log.addHandler(handler) - - -def get_logger(name, log_file=None, log_dir=_LOG_DIR): - if name not in LOGGERS: - LOGGERS[name] = logging.getLogger(name) - LOGGERS[name].setLevel(log_level) - add_stream_handler(LOGGERS[name]) - if log_file is not None: - log_dir = log_dir if log_dir is not None else _LOG_DIR - add_file_handler(LOGGERS[name], log_file, log_dir=log_dir) - return LOGGERS[name] diff --git a/modules/test/base/python/src/test_module.py b/modules/test/base/python/src/test_module.py index 00f74df82..21de78143 100644 --- a/modules/test/base/python/src/test_module.py +++ b/modules/test/base/python/src/test_module.py @@ -17,6 +17,9 @@ import os import util from datetime import datetime +import traceback + +from common.statuses import TestResult LOGGER = None RESULTS_DIR = '/runtime/output/' @@ -29,7 +32,6 @@ class TestModule: def __init__(self, module_name, log_name, - log_dir=None, conf_file=CONF_FILE, results_dir=RESULTS_DIR): self._module_name = module_name @@ -38,19 +40,17 @@ def __init__(self, self._ipv4_addr = os.environ.get('IPV4_ADDR', '') self._ipv4_subnet = os.environ.get('IPV4_SUBNET', '') self._ipv6_subnet = os.environ.get('IPV6_SUBNET', '') - self._add_logger(log_name=log_name, - module_name=module_name, - log_dir=log_dir) + self._dev_iface_mac = os.environ.get('DEV_IFACE_MAC', '') + self._device_test_pack = json.loads(os.environ.get('DEVICE_TEST_PACK', '')) + self._add_logger(log_name=log_name) self._config = self._read_config( conf_file=conf_file if conf_file is not None else CONF_FILE) self._device_ipv4_addr = None self._device_ipv6_addr = None - def _add_logger(self, log_name, module_name, log_dir=None): + def _add_logger(self, log_name): global LOGGER - LOGGER = logger.get_logger(name=log_name, - log_file=module_name, - log_dir=log_dir) # pylint: disable=E1123 + LOGGER = logger.get_logger(name=log_name) def generate_module_report(self): pass @@ -64,22 +64,48 @@ def _get_tests(self): def _get_device_tests(self, device_test_module): module_tests = self._config['config']['tests'] - if device_test_module is None: - return module_tests - elif not device_test_module['enabled']: - return [] - else: - for test in module_tests: - # Resolve device specific configurations for the test if it exists - # and update module test config with device config options - if 'tests' in device_test_module: - if test['name'] in device_test_module['tests']: - dev_test_config = device_test_module['tests'][test['name']] - if 'enabled' in dev_test_config: - test['enabled'] = dev_test_config['enabled'] - if 'config' in test and 'config' in dev_test_config: - test['config'].update(dev_test_config['config']) - return module_tests + tests_to_run = module_tests + + # If no device specific tests have been provided, add all + if device_test_module is not None: + # Do not run any tests if module is disabled for this device + if not device_test_module['enabled']: + return [] + + # Tests that will be removed because they are not in the test pack + remove_tests = [] + + # Check if all tests are in the test pack and enabled for the device + for test in tests_to_run: + + # Resolve device specific configurations for the test if it exists + # and update module test config with device config options + if 'tests' in device_test_module: + + if test['name'] in device_test_module['tests']: + dev_test_config = device_test_module['tests'][test['name']] + + # Check if the test is enabled in the device config + if 'enabled' in dev_test_config: + test['enabled'] = dev_test_config['enabled'] + + # Copy over any device specific test configuration + if 'config' in test and 'config' in dev_test_config: + test['config'].update(dev_test_config['config']) + + # Search for the module test in the test pack + found = False + for test_pack_test in self._device_test_pack['tests']: + if test_pack_test['name'] == test['name']: + # Test is in the test pack + found = True + + if not found: + remove_tests.append(test) + for test in remove_tests: + tests_to_run.remove(test) + + return tests_to_run def _get_device_test_module(self): if 'DEVICE_TEST_MODULES' in os.environ: @@ -113,21 +139,27 @@ def run_tests(self): except Exception as e: # pylint: disable=W0718 LOGGER.error(f'An error occurred whilst running {test["name"]}') LOGGER.error(e) + traceback.print_exc() else: - LOGGER.info(f'Test {test["name"]} not implemented. Skipping') + LOGGER.error(f'Test {test["name"]} has not been implemented') + result = TestResult.ERROR, 'This test could not be found' else: LOGGER.debug(f'Test {test["name"]} is disabled') + result = (TestResult.DISABLED, + 'This test did not run because it is disabled') + # Check if the test module has returned a result if result is not None: + # Compliant or non-compliant as a boolean only if isinstance(result, bool): - test['result'] = 'Compliant' if result else 'Non-Compliant' + test['result'] = (TestResult.COMPLIANT + if result else TestResult.NON_COMPLIANT) test['description'] = 'No description was provided for this test' else: - # TODO: This is assuming that result is an array but haven't checked # Error result if result[0] is None: - test['result'] = 'Error' + test['result'] = TestResult.ERROR if len(result) > 1: test['description'] = result[1] else: @@ -135,13 +167,14 @@ def run_tests(self): # Compliant / Non-Compliant result elif isinstance(result[0], bool): - test['result'] = 'Compliant' if result[0] else 'Non-Compliant' + test['result'] = (TestResult.COMPLIANT + if result[0] else TestResult.NON_COMPLIANT) # Result may be a string, e.g Error, Feature Not Detected elif isinstance(result[0], str): test['result'] = result[0] else: LOGGER.error(f'Unknown result detected: {result[0]}') - test['result'] = 'Error' + test['result'] = TestResult.ERROR # Check that description is a string if isinstance(result[1], str): @@ -152,12 +185,17 @@ def run_tests(self): # Check if details were provided if len(result)>2: test['details'] = result[2] + + # Check if tags were provided + if len(result)>3: + test['tags'] = result[3] else: - test['result'] = 'Error' + LOGGER.debug('No result was returned from the test module') + test['result'] = TestResult.ERROR test['description'] = 'An error occured whilst running this test' # Remove the steps to resolve if compliant already - if (test['result'] == 'Compliant' and 'recommendations' in test): + if (test['result'] == TestResult.COMPLIANT and 'recommendations' in test): test.pop('recommendations') test['end'] = datetime.now().isoformat() @@ -182,7 +220,7 @@ def _write_results(self, results): def _get_device_ipv4(self): command = f"""/testrun/bin/get_ipv4_addr {self._ipv4_subnet} {self._device_mac.upper()}""" - text = util.run_command(command)[0] + text = util.run_command(command)[0] # pylint: disable=E1120 if text: return text.split('\n')[0] return None diff --git a/modules/test/base/python/src/util.py b/modules/test/base/python/src/util.py deleted file mode 100644 index 006648037..000000000 --- a/modules/test/base/python/src/util.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Provides basic utilities for a test module.""" -import subprocess -import shlex -import logger - -LOGGER = logger.get_logger('util') - -def run_command(cmd, output=True): - """Runs a process at the os level - By default, returns the standard output and error output - If the caller sets optional output parameter to False, - will only return a boolean result indicating if it was - successful in running the command. Failure is indicated - by any return code from the process other than zero.""" - - success = False - with subprocess.Popen(shlex.split(cmd), - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) as process: - stdout, stderr = process.communicate() - if process.returncode != 0 and output: - err_msg = f'{stderr.strip()}. Code: {process.returncode}' - LOGGER.error('Command Failed: ' + cmd) - LOGGER.error('Error: ' + err_msg) - else: - success = True - LOGGER.debug('Command succeeded: ' + cmd) - if output: - out = stdout.strip().decode('utf-8') - LOGGER.debug('Command output: ' + out) - return out, stderr - else: - return success diff --git a/modules/test/baseline/baseline.Dockerfile b/modules/test/baseline/baseline.Dockerfile index f7d21f8c8..7a83c8de2 100644 --- a/modules/test/baseline/baseline.Dockerfile +++ b/modules/test/baseline/baseline.Dockerfile @@ -12,8 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Image name: test-run/baseline-test -FROM test-run/base-test:latest +# Image name: testrun/baseline-test +FROM testrun/base-test:latest ARG MODULE_NAME=baseline ARG MODULE_DIR=modules/test/$MODULE_NAME diff --git a/modules/test/baseline/bin/start_test_module b/modules/test/baseline/bin/start_test_module index a529c2fcf..c3209261a 100644 --- a/modules/test/baseline/bin/start_test_module +++ b/modules/test/baseline/bin/start_test_module @@ -41,11 +41,8 @@ else fi # Create and set permissions on the log files -LOG_FILE=/runtime/output/$MODULE_NAME.log RESULT_FILE=/runtime/output/$MODULE_NAME-result.json -touch $LOG_FILE touch $RESULT_FILE -chown $HOST_USER $LOG_FILE chown $HOST_USER $RESULT_FILE # Run the python scrip that will execute the tests for this module diff --git a/modules/test/baseline/conf/module_config.json b/modules/test/baseline/conf/module_config.json index f28f74a06..d120f1c71 100644 --- a/modules/test/baseline/conf/module_config.json +++ b/modules/test/baseline/conf/module_config.json @@ -16,20 +16,17 @@ { "name": "baseline.compliant", "test_description": "Simulate a compliant test", - "expected_behavior": "A compliant test result is generated", - "required_result": "Required" + "expected_behavior": "A compliant test result is generated" }, { "name": "baseline.non_compliant", "test_description": "Simulate a non-compliant test", - "expected_behavior": "A non-compliant test result is generated", - "required_result": "Recommended" + "expected_behavior": "A non-compliant test result is generated" }, { "name": "baseline.skipped", "test_description": "Simulate a skipped test", - "expected_behavior": "A skipped test result is generated", - "required_result": "Skipped" + "expected_behavior": "A skipped test result is generated" } ] } diff --git a/modules/test/conn/README.md b/modules/test/conn/README.md index c2f6377c6..10c7e6917 100644 --- a/modules/test/conn/README.md +++ b/modules/test/conn/README.md @@ -17,11 +17,15 @@ Within the ```python/src``` directory, the below tests are executed. A few dhcp | connection.port_link | The network switch port connected to the device has an active link without errors | When the etherent cable is connected to the port, the device triggers the port to its enabled \"Link UP\" (LEDs illuminate on device and switch ports if present) state, and the switch shows no errors with the LEDs and when interrogated with a \"show interface\" command on most network switches. | Required | | connection.port_speed | The network switch port connected to the device has auto-negotiated a speed that is 10 Mbps or higher | When the ethernet cable is connected to the port, the device autonegotiates a speed that can be checked with the \"show interface\" command on most network switches. The output of this command must also show that the \"configured speed\" is set to \"auto\". | Required | | connection.port_duplex | The network switch port connected to the device has auto-negotiated full-duplex. | When the ethernet cable is connected to the port, the device autonegotiates a full-duplex connection. | Required | +| connection.switch.arp_inspection | The device implements ARP correctly as per RFC826 | Device continues to operate correctly when ARP inspection is enabled on the switch. No functionality is lost with ARP inspection enabled. | Required | +| connection.switch.dhcp_snooping | The device operates as a DHCP client and operates correctly when DHCP snooping is enabled on a switch. | Device continues to operate correctly when DHCP snooping is enabled on the switch. | Required | | connection.dhcp_address | The device under test has received an IP address from the DHCP server and responds to an ICMP echo (ping) request | The device is not set up with a static IP address. The device accepts an IP address from a DHCP server (RFC 2131) and responds successfully to an ICMP echo (ping) request. | Required | | connection.mac_address | Check and note device physical address. | N/A | Required | | connection.mac_oui | The device under test has a MAC address prefix that is registered against a known manufacturer. | The MAC address prefix is registered in the IEEE Organizationally Unique Identifier database. | Required | | connection.private_address | The device under test accepts an IP address that is compliant with RFC 1918 Address Allocation for Private Internets. | The device under test accepts IP addresses within all ranges specified in RFC 1918 and communicates using these addresses. The Internet Assigned Numbers Authority (IANA) has reserved the following three blocks of the IP address space for private internets: 10.0.0.0 - 10.255.255.255 (10/8 prefix), 172.16.0.0 - 172.31.255.255 (172.16/12 prefix), 192.168.0.0 - 192.168.255.255 (192.168/16 prefix). | Required | | connection.shared_address | Ensure the device supports RFC 6598 IANA-Reserved IPv4 Prefix for Shared Address Space | The device under test accepts IP addresses within the range specified in RFC 6598 and communicates using these addresses. | Required | +| connection.dhcp_disconnect | The device under test issues a new DHCPREQUEST packet after a port ph ysical disconnection and reconnection. | A client SHOULD use DHCP to reacquire or verify its IP address and network parameters whenever the local network parameters may have changed; e.g., at system boot time or after a disconnection from the local network, as the local network configuration may change without the client's or user's knowledge. If a client has knowledge ofa previous network address and is unable to contact a local DHCP server, the client may continue to use the previous network addres until the lease for that address expires. If the lease expires before the client can contact a DHCP server, the client must immediately discontinue use of the previous network address and may inform local users of the problem. | Required | +| connection.dhcp_disconnect_ip_change | When device is disconnected, update device IP on the DHCP server and reconnect the device. Ensure device received new IP address. | If IP address for a device was changed on the DHCP server while the device was disconnected then the device should request and update the new IP upon reconnecting to the network | Required | | connection.single_ip | The network switch port connected to the device reports only one IP address for the device under test. | The device under test does not behave as a network switch and only requests one IP address. This test is to avoid that devices implement network switches that allow connecting strings of daisy-chained devices to one single network port, as this would not make 802.1x port-based authentication possible. | Required | | connection.target_ping | The device under test responds to an ICMP echo (ping) request. | The device under test responds to an ICMP echo (ping) request. | Required | | connection.ipaddr.ip_change | The device responds to a ping (ICMP echo request) to the new IP address it has received after the initial DHCP lease has expired. | If the lease expires before the client receives a DHCPACK, the client moves to the INIT state, MUST immediately stop any other network processing, and requires network initialization parameters as if the client were uninitialized. If the client then receives a DHCPACK allocating the client its previous network address, the client SHOULD continue network processing. If the client is given a new network address, it MUST NOT continue using the previous network address and SHOULD notify the local users of the problem. | Required | diff --git a/modules/test/conn/bin/get_packet_counts.sh b/modules/test/conn/bin/get_packet_counts.sh new file mode 100644 index 000000000..fab933a08 --- /dev/null +++ b/modules/test/conn/bin/get_packet_counts.sh @@ -0,0 +1,52 @@ +#!/bin/bash + +# Check if MAC address and pcap file arguments are provided +if [ -z "$1" ] || [ -z "$2" ]; then + echo "Usage: $0 " + exit 1 +fi + +# Assign MAC address and pcap file from arguments +PCAP_FILE="$1" +MAC_ADDRESS="$2" + +# Check if the pcap file exists +if [ ! -f "$PCAP_FILE" ]; then + echo "Error: File $PCAP_FILE does not exist." + exit 1 +fi + +# Count multicast packets from the MAC address +multicast_from_count=$(tshark -r "$PCAP_FILE" -Y "(eth.dst[0] == 1) && eth.src == $MAC_ADDRESS" -T fields -e frame.number | wc -l) + +# Count multicast packets to the MAC address +multicast_to_count=$(tshark -r "$PCAP_FILE" -Y "(eth.dst[0] == 1) && eth.dst == $MAC_ADDRESS" -T fields -e frame.number | wc -l) + +# Count broadcast packets from the MAC address (broadcast MAC address is FF:FF:FF:FF:FF:FF) +broadcast_from_count=$(tshark -r "$PCAP_FILE" -Y "eth.dst == ff:ff:ff:ff:ff:ff && eth.src == $MAC_ADDRESS" -T fields -e frame.number | wc -l) + +# Count broadcast packets to the MAC address +broadcast_to_count=$(tshark -r "$PCAP_FILE" -Y "eth.dst == ff:ff:ff:ff:ff:ff && eth.dst == $MAC_ADDRESS" -T fields -e frame.number | wc -l) + +# Count unicast packets from the MAC address +unicast_from_count=$(tshark -r "$PCAP_FILE" -Y "eth.dst != ff:ff:ff:ff:ff:ff && (eth.dst[0] & 1) == 0 && eth.src == $MAC_ADDRESS" -T fields -e frame.number | wc -l) + +# Count unicast packets to the MAC address +unicast_to_count=$(tshark -r "$PCAP_FILE" -Y "eth.dst != ff:ff:ff:ff:ff:ff && (eth.dst[0] & 1) == 0 && eth.dst == $MAC_ADDRESS" -T fields -e frame.number | wc -l) + +# Output the results as a JSON object +echo "{ + \"mac_address\": \"$MAC_ADDRESS\", + \"multicast\": { + \"from\": $multicast_from_count, + \"to\": $multicast_to_count + }, + \"broadcast\": { + \"from\": $broadcast_from_count, + \"to\": $broadcast_to_count + }, + \"unicast\": { + \"from\": $unicast_from_count, + \"to\": $unicast_to_count + } +}" diff --git a/modules/test/conn/bin/start_test_module b/modules/test/conn/bin/start_test_module index d85ae7d6b..4cdba0ae9 100644 --- a/modules/test/conn/bin/start_test_module +++ b/modules/test/conn/bin/start_test_module @@ -40,9 +40,7 @@ fi # Create and set permissions on the log files LOG_FILE=/runtime/output/$MODULE_NAME.log RESULT_FILE=/runtime/output/$MODULE_NAME-result.json -touch $LOG_FILE touch $RESULT_FILE -chown $HOST_USER $LOG_FILE chown $HOST_USER $RESULT_FILE # Run the python script that will execute the tests for this module diff --git a/modules/test/conn/conf/module_config.json b/modules/test/conn/conf/module_config.json index 5289e7eb0..03ae51d89 100644 --- a/modules/test/conn/conf/module_config.json +++ b/modules/test/conn/conf/module_config.json @@ -16,48 +16,42 @@ { "name": "connection.port_link", "test_description": "The network switch port connected to the device has an active link without errors", - "expected_behavior": "When the etherent cable is connected to the port, the device triggers the port to its enabled \"Link UP\" (LEDs illuminate on device and switch ports if present) state, and the switch shows no errors with the LEDs and when interrogated with a \"show interface\" command on most network switches.", - "required_result": "Required" + "expected_behavior": "When the ethernet cable is connected to the port, the device triggers the port to its enabled \"Link UP\" (LEDs illuminate on device and switch ports if present) state, and the switch shows no errors with the LEDs and when interrogated with a \"show interface\" command on most network switches." }, { "name": "connection.port_speed", "test_description": "The network switch port connected to the device has auto-negotiated a speed that is 10 Mbps or higher", - "expected_behavior": "When the ethernet cable is connected to the port, the device autonegotiates a speed that can be checked with the \"show interface\" command on most network switches. The output of this command must also show that the \"configured speed\" is set to \"auto\".", - "required_result": "Required" + "expected_behavior": "When the ethernet cable is connected to the port, the device autonegotiates a speed that can be checked with the \"show interface\" command on most network switches. The output of this command must also show that the \"configured speed\" is set to \"auto\"." }, { "name": "connection.port_duplex", "test_description": "The network switch port connected to the device has auto-negotiated full-duplex", - "expected_behavior": "When the ethernet cable is connected to the port, the device autonegotiates a full-duplex connection.", - "required_result": "Required" + "expected_behavior": "When the ethernet cable is connected to the port, the device autonegotiates a full-duplex connection." }, { "name": "connection.switch.arp_inspection", "test_description": "The device implements ARP correctly as per RFC826", - "expected_behavior": "Device continues to operate correctly when ARP inspection is enabled on the switch. No functionality is lost with ARP inspection enabled.", - "required_result": "Required" + "expected_behavior": "Device continues to operate correctly when ARP inspection is enabled on the switch. No functionality is lost with ARP inspection enabled." }, { "name": "connection.switch.dhcp_snooping", "test_description": "The device operates as a DHCP client and operates correctly when DHCP snooping is enabled on a switch.", - "expected_behavior": "Device continues to operate correctly when DHCP snooping is enabled on the switch.", - "required_result": "Required" + "expected_behavior": "Device continues to operate correctly when DHCP snooping is enabled on the switch." }, { "name": "connection.dhcp_address", "test_description": "The device under test has received an IP address from the DHCP server and responds to an ICMP echo (ping) request", - "expected_behavior": "The device is not setup with a static IP address. The device accepts an IP address from a DHCP server (RFC 2131) and responds succesfully to an ICMP echo (ping) request.", - "required_result": "Required", + "expected_behavior": "The device is not setup with a static IP address. The device accepts an IP address from a DHCP server (RFC 2131) and responds successfully to an ICMP echo (ping) request.", "recommendations": [ "Enable DHCP", - "Install a DHCP client" + "Install a DHCP client", + "Ensure that your DHCP client renews its lease at the correct time" ] }, { "name": "connection.mac_address", "test_description": "Check and note device physical address.", "expected_behavior": "N/A", - "required_result": "Required", "recommendations": [ "Ensure that the MAC address is set by hardware only" ] @@ -66,7 +60,6 @@ "name": "connection.mac_oui", "test_description": "The device under test has a MAC address prefix that is registered against a known manufacturer.", "expected_behavior": "The MAC address prefix is registered in the IEEE Organizationally Unique Identifier database.", - "required_result": "Required", "recommendations": [ "Register the device MAC address with IEEE" ] @@ -75,7 +68,6 @@ "name": "connection.private_address", "test_description": "The device under test accepts an IP address that is compliant with RFC 1918 Address Allocation for Private Internets.", "expected_behavior": "The device under test accepts IP addresses within all ranges specified in RFC 1918 and communicates using these addresses. The Internet Assigned Numbers Authority (IANA) has reserved the following three blocks of the IP address space for private internets. 10.0.0.0 - 10.255.255.255.255 (10/8 prefix). 172.16.0.0 - 172.31.255.255 (172.16/12 prefix). 192.168.0.0 - 192.168.255.255 (192.168/16 prefix)", - "required_result": "Required", "config": { "lease_wait_time_sec": 60, "ranges": [ @@ -101,7 +93,6 @@ "name": "connection.shared_address", "test_description": "Ensure the device supports RFC 6598 IANA-Reserved IPv4 Prefix for Shared Address Space", "expected_behavior": "The device under test accepts IP addresses within the ranges specified in RFC 6598 and communicates using these addresses", - "required_result": "Required", "config": { "lease_wait_time_sec": 60, "ranges": [ @@ -116,11 +107,20 @@ "Enable shared address space support in the DHCP client" ] }, + { + "name": "connection.dhcp_disconnect", + "test_description": "The device under test issues a new DHCPREQUEST packet after a port physical disconnection and reconnection", + "expected_behavior": "A client SHOULD use DHCP to reacquire or verify its IP address and network parameters whenever the local network parameters may have changed; e.g., at system boot time or after a disconnection from the local network, as the local network configuration may change without the client's or user's knowledge. If a client has knowledge ofa previous network address and is unable to contact a local DHCP server, the client may continue to use the previous network address until the lease for that address expires. If the lease expires before the client can contact a DHCP server, the client must immediately discontinue use of the previous network address and may inform local users of the problem." + }, + { + "name": "connection.dhcp_disconnect_ip_change", + "test_description": "When device is disconnected, update device IP on the DHCP server and reconnect the device. Ensure device received new IP address", + "expected_behavior": "If IP address for a device was changed on the DHCP server while the device was disconnected then the device should request and update the new IP upon reconnecting to the network" + }, { "name": "connection.single_ip", "test_description": "The network switch port connected to the device reports only one IP address for the device under test.", - "expected_behavior": "The device under test does not behave as a network switch and only requets one IP address. This test is to avoid that devices implement network switches that allow connecting strings of daisy chained devices to one single network port, as this would not make 802.1x port based authentication possible.", - "required_result": "Required", + "expected_behavior": "The device under test does not behave as a network switch and only requests one IP address. This test is to avoid that devices implement network switches that allow connecting strings of daisy chained devices to one single network port, as this would not make 802.1x port based authentication possible.", "recommendations": [ "Ensure that all ports on the device are isolated", "Ensure only one DHCP client is running" @@ -130,7 +130,6 @@ "name": "connection.target_ping", "test_description": "The device under test responds to an ICMP echo (ping) request.", "expected_behavior": "The device under test responds to an ICMP echo (ping) request.", - "required_result": "Required", "recommendations": [ "Configure device to allow ICMP requests (ping)", "Create a firewall exception to allow ICMP requests from LAN" @@ -139,8 +138,7 @@ { "name": "connection.ipaddr.ip_change", "test_description": "The device responds to a ping (ICMP echo request) to the new IP address it has received after the initial DHCP lease has expired.", - "expected_behavior": "If the lease expires before the client receiveds a DHCPACK, the client moves to INIT state, MUST immediately stop any other network processing and requires network initialization parameters as if the client were uninitialized. If the client then receives a DHCPACK allocating the client its previous network addres, the client SHOULD continue network processing. If the client is given a new network address, it MUST NOT continue using the previous network address and SHOULD notify the local users of the problem.", - "required_result": "Required", + "expected_behavior": "If the lease expires before the client receives a DHCPACK, the client moves to INIT state, MUST immediately stop any other network processing and requires network initialization parameters as if the client were uninitialized. If the client then receives a DHCPACK allocating the client its previous network address, the client SHOULD continue network processing. If the client is given a new network address, it MUST NOT continue using the previous network address and SHOULD notify the local users of the problem.", "config":{ "lease_wait_time_sec": 60 }, @@ -152,7 +150,6 @@ "name": "connection.ipaddr.dhcp_failover", "test_description": "The device has requested a DHCPREQUEST/REBIND to the DHCP failover server after the primary DHCP server has been brought down.", "expected_behavior": "", - "required_result": "Required", "config":{ "lease_wait_time_sec": 60 }, @@ -164,7 +161,6 @@ "name": "connection.ipv6_slaac", "test_description": "The device forms a valid IPv6 address as a combination of the IPv6 router prefix and the device interface identifier", "expected_behavior": "The device under test complies with RFC4862 and forms a valid IPv6 SLAAC address", - "required_result": "Required", "recommendations": [ "Install a network manager that supports IPv6", "Disable DHCPv6" @@ -174,11 +170,15 @@ "name": "connection.ipv6_ping", "test_description": "The device responds to an IPv6 ping (ICMPv6 Echo) request to the SLAAC address", "expected_behavior": "The device responds to the ping as per RFC4443", - "required_result": "Required", "recommendations": [ "Enable ping response to IPv6 ICMP requests in network manager settings", "Create a firewall exception to allow ICMPv6 via LAN" ] + }, + { + "name": "communication.network.type", + "test_description": "How does the device communicate (flow type) - Unicast, multicast broadcast?", + "expected_behavior": "Informational - One or more of these flow types are used" } ] } diff --git a/modules/test/conn/conn.Dockerfile b/modules/test/conn/conn.Dockerfile index a9f523e44..cda0858c9 100644 --- a/modules/test/conn/conn.Dockerfile +++ b/modules/test/conn/conn.Dockerfile @@ -12,8 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Image name: test-run/conn-test -FROM test-run/base-test:latest +# Image name: testrun/conn-test +FROM testrun/base-test:latest ARG MODULE_NAME=conn ARG MODULE_DIR=modules/test/$MODULE_NAME @@ -21,13 +21,13 @@ ARG GRPC_PROTO_DIR=/testrun/python/src/grpc/proto/dhcp ARG GRPC_PROTO_FILE="grpc.proto" # Install all necessary packages -RUN apt-get install -y wget +RUN apt-get install -y wget tshark # Load the requirements file COPY $MODULE_DIR/python/requirements.txt /testrun/python # Install all python requirements for the module -RUN pip3 install -r /testrun/python/requirements.txt +RUN pip install -r /testrun/python/requirements.txt # Copy over all configuration files COPY $MODULE_DIR/conf /testrun/conf @@ -35,5 +35,11 @@ COPY $MODULE_DIR/conf /testrun/conf # Copy over all binary files COPY $MODULE_DIR/bin /testrun/bin +# Remove incorrect line endings +RUN dos2unix /testrun/bin/* + +# Make sure all the bin files are executable +RUN chmod u+x /testrun/bin/* + # Copy over all python files COPY $MODULE_DIR/python /testrun/python \ No newline at end of file diff --git a/modules/test/conn/python/requirements.txt b/modules/test/conn/python/requirements.txt index 7244e9e75..50d56eb37 100644 --- a/modules/test/conn/python/requirements.txt +++ b/modules/test/conn/python/requirements.txt @@ -1,3 +1,12 @@ -pyOpenSSL -scapy -python-dateutil \ No newline at end of file +# Dependencies to user defined packages +# Package dependencies should always be defined before the user defined +# packages to prevent auto-upgrades of stable dependencies +cffi==1.17.1 +cryptography==43.0.1 +pycparser==2.22 +six==1.16.0 + +# User defined packages +pyOpenSSL==24.2.1 +scapy==2.6.0 +python-dateutil==2.9.0.post0 diff --git a/modules/test/conn/python/src/connection_module.py b/modules/test/conn/python/src/connection_module.py index 5e8b78ec3..cfdbad89b 100644 --- a/modules/test/conn/python/src/connection_module.py +++ b/modules/test/conn/python/src/connection_module.py @@ -15,15 +15,20 @@ import util import time import traceback -from scapy.all import rdpcap, DHCP, ARP, Ether, IPv6, ICMPv6ND_NS +import os +from scapy.error import Scapy_Exception +from scapy.all import rdpcap, DHCP, ARP, Ether, ICMP, IPv6, ICMPv6ND_NS from test_module import TestModule from dhcp1.client import Client as DHCPClient1 from dhcp2.client import Client as DHCPClient2 +from host.client import Client as HostClient from dhcp_util import DHCPUtil from port_stats_util import PortStatsUtil +import json LOG_NAME = 'test_connection' OUI_FILE = '/usr/local/etc/oui.txt' +DEFAULT_BIN_DIR = '/testrun/bin' STARTUP_CAPTURE_FILE = '/runtime/device/startup.pcap' MONITOR_CAPTURE_FILE = '/runtime/device/monitor.pcap' DHCP_CAPTURE_FILE = '/runtime/network/dhcp-1.pcap' @@ -39,19 +44,29 @@ class ConnectionModule(TestModule): """Connection Test module""" - def __init__(self, module, log_dir=None, conf_file=None, results_dir=None): + def __init__(self, + module, + conf_file=None, + results_dir=None, + startup_capture_file=STARTUP_CAPTURE_FILE, + monitor_capture_file=MONITOR_CAPTURE_FILE, + bin_dir=DEFAULT_BIN_DIR): + super().__init__(module_name=module, log_name=LOG_NAME, - log_dir=log_dir, conf_file=conf_file, results_dir=results_dir) global LOGGER LOGGER = self._get_logger() + self.startup_capture_file = startup_capture_file + self.monitor_capture_file = monitor_capture_file self._port_stats = PortStatsUtil(logger=LOGGER) self.dhcp1_client = DHCPClient1() self.dhcp2_client = DHCPClient2() + self.host_client = HostClient() self._dhcp_util = DHCPUtil(self.dhcp1_client, self.dhcp2_client, LOGGER) self._lease_wait_time_sec = LEASE_WAIT_TIME_DEFAULT + self._bin_dir = bin_dir # ToDo: Move this into some level of testing, leave for # reference until tests are implemented with these calls @@ -106,7 +121,8 @@ def _connection_switch_arp_inspection(self): no_arp = True # Read all the pcap files - packets = rdpcap(STARTUP_CAPTURE_FILE) + rdpcap(MONITOR_CAPTURE_FILE) + packets = rdpcap(self.startup_capture_file) + rdpcap( + self.monitor_capture_file) for packet in packets: # We are not interested in packets unless they are ARP packets @@ -123,12 +139,8 @@ def _connection_switch_arp_inspection(self): # Check MAC address matches IP address if (arp_packet.hwsrc == self._device_mac - and (arp_packet.psrc not in ( - self._device_ipv4_addr, - '0.0.0.0' - )) and not arp_packet.psrc.startswith( - '169.254' - )): + and (arp_packet.psrc not in (self._device_ipv4_addr, '0.0.0.0')) + and not arp_packet.psrc.startswith('169.254')): LOGGER.info(f'Bad ARP packet detected for MAC: {self._device_mac}') LOGGER.info(f'''ARP packet from IP {arp_packet.psrc} does not match {self._device_ipv4_addr}''') @@ -137,7 +149,7 @@ def _connection_switch_arp_inspection(self): if no_arp: return None, 'No ARP packets from the device found' - return True, 'Device uses ARP' + return True, 'Device uses ARP correctly' def _connection_switch_dhcp_snooping(self): LOGGER.info('Running connection.switch.dhcp_snooping') @@ -145,7 +157,8 @@ def _connection_switch_dhcp_snooping(self): disallowed_dhcp_types = [2, 4, 5, 6, 9, 10, 12, 13, 15, 17] # Read all the pcap files - packets = rdpcap(STARTUP_CAPTURE_FILE) + rdpcap(MONITOR_CAPTURE_FILE) + packets = rdpcap(self.startup_capture_file) + rdpcap( + self.monitor_capture_file) for packet in packets: # We are not interested in packets unless they are DHCP packets @@ -158,6 +171,11 @@ def _connection_switch_dhcp_snooping(self): dhcp_type = self._get_dhcp_type(packet) if dhcp_type in disallowed_dhcp_types: + + # Check if packet is responding with port unreachable + if ICMP in packet and packet[ICMP].type == 3: + continue + return False, 'Device has sent disallowed DHCP message' return True, 'Device does not act as a DHCP server' @@ -189,8 +207,10 @@ def _connection_dhcp_address(self): LOGGER.info('No IP information found in lease: ' + self._device_mac) return False, 'No IP information found in lease: ' + self._device_mac else: - LOGGER.info('No DHCP lease could be found: ' + self._device_mac) - return False, 'No DHCP lease could be found: ' + self._device_mac + LOGGER.info('No DHCP lease could be found for MAC ' + self._device_mac + + ' at the time of this test') + return (False, 'No DHCP lease could be found for MAC ' + + self._device_mac + ' at the time of this test') def _connection_mac_address(self): LOGGER.info('Running connection.mac_address') @@ -220,7 +240,8 @@ def _connection_single_ip(self): return result, 'No MAC address found.' # Read all the pcap files containing DHCP packet information - packets = rdpcap(STARTUP_CAPTURE_FILE) + rdpcap(MONITOR_CAPTURE_FILE) + packets = rdpcap(self.startup_capture_file) + rdpcap( + self.monitor_capture_file) # Extract MAC addresses from DHCP packets mac_addresses = set() @@ -230,7 +251,8 @@ def _connection_single_ip(self): if self._get_dhcp_type(packet) == 3: mac_address = packet[Ether].src LOGGER.info('DHCPREQUEST detected MAC address: ' + mac_address) - if not mac_address.startswith(TR_CONTAINER_MAC_PREFIX): + if (not mac_address.startswith(TR_CONTAINER_MAC_PREFIX) + and mac_address != self._dev_iface_mac): mac_addresses.add(mac_address.upper()) # Check if the device mac address is in the list of DHCPREQUESTs @@ -306,8 +328,10 @@ def _connection_ipaddr_ip_change(self, config): else: result = None, 'Failed to create reserved lease for device' else: - LOGGER.info('Device has no current DHCP lease') - result = None, 'Device has no current DHCP lease' + LOGGER.info('Device has no current DHCP lease so ' + + 'this test could not be run') + result = None, ('Device has no current DHCP lease so ' + + 'this test could not be run') # Restore the network self._dhcp_util.restore_failover_dhcp_server() LOGGER.info('Waiting 30 seconds for reserved lease to expire') @@ -360,12 +384,198 @@ def _connection_ipaddr_dhcp_failover(self, config): else: result = False, 'Device did not respond to ping' else: - result = None, 'Device has no current DHCP lease' + result = ( + None, + 'Device has no current DHCP lease so this test could not be run') else: LOGGER.error('Network is not ready for this test. Skipping') result = None, 'Network is not ready for this test' return result + def _connection_dhcp_disconnect(self): + LOGGER.info('Running connection.dhcp.disconnect') + result = None + description = '' + dev_iface = os.getenv('DEV_IFACE') + + try: + iface_status = self.host_client.check_interface_status(dev_iface) + if iface_status.code == 200: + LOGGER.info('Successfully resolved iface status') + if iface_status.status: + lease = self._dhcp_util.get_cur_lease(mac_address=self._device_mac, + timeout=self._lease_wait_time_sec) + if lease is not None: + LOGGER.info('Current device lease resolved') + if self._dhcp_util.is_lease_active(lease): + + # Disable the device interface + iface_down = self.host_client.set_iface_down(dev_iface) + if iface_down: + LOGGER.info('Device interface set to down state') + + # Wait for the lease to expire + self._dhcp_util.wait_for_lease_expire(lease, + self._lease_wait_time_sec) + + # Wait an additonal 10 seconds to better test a true disconnect + # state + LOGGER.info('Waiting 10 seconds before bringing iface back up') + time.sleep(10) + + # Enable the device interface + iface_up = self.host_client.set_iface_up(dev_iface) + if iface_up: + LOGGER.info('Device interface set to up state') + + # Confirm device receives a new lease + if self._dhcp_util.get_cur_lease( + mac_address=self._device_mac, + timeout=self._lease_wait_time_sec): + if self._dhcp_util.is_lease_active(lease): + result = True + description = ( + 'Device received a DHCP lease after disconnect') + else: + result = False + description = ( + 'Could not confirm DHCP lease active after disconnect') + else: + result = False + description = ( + 'Device did not recieve a DHCP lease after disconnect') + else: + result = 'Error' + description = 'Failed to set device interface to up state' + else: + result = 'Error' + description = 'Failed to set device interface to down state' + else: + result = 'Error' + description = 'No active lease available for device' + else: + result = 'Error' + description = 'Device interface is down' + else: + result = 'Error' + description = 'Device interface could not be resolved' + + except Exception: + LOGGER.error('Unable to connect to gRPC server') + result = 'Error' + description = ( + 'Unable to connect to gRPC server' + ) + return result, description + + def _connection_dhcp_disconnect_ip_change(self): + LOGGER.info('Running connection.dhcp.disconnect_ip_change') + result = None + description = '' + reserved_lease = None + dev_iface = os.getenv('DEV_IFACE') + if self._dhcp_util.setup_single_dhcp_server(): + try: + iface_status = self.host_client.check_interface_status(dev_iface) + if iface_status.code == 200: + LOGGER.info('Successfully resolved iface status') + if iface_status.status: + lease = self._dhcp_util.get_cur_lease( + mac_address=self._device_mac, timeout=self._lease_wait_time_sec) + if lease is not None: + LOGGER.info('Current device lease resolved') + if self._dhcp_util.is_lease_active(lease): + + # Add a reserved lease with a different IP + ip_address = '10.10.10.30' + reserved_lease = self._dhcp_util.add_reserved_lease( + lease['hostname'], self._device_mac, ip_address) + + # Disable the device interface + iface_down = self.host_client.set_iface_down(dev_iface) + if iface_down: + LOGGER.info('Device interface set to down state') + + # Wait for the lease to expire + self._dhcp_util.wait_for_lease_expire(lease, + self._lease_wait_time_sec) + + if reserved_lease: + # Wait an additonal 10 seconds to better test a true + # disconnect state + LOGGER.info( + 'Waiting 10 seconds before bringing iface back up') + time.sleep(10) + + # Enable the device interface + iface_up = self.host_client.set_iface_up(dev_iface) + if iface_up: + LOGGER.info('Device interface set to up state') + # Confirm device receives a new lease + reserved_lease_accepted = False + LOGGER.info('Checking device accepted new ip') + for _ in range(5): + LOGGER.info('Pinging device at IP: ' + ip_address) + if self._ping(ip_address): + LOGGER.debug('Ping success') + LOGGER.debug( + 'Reserved lease confirmed active in device') + reserved_lease_accepted = True + break + else: + LOGGER.info('Device did not respond to ping') + time.sleep(5) # Wait 5 seconds before trying again + + if reserved_lease_accepted: + result = True + description = ('Device received expected IP address ' + 'after disconnect') + else: + result = False + description = ( + 'Could not confirm DHCP lease active after disconnect' + ) + else: + result = 'Error' + description = 'Failed to set device interface to up state' + else: + result = 'Error' + description = ( + 'Failed to set reserved address in DHCP server' + ) + else: + result = 'Error' + description = 'Failed to set device interface to down state' + else: + result = 'Error' + description = 'No active lease available for device' + else: + result = 'Error' + description = 'Device interface is down' + else: + result = 'Error' + description = 'Device interface could not be resolved' + except Exception: + LOGGER.error('Unable to connect to gRPC server') + result = 'Error' + description = ( + 'Unable to connect to gRPC server' + ) + else: + result = 'Error' + description = 'Failed to configure network for test' + + if reserved_lease: + self._dhcp_util.delete_reserved_lease(self._device_mac) + + # Restore the network + self._dhcp_util.restore_failover_dhcp_server() + LOGGER.info('Waiting 30 seconds for reserved lease to expire') + time.sleep(30) + self._dhcp_util.get_cur_lease(mac_address=self._device_mac, + timeout=self._lease_wait_time_sec) + return result, description + def _get_oui_manufacturer(self, mac_address): # Do some quick fixes on the format of the mac_address # to match the oui file pattern @@ -381,10 +591,12 @@ def _connection_ipv6_slaac(self): LOGGER.info('Running connection.ipv6_slaac') result = None - slac_test, sends_ipv6 = self._has_slaac_addres() + slac_test, sends_ipv6 = self._has_slaac_address() if slac_test: result = True, f'Device has formed SLAAC address {self._device_ipv6_addr}' - if result is None: + elif slac_test is None: + result = 'Error', 'An error occurred whilst running this test' + else: if sends_ipv6: LOGGER.info('Device does not support IPv6 SLAAC') result = False, 'Device does not support IPv6 SLAAC' @@ -393,9 +605,15 @@ def _connection_ipv6_slaac(self): result = False, 'Device does not support IPv6' return result - def _has_slaac_addres(self): - packet_capture = (rdpcap(STARTUP_CAPTURE_FILE) + - rdpcap(MONITOR_CAPTURE_FILE) + rdpcap(DHCP_CAPTURE_FILE)) + def _has_slaac_address(self): + packet_capture = (rdpcap(self.startup_capture_file) + + rdpcap(self.monitor_capture_file)) + + try: + packet_capture += rdpcap(DHCP_CAPTURE_FILE) + except (FileNotFoundError, Scapy_Exception): + LOGGER.error('dhcp-1.pcap not found or empty, ignoring') + sends_ipv6 = False for packet_number, packet in enumerate(packet_capture, start=1): if IPv6 in packet and packet.src == self._device_mac: @@ -432,7 +650,7 @@ def _ping(self, host, ipv6=False): cmd += ' -6 ' if ipv6 else '' cmd += str(host) #cmd = 'ping -c 1 ' + str(host) - success = util.run_command(cmd, output=False) + success = util.run_command(cmd, output=False) # pylint: disable=E1120 return success def restore_failover_dhcp_server(self, subnet): @@ -478,6 +696,67 @@ def setup_single_dhcp_server(self): else: return False, 'Secondary DHCP server stop command failed' + def _communication_network_type(self): + try: + result = 'Informational' + description = '' + details = '' + packets = self.get_network_packet_types() + details = packets + # Initialize a list for detected packet types + packet_types = [] + + # Check for the presence of each packet type and append to the list + if (packets['multicast']['from'] > 0) or (packets['multicast']['to'] > 0): + packet_types.append('Multicast') + if (packets['broadcast']['from'] > 0) or (packets['broadcast']['to'] > 0): + packet_types.append('Broadcast') + if (packets['unicast']['from'] > 0) or (packets['unicast']['to'] > 0): + packet_types.append('Unicast') + + # Construct the description if any packet types were detected + if packet_types: + description = 'Packet types detected: ' + ', '.join(packet_types) + else: + description = 'No multicast, broadcast or unicast detected' + + except Exception as e: # pylint: disable=W0718 + LOGGER.error(e) + result = 'Error' + return result, description, details + + def get_network_packet_types(self): + combined_results = { + 'mac_address': self._device_mac, + 'multicast': { + 'from': 0, + 'to': 0 + }, + 'broadcast': { + 'from': 0, + 'to': 0 + }, + 'unicast': { + 'from': 0, + 'to': 0 + }, + } + capture_files = [self.startup_capture_file, self.monitor_capture_file] + for capture_file in capture_files: + bin_file = self._bin_dir + '/get_packet_counts.sh' + args = f'"{capture_file}" "{self._device_mac}"' + command = f'{bin_file} {args}' + response = util.run_command(command) + packets = json.loads(response[0].strip()) + # Combine results + combined_results['multicast']['from'] += packets['multicast']['from'] + combined_results['multicast']['to'] += packets['multicast']['to'] + combined_results['broadcast']['from'] += packets['broadcast']['from'] + combined_results['broadcast']['to'] += packets['broadcast']['to'] + combined_results['unicast']['from'] += packets['unicast']['from'] + combined_results['unicast']['to'] += packets['unicast']['to'] + return combined_results + def enable_failover(self): # Move primary DHCP server to primary failover LOGGER.info('Configuring primary failover DHCP server') @@ -500,6 +779,7 @@ def is_ip_in_range(self, ip, start_ip, end_ip): return start_int <= ip_int <= end_int def _run_subnet_test(self, config): + # Resolve the configured dhcp subnet ranges ranges = None if 'ranges' in config: @@ -514,6 +794,7 @@ def _run_subnet_test(self, config): response = self.dhcp1_client.get_dhcp_range() cur_range = {} + if response.code == 200: cur_range['start'] = response.start cur_range['end'] = response.end @@ -526,16 +807,21 @@ def _run_subnet_test(self, config): results = [] dhcp_setup = self.setup_single_dhcp_server() + if dhcp_setup[0]: LOGGER.info(dhcp_setup[1]) lease = self._dhcp_util.get_cur_lease(mac_address=self._device_mac, timeout=self._lease_wait_time_sec) + if lease is not None: if self._dhcp_util.is_lease_active(lease): results = self.test_subnets(ranges) else: - LOGGER.info('Failed to confirm a valid active lease for the device') - return None, 'Failed to confirm a valid active lease for the device' + LOGGER.info('Device has no current DHCP lease ' + + 'so this test could not be run') + return ( + None, + 'Device has no current DHCP lease so this test could not be run') else: LOGGER.error(dhcp_setup[1]) return None, 'Failed to setup DHCP server for test' @@ -549,7 +835,7 @@ def _run_subnet_test(self, config): else: if result['result'] is not None: final_result &= result['result'] - if result['result']: + if not result['result']: final_result_details += result['details'] + '\n' if final_result: @@ -562,10 +848,17 @@ def _run_subnet_test(self, config): # Wait for the current lease to expire lease = self._dhcp_util.get_cur_lease(mac_address=self._device_mac, timeout=self._lease_wait_time_sec) - self._dhcp_util.wait_for_lease_expire(lease, self._lease_wait_time_sec) + + # Check if lease is active + if lease is not None: + self._dhcp_util.wait_for_lease_expire(lease, self._lease_wait_time_sec) + else: + # If not, wait for 30 seconds as a fallback + time.sleep(30) # Wait for a new lease to be provided before exiting test # to prevent other test modules from failing + LOGGER.info('Checking for new lease') # Subnet changes tend to take longer to pick up so we'll allow # for twice the lease wait time @@ -580,9 +873,8 @@ def _run_subnet_test(self, config): else: LOGGER.info('New lease not found. Waiting to check again') - except Exception as e: # pylint: disable=W0718 - LOGGER.error('Failed to restore DHCP server configuration: ' + str(e)) - LOGGER.error(traceback.format_exc()) + except Exception: # pylint: disable=W0718 + LOGGER.error('Failed to restore DHCP server configuration') return final_result, final_result_details diff --git a/modules/test/conn/python/src/dhcp_util.py b/modules/test/conn/python/src/dhcp_util.py index be5f0cac2..22880dab0 100644 --- a/modules/test/conn/python/src/dhcp_util.py +++ b/modules/test/conn/python/src/dhcp_util.py @@ -207,7 +207,7 @@ def is_lease_active(self, lease): def ping(self, host): cmd = 'ping -c 1 ' + str(host) - success = util.run_command(cmd, output=False) + success = util.run_command(cmd, output=False) # pylint: disable=E1120 return success def add_reserved_lease(self, @@ -256,21 +256,30 @@ def setup_single_dhcp_server(self): return False def wait_for_lease_expire(self, lease, max_wait_time=30): - expiration_utc = datetime.strptime(lease['expires'], '%Y-%m-%d %H:%M:%S') - # lease information stored in UTC so we need to convert to local time - expiration = self.utc_to_local(expiration_utc) - time_to_expire = expiration - datetime.now(tz=tz.tzlocal()) - # Wait until the expiration time and padd 5 seconds - # If wait time is longer than max_wait_time, only wait - # for the max wait time - wait_time = min(max_wait_time, - time_to_expire.total_seconds() + - 5) if time_to_expire.total_seconds() > 0 else 0 - LOGGER.info('Time until lease expiration: ' + str(wait_time)) - LOGGER.info('Waiting for current lease to expire: ' + str(expiration)) - if wait_time > 0: - time.sleep(wait_time) - LOGGER.info('Current lease expired.') + + try: + expiration_utc = datetime.strptime(lease['expires'], '%Y-%m-%d %H:%M:%S') + + # Lease information stored in UTC so we need to convert to local time + expiration = self.utc_to_local(expiration_utc) + time_to_expire = expiration - datetime.now(tz=tz.tzlocal()) + + # Wait until the expiration time and padd 5 seconds + # If wait time is longer than max_wait_time, only wait + # for the max wait time + wait_time = min(max_wait_time, + time_to_expire.total_seconds() + + 5) if time_to_expire.total_seconds() > 0 else 0 + + LOGGER.info('Time until lease expiration: ' + str(wait_time)) + LOGGER.info('Waiting for current lease to expire: ' + str(expiration)) + + if wait_time > 0: + time.sleep(wait_time) + LOGGER.info('Current lease expired') + + except TypeError: + LOGGER.error('Device does not have an active lease') # Convert from a UTC datetime to the local time zone def utc_to_local(self, utc_datetime): diff --git a/modules/test/conn/python/src/port_stats_util.py b/modules/test/conn/python/src/port_stats_util.py index d923501eb..a1f68cb03 100644 --- a/modules/test/conn/python/src/port_stats_util.py +++ b/modules/test/conn/python/src/port_stats_util.py @@ -41,7 +41,7 @@ def __init__(self, self.conn_stats = self._read_stats_file(self.ethtool_conn_stats_file) def is_autonegotiate(self): - auto_negotiation = False + auto_negotiation = None auto_negotiation_status = self._get_stat_option(stats=self.conn_stats, option='Auto-negotiation:') if auto_negotiation_status is not None: @@ -66,15 +66,22 @@ def connection_port_link_test(self): option='rx_errors:') rx_errors_post = self._get_stat_option(stats=stats_post, option='rx_errors:') - tx_errors = int(tx_errors_post) - int(tx_errors_pre) - rx_errors = int(rx_errors_post) - int(rx_errors_pre) - if tx_errors > 0 or rx_errors > 0: - result = False - description = 'Port errors detected' - details = f'TX errors: {tx_errors}, RX errors: {rx_errors}' + + # Check that the above have been resolved correctly + if (tx_errors_pre is None or tx_errors_post is None or + rx_errors_pre is None or rx_errors_post is None): + result = 'Error' + description = 'Port stats not available' else: - result = True - description = 'No port errors detected' + tx_errors = int(tx_errors_post) - int(tx_errors_pre) + rx_errors = int(rx_errors_post) - int(rx_errors_pre) + if tx_errors > 0 or rx_errors > 0: + result = False + description = 'Port errors detected' + details = f'TX errors: {tx_errors}, RX errors: {rx_errors}' + else: + result = True + description = 'No port errors detected' return result, description, details def connection_port_duplex_test(self): @@ -83,7 +90,10 @@ def connection_port_duplex_test(self): result = None description = '' details = '' - if not auto_negotiation: + if auto_negotiation is None: + result = 'Error' + description = 'Port stats not available' + elif not auto_negotiation: result = False description = 'Interface not configured for auto-negotiation' else: @@ -104,7 +114,10 @@ def connection_port_speed_test(self): result = None description = '' details = '' - if not auto_negotiation: + if auto_negotiation is None: + result = 'Error' + description = 'Port stats not available' + elif not auto_negotiation: result = False description = 'Interface not configured for auto-negotiation' else: diff --git a/modules/test/dns/README.md b/modules/test/dns/README.md index 13f0df5fd..c2a917b13 100644 --- a/modules/test/dns/README.md +++ b/modules/test/dns/README.md @@ -15,4 +15,5 @@ Within the ```python/src``` directory, the below tests are executed. | ID | Description | Expected behavior | Required result |---|---|---|---| | dns.network.hostname_resolution | Verifies that the device resolves hostnames | The device sends DNS requests | Required | -| dns.network.from_dhcp | Verifies that the device allows for a DNS server to be provided by the DHCP server | The device sends DNS requests to the DNS server provided by the DHCP server | Roadmap | \ No newline at end of file +| dns.network.from_dhcp | Verifies that the device allows for a DNS server to be provided by the DHCP server | The device sends DNS requests to the DNS server provided by the DHCP server | Informational | +| dns.mdns | Does the device has MDNS (or any kind of IP multicast) | Device may send MDNS requests | Informational | \ No newline at end of file diff --git a/modules/test/dns/bin/start_test_module b/modules/test/dns/bin/start_test_module index a529c2fcf..c3209261a 100644 --- a/modules/test/dns/bin/start_test_module +++ b/modules/test/dns/bin/start_test_module @@ -41,11 +41,8 @@ else fi # Create and set permissions on the log files -LOG_FILE=/runtime/output/$MODULE_NAME.log RESULT_FILE=/runtime/output/$MODULE_NAME-result.json -touch $LOG_FILE touch $RESULT_FILE -chown $HOST_USER $LOG_FILE chown $HOST_USER $RESULT_FILE # Run the python scrip that will execute the tests for this module diff --git a/modules/test/dns/conf/module_config.json b/modules/test/dns/conf/module_config.json index 13c9b3236..662273cd7 100644 --- a/modules/test/dns/conf/module_config.json +++ b/modules/test/dns/conf/module_config.json @@ -16,7 +16,6 @@ "name": "dns.network.hostname_resolution", "test_description": "Verify the device sends DNS requests", "expected_behavior": "The device sends DNS requests.", - "required_result": "Required", "recommendations": [ "Install a supported DNS client", "Ensure DNS servers have been set correctly", @@ -27,10 +26,14 @@ "name": "dns.network.from_dhcp", "test_description": "Verify the device allows for a DNS server to be entered automatically", "expected_behavior": "The device sends DNS requests to the DNS server provided by the DHCP server", - "required_result": "Informational", "recommendations": [ "Install a DNS client that supports fetching DNS servers from DHCP options" ] + }, + { + "name": "dns.mdns", + "test_description": "Does the device have MDNS (or any kind of IP multicast)", + "expected_behavior": "Device may send MDNS requests" } ] } diff --git a/modules/test/dns/dns.Dockerfile b/modules/test/dns/dns.Dockerfile index 0197fd72e..461e87899 100644 --- a/modules/test/dns/dns.Dockerfile +++ b/modules/test/dns/dns.Dockerfile @@ -12,8 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Image name: test-run/conn-test -FROM test-run/base-test:latest +# Image name: testrun/conn-test +FROM testrun/base-test:latest ARG MODULE_NAME=dns ARG MODULE_DIR=modules/test/$MODULE_NAME @@ -22,7 +22,7 @@ ARG MODULE_DIR=modules/test/$MODULE_NAME COPY $MODULE_DIR/python/requirements.txt /testrun/python # Install all python requirements for the module -RUN pip3 install -r /testrun/python/requirements.txt +RUN pip install -r /testrun/python/requirements.txt # Copy over all configuration files COPY $MODULE_DIR/conf /testrun/conf diff --git a/modules/test/dns/python/requirements.txt b/modules/test/dns/python/requirements.txt index 93b351f44..f61132516 100644 --- a/modules/test/dns/python/requirements.txt +++ b/modules/test/dns/python/requirements.txt @@ -1 +1,6 @@ -scapy \ No newline at end of file +# Dependencies to user defined packages +# Package dependencies should always be defined before the user defined +# packages to prevent auto-upgrades of stable dependencies + +# User defined packages +scapy==2.6.0 diff --git a/modules/test/dns/python/src/dns_module.py b/modules/test/dns/python/src/dns_module.py index 607a026b5..fe244f0a7 100644 --- a/modules/test/dns/python/src/dns_module.py +++ b/modules/test/dns/python/src/dns_module.py @@ -13,12 +13,13 @@ # limitations under the License. """DNS test module""" import subprocess -from scapy.all import rdpcap, DNS, IP +from scapy.all import rdpcap, DNS, IP, Ether from test_module import TestModule import os +from collections import Counter LOG_NAME = 'test_dns' -MODULE_REPORT_FILE_NAME='dns_report.html' +MODULE_REPORT_FILE_NAME = 'dns_report.html' DNS_SERVER_CAPTURE_FILE = '/runtime/network/dns.pcap' STARTUP_CAPTURE_FILE = '/runtime/device/startup.pcap' MONITOR_CAPTURE_FILE = '/runtime/device/monitor.pcap' @@ -30,7 +31,6 @@ class DNSModule(TestModule): def __init__(self, module, - log_dir=None, conf_file=None, results_dir=None, dns_server_capture_file=DNS_SERVER_CAPTURE_FILE, @@ -38,12 +38,11 @@ def __init__(self, monitor_capture_file=MONITOR_CAPTURE_FILE): super().__init__(module_name=module, log_name=LOG_NAME, - log_dir=log_dir, conf_file=conf_file, results_dir=results_dir) - self.dns_server_capture_file=dns_server_capture_file - self.startup_capture_file=startup_capture_file - self.monitor_capture_file=monitor_capture_file + self.dns_server_capture_file = dns_server_capture_file + self.startup_capture_file = startup_capture_file + self.monitor_capture_file = monitor_capture_file self._dns_server = '10.10.10.4' global LOGGER LOGGER = self._get_logger() @@ -52,21 +51,20 @@ def generate_module_report(self): # Extract DNS data from the pcap file dns_table_data = self.extract_dns_data() - html_content = '

DNS Module

' + html_content = '

DNS Module

' # Set the summary variables - local_requests = sum(1 for row in dns_table_data - if row['Destination'] == - self._dns_server and row['Type'] == 'Query') - external_requests = sum(1 for row in dns_table_data - if row['Destination'] != - self._dns_server and row['Type'] == 'Query') + local_requests = sum( + 1 for row in dns_table_data + if row['Destination'] == self._dns_server and row['Type'] == 'Query') + external_requests = sum( + 1 for row in dns_table_data + if row['Destination'] != self._dns_server and row['Type'] == 'Query') - total_requests = sum(1 for row in dns_table_data - if row['Type'] == 'Query') + total_requests = sum(1 for row in dns_table_data if row['Type'] == 'Query') total_responses = sum(1 for row in dns_table_data - if row['Type'] == 'Response') + if row['Type'] == 'Response') # Add summary table html_content += (f''' @@ -97,25 +95,33 @@ def generate_module_report(self): Source Destination + Resolved IP Type URL + Count ''' - for row in dns_table_data: - table_content += (f''' - - {row['Source']} - {row['Destination']} - {row['Type']} - {row['Data']} - ''') + # Count unique combinations + counter = Counter((row['Source'], row['Destination'], row['ResolvedIP'], + row['Type'], row['Data']) for row in dns_table_data) + + # Generate the HTML table with the count column + for (src, dst, res_ip, typ, dat), count in counter.items(): + table_content += f''' + + {src} + {dst} + {res_ip} + {typ} + {dat} + {count} + ''' table_content += ''' - - ''' + ''' html_content += table_content @@ -149,30 +155,47 @@ def extract_dns_data(self): # Iterate through DNS packets for packet in packets: if DNS in packet and packet.haslayer(IP): - source_ip = packet[IP].src - destination_ip = packet[IP].dst - dns_layer = packet[DNS] - - # 'qr' field indicates query (0) or response (1) - dns_type = 'Query' if dns_layer.qr == 0 else 'Response' - - # Check for the presence of DNS query name - if hasattr(dns_layer, 'qd') and dns_layer.qd is not None: + + # Check if either source or destination MAC matches the device + if self._device_mac in (packet[Ether].src, packet[Ether].dst): + source_ip = packet[IP].src + destination_ip = packet[IP].dst + dns_layer = packet[DNS] + # 'qr' field indicates query (0) or response (1) + dns_type = 'Query' if dns_layer.qr == 0 else 'Response' + + # Check if 'qd' (query data) exists and has at least one entry + if hasattr(dns_layer, 'qd') and dns_layer.qdcount > 0: qname = dns_layer.qd.qname.decode() if dns_layer.qd.qname else 'N/A' - else: + else: qname = 'N/A' - dns_data.append({ - 'Timestamp': float(packet.time), # Timestamp of the DNS packet - 'Source': source_ip, - 'Destination': destination_ip, - 'Type': dns_type, - 'Data': qname[:-1] - }) + resolved_ip = 'N/A' + # If it's a response packet, extract the resolved IP address + # from the answer section + if dns_layer.qr == 1 and hasattr(dns_layer, + 'an') and dns_layer.ancount > 0: + # Loop through all answers in the DNS response + for i in range(dns_layer.ancount): + answer = dns_layer.an[i] + # Check for IPv4 (A record) or IPv6 (AAAA record) + if answer.type == 1: # Indicates an A record (IPv4 address) + resolved_ip = answer.rdata # Extract IPv4 address + break # Stop after finding the first valid resolved IP + elif answer.type == 28: # Indicates an AAAA record (IPv6 address) + resolved_ip = answer.rdata # Extract IPv6 address + break # Stop after finding the first valid resolved IP + + dns_data.append({ + 'Timestamp': float(packet.time), # Timestamp of the DNS packet + 'Source': source_ip, + 'Destination': destination_ip, + 'ResolvedIP': resolved_ip, # Adding the resolved IP address + 'Type': dns_type, + 'Data': qname[:-1] + }) # Filter unique entries based on 'Timestamp' - # DNS Server will duplicate messages caught by - # startup and monitor filtered_unique_dns_data = [] seen_timestamps = set() @@ -273,10 +296,10 @@ def _exec_tcpdump(self, tcpdump_filter, capture_file): LOGGER.debug('tcpdump command: ' + command) with subprocess.Popen(command, - universal_newlines=True, - shell=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) as process: + universal_newlines=True, + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) as process: text = str(process.stdout.read()).rstrip() LOGGER.debug('tcpdump response: ' + text) diff --git a/modules/test/ntp/bin/start_test_module b/modules/test/ntp/bin/start_test_module index a09349cf9..33b2881f4 100644 --- a/modules/test/ntp/bin/start_test_module +++ b/modules/test/ntp/bin/start_test_module @@ -27,11 +27,8 @@ else fi # Create and set permissions on the log files -LOG_FILE=/runtime/output/$MODULE_NAME.log RESULT_FILE=/runtime/output/$MODULE_NAME-result.json -touch $LOG_FILE touch $RESULT_FILE -chown $HOST_USER $LOG_FILE chown $HOST_USER $RESULT_FILE # Run the python scrip that will execute the tests for this module diff --git a/modules/test/ntp/conf/module_config.json b/modules/test/ntp/conf/module_config.json index 55eb3df76..6634b127d 100644 --- a/modules/test/ntp/conf/module_config.json +++ b/modules/test/ntp/conf/module_config.json @@ -9,14 +9,13 @@ "docker": { "depends_on": "base", "enable_container": true, - "timeout": 30 + "timeout": 60 }, "tests":[ { "name": "ntp.network.ntp_support", "test_description": "Does the device request network time sync as client as per RFC 5905 - Network Time Protocol Version 4: Protocol and Algorithms Specification", "expected_behavior": "The device sends an NTPv4 request to the configured NTP server.", - "required_result": "Required", "recommendations": [ "Set the NTP version to v4 in the NTP client", "Install an NTP client that supports NTPv4" @@ -26,7 +25,6 @@ "name": "ntp.network.ntp_dhcp", "test_description": "Accept NTP address over DHCP", "expected_behavior": "Device can accept NTP server address, provided by the DHCP server (DHCP OFFER PACKET)", - "required_result": "Roadmap", "recommendations": [ "Install an NTP client that supports fetching the NTP servers from DHCP options" ] diff --git a/modules/test/ntp/ntp.Dockerfile b/modules/test/ntp/ntp.Dockerfile index 33b06287e..4d9701464 100644 --- a/modules/test/ntp/ntp.Dockerfile +++ b/modules/test/ntp/ntp.Dockerfile @@ -1,5 +1,19 @@ -# Image name: test-run/ntp-test -FROM test-run/base-test:latest +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Image name: testrun/ntp-test +FROM testrun/base-test:latest ARG MODULE_NAME=ntp ARG MODULE_DIR=modules/test/$MODULE_NAME @@ -8,7 +22,7 @@ ARG MODULE_DIR=modules/test/$MODULE_NAME COPY $MODULE_DIR/python/requirements.txt /testrun/python # Install all python requirements for the module -RUN pip3 install -r /testrun/python/requirements.txt +RUN pip install -r /testrun/python/requirements.txt # Copy over all configuration files COPY $MODULE_DIR/conf /testrun/conf diff --git a/modules/test/ntp/python/requirements.txt b/modules/test/ntp/python/requirements.txt index 93b351f44..f61132516 100644 --- a/modules/test/ntp/python/requirements.txt +++ b/modules/test/ntp/python/requirements.txt @@ -1 +1,6 @@ -scapy \ No newline at end of file +# Dependencies to user defined packages +# Package dependencies should always be defined before the user defined +# packages to prevent auto-upgrades of stable dependencies + +# User defined packages +scapy==2.6.0 diff --git a/modules/test/ntp/python/src/ntp_module.py b/modules/test/ntp/python/src/ntp_module.py index 453c992e6..67e2a3c92 100644 --- a/modules/test/ntp/python/src/ntp_module.py +++ b/modules/test/ntp/python/src/ntp_module.py @@ -14,8 +14,8 @@ """NTP test module""" from test_module import TestModule from scapy.all import rdpcap, IP, IPv6, NTP, UDP, Ether -from datetime import datetime import os +from collections import defaultdict LOG_NAME = 'test_ntp' MODULE_REPORT_FILE_NAME = 'ntp_report.html' @@ -30,7 +30,6 @@ class NTPModule(TestModule): def __init__(self, module, - log_dir=None, conf_file=None, results_dir=None, ntp_server_capture_file=NTP_SERVER_CAPTURE_FILE, @@ -38,7 +37,6 @@ def __init__(self, monitor_capture_file=MONITOR_CAPTURE_FILE): super().__init__(module_name=module, log_name=LOG_NAME, - log_dir=log_dir, conf_file=conf_file, results_dir=results_dir) self.ntp_server_capture_file = ntp_server_capture_file @@ -54,7 +52,7 @@ def generate_module_report(self): # Extract NTP data from the pcap file ntp_table_data = self.extract_ntp_data() - html_content = '

NTP Module

' + html_content = '

NTP Module

' # Set the summary variables local_requests = sum( @@ -69,6 +67,33 @@ def generate_module_report(self): total_responses = sum(1 for row in ntp_table_data if row['Type'] == 'Server') + # Initialize a dictionary to store timestamps for each unique combination + timestamps = defaultdict(list) + + # Collect timestamps for each unique combination + for row in ntp_table_data: + # Add the timestamp to the corresponding combination + key = (row['Source'], row['Destination'], row['Type'], row['Version']) + timestamps[key].append(row['Timestamp']) + + # Calculate the average time between requests for each unique combination + average_time_between_requests = {} + + for key, times in timestamps.items(): + # Sort the timestamps + times.sort() + + # Calculate the time differences between consecutive timestamps + time_diffs = [t2 - t1 for t1, t2 in zip(times[:-1], times[1:])] + + # Calculate the average of the time differences + if time_diffs: + avg_diff = sum(time_diffs) / len(time_diffs) + else: + avg_diff = 0 # one timestamp, the average difference is 0 + + average_time_between_requests[key] = avg_diff + # Add summary table html_content += (f''' @@ -92,7 +117,6 @@ def generate_module_report(self): ''') if total_requests + total_responses > 0: - table_content = '''
@@ -101,37 +125,39 @@ def generate_module_report(self): - + + ''' - for row in ntp_table_data: - - # Timestamp of the NTP packet - dt_object = datetime.utcfromtimestamp(row['Timestamp']) - - # Extract milliseconds from the fractional part of the timestamp - milliseconds = int((row['Timestamp'] % 1) * 1000) + # Generate the HTML table with the count column + for (src, dst, typ, + version), avg_diff in average_time_between_requests.items(): + cnt = len(timestamps[(src, dst, typ, version)]) - # Format the datetime object with milliseconds - formatted_time = dt_object.strftime( - '%b %d, %Y %H:%M:%S.') + f'{milliseconds:03d}' + # Sync Average only applies to client requests + if 'Client' in typ: + # Convert avg_diff to seconds and format it + avg_diff_seconds = avg_diff + avg_formatted_time = f'{avg_diff_seconds:.3f} seconds' + else: + avg_formatted_time = 'N/A' - table_content += (f''' + table_content += f''' - - - - - - ''') + + + + + + + ''' table_content += '''
Destination Type VersionTimestampCountSync Request Average
{row['Source']}{row['Destination']}{row['Type']}{row['Version']}{formatted_time}
{src}{dst}{typ}{version}{cnt}{avg_formatted_time}
''' - html_content += table_content else: @@ -159,8 +185,8 @@ def extract_ntp_data(self): # Read the pcap files packets = (rdpcap(self.startup_capture_file) + - rdpcap(self.monitor_capture_file) + - rdpcap(self.ntp_server_capture_file)) + rdpcap(self.monitor_capture_file) + + rdpcap(self.ntp_server_capture_file)) # Iterate through NTP packets for packet in packets: @@ -171,6 +197,10 @@ def extract_ntp_data(self): # Local NTP server syncs to external servers so we need to filter only # for traffic to/from the device if self._device_mac in (source_mac, destination_mac): + + source_ip = None + dest_ip = None + if IP in packet: source_ip = packet[IP].src dest_ip = packet[IP].dst @@ -218,6 +248,9 @@ def _ntp_network_ntp_support(self): for packet in packet_capture: if NTP in packet and packet.src == self._device_mac: + + dest_ip = None + if IP in packet: dest_ip = packet[IP].dst elif IPv6 in packet: @@ -229,16 +262,15 @@ def _ntp_network_ntp_support(self): device_sends_ntp3 = True LOGGER.info(f'Device sent NTPv3 request to {dest_ip}') - if not (device_sends_ntp3 or device_sends_ntp4): - result = False, 'Device has not sent any NTP requests' - elif device_sends_ntp3 and device_sends_ntp4: - result = False, ('Device sent NTPv3 and NTPv4 packets. ' + - 'NTPv3 is not allowed.') + result = False, 'Device has not sent any NTP requests' + + if device_sends_ntp3 and device_sends_ntp4: + result = False, ('Device sent NTPv3 and NTPv4 packets') elif device_sends_ntp3: - result = False, ('Device sent NTPv3 packets. ' - 'NTPv3 is not allowed.') + result = False, ('Device sent NTPv3 packets') elif device_sends_ntp4: - result = True, 'Device sent NTPv4 packets.' + result = True, 'Device sent NTPv4 packets' + LOGGER.info(result[1]) return result @@ -255,6 +287,7 @@ def _ntp_network_ntp_dhcp(self): for packet in packet_capture: if NTP in packet and packet.src == self._device_mac: device_sends_ntp = True + dest_ip = None if IP in packet: dest_ip = packet[IP].dst elif IPv6 in packet: @@ -266,17 +299,17 @@ def _ntp_network_ntp_dhcp(self): LOGGER.info('Device sent NTP request to non-DHCP provided NTP server') ntp_to_remote = True + result = 'Feature Not Detected', 'Device has not sent any NTP requests' + if device_sends_ntp: if ntp_to_local and ntp_to_remote: result = False, ('Device sent NTP request to DHCP provided ' + 'server and non-DHCP provided server') elif ntp_to_remote: result = ('Feature Not Detected', - 'Device sent NTP request to non-DHCP provided server') + 'Device sent NTP request to non-DHCP provided server') elif ntp_to_local: result = True, 'Device sent NTP request to DHCP provided server' - else: - result = 'Feature Not Detected', 'Device has not sent any NTP requests' LOGGER.info(result[1]) return result diff --git a/modules/test/protocol/README.md b/modules/test/protocol/README.md index 765fbf758..08c8cd345 100644 --- a/modules/test/protocol/README.md +++ b/modules/test/protocol/README.md @@ -14,6 +14,6 @@ Within the ```python/src``` directory, the below tests are executed. | ID | Description | Expected behavior | Required result |---|---|---|---| -| protocol.valid_bacnet | Can valid BACnet traffic be seen | BACnet traffic can be seen on the network and packets are valid | Required if Applicable | +| protocol.valid_bacnet | Can valid BACnet traffic be seen | BACnet traffic can be seen on the network and packets are valid | Recommended | | protocol.bacnet.version | Obtain the version of BACnet client used | The BACnet client implements an up to date version of BACnet | Recommended | | protocol.valid_modbus | Can valid Modbus traffic be seen | Any Modbus functionality works as expected and valid Modbus traffic can be observed | Recommended | \ No newline at end of file diff --git a/modules/test/protocol/bin/start_test_module b/modules/test/protocol/bin/start_test_module index a0754836c..e51fdb7ed 100644 --- a/modules/test/protocol/bin/start_test_module +++ b/modules/test/protocol/bin/start_test_module @@ -1,53 +1,50 @@ -#!/bin/bash - -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Setup and start the connection test module - -# Define where the python source files are located -PYTHON_SRC_DIR=/testrun/python/src - -# Fetch module name -MODULE_NAME=$1 - -# Default interface should be veth0 for all containers -DEFAULT_IFACE=veth0 - -# Allow a user to define an interface by passing it into this script -DEFINED_IFACE=$2 - -# Select which interace to use -if [[ -z $DEFINED_IFACE || "$DEFINED_IFACE" == "null" ]] -then - echo "No interface defined, defaulting to veth0" - INTF=$DEFAULT_IFACE -else - INTF=$DEFINED_IFACE -fi - -# Create and set permissions on the log files -LOG_FILE=/runtime/output/$MODULE_NAME.log -RESULT_FILE=/runtime/output/$MODULE_NAME-result.json -touch $LOG_FILE -touch $RESULT_FILE -chown $HOST_USER $LOG_FILE -chown $HOST_USER $RESULT_FILE - -# Run the python script that will execute the tests for this module -# -u flag allows python print statements -# to be logged by docker by running unbuffered -python3 -u $PYTHON_SRC_DIR/run.py "-m $MODULE_NAME" - +#!/bin/bash + +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Setup and start the connection test module + +# Define where the python source files are located +PYTHON_SRC_DIR=/testrun/python/src + +# Fetch module name +MODULE_NAME=$1 + +# Default interface should be veth0 for all containers +DEFAULT_IFACE=veth0 + +# Allow a user to define an interface by passing it into this script +DEFINED_IFACE=$2 + +# Select which interace to use +if [[ -z $DEFINED_IFACE || "$DEFINED_IFACE" == "null" ]] +then + echo "No interface defined, defaulting to veth0" + INTF=$DEFAULT_IFACE +else + INTF=$DEFINED_IFACE +fi + +# Create and set permissions on the log files +RESULT_FILE=/runtime/output/$MODULE_NAME-result.json +touch $RESULT_FILE +chown $HOST_USER $RESULT_FILE + +# Run the python script that will execute the tests for this module +# -u flag allows python print statements +# to be logged by docker by running unbuffered +python3 -u $PYTHON_SRC_DIR/run.py "-m $MODULE_NAME" + echo Module has finished \ No newline at end of file diff --git a/modules/test/protocol/conf/module_config.json b/modules/test/protocol/conf/module_config.json index 365bd346b..554f43cc7 100644 --- a/modules/test/protocol/conf/module_config.json +++ b/modules/test/protocol/conf/module_config.json @@ -16,20 +16,17 @@ { "name": "protocol.valid_bacnet", "test_description": "Can valid BACnet traffic be seen", - "expected_behavior": "BACnet traffic can be seen on the network and packets are valid and not malformed", - "required_result": "Recommended" + "expected_behavior": "BACnet traffic can be seen on the network and packets are valid and not malformed" }, { "name": "protocol.bacnet.version", "test_description": "Obtain the version of BACnet client used", - "expected_behavior": "The BACnet client implements an up to date version of BACnet", - "required_result": "Recommended" + "expected_behavior": "The BACnet client implements an up to date version of BACnet" }, { "name": "protocol.valid_modbus", "test_description": "Can valid Modbus traffic be seen", "expected_behavior": "Any Modbus functionality works as expected and valid Modbus traffic can be observed", - "required_result": "Recommended", "config":{ "port": 502, "device_id": 1, diff --git a/modules/test/protocol/protocol.Dockerfile b/modules/test/protocol/protocol.Dockerfile index 6f55520e1..4494ae94e 100644 --- a/modules/test/protocol/protocol.Dockerfile +++ b/modules/test/protocol/protocol.Dockerfile @@ -12,8 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Image name: test-run/protocol-test -FROM test-run/base-test:latest +# Image name: testrun/protocol-test +FROM testrun/base-test:latest # Set DEBIAN_FRONTEND to noninteractive mode ENV DEBIAN_FRONTEND=noninteractive @@ -28,7 +28,7 @@ ARG MODULE_DIR=modules/test/$MODULE_NAME COPY $MODULE_DIR/python/requirements.txt /testrun/python #Install all python requirements for the module -RUN pip3 install -r /testrun/python/requirements.txt +RUN pip install -r /testrun/python/requirements.txt # Copy over all configuration files COPY $MODULE_DIR/conf /testrun/conf diff --git a/modules/test/protocol/python/requirements.txt b/modules/test/protocol/python/requirements.txt index 57917735d..1fe889fe9 100644 --- a/modules/test/protocol/python/requirements.txt +++ b/modules/test/protocol/python/requirements.txt @@ -1,7 +1,14 @@ -# Required for BACnet protocol tests -netifaces -BAC0 -pytz - -# Required for Modbus protocol tests -pymodbus \ No newline at end of file +# Dependencies to user defined packages +# Package dependencies should always be defined before the user defined +# packages to prevent auto-upgrades of stable dependencies +bacpypes==0.18.7 +colorama==0.4.6 + +# User defined packages +# Required for BACnet protocol tests +netifaces==0.11.0 +BAC0==23.7.3 +pytz==2024.2 + +# Required for Modbus protocol tests +pymodbus==3.7.4 diff --git a/modules/test/protocol/python/src/protocol_bacnet.py b/modules/test/protocol/python/src/protocol_bacnet.py index a17c9cdd3..9d4399b2b 100644 --- a/modules/test/protocol/python/src/protocol_bacnet.py +++ b/modules/test/protocol/python/src/protocol_bacnet.py @@ -82,8 +82,10 @@ def validate_device(self): for device in self.devices: object_id = str(device[3]) # BACnet Object ID LOGGER.info('Checking device: ' + str(device)) - result &= self.validate_bacnet_source( + device_valid = self.validate_bacnet_source( object_id=object_id, device_hw_addr=self.device_hw_addr) + if device_valid is not None: + result &= device_valid description = ('BACnet device discovered' if result else 'BACnet device was found but was not device under test') else: diff --git a/modules/test/protocol/python/src/protocol_modbus.py b/modules/test/protocol/python/src/protocol_modbus.py index 925e9517a..a722f928e 100644 --- a/modules/test/protocol/python/src/protocol_modbus.py +++ b/modules/test/protocol/python/src/protocol_modbus.py @@ -103,7 +103,7 @@ def __init__(self, log, device_ip, config): self._discrete_input_enabled = False # Initialize the modbus client - self.client = ModbusClient(device_ip, self._port) + self.client = ModbusClient(host=device_ip, port=self._port) # Connections created from this method are simple socket connections # and aren't indicative of valid modbus diff --git a/modules/test/protocol/python/src/protocol_module.py b/modules/test/protocol/python/src/protocol_module.py index 4f7c1a7e7..9d99c91bd 100644 --- a/modules/test/protocol/python/src/protocol_module.py +++ b/modules/test/protocol/python/src/protocol_module.py @@ -22,7 +22,7 @@ class ProtocolModule(TestModule): - """Protocol Test module""" + """Protocol test module""" def __init__(self, module): self._supports_bacnet = False diff --git a/modules/test/protocol/python/src/run.py b/modules/test/protocol/python/src/run.py index d47c81cb6..a2788c833 100644 --- a/modules/test/protocol/python/src/run.py +++ b/modules/test/protocol/python/src/run.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Run Baseline module""" +"""Run protocol module""" import argparse import signal import sys diff --git a/modules/test/services/README.md b/modules/test/services/README.md index eae8a0bd0..f6133cc6e 100644 --- a/modules/test/services/README.md +++ b/modules/test/services/README.md @@ -22,6 +22,6 @@ Within the ```python/src``` directory, the below tests are executed. | security.services.pop | Check POP ports 109 and 110 are disabled and POP is not running on any port | There is no POP service running on any port | Required | | security.services.imap | Check IMAP port 143 is disabled and IMAP is not running on any port | There is no IMAP service running on any port | Required | | security.services.snmpv3 | Check SNMP port 161/162 is disabled. If SNMP is an essential service, it should be v3 | Device is unreachable on port 161/162 unless SNMP is essential in which case it is SNMPv3 that is used | Required | -| security.services.vnc | Check VNS is disabled on any port | Device cannot be accessed via VNC on any port | Required | +| security.services.vnc | Check VNC is disabled on any port | Device cannot be accessed via VNC on any port | Required | | security.services.tftp | Check TFTP port 69 is disabled (UDP) | There is no TFTP service running on any port | Required | | ntp.network.ntp_server | Check NTP port 123 is disabled and the device is not acting as an NTP server | The devices does not respond to NTP requests | Required | \ No newline at end of file diff --git a/modules/test/services/bin/start_test_module b/modules/test/services/bin/start_test_module index d8cede486..a42ee4cf0 100644 --- a/modules/test/services/bin/start_test_module +++ b/modules/test/services/bin/start_test_module @@ -41,11 +41,8 @@ else fi # Create and set permissions on the log files -LOG_FILE=/runtime/output/$MODULE_NAME.log RESULT_FILE=/runtime/output/$MODULE_NAME-result.json -touch $LOG_FILE touch $RESULT_FILE -chown $HOST_USER $LOG_FILE chown $HOST_USER $RESULT_FILE # Run the python scrip that will execute the tests for this module diff --git a/modules/test/services/conf/module_config.json b/modules/test/services/conf/module_config.json index 5c20b4beb..b37435eda 100644 --- a/modules/test/services/conf/module_config.json +++ b/modules/test/services/conf/module_config.json @@ -9,14 +9,13 @@ "docker": { "depends_on": "base", "enable_container": true, - "timeout": 600 + "timeout": 900 }, "tests": [ { "name": "security.services.ftp", "test_description": "Check FTP port 20/21 is disabled and FTP is not running on any port", "expected_behavior": "There is no FTP service running on any port", - "required_result": "Required", "config": { "services": [ "ftp", @@ -50,7 +49,6 @@ "name": "security.ssh.version", "test_description": "If the device is running a SSH server ensure it is SSHv2", "expected_behavior": "SSH server is not running or server is SSHv2", - "required_result": "Required", "config": { "services": ["ssh"], "ports": [ @@ -70,7 +68,6 @@ "name": "security.services.telnet", "test_description": "Check TELNET port 23 is disabled and TELNET is not running on any port", "expected_behavior": "There is no Telnet service running on any port", - "required_result": "Required", "config": { "services": [ "telnet" @@ -95,7 +92,6 @@ "name": "security.services.smtp", "test_description": "Check SMTP ports 25, 465 and 587 are not enabled and SMTP is not running on any port.", "expected_behavior": "There is no SMTP service running on any port", - "required_result": "Required", "config": { "services": [ "smtp" @@ -123,7 +119,6 @@ "name": "security.services.http", "test_description": "Check that there is no HTTP server running on any port", "expected_behavior": "Device is unreachable on port 80 (or any other port) and only responds to HTTPS requests on port 443 (or any other port if HTTP is used at all)", - "required_result": "Required", "config": { "services": [ "http" @@ -158,7 +153,6 @@ "name": "security.services.pop", "test_description": "Check POP ports 109 and 110 are disabled and POP is not running on any port", "expected_behavior": "There is no POP service running on any port", - "required_result": "Required", "config": { "services": [ "pop2", @@ -200,7 +194,6 @@ "name": "security.services.imap", "test_description": "Check IMAP port 143 is disabled and IMAP is not running on any port", "expected_behavior": "There is no IMAP service running on any port", - "required_result": "Required", "config": { "services": [ "imap", @@ -250,7 +243,6 @@ "name": "security.services.snmpv3", "test_description": "Check SNMP port 161/162 is disabled. If SNMP is an essential service, check it supports version 3", "expected_behavior": "Device is unreachable on port 161 (or any other port) and device is unreachable on port 162 (or any other port) unless SNMP is essential in which case it is SNMPv3 is used.", - "required_result": "Required", "config": { "services": [ "snmp" @@ -275,7 +267,6 @@ "name": "security.services.vnc", "test_description": "Check VNC is disabled on any port", "expected_behavior": "Device cannot be accessed / connected to via VNC on any port", - "required_result": "Required", "config": { "services": [ "vnc", @@ -319,6 +310,10 @@ { "number": 5903, "type": "tcp" + }, + { + "number": 6001, + "type": "tcp" } ] }, @@ -330,7 +325,6 @@ "name": "security.services.tftp", "test_description": "Check TFTP port 69 is disabled (UDP)", "expected_behavior": "There is no TFTP service running on any port", - "required_result": "Required", "config": { "services": [ "tftp", @@ -363,7 +357,6 @@ "name": "ntp.network.ntp_server", "test_description": "Check NTP port 123 is disabled and the device is not operating as an NTP server", "expected_behavior": "The device does not respond to NTP requests when it's IP is set as the NTP server on another device", - "required_result": "Required", "config": { "services": [ "ntp" @@ -379,6 +372,22 @@ "Disable the NTP server", "Drop traffic entering port 123/udp" ] + }, + { + "name": "protocol.services.bacnet", + "test_description": "Report whether the device is running a BACnet server", + "expected_behavior": "The device may or may not be running a BACnet server", + "config": { + "services": [ + "bacnet" + ], + "ports": [ + { + "number": 47808, + "type": "udp" + } + ] + } } ] } diff --git a/modules/test/services/python/requirements.txt b/modules/test/services/python/requirements.txt index a3fdd1857..02acc1e19 100644 --- a/modules/test/services/python/requirements.txt +++ b/modules/test/services/python/requirements.txt @@ -1 +1,6 @@ -xmltodict==0.13.0 \ No newline at end of file +# Dependencies to user defined packages +# Package dependencies should always be defined before the user defined +# packages to prevent auto-upgrades of stable dependencies + +# User defined packages +xmltodict==0.14.2 diff --git a/modules/test/services/python/src/services_module.py b/modules/test/services/python/src/services_module.py index bfa232c87..1a783e7dc 100644 --- a/modules/test/services/python/src/services_module.py +++ b/modules/test/services/python/src/services_module.py @@ -31,14 +31,12 @@ class ServicesModule(TestModule): def __init__(self, module, - log_dir=None, conf_file=None, results_dir=None, run=True, nmap_scan_results_path=None): super().__init__(module_name=module, log_name=LOG_NAME, - log_dir=log_dir, conf_file=conf_file, results_dir=results_dir) self._scan_tcp_results = None @@ -83,7 +81,7 @@ def generate_module_report(self): else: udp_open += 1 - html_content = '

Services Module

' + html_content = '

Services Module

' # Add summary table html_content += (f''' @@ -198,10 +196,9 @@ def _process_port_results(self): self._scan_results.update(self._scan_udp_results) def _scan_tcp_ports(self): - max_port = 1000 LOGGER.info('Running nmap TCP port scan') - nmap_results = util.run_command( - f'''nmap --open -sT -sV -Pn -v -p 1-{max_port} + nmap_results = util.run_command( # pylint: disable=E1120 + f'''nmap --open -sT -sV -Pn -v -p 1-65535 --version-intensity 7 -T4 -oX - {self._ipv4_addr}''')[0] LOGGER.info('TCP port scan complete') @@ -227,8 +224,8 @@ def _scan_udp_ports(self): if len(ports) > 0: port_list = ','.join(ports) LOGGER.info('Running nmap UDP port scan') - LOGGER.debug('UDP ports: ' + str(port_list)) - nmap_results = util.run_command( + LOGGER.info('UDP ports: ' + str(port_list)) + nmap_results = util.run_command( # pylint: disable=E1120 f'nmap -sU -sV -p {port_list} -oX - {self._ipv4_addr}')[0] LOGGER.info('UDP port scan complete') nmap_results_json = self._nmap_results_to_json(nmap_results) @@ -423,3 +420,15 @@ def _security_ssh_version(self, config): else: return (False, f"SSH server found running {open_port_info['version']}") + + def _protocol_services_bacnet(self, config): + LOGGER.info('Running protocol.services.bacnet') + + open_ports = self._check_results(config['ports'], config['services']) + if len(open_ports) == 0: + return False, 'No BACnet server found' + else: + return ( + True, + f'''Found BACnet server running on port {', '.join(open_ports)}''' + ) diff --git a/modules/test/services/services.Dockerfile b/modules/test/services/services.Dockerfile index 3a89fc33c..8dcaafcc1 100644 --- a/modules/test/services/services.Dockerfile +++ b/modules/test/services/services.Dockerfile @@ -12,8 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Image name: test-run/services-test -FROM test-run/base-test:latest +# Image name: testrun/services-test +FROM testrun/base-test:latest ARG MODULE_NAME=services ARG MODULE_DIR=modules/test/$MODULE_NAME @@ -22,7 +22,7 @@ ARG MODULE_DIR=modules/test/$MODULE_NAME COPY $MODULE_DIR/python/requirements.txt /testrun/python # Install all python requirements for the module -RUN pip3 install -r /testrun/python/requirements.txt +RUN pip install -r /testrun/python/requirements.txt # Copy over all configuration files COPY $MODULE_DIR/conf /testrun/conf diff --git a/modules/test/tls/README.md b/modules/test/tls/README.md index ba1c6b1db..f0e0119af 100644 --- a/modules/test/tls/README.md +++ b/modules/test/tls/README.md @@ -14,5 +14,9 @@ Within the ```python/src``` directory, the below tests are executed. | ID | Description | Expected behavior | Required result |---|---|---|---| -| security.tls.v1_2_server | Check the device web server is TLSv1.2 minimum and the certificate is valid | TLS 1.2 certificate is issues to the client when accessed | Required | -| security.tls.v1_2_client | Device uses TLS with connections to external services on any port | The packet indicates a TLS connection with at least TLS v1.2 and support for ECDH and ECDSA ciphers | Required | \ No newline at end of file +| security.tls.v1_0_client | Device uses TLS with connection to an external service on port 443 (or any other port which could be running the webserver-HTTPS) | The packet indicates a TLS connection with at least TLS 1.0 and support | Informational | +| security.tls.v1_2_server | Check the device web server TLS 1.2 and the certificate is valid | TLS 1.2 certificate is issues to the web browser client when accessed | Required if Applicable | +| security.tls.v1_2_client | Device uses TLS with connection to an external service on port 443 (or any other port which could be running the webserver-HTTPS) | The packet indicates a TLS connection with at least TLS v1.2 and support for ECDH and ECDSA ciphers | Required if Applicable | +| security.tls.v1_3_server | Check the device web server TLS 1.3 and the certificate is valid | TLS 1.3 certificate is issued to the web browser client when accessed | Informational | +| security.tls.v1_3_client | Device uses TLS with connection to an external service on port 443 (or any other port which could be running the webserver-HTTPS) | The packet indicates a TLS connection with at least TLS 1.3 | Informational | + diff --git a/modules/test/tls/bin/get_client_hello_packets.sh b/modules/test/tls/bin/get_client_hello_packets.sh index d563d11f2..317657187 100755 --- a/modules/test/tls/bin/get_client_hello_packets.sh +++ b/modules/test/tls/bin/get_client_hello_packets.sh @@ -21,13 +21,21 @@ TLS_VERSION="$3" TSHARK_OUTPUT="-T json -e ip.src -e tcp.dstport -e ip.dst" TSHARK_FILTER="ssl.handshake.type==1 and ip.src==$SRC_IP" -if [[ $TLS_VERSION == '1.2' || -z $TLS_VERSION ]];then +if [[ $TLS_VERSION == '1.0' ]]; then + TSHARK_FILTER="$TSHARK_FILTER and ssl.handshake.version==0x0301" +elif [[ $TLS_VERSION == '1.1' ]]; then + TSHARK_FILTER="$TSHARK_FILTER and ssl.handshake.version==0x0302" +elif [[ $TLS_VERSION == '1.2' || -z $TLS_VERSION ]]; then TSHARK_FILTER="$TSHARK_FILTER and ssl.handshake.version==0x0303" -elif [ $TLS_VERSION == '1.3' ];then +elif [[ $TLS_VERSION == '1.3' ]]; then TSHARK_FILTER="$TSHARK_FILTER and (ssl.handshake.version==0x0304 or tls.handshake.extensions.supported_version==0x0304)" +else + echo "Unsupported TLS version: $TLS_VERSION" + exit 1 fi response=$(tshark -r "$CAPTURE_FILE" $TSHARK_OUTPUT $TSHARK_FILTER) echo "$response" + \ No newline at end of file diff --git a/modules/test/tls/bin/get_handshake_complete.sh b/modules/test/tls/bin/get_handshake_complete.sh index 9bf9c525d..b36997f6d 100755 --- a/modules/test/tls/bin/get_handshake_complete.sh +++ b/modules/test/tls/bin/get_handshake_complete.sh @@ -1,33 +1,39 @@ -#!/bin/bash - -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -CAPTURE_FILE="$1" -SRC_IP="$2" -DST_IP="$3" -TLS_VERSION="$4" - -TSHARK_FILTER="ip.src==$SRC_IP and ip.dst==$DST_IP " - -if [[ $TLS_VERSION == '1.2' || -z $TLS_VERSION ]];then - TSHARK_FILTER=$TSHARK_FILTER " and ssl.handshake.type==2 and tls.handshake.type==14 " -elif [ $TLS_VERSION == '1.2' ];then - TSHARK_FILTER=$TSHARK_FILTER "and ssl.handshake.type==2 and tls.handshake.extensions.supported_version==0x0304" -fi - -response=$(tshark -r "$CAPTURE_FILE" $TSHARK_FILTER) - -echo "$response" - \ No newline at end of file +#!/bin/bash + +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +CAPTURE_FILE="$1" +SRC_IP="$2" +DST_IP="$3" +TLS_VERSION="$4" + +TSHARK_FILTER="ip.src==$SRC_IP and ip.dst==$DST_IP" + +if [[ $TLS_VERSION == '1.0' ]]; then + TSHARK_FILTER=$TSHARK_FILTER "and ssl.handshake.type==2 and tls.handshake.type==14" +elif [[ $TLS_VERSION == '1.1' ]]; then + TSHARK_FILTER=$TSHARK_FILTER "and ssl.handshake.type==2 and tls.handshake.type==14" +elif [[ $TLS_VERSION == '1.2' || -z $TLS_VERSION ]]; then + TSHARK_FILTER=$TSHARK_FILTER "and ssl.handshake.type==2 and tls.handshake.type==14" +elif [[ $TLS_VERSION == '1.3' ]]; then + TSHARK_FILTER=$TSHARK_FILTER "and ssl.handshake.type==2 and tls.handshake.extensions.supported_version==0x0304" +else + echo "Unsupported TLS version: $TLS_VERSION" + exit 1 +fi + +response=$(tshark -r "$CAPTURE_FILE" $TSHARK_FILTER) + +echo "$response" diff --git a/modules/test/tls/bin/get_tls_client_connections.sh b/modules/test/tls/bin/get_tls_client_connections.sh index e2e6da91b..7335cac80 100755 --- a/modules/test/tls/bin/get_tls_client_connections.sh +++ b/modules/test/tls/bin/get_tls_client_connections.sh @@ -1,32 +1,32 @@ -#!/bin/bash - -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -CAPTURE_FILE="$1" -SRC_IP="$2" -PROTOCOL=$3 - -TSHARK_OUTPUT="-T json -e ip.src -e tcp.dstport -e ip.dst" -TSHARK_FILTER="ip.src == $SRC_IP and tls" - -# Add a protocol filter if defined -if [ -n "$PROTOCOL" ];then - TSHARK_FILTER="$TSHARK_FILTER and $PROTOCOL" -fi - -response=$(tshark -r "$CAPTURE_FILE" $TSHARK_OUTPUT $TSHARK_FILTER) - -echo "$response" +#!/bin/bash + +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +CAPTURE_FILE="$1" +SRC_IP="$2" +PROTOCOL=$3 + +TSHARK_OUTPUT="-T json -e ip.src -e tcp.dstport -e ip.dst" +TSHARK_FILTER="ip.src == $SRC_IP and tls" + +# Add a protocol filter if defined +if [ -n "$PROTOCOL" ];then + TSHARK_FILTER="$TSHARK_FILTER and $PROTOCOL" +fi + +response=$(tshark -r "$CAPTURE_FILE" $TSHARK_OUTPUT $TSHARK_FILTER) + +echo "$response" \ No newline at end of file diff --git a/modules/test/tls/bin/start_test_module b/modules/test/tls/bin/start_test_module index d8cede486..a42ee4cf0 100644 --- a/modules/test/tls/bin/start_test_module +++ b/modules/test/tls/bin/start_test_module @@ -41,11 +41,8 @@ else fi # Create and set permissions on the log files -LOG_FILE=/runtime/output/$MODULE_NAME.log RESULT_FILE=/runtime/output/$MODULE_NAME-result.json -touch $LOG_FILE touch $RESULT_FILE -chown $HOST_USER $LOG_FILE chown $HOST_USER $RESULT_FILE # Run the python scrip that will execute the tests for this module diff --git a/modules/test/tls/conf/module_config.json b/modules/test/tls/conf/module_config.json index cd77f8299..882bf91c7 100644 --- a/modules/test/tls/conf/module_config.json +++ b/modules/test/tls/conf/module_config.json @@ -12,11 +12,19 @@ "timeout": 300 }, "tests":[ + { + "name": "security.tls.v1_0_client", + "test_description": "Device uses TLS with connection to an external service on port 443 (or any other port which could be running the webserver-HTTPS)", + "expected_behavior": "The packet indicates a TLS connection with at least TLS 1.0 and support", + "recommendations": [ + "Disable connections to unsecure services", + "Ensure any URLs connected to are secure (https)" + ] + }, { "name": "security.tls.v1_2_server", - "test_description": "Check the device web server TLS 1.2 & certificate is valid", + "test_description": "Check the device web server TLS 1.2 and the certificate is valid", "expected_behavior": "TLS 1.2 certificate is issued to the web browser client when accessed", - "required_result": "Required if Applicable", "recommendations": [ "Enable TLS 1.2 support in the web server configuration", "Disable TLS 1.0 and 1.1", @@ -27,7 +35,25 @@ "name": "security.tls.v1_2_client", "test_description": "Device uses TLS with connection to an external service on port 443 (or any other port which could be running the webserver-HTTPS)", "expected_behavior": "The packet indicates a TLS connection with at least TLS 1.2 and support for ECDH and ECDSA ciphers", - "required_result": "Required if Applicable", + "recommendations": [ + "Disable connections to unsecure services", + "Ensure any URLs connected to are secure (https)" + ] + }, + { + "name": "security.tls.v1_3_server", + "test_description": "Check the device web server TLS 1.3 and the certificate is valid", + "expected_behavior": "TLS 1.3 certificate is issued to the web browser client when accessed", + "recommendations": [ + "Enable TLS 1.3 support in the web server configuration", + "Disable TLS 1.0 and 1.1", + "Sign the certificate used by the web server" + ] + }, + { + "name": "security.tls.v1_3_client", + "test_description": "Device uses TLS with connection to an external service on port 443 (or any other port which could be running the webserver-HTTPS)", + "expected_behavior": "The packet indicates a TLS connection with at least TLS 1.3", "recommendations": [ "Disable connections to unsecure services", "Ensure any URLs connected to are secure (https)" diff --git a/modules/test/tls/python/requirements-test.txt b/modules/test/tls/python/requirements-test.txt new file mode 100644 index 000000000..10f9fa8d1 --- /dev/null +++ b/modules/test/tls/python/requirements-test.txt @@ -0,0 +1,6 @@ +# Dependencies to user defined packages +# Package dependencies should always be defined before the user defined +# packages to prevent auto-upgrades of stable dependencies + +# User defined packages +scapy==2.6.0 diff --git a/modules/test/tls/python/requirements.txt b/modules/test/tls/python/requirements.txt index 7624a2c68..88b8abd77 100644 --- a/modules/test/tls/python/requirements.txt +++ b/modules/test/tls/python/requirements.txt @@ -1,5 +1,21 @@ -cryptography==42.0.4 # Do not upgrade until TLS module can be fixed to account for removed x509 property in version 39 -pyOpenSSL==24.1.0 -lxml==5.1.0 # Requirement of pyshark but if upgraded automatically above 5.1 will cause a python crash -pyshark==0.6 -requests==2.32.0 +# Dependencies to user defined packages +# Package dependencies should always be defined before the user defined +# packages to prevent auto-upgrades of stable dependencies +appdirs==1.4.4 +certifi==2024.8.30 +cffi==1.17.1 +charset-normalizer==3.3.2 +idna==3.8 +packaging==24.1 +pycparser==2.22 +pyshark==0.6 +termcolor==2.4.0 +urllib3==2.2.2 + +# User defined packages +cryptography==43.0.3 +pyOpenSSL==24.2.1 +lxml==5.1.0 # Requirement of pyshark but if upgraded automatically above 5.1 will cause a +pyshark==0.6 +requests==2.32.3 + diff --git a/modules/test/tls/python/src/tls_module.py b/modules/test/tls/python/src/tls_module.py index 9aab1b782..9fc89d549 100644 --- a/modules/test/tls/python/src/tls_module.py +++ b/modules/test/tls/python/src/tls_module.py @@ -26,12 +26,12 @@ GATEWAY_CAPTURE_FILE = '/runtime/network/gateway.pcap' LOGGER = None + class TLSModule(TestModule): """The TLS testing module.""" def __init__(self, module, - log_dir=None, conf_file=None, results_dir=None, startup_capture_file=STARTUP_CAPTURE_FILE, @@ -39,7 +39,6 @@ def __init__(self, tls_capture_file=TLS_CAPTURE_FILE): super().__init__(module_name=module, log_name=LOG_NAME, - log_dir=log_dir, conf_file=conf_file, results_dir=results_dir) self.startup_capture_file = startup_capture_file @@ -234,17 +233,20 @@ def _security_tls_v1_2_server(self): self._device_ipv4_addr, tls_version='1.2') tls_1_3_results = self._tls_util.validate_tls_server( self._device_ipv4_addr, tls_version='1.3') - results = self._tls_util.process_tls_server_results(tls_1_2_results, - tls_1_3_results) + results = self._tls_util.process_tls_server_results( + tls_1_2_results, tls_1_3_results) # Determine results and return proper messaging and details description = '' - if results[0] is None: + result = results[0] + details = results[1] + if result is None: + result = 'Feature Not Detected' description = 'TLS 1.2 certificate could not be validated' - elif results[0]: + elif result: description = 'TLS 1.2 certificate is valid' else: description = 'TLS 1.2 certificate is invalid' - return results[0], description,results[1] + return result, description, details else: LOGGER.error('Could not resolve device IP address. Skipping') @@ -256,40 +258,70 @@ def _security_tls_v1_3_server(self): # If the ipv4 address wasn't resolved yet, try again if self._device_ipv4_addr is not None: results = self._tls_util.validate_tls_server(self._device_ipv4_addr, - tls_version='1.3') + tls_version='1.3') # Determine results and return proper messaging and details description = '' - if results[0] is None: + result = results[0] + details = results[1] + description = '' + if result is None: + result = 'Feature Not Detected' description = 'TLS 1.3 certificate could not be validated' elif results[0]: description = 'TLS 1.3 certificate is valid' else: description = 'TLS 1.3 certificate is invalid' - return results[0], description,results[1] + return result, description, details else: LOGGER.error('Could not resolve device IP address') return 'Error', 'Could not resolve device IP address' + def _security_tls_v1_0_client(self): + LOGGER.info('Running security.tls.v1_0_client') + self._resolve_device_ip() + # If the ipv4 address wasn't resolved yet, try again + if self._device_ipv4_addr is not None: + tls_1_0_valid = self._validate_tls_client(self._device_ipv4_addr, '1.0') + tls_1_1_valid = self._validate_tls_client(self._device_ipv4_addr, '1.1') + tls_1_2_valid = self._validate_tls_client(self._device_ipv4_addr, '1.2') + tls_1_3_valid = self._validate_tls_client(self._device_ipv4_addr, '1.3') + states = [ + tls_1_0_valid[0], tls_1_1_valid[0], tls_1_2_valid[0], tls_1_3_valid[0] + ] + if any(state is True for state in states): + # If any state is True, return True + result_state = True + result_message = 'TLS 1.0 or higher detected' + elif all(state == 'Feature Not Detected' for state in states): + # If all states are "Feature not Detected" + result_state = 'Feature Not Detected' + result_message = tls_1_0_valid[1] + elif all(state == 'Error' for state in states): + # If all states are "Error" + result_state = 'Error' + result_message = '' + else: + result_state = False + result_message = 'TLS 1.0 or higher was not detected' + result_details = tls_1_0_valid[2] + tls_1_1_valid[2] + tls_1_2_valid[ + 2] + tls_1_3_valid[2] + result_tags = list( + set(tls_1_0_valid[3] + tls_1_1_valid[3] + tls_1_2_valid[3] + + tls_1_3_valid[3])) + return result_state, result_message, result_details, result_tags + else: + LOGGER.error('Could not resolve device IP address. Skipping') + return 'Error', 'Could not resolve device IP address' + def _security_tls_v1_2_client(self): LOGGER.info('Running security.tls.v1_2_client') self._resolve_device_ip() # If the ipv4 address wasn't resolved yet, try again if self._device_ipv4_addr is not None: - results = self._validate_tls_client(self._device_ipv4_addr, '1.2') - # Determine results and return proper messaging and details - description = '' - result = None - if results[0] is None: - description = 'No outbound connections were found' - result = 'Feature Not Detected' - elif results[0]: - description = 'TLS 1.2 client connections valid' - result = True - else: - description = 'TLS 1.2 client connections invalid' - result = False - return result, description, results[1] + return self._validate_tls_client(self._device_ipv4_addr, + '1.2', + unsupported_versions=['1.0', '1.1']) else: LOGGER.error('Could not resolve device IP address. Skipping') return 'Error', 'Could not resolve device IP address' @@ -299,41 +331,43 @@ def _security_tls_v1_3_client(self): self._resolve_device_ip() # If the ipv4 address wasn't resolved yet, try again if self._device_ipv4_addr is not None: - results = self._validate_tls_client(self._device_ipv4_addr, '1.3') - # Determine results and return proper messaging and details - description = '' - if results[0] is None: - description = 'No outbound connections were found' - elif results[0]: - description = 'TLS 1.3 client connections valid' - else: - description = 'TLS 1.3 client connections invalid' - return results[0], description, results[1] + return self._validate_tls_client(self._device_ipv4_addr, + '1.3', + unsupported_versions=['1.0', '1.1']) else: - LOGGER.error('Could not resolve device IP address') + LOGGER.error('Could not resolve device IP address. Skipping') return 'Error', 'Could not resolve device IP address' - def _validate_tls_client(self, client_ip, tls_version): + def _validate_tls_client(self, + client_ip, + tls_version, + unsupported_versions=None): client_results = self._tls_util.validate_tls_client( client_ip=client_ip, tls_version=tls_version, capture_files=[ MONITOR_CAPTURE_FILE, STARTUP_CAPTURE_FILE, TLS_CAPTURE_FILE - ]) + ], + unsupported_versions=unsupported_versions) # Generate results based on the state - result_message = 'No outbound connections were found.' - result_state = 'Feature Not Detected' + result_state = None + result_message = '' + result_details = '' + result_tags = [] - # If any of the packets detect failed client comms, fail the test - if not client_results[0] and client_results[0] is not None: - result_state = False - result_message = client_results[1] - else: + if client_results[0] is not None: + result_details = client_results[1] if client_results[0]: result_state = True - result_message = client_results[1] - return result_state, result_message + result_message = f'TLS {tls_version} client connections valid' + else: + result_state = False + result_message = f'TLS {tls_version} client connections invalid' + else: + result_state = 'Feature Not Detected' + result_message = 'No outbound connections were found' + return result_state, result_message, result_details, result_tags def _resolve_device_ip(self): # If the ipv4 address wasn't resolved yet, try again diff --git a/modules/test/tls/python/src/tls_util.py b/modules/test/tls/python/src/tls_util.py index d8c1d7a16..9f00b96ef 100644 --- a/modules/test/tls/python/src/tls_util.py +++ b/modules/test/tls/python/src/tls_util.py @@ -236,8 +236,8 @@ def validate_cert_chain(self, device_cert_path): ca_issuer_cert, cert_file_path = self.get_ca_issuer(certificate) if ca_issuer_cert is not None and cert_file_path is not None: LOGGER.info('CA Issuer resolved') - cert_text = crypto.dump_certificate(crypto.FILETYPE_TEXT, - ca_issuer_cert).decode() + cert_text = ca_issuer_cert.public_bytes( + encoding=serialization.Encoding.PEM).decode() LOGGER.info(cert_text) return self.validate_trusted_ca_signature_chain( device_cert_path=device_cert_path, @@ -372,6 +372,8 @@ def validate_tls_server(self, host, tls_version): public_key = self.get_public_key(public_cert) if public_key: key_valid = self.verify_public_key(public_key) + else: + key_valid = [0] sig_valid = self.validate_signature(host) @@ -527,7 +529,7 @@ def process_hello_packets(self, LOGGER.info('Checking client ciphers: ' + str(packet)) if packet['cipher_support']['ecdh'] and packet['cipher_support'][ 'ecdsa']: - LOGGER.info('Valid ciphers detected') + LOGGER.info('Required ciphers detected') client_hello_results['valid'].append(packet) # If a previous hello packet to the same destination failed, # we can now remove it as it has passed on a different attempt @@ -537,7 +539,7 @@ def process_hello_packets(self, if packet['dst_ip'] in str(invalid_packet): client_hello_results['invalid'].remove(invalid_packet) else: - LOGGER.info('Invalid ciphers detected') + LOGGER.info('Required ciphers not detected') if packet['dst_ip'] not in allowed_protocol_client_ips: if packet['dst_ip'] not in str(client_hello_results['invalid']): client_hello_results['invalid'].append(packet) @@ -549,7 +551,7 @@ def process_hello_packets(self, f'\nAllowing {protocol_name} traffic to {packet["dst_ip"]}') client_hello_results['valid'].append(packet) else: - # No cipher check for TLS 1.3 + # No cipher check for TLS 1.0, 1.1 or TLS 1.3 client_hello_results['valid'] = hello_packets return client_hello_results @@ -587,36 +589,31 @@ def get_non_tls_client_connection_ips(self, client_ip, capture_files): # we will assume any local connections using the same IP subnet as our # local network are approved and only connections to IP addresses outside # our network will be flagged. - def get_unsupported_tls_ips(self, client_ip, capture_files): + def get_unsupported_tls_ips(self, + client_ip, + capture_files, + unsupported_versions=None): LOGGER.info('Checking client for unsupported TLS client connections') - tls_1_0_packets = self.get_tls_packets(capture_files, client_ip, '1.0') - tls_1_1_packets = self.get_tls_packets(capture_files, client_ip, '1.1') - unsupported_tls_dst_ips = {} - if len(tls_1_0_packets) > 0: - for packet in tls_1_0_packets: - dst_ip = packet['dst_ip'] - tls_version = '1.0' - if dst_ip not in unsupported_tls_dst_ips: - LOGGER.info(f'''Unsupported TLS {tls_version} - connections detected to {dst_ip}''') - unsupported_tls_dst_ips[dst_ip] = [tls_version] - - if len(tls_1_1_packets) > 0: - for packet in tls_1_1_packets: - dst_ip = packet['dst_ip'] - tls_version = '1.1' - # Check if the IP is already in the dictionary - if dst_ip in unsupported_tls_dst_ips: - # If the IP is already present, append the new TLS version to the - # list - unsupported_tls_dst_ips[dst_ip].append(tls_version) - else: - # If the IP is not present, create a new list with the current - # TLS version - LOGGER.info(f'''Unsupported TLS {tls_version} connections detected - to {dst_ip}''') - unsupported_tls_dst_ips[dst_ip] = [tls_version] + if unsupported_versions is not None: + for unsupported_version in unsupported_versions: + tls_packets = self.get_tls_packets(capture_files, client_ip, '1.0') + if len(tls_packets) > 0: + for packet in tls_packets: + dst_ip = packet['dst_ip'] + tls_version = unsupported_version + if dst_ip not in unsupported_tls_dst_ips: + # If the IP is already present, append the new TLS version to the + # list + LOGGER.info(f'''Unsupported TLS {tls_version} + connections detected to {dst_ip}''') + unsupported_tls_dst_ips[dst_ip] = [tls_version] + else: + # If the IP is not present, create a new list with the current + # TLS version + LOGGER.info(f'''Unsupported TLS {tls_version} connections detected + to {dst_ip}''') + unsupported_tls_dst_ips[dst_ip] = [tls_version] return unsupported_tls_dst_ips # Check if the device has made any outbound connections that use any @@ -655,20 +652,26 @@ def is_private_ip(self, ip): return True return False - def validate_tls_client(self, client_ip, tls_version, capture_files): + def validate_tls_client(self, + client_ip, + tls_version, + capture_files, + unsupported_versions=None): LOGGER.info('Validating client for TLS: ' + tls_version) hello_packets = self.get_hello_packets(capture_files, client_ip, tls_version) # Resolve allowed protocol connections that require # additional consideration beyond packet inspection - allowed_protocol_client_ips = ( - self.get_allowed_protocol_client_connection_ips(client_ip, - capture_files)) + protocol_client_ips = (self.get_allowed_protocol_client_connection_ips( + client_ip, capture_files)) - LOGGER.info(f'Protocol IPS: {allowed_protocol_client_ips}') - client_hello_results = self.process_hello_packets( - hello_packets, allowed_protocol_client_ips, tls_version) + if len(protocol_client_ips) > 0: + LOGGER.info( + f'Allowed Protocol IP connections detected: {protocol_client_ips}') + client_hello_results = self.process_hello_packets(hello_packets, + protocol_client_ips, + tls_version) handshakes = {'complete': [], 'incomplete': []} for packet in client_hello_results['valid']: @@ -756,7 +759,8 @@ def validate_tls_client(self, client_ip, tls_version, capture_files): LOGGER.info(f'''TLS connection detected to {ip}. Ignoring non-TLS traffic detected to this IP''') - unsupported_tls_ips = self.get_unsupported_tls_ips(client_ip, capture_files) + unsupported_tls_ips = self.get_unsupported_tls_ips(client_ip, capture_files, + unsupported_versions) if len(unsupported_tls_ips) > 0: tls_client_valid = False for ip, tls_versions in unsupported_tls_ips.items(): diff --git a/modules/test/tls/tls.Dockerfile b/modules/test/tls/tls.Dockerfile index cedf9531b..c448c8478 100644 --- a/modules/test/tls/tls.Dockerfile +++ b/modules/test/tls/tls.Dockerfile @@ -12,8 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Image name: test-run/tls-test -FROM test-run/base-test:latest +# Image name: testrun/tls-test +FROM testrun/base-test:latest # Set DEBIAN_FRONTEND to noninteractive mode ENV DEBIAN_FRONTEND=noninteractive @@ -31,14 +31,23 @@ COPY $MODULE_DIR/conf /testrun/conf # Copy over all binary files COPY $MODULE_DIR/bin /testrun/bin +# Remove incorrect line endings +RUN dos2unix /testrun/bin/* + +# Make sure all the bin files are executable +RUN chmod u+x /testrun/bin/* + # Copy over all python files COPY $MODULE_DIR/python /testrun/python -#Install all python requirements for the module -RUN pip3 install -r /testrun/python/requirements.txt - -# Create a directory inside the container to store the root certificates -RUN mkdir -p /testrun/root_certs +# Install all python requirements for the module +RUN pip install -r /testrun/python/requirements.txt +# Install all python requirements for the modules unit test +RUN pip install -r /testrun/python/requirements-test.txt +# Install all python requirements for the modules unit test +RUN pip3 install -r /testrun/python/requirements-test.txt +# Create a directory inside the container to store the root certificates +RUN mkdir -p /testrun/root_certs diff --git a/modules/ui/angular.json b/modules/ui/angular.json index d72fee51f..28e0e9a36 100644 --- a/modules/ui/angular.json +++ b/modules/ui/angular.json @@ -25,20 +25,21 @@ "inlineStyleLanguage": "scss", "assets": ["src/favicon.ico", "src/assets"], "styles": ["src/styles.scss"], - "scripts": [] + "scripts": [], + "allowedCommonJsDependencies": ["mqtt-browser"] }, "configurations": { "production": { "budgets": [ { "type": "initial", - "maximumWarning": "1000kb", + "maximumWarning": "1500kb", "maximumError": "3000kb" }, { "type": "anyComponentStyle", - "maximumWarning": "3kb", - "maximumError": "4kb" + "maximumWarning": "5kb", + "maximumError": "6kb" } ], "outputHashing": "all" @@ -80,7 +81,8 @@ "inlineStyleLanguage": "scss", "assets": ["src/favicon.ico", "src/assets"], "styles": ["src/styles.scss"], - "scripts": [] + "scripts": [], + "karmaConfig": "karma.conf.js" } }, "lint": { @@ -93,6 +95,7 @@ } }, "cli": { - "schematicCollections": ["@angular-eslint/schematics"] + "schematicCollections": ["@angular-eslint/schematics"], + "analytics": false } } diff --git a/modules/ui/build.Dockerfile b/modules/ui/build.Dockerfile new file mode 100644 index 000000000..082efbbcc --- /dev/null +++ b/modules/ui/build.Dockerfile @@ -0,0 +1,19 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Image name: testrun/build-ui +FROM node@sha256:ffebb4405810c92d267a764b21975fb2d96772e41877248a37bf3abaa0d3b590 as build + +# Set the working directory +WORKDIR /modules/ui \ No newline at end of file diff --git a/modules/ui/karma.conf.js b/modules/ui/karma.conf.js new file mode 100644 index 000000000..b6d22bd62 --- /dev/null +++ b/modules/ui/karma.conf.js @@ -0,0 +1,43 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma'), + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + }, + jasmineHtmlReporter: { + suppressAll: true, // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage/test-run-ui'), + subdir: '.', + reporters: [{ type: 'html' }, { type: 'text-summary' }], + check: { + global: { + statements: 75, + branches: 75, + functions: 75, + lines: 75, + }, + }, + }, + reporters: ['progress', 'kjhtml'], + browsers: ['Chrome'], + restartOnFileChange: true, + }); +}; diff --git a/modules/ui/package-lock.json b/modules/ui/package-lock.json index e6903631a..b0ec71c07 100644 --- a/modules/ui/package-lock.json +++ b/modules/ui/package-lock.json @@ -8,36 +8,37 @@ "name": "test-run-ui", "version": "0.0.0", "dependencies": { - "@angular/animations": "^17.0.8", - "@angular/cdk": "^17.0.4", - "@angular/common": "^17.0.8", - "@angular/compiler": "^17.0.8", - "@angular/core": "^17.0.8", - "@angular/forms": "^17.3.1", - "@angular/material": "^17.3.1", - "@angular/platform-browser": "^17.0.8", - "@angular/platform-browser-dynamic": "^17.3.1", - "@angular/router": "^17.3.1", - "@ngrx/component-store": "^17.1.1", - "@ngrx/effects": "^17.1.1", - "@ngrx/store": "^17.0.1", + "@angular/animations": "^18.2.4", + "@angular/cdk": "^18.2.0", + "@angular/common": "^18.2.4", + "@angular/compiler": "^18.2.4", + "@angular/core": "^18.2.4", + "@angular/forms": "^18.2.4", + "@angular/material": "^18.2.0", + "@angular/platform-browser": "^18.2.4", + "@angular/platform-browser-dynamic": "^18.2.4", + "@angular/router": "^18.2.4", + "@ngrx/component-store": "^18.0.2", + "@ngrx/effects": "^18.0.2", + "@ngrx/store": "^18.0.2", "ngx-mask": "^16.4.2", + "ngx-mqtt": "^17.0.0", "rxjs": "~7.8.0", "tslib": "^2.6.2", - "zone.js": "^0.14.4" + "zone.js": "^0.14.10" }, "devDependencies": { - "@angular-devkit/build-angular": "^17.3.2", - "@angular-eslint/builder": "17.2.0", - "@angular-eslint/eslint-plugin": "17.2.0", - "@angular-eslint/eslint-plugin-template": "17.2.0", - "@angular-eslint/schematics": "17.2.0", - "@angular-eslint/template-parser": "17.2.0", - "@angular/cli": "~17.0.9", - "@angular/compiler-cli": "^17.3.1", + "@angular-devkit/build-angular": "^18.1.4", + "@angular-eslint/builder": "18.3.0", + "@angular-eslint/eslint-plugin": "^18.3.0", + "@angular-eslint/eslint-plugin-template": "^18.3.0", + "@angular-eslint/schematics": "^18.3.0", + "@angular-eslint/template-parser": "18.3.0", + "@angular/cli": "~18.2.4", + "@angular/compiler-cli": "^18.2.4", "@types/jasmine": "~4.3.6", - "@typescript-eslint/eslint-plugin": "^6.21.0", - "@typescript-eslint/parser": "^6.21.0", + "@typescript-eslint/eslint-plugin": "^8.2.0", + "@typescript-eslint/parser": "^8.2.0", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", @@ -48,8 +49,8 @@ "karma-jasmine": "~5.1.0", "karma-jasmine-html-reporter": "~2.1.0", "prettier": "^3.2.5", - "prettier-eslint": "^16.3.0", - "typescript": "~5.2.2" + "prettier-eslint": "^13.0.0", + "typescript": "~5.5.4" } }, "node_modules/@ampproject/remapping": { @@ -66,112 +67,111 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.1703.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1703.6.tgz", - "integrity": "sha512-Ck501FD/QuOjeKVFs7hU92w8+Ffetv0d5Sq09XY2/uygo5c/thMzp9nkevaIWBxUSeU5RqYZizDrhFVgYzbbOw==", + "version": "0.1802.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1802.6.tgz", + "integrity": "sha512-oF7cPFdTLxeuvXkK/opSdIxZ1E4LrBbmuytQ/nCoAGOaKBWdqvwagRZ6jVhaI0Gwu48rkcV7Zhesg/ESNnROdw==", "dev": true, "dependencies": { - "@angular-devkit/core": "17.3.6", + "@angular-devkit/core": "18.2.6", "rxjs": "7.8.1" }, "engines": { - "node": "^18.13.0 || >=20.9.0", + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" } }, "node_modules/@angular-devkit/build-angular": { - "version": "17.3.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-17.3.6.tgz", - "integrity": "sha512-K4CEZvhQZUUOpmXPVoI1YBM8BARbIlqE6FZRxakmnr+YOtVTYE5s+Dr1wgja8hZIohNz6L7j167G9Aut7oPU/w==", + "version": "18.2.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-18.2.6.tgz", + "integrity": "sha512-u12cJZttgs5j7gICHWSmcaTCu0EFXEzKqI8nkYCwq2MtuJlAXiMQSXYuEP9OU3Go4vMAPtQh2kShyOWCX5b4EQ==", "dev": true, "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.1703.6", - "@angular-devkit/build-webpack": "0.1703.6", - "@angular-devkit/core": "17.3.6", - "@babel/core": "7.24.0", - "@babel/generator": "7.23.6", - "@babel/helper-annotate-as-pure": "7.22.5", - "@babel/helper-split-export-declaration": "7.22.6", - "@babel/plugin-transform-async-generator-functions": "7.23.9", - "@babel/plugin-transform-async-to-generator": "7.23.3", - "@babel/plugin-transform-runtime": "7.24.0", - "@babel/preset-env": "7.24.0", - "@babel/runtime": "7.24.0", - "@discoveryjs/json-ext": "0.5.7", - "@ngtools/webpack": "17.3.6", + "@angular-devkit/architect": "0.1802.6", + "@angular-devkit/build-webpack": "0.1802.6", + "@angular-devkit/core": "18.2.6", + "@angular/build": "18.2.6", + "@babel/core": "7.25.2", + "@babel/generator": "7.25.0", + "@babel/helper-annotate-as-pure": "7.24.7", + "@babel/helper-split-export-declaration": "7.24.7", + "@babel/plugin-transform-async-generator-functions": "7.25.0", + "@babel/plugin-transform-async-to-generator": "7.24.7", + "@babel/plugin-transform-runtime": "7.24.7", + "@babel/preset-env": "7.25.3", + "@babel/runtime": "7.25.0", + "@discoveryjs/json-ext": "0.6.1", + "@ngtools/webpack": "18.2.6", "@vitejs/plugin-basic-ssl": "1.1.0", "ansi-colors": "4.1.3", - "autoprefixer": "10.4.18", + "autoprefixer": "10.4.20", "babel-loader": "9.1.3", - "babel-plugin-istanbul": "6.1.1", "browserslist": "^4.21.5", - "copy-webpack-plugin": "11.0.0", - "critters": "0.0.22", - "css-loader": "6.10.0", - "esbuild-wasm": "0.20.1", + "copy-webpack-plugin": "12.0.2", + "critters": "0.0.24", + "css-loader": "7.1.2", + "esbuild-wasm": "0.23.0", "fast-glob": "3.3.2", - "http-proxy-middleware": "2.0.6", - "https-proxy-agent": "7.0.4", - "inquirer": "9.2.15", - "jsonc-parser": "3.2.1", + "http-proxy-middleware": "3.0.0", + "https-proxy-agent": "7.0.5", + "istanbul-lib-instrument": "6.0.3", + "jsonc-parser": "3.3.1", "karma-source-map-support": "1.4.0", "less": "4.2.0", - "less-loader": "11.1.0", + "less-loader": "12.2.0", "license-webpack-plugin": "4.0.2", - "loader-utils": "3.2.1", - "magic-string": "0.30.8", - "mini-css-extract-plugin": "2.8.1", + "loader-utils": "3.3.1", + "magic-string": "0.30.11", + "mini-css-extract-plugin": "2.9.0", "mrmime": "2.0.0", - "open": "8.4.2", + "open": "10.1.0", "ora": "5.4.1", "parse5-html-rewriting-stream": "7.0.0", - "picomatch": "4.0.1", - "piscina": "4.4.0", - "postcss": "8.4.35", + "picomatch": "4.0.2", + "piscina": "4.6.1", + "postcss": "8.4.41", "postcss-loader": "8.1.1", "resolve-url-loader": "5.0.0", "rxjs": "7.8.1", - "sass": "1.71.1", - "sass-loader": "14.1.1", - "semver": "7.6.0", + "sass": "1.77.6", + "sass-loader": "16.0.0", + "semver": "7.6.3", "source-map-loader": "5.0.0", "source-map-support": "0.5.21", - "terser": "5.29.1", + "terser": "5.31.6", "tree-kill": "1.2.2", - "tslib": "2.6.2", - "undici": "6.11.1", - "vite": "5.1.7", - "watchpack": "2.4.0", - "webpack": "5.90.3", - "webpack-dev-middleware": "6.1.2", - "webpack-dev-server": "4.15.1", - "webpack-merge": "5.10.0", + "tslib": "2.6.3", + "vite": "5.4.6", + "watchpack": "2.4.1", + "webpack": "5.94.0", + "webpack-dev-middleware": "7.4.2", + "webpack-dev-server": "5.0.4", + "webpack-merge": "6.0.1", "webpack-subresource-integrity": "5.1.0" }, "engines": { - "node": "^18.13.0 || >=20.9.0", + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" }, "optionalDependencies": { - "esbuild": "0.20.1" + "esbuild": "0.23.0" }, "peerDependencies": { - "@angular/compiler-cli": "^17.0.0", - "@angular/localize": "^17.0.0", - "@angular/platform-server": "^17.0.0", - "@angular/service-worker": "^17.0.0", + "@angular/compiler-cli": "^18.0.0", + "@angular/localize": "^18.0.0", + "@angular/platform-server": "^18.0.0", + "@angular/service-worker": "^18.0.0", "@web/test-runner": "^0.18.0", "browser-sync": "^3.0.2", "jest": "^29.5.0", "jest-environment-jsdom": "^29.5.0", "karma": "^6.3.0", - "ng-packagr": "^17.0.0", + "ng-packagr": "^18.0.0", "protractor": "^7.0.0", "tailwindcss": "^2.0.0 || ^3.0.0", - "typescript": ">=5.2 <5.5" + "typescript": ">=5.4 <5.6" }, "peerDependenciesMeta": { "@angular/localize": { @@ -209,40 +209,46 @@ } } }, + "node_modules/@angular-devkit/build-angular/node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "dev": true + }, "node_modules/@angular-devkit/build-webpack": { - "version": "0.1703.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1703.6.tgz", - "integrity": "sha512-pJu0et2SiF0kfXenHSTtAART0omzbWpLgBfeUo4hBh4uwX5IaT+mRpYpr8gCXMq+qsjoQp3HobSU3lPDeBn+bg==", + "version": "0.1802.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1802.6.tgz", + "integrity": "sha512-JMLcXFaitJplwZMKkqhbYirINCRD6eOPZuIGaIOVynXYGWgvJkLT9t5C2wm9HqSLtp1K7NcYG2Y7PtTVR4krnQ==", "dev": true, "dependencies": { - "@angular-devkit/architect": "0.1703.6", + "@angular-devkit/architect": "0.1802.6", "rxjs": "7.8.1" }, "engines": { - "node": "^18.13.0 || >=20.9.0", + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" }, "peerDependencies": { "webpack": "^5.30.0", - "webpack-dev-server": "^4.0.0" + "webpack-dev-server": "^5.0.2" } }, "node_modules/@angular-devkit/core": { - "version": "17.3.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.6.tgz", - "integrity": "sha512-FVbkT9dEwHEvjnxr4mvMNSMg2bCFoGoP4X68xXU9dhLEUpC05opLvfbaR3Qh543eCJ5AstosBFVzB/krfIkOvA==", + "version": "18.2.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.2.6.tgz", + "integrity": "sha512-la4CFvs5PcRWSkQ/H7TB5cPZirFVA9GoWk5LzIk8si6VjWBJRm8b3keKJoC9LlNeABRUIR5z0ocYkyQQUhdMfg==", "dev": true, "dependencies": { - "ajv": "8.12.0", - "ajv-formats": "2.1.1", - "jsonc-parser": "3.2.1", - "picomatch": "4.0.1", + "ajv": "8.17.1", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.2", "rxjs": "7.8.1", "source-map": "0.7.4" }, "engines": { - "node": "^18.13.0 || >=20.9.0", + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" }, @@ -256,440 +262,280 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "17.0.10", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.0.10.tgz", - "integrity": "sha512-hjf4gaMx2uB6ZhBstBSH0Q2hzfp6kxI4IiJ5i1QrxPNE1MdGnb2h+LgPTRCdO72a7PGeWcSxFRE7cxrXeQy19g==", + "version": "18.2.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-18.2.6.tgz", + "integrity": "sha512-uIttrQ2cQ2PWAFFVPeCoNR8xvs7tPJ2i8gzqsIwYdge107xDC6u9CqfgmBqPDSFpWj+IiC2Jwcm8Z4HYKU4+7A==", "dev": true, "dependencies": { - "@angular-devkit/core": "17.0.10", - "jsonc-parser": "3.2.0", - "magic-string": "0.30.5", + "@angular-devkit/core": "18.2.6", + "jsonc-parser": "3.3.1", + "magic-string": "0.30.11", "ora": "5.4.1", "rxjs": "7.8.1" }, "engines": { - "node": "^18.13.0 || >=20.9.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - } - }, - "node_modules/@angular-devkit/schematics/node_modules/@angular-devkit/core": { - "version": "17.0.10", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.0.10.tgz", - "integrity": "sha512-93N6oHnmtRt0hL3AXxvnk47sN1rHndfj+pqI5haEY41AGWzIWv9cSBsqlM0PWltNpo6VivcExZESvbLJ71wqbQ==", - "dev": true, - "dependencies": { - "ajv": "8.12.0", - "ajv-formats": "2.1.1", - "jsonc-parser": "3.2.0", - "picomatch": "3.0.1", - "rxjs": "7.8.1", - "source-map": "0.7.4" - }, - "engines": { - "node": "^18.13.0 || >=20.9.0", + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "chokidar": "^3.5.2" - }, - "peerDependenciesMeta": { - "chokidar": { - "optional": true - } - } - }, - "node_modules/@angular-devkit/schematics/node_modules/jsonc-parser": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", - "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", - "dev": true - }, - "node_modules/@angular-devkit/schematics/node_modules/magic-string": { - "version": "0.30.5", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", - "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==", - "dev": true, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@angular-devkit/schematics/node_modules/picomatch": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-3.0.1.tgz", - "integrity": "sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/@angular-eslint/builder": { - "version": "17.2.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-17.2.0.tgz", - "integrity": "sha512-xPxgCTPcnFRT8OYs9R5UZVAtzVouIIfdMOqTcB847Cev4H8kqRz0gO5aqkQiL+0erwnLf8D4nRzMTJjSBpQQNw==", + "version": "18.3.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-18.3.0.tgz", + "integrity": "sha512-httEQyqyBw3+0CRtAa7muFxHrauRfkEfk/jmrh5fn2Eiu+I53hAqFPgrwVi1V6AP/kj2zbAiWhd5xM3pMJdoRQ==", "dev": true, - "dependencies": { - "@nx/devkit": "17.2.8", - "nx": "17.2.8" - }, "peerDependencies": { - "eslint": "^7.20.0 || ^8.0.0", + "eslint": "^8.57.0 || ^9.0.0", "typescript": "*" } }, "node_modules/@angular-eslint/bundled-angular-compiler": { - "version": "17.2.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-17.2.0.tgz", - "integrity": "sha512-uBvPbPE2JxqpdLs//Nd5+TRLgjxDxvTYgmGFTKI9Eo98krqps+rhSQCRSHWACukzc25X3Q4ITHfvjODQL8qQkg==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-18.3.1.tgz", + "integrity": "sha512-sikmkjfsXPpPTku1aQkQ1MNNEKGBgGGRvUN/WeNS9dhCJ4dxU3O7dZctt1aQWj+W3nbuUtDiimAWF5fZHGFE2Q==", "dev": true }, "node_modules/@angular-eslint/eslint-plugin": { - "version": "17.2.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-17.2.0.tgz", - "integrity": "sha512-8A3hD/11N6QEchsAGggTPmNsa0GS5p44t930slMsxrTvdSlKAo56FzVdxwSkOcejKIJs57oWxoKvtK4UyLYkeA==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-18.3.1.tgz", + "integrity": "sha512-MP4Nm+SHboF8KdnN0KpPEGAaTTzDLPm3+S/4W3Mg8onqWCyadyd4mActh9mK/pvCj8TVlb/SW1zeTtdMYhwonw==", "dev": true, "dependencies": { - "@angular-eslint/utils": "17.2.0", - "@typescript-eslint/utils": "6.18.0" + "@angular-eslint/bundled-angular-compiler": "18.3.1", + "@angular-eslint/utils": "18.3.1" }, "peerDependencies": { - "eslint": "^7.20.0 || ^8.0.0", + "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", + "eslint": "^8.57.0 || ^9.0.0", "typescript": "*" } }, "node_modules/@angular-eslint/eslint-plugin-template": { - "version": "17.2.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-17.2.0.tgz", - "integrity": "sha512-CkcAOWWqNwX0FXeLwJu0Vctso8q/NPHJ95R2Cy8hjwuMyFF83/vDouyeIjYC+SRv6hbevmNa+BbdYXhQZinIHw==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-18.3.1.tgz", + "integrity": "sha512-hBJ3+f7VSidvrtYaXH7Vp0sWvblA9jLK2c6uQzhYGWdEDUcTg7g7VI9ThW39WvMbHqkyzNE4PPOynK69cBEDGg==", "dev": true, "dependencies": { - "@angular-eslint/bundled-angular-compiler": "17.2.0", - "@angular-eslint/utils": "17.2.0", - "@typescript-eslint/type-utils": "6.18.0", - "@typescript-eslint/utils": "6.18.0", + "@angular-eslint/bundled-angular-compiler": "18.3.1", + "@angular-eslint/utils": "18.3.1", "aria-query": "5.3.0", - "axobject-query": "4.0.0" + "axobject-query": "4.1.0" }, "peerDependencies": { - "eslint": "^7.20.0 || ^8.0.0", + "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", + "eslint": "^8.57.0 || ^9.0.0", "typescript": "*" } }, "node_modules/@angular-eslint/schematics": { - "version": "17.2.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-17.2.0.tgz", - "integrity": "sha512-lV2+2H3Hf6FCJfM+cddJ/7ss3qc99OO2wuvTjGNH512mP75tvfLakV+e6TFFdzK0km+ceXvB2VqNXMSShB4PVQ==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-18.3.1.tgz", + "integrity": "sha512-BTsQHDu7LjvXannJTb5BqMPCFIHRNN94eRyb60VfjJxB/ZFtsbAQDFFOi5lEZsRsd4mBeUMuL9mW4IMcPtUQ9Q==", "dev": true, "dependencies": { - "@angular-eslint/eslint-plugin": "17.2.0", - "@angular-eslint/eslint-plugin-template": "17.2.0", - "@nx/devkit": "17.2.8", - "ignore": "5.3.0", - "nx": "17.2.8", - "strip-json-comments": "3.1.1", - "tmp": "0.2.1" + "@angular-eslint/eslint-plugin": "18.3.1", + "@angular-eslint/eslint-plugin-template": "18.3.1", + "ignore": "5.3.2", + "semver": "7.6.3", + "strip-json-comments": "3.1.1" }, "peerDependencies": { - "@angular/cli": ">= 17.0.0 < 18.0.0" + "@angular-devkit/core": ">= 18.0.0 < 19.0.0", + "@angular-devkit/schematics": ">= 18.0.0 < 19.0.0" } }, "node_modules/@angular-eslint/template-parser": { - "version": "17.2.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-17.2.0.tgz", - "integrity": "sha512-Js9w1IXWPvXEjd05bWkZzRaLw0g0mJPztAWOj3DiU7H9LKkautQq0zZu02cAAnXZim2CsAagEh2GmGjhaYvoKg==", + "version": "18.3.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-18.3.0.tgz", + "integrity": "sha512-1mUquqcnugI4qsoxcYZKZ6WMi6RPelDcJZg2YqGyuaIuhWmi3ZqJZLErSSpjP60+TbYZu7wM8Kchqa1bwJtEaQ==", "dev": true, "dependencies": { - "@angular-eslint/bundled-angular-compiler": "17.2.0", - "eslint-scope": "^8.0.0" + "@angular-eslint/bundled-angular-compiler": "18.3.0", + "eslint-scope": "^8.0.2" }, "peerDependencies": { - "eslint": "^7.20.0 || ^8.0.0", + "eslint": "^8.57.0 || ^9.0.0", "typescript": "*" } }, + "node_modules/@angular-eslint/template-parser/node_modules/@angular-eslint/bundled-angular-compiler": { + "version": "18.3.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-18.3.0.tgz", + "integrity": "sha512-v/59FxUKnMzymVce99gV43huxoqXWMb85aKvzlNvLN+ScDu6ZE4YMiTQNpfapVL2lkxhs0uwB3jH17EYd5TcsA==", + "dev": true + }, "node_modules/@angular-eslint/utils": { - "version": "17.2.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-17.2.0.tgz", - "integrity": "sha512-J7DsFKb5yxv8te8LQvChNn6MBvKulcBx+jtHX1uen+uuuv8XhZuVMZXS0rolUkdl1Q0mBeHpkuO2q6Vh17pqbQ==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-18.3.1.tgz", + "integrity": "sha512-sd9niZI7h9H2FQ7OLiQsLFBhjhRQTASh+Q0+4+hyjv9idbSHBJli8Gsi2fqj9zhtMKpAZFTrWzuLUpubJ9UYbA==", "dev": true, "dependencies": { - "@angular-eslint/bundled-angular-compiler": "17.2.0", - "@typescript-eslint/utils": "6.18.0" + "@angular-eslint/bundled-angular-compiler": "18.3.1" }, "peerDependencies": { - "eslint": "^7.20.0 || ^8.0.0", + "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", + "eslint": "^8.57.0 || ^9.0.0", "typescript": "*" } }, "node_modules/@angular/animations": { - "version": "17.3.7", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-17.3.7.tgz", - "integrity": "sha512-ahenGALPPweeHgqtl9BMkGIAV4fUNI5kOWUrLNbKBfwIJN+aOBOYV1Jz6NKUQq6eYn/1ZYtm0f3lIkHIdtLKEw==", + "version": "18.2.6", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-18.2.6.tgz", + "integrity": "sha512-vy9wy+Q9beiRxkEO8wNxFQ63AqAujGvk8AUHepxxIT7QNNc512TNKz8uH+feWDPO38Dm2obwYQHMGzs3WO7pUA==", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^18.13.0 || >=20.9.0" + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/core": "17.3.7" - } - }, - "node_modules/@angular/cdk": { - "version": "17.3.7", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-17.3.7.tgz", - "integrity": "sha512-aFEh8tzKFOwini6aNEp57S54Ocp9T7YIJfBVMESptu2TCPdMTlJ1HJTg5XS8NcQO+vwi9cFPGVwGF1frOx4LXA==", - "dependencies": { - "tslib": "^2.3.0" - }, - "optionalDependencies": { - "parse5": "^7.1.2" - }, - "peerDependencies": { - "@angular/common": "^17.0.0 || ^18.0.0", - "@angular/core": "^17.0.0 || ^18.0.0", - "rxjs": "^6.5.3 || ^7.4.0" - } - }, - "node_modules/@angular/cli": { - "version": "17.0.10", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-17.0.10.tgz", - "integrity": "sha512-52rd8KmOMe3NJDp/wA+Mwj21qd4HR8fuLtfrErgVnZaJZKX2Bzi/z7FHQD3gdgMAdzUiG0OJWGM0h75Ls9X6Gw==", - "dev": true, - "dependencies": { - "@angular-devkit/architect": "0.1700.10", - "@angular-devkit/core": "17.0.10", - "@angular-devkit/schematics": "17.0.10", - "@schematics/angular": "17.0.10", - "@yarnpkg/lockfile": "1.1.0", - "ansi-colors": "4.1.3", - "ini": "4.1.1", - "inquirer": "9.2.11", - "jsonc-parser": "3.2.0", - "npm-package-arg": "11.0.1", - "npm-pick-manifest": "9.0.0", - "open": "8.4.2", - "ora": "5.4.1", - "pacote": "17.0.4", - "resolve": "1.22.8", - "semver": "7.5.4", - "symbol-observable": "4.0.0", - "yargs": "17.7.2" - }, - "bin": { - "ng": "bin/ng.js" - }, - "engines": { - "node": "^18.13.0 || >=20.9.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - } - }, - "node_modules/@angular/cli/node_modules/@angular-devkit/architect": { - "version": "0.1700.10", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1700.10.tgz", - "integrity": "sha512-JD/3jkdN1jrFMIDEk9grKdbjutIoxUDMRazq1LZooWjTkzlYk09i/s6HwvIPao7zvxJfelD6asTPspgkjOMP5A==", - "dev": true, - "dependencies": { - "@angular-devkit/core": "17.0.10", - "rxjs": "7.8.1" - }, - "engines": { - "node": "^18.13.0 || >=20.9.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" + "@angular/core": "18.2.6" } }, - "node_modules/@angular/cli/node_modules/@angular-devkit/core": { - "version": "17.0.10", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.0.10.tgz", - "integrity": "sha512-93N6oHnmtRt0hL3AXxvnk47sN1rHndfj+pqI5haEY41AGWzIWv9cSBsqlM0PWltNpo6VivcExZESvbLJ71wqbQ==", + "node_modules/@angular/build": { + "version": "18.2.6", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-18.2.6.tgz", + "integrity": "sha512-TQzX6Mi7uXFvmz7+OVl4Za7WawYPcx+B5Ewm6IY/DdMyB9P/Z4tbKb1LO+ynWUXYwm7avXo6XQQ4m5ArDY5F/A==", "dev": true, "dependencies": { - "ajv": "8.12.0", - "ajv-formats": "2.1.1", - "jsonc-parser": "3.2.0", - "picomatch": "3.0.1", - "rxjs": "7.8.1", - "source-map": "0.7.4" + "@ampproject/remapping": "2.3.0", + "@angular-devkit/architect": "0.1802.6", + "@babel/core": "7.25.2", + "@babel/helper-annotate-as-pure": "7.24.7", + "@babel/helper-split-export-declaration": "7.24.7", + "@babel/plugin-syntax-import-attributes": "7.24.7", + "@inquirer/confirm": "3.1.22", + "@vitejs/plugin-basic-ssl": "1.1.0", + "browserslist": "^4.23.0", + "critters": "0.0.24", + "esbuild": "0.23.0", + "fast-glob": "3.3.2", + "https-proxy-agent": "7.0.5", + "listr2": "8.2.4", + "lmdb": "3.0.13", + "magic-string": "0.30.11", + "mrmime": "2.0.0", + "parse5-html-rewriting-stream": "7.0.0", + "picomatch": "4.0.2", + "piscina": "4.6.1", + "rollup": "4.22.4", + "sass": "1.77.6", + "semver": "7.6.3", + "vite": "5.4.6", + "watchpack": "2.4.1" }, "engines": { - "node": "^18.13.0 || >=20.9.0", + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" }, "peerDependencies": { - "chokidar": "^3.5.2" + "@angular/compiler-cli": "^18.0.0", + "@angular/localize": "^18.0.0", + "@angular/platform-server": "^18.0.0", + "@angular/service-worker": "^18.0.0", + "less": "^4.2.0", + "postcss": "^8.4.0", + "tailwindcss": "^2.0.0 || ^3.0.0", + "typescript": ">=5.4 <5.6" }, "peerDependenciesMeta": { - "chokidar": { + "@angular/localize": { + "optional": true + }, + "@angular/platform-server": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "less": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tailwindcss": { "optional": true } } }, - "node_modules/@angular/cli/node_modules/chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", - "dev": true, - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@angular/cli/node_modules/escape-string-regexp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", - "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@angular/cli/node_modules/figures": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-5.0.0.tgz", - "integrity": "sha512-ej8ksPF4x6e5wvK9yevct0UCXh8TTFlWGVLlgjZuoBH1HwjIfKE/IdL5mq89sFA7zELi1VhKpmtDnrs7zWyeyg==", - "dev": true, - "dependencies": { - "escape-string-regexp": "^5.0.0", - "is-unicode-supported": "^1.2.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@angular/cli/node_modules/inquirer": { - "version": "9.2.11", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.11.tgz", - "integrity": "sha512-B2LafrnnhbRzCWfAdOXisUzL89Kg8cVJlYmhqoi3flSiV/TveO+nsXwgKr9h9PIo+J1hz7nBSk6gegRIMBBf7g==", - "dev": true, - "dependencies": { - "@ljharb/through": "^2.3.9", - "ansi-escapes": "^4.3.2", - "chalk": "^5.3.0", - "cli-cursor": "^3.1.0", - "cli-width": "^4.1.0", - "external-editor": "^3.1.0", - "figures": "^5.0.0", - "lodash": "^4.17.21", - "mute-stream": "1.0.0", - "ora": "^5.4.1", - "run-async": "^3.0.0", - "rxjs": "^7.8.1", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^6.2.0" - }, - "engines": { - "node": ">=14.18.0" - } - }, - "node_modules/@angular/cli/node_modules/is-unicode-supported": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", - "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@angular/cli/node_modules/jsonc-parser": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", - "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", - "dev": true - }, - "node_modules/@angular/cli/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, + "node_modules/@angular/cdk": { + "version": "18.2.6", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-18.2.6.tgz", + "integrity": "sha512-Gfq/iv4zhlKYpdQkDaBRwxI71NHNUHM1Cs1XhnZ0/oFct5HXvSv1RHRGTKqBJLLACaAPzZKXJ/UglLoyO5CNiQ==", "dependencies": { - "yallist": "^4.0.0" + "tslib": "^2.3.0" }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@angular/cli/node_modules/picomatch": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-3.0.1.tgz", - "integrity": "sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag==", - "dev": true, - "engines": { - "node": ">=10" + "optionalDependencies": { + "parse5": "^7.1.2" }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "peerDependencies": { + "@angular/common": "^18.0.0 || ^19.0.0", + "@angular/core": "^18.0.0 || ^19.0.0", + "rxjs": "^6.5.3 || ^7.4.0" } }, - "node_modules/@angular/cli/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "node_modules/@angular/cli": { + "version": "18.2.6", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-18.2.6.tgz", + "integrity": "sha512-tdXsnV/w+Rgu8q0zFsLU5L9ImTVqrTol1vppHaQkJ/vuoHy+s8ZEbBqhVrO/ffosNb2xseUybGYvqMS4zkNQjg==", "dev": true, "dependencies": { - "lru-cache": "^6.0.0" + "@angular-devkit/architect": "0.1802.6", + "@angular-devkit/core": "18.2.6", + "@angular-devkit/schematics": "18.2.6", + "@inquirer/prompts": "5.3.8", + "@listr2/prompt-adapter-inquirer": "2.0.15", + "@schematics/angular": "18.2.6", + "@yarnpkg/lockfile": "1.1.0", + "ini": "4.1.3", + "jsonc-parser": "3.3.1", + "listr2": "8.2.4", + "npm-package-arg": "11.0.3", + "npm-pick-manifest": "9.1.0", + "pacote": "18.0.6", + "resolve": "1.22.8", + "semver": "7.6.3", + "symbol-observable": "4.0.0", + "yargs": "17.7.2" }, "bin": { - "semver": "bin/semver.js" + "ng": "bin/ng.js" }, "engines": { - "node": ">=10" + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" } }, - "node_modules/@angular/cli/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/@angular/common": { - "version": "17.3.7", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-17.3.7.tgz", - "integrity": "sha512-A7LRJu1vVCGGgrfZXjU+njz50SiU4weheKCar5PIUprcdIofS1IrHAJDqYh+kwXxkjXbZMOr/ijQY0+AESLEsw==", + "version": "18.2.6", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-18.2.6.tgz", + "integrity": "sha512-89793ow+wrI1c7C6kyMbnweLNIZHzXthosxAEjipRZGBrqBYjvTtkE45Fl+5yBa3JO7bAhyGkUnEoyvWtZIAEA==", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^18.13.0 || >=20.9.0" + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/core": "17.3.7", + "@angular/core": "18.2.6", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/compiler": { - "version": "17.3.7", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-17.3.7.tgz", - "integrity": "sha512-AlKiqPoxnrpQ0hn13fIaQPSVodaVAIjBW4vpFyuKFqs2LBKg6iolwZ21s8rEI0KR2gXl+8ugj0/UZ6YADiVM5w==", + "version": "18.2.6", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-18.2.6.tgz", + "integrity": "sha512-3tX2/Qw+bZ8XzKitviH8jzNGyY0uohhehhBB57OJOCc+yr4ojy/7SYFnun1lSsRnDztdCE461641X4iQLCQ94w==", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^18.13.0 || >=20.9.0" + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/core": "17.3.7" + "@angular/core": "18.2.6" }, "peerDependenciesMeta": { "@angular/core": { @@ -698,12 +544,12 @@ } }, "node_modules/@angular/compiler-cli": { - "version": "17.3.7", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-17.3.7.tgz", - "integrity": "sha512-vSg5IQZ9jGmvYjpbfH8KbH4Sl1IVeE+Mr1ogcxkGEsURSRvKo7EWc0K7LSEI9+gg0VLamMiP9EyCJdPxiJeLJQ==", + "version": "18.2.6", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-18.2.6.tgz", + "integrity": "sha512-b5x9STfjNiNM/S0D+CnqRP9UOxPtSz1+RlCH5WdOMiW/p8j5p6dBix8YYgTe6Wg3OD7eItD2pnFQKgF/dWiopA==", "dev": true, "dependencies": { - "@babel/core": "7.23.9", + "@babel/core": "7.25.2", "@jridgewell/sourcemap-codec": "^1.4.14", "chokidar": "^3.0.0", "convert-source-map": "^1.5.1", @@ -718,168 +564,76 @@ "ngcc": "bundles/ngcc/index.js" }, "engines": { - "node": "^18.13.0 || >=20.9.0" + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/compiler": "17.3.7", - "typescript": ">=5.2 <5.5" - } - }, - "node_modules/@angular/compiler-cli/node_modules/@babel/core": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.9.tgz", - "integrity": "sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==", - "dev": true, - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.23.5", - "@babel/generator": "^7.23.6", - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.23.9", - "@babel/parser": "^7.23.9", - "@babel/template": "^7.23.9", - "@babel/traverse": "^7.23.9", - "@babel/types": "^7.23.9", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true - }, - "node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" + "@angular/compiler": "18.2.6", + "typescript": ">=5.4 <5.6" } }, "node_modules/@angular/core": { - "version": "17.3.7", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-17.3.7.tgz", - "integrity": "sha512-HWcrbxqnvIMSxFuQdN0KPt08bc87hqr0LKm89yuRTUwx/2sNJlNQUobk6aJj4trswGBttcRDT+GOS4DQP2Nr4g==", + "version": "18.2.6", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-18.2.6.tgz", + "integrity": "sha512-PjFad2j4YBwLVTw+0Te8CJCa/tV0W8caTHG8aOjj3ObdL6ihGI+FKnwerLc9RVzDFd14BOO4C6/+LbOQAh3Ltw==", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^18.13.0 || >=20.9.0" + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { "rxjs": "^6.5.3 || ^7.4.0", - "zone.js": "~0.14.0" + "zone.js": "~0.14.10" } }, "node_modules/@angular/forms": { - "version": "17.3.7", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-17.3.7.tgz", - "integrity": "sha512-FEhXh/VmT++XCoO8i7bBtzxG7Am/cE1zrr9aF+fWW+4jpWvJvVN1IaSiJxgBB+iPsOJ9lTBRwfRW3onlcDkhrw==", + "version": "18.2.6", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-18.2.6.tgz", + "integrity": "sha512-quGkUqTxlBaLB8C/RnpfFG57fdmNF5RQ+368N89Ma++2lpIsVAHaGZZn4yOyo3wNYaM2jBxNqaYxOzZNUl5Tig==", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^18.13.0 || >=20.9.0" + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "17.3.7", - "@angular/core": "17.3.7", - "@angular/platform-browser": "17.3.7", + "@angular/common": "18.2.6", + "@angular/core": "18.2.6", + "@angular/platform-browser": "18.2.6", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/material": { - "version": "17.3.7", - "resolved": "https://registry.npmjs.org/@angular/material/-/material-17.3.7.tgz", - "integrity": "sha512-wjSKkk9KZE8QiBPkMd5axh5u/3pUSxoLKNO7OasFhEagMmSv5oYTLm40cErhtb4UdkSmbC19WuuluS6P3leoPA==", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/auto-init": "15.0.0-canary.7f224ddd4.0", - "@material/banner": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/button": "15.0.0-canary.7f224ddd4.0", - "@material/card": "15.0.0-canary.7f224ddd4.0", - "@material/checkbox": "15.0.0-canary.7f224ddd4.0", - "@material/chips": "15.0.0-canary.7f224ddd4.0", - "@material/circular-progress": "15.0.0-canary.7f224ddd4.0", - "@material/data-table": "15.0.0-canary.7f224ddd4.0", - "@material/density": "15.0.0-canary.7f224ddd4.0", - "@material/dialog": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/drawer": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/fab": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/floating-label": "15.0.0-canary.7f224ddd4.0", - "@material/form-field": "15.0.0-canary.7f224ddd4.0", - "@material/icon-button": "15.0.0-canary.7f224ddd4.0", - "@material/image-list": "15.0.0-canary.7f224ddd4.0", - "@material/layout-grid": "15.0.0-canary.7f224ddd4.0", - "@material/line-ripple": "15.0.0-canary.7f224ddd4.0", - "@material/linear-progress": "15.0.0-canary.7f224ddd4.0", - "@material/list": "15.0.0-canary.7f224ddd4.0", - "@material/menu": "15.0.0-canary.7f224ddd4.0", - "@material/menu-surface": "15.0.0-canary.7f224ddd4.0", - "@material/notched-outline": "15.0.0-canary.7f224ddd4.0", - "@material/radio": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/segmented-button": "15.0.0-canary.7f224ddd4.0", - "@material/select": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/slider": "15.0.0-canary.7f224ddd4.0", - "@material/snackbar": "15.0.0-canary.7f224ddd4.0", - "@material/switch": "15.0.0-canary.7f224ddd4.0", - "@material/tab": "15.0.0-canary.7f224ddd4.0", - "@material/tab-bar": "15.0.0-canary.7f224ddd4.0", - "@material/tab-indicator": "15.0.0-canary.7f224ddd4.0", - "@material/tab-scroller": "15.0.0-canary.7f224ddd4.0", - "@material/textfield": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/tooltip": "15.0.0-canary.7f224ddd4.0", - "@material/top-app-bar": "15.0.0-canary.7f224ddd4.0", - "@material/touch-target": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", + "version": "18.2.6", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-18.2.6.tgz", + "integrity": "sha512-ObxC/vomSb9QF3vIztuiInQzws+D6u09Dhfx6uNFjtyICqxEFpF7+Qx7QVDWrsuXOgxZTKgacK8f46iV8hWUfg==", + "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { - "@angular/animations": "^17.0.0 || ^18.0.0", - "@angular/cdk": "17.3.7", - "@angular/common": "^17.0.0 || ^18.0.0", - "@angular/core": "^17.0.0 || ^18.0.0", - "@angular/forms": "^17.0.0 || ^18.0.0", - "@angular/platform-browser": "^17.0.0 || ^18.0.0", + "@angular/animations": "^18.0.0 || ^19.0.0", + "@angular/cdk": "18.2.6", + "@angular/common": "^18.0.0 || ^19.0.0", + "@angular/core": "^18.0.0 || ^19.0.0", + "@angular/forms": "^18.0.0 || ^19.0.0", + "@angular/platform-browser": "^18.0.0 || ^19.0.0", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/platform-browser": { - "version": "17.3.7", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-17.3.7.tgz", - "integrity": "sha512-Nn8ZMaftAvO9dEwribWdNv+QBHhYIBrRkv85G6et80AXfXoYAr/xcfnQECRFtZgPmANqHC5auv/xrmExQG+Yeg==", + "version": "18.2.6", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-18.2.6.tgz", + "integrity": "sha512-RA8UMiYNLga+QMwpKcDw1357gYPfPyY/rmLeezMak//BbsENFYQOJ4Z6DBOBNiPlHxmBsUJMGaKdlpQhfCROyQ==", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^18.13.0 || >=20.9.0" + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/animations": "17.3.7", - "@angular/common": "17.3.7", - "@angular/core": "17.3.7" + "@angular/animations": "18.2.6", + "@angular/common": "18.2.6", + "@angular/core": "18.2.6" }, "peerDependenciesMeta": { "@angular/animations": { @@ -888,46 +642,46 @@ } }, "node_modules/@angular/platform-browser-dynamic": { - "version": "17.3.7", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-17.3.7.tgz", - "integrity": "sha512-9c2I4u0L1p2v1/lW8qy+WaNHisUWbyy6wqsv2v9FfCaSM49Lxymgo9LPFPC4qEG5ei5nE+eIQ2ocRiXXsf5QkQ==", + "version": "18.2.6", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-18.2.6.tgz", + "integrity": "sha512-kGBU3FNc+DF9r33hwHZqiWoZgQbCDdEIucU0NCLCIg0Hw6/Q9Hr2ndjxQI+WynCPg0JeBn34jpouvpeJer3YDQ==", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^18.13.0 || >=20.9.0" + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "17.3.7", - "@angular/compiler": "17.3.7", - "@angular/core": "17.3.7", - "@angular/platform-browser": "17.3.7" + "@angular/common": "18.2.6", + "@angular/compiler": "18.2.6", + "@angular/core": "18.2.6", + "@angular/platform-browser": "18.2.6" } }, "node_modules/@angular/router": { - "version": "17.3.7", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-17.3.7.tgz", - "integrity": "sha512-lMkuRrc1ZjP5JPDxNHqoAhB0uAnfPQ/q6mJrw1s8IZoVV6VyM+FxR5r13ajNcXWC38xy/YhBjpXPF1vBdxuLXg==", + "version": "18.2.6", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-18.2.6.tgz", + "integrity": "sha512-t57Sqja8unHhZlPr+4CWnQacuox2M4p2pMHps+31wt337qH6mKf4jqDmK0dE/MFdRyKjT2a2E/2NwtxXxcWNuw==", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^18.13.0 || >=20.9.0" + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "17.3.7", - "@angular/core": "17.3.7", - "@angular/platform-browser": "17.3.7", + "@angular/common": "18.2.6", + "@angular/core": "18.2.6", + "@angular/platform-browser": "18.2.6", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@babel/code-frame": { - "version": "7.24.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", - "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", "dev": true, "dependencies": { - "@babel/highlight": "^7.24.2", + "@babel/highlight": "^7.24.7", "picocolors": "^1.0.0" }, "engines": { @@ -935,30 +689,30 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.4.tgz", - "integrity": "sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.4.tgz", + "integrity": "sha512-+LGRog6RAsCJrrrg/IO6LGmpphNe5DiK30dGjCoxxeGv49B10/3XYGxPsAwrDlMFcFEvdAUavDT8r9k/hSyQqQ==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz", - "integrity": "sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz", + "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.23.5", - "@babel/generator": "^7.23.6", - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.24.0", - "@babel/parser": "^7.24.0", - "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.0", - "@babel/types": "^7.24.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.0", + "@babel/helper-compilation-targets": "^7.25.2", + "@babel/helper-module-transforms": "^7.25.2", + "@babel/helpers": "^7.25.0", + "@babel/parser": "^7.25.0", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.2", + "@babel/types": "^7.25.2", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -989,14 +743,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", - "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.0.tgz", + "integrity": "sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==", "dev": true, "dependencies": { - "@babel/types": "^7.23.6", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", + "@babel/types": "^7.25.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" }, "engines": { @@ -1004,38 +758,39 @@ } }, "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", - "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz", + "integrity": "sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==", "dev": true, "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz", - "integrity": "sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.24.7.tgz", + "integrity": "sha512-xZeCVVdwb4MsDBkkyZ64tReWYrLRHlMN72vP7Bdm3OUOuyFZExhsHUUnuWnm2/XOlAJzR0LfPpB56WXZn0X/lA==", "dev": true, "dependencies": { - "@babel/types": "^7.22.15" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", - "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz", + "integrity": "sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.23.5", - "@babel/helper-validator-option": "^7.23.5", - "browserslist": "^4.22.2", + "@babel/compat-data": "^7.25.2", + "@babel/helper-validator-option": "^7.24.8", + "browserslist": "^4.23.1", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -1053,19 +808,17 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.5.tgz", - "integrity": "sha512-uRc4Cv8UQWnE4NXlYTIIdM7wfFkOqlFztcC/gVXDKohKoVB3OyonfelUBaJzSwpBntZ2KYGF/9S7asCHsXwW6g==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-member-expression-to-functions": "^7.24.5", - "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/helper-replace-supers": "^7.24.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.24.5", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.4.tgz", + "integrity": "sha512-ro/bFs3/84MDgDmMwbcHgDa8/E6J3QKNTk4xJJnVeFtGE+tL0K26E3pNxhYz2b67fJpt7Aphw5XcploKXuCvCQ==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-member-expression-to-functions": "^7.24.8", + "@babel/helper-optimise-call-expression": "^7.24.7", + "@babel/helper-replace-supers": "^7.25.0", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", + "@babel/traverse": "^7.25.4", "semver": "^6.3.1" }, "engines": { @@ -1075,18 +828,6 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/helper-split-export-declaration": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.5.tgz", - "integrity": "sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q==", - "dev": true, - "dependencies": { - "@babel/types": "^7.24.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -1097,12 +838,12 @@ } }, "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz", - "integrity": "sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.25.2.tgz", + "integrity": "sha512-+wqVGP+DFmqwFD3EH6TMTfUNeqDehV3E/dl+Sd54eaXqm17tEUNbEIn4sVivVowbvUpOtIGxdo3GoXyDH9N/9g==", "dev": true, "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-annotate-as-pure": "^7.24.7", "regexpu-core": "^5.3.1", "semver": "^6.3.1" }, @@ -1138,125 +879,80 @@ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", - "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.8.tgz", + "integrity": "sha512-LABppdt+Lp/RlBxqrh4qgf1oEH/WxdzQNDJIu5gC/W1GyvPVrOBiItmmM8wan2fm4oYqFuFfkXmlGpLQhPY8CA==", "dev": true, + "dependencies": { + "@babel/traverse": "^7.24.8", + "@babel/types": "^7.24.8" + }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/helper-function-name": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", - "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "node_modules/@babel/helper-module-imports": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", "dev": true, "dependencies": { - "@babel/template": "^7.22.15", - "@babel/types": "^7.23.0" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "node_modules/@babel/helper-module-transforms": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz", + "integrity": "sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==", "dev": true, "dependencies": { - "@babel/types": "^7.22.5" + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", + "@babel/traverse": "^7.25.2" }, "engines": { "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.5.tgz", - "integrity": "sha512-4owRteeihKWKamtqg4JmWSsEZU445xpFRXPEwp44HbgbxdWlUV1b4Agg4lkA806Lil5XM/e+FJyS0vj5T6vmcA==", + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.24.7.tgz", + "integrity": "sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A==", "dev": true, "dependencies": { - "@babel/types": "^7.24.5" + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/helper-module-imports": { - "version": "7.24.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz", - "integrity": "sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.24.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.5.tgz", - "integrity": "sha512-9GxeY8c2d2mdQUP1Dye0ks3VDyIMS98kt/llQ2nUId8IsWqTF0l1LkSX0/uP7l7MCDrzXS009Hyhe2gzTiGW8A==", - "dev": true, - "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-module-imports": "^7.24.3", - "@babel/helper-simple-access": "^7.24.5", - "@babel/helper-split-export-declaration": "^7.24.5", - "@babel/helper-validator-identifier": "^7.24.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-module-transforms/node_modules/@babel/helper-split-export-declaration": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.5.tgz", - "integrity": "sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q==", - "dev": true, - "dependencies": { - "@babel/types": "^7.24.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", - "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.5.tgz", - "integrity": "sha512-xjNLDopRzW2o6ba0gKbkZq5YWEBaK3PCyTOY1K2P/O07LGMhMqlMXPxwN4S5/RhWuCobT8z0jrlKGlYmeR1OhQ==", + "node_modules/@babel/helper-plugin-utils": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", + "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.20.tgz", - "integrity": "sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.0.tgz", + "integrity": "sha512-NhavI2eWEIz/H9dbrG0TuOicDhNexze43i5z7lEqwYm0WEZVTwnPpA0EafUTP7+6/W79HWIP2cTe3Z5NiSTVpw==", "dev": true, "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-wrap-function": "^7.22.20" + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-wrap-function": "^7.25.0", + "@babel/traverse": "^7.25.0" }, "engines": { "node": ">=6.9.0" @@ -1266,14 +962,14 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.24.1.tgz", - "integrity": "sha512-QCR1UqC9BzG5vZl8BMicmZ28RuUBnHhAMddD8yHFHDRH9lLTZ9uUPehX8ctVPT8l0TKblJidqcgUUKGVrePleQ==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.0.tgz", + "integrity": "sha512-q688zIvQVYtZu+i2PsdIu/uWGRpfxzr5WESsfpShfZECkO+d2o+WROWezCi/Q6kJ0tfPa5+pUGUlfx2HhrA3Bg==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-member-expression-to-functions": "^7.23.0", - "@babel/helper-optimise-call-expression": "^7.22.5" + "@babel/helper-member-expression-to-functions": "^7.24.8", + "@babel/helper-optimise-call-expression": "^7.24.7", + "@babel/traverse": "^7.25.0" }, "engines": { "node": ">=6.9.0" @@ -1283,103 +979,104 @@ } }, "node_modules/@babel/helper-simple-access": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.5.tgz", - "integrity": "sha512-uH3Hmf5q5n7n8mz7arjUlDOCbttY/DW4DYhE6FUsjKJ/oYC1kQQUvwEQWxRwUpX9qQKRXeqLwWxrqilMrf32sQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", "dev": true, "dependencies": { - "@babel/types": "^7.24.5" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", - "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.24.7.tgz", + "integrity": "sha512-IO+DLT3LQUElMbpzlatRASEyQtfhSE0+m465v++3jyyXeBTBUjtVZg28/gHeV5mrTJqvEKhKroBGAvhW+qPHiQ==", "dev": true, "dependencies": { - "@babel/types": "^7.22.5" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", - "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", "dev": true, "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", - "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", + "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz", - "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", - "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", + "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-wrap-function": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.24.5.tgz", - "integrity": "sha512-/xxzuNvgRl4/HLNKvnFwdhdgN3cpLxgLROeLDl83Yx0AJ1SGvq1ak0OszTOjDfiB8Vx03eJbeDWh9r+jCCWttw==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.0.tgz", + "integrity": "sha512-s6Q1ebqutSiZnEjaofc/UKDyC4SbzV5n5SrA2Gq8UawLycr3i04f1dX4OzoQVnexm6aOCh37SQNYlJ/8Ku+PMQ==", "dev": true, "dependencies": { - "@babel/helper-function-name": "^7.23.0", - "@babel/template": "^7.24.0", - "@babel/types": "^7.24.5" + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.0", + "@babel/types": "^7.25.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.5.tgz", - "integrity": "sha512-CiQmBMMpMQHwM5m01YnrM6imUG1ebgYJ+fAIW4FZe6m4qHTPaRHti+R8cggAwkdz4oXhtO4/K9JWlh+8hIfR2Q==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.6.tgz", + "integrity": "sha512-Xg0tn4HcfTijTwfDwYlvVCl43V6h4KyVVX2aEm4qdO/PC6L2YvzLHFdmxhoeSA3eslcE6+ZVXHgWwopXYLNq4Q==", "dev": true, "dependencies": { - "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.5", - "@babel/types": "^7.24.5" + "@babel/template": "^7.25.0", + "@babel/types": "^7.25.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.5.tgz", - "integrity": "sha512-8lLmua6AVh/8SLJRRVD6V8p73Hir9w5mJrhE+IPpILG31KKlI9iz5zmBYKcWPS59qSfgP9RaSBQSHHE81WKuEw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.24.5", + "@babel/helper-validator-identifier": "^7.24.7", "chalk": "^2.4.2", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" @@ -1389,10 +1086,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz", - "integrity": "sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.6.tgz", + "integrity": "sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==", "dev": true, + "dependencies": { + "@babel/types": "^7.25.6" + }, "bin": { "parser": "bin/babel-parser.js" }, @@ -1400,13 +1100,44 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.25.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.3.tgz", + "integrity": "sha512-wUrcsxZg6rqBXG05HG1FPYgsP6EvwF4WpBbxIpWIIYnH8wG0gzx3yZY3dtEHas4sTAOGkbTsc9EGPxwff8lRoA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/traverse": "^7.25.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.0.tgz", + "integrity": "sha512-Bm4bH2qsX880b/3ziJ8KD711LT7z4u8CFudmjqle65AZj/HNUFhEf90dqYv6O86buWvSBmeQDjv0Tn2aF/bIBA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.8" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.24.1.tgz", - "integrity": "sha512-y4HqEnkelJIOQGd+3g1bTeKsA5c6qM7eOn7VggGVbBc0y8MLSKHacwcIE2PplNlQSj0PqS9rrXL/nkPVK+kUNg==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.0.tgz", + "integrity": "sha512-lXwdNZtTmeVOOFtwM/WDe7yg1PL8sYhRk/XH0FzbR2HDQ0xC+EnQ/JHeoMYSavtU115tnUk0q9CDyq8si+LMAA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -1416,14 +1147,14 @@ } }, "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.1.tgz", - "integrity": "sha512-Hj791Ii4ci8HqnaKHAlLNs+zaLXb0EzSDhiAWp5VNlyvCNymYfacs64pxTxbH1znW/NcArSmwpmG9IKE/TUVVQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.7.tgz", + "integrity": "sha512-+izXIbke1T33mY4MSNnrqhPXDz01WYhEf3yF5NbnUtkiNnm+XBZJl3kNfoK6NKmYlz/D07+l2GWVK/QfDkNCuQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/plugin-transform-optional-chaining": "^7.24.1" + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1433,13 +1164,13 @@ } }, "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.24.1.tgz", - "integrity": "sha512-m9m/fXsXLiHfwdgydIFnpk+7jlVbnvlK5B2EKiPdLUb6WX654ZaaEWJUjk8TftRbZpK0XibovlLWX4KIZhV6jw==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.0.tgz", + "integrity": "sha512-tggFrk1AIShG/RUQbEwt2Tr/E+ObkfwrPjR6BjbRvsx24+PSjK8zrq0GWPNCjo8qpRx4DuJzlcvWJqlm+0h3kw==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/traverse": "^7.25.0" }, "engines": { "node": ">=6.9.0" @@ -1524,12 +1255,12 @@ } }, "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.1.tgz", - "integrity": "sha512-IuwnI5XnuF189t91XbxmXeCDz3qs6iDRO7GJ++wcfgeXNs/8FmIlKcpDSXNVyuLQxlwvskmI3Ct73wUODkJBlQ==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.25.6.tgz", + "integrity": "sha512-aABl0jHw9bZ2karQ/uUD6XP4u0SG22SJrOHFoL6XB1R7dTovOP4TzTlsxOYC5yQ1pdscVK2JTUnF6QL3ARoAiQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -1539,12 +1270,12 @@ } }, "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.1.tgz", - "integrity": "sha512-zhQTMH0X2nVLnb04tz+s7AMuasX8U0FnpE+nHTOhSOINjWMnopoZTxtIKsd45n4GQ/HIZLyfIpoul8e2m0DnRA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.7.tgz", + "integrity": "sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1696,12 +1427,12 @@ } }, "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.1.tgz", - "integrity": "sha512-ngT/3NkRhsaep9ck9uj2Xhv9+xB1zShY3tM3g6om4xxCELwCDN4g4Aq5dRn48+0hasAql7s2hdBOysCfNpr4fw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.7.tgz", + "integrity": "sha512-Dt9LQs6iEY++gXUwY03DNFat5C2NbO48jj+j/bSAz6b3HgPs39qcPiYt77fDObIcFwj3/C2ICX9YMwGflUoSHQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1711,15 +1442,15 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.9.tgz", - "integrity": "sha512-8Q3veQEDGe14dTYuwagbRtwxQDnytyg1JFu4/HwEMETeofocrB0U0ejBJIXoeG/t2oXZ8kzCyI0ZZfbT80VFNQ==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.0.tgz", + "integrity": "sha512-uaIi2FdqzjpAMvVqvB51S42oC2JEVgh0LDsGfZVDysWE8LrJtQC2jvKmOqEYThKyB7bDEb7BP1GYWDm7tABA0Q==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.20", - "@babel/plugin-syntax-async-generators": "^7.8.4" + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/helper-remap-async-to-generator": "^7.25.0", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/traverse": "^7.25.0" }, "engines": { "node": ">=6.9.0" @@ -1729,14 +1460,14 @@ } }, "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.23.3.tgz", - "integrity": "sha512-A7LFsKi4U4fomjqXJlZg/u0ft/n8/7n7lpffUP/ZULx/DtV9SGlNKZolHH6PE8Xl1ngCc0M11OaeZptXVkfKSw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.24.7.tgz", + "integrity": "sha512-SQY01PcJfmQ+4Ash7NE+rpbLFbmqA2GPIgqzxfFTL4t1FKRq4zTms/7htKpoCUI9OcFYgzqfmCdH53s6/jn5fA==", "dev": true, "dependencies": { - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.20" + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-remap-async-to-generator": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1746,12 +1477,12 @@ } }, "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.1.tgz", - "integrity": "sha512-TWWC18OShZutrv9C6mye1xwtam+uNi2bnTOCBUd5sZxyHOiWbU6ztSROofIMrK84uweEZC219POICK/sTYwfgg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.7.tgz", + "integrity": "sha512-yO7RAz6EsVQDaBH18IDJcMB1HnrUn2FJ/Jslc/WtPPWcjhpUJXU/rjbwmluzp7v/ZzWcEhTMXELnnsz8djWDwQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1761,12 +1492,12 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.5.tgz", - "integrity": "sha512-sMfBc3OxghjC95BkYrYocHL3NaOplrcaunblzwXhGmlPwpmfsxr4vK+mBBt49r+S240vahmv+kUxkeKgs+haCw==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.0.tgz", + "integrity": "sha512-yBQjYoOjXlFv9nlXb3f1casSHOZkWr29NX+zChVanLg5Nc157CrbEX9D7hxxtTpuFy7Q0YzmmWfJxzvps4kXrQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.5" + "@babel/helper-plugin-utils": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -1776,13 +1507,13 @@ } }, "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.1.tgz", - "integrity": "sha512-OMLCXi0NqvJfORTaPQBwqLXHhb93wkBKZ4aNwMl6WtehO7ar+cmp+89iPEQPqxAnxsOKTaMcs3POz3rKayJ72g==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.4.tgz", + "integrity": "sha512-nZeZHyCWPfjkdU5pA/uHiTaDAFUEqkpzf1YoQT2NeSynCGYq9rxfyI3XpQbfx/a0hSnFH6TGlEXvae5Vi7GD8g==", "dev": true, "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.24.1", - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-create-class-features-plugin": "^7.25.4", + "@babel/helper-plugin-utils": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -1792,13 +1523,13 @@ } }, "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.4.tgz", - "integrity": "sha512-B8q7Pz870Hz/q9UgP8InNpY01CSLDSCyqX7zcRuv3FcPl87A2G17lASroHWaCtbdIcbYzOZ7kWmXFKbijMSmFg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.7.tgz", + "integrity": "sha512-HMXK3WbBPpZQufbMG4B46A90PkuuhN9vBCb5T8+VAHqvAqvcLi+2cKoukcpmUYkszLhScU3l1iudhrks3DggRQ==", "dev": true, "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.24.4", - "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", "@babel/plugin-syntax-class-static-block": "^7.14.5" }, "engines": { @@ -1809,18 +1540,16 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.5.tgz", - "integrity": "sha512-gWkLP25DFj2dwe9Ck8uwMOpko4YsqyfZJrOmqqcegeDYEbp7rmn4U6UQZNj08UF6MaX39XenSpKRCvpDRBtZ7Q==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-plugin-utils": "^7.24.5", - "@babel/helper-replace-supers": "^7.24.1", - "@babel/helper-split-export-declaration": "^7.24.5", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.4.tgz", + "integrity": "sha512-oexUfaQle2pF/b6E0dwsxQtAol9TLSO88kQvym6HHBWFliV2lGdrPieX+WgMRLSJDVzdYywk7jXbLPuO2KLTLg==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-compilation-targets": "^7.25.2", + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/helper-replace-supers": "^7.25.0", + "@babel/traverse": "^7.25.4", "globals": "^11.1.0" }, "engines": { @@ -1830,26 +1559,29 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-classes/node_modules/@babel/helper-split-export-declaration": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.5.tgz", - "integrity": "sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q==", + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.7.tgz", + "integrity": "sha512-25cS7v+707Gu6Ds2oY6tCkUwsJ9YIDbggd9+cu9jzzDgiNq7hR/8dkzxWfKWnTic26vsI3EsCXNd4iEB6e8esQ==", "dev": true, "dependencies": { - "@babel/types": "^7.24.5" + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/template": "^7.24.7" }, "engines": { "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.1.tgz", - "integrity": "sha512-5pJGVIUfJpOS+pAqBQd+QMaTD2vCL/HcePooON6pDpHgRp4gNRmzyHTPIkXntwKsq3ayUFVfJaIKPw2pOkOcTw==", + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.8.tgz", + "integrity": "sha512-36e87mfY8TnRxc7yc6M9g9gOB7rKgSahqkIKwLpz4Ppk2+zC2Cy1is0uwtuSG6AE4zlTOUa+7JGz9jCJGLqQFQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/template": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -1858,13 +1590,14 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.5.tgz", - "integrity": "sha512-SZuuLyfxvsm+Ah57I/i1HVjveBENYK9ue8MJ7qkc7ndoNjqquJiElzA7f5yaAXjyW2hKojosOTAQQRX50bPSVg==", + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.7.tgz", + "integrity": "sha512-ZOA3W+1RRTSWvyqcMJDLqbchh7U4NRGqwRfFSVbOLS/ePIP4vHB5e8T8eXcuqyN1QkgKyj5wuW0lcS85v4CrSw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.5" + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1873,14 +1606,13 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.1.tgz", - "integrity": "sha512-p7uUxgSoZwZ2lPNMzUkqCts3xlp8n+o05ikjy7gbtFJSt9gdU88jAmtfmOxHM14noQXBxfgzf2yRWECiNVhTCw==", + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.7.tgz", + "integrity": "sha512-JdYfXyCRihAe46jUIliuL2/s0x0wObgwwiGxw/UbgJBr20gQBThrokO4nYKgWkD7uBaqM7+9x5TU7NkExZJyzw==", "dev": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1889,28 +1621,29 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.1.tgz", - "integrity": "sha512-msyzuUnvsjsaSaocV6L7ErfNsa5nDWL1XKNnDePLgmz+WdU4w/J8+AxBMrWfi9m4IxfL5sZQKUPQKDQeeAT6lA==", + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.0.tgz", + "integrity": "sha512-YLpb4LlYSc3sCUa35un84poXoraOiQucUTTu8X1j18JV+gNa8E0nyUf/CjZ171IRGr4jEguF+vzJU66QZhn29g==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-create-regexp-features-plugin": "^7.25.0", + "@babel/helper-plugin-utils": "^7.24.8" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.0.0" } }, "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.24.1.tgz", - "integrity": "sha512-av2gdSTyXcJVdI+8aFZsCAtR29xJt0S5tas+Ef8NvBNmD1a+N/3ecMLeMBgfcK+xzsjdLDT6oHt+DFPyeqUbDA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.24.7.tgz", + "integrity": "sha512-sc3X26PhZQDb3JhORmakcbvkeInvxz+A8oda99lj7J60QRuPZvNAk9wQlTBS1ZynelDrDmTU4pw1tyc5d5ZMUg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-plugin-utils": "^7.24.7", "@babel/plugin-syntax-dynamic-import": "^7.8.3" }, "engines": { @@ -1921,13 +1654,13 @@ } }, "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.1.tgz", - "integrity": "sha512-U1yX13dVBSwS23DEAqU+Z/PkwE9/m7QQy8Y9/+Tdb8UWYaGNDYwTLi19wqIAiROr8sXVum9A/rtiH5H0boUcTw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.7.tgz", + "integrity": "sha512-Rqe/vSc9OYgDajNIK35u7ot+KeCoetqQYFXM4Epf7M7ez3lWlOjrDjrwMei6caCVhfdw+mIKD4cgdGNy5JQotQ==", "dev": true, "dependencies": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.15", - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1937,12 +1670,12 @@ } }, "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.1.tgz", - "integrity": "sha512-Ft38m/KFOyzKw2UaJFkWG9QnHPG/Q/2SkOrRk4pNBPg5IPZ+dOxcmkK5IyuBcxiNPyyYowPGUReyBvrvZs7IlQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.7.tgz", + "integrity": "sha512-v0K9uNYsPL3oXZ/7F9NNIbAj2jv1whUEtyA6aujhekLs56R++JDQuzRcP2/z4WX5Vg/c5lE9uWZA0/iUoFhLTA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-plugin-utils": "^7.24.7", "@babel/plugin-syntax-export-namespace-from": "^7.8.3" }, "engines": { @@ -1953,13 +1686,13 @@ } }, "node_modules/@babel/plugin-transform-for-of": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.1.tgz", - "integrity": "sha512-OxBdcnF04bpdQdR3i4giHZNZQn7cm8RQKcSwA17wAAqEELo1ZOwp5FFgeptWUQXFyT9kwHo10aqqauYkRZPCAg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.7.tgz", + "integrity": "sha512-wo9ogrDG1ITTTBsy46oGiN1dS9A7MROBTcYsfS8DtsImMkHk9JXJ3EWQM6X2SUw4x80uGPlwj0o00Uoc6nEE3g==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1969,14 +1702,14 @@ } }, "node_modules/@babel/plugin-transform-function-name": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.24.1.tgz", - "integrity": "sha512-BXmDZpPlh7jwicKArQASrj8n22/w6iymRnvHYYd2zO30DbE277JO20/7yXJT3QxDPtiQiOxQBbZH4TpivNXIxA==", + "version": "7.25.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.1.tgz", + "integrity": "sha512-TVVJVdW9RKMNgJJlLtHsKDTydjZAbwIsn6ySBPQaEAUU5+gVvlJt/9nRmqVbsV/IBanRjzWoaAQKLoamWVOUuA==", "dev": true, "dependencies": { - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-compilation-targets": "^7.24.8", + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/traverse": "^7.25.1" }, "engines": { "node": ">=6.9.0" @@ -1986,12 +1719,12 @@ } }, "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.1.tgz", - "integrity": "sha512-U7RMFmRvoasscrIFy5xA4gIp8iWnWubnKkKuUGJjsuOH7GfbMkB+XZzeslx2kLdEGdOJDamEmCqOks6e8nv8DQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.7.tgz", + "integrity": "sha512-2yFnBGDvRuxAaE/f0vfBKvtnvvqU8tGpMHqMNpTN2oWMKIR3NqFkjaAgGwawhqK/pIN2T3XdjGPdaG0vDhOBGw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-plugin-utils": "^7.24.7", "@babel/plugin-syntax-json-strings": "^7.8.3" }, "engines": { @@ -2002,12 +1735,12 @@ } }, "node_modules/@babel/plugin-transform-literals": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.24.1.tgz", - "integrity": "sha512-zn9pwz8U7nCqOYIiBaOxoQOtYmMODXTJnkxG4AtX8fPmnCRYWBOHD0qcpwS9e2VDSp1zNJYpdnFMIKb8jmwu6g==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.2.tgz", + "integrity": "sha512-HQI+HcTbm9ur3Z2DkO+jgESMAMcYLuN/A7NRw9juzxAezN9AvqvUTnpKP/9kkYANz6u7dFlAyOu44ejuGySlfw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -2017,12 +1750,12 @@ } }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.1.tgz", - "integrity": "sha512-OhN6J4Bpz+hIBqItTeWJujDOfNP+unqv/NJgyhlpSqgBTPm37KkMmZV6SYcOj+pnDbdcl1qRGV/ZiIjX9Iy34w==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.7.tgz", + "integrity": "sha512-4D2tpwlQ1odXmTEIFWy9ELJcZHqrStlzK/dAOWYyxX3zT0iXQB6banjgeOJQXzEc4S0E0a5A+hahxPaEFYftsw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-plugin-utils": "^7.24.7", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" }, "engines": { @@ -2033,12 +1766,12 @@ } }, "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.1.tgz", - "integrity": "sha512-4ojai0KysTWXzHseJKa1XPNXKRbuUrhkOPY4rEGeR+7ChlJVKxFa3H3Bz+7tWaGKgJAXUWKOGmltN+u9B3+CVg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.7.tgz", + "integrity": "sha512-T/hRC1uqrzXMKLQ6UCwMT85S3EvqaBXDGf0FaMf4446Qx9vKwlghvee0+uuZcDUCZU5RuNi4781UQ7R308zzBw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -2048,13 +1781,13 @@ } }, "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.1.tgz", - "integrity": "sha512-lAxNHi4HVtjnHd5Rxg3D5t99Xm6H7b04hUS7EHIXcUl2EV4yl1gWdqZrNzXnSrHveL9qMdbODlLF55mvgjAfaQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.7.tgz", + "integrity": "sha512-9+pB1qxV3vs/8Hdmz/CulFB8w2tuu6EB94JZFsjdqxQokwGa9Unap7Bo2gGBGIvPmDIVvQrom7r5m/TCDMURhg==", "dev": true, "dependencies": { - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -2064,14 +1797,14 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.1.tgz", - "integrity": "sha512-szog8fFTUxBfw0b98gEWPaEqF42ZUD/T3bkynW/wtgx2p/XCP55WEsb+VosKceRSd6njipdZvNogqdtI4Q0chw==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.8.tgz", + "integrity": "sha512-WHsk9H8XxRs3JXKWFiqtQebdh9b/pTk4EgueygFzYlTKAg0Ud985mSevdNjdXdFBATSKVJGQXP1tv6aGbssLKA==", "dev": true, "dependencies": { - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/helper-simple-access": "^7.22.5" + "@babel/helper-module-transforms": "^7.24.8", + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/helper-simple-access": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -2081,15 +1814,15 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.24.1.tgz", - "integrity": "sha512-mqQ3Zh9vFO1Tpmlt8QPnbwGHzNz3lpNEMxQb1kAemn/erstyqw1r9KeOlOfo3y6xAnFEcOv2tSyrXfmMk+/YZA==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.0.tgz", + "integrity": "sha512-YPJfjQPDXxyQWg/0+jHKj1llnY5f/R6a0p/vP4lPymxLu7Lvl4k2WMitqi08yxwQcCVUUdG9LCUj4TNEgAp3Jw==", "dev": true, "dependencies": { - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/helper-validator-identifier": "^7.22.20" + "@babel/helper-module-transforms": "^7.25.0", + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/helper-validator-identifier": "^7.24.7", + "@babel/traverse": "^7.25.0" }, "engines": { "node": ">=6.9.0" @@ -2099,13 +1832,13 @@ } }, "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.1.tgz", - "integrity": "sha512-tuA3lpPj+5ITfcCluy6nWonSL7RvaG0AOTeAuvXqEKS34lnLzXpDb0dcP6K8jD0zWZFNDVly90AGFJPnm4fOYg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.7.tgz", + "integrity": "sha512-3aytQvqJ/h9z4g8AsKPLvD4Zqi2qT+L3j7XoFFu1XBlZWEl2/1kWnhmAbxpLgPrHSY0M6UA02jyTiwUVtiKR6A==", "dev": true, "dependencies": { - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -2115,13 +1848,13 @@ } }, "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz", - "integrity": "sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.24.7.tgz", + "integrity": "sha512-/jr7h/EWeJtk1U/uz2jlsCioHkZk1JJZVcc8oQsJ1dUlaJD83f4/6Zeh2aHt9BIFokHIsSeDfhUmju0+1GPd6g==", "dev": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -2131,12 +1864,12 @@ } }, "node_modules/@babel/plugin-transform-new-target": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.1.tgz", - "integrity": "sha512-/rurytBM34hYy0HKZQyA0nHbQgQNFm4Q/BOc9Hflxi2X3twRof7NaE5W46j4kQitm7SvACVRXsa6N/tSZxvPug==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.7.tgz", + "integrity": "sha512-RNKwfRIXg4Ls/8mMTza5oPF5RkOW8Wy/WgMAp1/F1yZ8mMbtwXW+HDoJiOsagWrAhI5f57Vncrmr9XeT4CVapA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -2146,12 +1879,12 @@ } }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.24.1.tgz", - "integrity": "sha512-iQ+caew8wRrhCikO5DrUYx0mrmdhkaELgFa+7baMcVuhxIkN7oxt06CZ51D65ugIb1UWRQ8oQe+HXAVM6qHFjw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.24.7.tgz", + "integrity": "sha512-Ts7xQVk1OEocqzm8rHMXHlxvsfZ0cEF2yomUqpKENHWMF4zKk175Y4q8H5knJes6PgYad50uuRmt3UJuhBw8pQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-plugin-utils": "^7.24.7", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" }, "engines": { @@ -2162,12 +1895,12 @@ } }, "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.1.tgz", - "integrity": "sha512-7GAsGlK4cNL2OExJH1DzmDeKnRv/LXq0eLUSvudrehVA5Rgg4bIrqEUW29FbKMBRT0ztSqisv7kjP+XIC4ZMNw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.7.tgz", + "integrity": "sha512-e6q1TiVUzvH9KRvicuxdBTUj4AdKSRwzIyFFnfnezpCfP2/7Qmbb8qbU2j7GODbl4JMkblitCQjKYUaX/qkkwA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-plugin-utils": "^7.24.7", "@babel/plugin-syntax-numeric-separator": "^7.10.4" }, "engines": { @@ -2178,15 +1911,15 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.5.tgz", - "integrity": "sha512-7EauQHszLGM3ay7a161tTQH7fj+3vVM/gThlz5HpFtnygTxjrlvoeq7MPVA1Vy9Q555OB8SnAOsMkLShNkkrHA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.7.tgz", + "integrity": "sha512-4QrHAr0aXQCEFni2q4DqKLD31n2DL+RxcwnNjDFkSG0eNQ/xCavnRkfCUjsyqGC2OviNJvZOF/mQqZBw7i2C5Q==", "dev": true, "dependencies": { - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-plugin-utils": "^7.24.5", + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.24.5" + "@babel/plugin-transform-parameters": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -2196,13 +1929,13 @@ } }, "node_modules/@babel/plugin-transform-object-super": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.1.tgz", - "integrity": "sha512-oKJqR3TeI5hSLRxudMjFQ9re9fBVUU0GICqM3J1mi8MqlhVr6hC/ZN4ttAyMuQR6EZZIY6h/exe5swqGNNIkWQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.7.tgz", + "integrity": "sha512-A/vVLwN6lBrMFmMDmPPz0jnE6ZGx7Jq7d6sT/Ev4H65RER6pZ+kczlf1DthF5N0qaPHBsI7UXiE8Zy66nmAovg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/helper-replace-supers": "^7.24.1" + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-replace-supers": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -2212,12 +1945,12 @@ } }, "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.1.tgz", - "integrity": "sha512-oBTH7oURV4Y+3EUrf6cWn1OHio3qG/PVwO5J03iSJmBg6m2EhKjkAu/xuaXaYwWW9miYtvbWv4LNf0AmR43LUA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.7.tgz", + "integrity": "sha512-uLEndKqP5BfBbC/5jTwPxLh9kqPWWgzN/f8w6UwAIirAEqiIVJWWY312X72Eub09g5KF9+Zn7+hT7sDxmhRuKA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-plugin-utils": "^7.24.7", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" }, "engines": { @@ -2228,13 +1961,13 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.5.tgz", - "integrity": "sha512-xWCkmwKT+ihmA6l7SSTpk8e4qQl/274iNbSKRRS8mpqFR32ksy36+a+LWY8OXCCEefF8WFlnOHVsaDI2231wBg==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.8.tgz", + "integrity": "sha512-5cTOLSMs9eypEy8JUVvIKOu6NgvbJMnpG62VpIHrTmROdQ+L5mDAaI40g25k5vXti55JWNX5jCkq3HZxXBQANw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", "@babel/plugin-syntax-optional-chaining": "^7.8.3" }, "engines": { @@ -2245,12 +1978,12 @@ } }, "node_modules/@babel/plugin-transform-parameters": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.5.tgz", - "integrity": "sha512-9Co00MqZ2aoky+4j2jhofErthm6QVLKbpQrvz20c3CH9KQCLHyNB+t2ya4/UrRpQGR+Wrwjg9foopoeSdnHOkA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.7.tgz", + "integrity": "sha512-yGWW5Rr+sQOhK0Ot8hjDJuxU3XLRQGflvT4lhlSY0DFvdb3TwKaY26CJzHtYllU0vT9j58hc37ndFPsqT1SrzA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.5" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -2260,13 +1993,13 @@ } }, "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.24.1.tgz", - "integrity": "sha512-tGvisebwBO5em4PaYNqt4fkw56K2VALsAbAakY0FjTYqJp7gfdrgr7YX76Or8/cpik0W6+tj3rZ0uHU9Oil4tw==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.4.tgz", + "integrity": "sha512-ao8BG7E2b/URaUQGqN3Tlsg+M3KlHY6rJ1O1gXAEUnZoyNQnvKyH87Kfg+FoxSeyWUB8ISZZsC91C44ZuBFytw==", "dev": true, "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.24.1", - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-create-class-features-plugin": "^7.25.4", + "@babel/helper-plugin-utils": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -2276,14 +2009,14 @@ } }, "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.5.tgz", - "integrity": "sha512-JM4MHZqnWR04jPMujQDTBVRnqxpLLpx2tkn7iPn+Hmsc0Gnb79yvRWOkvqFOx3Z7P7VxiRIR22c4eGSNj87OBQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.7.tgz", + "integrity": "sha512-9z76mxwnwFxMyxZWEgdgECQglF2Q7cFLm0kMf8pGwt+GSJsY0cONKj/UuO4bOH0w/uAel3ekS4ra5CEAyJRmDA==", "dev": true, "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-create-class-features-plugin": "^7.24.5", - "@babel/helper-plugin-utils": "^7.24.5", + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", "@babel/plugin-syntax-private-property-in-object": "^7.14.5" }, "engines": { @@ -2294,12 +2027,12 @@ } }, "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.1.tgz", - "integrity": "sha512-LetvD7CrHmEx0G442gOomRr66d7q8HzzGGr4PMHGr+5YIm6++Yke+jxj246rpvsbyhJwCLxcTn6zW1P1BSenqA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.7.tgz", + "integrity": "sha512-EMi4MLQSHfd2nrCqQEWxFdha2gBCqU4ZcCng4WBGZ5CJL4bBRW0ptdqqDdeirGZcpALazVVNJqRmsO8/+oNCBA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -2309,12 +2042,12 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.1.tgz", - "integrity": "sha512-sJwZBCzIBE4t+5Q4IGLaaun5ExVMRY0lYwos/jNecjMrVCygCdph3IKv0tkP5Fc87e/1+bebAmEAGBfnRD+cnw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.7.tgz", + "integrity": "sha512-lq3fvXPdimDrlg6LWBoqj+r/DEWgONuwjuOuQCSYgRroXDH/IdM1C0IZf59fL5cHLpjEH/O6opIRBbqv7ELnuA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-plugin-utils": "^7.24.7", "regenerator-transform": "^0.15.2" }, "engines": { @@ -2325,12 +2058,12 @@ } }, "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.1.tgz", - "integrity": "sha512-JAclqStUfIwKN15HrsQADFgeZt+wexNQ0uLhuqvqAUFoqPMjEcFCYZBhq0LUdz6dZK/mD+rErhW71fbx8RYElg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.7.tgz", + "integrity": "sha512-0DUq0pHcPKbjFZCfTss/pGkYMfy3vFWydkUBd9r0GHpIyfs2eCDENvqadMycRS9wZCXR41wucAfJHJmwA0UmoQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -2340,16 +2073,16 @@ } }, "node_modules/@babel/plugin-transform-runtime": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.24.0.tgz", - "integrity": "sha512-zc0GA5IitLKJrSfXlXmp8KDqLrnGECK7YRfQBmEKg1NmBOQ7e+KuclBEKJgzifQeUYLdNiAw4B4bjyvzWVLiSA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.24.7.tgz", + "integrity": "sha512-YqXjrk4C+a1kZjewqt+Mmu2UuV1s07y8kqcUf4qYLnoqemhR4gRQikhdAhSVJioMjVTu6Mo6pAbaypEA3jY6fw==", "dev": true, "dependencies": { - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-plugin-utils": "^7.24.0", - "babel-plugin-polyfill-corejs2": "^0.4.8", - "babel-plugin-polyfill-corejs3": "^0.9.0", - "babel-plugin-polyfill-regenerator": "^0.5.5", + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.10.1", + "babel-plugin-polyfill-regenerator": "^0.6.1", "semver": "^6.3.1" }, "engines": { @@ -2369,12 +2102,12 @@ } }, "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.1.tgz", - "integrity": "sha512-LyjVB1nsJ6gTTUKRjRWx9C1s9hE7dLfP/knKdrfeH9UPtAGjYGgxIbFfx7xyLIEWs7Xe1Gnf8EWiUqfjLhInZA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.7.tgz", + "integrity": "sha512-KsDsevZMDsigzbA09+vacnLpmPH4aWjcZjXdyFKGzpplxhbeB4wYtury3vglQkg6KM/xEPKt73eCjPPf1PgXBA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -2384,13 +2117,13 @@ } }, "node_modules/@babel/plugin-transform-spread": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.1.tgz", - "integrity": "sha512-KjmcIM+fxgY+KxPVbjelJC6hrH1CgtPmTvdXAfn3/a9CnWGSTY7nH4zm5+cjmWJybdcPSsD0++QssDsjcpe47g==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.7.tgz", + "integrity": "sha512-x96oO0I09dgMDxJaANcRyD4ellXFLLiWhuwDxKZX5g2rWP1bTPkBSwCYv96VDXVT1bD9aPj8tppr5ITIh8hBng==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -2400,12 +2133,12 @@ } }, "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.1.tgz", - "integrity": "sha512-9v0f1bRXgPVcPrngOQvLXeGNNVLc8UjMVfebo9ka0WF3/7+aVUHmaJVT3sa0XCzEFioPfPHZiOcYG9qOsH63cw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.7.tgz", + "integrity": "sha512-kHPSIJc9v24zEml5geKg9Mjx5ULpfncj0wRpYtxbvKyTtHCYDkVE3aHQ03FrpEo4gEe2vrJJS1Y9CJTaThA52g==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -2415,12 +2148,12 @@ } }, "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.1.tgz", - "integrity": "sha512-WRkhROsNzriarqECASCNu/nojeXCDTE/F2HmRgOzi7NGvyfYGq1NEjKBK3ckLfRgGc6/lPAqP0vDOSw3YtG34g==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.7.tgz", + "integrity": "sha512-AfDTQmClklHCOLxtGoP7HkeMw56k1/bTQjwsfhL6pppo/M4TOBSq+jjBUBLmV/4oeFg4GWMavIl44ZeCtmmZTw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -2430,12 +2163,12 @@ } }, "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.5.tgz", - "integrity": "sha512-UTGnhYVZtTAjdwOTzT+sCyXmTn8AhaxOS/MjG9REclZ6ULHWF9KoCZur0HSGU7hk8PdBFKKbYe6+gqdXWz84Jg==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.8.tgz", + "integrity": "sha512-adNTUpDCVnmAE58VEqKlAA6ZBlNkMnWD0ZcW76lyNFN3MJniyGFZfNwERVk8Ap56MCnXztmDr19T4mPTztcuaw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.5" + "@babel/helper-plugin-utils": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -2445,12 +2178,12 @@ } }, "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.1.tgz", - "integrity": "sha512-RlkVIcWT4TLI96zM660S877E7beKlQw7Ig+wqkKBiWfj0zH5Q4h50q6er4wzZKRNSYpfo6ILJ+hrJAGSX2qcNw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.7.tgz", + "integrity": "sha512-U3ap1gm5+4edc2Q/P+9VrBNhGkfnf+8ZqppY71Bo/pzZmXhhLdqgaUl6cuB07O1+AQJtCLfaOmswiNbSQ9ivhw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -2460,13 +2193,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.1.tgz", - "integrity": "sha512-Ss4VvlfYV5huWApFsF8/Sq0oXnGO+jB+rijFEFugTd3cwSObUSnUi88djgR5528Csl0uKlrI331kRqe56Ov2Ng==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.7.tgz", + "integrity": "sha512-uH2O4OV5M9FZYQrwc7NdVmMxQJOCCzFeYudlZSzUAHRFeOujQefa92E74TQDVskNHCzOXoigEuoyzHDhaEaK5w==", "dev": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -2476,13 +2209,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.1.tgz", - "integrity": "sha512-2A/94wgZgxfTsiLaQ2E36XAOdcZmGAaEEgVmxQWwZXWkGhvoHbaqXcKnU8zny4ycpu3vNqg0L/PcCiYtHtA13g==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.7.tgz", + "integrity": "sha512-hlQ96MBZSAXUq7ltkjtu3FJCCSMx/j629ns3hA3pXnBXjanNP0LHi+JpPeA81zaWgVK1VGH95Xuy7u0RyQ8kMg==", "dev": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -2492,13 +2225,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.24.1.tgz", - "integrity": "sha512-fqj4WuzzS+ukpgerpAoOnMfQXwUHFxXUZUE84oL2Kao2N8uSlvcpnAidKASgsNgzZHBsHWvcm8s9FPWUhAb8fA==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.4.tgz", + "integrity": "sha512-qesBxiWkgN1Q+31xUE9RcMk79eOXXDCv6tfyGMRSs4RGlioSg2WVyQAm07k726cSE56pa+Kb0y9epX2qaXzTvA==", "dev": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-create-regexp-features-plugin": "^7.25.2", + "@babel/helper-plugin-utils": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -2508,26 +2241,28 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.24.0.tgz", - "integrity": "sha512-ZxPEzV9IgvGn73iK0E6VB9/95Nd7aMFpbE0l8KQFDG70cOV9IxRP7Y2FUPmlK0v6ImlLqYX50iuZ3ZTVhOF2lA==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.23.5", - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/helper-validator-option": "^7.23.5", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.23.3", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.23.3", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.23.7", + "version": "7.25.3", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.25.3.tgz", + "integrity": "sha512-QsYW7UeAaXvLPX9tdVliMJE7MD7M6MLYVTovRTIwhoYQVFHR1rM4wO8wqAezYi3/BpSD+NzVCZ69R6smWiIi8g==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.25.2", + "@babel/helper-compilation-targets": "^7.25.2", + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/helper-validator-option": "^7.24.8", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.25.3", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.25.0", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.25.0", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.24.7", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.25.0", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-class-properties": "^7.12.13", "@babel/plugin-syntax-class-static-block": "^7.14.5", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-import-assertions": "^7.23.3", - "@babel/plugin-syntax-import-attributes": "^7.23.3", + "@babel/plugin-syntax-import-assertions": "^7.24.7", + "@babel/plugin-syntax-import-attributes": "^7.24.7", "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.3", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", @@ -2539,59 +2274,60 @@ "@babel/plugin-syntax-private-property-in-object": "^7.14.5", "@babel/plugin-syntax-top-level-await": "^7.14.5", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.23.3", - "@babel/plugin-transform-async-generator-functions": "^7.23.9", - "@babel/plugin-transform-async-to-generator": "^7.23.3", - "@babel/plugin-transform-block-scoped-functions": "^7.23.3", - "@babel/plugin-transform-block-scoping": "^7.23.4", - "@babel/plugin-transform-class-properties": "^7.23.3", - "@babel/plugin-transform-class-static-block": "^7.23.4", - "@babel/plugin-transform-classes": "^7.23.8", - "@babel/plugin-transform-computed-properties": "^7.23.3", - "@babel/plugin-transform-destructuring": "^7.23.3", - "@babel/plugin-transform-dotall-regex": "^7.23.3", - "@babel/plugin-transform-duplicate-keys": "^7.23.3", - "@babel/plugin-transform-dynamic-import": "^7.23.4", - "@babel/plugin-transform-exponentiation-operator": "^7.23.3", - "@babel/plugin-transform-export-namespace-from": "^7.23.4", - "@babel/plugin-transform-for-of": "^7.23.6", - "@babel/plugin-transform-function-name": "^7.23.3", - "@babel/plugin-transform-json-strings": "^7.23.4", - "@babel/plugin-transform-literals": "^7.23.3", - "@babel/plugin-transform-logical-assignment-operators": "^7.23.4", - "@babel/plugin-transform-member-expression-literals": "^7.23.3", - "@babel/plugin-transform-modules-amd": "^7.23.3", - "@babel/plugin-transform-modules-commonjs": "^7.23.3", - "@babel/plugin-transform-modules-systemjs": "^7.23.9", - "@babel/plugin-transform-modules-umd": "^7.23.3", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", - "@babel/plugin-transform-new-target": "^7.23.3", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.23.4", - "@babel/plugin-transform-numeric-separator": "^7.23.4", - "@babel/plugin-transform-object-rest-spread": "^7.24.0", - "@babel/plugin-transform-object-super": "^7.23.3", - "@babel/plugin-transform-optional-catch-binding": "^7.23.4", - "@babel/plugin-transform-optional-chaining": "^7.23.4", - "@babel/plugin-transform-parameters": "^7.23.3", - "@babel/plugin-transform-private-methods": "^7.23.3", - "@babel/plugin-transform-private-property-in-object": "^7.23.4", - "@babel/plugin-transform-property-literals": "^7.23.3", - "@babel/plugin-transform-regenerator": "^7.23.3", - "@babel/plugin-transform-reserved-words": "^7.23.3", - "@babel/plugin-transform-shorthand-properties": "^7.23.3", - "@babel/plugin-transform-spread": "^7.23.3", - "@babel/plugin-transform-sticky-regex": "^7.23.3", - "@babel/plugin-transform-template-literals": "^7.23.3", - "@babel/plugin-transform-typeof-symbol": "^7.23.3", - "@babel/plugin-transform-unicode-escapes": "^7.23.3", - "@babel/plugin-transform-unicode-property-regex": "^7.23.3", - "@babel/plugin-transform-unicode-regex": "^7.23.3", - "@babel/plugin-transform-unicode-sets-regex": "^7.23.3", + "@babel/plugin-transform-arrow-functions": "^7.24.7", + "@babel/plugin-transform-async-generator-functions": "^7.25.0", + "@babel/plugin-transform-async-to-generator": "^7.24.7", + "@babel/plugin-transform-block-scoped-functions": "^7.24.7", + "@babel/plugin-transform-block-scoping": "^7.25.0", + "@babel/plugin-transform-class-properties": "^7.24.7", + "@babel/plugin-transform-class-static-block": "^7.24.7", + "@babel/plugin-transform-classes": "^7.25.0", + "@babel/plugin-transform-computed-properties": "^7.24.7", + "@babel/plugin-transform-destructuring": "^7.24.8", + "@babel/plugin-transform-dotall-regex": "^7.24.7", + "@babel/plugin-transform-duplicate-keys": "^7.24.7", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.25.0", + "@babel/plugin-transform-dynamic-import": "^7.24.7", + "@babel/plugin-transform-exponentiation-operator": "^7.24.7", + "@babel/plugin-transform-export-namespace-from": "^7.24.7", + "@babel/plugin-transform-for-of": "^7.24.7", + "@babel/plugin-transform-function-name": "^7.25.1", + "@babel/plugin-transform-json-strings": "^7.24.7", + "@babel/plugin-transform-literals": "^7.25.2", + "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", + "@babel/plugin-transform-member-expression-literals": "^7.24.7", + "@babel/plugin-transform-modules-amd": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.8", + "@babel/plugin-transform-modules-systemjs": "^7.25.0", + "@babel/plugin-transform-modules-umd": "^7.24.7", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", + "@babel/plugin-transform-new-target": "^7.24.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", + "@babel/plugin-transform-numeric-separator": "^7.24.7", + "@babel/plugin-transform-object-rest-spread": "^7.24.7", + "@babel/plugin-transform-object-super": "^7.24.7", + "@babel/plugin-transform-optional-catch-binding": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.8", + "@babel/plugin-transform-parameters": "^7.24.7", + "@babel/plugin-transform-private-methods": "^7.24.7", + "@babel/plugin-transform-private-property-in-object": "^7.24.7", + "@babel/plugin-transform-property-literals": "^7.24.7", + "@babel/plugin-transform-regenerator": "^7.24.7", + "@babel/plugin-transform-reserved-words": "^7.24.7", + "@babel/plugin-transform-shorthand-properties": "^7.24.7", + "@babel/plugin-transform-spread": "^7.24.7", + "@babel/plugin-transform-sticky-regex": "^7.24.7", + "@babel/plugin-transform-template-literals": "^7.24.7", + "@babel/plugin-transform-typeof-symbol": "^7.24.8", + "@babel/plugin-transform-unicode-escapes": "^7.24.7", + "@babel/plugin-transform-unicode-property-regex": "^7.24.7", + "@babel/plugin-transform-unicode-regex": "^7.24.7", + "@babel/plugin-transform-unicode-sets-regex": "^7.24.7", "@babel/preset-modules": "0.1.6-no-external-plugins", - "babel-plugin-polyfill-corejs2": "^0.4.8", - "babel-plugin-polyfill-corejs3": "^0.9.0", - "babel-plugin-polyfill-regenerator": "^0.5.5", - "core-js-compat": "^3.31.0", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.10.4", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "core-js-compat": "^3.37.1", "semver": "^6.3.1" }, "engines": { @@ -2631,9 +2367,9 @@ "dev": true }, "node_modules/@babel/runtime": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.0.tgz", - "integrity": "sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.0.tgz", + "integrity": "sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==", "dev": true, "dependencies": { "regenerator-runtime": "^0.14.0" @@ -2643,33 +2379,30 @@ } }, "node_modules/@babel/template": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", - "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz", + "integrity": "sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.23.5", - "@babel/parser": "^7.24.0", - "@babel/types": "^7.24.0" + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.25.0", + "@babel/types": "^7.25.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.5.tgz", - "integrity": "sha512-7aaBLeDQ4zYcUFDUD41lJc1fG8+5IU9DaNSJAgal866FGvmD5EbWQgnEC6kO1gGLsX0esNkfnJSndbTXA3r7UA==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.24.2", - "@babel/generator": "^7.24.5", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.24.5", - "@babel/parser": "^7.24.5", - "@babel/types": "^7.24.5", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.6.tgz", + "integrity": "sha512-9Vrcx5ZW6UwK5tvqsj0nGpp/XzqthkT0dqIc9g1AdtygFToNtTF67XzYS//dm+SAK9cp3B9R4ZO/46p63SCjlQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.6", + "@babel/parser": "^7.25.6", + "@babel/template": "^7.25.0", + "@babel/types": "^7.25.6", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -2678,12 +2411,12 @@ } }, "node_modules/@babel/traverse/node_modules/@babel/generator": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.5.tgz", - "integrity": "sha512-x32i4hEXvr+iI0NEoEfDKzlemF8AmtOP8CcrRaEcpzysWuoEb1KknpcvMsHKPONoKZiDuItklgWhB18xEhr9PA==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.6.tgz", + "integrity": "sha512-VPC82gr1seXOpkjAAKoLhP50vx4vGNlF4msF64dSFq1P8RfB+QAuJWGHPXXPc8QyfVWwwB/TNNU4+ayZmHNbZw==", "dev": true, "dependencies": { - "@babel/types": "^7.24.5", + "@babel/types": "^7.25.6", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" @@ -2692,26 +2425,14 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/traverse/node_modules/@babel/helper-split-export-declaration": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.5.tgz", - "integrity": "sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q==", - "dev": true, - "dependencies": { - "@babel/types": "^7.24.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/types": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.5.tgz", - "integrity": "sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.6.tgz", + "integrity": "sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.24.1", - "@babel/helper-validator-identifier": "^7.24.5", + "@babel/helper-string-parser": "^7.24.8", + "@babel/helper-validator-identifier": "^7.24.7", "to-fast-properties": "^2.0.0" }, "engines": { @@ -2728,18 +2449,18 @@ } }, "node_modules/@discoveryjs/json-ext": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", - "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.1.tgz", + "integrity": "sha512-boghen8F0Q8D+0/Q1/1r6DUEieUJ8w2a1gIknExMSHBsJFOr2+0KUfHiVYBvucPwl3+RU5PFBK833FjFCh3BhA==", "dev": true, "engines": { - "node": ">=10.0.0" + "node": ">=14.17.0" } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.1.tgz", - "integrity": "sha512-m55cpeupQ2DbuRGQMMZDzbv9J9PgVelPjlcmM5kxHnrBdBx6REaEd7LamYV7Dm8N7rCyR/XwU6rVP8ploKtIkA==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.0.tgz", + "integrity": "sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ==", "cpu": [ "ppc64" ], @@ -2749,13 +2470,13 @@ "aix" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.1.tgz", - "integrity": "sha512-4j0+G27/2ZXGWR5okcJi7pQYhmkVgb4D7UKwxcqrjhvp5TKWx3cUjgB1CGj1mfdmJBQ9VnUGgUhign+FPF2Zgw==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.0.tgz", + "integrity": "sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g==", "cpu": [ "arm" ], @@ -2765,13 +2486,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.1.tgz", - "integrity": "sha512-hCnXNF0HM6AjowP+Zou0ZJMWWa1VkD77BXe959zERgGJBBxB+sV+J9f/rcjeg2c5bsukD/n17RKWXGFCO5dD5A==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.0.tgz", + "integrity": "sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ==", "cpu": [ "arm64" ], @@ -2781,13 +2502,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-x64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.1.tgz", - "integrity": "sha512-MSfZMBoAsnhpS+2yMFYIQUPs8Z19ajwfuaSZx+tSl09xrHZCjbeXXMsUF/0oq7ojxYEpsSo4c0SfjxOYXRbpaA==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.0.tgz", + "integrity": "sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ==", "cpu": [ "x64" ], @@ -2797,13 +2518,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.1.tgz", - "integrity": "sha512-Ylk6rzgMD8klUklGPzS414UQLa5NPXZD5tf8JmQU8GQrj6BrFA/Ic9tb2zRe1kOZyCbGl+e8VMbDRazCEBqPvA==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.0.tgz", + "integrity": "sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow==", "cpu": [ "arm64" ], @@ -2813,13 +2534,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.1.tgz", - "integrity": "sha512-pFIfj7U2w5sMp52wTY1XVOdoxw+GDwy9FsK3OFz4BpMAjvZVs0dT1VXs8aQm22nhwoIWUmIRaE+4xow8xfIDZA==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.0.tgz", + "integrity": "sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ==", "cpu": [ "x64" ], @@ -2829,13 +2550,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.1.tgz", - "integrity": "sha512-UyW1WZvHDuM4xDz0jWun4qtQFauNdXjXOtIy7SYdf7pbxSWWVlqhnR/T2TpX6LX5NI62spt0a3ldIIEkPM6RHw==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.0.tgz", + "integrity": "sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw==", "cpu": [ "arm64" ], @@ -2845,13 +2566,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.1.tgz", - "integrity": "sha512-itPwCw5C+Jh/c624vcDd9kRCCZVpzpQn8dtwoYIt2TJF3S9xJLiRohnnNrKwREvcZYx0n8sCSbvGH349XkcQeg==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.0.tgz", + "integrity": "sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ==", "cpu": [ "x64" ], @@ -2861,13 +2582,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.1.tgz", - "integrity": "sha512-LojC28v3+IhIbfQ+Vu4Ut5n3wKcgTu6POKIHN9Wpt0HnfgUGlBuyDDQR4jWZUZFyYLiz4RBBBmfU6sNfn6RhLw==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.0.tgz", + "integrity": "sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw==", "cpu": [ "arm" ], @@ -2877,13 +2598,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.1.tgz", - "integrity": "sha512-cX8WdlF6Cnvw/DO9/X7XLH2J6CkBnz7Twjpk56cshk9sjYVcuh4sXQBy5bmTwzBjNVZze2yaV1vtcJS04LbN8w==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.0.tgz", + "integrity": "sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw==", "cpu": [ "arm64" ], @@ -2893,13 +2614,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.1.tgz", - "integrity": "sha512-4H/sQCy1mnnGkUt/xszaLlYJVTz3W9ep52xEefGtd6yXDQbz/5fZE5dFLUgsPdbUOQANcVUa5iO6g3nyy5BJiw==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.0.tgz", + "integrity": "sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA==", "cpu": [ "ia32" ], @@ -2909,13 +2630,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.1.tgz", - "integrity": "sha512-c0jgtB+sRHCciVXlyjDcWb2FUuzlGVRwGXgI+3WqKOIuoo8AmZAddzeOHeYLtD+dmtHw3B4Xo9wAUdjlfW5yYA==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.0.tgz", + "integrity": "sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A==", "cpu": [ "loong64" ], @@ -2925,13 +2646,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.1.tgz", - "integrity": "sha512-TgFyCfIxSujyuqdZKDZ3yTwWiGv+KnlOeXXitCQ+trDODJ+ZtGOzLkSWngynP0HZnTsDyBbPy7GWVXWaEl6lhA==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.0.tgz", + "integrity": "sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w==", "cpu": [ "mips64el" ], @@ -2941,13 +2662,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.1.tgz", - "integrity": "sha512-b+yuD1IUeL+Y93PmFZDZFIElwbmFfIKLKlYI8M6tRyzE6u7oEP7onGk0vZRh8wfVGC2dZoy0EqX1V8qok4qHaw==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.0.tgz", + "integrity": "sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw==", "cpu": [ "ppc64" ], @@ -2957,13 +2678,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.1.tgz", - "integrity": "sha512-wpDlpE0oRKZwX+GfomcALcouqjjV8MIX8DyTrxfyCfXxoKQSDm45CZr9fanJ4F6ckD4yDEPT98SrjvLwIqUCgg==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.0.tgz", + "integrity": "sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw==", "cpu": [ "riscv64" ], @@ -2973,13 +2694,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.1.tgz", - "integrity": "sha512-5BepC2Au80EohQ2dBpyTquqGCES7++p7G+7lXe1bAIvMdXm4YYcEfZtQrP4gaoZ96Wv1Ute61CEHFU7h4FMueQ==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.0.tgz", + "integrity": "sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg==", "cpu": [ "s390x" ], @@ -2989,13 +2710,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-x64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.1.tgz", - "integrity": "sha512-5gRPk7pKuaIB+tmH+yKd2aQTRpqlf1E4f/mC+tawIm/CGJemZcHZpp2ic8oD83nKgUPMEd0fNanrnFljiruuyA==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.0.tgz", + "integrity": "sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ==", "cpu": [ "x64" ], @@ -3005,13 +2726,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.1.tgz", - "integrity": "sha512-4fL68JdrLV2nVW2AaWZBv3XEm3Ae3NZn/7qy2KGAt3dexAgSVT+Hc97JKSZnqezgMlv9x6KV0ZkZY7UO5cNLCg==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.0.tgz", + "integrity": "sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw==", "cpu": [ "x64" ], @@ -3021,13 +2742,29 @@ "netbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.0.tgz", + "integrity": "sha512-suXjq53gERueVWu0OKxzWqk7NxiUWSUlrxoZK7usiF50C6ipColGR5qie2496iKGYNLhDZkPxBI3erbnYkU0rQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.1.tgz", - "integrity": "sha512-GhRuXlvRE+twf2ES+8REbeCb/zeikNqwD3+6S5y5/x+DYbAQUNl0HNBs4RQJqrechS4v4MruEr8ZtAin/hK5iw==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.0.tgz", + "integrity": "sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg==", "cpu": [ "x64" ], @@ -3037,13 +2774,13 @@ "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.1.tgz", - "integrity": "sha512-ZnWEyCM0G1Ex6JtsygvC3KUUrlDXqOihw8RicRuQAzw+c4f1D66YlPNNV3rkjVW90zXVsHwZYWbJh3v+oQFM9Q==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.0.tgz", + "integrity": "sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA==", "cpu": [ "x64" ], @@ -3053,13 +2790,13 @@ "sunos" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.1.tgz", - "integrity": "sha512-QZ6gXue0vVQY2Oon9WyLFCdSuYbXSoxaZrPuJ4c20j6ICedfsDilNPYfHLlMH7vGfU5DQR0czHLmJvH4Nzis/A==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.0.tgz", + "integrity": "sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ==", "cpu": [ "arm64" ], @@ -3069,13 +2806,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.1.tgz", - "integrity": "sha512-HzcJa1NcSWTAU0MJIxOho8JftNp9YALui3o+Ny7hCh0v5f90nprly1U3Sj1Ldj/CvKKdvvFsCRvDkpsEMp4DNw==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.0.tgz", + "integrity": "sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA==", "cpu": [ "ia32" ], @@ -3085,13 +2822,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-x64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.1.tgz", - "integrity": "sha512-0MBh53o6XtI6ctDnRMeQ+xoCN8kD2qI1rY1KgF/xdWQwoFeKou7puvDfV8/Wv4Ctx2rRpET/gGdz3YlNtNACSA==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.0.tgz", + "integrity": "sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g==", "cpu": [ "x64" ], @@ -3101,7 +2838,7 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@eslint-community/eslint-utils": { @@ -3120,9 +2857,9 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", - "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz", + "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==", "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" @@ -3167,12 +2904,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/@eslint/eslintrc/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -3198,18 +2929,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@eslint/eslintrc/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -3241,21 +2960,22 @@ } }, "node_modules/@eslint/js": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", "dev": true, "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", + "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", "minimatch": "^3.0.5" }, @@ -3302,1000 +3022,653 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", "dev": true }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "node_modules/@inquirer/checkbox": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-2.5.0.tgz", + "integrity": "sha512-sMgdETOfi2dUHT8r7TT1BTKOwNvdDGFDXYWtQ2J69SvlYNntk9I/gJe7r5yvMwwsuKnYbuRs3pNhx4tgNck5aA==", "dev": true, "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + "@inquirer/core": "^9.1.0", + "@inquirer/figures": "^1.0.5", + "@inquirer/type": "^1.5.3", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" }, "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "node_modules/@inquirer/confirm": { + "version": "3.1.22", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-3.1.22.tgz", + "integrity": "sha512-gsAKIOWBm2Q87CDfs9fEo7wJT3fwWIJfnDGMn9Qy74gBnNFOACDNfhUzovubbJjWnKLGBln7/NcSmZwj5DuEXg==", "dev": true, - "engines": { - "node": ">=12" + "dependencies": { + "@inquirer/core": "^9.0.10", + "@inquirer/type": "^1.5.2" }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">=18" } }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "node_modules/@inquirer/core": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-9.2.1.tgz", + "integrity": "sha512-F2VBt7W/mwqEU4bL0RnHNZmC/OxzNx9cOYxHqnXX3MP6ruYvZUZAW9imgN9+h/uBT/oP8Gh888J2OZSbjSeWcg==", "dev": true, "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" + "@inquirer/figures": "^1.0.6", + "@inquirer/type": "^2.0.0", + "@types/mute-stream": "^0.0.4", + "@types/node": "^22.5.5", + "@types/wrap-ansi": "^3.0.0", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "^1.0.0", + "signal-exit": "^4.1.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=18" } }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "node_modules/@inquirer/core/node_modules/@inquirer/type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-2.0.0.tgz", + "integrity": "sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag==", "dev": true, "dependencies": { - "ansi-regex": "^6.0.1" + "mute-stream": "^1.0.0" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "node": ">=18" } }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "node_modules/@inquirer/editor": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-2.2.0.tgz", + "integrity": "sha512-9KHOpJ+dIL5SZli8lJ6xdaYLPPzB8xB9GZItg39MBybzhxA16vxmszmQFrRwbOA918WA2rvu8xhDEg/p6LXKbw==", "dev": true, "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3", + "external-editor": "^3.1.0" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "node": ">=18" } }, - "node_modules/@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "node_modules/@inquirer/expand": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-2.3.0.tgz", + "integrity": "sha512-qnJsUcOGCSG1e5DTOErmv2BPQqrtT6uzqn1vI/aYGiPKq+FgslGZmtdnXbhuI7IlT7OByDoEEqdnhUnVR2hhLw==", "dev": true, "dependencies": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3", + "yoctocolors-cjs": "^2.1.2" }, "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "node_modules/@inquirer/figures": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.6.tgz", + "integrity": "sha512-yfZzps3Cso2UbM7WlxKwZQh2Hs6plrbjs1QnzQDZhK2DgyCo6D8AaHps9olkNcUFlcYERMqU3uJSp1gmy3s/qQ==", "dev": true, "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "node_modules/@inquirer/input": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-2.3.0.tgz", + "integrity": "sha512-XfnpCStx2xgh1LIRqPXrTNEEByqQWoxsWYzNRSEUxJ5c6EQlhMogJ3vHKu8aXuTacebtaZzMAHwEL0kAflKOBw==", "dev": true, "dependencies": { - "@sinclair/typebox": "^0.27.8" + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=18" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "node_modules/@inquirer/number": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-1.1.0.tgz", + "integrity": "sha512-ilUnia/GZUtfSZy3YEErXLJ2Sljo/mf9fiKc08n18DdwdmDbOzRcTv65H1jjDvlsAuvdFXf4Sa/aL7iw/NanVA==", "dev": true, "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3" }, "engines": { - "node": ">=6.0.0" + "node": ">=18" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "node_modules/@inquirer/password": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-2.2.0.tgz", + "integrity": "sha512-5otqIpgsPYIshqhgtEwSspBQE40etouR8VIxzpJkv9i0dVHIpyhiivbkH9/dGiMLdyamT54YRdGJLfl8TFnLHg==", "dev": true, + "dependencies": { + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3", + "ansi-escapes": "^4.3.2" + }, "engines": { - "node": ">=6.0.0" + "node": ">=18" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "node_modules/@inquirer/prompts": { + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-5.3.8.tgz", + "integrity": "sha512-b2BudQY/Si4Y2a0PdZZL6BeJtl8llgeZa7U2j47aaJSCeAl1e4UI7y8a9bSkO3o/ZbZrgT5muy/34JbsjfIWxA==", "dev": true, + "dependencies": { + "@inquirer/checkbox": "^2.4.7", + "@inquirer/confirm": "^3.1.22", + "@inquirer/editor": "^2.1.22", + "@inquirer/expand": "^2.1.22", + "@inquirer/input": "^2.2.9", + "@inquirer/number": "^1.0.10", + "@inquirer/password": "^2.1.22", + "@inquirer/rawlist": "^2.2.4", + "@inquirer/search": "^1.0.7", + "@inquirer/select": "^2.4.7" + }, "engines": { - "node": ">=6.0.0" + "node": ">=18" } }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", - "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "node_modules/@inquirer/rawlist": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-2.3.0.tgz", + "integrity": "sha512-zzfNuINhFF7OLAtGHfhwOW2TlYJyli7lOUoJUXw/uyklcwalV6WRXBXtFIicN8rTRK1XTiPWB4UY+YuW8dsnLQ==", "dev": true, "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "node_modules/@inquirer/search": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-1.1.0.tgz", + "integrity": "sha512-h+/5LSj51dx7hp5xOn4QFnUaKeARwUCLs6mIhtkJ0JYPBLmEYjdHSYh7I6GrLg9LwpJ3xeX0FZgAG1q0QdCpVQ==", "dev": true, "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "@inquirer/core": "^9.1.0", + "@inquirer/figures": "^1.0.5", + "@inquirer/type": "^1.5.3", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" } }, - "node_modules/@leichtgewicht/ip-codec": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", - "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", - "dev": true - }, - "node_modules/@ljharb/through": { - "version": "2.3.13", - "resolved": "https://registry.npmjs.org/@ljharb/through/-/through-2.3.13.tgz", - "integrity": "sha512-/gKJun8NNiWGZJkGzI/Ragc53cOdcLNdzjLaIa+GEjguQs0ulsurx8WN0jijdK9yPqDvziX995sMRLyLt1uZMQ==", + "node_modules/@inquirer/select": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-2.5.0.tgz", + "integrity": "sha512-YmDobTItPP3WcEI86GvPo+T2sRHkxxOq/kXmsBjHS5BVXUgvgZ5AfJjkvQvZr03T81NnI3KrrRuMzeuYUQRFOA==", "dev": true, "dependencies": { - "call-bind": "^1.0.7" + "@inquirer/core": "^9.1.0", + "@inquirer/figures": "^1.0.5", + "@inquirer/type": "^1.5.3", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" }, "engines": { - "node": ">= 0.4" - } - }, - "node_modules/@material/animation": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/animation/-/animation-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-1GSJaPKef+7HRuV+HusVZHps64cmZuOItDbt40tjJVaikcaZvwmHlcTxRIqzcRoCdt5ZKHh3NoO7GB9Khg4Jnw==", - "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/@material/auto-init": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/auto-init/-/auto-init-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-t7ZGpRJ3ec0QDUO0nJu/SMgLW7qcuG2KqIsEYD1Ej8qhI2xpdR2ydSDQOkVEitXmKoGol1oq4nYSBjTlB65GqA==", - "dependencies": { - "@material/base": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } - }, - "node_modules/@material/banner": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/banner/-/banner-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-g9wBUZzYBizyBcBQXTIafnRUUPi7efU9gPJfzeGgkynXiccP/vh5XMmH+PBxl5v+4MlP/d4cZ2NUYoAN7UTqSA==", - "dependencies": { - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/button": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/tokens": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } - }, - "node_modules/@material/base": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/base/-/base-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-I9KQOKXpLfJkP8MqZyr8wZIzdPHrwPjFvGd9zSK91/vPyE4hzHRJc/0njsh9g8Lm9PRYLbifXX+719uTbHxx+A==", - "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/@material/button": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/button/-/button-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-BHB7iyHgRVH+JF16+iscR+Qaic+p7LU1FOLgP8KucRlpF9tTwIxQA6mJwGRi5gUtcG+vyCmzVS+hIQ6DqT/7BA==", - "dependencies": { - "@material/density": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/tokens": "15.0.0-canary.7f224ddd4.0", - "@material/touch-target": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } - }, - "node_modules/@material/card": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/card/-/card-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-kt7y9/IWOtJTr3Z/AoWJT3ZLN7CLlzXhx2udCLP9ootZU2bfGK0lzNwmo80bv/pJfrY9ihQKCtuGTtNxUy+vIw==", - "dependencies": { - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/tokens": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } - }, - "node_modules/@material/checkbox": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/checkbox/-/checkbox-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-rURcrL5O1u6hzWR+dNgiQ/n89vk6tdmdP3mZgnxJx61q4I/k1yijKqNJSLrkXH7Rto3bM5NRKMOlgvMvVd7UMQ==", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/density": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/touch-target": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } - }, - "node_modules/@material/chips": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/chips/-/chips-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-AYAivV3GSk/T/nRIpH27sOHFPaSMrE3L0WYbnb5Wa93FgY8a0fbsFYtSH2QmtwnzXveg+B1zGTt7/xIIcynKdQ==", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/checkbox": "15.0.0-canary.7f224ddd4.0", - "@material/density": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/tokens": "15.0.0-canary.7f224ddd4.0", - "@material/touch-target": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "safevalues": "^0.3.4", - "tslib": "^2.1.0" - } - }, - "node_modules/@material/circular-progress": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/circular-progress/-/circular-progress-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-DJrqCKb+LuGtjNvKl8XigvyK02y36GRkfhMUYTcJEi3PrOE00bwXtyj7ilhzEVshQiXg6AHGWXtf5UqwNrx3Ow==", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/progress-indicator": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } - }, - "node_modules/@material/data-table": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/data-table/-/data-table-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-/2WZsuBIq9z9RWYF5Jo6b7P6u0fwit+29/mN7rmAZ6akqUR54nXyNfoSNiyydMkzPlZZsep5KrSHododDhBZbA==", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/checkbox": "15.0.0-canary.7f224ddd4.0", - "@material/density": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/icon-button": "15.0.0-canary.7f224ddd4.0", - "@material/linear-progress": "15.0.0-canary.7f224ddd4.0", - "@material/list": "15.0.0-canary.7f224ddd4.0", - "@material/menu": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/select": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/tokens": "15.0.0-canary.7f224ddd4.0", - "@material/touch-target": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } - }, - "node_modules/@material/density": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/density/-/density-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-o9EXmGKVpiQ6mHhyV3oDDzc78Ow3E7v8dlaOhgaDSXgmqaE8v5sIlLNa/LKSyUga83/fpGk3QViSGXotpQx0jA==", - "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/@material/dialog": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/dialog/-/dialog-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-u0XpTlv1JqWC/bQ3DavJ1JguofTelLT2wloj59l3/1b60jv42JQ6Am7jU3I8/SIUB1MKaW7dYocXjDWtWJakLA==", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/button": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/icon-button": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/tokens": "15.0.0-canary.7f224ddd4.0", - "@material/touch-target": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" + "node": ">=18" } }, - "node_modules/@material/dom": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/dom/-/dom-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-mQ1HT186GPQSkRg5S18i70typ5ZytfjL09R0gJ2Qg5/G+MLCGi7TAjZZSH65tuD/QGOjel4rDdWOTmYbPYV6HA==", + "node_modules/@inquirer/type": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-1.5.5.tgz", + "integrity": "sha512-MzICLu4yS7V8AA61sANROZ9vT1H3ooca5dSmI1FjZkzq7o/koMsRfQSzRtFo+F3Ao4Sf1C0bpLKejpKB/+j6MA==", + "dev": true, "dependencies": { - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } - }, - "node_modules/@material/drawer": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/drawer/-/drawer-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-qyO0W0KBftfH8dlLR0gVAgv7ZHNvU8ae11Ao6zJif/YxcvK4+gph1z8AO4H410YmC2kZiwpSKyxM1iQCCzbb4g==", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/list": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" + "mute-stream": "^1.0.0" + }, + "engines": { + "node": ">=18" } }, - "node_modules/@material/elevation": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/elevation/-/elevation-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-tV6s4/pUBECedaI36Yj18KmRCk1vfue/JP/5yYRlFNnLMRVISePbZaKkn/BHXVf+26I3W879+XqIGlDVdmOoMA==", + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } - }, - "node_modules/@material/fab": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/fab/-/fab-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-4h76QrzfZTcPdd+awDPZ4Q0YdSqsXQnS540TPtyXUJ/5G99V6VwGpjMPIxAsW0y+pmI9UkLL/srrMaJec+7r4Q==", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/tokens": "15.0.0-canary.7f224ddd4.0", - "@material/touch-target": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" } }, - "node_modules/@material/feature-targeting": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/feature-targeting/-/feature-targeting-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-SAjtxYh6YlKZriU83diDEQ7jNSP2MnxKsER0TvFeyG1vX/DWsUyYDOIJTOEa9K1N+fgJEBkNK8hY55QhQaspew==", - "dependencies": { - "tslib": "^2.1.0" + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/@material/floating-label": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/floating-label/-/floating-label-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-0KMo5ijjYaEHPiZ2pCVIcbaTS2LycvH9zEhEMKwPPGssBCX7iz5ffYQFk7e5yrQand1r3jnQQgYfHAwtykArnQ==", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@material/focus-ring": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/focus-ring/-/focus-ring-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-Jmg1nltq4J6S6A10EGMZnvufrvU3YTi+8R8ZD9lkSbun0Fm2TVdICQt/Auyi6An9zP66oQN6c31eqO6KfIPsDg==", - "dependencies": { - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0" - } + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true }, - "node_modules/@material/form-field": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/form-field/-/form-field-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-fEPWgDQEPJ6WF7hNnIStxucHR9LE4DoDSMqCsGWS2Yu+NLZYLuCEecgR0UqQsl1EQdNRaFh8VH93KuxGd2hiPg==", + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, "dependencies": { - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } - }, - "node_modules/@material/icon-button": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/icon-button/-/icon-button-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-DcK7IL4ICY/DW+48YQZZs9g0U1kRaW0Wb0BxhvppDMYziHo/CTpFdle4gjyuTyRxPOdHQz5a97ru48Z9O4muTw==", - "dependencies": { - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/density": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/touch-target": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@material/image-list": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/image-list/-/image-list-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-voMjG2p80XbjL1B2lmF65zO5gEgJOVKClLdqh4wbYzYfwY/SR9c8eLvlYG7DLdFaFBl/7gGxD8TvvZ329HUFPw==", + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, "dependencies": { - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/@material/layout-grid": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/layout-grid/-/layout-grid-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-veDABLxMn2RmvfnUO2RUmC1OFfWr4cU+MrxKPoDD2hl3l3eDYv5fxws6r5T1JoSyXoaN+oEZpheS0+M9Ure8Pg==", + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, "dependencies": { - "tslib": "^2.1.0" + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/@material/line-ripple": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/line-ripple/-/line-ripple-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-f60hVJhIU6I3/17Tqqzch1emUKEcfVVgHVqADbU14JD+oEIz429ZX9ksZ3VChoU3+eejFl+jVdZMLE/LrAuwpg==", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" } }, - "node_modules/@material/linear-progress": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/linear-progress/-/linear-progress-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-pRDEwPQielDiC9Sc5XhCXrGxP8wWOnAO8sQlMebfBYHYqy5hhiIzibezS8CSaW4MFQFyXmCmpmqWlbqGYRmiyg==", + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/progress-indicator": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" } }, - "node_modules/@material/list": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/list/-/list-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-Is0NV91sJlXF5pOebYAtWLF4wU2MJDbYqztML/zQNENkQxDOvEXu3nWNb3YScMIYJJXvARO0Liur5K4yPagS1Q==", - "dependencies": { - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/density": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/tokens": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" } }, - "node_modules/@material/menu": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/menu/-/menu-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-D11QU1dXqLbh5X1zKlEhS3QWh0b5BPNXlafc5MXfkdJHhOiieb7LC9hMJhbrHtj24FadJ7evaFW/T2ugJbJNnQ==", - "dependencies": { - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/list": "15.0.0-canary.7f224ddd4.0", - "@material/menu-surface": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/tokens": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" } }, - "node_modules/@material/menu-surface": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/menu-surface/-/menu-surface-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-7RZHvw0gbwppaAJ/Oh5SWmfAKJ62aw1IMB3+3MRwsb5PLoV666wInYa+zJfE4i7qBeOn904xqT2Nko5hY0ssrg==", + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "dev": true, "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" } }, - "node_modules/@material/notched-outline": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/notched-outline/-/notched-outline-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-Yg2usuKB2DKlKIBISbie9BFsOVuffF71xjbxPbybvqemxqUBd+bD5/t6H1fLE+F8/NCu5JMigho4ewUU+0RCiw==", - "dependencies": { - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/floating-label": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true }, - "node_modules/@material/progress-indicator": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/progress-indicator/-/progress-indicator-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-UPbDjE5CqT+SqTs0mNFG6uFEw7wBlgYmh+noSkQ6ty/EURm8lF125dmi4dv4kW0+octonMXqkGtAoZwLIHKf/w==", + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/@material/radio": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/radio/-/radio-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-wR1X0Sr0KmQLu6+YOFKAI84G3L6psqd7Kys5kfb8WKBM36zxO5HQXC5nJm/Y0rdn22ixzsIz2GBo0MNU4V4k1A==", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/density": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/touch-target": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@material/ripple": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/ripple/-/ripple-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-JqOsWM1f4aGdotP0rh1vZlPZTg6lZgh39FIYHFMfOwfhR+LAikUJ+37ciqZuewgzXB6iiRO6a8aUH6HR5SJYPg==", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" + "node_modules/@jsonjoy.com/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", + "dev": true, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "node_modules/@material/rtl": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/rtl/-/rtl-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-UVf14qAtmPiaaZjuJtmN36HETyoKWmsZM/qn1L5ciR2URb8O035dFWnz4ZWFMmAYBno/L7JiZaCkPurv2ZNrGA==", + "node_modules/@jsonjoy.com/json-pack": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.1.0.tgz", + "integrity": "sha512-zlQONA+msXPPwHWZMKFVS78ewFczIll5lXiVPwFPCZUsrOKdxc2AvxU1HoNBmMRhqDZUR9HkC3UOm+6pME6Xsg==", + "dev": true, "dependencies": { - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" + "@jsonjoy.com/base64": "^1.1.1", + "@jsonjoy.com/util": "^1.1.2", + "hyperdyperid": "^1.2.0", + "thingies": "^1.20.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "node_modules/@material/segmented-button": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/segmented-button/-/segmented-button-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-LCnVRUSAhELTKI/9hSvyvIvQIpPpqF29BV+O9yM4WoNNmNWqTulvuiv7grHZl6Z+kJuxSg4BGbsPxxb9dXozPg==", - "dependencies": { - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/touch-target": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" + "node_modules/@jsonjoy.com/util": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.3.0.tgz", + "integrity": "sha512-Cebt4Vk7k1xHy87kHY7KSPLT77A7Ev7IfOblyLZhtYEhrdQ6fX4EoLq3xOQ3O/DRMEh2ok5nyC180E+ABS8Wmw==", + "dev": true, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "node_modules/@material/select": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/select/-/select-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-WioZtQEXRpglum0cMSzSqocnhsGRr+ZIhvKb3FlaNrTaK8H3Y4QA7rVjv3emRtrLOOjaT6/RiIaUMTo9AGzWQQ==", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/density": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/floating-label": "15.0.0-canary.7f224ddd4.0", - "@material/line-ripple": "15.0.0-canary.7f224ddd4.0", - "@material/list": "15.0.0-canary.7f224ddd4.0", - "@material/menu": "15.0.0-canary.7f224ddd4.0", - "@material/menu-surface": "15.0.0-canary.7f224ddd4.0", - "@material/notched-outline": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/tokens": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", + "dev": true }, - "node_modules/@material/shape": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/shape/-/shape-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-8z8l1W3+cymObunJoRhwFPKZ+FyECfJ4MJykNiaZq7XJFZkV6xNmqAVrrbQj93FtLsECn9g4PjjIomguVn/OEw==", + "node_modules/@listr2/prompt-adapter-inquirer": { + "version": "2.0.15", + "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-2.0.15.tgz", + "integrity": "sha512-MZrGem/Ujjd4cPTLYDfCZK2iKKeiO/8OX13S6jqxldLs0Prf2aGqVlJ77nMBqMv7fzqgXEgjrNHLXcKR8l9lOg==", + "dev": true, "dependencies": { - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } - }, - "node_modules/@material/slider": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/slider/-/slider-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-QU/WSaSWlLKQRqOhJrPgm29wqvvzRusMqwAcrCh1JTrCl+xwJ43q5WLDfjYhubeKtrEEgGu9tekkAiYfMG7EBw==", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/tokens": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } - }, - "node_modules/@material/snackbar": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/snackbar/-/snackbar-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-sm7EbVKddaXpT/aXAYBdPoN0k8yeg9+dprgBUkrdqGzWJAeCkxb4fv2B3He88YiCtvkTz2KLY4CThPQBSEsMFQ==", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/button": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/icon-button": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/tokens": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" + "@inquirer/type": "^1.5.1" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@inquirer/prompts": ">= 3 < 6" } }, - "node_modules/@material/switch": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/switch/-/switch-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-lEDJfRvkVyyeHWIBfoxYjJVl+WlEAE2kZ/+6OqB1FW0OV8ftTODZGhHRSzjVBA1/p4FPuhAtKtoK9jTpa4AZjA==", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/density": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/tokens": "15.0.0-canary.7f224ddd4.0", - "safevalues": "^0.3.4", - "tslib": "^2.1.0" - } + "node_modules/@lmdb/lmdb-darwin-arm64": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-3.0.13.tgz", + "integrity": "sha512-uiKPB0Fv6WEEOZjruu9a6wnW/8jrjzlZbxXscMB8kuCJ1k6kHpcBnuvaAWcqhbI7rqX5GKziwWEdD+wi2gNLfA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/@material/tab": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/tab/-/tab-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-E1xGACImyCLurhnizyOTCgOiVezce4HlBFAI6YhJo/AyVwjN2Dtas4ZLQMvvWWqpyhITNkeYdOchwCC1mrz3AQ==", - "dependencies": { - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/tab-indicator": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/tokens": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } + "node_modules/@lmdb/lmdb-darwin-x64": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-3.0.13.tgz", + "integrity": "sha512-bEVIIfK5mSQoG1R19qA+fJOvCB+0wVGGnXHT3smchBVahYBdlPn2OsZZKzlHWfb1E+PhLBmYfqB5zQXFP7hJig==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/@material/tab-bar": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/tab-bar/-/tab-bar-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-p1Asb2NzrcECvAQU3b2SYrpyJGyJLQWR+nXTYzDKE8WOpLIRCXap2audNqD7fvN/A20UJ1J8U01ptrvCkwJ4eA==", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/density": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/tab": "15.0.0-canary.7f224ddd4.0", - "@material/tab-indicator": "15.0.0-canary.7f224ddd4.0", - "@material/tab-scroller": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/tokens": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } + "node_modules/@lmdb/lmdb-linux-arm": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-3.0.13.tgz", + "integrity": "sha512-Yml1KlMzOnXj/tnW7yX8U78iAzTk39aILYvCPbqeewAq1kSzl+w59k/fiVkTBfvDi/oW/5YRxL+Fq+Y1Fr1r2Q==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@material/tab-indicator": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/tab-indicator/-/tab-indicator-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-h9Td3MPqbs33spcPS7ecByRHraYgU4tNCZpZzZXw31RypjKvISDv/PS5wcA4RmWqNGih78T7xg4QIGsZg4Pk4w==", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } + "node_modules/@lmdb/lmdb-linux-arm64": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-3.0.13.tgz", + "integrity": "sha512-afbVrsMgZ9dUTNUchFpj5VkmJRxvht/u335jUJ7o23YTbNbnpmXif3VKQGCtnjSh+CZaqm6N3CPG8KO3zwyZ1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@material/tab-scroller": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/tab-scroller/-/tab-scroller-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-LFeYNjQpdXecwECd8UaqHYbhscDCwhGln5Yh+3ctvcEgvmDPNjhKn/DL3sWprWvG8NAhP6sHMrsGhQFVdCWtTg==", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/tab": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } + "node_modules/@lmdb/lmdb-linux-x64": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-3.0.13.tgz", + "integrity": "sha512-vOtxu0xC0SLdQ2WRXg8Qgd8T32ak4SPqk5zjItRszrJk2BdeXqfGxBJbP7o4aOvSPSmSSv46Lr1EP4HXU8v7Kg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@material/textfield": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/textfield/-/textfield-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-AExmFvgE5nNF0UA4l2cSzPghtxSUQeeoyRjFLHLy+oAaE4eKZFrSy0zEpqPeWPQpEMDZk+6Y+6T3cOFYBeSvsw==", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/density": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/floating-label": "15.0.0-canary.7f224ddd4.0", - "@material/line-ripple": "15.0.0-canary.7f224ddd4.0", - "@material/notched-outline": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/tokens": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } + "node_modules/@lmdb/lmdb-win32-x64": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-3.0.13.tgz", + "integrity": "sha512-UCrMJQY/gJnOl3XgbWRZZUvGGBuKy6i0YNSptgMzHBjs+QYDYR1Mt/RLTOPy4fzzves65O1EDmlL//OzEqoLlA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/@material/theme": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/theme/-/theme-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-hs45hJoE9yVnoVOcsN1jklyOa51U4lzWsEnQEuJTPOk2+0HqCQ0yv/q0InpSnm2i69fNSyZC60+8HADZGF8ugQ==", - "dependencies": { - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", + "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/@material/tokens": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/tokens/-/tokens-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-r9TDoicmcT7FhUXC4eYMFnt9TZsz0G8T3wXvkKncLppYvZ517gPyD/1+yhuGfGOxAzxTrM66S/oEc1fFE2q4hw==", - "dependencies": { - "@material/elevation": "15.0.0-canary.7f224ddd4.0" - } + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", + "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/@material/tooltip": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/tooltip/-/tooltip-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-8qNk3pmPLTnam3XYC1sZuplQXW9xLn4Z4MI3D+U17Q7pfNZfoOugGr+d2cLA9yWAEjVJYB0mj8Yu86+udo4N9w==", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/button": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/tokens": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "safevalues": "^0.3.4", - "tslib": "^2.1.0" - } + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", + "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@material/top-app-bar": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/top-app-bar/-/top-app-bar-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-SARR5/ClYT4CLe9qAXakbr0i0cMY0V3V4pe3ElIJPfL2Z2c4wGR1mTR8m2LxU1MfGKK8aRoUdtfKaxWejp+eNA==", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", + "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@material/touch-target": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/touch-target/-/touch-target-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-BJo/wFKHPYLGsRaIpd7vsQwKr02LtO2e89Psv0on/p0OephlNIgeB9dD9W+bQmaeZsZ6liKSKRl6wJWDiK71PA==", - "dependencies": { - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", + "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@material/typography": { - "version": "15.0.0-canary.7f224ddd4.0", - "resolved": "https://registry.npmjs.org/@material/typography/-/typography-15.0.0-canary.7f224ddd4.0.tgz", - "integrity": "sha512-kBaZeCGD50iq1DeRRH5OM5Jl7Gdk+/NOfKArkY4ksBZvJiStJ7ACAhpvb8MEGm4s3jvDInQFLsDq3hL+SA79sQ==", - "dependencies": { - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", + "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] }, "node_modules/@ngrx/component-store": { - "version": "17.2.0", - "resolved": "https://registry.npmjs.org/@ngrx/component-store/-/component-store-17.2.0.tgz", - "integrity": "sha512-ywhyoZpkbVIY1t5zf7xfWLGkY0A/fQdMjPehHloDI6bRLrmbllBhQRazwZ+FAGIi2myx1+mGcmAc6FbtIikedA==", + "version": "18.0.2", + "resolved": "https://registry.npmjs.org/@ngrx/component-store/-/component-store-18.0.2.tgz", + "integrity": "sha512-IB7ZKFqjDt4duQbfYqXxAOKf9Si9O1HFodqbNCSgi7gnovK/frf/H429a+lYOyItPcpno3ECom6/1k8pE8fWlg==", "dependencies": { - "@ngrx/operators": "17.0.0-beta.0", + "@ngrx/operators": "18.0.1", "tslib": "^2.0.0" }, "peerDependencies": { - "@angular/core": "^17.0.0", + "@angular/core": "^18.0.0", "rxjs": "^6.5.3 || ^7.5.0" } }, "node_modules/@ngrx/effects": { - "version": "17.2.0", - "resolved": "https://registry.npmjs.org/@ngrx/effects/-/effects-17.2.0.tgz", - "integrity": "sha512-tXDJNsuBtbvI/7+vYnkDKKpUvLbopw1U5G6LoPnKNrbTPsPcUGmCqF5Su/ZoRN3BhXjt2j+eoeVdpBkxdxMRgg==", + "version": "18.0.2", + "resolved": "https://registry.npmjs.org/@ngrx/effects/-/effects-18.0.2.tgz", + "integrity": "sha512-YojXcOD9Lsq4kl2HCjENccyUM/mOlgBdtddsg9j/ojzSUgu3ZuBVKLN3atrL2TJYkbMX1MN0RzafSkL3TPGFIA==", "dependencies": { - "@ngrx/operators": "17.0.0-beta.0", + "@ngrx/operators": "18.0.1", "tslib": "^2.0.0" }, "peerDependencies": { - "@angular/core": "^17.0.0", - "@ngrx/store": "17.2.0", + "@angular/core": "^18.0.0", + "@ngrx/store": "18.0.2", "rxjs": "^6.5.3 || ^7.5.0" } }, "node_modules/@ngrx/operators": { - "version": "17.0.0-beta.0", - "resolved": "https://registry.npmjs.org/@ngrx/operators/-/operators-17.0.0-beta.0.tgz", - "integrity": "sha512-EbO8AONuQ6zo2v/mPyBOi4y0CTAp1x4Z+bx7ZF+Pd8BL5ma53BTCL1TmzaeK5zPUe0yApudLk9/ZbHXPnVox5A==", + "version": "18.0.1", + "resolved": "https://registry.npmjs.org/@ngrx/operators/-/operators-18.0.1.tgz", + "integrity": "sha512-M+QMrHNKgcuiLaRGZxJ4aQi5/OCRfKC4+T/63dsHyLFZ53/FFpF6a/ytSO1Q+tzOplZ5o99S+i8FVaZqNQ3LmQ==", "dependencies": { "tslib": "^2.3.0" }, @@ -4304,30 +3677,30 @@ } }, "node_modules/@ngrx/store": { - "version": "17.2.0", - "resolved": "https://registry.npmjs.org/@ngrx/store/-/store-17.2.0.tgz", - "integrity": "sha512-7wKgZ59B/6yQSvvsU0DQXipDqpkAXv7LwcXLD5Ww7nvqN0fQoRPThMh4+Wv55DCJhE0bQc1NEMciLA47uRt7Wg==", + "version": "18.0.2", + "resolved": "https://registry.npmjs.org/@ngrx/store/-/store-18.0.2.tgz", + "integrity": "sha512-ajwv0+njsO4vzArp9esnFvs1wyUb1U1W8E8LSCKrcW2hWWo9o1Pezj+JRsdQwatxHfrrPFuTDyajsl6GQM/JSA==", "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { - "@angular/core": "^17.0.0", + "@angular/core": "^18.0.0", "rxjs": "^6.5.3 || ^7.5.0" } }, "node_modules/@ngtools/webpack": { - "version": "17.3.6", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-17.3.6.tgz", - "integrity": "sha512-equxbgh2DKzZtiFMoVf1KD4yJcH1q8lpqQ/GSPPQUvONcmHrr+yqdRUdaJ7oZCyCYmXF/nByBxtMKtJr6nKZVg==", + "version": "18.2.6", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-18.2.6.tgz", + "integrity": "sha512-7HwOPE1EOgcHnpt4brSiT8G2CcXB50G0+CbCBaKGy4LYCG3Y3mrlzF5Fup9HvMJ6Tzqd62RqzpKKYBiGUT7hxg==", "dev": true, "engines": { - "node": "^18.13.0 || >=20.9.0", + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" }, "peerDependencies": { - "@angular/compiler-cli": "^17.0.0", - "typescript": ">=5.2 <5.5", + "@angular/compiler-cli": "^18.0.0", + "typescript": ">=5.4 <5.6", "webpack": "^5.54.0" } }, @@ -4383,18 +3756,15 @@ } }, "node_modules/@npmcli/agent/node_modules/lru-cache": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", - "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", - "dev": true, - "engines": { - "node": "14 || >=16.14" - } + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true }, "node_modules/@npmcli/fs": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.0.tgz", - "integrity": "sha512-7kZUAaLscfgbwBQRbvdMYaZOWyMEcPTH/tJjnyAWJ/dvvs9Ef+CERx/qJb9GExJpl1qipaDGn7KqHnFGGixd0w==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.1.tgz", + "integrity": "sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==", "dev": true, "dependencies": { "semver": "^7.3.5" @@ -4404,12 +3774,13 @@ } }, "node_modules/@npmcli/git": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-5.0.7.tgz", - "integrity": "sha512-WaOVvto604d5IpdCRV2KjQu8PzkfE96d50CQGKgywXh2GxXmDeUO5EWcBC4V57uFyrNqx83+MewuJh3WTR3xPA==", + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-5.0.8.tgz", + "integrity": "sha512-liASfw5cqhjNW9UFd+ruwwdEf/lbOAQjLL2XY2dFW/bkJheXDYZgOyul/4gVvEV4BWkTXjYGmDqMw9uegdbJNQ==", "dev": true, "dependencies": { "@npmcli/promise-spawn": "^7.0.0", + "ini": "^4.1.3", "lru-cache": "^10.0.1", "npm-pick-manifest": "^9.0.0", "proc-log": "^4.0.0", @@ -4432,22 +3803,10 @@ } }, "node_modules/@npmcli/git/node_modules/lru-cache": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", - "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", - "dev": true, - "engines": { - "node": "14 || >=16.14" - } - }, - "node_modules/@npmcli/git/node_modules/proc-log": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", - "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", - "dev": true, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true }, "node_modules/@npmcli/git/node_modules/which": { "version": "4.0.0", @@ -4490,9 +3849,9 @@ } }, "node_modules/@npmcli/package-json": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-5.1.0.tgz", - "integrity": "sha512-1aL4TuVrLS9sf8quCLerU3H9J4vtCtgu8VauYozrmEyU57i/EdKleCnsQ7vpnABIH6c9mnTxcH5sFkO3BlV8wQ==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-5.2.1.tgz", + "integrity": "sha512-f7zYC6kQautXHvNbLEWgD/uGu1+xCn9izgqBfgItWSx22U0ZDekxN08A1vM8cTxj/cRVe0Q94Ode+tdoYmIOOQ==", "dev": true, "dependencies": { "@npmcli/git": "^5.0.0", @@ -4508,36 +3867,25 @@ } }, "node_modules/@npmcli/package-json/node_modules/glob": { - "version": "10.3.12", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", - "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "dev": true, "dependencies": { "foreground-child": "^3.1.0", - "jackspeak": "^2.3.6", - "minimatch": "^9.0.1", - "minipass": "^7.0.4", - "path-scurry": "^1.10.2" + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@npmcli/package-json/node_modules/proc-log": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", - "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", - "dev": true, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, "node_modules/@npmcli/promise-spawn": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-7.0.2.tgz", @@ -4574,286 +3922,54 @@ "node": "^16.13.0 || >=18.0.0" } }, - "node_modules/@npmcli/redact": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-1.1.0.tgz", - "integrity": "sha512-PfnWuOkQgu7gCbnSsAisaX7hKOdZ4wSAhAzH3/ph5dSGau52kCRrMMGbiSQLwyTZpgldkZ49b0brkOr1AzGBHQ==", - "dev": true, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/@npmcli/run-script": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-7.0.4.tgz", - "integrity": "sha512-9ApYM/3+rBt9V80aYg6tZfzj3UWdiYyCt7gJUD1VJKvWF5nwKDSICXbYIQbspFTq6TOpbsEtIC0LArB8d9PFmg==", - "dev": true, - "dependencies": { - "@npmcli/node-gyp": "^3.0.0", - "@npmcli/package-json": "^5.0.0", - "@npmcli/promise-spawn": "^7.0.0", - "node-gyp": "^10.0.0", - "which": "^4.0.0" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/@npmcli/run-script/node_modules/isexe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", - "dev": true, - "engines": { - "node": ">=16" - } - }, - "node_modules/@npmcli/run-script/node_modules/which": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", - "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", - "dev": true, - "dependencies": { - "isexe": "^3.1.1" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^16.13.0 || >=18.0.0" - } - }, - "node_modules/@nrwl/devkit": { - "version": "17.2.8", - "resolved": "https://registry.npmjs.org/@nrwl/devkit/-/devkit-17.2.8.tgz", - "integrity": "sha512-l2dFy5LkWqSA45s6pee6CoqJeluH+sjRdVnAAQfjLHRNSx6mFAKblyzq5h1f4P0EUCVVVqLs+kVqmNx5zxYqvw==", - "dev": true, - "dependencies": { - "@nx/devkit": "17.2.8" - } - }, - "node_modules/@nrwl/tao": { - "version": "17.2.8", - "resolved": "https://registry.npmjs.org/@nrwl/tao/-/tao-17.2.8.tgz", - "integrity": "sha512-Qpk5YKeJ+LppPL/wtoDyNGbJs2MsTi6qyX/RdRrEc8lc4bk6Cw3Oul1qTXCI6jT0KzTz+dZtd0zYD/G7okkzvg==", - "dev": true, - "dependencies": { - "nx": "17.2.8", - "tslib": "^2.3.0" - }, - "bin": { - "tao": "index.js" - } - }, - "node_modules/@nx/devkit": { - "version": "17.2.8", - "resolved": "https://registry.npmjs.org/@nx/devkit/-/devkit-17.2.8.tgz", - "integrity": "sha512-6LtiQihtZwqz4hSrtT5cCG5XMCWppG6/B8c1kNksg97JuomELlWyUyVF+sxmeERkcLYFaKPTZytP0L3dmCFXaw==", - "dev": true, - "dependencies": { - "@nrwl/devkit": "17.2.8", - "ejs": "^3.1.7", - "enquirer": "~2.3.6", - "ignore": "^5.0.4", - "semver": "7.5.3", - "tmp": "~0.2.1", - "tslib": "^2.3.0" - }, - "peerDependencies": { - "nx": ">= 16 <= 18" - } - }, - "node_modules/@nx/devkit/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@nx/devkit/node_modules/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@nx/devkit/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "node_modules/@nx/nx-darwin-arm64": { - "version": "17.2.8", - "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-17.2.8.tgz", - "integrity": "sha512-dMb0uxug4hM7tusISAU1TfkDK3ixYmzc1zhHSZwpR7yKJIyKLtUpBTbryt8nyso37AS1yH+dmfh2Fj2WxfBHTg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@nx/nx-darwin-x64": { - "version": "17.2.8", - "resolved": "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-17.2.8.tgz", - "integrity": "sha512-0cXzp1tGr7/6lJel102QiLA4NkaLCkQJj6VzwbwuvmuCDxPbpmbz7HC1tUteijKBtOcdXit1/MEoEU007To8Bw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@nx/nx-freebsd-x64": { - "version": "17.2.8", - "resolved": "https://registry.npmjs.org/@nx/nx-freebsd-x64/-/nx-freebsd-x64-17.2.8.tgz", - "integrity": "sha512-YFMgx5Qpp2btCgvaniDGdu7Ctj56bfFvbbaHQWmOeBPK1krNDp2mqp8HK6ZKOfEuDJGOYAp7HDtCLvdZKvJxzA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@nx/nx-linux-arm-gnueabihf": { - "version": "17.2.8", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-17.2.8.tgz", - "integrity": "sha512-iN2my6MrhLRkVDtdivQHugK8YmR7URo1wU9UDuHQ55z3tEcny7LV3W9NSsY9UYPK/FrxdDfevj0r2hgSSdhnzA==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@nx/nx-linux-arm64-gnu": { - "version": "17.2.8", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-17.2.8.tgz", - "integrity": "sha512-Iy8BjoW6mOKrSMiTGujUcNdv+xSM1DALTH6y3iLvNDkGbjGK1Re6QNnJAzqcXyDpv32Q4Fc57PmuexyysZxIGg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@nx/nx-linux-arm64-musl": { - "version": "17.2.8", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-17.2.8.tgz", - "integrity": "sha512-9wkAxWzknjpzdofL1xjtU6qPFF1PHlvKCZI3hgEYJDo4mQiatGI+7Ttko+lx/ZMP6v4+Umjtgq7+qWrApeKamQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@nx/nx-linux-x64-gnu": { - "version": "17.2.8", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-17.2.8.tgz", - "integrity": "sha512-sjG1bwGsjLxToasZ3lShildFsF0eyeGu+pOQZIp9+gjFbeIkd19cTlCnHrOV9hoF364GuKSXQyUlwtFYFR4VTQ==", - "cpu": [ - "x64" - ], + "node_modules/@npmcli/redact": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-2.0.1.tgz", + "integrity": "sha512-YgsR5jCQZhVmTJvjduTOIHph0L73pK8xwMVaDY0PatySqVM9AZj93jpoXYSJqfHFxFkN9dmqTw6OiqExsS3LPw==", "dev": true, - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">= 10" + "node": "^16.14.0 || >=18.0.0" } }, - "node_modules/@nx/nx-linux-x64-musl": { - "version": "17.2.8", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-17.2.8.tgz", - "integrity": "sha512-QiakXZ1xBCIptmkGEouLHQbcM4klQkcr+kEaz2PlNwy/sW3gH1b/1c0Ed5J1AN9xgQxWspriAONpScYBRgxdhA==", - "cpu": [ - "x64" - ], + "node_modules/@npmcli/run-script": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-8.1.0.tgz", + "integrity": "sha512-y7efHHwghQfk28G2z3tlZ67pLG0XdfYbcVG26r7YIXALRsrVQcTq4/tdenSmdOrEsNahIYA/eh8aEVROWGFUDg==", "dev": true, - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@npmcli/node-gyp": "^3.0.0", + "@npmcli/package-json": "^5.0.0", + "@npmcli/promise-spawn": "^7.0.0", + "node-gyp": "^10.0.0", + "proc-log": "^4.0.0", + "which": "^4.0.0" + }, "engines": { - "node": ">= 10" + "node": "^16.14.0 || >=18.0.0" } }, - "node_modules/@nx/nx-win32-arm64-msvc": { - "version": "17.2.8", - "resolved": "https://registry.npmjs.org/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-17.2.8.tgz", - "integrity": "sha512-XBWUY/F/GU3vKN9CAxeI15gM4kr3GOBqnzFZzoZC4qJt2hKSSUEWsMgeZtsMgeqEClbi4ZyCCkY7YJgU32WUGA==", - "cpu": [ - "arm64" - ], + "node_modules/@npmcli/run-script/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", "dev": true, - "optional": true, - "os": [ - "win32" - ], "engines": { - "node": ">= 10" + "node": ">=16" } }, - "node_modules/@nx/nx-win32-x64-msvc": { - "version": "17.2.8", - "resolved": "https://registry.npmjs.org/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-17.2.8.tgz", - "integrity": "sha512-HTqDv+JThlLzbcEm/3f+LbS5/wYQWzb5YDXbP1wi7nlCTihNZOLNqGOkEmwlrR5tAdNHPRpHSmkYg4305W0CtA==", - "cpu": [ - "x64" - ], + "node_modules/@npmcli/run-script/node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", "dev": true, - "optional": true, - "os": [ - "win32" - ], + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, "engines": { - "node": ">= 10" + "node": "^16.13.0 || >=18.0.0" } }, "node_modules/@pkgjs/parseargs": { @@ -4879,9 +3995,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.17.2.tgz", - "integrity": "sha512-NM0jFxY8bB8QLkoKxIQeObCaDlJKewVlIEkuyYKm5An1tdVZ966w2+MPQ2l8LBZLjR+SgyV+nRkTIunzOYBMLQ==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.4.tgz", + "integrity": "sha512-Fxamp4aEZnfPOcGA8KSNEohV8hX7zVHOemC8jVBoBUHu5zpJK/Eu3uJwt6BMgy9fkvzxDaurgj96F/NiLukF2w==", "cpu": [ "arm" ], @@ -4892,9 +4008,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.17.2.tgz", - "integrity": "sha512-yeX/Usk7daNIVwkq2uGoq2BYJKZY1JfyLTaHO/jaiSwi/lsf8fTFoQW/n6IdAsx5tx+iotu2zCJwz8MxI6D/Bw==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.4.tgz", + "integrity": "sha512-VXoK5UMrgECLYaMuGuVTOx5kcuap1Jm8g/M83RnCHBKOqvPPmROFJGQaZhGccnsFtfXQ3XYa4/jMCJvZnbJBdA==", "cpu": [ "arm64" ], @@ -4905,9 +4021,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.17.2.tgz", - "integrity": "sha512-kcMLpE6uCwls023+kknm71ug7MZOrtXo+y5p/tsg6jltpDtgQY1Eq5sGfHcQfb+lfuKwhBmEURDga9N0ol4YPw==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.4.tgz", + "integrity": "sha512-xMM9ORBqu81jyMKCDP+SZDhnX2QEVQzTcC6G18KlTQEzWK8r/oNZtKuZaCcHhnsa6fEeOBionoyl5JsAbE/36Q==", "cpu": [ "arm64" ], @@ -4918,9 +4034,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.17.2.tgz", - "integrity": "sha512-AtKwD0VEx0zWkL0ZjixEkp5tbNLzX+FCqGG1SvOu993HnSz4qDI6S4kGzubrEJAljpVkhRSlg5bzpV//E6ysTQ==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.4.tgz", + "integrity": "sha512-aJJyYKQwbHuhTUrjWjxEvGnNNBCnmpHDvrb8JFDbeSH3m2XdHcxDd3jthAzvmoI8w/kSjd2y0udT+4okADsZIw==", "cpu": [ "x64" ], @@ -4931,9 +4047,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.17.2.tgz", - "integrity": "sha512-3reX2fUHqN7sffBNqmEyMQVj/CKhIHZd4y631duy0hZqI8Qoqf6lTtmAKvJFYa6bhU95B1D0WgzHkmTg33In0A==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.4.tgz", + "integrity": "sha512-j63YtCIRAzbO+gC2L9dWXRh5BFetsv0j0va0Wi9epXDgU/XUi5dJKo4USTttVyK7fGw2nPWK0PbAvyliz50SCQ==", "cpu": [ "arm" ], @@ -4944,9 +4060,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.17.2.tgz", - "integrity": "sha512-uSqpsp91mheRgw96xtyAGP9FW5ChctTFEoXP0r5FAzj/3ZRv3Uxjtc7taRQSaQM/q85KEKjKsZuiZM3GyUivRg==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.4.tgz", + "integrity": "sha512-dJnWUgwWBX1YBRsuKKMOlXCzh2Wu1mlHzv20TpqEsfdZLb3WoJW2kIEsGwLkroYf24IrPAvOT/ZQ2OYMV6vlrg==", "cpu": [ "arm" ], @@ -4957,9 +4073,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.17.2.tgz", - "integrity": "sha512-EMMPHkiCRtE8Wdk3Qhtciq6BndLtstqZIroHiiGzB3C5LDJmIZcSzVtLRbwuXuUft1Cnv+9fxuDtDxz3k3EW2A==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.4.tgz", + "integrity": "sha512-AdPRoNi3NKVLolCN/Sp4F4N1d98c4SBnHMKoLuiG6RXgoZ4sllseuGioszumnPGmPM2O7qaAX/IJdeDU8f26Aw==", "cpu": [ "arm64" ], @@ -4970,9 +4086,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.17.2.tgz", - "integrity": "sha512-NMPylUUZ1i0z/xJUIx6VUhISZDRT+uTWpBcjdv0/zkp7b/bQDF+NfnfdzuTiB1G6HTodgoFa93hp0O1xl+/UbA==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.4.tgz", + "integrity": "sha512-Gl0AxBtDg8uoAn5CCqQDMqAx22Wx22pjDOjBdmG0VIWX3qUBHzYmOKh8KXHL4UpogfJ14G4wk16EQogF+v8hmA==", "cpu": [ "arm64" ], @@ -4983,9 +4099,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.17.2.tgz", - "integrity": "sha512-T19My13y8uYXPw/L/k0JYaX1fJKFT/PWdXiHr8mTbXWxjVF1t+8Xl31DgBBvEKclw+1b00Chg0hxE2O7bTG7GQ==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.4.tgz", + "integrity": "sha512-3aVCK9xfWW1oGQpTsYJJPF6bfpWfhbRnhdlyhak2ZiyFLDaayz0EP5j9V1RVLAAxlmWKTDfS9wyRyY3hvhPoOg==", "cpu": [ "ppc64" ], @@ -4996,9 +4112,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.17.2.tgz", - "integrity": "sha512-BOaNfthf3X3fOWAB+IJ9kxTgPmMqPPH5f5k2DcCsRrBIbWnaJCgX2ll77dV1TdSy9SaXTR5iDXRL8n7AnoP5cg==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.4.tgz", + "integrity": "sha512-ePYIir6VYnhgv2C5Xe9u+ico4t8sZWXschR6fMgoPUK31yQu7hTEJb7bCqivHECwIClJfKgE7zYsh1qTP3WHUA==", "cpu": [ "riscv64" ], @@ -5009,9 +4125,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.17.2.tgz", - "integrity": "sha512-W0UP/x7bnn3xN2eYMql2T/+wpASLE5SjObXILTMPUBDB/Fg/FxC+gX4nvCfPBCbNhz51C+HcqQp2qQ4u25ok6g==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.4.tgz", + "integrity": "sha512-GqFJ9wLlbB9daxhVlrTe61vJtEY99/xB3C8e4ULVsVfflcpmR6c8UZXjtkMA6FhNONhj2eA5Tk9uAVw5orEs4Q==", "cpu": [ "s390x" ], @@ -5022,9 +4138,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.17.2.tgz", - "integrity": "sha512-Hy7pLwByUOuyaFC6mAr7m+oMC+V7qyifzs/nW2OJfC8H4hbCzOX07Ov0VFk/zP3kBsELWNFi7rJtgbKYsav9QQ==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.4.tgz", + "integrity": "sha512-87v0ol2sH9GE3cLQLNEy0K/R0pz1nvg76o8M5nhMR0+Q+BBGLnb35P0fVz4CQxHYXaAOhE8HhlkaZfsdUOlHwg==", "cpu": [ "x64" ], @@ -5035,9 +4151,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.17.2.tgz", - "integrity": "sha512-h1+yTWeYbRdAyJ/jMiVw0l6fOOm/0D1vNLui9iPuqgRGnXA0u21gAqOyB5iHjlM9MMfNOm9RHCQ7zLIzT0x11Q==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.4.tgz", + "integrity": "sha512-UV6FZMUgePDZrFjrNGIWzDo/vABebuXBhJEqrHxrGiU6HikPy0Z3LfdtciIttEUQfuDdCn8fqh7wiFJjCNwO+g==", "cpu": [ "x64" ], @@ -5048,9 +4164,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.17.2.tgz", - "integrity": "sha512-tmdtXMfKAjy5+IQsVtDiCfqbynAQE/TQRpWdVataHmhMb9DCoJxp9vLcCBjEQWMiUYxO1QprH/HbY9ragCEFLA==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.4.tgz", + "integrity": "sha512-BjI+NVVEGAXjGWYHz/vv0pBqfGoUH0IGZ0cICTn7kB9PyjrATSkX+8WkguNjWoj2qSr1im/+tTGRaY+4/PdcQw==", "cpu": [ "arm64" ], @@ -5061,9 +4177,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.17.2.tgz", - "integrity": "sha512-7II/QCSTAHuE5vdZaQEwJq2ZACkBpQDOmQsE6D6XUbnBHW8IAhm4eTufL6msLJorzrHDFv3CF8oCA/hSIRuZeQ==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.4.tgz", + "integrity": "sha512-SiWG/1TuUdPvYmzmYnmd3IEifzR61Tragkbx9D3+R8mzQqDBz8v+BvZNDlkiTtI9T15KYZhP0ehn3Dld4n9J5g==", "cpu": [ "ia32" ], @@ -5074,9 +4190,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.17.2.tgz", - "integrity": "sha512-TGGO7v7qOq4CYmSBVEYpI1Y5xDuCEnbVC5Vth8mOsW0gDSzxNrVERPc790IGHsrT2dQSimgMr9Ub3Y1Jci5/8w==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.4.tgz", + "integrity": "sha512-j8pPKp53/lq9lMXN57S8cFz0MynJk8OWNuUnXct/9KCpKU7DgU3bYMJhwWmcqC0UU29p8Lr0/7KEVcaM6bf47Q==", "cpu": [ "x64" ], @@ -5087,73 +4203,28 @@ ] }, "node_modules/@schematics/angular": { - "version": "17.0.10", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-17.0.10.tgz", - "integrity": "sha512-rRBlDMXfVPkW3CqVQxazFqkuJXd0BFnD1zjI9WtDiNt3o2pTHbLzuWJnXKuIt5rzv0x/bFwNqIt4CPW2DYGNMg==", - "dev": true, - "dependencies": { - "@angular-devkit/core": "17.0.10", - "@angular-devkit/schematics": "17.0.10", - "jsonc-parser": "3.2.0" - }, - "engines": { - "node": "^18.13.0 || >=20.9.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - } - }, - "node_modules/@schematics/angular/node_modules/@angular-devkit/core": { - "version": "17.0.10", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.0.10.tgz", - "integrity": "sha512-93N6oHnmtRt0hL3AXxvnk47sN1rHndfj+pqI5haEY41AGWzIWv9cSBsqlM0PWltNpo6VivcExZESvbLJ71wqbQ==", + "version": "18.2.6", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-18.2.6.tgz", + "integrity": "sha512-Y988EoOEQDLEyHu3414T6AeVUyx21AexBHQNbUNQkK8cxlxyB6m1eH1cx6vFgLRFUTsLVv+C6Ln/ICNTfLcG4A==", "dev": true, "dependencies": { - "ajv": "8.12.0", - "ajv-formats": "2.1.1", - "jsonc-parser": "3.2.0", - "picomatch": "3.0.1", - "rxjs": "7.8.1", - "source-map": "0.7.4" + "@angular-devkit/core": "18.2.6", + "@angular-devkit/schematics": "18.2.6", + "jsonc-parser": "3.3.1" }, "engines": { - "node": "^18.13.0 || >=20.9.0", + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "chokidar": "^3.5.2" - }, - "peerDependenciesMeta": { - "chokidar": { - "optional": true - } - } - }, - "node_modules/@schematics/angular/node_modules/jsonc-parser": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", - "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", - "dev": true - }, - "node_modules/@schematics/angular/node_modules/picomatch": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-3.0.1.tgz", - "integrity": "sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/@sigstore/bundle": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-2.3.1.tgz", - "integrity": "sha512-eqV17lO3EIFqCWK3969Rz+J8MYrRZKw9IBHpSo6DEcEX2c+uzDFOgHE9f2MnyDpfs48LFO4hXmk9KhQ74JzU1g==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-2.3.2.tgz", + "integrity": "sha512-wueKWDk70QixNLB363yHc2D2ItTgYiMTdPwK8D9dKQMR3ZQ0c35IxP5xnwQ8cNLoCgCRcHf14kE+CLIvNX1zmA==", "dev": true, "dependencies": { - "@sigstore/protobuf-specs": "^0.3.1" + "@sigstore/protobuf-specs": "^0.3.2" }, "engines": { "node": "^16.14.0 || >=18.0.0" @@ -5169,61 +4240,69 @@ } }, "node_modules/@sigstore/protobuf-specs": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.3.1.tgz", - "integrity": "sha512-aIL8Z9NsMr3C64jyQzE0XlkEyBLpgEJJFDHLVVStkFV5Q3Il/r/YtY6NJWKQ4cy4AE7spP1IX5Jq7VCAxHHMfQ==", + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.3.2.tgz", + "integrity": "sha512-c6B0ehIWxMI8wiS/bj6rHMPqeFvngFV7cDU/MY+B16P9Z3Mp9k8L93eYZ7BYzSickzuqAQqAq0V956b3Ju6mLw==", "dev": true, "engines": { "node": "^16.14.0 || >=18.0.0" } }, "node_modules/@sigstore/sign": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-2.3.0.tgz", - "integrity": "sha512-tsAyV6FC3R3pHmKS880IXcDJuiFJiKITO1jxR1qbplcsBkZLBmjrEw5GbC7ikD6f5RU1hr7WnmxB/2kKc1qUWQ==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-2.3.2.tgz", + "integrity": "sha512-5Vz5dPVuunIIvC5vBb0APwo7qKA4G9yM48kPWJT+OEERs40md5GoUR1yedwpekWZ4m0Hhw44m6zU+ObsON+iDA==", "dev": true, "dependencies": { - "@sigstore/bundle": "^2.3.0", + "@sigstore/bundle": "^2.3.2", "@sigstore/core": "^1.0.0", - "@sigstore/protobuf-specs": "^0.3.1", - "make-fetch-happen": "^13.0.0" + "@sigstore/protobuf-specs": "^0.3.2", + "make-fetch-happen": "^13.0.1", + "proc-log": "^4.2.0", + "promise-retry": "^2.0.1" }, "engines": { "node": "^16.14.0 || >=18.0.0" } }, "node_modules/@sigstore/tuf": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-2.3.2.tgz", - "integrity": "sha512-mwbY1VrEGU4CO55t+Kl6I7WZzIl+ysSzEYdA1Nv/FTrl2bkeaPXo5PnWZAVfcY2zSdhOpsUTJW67/M2zHXGn5w==", + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-2.3.4.tgz", + "integrity": "sha512-44vtsveTPUpqhm9NCrbU8CWLe3Vck2HO1PNLw7RIajbB7xhtn5RBPm1VNSCMwqGYHhDsBJG8gDF0q4lgydsJvw==", "dev": true, "dependencies": { - "@sigstore/protobuf-specs": "^0.3.0", - "tuf-js": "^2.2.0" + "@sigstore/protobuf-specs": "^0.3.2", + "tuf-js": "^2.2.1" }, "engines": { "node": "^16.14.0 || >=18.0.0" } }, "node_modules/@sigstore/verify": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-1.2.0.tgz", - "integrity": "sha512-hQF60nc9yab+Csi4AyoAmilGNfpXT+EXdBgFkP9OgPwIBPwyqVf7JAWPtmqrrrneTmAT6ojv7OlH1f6Ix5BG4Q==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-1.2.1.tgz", + "integrity": "sha512-8iKx79/F73DKbGfRf7+t4dqrc0bRr0thdPrxAtCKWRm/F0tG71i6O1rvlnScncJLLBZHn3h8M3c1BSUAb9yu8g==", "dev": true, "dependencies": { - "@sigstore/bundle": "^2.3.1", + "@sigstore/bundle": "^2.3.2", "@sigstore/core": "^1.1.0", - "@sigstore/protobuf-specs": "^0.3.1" + "@sigstore/protobuf-specs": "^0.3.2" }, "engines": { "node": "^16.14.0 || >=18.0.0" } }, - "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true + "node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/@socket.io/component-emitter": { "version": "3.1.2", @@ -5253,21 +4332,6 @@ "node": "^16.14.0 || >=18.0.0" } }, - "node_modules/@tufjs/models/node_modules/minimatch": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", - "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@types/body-parser": { "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", @@ -5321,25 +4385,11 @@ "@types/node": "*" } }, - "node_modules/@types/eslint": { - "version": "8.56.10", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz", - "integrity": "sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==", - "dev": true, - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "dev": true, - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } + "node_modules/@types/eslint-visitor-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", + "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==", + "dev": true }, "node_modules/@types/estree": { "version": "1.0.5", @@ -5360,9 +4410,21 @@ } }, "node_modules/@types/express-serve-static-core": { - "version": "4.19.0", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.0.tgz", - "integrity": "sha512-bGyep3JqPCRry1wq+O5n7oiBgGWmeIJXPjXXCo8EK0u8duZGSYar7cGqd3ML2JUsLGeB7fmc06KYo9fLGWqPvQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.0.tgz", + "integrity": "sha512-AbXMTZGt40T+KON9/Fdxx0B2WK5hsgxcfXJLr5bFpZ7b4JCex2WyQPTEKdXqfHiY5nKKBScZ7yCoO6Pvgxfvnw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/express/node_modules/@types/express-serve-static-core": { + "version": "4.19.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", + "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", "dev": true, "dependencies": { "@types/node": "*", @@ -5378,9 +4440,9 @@ "dev": true }, "node_modules/@types/http-proxy": { - "version": "1.17.14", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.14.tgz", - "integrity": "sha512-SSrD0c1OQzlFX7pGu1eXxSEjemej64aaNPRhhVYUGqXh0BtldAAx37MG8btcumvpgKyZp1F5Gn3JkktdxiFv6w==", + "version": "1.17.15", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.15.tgz", + "integrity": "sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ==", "dev": true, "dependencies": { "@types/node": "*" @@ -5404,13 +4466,22 @@ "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", "dev": true }, + "node_modules/@types/mute-stream": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@types/mute-stream/-/mute-stream-0.0.4.tgz", + "integrity": "sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/node": { - "version": "20.12.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.10.tgz", - "integrity": "sha512-Eem5pH9pmWBHoGAT8Dr5fdc5rYA+4NAovdM4EktRPVAAiJhmWWfQrA0cFhAbOsQdSfIHjAud6YdkbL69+zSKjw==", + "version": "22.7.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.4.tgz", + "integrity": "sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg==", "dev": true, "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.2" } }, "node_modules/@types/node-forge": { @@ -5423,9 +4494,9 @@ } }, "node_modules/@types/qs": { - "version": "6.9.15", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz", - "integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==", + "version": "6.9.16", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.16.tgz", + "integrity": "sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==", "dev": true }, "node_modules/@types/range-parser": { @@ -5435,15 +4506,9 @@ "dev": true }, "node_modules/@types/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", - "dev": true - }, - "node_modules/@types/semver": { - "version": "7.5.8", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", - "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", + "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", "dev": true }, "node_modules/@types/send": { @@ -5485,43 +4550,47 @@ "@types/node": "*" } }, + "node_modules/@types/wrap-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz", + "integrity": "sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==", + "dev": true + }, "node_modules/@types/ws": { - "version": "8.5.10", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", - "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz", + "integrity": "sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==", "dev": true, "dependencies": { "@types/node": "*" } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", - "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.8.0.tgz", + "integrity": "sha512-wORFWjU30B2WJ/aXBfOm1LX9v9nyt9D3jsSOxC3cCaTQGCW5k4jNpmjFv3U7p/7s4yvdjHzwtv2Sd2dOyhjS0A==", "dev": true, "dependencies": { - "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/type-utils": "6.21.0", - "@typescript-eslint/utils": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4", + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.8.0", + "@typescript-eslint/type-utils": "8.8.0", + "@typescript-eslint/utils": "8.8.0", + "@typescript-eslint/visitor-keys": "8.8.0", "graphemer": "^1.4.0", - "ignore": "^5.2.4", + "ignore": "^5.3.1", "natural-compare": "^1.4.0", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" + "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", - "eslint": "^7.0.0 || ^8.0.0" + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -5529,218 +4598,175 @@ } } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", - "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", + "node_modules/@typescript-eslint/experimental-utils": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-3.10.1.tgz", + "integrity": "sha512-DewqIgscDzmAfd5nOGe4zm6Bl7PKtMG2Ad0KG8CUZAHlXfAKTF9Ol5PXhiMh39yRL2ChRH1cuuUGOcVyyrhQIw==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "6.21.0", - "@typescript-eslint/utils": "6.21.0", - "debug": "^4.3.4", - "ts-api-utils": "^1.0.1" + "@types/json-schema": "^7.0.3", + "@typescript-eslint/types": "3.10.1", + "@typescript-eslint/typescript-estree": "3.10.1", + "eslint-scope": "^5.0.0", + "eslint-utils": "^2.0.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^10.12.0 || >=12.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "*" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", - "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", + "node_modules/@typescript-eslint/experimental-utils/node_modules/@typescript-eslint/types": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-3.10.1.tgz", + "integrity": "sha512-+3+FCUJIahE9q0lDi1WleYzjCwJs5hIsbugIgnbB+dSCYUxl8L6PwmsyOPFZde2hc1DlTo/xnkOgiTLSyAbHiQ==", "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.12", - "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/typescript-estree": "6.21.0", - "semver": "^7.5.4" - }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" } }, - "node_modules/@typescript-eslint/parser": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", - "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "node_modules/@typescript-eslint/experimental-utils/node_modules/@typescript-eslint/typescript-estree": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-3.10.1.tgz", + "integrity": "sha512-QbcXOuq6WYvnB3XPsZpIwztBoquEYLXh2MtwVU+kO8jgYCiv4G5xrSP/1wg4tkvrEE+esZVquIPX/dxPlePk1w==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/typescript-estree": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4" + "@typescript-eslint/types": "3.10.1", + "@typescript-eslint/visitor-keys": "3.10.1", + "debug": "^4.1.1", + "glob": "^7.1.6", + "is-glob": "^4.0.1", + "lodash": "^4.17.15", + "semver": "^7.3.2", + "tsutils": "^3.17.1" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^10.12.0 || >=12.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - }, "peerDependenciesMeta": { "typescript": { "optional": true } } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", - "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "node_modules/@typescript-eslint/experimental-utils/node_modules/@typescript-eslint/visitor-keys": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-3.10.1.tgz", + "integrity": "sha512-9JgC82AaQeglebjZMgYR5wgmfUdUc+EitGUUMW8u2nDckaeimzW+VsoLV6FoimPv2id3VQzfjwBxEMVz08ameQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0" + "eslint-visitor-keys": "^1.1.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/type-utils": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.18.0.tgz", - "integrity": "sha512-ZeMtrXnGmTcHciJN1+u2CigWEEXgy1ufoxtWcHORt5kGvpjjIlK9MUhzHm4RM8iVy6dqSaZA/6PVkX6+r+ChjQ==", + "node_modules/@typescript-eslint/experimental-utils/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "6.18.0", - "@typescript-eslint/utils": "6.18.0", - "debug": "^4.3.4", - "ts-api-utils": "^1.0.1" + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" }, "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "node": ">=8.0.0" } }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.18.0.tgz", - "integrity": "sha512-/RFVIccwkwSdW/1zeMx3hADShWbgBxBnV/qSrex6607isYjj05t36P6LyONgqdUrNLl5TYU8NIKdHUYpFvExkA==", + "node_modules/@typescript-eslint/experimental-utils/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", "dev": true, "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "node": ">=4" + } + }, + "node_modules/@typescript-eslint/experimental-utils/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" } }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.18.0.tgz", - "integrity": "sha512-klNvl+Ql4NsBNGB4W9TZ2Od03lm7aGvTbs0wYaFYsplVPhr+oeXjlPZCDI4U9jgJIDK38W1FKhacCFzCC+nbIg==", + "node_modules/@typescript-eslint/parser": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.8.0.tgz", + "integrity": "sha512-uEFUsgR+tl8GmzmLjRqz+VrDv4eoaMqMXW7ruXfgThaAShO9JTciKpEsB+TvnfFfbg5IpujgMXVV36gOJRLtZg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.18.0", - "@typescript-eslint/visitor-keys": "6.18.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" + "@typescript-eslint/scope-manager": "8.8.0", + "@typescript-eslint/types": "8.8.0", + "@typescript-eslint/typescript-estree": "8.8.0", + "@typescript-eslint/visitor-keys": "8.8.0", + "debug": "^4.3.4" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, "peerDependenciesMeta": { "typescript": { "optional": true } } }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.18.0.tgz", - "integrity": "sha512-1wetAlSZpewRDb2h9p/Q8kRjdGuqdTAQbkJIOUMLug2LBLG+QOjiWoSj6/3B/hA9/tVTFFdtiKvAYoYnSRW/RA==", + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.8.0.tgz", + "integrity": "sha512-EL8eaGC6gx3jDd8GwEFEV091210U97J0jeEHrAYvIYosmEGet4wJ+g0SYmLu+oRiAwbSA5AVrt6DxLHfdd+bUg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.18.0", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" + "@typescript-eslint/types": "8.8.0", + "@typescript-eslint/visitor-keys": "8.8.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", - "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", - "dev": true, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", - "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "node_modules/@typescript-eslint/type-utils": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.8.0.tgz", + "integrity": "sha512-IKwJSS7bCqyCeG4NVGxnOP6lLT9Okc3Zj8hLO96bpMkJab+10HIfJbMouLrlpyOr3yrQ1cA413YPFiGd1mW9/Q==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", + "@typescript-eslint/typescript-estree": "8.8.0", + "@typescript-eslint/utils": "8.8.0", "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" + "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -5752,78 +4778,36 @@ } } }, - "node_modules/@typescript-eslint/utils": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.18.0.tgz", - "integrity": "sha512-wiKKCbUeDPGaYEYQh1S580dGxJ/V9HI7K5sbGAVklyf+o5g3O+adnS4UNJajplF4e7z2q0uVBaTdT/yLb4XAVA==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.12", - "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.18.0", - "@typescript-eslint/types": "6.18.0", - "@typescript-eslint/typescript-estree": "6.18.0", - "semver": "^7.5.4" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/scope-manager": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.18.0.tgz", - "integrity": "sha512-o/UoDT2NgOJ2VfHpfr+KBY2ErWvCySNUIX/X7O9g8Zzt/tXdpfEU43qbNk8LVuWUT2E0ptzTWXh79i74PP0twA==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.18.0", - "@typescript-eslint/visitor-keys": "6.18.0" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.18.0.tgz", - "integrity": "sha512-/RFVIccwkwSdW/1zeMx3hADShWbgBxBnV/qSrex6607isYjj05t36P6LyONgqdUrNLl5TYU8NIKdHUYpFvExkA==", + "node_modules/@typescript-eslint/types": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.8.0.tgz", + "integrity": "sha512-QJwc50hRCgBd/k12sTykOJbESe1RrzmX6COk8Y525C9l7oweZ+1lw9JiU56im7Amm8swlz00DRIlxMYLizr2Vw==", "dev": true, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.18.0.tgz", - "integrity": "sha512-klNvl+Ql4NsBNGB4W9TZ2Od03lm7aGvTbs0wYaFYsplVPhr+oeXjlPZCDI4U9jgJIDK38W1FKhacCFzCC+nbIg==", + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.8.0.tgz", + "integrity": "sha512-ZaMJwc/0ckLz5DaAZ+pNLmHv8AMVGtfWxZe/x2JVEkD5LnmhWiQMMcYT7IY7gkdJuzJ9P14fRy28lUrlDSWYdw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.18.0", - "@typescript-eslint/visitor-keys": "6.18.0", + "@typescript-eslint/types": "8.8.0", + "@typescript-eslint/visitor-keys": "8.8.0", "debug": "^4.3.4", - "globby": "^11.1.0", + "fast-glob": "^3.3.2", "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -5835,34 +4819,39 @@ } } }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/visitor-keys": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.18.0.tgz", - "integrity": "sha512-1wetAlSZpewRDb2h9p/Q8kRjdGuqdTAQbkJIOUMLug2LBLG+QOjiWoSj6/3B/hA9/tVTFFdtiKvAYoYnSRW/RA==", + "node_modules/@typescript-eslint/utils": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.8.0.tgz", + "integrity": "sha512-QE2MgfOTem00qrlPgyByaCHay9yb1+9BjnMFnSFkUKQfu7adBXDTnCAivURnuPPAG/qiB+kzKkZKmKfaMT0zVg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.18.0", - "eslint-visitor-keys": "^3.4.1" + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.8.0", + "@typescript-eslint/types": "8.8.0", + "@typescript-eslint/typescript-estree": "8.8.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", - "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.8.0.tgz", + "integrity": "sha512-8mq51Lx6Hpmd7HnA2fcHQo3YgfX1qbccxQOgZcb4tvasu//zXRaA1j5ZRFeCw/VRAdFi4mRM9DnZw0Nu0Q2d1g==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.21.0", - "eslint-visitor-keys": "^3.4.1" + "@typescript-eslint/types": "8.8.0", + "eslint-visitor-keys": "^3.4.3" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -6051,37 +5040,6 @@ "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", "dev": true }, - "node_modules/@yarnpkg/parsers": { - "version": "3.0.0-rc.46", - "resolved": "https://registry.npmjs.org/@yarnpkg/parsers/-/parsers-3.0.0-rc.46.tgz", - "integrity": "sha512-aiATs7pSutzda/rq8fnuPwTglyVwjM22bNnK2ZgjrpAjQHSSl3lztd2f9evst1W/qnC58DRz7T7QndUDumAR4Q==", - "dev": true, - "dependencies": { - "js-yaml": "^3.10.0", - "tslib": "^2.4.0" - }, - "engines": { - "node": ">=14.15.0" - } - }, - "node_modules/@zkochan/js-yaml": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@zkochan/js-yaml/-/js-yaml-0.0.6.tgz", - "integrity": "sha512-nzvgl3VfhcELQ8LyVrYOru+UtAy1nrygk2+AGbTm8a5YcO6o8lSjAT+pfg3vJWxIoZKOUhrK6UU7xW/+00kQrg==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/@zkochan/js-yaml/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, "node_modules/abbrev": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", @@ -6105,9 +5063,9 @@ } }, "node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -6116,10 +5074,10 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-import-assertions": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", - "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", "dev": true, "peerDependencies": { "acorn": "^8" @@ -6187,15 +5145,15 @@ } }, "node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "dependencies": { - "fast-deep-equal": "^3.1.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" + "require-from-string": "^2.0.2" }, "funding": { "type": "github", @@ -6203,9 +5161,9 @@ } }, "node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", "dev": true, "dependencies": { "ajv": "^8.0.0" @@ -6268,12 +5226,12 @@ } }, "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", + "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", "dev": true, "engines": { - "node": ">=8" + "node": ">=4" } }, "node_modules/ansi-styles": { @@ -6314,13 +5272,10 @@ } }, "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "dependencies": { - "sprintf-js": "~1.0.2" - } + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true }, "node_modules/aria-query": { "version": "5.3.0", @@ -6337,31 +5292,19 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "dev": true }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", "dev": true, "engines": { "node": ">=8" } }, - "node_modules/async": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", - "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", - "dev": true - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true - }, "node_modules/autoprefixer": { - "version": "10.4.18", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.18.tgz", - "integrity": "sha512-1DKbDfsr6KUElM6wg+0zRNkB/Q7WcKYAaK+pzXn+Xqmszm/5Xa9coeNdtP88Vi+dPzZnMjhge8GIV49ZQkDa+g==", + "version": "10.4.20", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", + "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", "dev": true, "funding": [ { @@ -6378,11 +5321,11 @@ } ], "dependencies": { - "browserslist": "^4.23.0", - "caniuse-lite": "^1.0.30001591", + "browserslist": "^4.23.3", + "caniuse-lite": "^1.0.30001646", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", + "picocolors": "^1.0.1", "postcss-value-parser": "^4.2.0" }, "bin": { @@ -6395,24 +5338,13 @@ "postcss": "^8.1.0" } }, - "node_modules/axios": { - "version": "1.6.8", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz", - "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==", - "dev": true, - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, "node_modules/axobject-query": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.0.0.tgz", - "integrity": "sha512-+60uv1hiVFhHZeO+Lz0RYzsVHy5Wr1ayX0mwda9KPDVLNJgZ1T9Ny7VmFbLDzxsH0D87I86vgj3gFrjTJUYznw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", "dev": true, - "dependencies": { - "dequal": "^2.0.3" + "engines": { + "node": ">= 0.4" } }, "node_modules/babel-loader": { @@ -6432,22 +5364,6 @@ "webpack": ">=5" } }, - "node_modules/babel-plugin-istanbul": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", - "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^5.0.4", - "test-exclude": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/babel-plugin-polyfill-corejs2": { "version": "0.4.11", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz", @@ -6472,57 +5388,25 @@ } }, "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.9.0.tgz", - "integrity": "sha512-7nZPG1uzK2Ymhy/NbaOWTg3uibM2BmGASS4vHS4szRZAIR8R6GwA/xAujpdrXU5iyklrimWnLWU+BLF9suPTqg==", - "dev": true, - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.5.0", - "core-js-compat": "^3.34.0" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-polyfill-corejs3/node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.5.0.tgz", - "integrity": "sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q==", + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz", + "integrity": "sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==", "dev": true, "dependencies": { - "@babel/helper-compilation-targets": "^7.22.6", - "@babel/helper-plugin-utils": "^7.22.5", - "debug": "^4.1.1", - "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2" + "@babel/helper-define-polyfill-provider": "^0.6.2", + "core-js-compat": "^3.38.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.5.tgz", - "integrity": "sha512-OJGYZlhLqBh2DDHeqAxWB1XIvr49CxiJ2gIt61/PU55CQK4Z58OzMqjDe1zwQdQk+rBYsRc+1rJmdajM3gimHg==", - "dev": true, - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.5.0" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-polyfill-regenerator/node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.5.0.tgz", - "integrity": "sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q==", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.2.tgz", + "integrity": "sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg==", "dev": true, "dependencies": { - "@babel/helper-compilation-targets": "^7.22.6", - "@babel/helper-plugin-utils": "^7.22.5", - "debug": "^4.1.1", - "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2" + "@babel/helper-define-polyfill-provider": "^0.6.2" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -6531,14 +5415,12 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, "funding": [ { "type": "github", @@ -6594,7 +5476,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", @@ -6602,9 +5483,9 @@ } }, "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "dev": true, "dependencies": { "bytes": "3.1.2", @@ -6615,7 +5496,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -6678,9 +5559,9 @@ } }, "node_modules/browserslist": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", - "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.0.tgz", + "integrity": "sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==", "dev": true, "funding": [ { @@ -6697,10 +5578,10 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001587", - "electron-to-chromium": "^1.4.668", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.13" + "caniuse-lite": "^1.0.30001663", + "electron-to-chromium": "^1.5.28", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.0" }, "bin": { "browserslist": "cli.js" @@ -6713,7 +5594,6 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, "funding": [ { "type": "github", @@ -6736,8 +5616,22 @@ "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/bytes": { "version": "3.1.2", @@ -6749,9 +5643,9 @@ } }, "node_modules/cacache": { - "version": "18.0.3", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.3.tgz", - "integrity": "sha512-qXCd4rh6I07cnDqh8V48/94Tc/WSfj+o3Gn6NZ0aZovS255bUx8O13uKxRFd2eWG0xgsco7+YItQNPaa5E85hg==", + "version": "18.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.4.tgz", + "integrity": "sha512-B+L5iIa9mgcjLbliir2th36yEwPftrzteHYujzsx3dFP/31GCHcIeS8f5MGd80odLOjaOvSpU3EEAmRQptkxLQ==", "dev": true, "dependencies": { "@npmcli/fs": "^3.1.0", @@ -6772,35 +5666,30 @@ } }, "node_modules/cacache/node_modules/glob": { - "version": "10.3.12", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", - "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "dev": true, "dependencies": { "foreground-child": "^3.1.0", - "jackspeak": "^2.3.6", - "minimatch": "^9.0.1", - "minipass": "^7.0.4", - "path-scurry": "^1.10.2" + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/cacache/node_modules/lru-cache": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", - "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", - "dev": true, - "engines": { - "node": "14 || >=16.14" - } + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true }, "node_modules/call-bind": { "version": "1.0.7", @@ -6830,19 +5719,10 @@ "node": ">=6" } }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/caniuse-lite": { - "version": "1.0.30001616", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001616.tgz", - "integrity": "sha512-RHVYKov7IcdNjVHJFNY/78RdG4oGVjbayxv8u5IO74Wv7Hlq4PnJE6mo/OjFijjVFNy5ijnCt6H3IIo4t+wfEw==", + "version": "1.0.30001664", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001664.tgz", + "integrity": "sha512-AmE7k4dXiNKQipgn7a2xg558IRqPN3jMQY/rOsbxDhrd0tyChwbITBfiwtnqz8bi2M5mIWbxAYBvk7W7QBUS2g==", "dev": true, "funding": [ { @@ -6913,9 +5793,9 @@ } }, "node_modules/chrome-trace-event": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", - "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", "dev": true, "engines": { "node": ">=6.0" @@ -6931,21 +5811,24 @@ } }, "node_modules/cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", "dev": true, "dependencies": { - "restore-cursor": "^3.1.0" + "restore-cursor": "^5.0.0" }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/cli-spinners": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.6.1.tgz", - "integrity": "sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==", + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", "dev": true, "engines": { "node": ">=6" @@ -6954,6 +5837,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "dev": true, + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cli-width": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", @@ -7010,6 +5909,35 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/cliui/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/cliui/node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -7071,24 +5999,21 @@ "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "dev": true }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true }, + "node_modules/commist": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/commist/-/commist-1.1.0.tgz", + "integrity": "sha512-rraC8NXWOEjhADbZe9QBNzLAN5Q3fsTPQtBV+fEVj6xKIgDgNiEVE6ZNfHpZOqfQ21YUzfVNUXLOEZquYvQPPg==", + "dependencies": { + "leven": "^2.1.0", + "minimist": "^1.1.0" + } + }, "node_modules/common-path-prefix": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", @@ -7167,8 +6092,21 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } }, "node_modules/connect": { "version": "3.7.0", @@ -7237,9 +6175,9 @@ "dev": true }, "node_modules/cookie": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", - "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "dev": true, "engines": { "node": ">= 0.6" @@ -7264,20 +6202,20 @@ } }, "node_modules/copy-webpack-plugin": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz", - "integrity": "sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==", + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-12.0.2.tgz", + "integrity": "sha512-SNwdBeHyII+rWvee/bTnAYyO8vfVdcSTud4EIb6jcZ8inLeWucJE0DnxXQBjlQ5zlteuuvooGQy3LIyGxhvlOA==", "dev": true, "dependencies": { - "fast-glob": "^3.2.11", + "fast-glob": "^3.3.2", "glob-parent": "^6.0.1", - "globby": "^13.1.1", + "globby": "^14.0.0", "normalize-path": "^3.0.0", - "schema-utils": "^4.0.0", - "serialize-javascript": "^6.0.0" + "schema-utils": "^4.2.0", + "serialize-javascript": "^6.0.2" }, "engines": { - "node": ">= 14.15.0" + "node": ">= 18.12.0" }, "funding": { "type": "opencollective", @@ -7299,44 +6237,13 @@ "node": ">=10.13.0" } }, - "node_modules/copy-webpack-plugin/node_modules/globby": { - "version": "13.2.2", - "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", - "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==", - "dev": true, - "dependencies": { - "dir-glob": "^3.0.1", - "fast-glob": "^3.3.0", - "ignore": "^5.2.4", - "merge2": "^1.4.1", - "slash": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/copy-webpack-plugin/node_modules/slash": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", - "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/core-js-compat": { - "version": "3.37.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.0.tgz", - "integrity": "sha512-vYq4L+T8aS5UuFg4UwDhc7YNRWVeVZwltad9C/jV3R2LgVOpS9BDr7l/WL6BN0dbV3k1XejPTHqqEzJgsa0frA==", + "version": "3.38.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.38.1.tgz", + "integrity": "sha512-JRH6gfXxGmrzF3tZ57lFx97YARxCXPaMzPo6jELZhv88pBH5VXpQ+y0znKGlFnzuaihqhLbefxSJxWJMPtfDzw==", "dev": true, "dependencies": { - "browserslist": "^4.23.0" + "browserslist": "^4.23.3" }, "funding": { "type": "opencollective", @@ -7376,40 +6283,22 @@ "engines": { "node": ">=14" }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/cosmiconfig/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/cosmiconfig/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" + "funding": { + "url": "https://github.com/sponsors/d-fischer" }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/critters": { - "version": "0.0.22", - "resolved": "https://registry.npmjs.org/critters/-/critters-0.0.22.tgz", - "integrity": "sha512-NU7DEcQZM2Dy8XTKFHxtdnIM/drE312j2T4PCVaSUcS0oBeyT/NImpRw/Ap0zOr/1SE7SgPK9tGPg1WK/sVakw==", + "version": "0.0.24", + "resolved": "https://registry.npmjs.org/critters/-/critters-0.0.24.tgz", + "integrity": "sha512-Oyqew0FGM0wYUSNqR0L6AteO5MpMoUU0rhKRieXeiKs+PmRTxiJMyaunYB2KF6fQ3dzChXKCpbFOEJx3OQ1v/Q==", "dev": true, "dependencies": { "chalk": "^4.1.0", @@ -7506,22 +6395,22 @@ } }, "node_modules/css-loader": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.10.0.tgz", - "integrity": "sha512-LTSA/jWbwdMlk+rhmElbDR2vbtQoTBPr7fkJE+mxrHj+7ru0hUmHafDRzWIjIHTwpitWVaqY2/UWGRca3yUgRw==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.2.tgz", + "integrity": "sha512-6WvYYn7l/XEGN8Xu2vWFt9nVzrCn39vKyTEFf/ExEyoksJjjSZV/0/35XPlMbpnr6VGhZIUg5yJrL8tGfes/FA==", "dev": true, "dependencies": { "icss-utils": "^5.1.0", "postcss": "^8.4.33", - "postcss-modules-extract-imports": "^3.0.0", - "postcss-modules-local-by-default": "^4.0.4", - "postcss-modules-scope": "^3.1.1", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", "postcss-modules-values": "^4.0.0", "postcss-value-parser": "^4.2.0", "semver": "^7.5.4" }, "engines": { - "node": ">= 12.13.0" + "node": ">= 18.12.0" }, "funding": { "type": "opencollective", @@ -7529,7 +6418,7 @@ }, "peerDependencies": { "@rspack/core": "0.x || 1.x", - "webpack": "^5.0.0" + "webpack": "^5.27.0" }, "peerDependenciesMeta": { "@rspack/core": { @@ -7596,12 +6485,11 @@ } }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -7618,6 +6506,34 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "node_modules/default-browser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "dev": true, + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/default-gateway": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", @@ -7660,21 +6576,15 @@ } }, "node_modules/define-lazy-prop": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", - "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", "dev": true, "engines": { - "node": ">=0.4.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/depd": { @@ -7705,6 +6615,15 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/detect-node": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", @@ -7717,27 +6636,6 @@ "integrity": "sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==", "dev": true }, - "node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", @@ -7835,33 +6733,17 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, - "node_modules/dotenv": { - "version": "16.3.2", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.2.tgz", - "integrity": "sha512-HTlk5nmhkm8F6JcdXvHIzaorzCoziNQT9mGxLPVXW8wJF1TiGSL60ZGB4gHWabHOaMmWmhvk2/lPHfnBiT78AQ==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/motdotla/dotenv?sponsor=1" - } - }, - "node_modules/dotenv-expand": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz", - "integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==", - "dev": true, - "engines": { - "node": ">=12" + "node_modules/duplexify": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" } }, - "node_modules/duplexer": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", - "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", - "dev": true - }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -7874,31 +6756,16 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "dev": true }, - "node_modules/ejs": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", - "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", - "dev": true, - "dependencies": { - "jake": "^10.8.5" - }, - "bin": { - "ejs": "bin/cli.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/electron-to-chromium": { - "version": "1.4.757", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.757.tgz", - "integrity": "sha512-jftDaCknYSSt/+KKeXzH3LX5E2CvRLm75P3Hj+J/dv3CL0qUYcOt13d5FN1NiL5IJbbhzHrb3BomeG2tkSlZmw==", + "version": "1.5.30", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.30.tgz", + "integrity": "sha512-sXI35EBN4lYxzc/pIGorlymYNzDBOqkSlVRe6MkgBsW/hW1tpC/HDJ2fjG7XnjakzfLEuvdmux0Mjs6jHq4UOA==", "dev": true }, "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", "dev": true }, "node_modules/emojis-list": { @@ -7946,15 +6813,14 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, "dependencies": { "once": "^1.4.0" } }, "node_modules/engine.io": { - "version": "6.5.5", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.5.tgz", - "integrity": "sha512-C5Pn8Wk+1vKBoHghJODM63yk8MvrO9EWZUfkAt5HAqIgPE4/8FF0PEGHXtEd40l223+cE5ABWuPzm38PHFXfMA==", + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz", + "integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==", "dev": true, "dependencies": { "@types/cookie": "^0.4.1", @@ -7962,7 +6828,7 @@ "@types/node": ">=10.0.0", "accepts": "~1.3.4", "base64id": "2.0.0", - "cookie": "~0.4.1", + "cookie": "~0.7.2", "cors": "~2.8.5", "debug": "~4.3.1", "engine.io-parser": "~5.2.1", @@ -7973,18 +6839,39 @@ } }, "node_modules/engine.io-parser": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz", - "integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "dev": true, "engines": { "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } } }, "node_modules/enhanced-resolve": { - "version": "5.16.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.16.0.tgz", - "integrity": "sha512-O+QWCviPNSSLAD9Ucn8Awv+poAkqn3T1XY5/N7kR7rQO9yfSGWkYZDwpJ+iKF7B8rxaQKWngSqACpgzeapSyoA==", + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", + "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", "dev": true, "dependencies": { "graceful-fs": "^4.2.4", @@ -7995,22 +6882,29 @@ } }, "node_modules/enquirer": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", - "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", "dev": true, "dependencies": { - "ansi-colors": "^4.1.1" + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" }, "engines": { "node": ">=8.6" } }, "node_modules/ent": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", - "integrity": "sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==", - "dev": true + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.1.tgz", + "integrity": "sha512-QHuXVeZx9d+tIQAz/XztU0ZwZf2Agg9CcXcgE1rurqvdBeDBrpSwjl8/6XUqMg7tw2Y7uAdKb2sRv+bSEFqQ5A==", + "dev": true, + "dependencies": { + "punycode": "^1.4.1" + }, + "engines": { + "node": ">= 0.4" + } }, "node_modules/entities": { "version": "4.5.0", @@ -8033,6 +6927,18 @@ "node": ">=6" } }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/err-code": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", @@ -8083,66 +6989,66 @@ } }, "node_modules/es-module-lexer": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.2.tgz", - "integrity": "sha512-l60ETUTmLqbVbVHv1J4/qj+M8nq7AwMzEcg3kmJDt9dCNrTk+yHcYFf/Kw75pMDwd9mPcIGCG5LcS20SxYRzFA==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", + "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", "dev": true }, "node_modules/esbuild": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.1.tgz", - "integrity": "sha512-OJwEgrpWm/PCMsLVWXKqvcjme3bHNpOgN7Tb6cQnR5n0TPbQx1/Xrn7rqM+wn17bYeT6MGB5sn1Bh5YiGi70nA==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.0.tgz", + "integrity": "sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA==", "dev": true, "hasInstallScript": true, - "optional": true, "bin": { "esbuild": "bin/esbuild" }, "engines": { - "node": ">=12" + "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.20.1", - "@esbuild/android-arm": "0.20.1", - "@esbuild/android-arm64": "0.20.1", - "@esbuild/android-x64": "0.20.1", - "@esbuild/darwin-arm64": "0.20.1", - "@esbuild/darwin-x64": "0.20.1", - "@esbuild/freebsd-arm64": "0.20.1", - "@esbuild/freebsd-x64": "0.20.1", - "@esbuild/linux-arm": "0.20.1", - "@esbuild/linux-arm64": "0.20.1", - "@esbuild/linux-ia32": "0.20.1", - "@esbuild/linux-loong64": "0.20.1", - "@esbuild/linux-mips64el": "0.20.1", - "@esbuild/linux-ppc64": "0.20.1", - "@esbuild/linux-riscv64": "0.20.1", - "@esbuild/linux-s390x": "0.20.1", - "@esbuild/linux-x64": "0.20.1", - "@esbuild/netbsd-x64": "0.20.1", - "@esbuild/openbsd-x64": "0.20.1", - "@esbuild/sunos-x64": "0.20.1", - "@esbuild/win32-arm64": "0.20.1", - "@esbuild/win32-ia32": "0.20.1", - "@esbuild/win32-x64": "0.20.1" + "@esbuild/aix-ppc64": "0.23.0", + "@esbuild/android-arm": "0.23.0", + "@esbuild/android-arm64": "0.23.0", + "@esbuild/android-x64": "0.23.0", + "@esbuild/darwin-arm64": "0.23.0", + "@esbuild/darwin-x64": "0.23.0", + "@esbuild/freebsd-arm64": "0.23.0", + "@esbuild/freebsd-x64": "0.23.0", + "@esbuild/linux-arm": "0.23.0", + "@esbuild/linux-arm64": "0.23.0", + "@esbuild/linux-ia32": "0.23.0", + "@esbuild/linux-loong64": "0.23.0", + "@esbuild/linux-mips64el": "0.23.0", + "@esbuild/linux-ppc64": "0.23.0", + "@esbuild/linux-riscv64": "0.23.0", + "@esbuild/linux-s390x": "0.23.0", + "@esbuild/linux-x64": "0.23.0", + "@esbuild/netbsd-x64": "0.23.0", + "@esbuild/openbsd-arm64": "0.23.0", + "@esbuild/openbsd-x64": "0.23.0", + "@esbuild/sunos-x64": "0.23.0", + "@esbuild/win32-arm64": "0.23.0", + "@esbuild/win32-ia32": "0.23.0", + "@esbuild/win32-x64": "0.23.0" } }, "node_modules/esbuild-wasm": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.20.1.tgz", - "integrity": "sha512-6v/WJubRsjxBbQdz6izgvx7LsVFvVaGmSdwrFHmEzoVgfXL89hkKPoQHsnVI2ngOkcBUQT9kmAM1hVL1k/Av4A==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.23.0.tgz", + "integrity": "sha512-6jP8UmWy6R6TUUV8bMuC3ZyZ6lZKI56x0tkxyCIqWwRRJ/DgeQKneh/Oid5EoGoPFLrGNkz47ZEtWAYuiY/u9g==", "dev": true, "bin": { "esbuild": "bin/esbuild" }, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, "engines": { "node": ">=6" @@ -8164,16 +7070,16 @@ } }, "node_modules/eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.0", - "@humanwhocodes/config-array": "^0.11.14", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", @@ -8231,13 +7137,13 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz", - "integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", + "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==", "dev": true, "dependencies": { "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.8.6" + "synckit": "^0.9.1" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -8261,9 +7167,9 @@ } }, "node_modules/eslint-scope": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.1.tgz", - "integrity": "sha512-pL8XjgP4ZOmmwfFE8mEhSxA7ZY4C+LWyqjQ3o4yWkkmD0qcMT9kkW3zWHOczhWcjTSgqycYAgwSlXvZltv65og==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.1.0.tgz", + "integrity": "sha512-14dSvlhaVhKKsa9Fx1l8A17s7ah7Ef7wCakJ10LYk6+GYmP9yDti2oq2SEwcyndt6knfcZyhyxwY3i9yL78EQw==", "dev": true, "dependencies": { "esrecurse": "^4.3.0", @@ -8276,6 +7182,30 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/eslint-visitor-keys": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", @@ -8319,12 +7249,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/eslint/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, "node_modules/eslint/node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -8397,22 +7321,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/eslint/node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -8434,94 +7342,37 @@ "type-fest": "^0.20.2" }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/eslint/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/eslint/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "node_modules/eslint/node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/eslint/node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" + "node": ">=8" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint/node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "node_modules/eslint/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "dependencies": { - "p-limit": "^3.0.2" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "*" } }, "node_modules/eslint/node_modules/supports-color": { @@ -8579,9 +7430,9 @@ } }, "node_modules/esquery": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", "dev": true, "dependencies": { "estraverse": "^5.1.0" @@ -8667,6 +7518,27 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/execa/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, "node_modules/exponential-backoff": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz", @@ -8674,37 +7546,37 @@ "dev": true }, "node_modules/express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", "dev": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.6.0", + "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -8716,9 +7588,9 @@ } }, "node_modules/express/node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "dev": true, "engines": { "node": ">= 0.6" @@ -8733,14 +7605,23 @@ "ms": "2.0.0" } }, + "node_modules/express/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/express/node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "dev": true, "dependencies": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", @@ -8786,18 +7667,6 @@ "node": ">=4" } }, - "node_modules/external-editor/node_modules/tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, - "dependencies": { - "os-tmpdir": "~1.0.2" - }, - "engines": { - "node": ">=0.6.0" - } - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -8838,6 +7707,12 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fast-uri": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.2.tgz", + "integrity": "sha512-GR6f0hD7XXyNJa25Tb9BuIdN0tdr+0BMi6/CJPH3wJO1JjNG3n/VsSw38AwRdKZABm8lGbPfakLRkYzx2V9row==", + "dev": true + }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", @@ -8859,21 +7734,6 @@ "node": ">=0.8.0" } }, - "node_modules/figures": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", - "dev": true, - "dependencies": { - "escape-string-regexp": "^1.0.5" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -8886,27 +7746,6 @@ "node": "^10.12.0 || >=12.0.0" } }, - "node_modules/filelist": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", - "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", - "dev": true, - "dependencies": { - "minimatch": "^5.0.1" - } - }, - "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -8981,16 +7820,19 @@ } }, "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "dependencies": { - "locate-path": "^5.0.0", + "locate-path": "^6.0.0", "path-exists": "^4.0.0" }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/flat": { @@ -9023,9 +7865,9 @@ "dev": true }, "node_modules/follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", "dev": true, "funding": [ { @@ -9043,9 +7885,9 @@ } }, "node_modules/foreground-child": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", - "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", "dev": true, "dependencies": { "cross-spawn": "^7.0.0", @@ -9058,32 +7900,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/foreground-child/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -9115,24 +7931,18 @@ "node": ">= 0.6" } }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "dev": true - }, "node_modules/fs-extra": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", - "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", "dev": true, "dependencies": { "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" }, "engines": { - "node": ">=14.14" + "node": ">=6 <7 || >=8" } }, "node_modules/fs-minipass": { @@ -9147,17 +7957,10 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/fs-monkey": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.6.tgz", - "integrity": "sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg==", - "dev": true - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "node_modules/fsevents": { "version": "2.3.3", @@ -9182,6 +7985,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "dev": true + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -9200,6 +8009,18 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-east-asian-width": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.2.0.tgz", + "integrity": "sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-intrinsic": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", @@ -9219,15 +8040,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "dev": true, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -9244,7 +8056,7 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, + "deprecated": "Glob versions prior to v9 are no longer supported", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -9282,7 +8094,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -9292,7 +8103,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -9310,20 +8120,20 @@ } }, "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.2.tgz", + "integrity": "sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==", "dev": true, "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.2", + "ignore": "^5.2.4", + "path-type": "^5.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.1.0" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -9437,6 +8247,15 @@ "node": ">= 0.4" } }, + "node_modules/help-me": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-3.0.0.tgz", + "integrity": "sha512-hx73jClhyk910sidBB7ERlnhMlFsJJIBqSVMFDwPN8o2v9nmp5KgLq1Xz1Bf1fCMMZ6mPrX159iG0VLy/fPMtQ==", + "dependencies": { + "glob": "^7.1.6", + "readable-stream": "^3.6.0" + } + }, "node_modules/hosted-git-info": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", @@ -9450,13 +8269,10 @@ } }, "node_modules/hosted-git-info/node_modules/lru-cache": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", - "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", - "dev": true, - "engines": { - "node": "14 || >=16.14" - } + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true }, "node_modules/hpack.js": { "version": "2.1.6", @@ -9612,33 +8428,26 @@ } }, "node_modules/http-proxy-middleware": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", - "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-3.0.0.tgz", + "integrity": "sha512-36AV1fIaI2cWRzHo+rbcxhe3M3jUDCNzc4D5zRl57sEWRAxdXYtw7FSQKYY6PDKssiAKjLYypbssHk+xs/kMXw==", "dev": true, "dependencies": { - "@types/http-proxy": "^1.17.8", + "@types/http-proxy": "^1.17.10", + "debug": "^4.3.4", "http-proxy": "^1.18.1", "is-glob": "^4.0.1", "is-plain-obj": "^3.0.0", - "micromatch": "^4.0.2" + "micromatch": "^4.0.5" }, "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "@types/express": "^4.17.13" - }, - "peerDependenciesMeta": { - "@types/express": { - "optional": true - } + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/https-proxy-agent": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", - "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", "dev": true, "dependencies": { "agent-base": "^7.0.2", @@ -9657,6 +8466,15 @@ "node": ">=10.17.0" } }, + "node_modules/hyperdyperid": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", + "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", + "dev": true, + "engines": { + "node": ">=10.18" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -9685,7 +8503,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, "funding": [ { "type": "github", @@ -9702,9 +8519,9 @@ ] }, "node_modules/ignore": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", - "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "engines": { "node": ">= 4" @@ -9736,9 +8553,9 @@ } }, "node_modules/immutable": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.5.tgz", - "integrity": "sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz", + "integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==", "dev": true }, "node_modules/import-fresh": { @@ -9757,15 +8574,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/import-fresh/node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -9788,7 +8596,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -9797,56 +8605,17 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/ini": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", - "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz", + "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==", "dev": true, "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/inquirer": { - "version": "9.2.15", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.15.tgz", - "integrity": "sha512-vI2w4zl/mDluHt9YEQ/543VTCwPKWiHzKtm9dM2V0NdFcqEexDAjUHzO1oA60HRNaVifGXXM1tRRNluLVHa0Kg==", - "dev": true, - "dependencies": { - "@ljharb/through": "^2.3.12", - "ansi-escapes": "^4.3.2", - "chalk": "^5.3.0", - "cli-cursor": "^3.1.0", - "cli-width": "^4.1.0", - "external-editor": "^3.1.0", - "figures": "^3.2.0", - "lodash": "^4.17.21", - "mute-stream": "1.0.0", - "ora": "^5.4.1", - "run-async": "^3.0.0", - "rxjs": "^7.8.1", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^6.2.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/inquirer/node_modules/chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", - "dev": true, - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/ip-address": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", @@ -9860,12 +8629,6 @@ "node": ">= 12" } }, - "node_modules/ip-address/node_modules/sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", - "dev": true - }, "node_modules/ipaddr.js": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", @@ -9894,27 +8657,30 @@ } }, "node_modules/is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", "dev": true, "dependencies": { - "hasown": "^2.0.0" + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", "dev": true, "bin": { "is-docker": "cli.js" }, "engines": { - "node": ">=8" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -9930,12 +8696,15 @@ } }, "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", "dev": true, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/is-glob": { @@ -9950,6 +8719,24 @@ "node": ">=0.10.0" } }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-interactive": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", @@ -9965,6 +8752,18 @@ "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", "dev": true }, + "node_modules/is-network-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.1.0.tgz", + "integrity": "sha512-tUdRRAnhT+OtCZR/LxZelH/C7QtjtFrTu5tXCA8pl55eTUElUHT+GPYV8MBMBvea/j+NxQqVt3LbWMRir7Gx9g==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -10038,15 +8837,18 @@ "dev": true }, "node_modules/is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", "dev": true, "dependencies": { - "is-docker": "^2.0.0" + "is-inside-container": "^1.0.0" }, "engines": { - "node": ">=8" + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/isarray": { @@ -10082,310 +8884,46 @@ "node": ">=0.10.0" } }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-instrument": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", - "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", - "dev": true, - "dependencies": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-report/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-report/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-source-maps": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", - "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", - "dev": true, - "dependencies": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-source-maps/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/istanbul-reports": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", - "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", - "dev": true, - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jackspeak": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", - "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", - "dev": true, - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/jake": { - "version": "10.9.1", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.1.tgz", - "integrity": "sha512-61btcOHNnLnsOdtLgA5efqQWjnSi/vow5HbI7HMdKKWqvrKR1bLK3BPlJn9gcSaP2ewuamUSMB5XEy76KUIS2w==", - "dev": true, - "dependencies": { - "async": "^3.2.3", - "chalk": "^4.0.2", - "filelist": "^1.0.4", - "minimatch": "^3.1.2" - }, - "bin": { - "jake": "bin/cli.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jake/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jake/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/jake/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jake/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jake/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/jake/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/jake/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/jake/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jasmine-core": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.6.0.tgz", - "integrity": "sha512-O236+gd0ZXS8YAjFx8xKaJ94/erqUliEkJTDedyE7iHvv4ZVqi+q+8acJxu05/WJDKm512EUNn809In37nWlAQ==", - "dev": true - }, - "node_modules/jest-diff": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", - "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^29.6.3", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-diff/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, "engines": { "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/jest-diff/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", "dev": true, "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" }, "engines": { "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/jest-diff/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dev": true, "dependencies": { - "color-name": "~1.1.4" + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">=7.0.0" + "node": ">=10" } }, - "node_modules/jest-diff/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/jest-diff/node_modules/has-flag": { + "node_modules/istanbul-lib-report/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", @@ -10394,7 +8932,7 @@ "node": ">=8" } }, - "node_modules/jest-diff/node_modules/supports-color": { + "node_modules/istanbul-lib-report/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", @@ -10406,15 +8944,63 @@ "node": ">=8" } }, - "node_modules/jest-get-type": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", - "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jasmine-core": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.6.1.tgz", + "integrity": "sha512-VYz/BjjmC3klLJlLwA4Kw8ytk0zDSmbbDLNs794VnWmkcCB7I9aAL/D48VNQtmITyPvea2C3jdUMfc3kAoy0PQ==", + "dev": true + }, "node_modules/jest-worker": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", @@ -10454,14 +9040,23 @@ } }, "node_modules/jiti": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", - "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", + "version": "1.21.6", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", + "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==", "dev": true, "bin": { "jiti": "bin/jiti.js" } }, + "node_modules/js-sdsl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz", + "integrity": "sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -10469,13 +9064,12 @@ "dev": true }, "node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" + "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" @@ -10539,19 +9133,16 @@ } }, "node_modules/jsonc-parser": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", - "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", "dev": true }, "node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", "dev": true, - "dependencies": { - "universalify": "^2.0.0" - }, "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -10566,9 +9157,9 @@ ] }, "node_modules/karma": { - "version": "6.4.3", - "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.3.tgz", - "integrity": "sha512-LuucC/RE92tJ8mlCwqEoRWXP38UMAqpnq98vktmS9SznSoUPPUJQbc91dHcxcunROvfQjdORVA/YFviH+Xci9Q==", + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.4.tgz", + "integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==", "dev": true, "dependencies": { "@colors/colors": "1.5.0", @@ -10651,6 +9242,22 @@ "concat-map": "0.0.1" } }, + "node_modules/karma-coverage/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/karma-coverage/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -10663,6 +9270,15 @@ "node": "*" } }, + "node_modules/karma-coverage/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/karma-jasmine": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-5.1.0.tgz", @@ -10752,6 +9368,21 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/karma/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/karma/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/karma/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -10773,6 +9404,29 @@ "node": ">=0.10.0" } }, + "node_modules/karma/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/karma/node_modules/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "dev": true, + "engines": { + "node": ">=14.14" + } + }, "node_modules/karma/node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -10835,19 +9489,10 @@ "node": ">=0.10.0" } }, - "node_modules/klona": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", - "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, "node_modules/launch-editor": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.6.1.tgz", - "integrity": "sha512-eB/uXmFVpY4zezmGp5XtU21kwo7GBbKB+EQ+UZeWtGb9yAM5xt/Evk+lYH3eRNAtId+ej4u7TYPFZ07w4s7rRw==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.9.1.tgz", + "integrity": "sha512-Gcnl4Bd+hRO9P9icCP/RVVT2o8SFlPXofuCxvA2SaZuH45whSvf5p8x5oih5ftLiVhEI4sp5xDY+R+b3zJBh5w==", "dev": true, "dependencies": { "picocolors": "^1.0.0", @@ -10881,23 +9526,29 @@ } }, "node_modules/less-loader": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-11.1.0.tgz", - "integrity": "sha512-C+uDBV7kS7W5fJlUjq5mPBeBVhYpTIm5gB09APT9o3n/ILeaXVsiSFTbZpTJCJwQ/Crczfn3DmfQFwxYusWFug==", + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-12.2.0.tgz", + "integrity": "sha512-MYUxjSQSBUQmowc0l5nPieOYwMzGPUaTzB6inNW/bdPEG9zOL3eAAD1Qw5ZxSPk7we5dMojHwNODYMV1hq4EVg==", "dev": true, - "dependencies": { - "klona": "^2.0.4" - }, "engines": { - "node": ">= 14.15.0" + "node": ">= 18.12.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { + "@rspack/core": "0.x || 1.x", "less": "^3.5.0 || ^4.0.0", "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } } }, "node_modules/less/node_modules/make-dir": { @@ -10947,6 +9598,14 @@ "node": ">=0.10.0" } }, + "node_modules/leven": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", + "integrity": "sha512-nvVPLpIHUxCUoRLrFqTgSxXJ614d8AgQoWl7zPe/2VadE8+1dpU3LBhowRuBAcuwruWtOdD8oYC9jDNJjXDPyA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -10966,24 +9625,125 @@ "integrity": "sha512-771TFWFD70G1wLTC4oU2Cw4qvtmNrIw+wRvBtn+okgHl7slJVi7zfNcdmqDL72BojM30VNJ2UHylr1o77U37Jw==", "dev": true, "dependencies": { - "webpack-sources": "^3.0.0" + "webpack-sources": "^3.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-sources": { + "optional": true + } + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/listr2": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.4.tgz", + "integrity": "sha512-opevsywziHd3zHCVQGAj8zu+Z3yHNkkoYhWIGnq54RrCVwLz0MozotJEDnKsIBLvkfLGN6BLOyAeRrYI0pKA4g==", + "dev": true, + "dependencies": { + "cli-truncate": "^4.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/listr2/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/listr2/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/listr2/node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true + }, + "node_modules/listr2/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" }, - "peerDependenciesMeta": { - "webpack": { - "optional": true - }, - "webpack-sources": { - "optional": true - } + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/lines-and-columns": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-2.0.4.tgz", - "integrity": "sha512-wM1+Z03eypVAVUCE7QdSqpVIvelbOakn1M0bPDoA4SGWPx3sNDVUiMo3L6To6WWGClB7VyXnhQ4Sn7gxiJbE6A==", + "node_modules/lmdb": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-3.0.13.tgz", + "integrity": "sha512-UGe+BbaSUQtAMZobTb4nHvFMrmvuAQKSeaqAX2meTEQjfsbpl5sxdHD8T72OnwD4GU9uwNhYXIVe4QGs8N9Zyw==", "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "hasInstallScript": true, + "dependencies": { + "msgpackr": "^1.10.2", + "node-addon-api": "^6.1.0", + "node-gyp-build-optional-packages": "5.2.2", + "ordered-binary": "^1.4.1", + "weak-lru-cache": "^1.2.2" + }, + "bin": { + "download-lmdb-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@lmdb/lmdb-darwin-arm64": "3.0.13", + "@lmdb/lmdb-darwin-x64": "3.0.13", + "@lmdb/lmdb-linux-arm": "3.0.13", + "@lmdb/lmdb-linux-arm64": "3.0.13", + "@lmdb/lmdb-linux-x64": "3.0.13", + "@lmdb/lmdb-win32-x64": "3.0.13" } }, "node_modules/loader-runner": { @@ -10996,24 +9756,27 @@ } }, "node_modules/loader-utils": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz", - "integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.3.1.tgz", + "integrity": "sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==", "dev": true, "engines": { "node": ">= 12.13.0" } }, "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "dependencies": { - "p-locate": "^4.1.0" + "p-locate": "^5.0.0" }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/lodash": { @@ -11034,6 +9797,12 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "dev": true + }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -11120,6 +9889,127 @@ "node": ">=8" } }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-escapes": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", + "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", + "dev": true, + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", + "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", + "dev": true, + "dependencies": { + "get-east-asian-width": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", + "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/log4js": { "version": "6.9.1", "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.9.1.tgz", @@ -11137,9 +10027,9 @@ } }, "node_modules/loglevel": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.1.tgz", - "integrity": "sha512-hP3I3kCrDIMuRwAwHltphhDM1r8i55H33GgqjXbrisuJhF4kRhW1dNuxsRklp4bXl8DSdLaNLuiL4A/LWRfxvg==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", + "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==", "dev": true, "engines": { "node": ">= 0.6.0" @@ -11224,15 +10114,12 @@ } }, "node_modules/magic-string": { - "version": "0.30.8", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz", - "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==", + "version": "0.30.11", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", + "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", "dev": true, "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" - }, - "engines": { - "node": ">=12" + "@jridgewell/sourcemap-codec": "^1.5.0" } }, "node_modules/make-dir": { @@ -11273,15 +10160,6 @@ "node": "^16.14.0 || >=18.0.0" } }, - "node_modules/make-fetch-happen/node_modules/proc-log": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", - "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", - "dev": true, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -11292,22 +10170,32 @@ } }, "node_modules/memfs": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", - "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.12.0.tgz", + "integrity": "sha512-74wDsex5tQDSClVkeK1vtxqYCAgCoXxx+K4NSHzgU/muYVYByFqa+0RnrPO9NM6naWm1+G9JmZ0p6QHhXmeYfA==", "dev": true, "dependencies": { - "fs-monkey": "^1.0.4" + "@jsonjoy.com/json-pack": "^1.0.3", + "@jsonjoy.com/util": "^1.3.0", + "tree-dump": "^1.0.1", + "tslib": "^2.0.0" }, "engines": { "node": ">= 4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" } }, "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", - "dev": true + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/merge-stream": { "version": "2.0.0", @@ -11334,12 +10222,12 @@ } }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -11400,10 +10288,22 @@ "node": ">=6" } }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mini-css-extract-plugin": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.8.1.tgz", - "integrity": "sha512-/1HDlyFRxWIZPI1ZpgqlZ8jMw/1Dp/dl3P0L1jtZ+zVcHqwPhGwaJwKL00WVgfnBy6PWCde9W65or7IIETImuA==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.0.tgz", + "integrity": "sha512-Zs1YsZVfemekSZG+44vBsYTLQORkPMwnlv+aehcxK/NLKC+EGhDB39/YePYYqx/sTk6NnYpuqikhSn7+JIevTA==", "dev": true, "dependencies": { "schema-utils": "^4.0.0", @@ -11427,9 +10327,9 @@ "dev": true }, "node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, "dependencies": { "brace-expansion": "^2.0.1" @@ -11445,15 +10345,14 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/minipass": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.0.tgz", - "integrity": "sha512-oGZRv2OT1lO2UF1zUcwdTb3wqUwI0kBGTgt/T7OdSj6M6N5m3o5uPf0AIW6lVxGGoiWUR7e2AwTE+xiwK8WQig==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "dev": true, "engines": { "node": ">=16 || 14 >=14.17" @@ -11518,34 +10417,6 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, - "node_modules/minipass-json-stream": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minipass-json-stream/-/minipass-json-stream-1.0.1.tgz", - "integrity": "sha512-ODqY18UZt/I8k+b7rl2AENgbWE8IDYam+undIJONvigAz8KR5GWblsFTEfQs0WODsjbSXWlm+JHEv8Gr6Tfdbg==", - "dev": true, - "dependencies": { - "jsonparse": "^1.3.1", - "minipass": "^3.0.0" - } - }, - "node_modules/minipass-json-stream/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-json-stream/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/minipass-pipeline": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", @@ -11649,6 +10520,72 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/mqtt": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-4.3.7.tgz", + "integrity": "sha512-ew3qwG/TJRorTz47eW46vZ5oBw5MEYbQZVaEji44j5lAUSQSqIEoul7Kua/BatBW0H0kKQcC9kwUHa1qzaWHSw==", + "dependencies": { + "commist": "^1.0.0", + "concat-stream": "^2.0.0", + "debug": "^4.1.1", + "duplexify": "^4.1.1", + "help-me": "^3.0.0", + "inherits": "^2.0.3", + "lru-cache": "^6.0.0", + "minimist": "^1.2.5", + "mqtt-packet": "^6.8.0", + "number-allocator": "^1.0.9", + "pump": "^3.0.0", + "readable-stream": "^3.6.0", + "reinterval": "^1.1.0", + "rfdc": "^1.3.0", + "split2": "^3.1.0", + "ws": "^7.5.5", + "xtend": "^4.0.2" + }, + "bin": { + "mqtt": "bin/mqtt.js", + "mqtt_pub": "bin/pub.js", + "mqtt_sub": "bin/sub.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/mqtt-browser": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/mqtt-browser/-/mqtt-browser-4.3.7.tgz", + "integrity": "sha512-4pxHxa3avIILr2CXhTKlArVpATqfyTu4zr5u2PoUwzgw0GDr5dpzZ0pmPgZyOoQBVgrVDEboCzb/b1Q0yWOm7g==", + "dependencies": { + "mqtt": "4.3.7" + } + }, + "node_modules/mqtt-packet": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-6.10.0.tgz", + "integrity": "sha512-ja8+mFKIHdB1Tpl6vac+sktqy3gA8t9Mduom1BA75cI+R9AHnZOiaBQwpGiWnaVJLDGRdNhQmFaAqd7tkKSMGA==", + "dependencies": { + "bl": "^4.0.2", + "debug": "^4.1.1", + "process-nextick-args": "^2.0.1" + } + }, + "node_modules/mqtt/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mqtt/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, "node_modules/mrmime": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", @@ -11659,10 +10596,40 @@ } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/msgpackr": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.0.tgz", + "integrity": "sha512-I8qXuuALqJe5laEBYoFykChhSXLikZmUhccjGsPuSJ/7uPip2TJ7lwdIQwWSAi0jGZDXv4WOP8Qg65QZRuXxXw==", + "dev": true, + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", + "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" + } }, "node_modules/multicast-dns": { "version": "7.2.5", @@ -11768,6 +10735,20 @@ "@angular/forms": ">=14.0.0" } }, + "node_modules/ngx-mqtt": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/ngx-mqtt/-/ngx-mqtt-17.0.0.tgz", + "integrity": "sha512-54wVMyDOZkpTZEs0rTMWPP1Yz+6q3rRnHzIBnpqnBkDcyMfNrti45C7ijwnEIaPDzQHMOqVrDgh/6C4ocPPLJQ==", + "dependencies": { + "mqtt-browser": "4.3.7", + "mqtt-packet": "^6.10.0", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": ">=14", + "@angular/core": ">=14" + } + }, "node_modules/nice-napi": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz", @@ -11783,13 +10764,19 @@ "node-gyp-build": "^4.2.2" } }, - "node_modules/node-addon-api": { + "node_modules/nice-napi/node_modules/node-addon-api": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", "dev": true, "optional": true }, + "node_modules/node-addon-api": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", + "dev": true + }, "node_modules/node-forge": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", @@ -11800,9 +10787,9 @@ } }, "node_modules/node-gyp": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-10.1.0.tgz", - "integrity": "sha512-B4J5M1cABxPc5PwfjhbV5hoy2DP9p8lFXASnEN6hugXOa61416tnTZ29x9sSwAd0o99XNIcpvDDy1swAExsVKA==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-10.2.0.tgz", + "integrity": "sha512-sp3FonBAaFe4aYTcFdZUn2NYkbP7xroPGYvQmP4Nl5PxamznItBnNCgjrVTKrEfQynInMsJvZrdmqUnysCJ8rw==", "dev": true, "dependencies": { "env-paths": "^2.2.0", @@ -11811,9 +10798,9 @@ "graceful-fs": "^4.2.6", "make-fetch-happen": "^13.0.0", "nopt": "^7.0.0", - "proc-log": "^3.0.0", + "proc-log": "^4.1.0", "semver": "^7.3.5", - "tar": "^6.1.2", + "tar": "^6.2.1", "which": "^4.0.0" }, "bin": { @@ -11824,9 +10811,9 @@ } }, "node_modules/node-gyp-build": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.1.tgz", - "integrity": "sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==", + "version": "4.8.2", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.2.tgz", + "integrity": "sha512-IRUxE4BVsHWXkV/SFOut4qTlagw2aM8T5/vnTsmrHJvVoKueJHRc/JaFND7QDDc61kLYUJ6qlZM3sqTSyx2dTw==", "dev": true, "optional": true, "bin": { @@ -11835,24 +10822,36 @@ "node-gyp-build-test": "build-test.js" } }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "dev": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, "node_modules/node-gyp/node_modules/glob": { - "version": "10.3.12", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", - "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "dev": true, "dependencies": { "foreground-child": "^3.1.0", - "jackspeak": "^2.3.6", - "minimatch": "^9.0.1", - "minipass": "^7.0.4", - "path-scurry": "^1.10.2" + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, "funding": { "url": "https://github.com/sponsors/isaacs" } @@ -11881,16 +10880,10 @@ "node": "^16.13.0 || >=18.0.0" } }, - "node_modules/node-machine-id": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/node-machine-id/-/node-machine-id-1.1.12.tgz", - "integrity": "sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==", - "dev": true - }, "node_modules/node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", "dev": true }, "node_modules/nopt": { @@ -11909,1217 +10902,1495 @@ } }, "node_modules/normalize-package-data": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.1.tgz", - "integrity": "sha512-6rvCfeRW+OEZagAB4lMLSNuTNYZWLVtKccK79VSTf//yTY5VOCgcpH80O+bZK8Neps7pUnd5G+QlMg1yV/2iZQ==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", + "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", "dev": true, "dependencies": { "hosted-git-info": "^7.0.0", - "is-core-module": "^2.8.1", "semver": "^7.3.5", "validate-npm-package-license": "^3.0.4" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-bundled": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-3.0.1.tgz", + "integrity": "sha512-+AvaheE/ww1JEwRHOrn4WHNzOxGtVp+adrg2AeZS/7KuxGUYFuBta98wYpfHBbJp6Tg6j1NKSEVHNcfZzJHQwQ==", + "dev": true, + "dependencies": { + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-install-checks": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-6.3.0.tgz", + "integrity": "sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==", + "dev": true, + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz", + "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-package-arg": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-11.0.3.tgz", + "integrity": "sha512-sHGJy8sOC1YraBywpzQlIKBE4pBbGbiF95U6Auspzyem956E0+FtDtsx1ZxlOJkQCZ1AFXAY/yuvtFYrOxF+Bw==", + "dev": true, + "dependencies": { + "hosted-git-info": "^7.0.0", + "proc-log": "^4.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^5.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm-packlist": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-8.0.2.tgz", + "integrity": "sha512-shYrPFIS/JLP4oQmAwDyk5HcyysKW8/JLTEA32S0Z5TzvpaeeX2yMFfoK1fjEBnCBvVyIB/Jj/GBFdm0wsgzbA==", + "dev": true, + "dependencies": { + "ignore-walk": "^6.0.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-pick-manifest": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-9.1.0.tgz", + "integrity": "sha512-nkc+3pIIhqHVQr085X9d2JzPzLyjzQS96zbruppqC9aZRm/x8xx6xhI98gHtsfELP2bE+loHq8ZaHFHhe+NauA==", + "dev": true, + "dependencies": { + "npm-install-checks": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0", + "npm-package-arg": "^11.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm-registry-fetch": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-17.1.0.tgz", + "integrity": "sha512-5+bKQRH0J1xG1uZ1zMNvxW0VEyoNWgJpY9UDuluPFLKDfJ9u2JmmjmTJV1srBGQOROfdBMiVvnH2Zvpbm+xkVA==", + "dev": true, + "dependencies": { + "@npmcli/redact": "^2.0.0", + "jsonparse": "^1.3.1", + "make-fetch-happen": "^13.0.0", + "minipass": "^7.0.2", + "minipass-fetch": "^3.0.0", + "minizlib": "^2.1.2", + "npm-package-arg": "^11.0.0", + "proc-log": "^4.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/number-allocator": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/number-allocator/-/number-allocator-1.0.14.tgz", + "integrity": "sha512-OrL44UTVAvkKdOdRQZIJpLkAdjXGTRda052sN4sO77bKEzYYqWKMBjQvrJFzqygI99gL6Z4u2xctPW1tB8ErvA==", + "dependencies": { + "debug": "^4.3.1", + "js-sdsl": "4.3.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz", + "integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==", + "dev": true, + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "node_modules/ora/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, "engines": { - "node": ">=0.10.0" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "node_modules/ora/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, "engines": { - "node": ">=0.10.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/npm-bundled": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-3.0.1.tgz", - "integrity": "sha512-+AvaheE/ww1JEwRHOrn4WHNzOxGtVp+adrg2AeZS/7KuxGUYFuBta98wYpfHBbJp6Tg6j1NKSEVHNcfZzJHQwQ==", + "node_modules/ora/node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", "dev": true, "dependencies": { - "npm-normalize-package-bin": "^3.0.0" + "restore-cursor": "^3.1.0" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": ">=8" } }, - "node_modules/npm-install-checks": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-6.3.0.tgz", - "integrity": "sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==", + "node_modules/ora/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "dependencies": { - "semver": "^7.1.1" + "color-name": "~1.1.4" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": ">=7.0.0" } }, - "node_modules/npm-normalize-package-bin": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz", - "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==", - "dev": true, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } + "node_modules/ora/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, - "node_modules/npm-package-arg": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-11.0.1.tgz", - "integrity": "sha512-M7s1BD4NxdAvBKUPqqRW957Xwcl/4Zvo8Aj+ANrzvIPzGJZElrH7Z//rSaec2ORcND6FHHLnZeY8qgTpXDMFQQ==", + "node_modules/ora/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, - "dependencies": { - "hosted-git-info": "^7.0.0", - "proc-log": "^3.0.0", - "semver": "^7.3.5", - "validate-npm-package-name": "^5.0.0" - }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": ">=8" } }, - "node_modules/npm-packlist": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-8.0.2.tgz", - "integrity": "sha512-shYrPFIS/JLP4oQmAwDyk5HcyysKW8/JLTEA32S0Z5TzvpaeeX2yMFfoK1fjEBnCBvVyIB/Jj/GBFdm0wsgzbA==", + "node_modules/ora/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "dev": true, "dependencies": { - "ignore-walk": "^6.0.4" + "mimic-fn": "^2.1.0" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/npm-pick-manifest": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-9.0.0.tgz", - "integrity": "sha512-VfvRSs/b6n9ol4Qb+bDwNGUXutpy76x6MARw/XssevE0TnctIKcmklJZM5Z7nqs5z5aW+0S63pgCNbpkUNNXBg==", + "node_modules/ora/node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", "dev": true, "dependencies": { - "npm-install-checks": "^6.0.0", - "npm-normalize-package-bin": "^3.0.0", - "npm-package-arg": "^11.0.0", - "semver": "^7.3.5" + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": ">=8" } }, - "node_modules/npm-registry-fetch": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-16.2.1.tgz", - "integrity": "sha512-8l+7jxhim55S85fjiDGJ1rZXBWGtRLi1OSb4Z3BPLObPuIaeKRlPRiYMSHU4/81ck3t71Z+UwDDl47gcpmfQQA==", + "node_modules/ora/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/ora/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "dependencies": { - "@npmcli/redact": "^1.1.0", - "make-fetch-happen": "^13.0.0", - "minipass": "^7.0.2", - "minipass-fetch": "^3.0.0", - "minipass-json-stream": "^1.0.1", - "minizlib": "^2.1.2", - "npm-package-arg": "^11.0.0", - "proc-log": "^4.0.0" + "has-flag": "^4.0.0" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": ">=8" } }, - "node_modules/npm-registry-fetch/node_modules/proc-log": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", - "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", + "node_modules/ordered-binary": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.5.2.tgz", + "integrity": "sha512-JTo+4+4Fw7FreyAvlSLjb1BBVaxEQAacmjD3jjuyPZclpbEghTvQZbXBb2qPd2LeIMxiHwXBZUcpmG2Gl/mDEA==", + "dev": true + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", "dev": true, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": ">=0.10.0" } }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, "dependencies": { - "path-key": "^3.0.0" + "yocto-queue": "^0.1.0" }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "dependencies": { - "boolbase": "^1.0.0" + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" }, "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/nx": { - "version": "17.2.8", - "resolved": "https://registry.npmjs.org/nx/-/nx-17.2.8.tgz", - "integrity": "sha512-rM5zXbuXLEuqQqcjVjClyvHwRJwt+NVImR2A6KFNG40Z60HP6X12wAxxeLHF5kXXTDRU0PFhf/yACibrpbPrAw==", + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", "dev": true, - "hasInstallScript": true, "dependencies": { - "@nrwl/tao": "17.2.8", - "@yarnpkg/lockfile": "^1.1.0", - "@yarnpkg/parsers": "3.0.0-rc.46", - "@zkochan/js-yaml": "0.0.6", - "axios": "^1.5.1", - "chalk": "^4.1.0", - "cli-cursor": "3.1.0", - "cli-spinners": "2.6.1", - "cliui": "^8.0.1", - "dotenv": "~16.3.1", - "dotenv-expand": "~10.0.0", - "enquirer": "~2.3.6", - "figures": "3.2.0", - "flat": "^5.0.2", - "fs-extra": "^11.1.0", - "glob": "7.1.4", - "ignore": "^5.0.4", - "jest-diff": "^29.4.1", - "js-yaml": "4.1.0", - "jsonc-parser": "3.2.0", - "lines-and-columns": "~2.0.3", - "minimatch": "3.0.5", - "node-machine-id": "1.1.12", - "npm-run-path": "^4.0.1", - "open": "^8.4.0", - "semver": "7.5.3", - "string-width": "^4.2.3", - "strong-log-transformer": "^2.1.0", - "tar-stream": "~2.2.0", - "tmp": "~0.2.1", - "tsconfig-paths": "^4.1.2", - "tslib": "^2.3.0", - "yargs": "^17.6.2", - "yargs-parser": "21.1.1" - }, - "bin": { - "nx": "bin/nx.js", - "nx-cloud": "bin/nx-cloud.js" - }, - "optionalDependencies": { - "@nx/nx-darwin-arm64": "17.2.8", - "@nx/nx-darwin-x64": "17.2.8", - "@nx/nx-freebsd-x64": "17.2.8", - "@nx/nx-linux-arm-gnueabihf": "17.2.8", - "@nx/nx-linux-arm64-gnu": "17.2.8", - "@nx/nx-linux-arm64-musl": "17.2.8", - "@nx/nx-linux-x64-gnu": "17.2.8", - "@nx/nx-linux-x64-musl": "17.2.8", - "@nx/nx-win32-arm64-msvc": "17.2.8", - "@nx/nx-win32-x64-msvc": "17.2.8" + "aggregate-error": "^3.0.0" }, - "peerDependencies": { - "@swc-node/register": "^1.6.7", - "@swc/core": "^1.3.85" + "engines": { + "node": ">=10" }, - "peerDependenciesMeta": { - "@swc-node/register": { - "optional": true - }, - "@swc/core": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/nx/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/p-retry": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.0.tgz", + "integrity": "sha512-JA6nkq6hKyWLLasXQXUrO4z8BUZGUt/LjlJxx8Gb2+2ntodU/SS63YZ8b0LUTbQ8ZB9iwOfhEPhg4ykKnn2KsA==", "dev": true, "dependencies": { - "color-convert": "^2.0.1" + "@types/retry": "0.12.2", + "is-network-error": "^1.0.0", + "retry": "^0.13.1" }, "engines": { - "node": ">=8" + "node": ">=16.17" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/nx/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/nx/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "node_modules/p-retry/node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "engines": { + "node": ">= 4" } }, - "node_modules/nx/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true + }, + "node_modules/pacote": { + "version": "18.0.6", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-18.0.6.tgz", + "integrity": "sha512-+eK3G27SMwsB8kLIuj4h1FUhHtwiEUo21Tw8wNjmvdlpOEr613edv+8FUsTj/4F/VN5ywGE19X18N7CC2EJk6A==", "dev": true, "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "@npmcli/git": "^5.0.0", + "@npmcli/installed-package-contents": "^2.0.1", + "@npmcli/package-json": "^5.1.0", + "@npmcli/promise-spawn": "^7.0.0", + "@npmcli/run-script": "^8.0.0", + "cacache": "^18.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^7.0.2", + "npm-package-arg": "^11.0.0", + "npm-packlist": "^8.0.0", + "npm-pick-manifest": "^9.0.0", + "npm-registry-fetch": "^17.0.0", + "proc-log": "^4.0.0", + "promise-retry": "^2.0.1", + "sigstore": "^2.2.0", + "ssri": "^10.0.0", + "tar": "^6.1.11" + }, + "bin": { + "pacote": "bin/index.js" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": "^16.14.0 || >=18.0.0" } }, - "node_modules/nx/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, "dependencies": { - "color-name": "~1.1.4" + "callsites": "^3.0.0" }, "engines": { - "node": ">=7.0.0" + "node": ">=6" } }, - "node_modules/nx/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/nx/node_modules/glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", "dev": true, "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" }, "engines": { - "node": "*" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/nx/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/parse-json/node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/parse-node-version": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", + "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", "dev": true, "engines": { - "node": ">=8" + "node": ">= 0.10" } }, - "node_modules/nx/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, + "node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "devOptional": true, "dependencies": { - "argparse": "^2.0.1" + "entities": "^4.4.0" }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "node_modules/nx/node_modules/jsonc-parser": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", - "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", - "dev": true - }, - "node_modules/nx/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "node_modules/parse5-html-rewriting-stream": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-7.0.0.tgz", + "integrity": "sha512-mazCyGWkmCRWDI15Zp+UiCqMp/0dgEmkZRvhlsqqKYr4SsVm/TvnSpD9fCvqCA2zoWJcfRym846ejWBBHRiYEg==", "dev": true, "dependencies": { - "yallist": "^4.0.0" + "entities": "^4.3.0", + "parse5": "^7.0.0", + "parse5-sax-parser": "^7.0.0" }, - "engines": { - "node": ">=10" + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "node_modules/nx/node_modules/minimatch": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.5.tgz", - "integrity": "sha512-tUpxzX0VAzJHjLu0xUfFv1gwVp9ba3IOuRAVH2EGuRW8a5emA2FlACLqiT/lDVtS1W+TGNwqz3sWaNyLgDJWuw==", + "node_modules/parse5-sax-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-7.0.0.tgz", + "integrity": "sha512-5A+v2SNsq8T6/mG3ahcz8ZtQ0OUFTatxPbeidoMB7tkJSGDY3tdfl4MHovtLQHkEn5CGxijNWRQHhRQ6IRpXKg==", "dev": true, "dependencies": { - "brace-expansion": "^1.1.7" + "parse5": "^7.0.0" }, - "engines": { - "node": "*" + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "node_modules/nx/node_modules/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, "engines": { - "node": ">=10" + "node": ">= 0.8" } }, - "node_modules/nx/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, "engines": { "node": ">=8" } }, - "node_modules/nx/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "engines": { "node": ">=0.10.0" } }, - "node_modules/object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=8" } }, - "node_modules/obuf": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", - "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "dev": true, "dependencies": { - "ee-first": "1.1.1" + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { - "node": ">= 0.8" + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", - "dev": true, - "engines": { - "node": ">= 0.8" - } + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "dependencies": { - "wrappy": "1" - } + "node_modules/path-to-regexp": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", + "dev": true }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "node_modules/path-type": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz", + "integrity": "sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==", "dev": true, - "dependencies": { - "mimic-fn": "^2.1.0" - }, "engines": { - "node": ">=6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/open": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", - "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "node_modules/picocolors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "dev": true + }, + "node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, - "dependencies": { - "define-lazy-prop": "^2.0.0", - "is-docker": "^2.1.1", - "is-wsl": "^2.2.0" - }, "engines": { "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", "dev": true, - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, + "optional": true, "engines": { - "node": ">= 0.8.0" + "node": ">=6" } }, - "node_modules/ora": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", - "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "node_modules/piscina": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.6.1.tgz", + "integrity": "sha512-z30AwWGtQE+Apr+2WBZensP2lIvwoaMcOPkQlIEmSGMJNUvaYACylPYrQM6wSdUNJlnDVMSpLv7xTMJqlVshOA==", "dev": true, - "dependencies": { - "bl": "^4.1.0", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-spinners": "^2.5.0", - "is-interactive": "^1.0.0", - "is-unicode-supported": "^0.1.0", - "log-symbols": "^4.1.0", - "strip-ansi": "^6.0.0", - "wcwidth": "^1.0.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "optionalDependencies": { + "nice-napi": "^1.0.2" } }, - "node_modules/ora/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/pkg-dir": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", + "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==", "dev": true, "dependencies": { - "color-convert": "^2.0.1" + "find-up": "^6.3.0" }, "engines": { - "node": ">=8" + "node": ">=14.16" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ora/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/pkg-dir/node_modules/find-up": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", "dev": true, "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" }, "engines": { - "node": ">=10" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ora/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", "dev": true, "dependencies": { - "color-name": "~1.1.4" + "p-locate": "^6.0.0" }, "engines": { - "node": ">=7.0.0" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ora/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/ora/node_modules/has-flag": { + "node_modules/pkg-dir/node_modules/p-limit": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", "dev": true, + "dependencies": { + "yocto-queue": "^1.0.0" + }, "engines": { - "node": ">=8" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ora/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", "dev": true, "dependencies": { - "has-flag": "^4.0.0" + "p-limit": "^4.0.0" }, "engines": { - "node": ">=8" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "node_modules/pkg-dir/node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", "dev": true, "engines": { - "node": ">=0.10.0" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, - "node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "node_modules/pkg-dir/node_modules/yocto-queue": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz", + "integrity": "sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==", "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, "engines": { - "node": ">=6" + "node": ">=12.20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "node_modules/postcss": { + "version": "8.4.41", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz", + "integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "dependencies": { - "p-limit": "^2.2.0" + "nanoid": "^3.3.7", + "picocolors": "^1.0.1", + "source-map-js": "^1.2.0" }, "engines": { - "node": ">=8" + "node": "^10 || ^12 || >=14" } }, - "node_modules/p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "node_modules/postcss-loader": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-8.1.1.tgz", + "integrity": "sha512-0IeqyAsG6tYiDRCYKQJLAmgQr47DX6N7sFSWvQxt6AcupX8DIdmykuk/o/tx0Lze3ErGHJEp5OSRxrelC6+NdQ==", "dev": true, "dependencies": { - "aggregate-error": "^3.0.0" + "cosmiconfig": "^9.0.0", + "jiti": "^1.20.0", + "semver": "^7.5.4" }, "engines": { - "node": ">=10" + "node": ">= 18.12.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-retry": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", - "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", - "dev": true, - "dependencies": { - "@types/retry": "0.12.0", - "retry": "^0.13.1" + "type": "opencollective", + "url": "https://opencollective.com/webpack" }, - "engines": { - "node": ">=8" + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "postcss": "^7.0.0 || ^8.0.1", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } } }, - "node_modules/p-retry/node_modules/retry": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", - "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "node_modules/postcss-media-query-parser": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", + "integrity": "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==", + "dev": true + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", "dev": true, "engines": { - "node": ">= 4" + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" } }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "node_modules/postcss-modules-local-by-default": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.5.tgz", + "integrity": "sha512-6MieY7sIfTK0hYfafw1OMEG+2bg8Q1ocHCpoWLqOKj3JXlKu4G7btkmM/B7lFubYkYWmRSPLZi5chid63ZaZYw==", "dev": true, + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + }, "engines": { - "node": ">=6" + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" } }, - "node_modules/pacote": { - "version": "17.0.4", - "resolved": "https://registry.npmjs.org/pacote/-/pacote-17.0.4.tgz", - "integrity": "sha512-eGdLHrV/g5b5MtD5cTPyss+JxOlaOloSMG3UwPMAvL8ywaLJ6beONPF40K4KKl/UI6q5hTKCJq5rCu8tkF+7Dg==", + "node_modules/postcss-modules-scope": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.0.tgz", + "integrity": "sha512-oq+g1ssrsZOsx9M96c5w8laRmvEu9C3adDSjI8oTcbfkrTE8hx/zfyobUoWIxaKPO8bt6S62kxpw5GqypEw1QQ==", "dev": true, "dependencies": { - "@npmcli/git": "^5.0.0", - "@npmcli/installed-package-contents": "^2.0.1", - "@npmcli/promise-spawn": "^7.0.0", - "@npmcli/run-script": "^7.0.0", - "cacache": "^18.0.0", - "fs-minipass": "^3.0.0", - "minipass": "^7.0.2", - "npm-package-arg": "^11.0.0", - "npm-packlist": "^8.0.0", - "npm-pick-manifest": "^9.0.0", - "npm-registry-fetch": "^16.0.0", - "proc-log": "^3.0.0", - "promise-retry": "^2.0.1", - "read-package-json": "^7.0.0", - "read-package-json-fast": "^3.0.0", - "sigstore": "^2.0.0", - "ssri": "^10.0.0", - "tar": "^6.1.11" - }, - "bin": { - "pacote": "lib/bin.js" + "postcss-selector-parser": "^6.0.4" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" } }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", "dev": true, "dependencies": { - "callsites": "^3.0.0" + "icss-utils": "^5.0.0" }, "engines": { - "node": ">=6" + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" } }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=4" } }, - "node_modules/parse-json/node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true - }, - "node_modules/parse-json/node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true - }, - "node_modules/parse-node-version": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", - "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, "engines": { - "node": ">= 0.10" + "node": ">= 0.8.0" } }, - "node_modules/parse5": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", - "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", - "devOptional": true, - "dependencies": { - "entities": "^4.4.0" + "node_modules/prettier": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" }, "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" + "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/parse5-html-rewriting-stream": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-7.0.0.tgz", - "integrity": "sha512-mazCyGWkmCRWDI15Zp+UiCqMp/0dgEmkZRvhlsqqKYr4SsVm/TvnSpD9fCvqCA2zoWJcfRym846ejWBBHRiYEg==", + "node_modules/prettier-eslint": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/prettier-eslint/-/prettier-eslint-13.0.0.tgz", + "integrity": "sha512-P5K31qWgUOQCtJL/3tpvEe28KfP49qbr6MTVEXC7I2k7ci55bP3YDr+glhyCdhIzxGCVp2f8eobfQ5so52RIIA==", "dev": true, "dependencies": { - "entities": "^4.3.0", - "parse5": "^7.0.0", - "parse5-sax-parser": "^7.0.0" + "@typescript-eslint/parser": "^3.0.0", + "common-tags": "^1.4.0", + "dlv": "^1.1.0", + "eslint": "^7.9.0", + "indent-string": "^4.0.0", + "lodash.merge": "^4.6.0", + "loglevel-colored-level-prefix": "^1.0.0", + "prettier": "^2.0.0", + "pretty-format": "^23.0.1", + "require-relative": "^0.8.7", + "typescript": "^3.9.3", + "vue-eslint-parser": "~7.1.0" }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" + "engines": { + "node": ">=10.0.0" } }, - "node_modules/parse5-sax-parser": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-7.0.0.tgz", - "integrity": "sha512-5A+v2SNsq8T6/mG3ahcz8ZtQ0OUFTatxPbeidoMB7tkJSGDY3tdfl4MHovtLQHkEn5CGxijNWRQHhRQ6IRpXKg==", + "node_modules/prettier-eslint/node_modules/@babel/code-frame": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", + "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", "dev": true, "dependencies": { - "parse5": "^7.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" + "@babel/highlight": "^7.10.4" } }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "node_modules/prettier-eslint/node_modules/@eslint/eslintrc": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", + "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==", "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.1.1", + "espree": "^7.3.0", + "globals": "^13.9.0", + "ignore": "^4.0.6", + "import-fresh": "^3.2.1", + "js-yaml": "^3.13.1", + "minimatch": "^3.0.4", + "strip-json-comments": "^3.1.1" + }, "engines": { - "node": ">= 0.8" + "node": "^10.12.0 || >=12.0.0" } }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "node_modules/prettier-eslint/node_modules/@humanwhocodes/config-array": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", + "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==", + "deprecated": "Use @eslint/config-array instead", "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.0", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + }, "engines": { - "node": ">=8" + "node": ">=10.10.0" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "node_modules/prettier-eslint/node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true + }, + "node_modules/prettier-eslint/node_modules/@typescript-eslint/parser": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-3.10.1.tgz", + "integrity": "sha512-Ug1RcWcrJP02hmtaXVS3axPPTTPnZjupqhgj+NnZ6BCkwSImWk/283347+x9wN+lqOdK9Eo3vsyiyDHgsmiEJw==", "dev": true, + "dependencies": { + "@types/eslint-visitor-keys": "^1.0.0", + "@typescript-eslint/experimental-utils": "3.10.1", + "@typescript-eslint/types": "3.10.1", + "@typescript-eslint/typescript-estree": "3.10.1", + "eslint-visitor-keys": "^1.1.0" + }, "engines": { - "node": ">=0.10.0" + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "node_modules/prettier-eslint/node_modules/@typescript-eslint/types": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-3.10.1.tgz", + "integrity": "sha512-+3+FCUJIahE9q0lDi1WleYzjCwJs5hIsbugIgnbB+dSCYUxl8L6PwmsyOPFZde2hc1DlTo/xnkOgiTLSyAbHiQ==", "dev": true, "engines": { - "node": ">=8" + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "node_modules/path-scurry": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.2.tgz", - "integrity": "sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==", + "node_modules/prettier-eslint/node_modules/@typescript-eslint/typescript-estree": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-3.10.1.tgz", + "integrity": "sha512-QbcXOuq6WYvnB3XPsZpIwztBoquEYLXh2MtwVU+kO8jgYCiv4G5xrSP/1wg4tkvrEE+esZVquIPX/dxPlePk1w==", "dev": true, "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + "@typescript-eslint/types": "3.10.1", + "@typescript-eslint/visitor-keys": "3.10.1", + "debug": "^4.1.1", + "glob": "^7.1.6", + "is-glob": "^4.0.1", + "lodash": "^4.17.15", + "semver": "^7.3.2", + "tsutils": "^3.17.1" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "^10.12.0 || >=12.0.0" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", - "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", + "node_modules/prettier-eslint/node_modules/@typescript-eslint/visitor-keys": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-3.10.1.tgz", + "integrity": "sha512-9JgC82AaQeglebjZMgYR5wgmfUdUc+EitGUUMW8u2nDckaeimzW+VsoLV6FoimPv2id3VQzfjwBxEMVz08ameQ==", "dev": true, + "dependencies": { + "eslint-visitor-keys": "^1.1.0" + }, "engines": { - "node": "14 || >=16.14" + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", - "dev": true - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "node_modules/prettier-eslint/node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", "dev": true, + "bin": { + "acorn": "bin/acorn" + }, "engines": { - "node": ">=8" + "node": ">=0.4.0" } }, - "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true + "node_modules/prettier-eslint/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } }, - "node_modules/picomatch": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.1.tgz", - "integrity": "sha512-xUXwsxNjwTQ8K3GnT4pCJm+xq3RUPQbmkYJTP5aFIfNIvbcc/4MUxgBaaRSZJ6yGJZiGSyYlM6MzwTsRk8SYCg==", + "node_modules/prettier-eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, "engines": { - "node": ">=12" + "node": ">=8" }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "node_modules/prettier-eslint/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dev": true, - "optional": true, - "engines": { - "node": ">=6" + "dependencies": { + "sprintf-js": "~1.0.2" } }, - "node_modules/piscina": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.4.0.tgz", - "integrity": "sha512-+AQduEJefrOApE4bV7KRmp3N2JnnyErlVqq4P/jmko4FPz9Z877BCccl/iB3FdrWSUkvbGV9Kan/KllJgat3Vg==", + "node_modules/prettier-eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, - "optionalDependencies": { - "nice-napi": "^1.0.2" + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "node_modules/pkg-dir": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", - "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==", + "node_modules/prettier-eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "dependencies": { - "find-up": "^6.3.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">=14.16" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/pkg-dir/node_modules/find-up": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", - "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "node_modules/prettier-eslint/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "dependencies": { - "locate-path": "^7.1.0", - "path-exists": "^5.0.0" + "color-name": "~1.1.4" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=7.0.0" + } + }, + "node_modules/prettier-eslint/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/prettier-eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/pkg-dir/node_modules/locate-path": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", - "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "node_modules/prettier-eslint/node_modules/eslint": { + "version": "7.32.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", + "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", "dev": true, "dependencies": { - "p-locate": "^6.0.0" + "@babel/code-frame": "7.12.11", + "@eslint/eslintrc": "^0.4.3", + "@humanwhocodes/config-array": "^0.5.0", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "enquirer": "^2.3.5", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^2.1.0", + "eslint-visitor-keys": "^2.0.0", + "espree": "^7.3.1", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.1.2", + "globals": "^13.6.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.0.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "progress": "^2.0.0", + "regexpp": "^3.1.0", + "semver": "^7.2.1", + "strip-ansi": "^6.0.0", + "strip-json-comments": "^3.1.0", + "table": "^6.0.9", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "bin": { + "eslint": "bin/eslint.js" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": "^10.12.0 || >=12.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://opencollective.com/eslint" } }, - "node_modules/pkg-dir/node_modules/p-limit": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", - "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "node_modules/prettier-eslint/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, "dependencies": { - "yocto-queue": "^1.0.0" + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8.0.0" } }, - "node_modules/pkg-dir/node_modules/p-locate": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", - "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "node_modules/prettier-eslint/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", "dev": true, - "dependencies": { - "p-limit": "^4.0.0" - }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=4" } }, - "node_modules/pkg-dir/node_modules/path-exists": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", - "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "node_modules/prettier-eslint/node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", "dev": true, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=10" } }, - "node_modules/pkg-dir/node_modules/yocto-queue": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", - "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "node_modules/prettier-eslint/node_modules/espree": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", + "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", "dev": true, - "engines": { - "node": ">=12.20" + "dependencies": { + "acorn": "^7.4.0", + "acorn-jsx": "^5.3.1", + "eslint-visitor-keys": "^1.3.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": "^10.12.0 || >=12.0.0" } }, - "node_modules/postcss": { - "version": "8.4.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", - "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", + "node_modules/prettier-eslint/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, "engines": { - "node": "^10 || ^12 || >=14" + "node": ">=4.0" } }, - "node_modules/postcss-loader": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-8.1.1.tgz", - "integrity": "sha512-0IeqyAsG6tYiDRCYKQJLAmgQr47DX6N7sFSWvQxt6AcupX8DIdmykuk/o/tx0Lze3ErGHJEp5OSRxrelC6+NdQ==", + "node_modules/prettier-eslint/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "dependencies": { - "cosmiconfig": "^9.0.0", - "jiti": "^1.20.0", - "semver": "^7.5.4" + "type-fest": "^0.20.2" }, "engines": { - "node": ">= 18.12.0" + "node": ">=8" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "@rspack/core": "0.x || 1.x", - "postcss": "^7.0.0 || ^8.0.1", - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "webpack": { - "optional": true - } + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/postcss-media-query-parser": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", - "integrity": "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==", - "dev": true - }, - "node_modules/postcss-modules-extract-imports": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", - "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "node_modules/prettier-eslint/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" + "node": ">=8" } }, - "node_modules/postcss-modules-local-by-default": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.5.tgz", - "integrity": "sha512-6MieY7sIfTK0hYfafw1OMEG+2bg8Q1ocHCpoWLqOKj3JXlKu4G7btkmM/B7lFubYkYWmRSPLZi5chid63ZaZYw==", + "node_modules/prettier-eslint/node_modules/ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", "dev": true, - "dependencies": { - "icss-utils": "^5.0.0", - "postcss-selector-parser": "^6.0.2", - "postcss-value-parser": "^4.1.0" - }, "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" + "node": ">= 4" } }, - "node_modules/postcss-modules-scope": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.0.tgz", - "integrity": "sha512-oq+g1ssrsZOsx9M96c5w8laRmvEu9C3adDSjI8oTcbfkrTE8hx/zfyobUoWIxaKPO8bt6S62kxpw5GqypEw1QQ==", + "node_modules/prettier-eslint/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", "dev": true, "dependencies": { - "postcss-selector-parser": "^6.0.4" - }, - "engines": { - "node": "^10 || ^12 || >= 14" + "argparse": "^1.0.7", + "esprima": "^4.0.0" }, - "peerDependencies": { - "postcss": "^8.1.0" + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "node_modules/postcss-modules-values": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", - "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "node_modules/prettier-eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/prettier-eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "dependencies": { - "icss-utils": "^5.0.0" + "brace-expansion": "^1.1.7" }, "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" + "node": "*" } }, - "node_modules/postcss-selector-parser": { - "version": "6.0.16", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz", - "integrity": "sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==", + "node_modules/prettier-eslint/node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", "dev": true, - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" + "bin": { + "prettier": "bin-prettier.js" }, "engines": { - "node": ">=4" + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "node_modules/prettier-eslint/node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "node_modules/prettier-eslint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, "engines": { - "node": ">= 0.8.0" + "node": ">=8" } }, - "node_modules/prettier": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", - "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "node_modules/prettier-eslint/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true, - "bin": { - "prettier": "bin/prettier.cjs" - }, "engines": { - "node": ">=14" + "node": ">=10" }, "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/prettier-eslint": { - "version": "16.3.0", - "resolved": "https://registry.npmjs.org/prettier-eslint/-/prettier-eslint-16.3.0.tgz", - "integrity": "sha512-Lh102TIFCr11PJKUMQ2kwNmxGhTsv/KzUg9QYF2Gkw259g/kPgndZDWavk7/ycbRvj2oz4BPZ1gCU8bhfZH/Xg==", + "node_modules/prettier-eslint/node_modules/typescript": { + "version": "3.9.10", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz", + "integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==", "dev": true, - "dependencies": { - "@typescript-eslint/parser": "^6.7.5", - "common-tags": "^1.4.0", - "dlv": "^1.1.0", - "eslint": "^8.7.0", - "indent-string": "^4.0.0", - "lodash.merge": "^4.6.0", - "loglevel-colored-level-prefix": "^1.0.0", - "prettier": "^3.0.1", - "pretty-format": "^29.7.0", - "require-relative": "^0.8.7", - "typescript": "^5.2.2", - "vue-eslint-parser": "^9.1.0" + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" }, "engines": { - "node": ">=16.10.0" - }, - "peerDependencies": { - "prettier-plugin-svelte": "^3.0.0", - "svelte-eslint-parser": "*" - }, - "peerDependenciesMeta": { - "prettier-plugin-svelte": { - "optional": true - }, - "svelte-eslint-parser": { - "optional": true - } + "node": ">=4.2.0" } }, "node_modules/prettier-linter-helpers": { @@ -13135,46 +12406,38 @@ } }, "node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-23.6.0.tgz", + "integrity": "sha512-zf9NV1NSlDLDjycnwm6hpFATCGl/K1lt0R/GdkAK2O5LN/rwJoB+Mh93gGJjut4YbmecbfgLWVGSTCr0Ewvvbw==", "dev": true, "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "ansi-regex": "^3.0.0", + "ansi-styles": "^3.2.0" } }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "node_modules/proc-log": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", + "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", "dev": true, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/proc-log": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz", - "integrity": "sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==", + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "dev": true, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": ">=0.4.0" } }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true - }, "node_modules/promise-inflight": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", @@ -13216,12 +12479,6 @@ "node": ">= 0.10" } }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true - }, "node_modules/prr": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", @@ -13229,15 +12486,21 @@ "dev": true, "optional": true }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "engines": { - "node": ">=6" + "node_modules/pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" } }, + "node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "dev": true + }, "node_modules/qjobs": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", @@ -13248,12 +12511,12 @@ } }, "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "dev": true, "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -13315,67 +12578,10 @@ "node": ">= 0.8" } }, - "node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true - }, - "node_modules/read-package-json": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-7.0.1.tgz", - "integrity": "sha512-8PcDiZ8DXUjLf687Ol4BR8Bpm2umR7vhoZOzNRt+uxD9GpBh/K+CAAALVIiYFknmvlmyg7hM7BSNUXPaCCqd0Q==", - "dev": true, - "dependencies": { - "glob": "^10.2.2", - "json-parse-even-better-errors": "^3.0.0", - "normalize-package-data": "^6.0.0", - "npm-normalize-package-bin": "^3.0.0" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/read-package-json-fast": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-3.0.2.tgz", - "integrity": "sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw==", - "dev": true, - "dependencies": { - "json-parse-even-better-errors": "^3.0.0", - "npm-normalize-package-bin": "^3.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/read-package-json/node_modules/glob": { - "version": "10.3.12", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", - "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", - "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.6", - "minimatch": "^9.0.1", - "minipass": "^7.0.4", - "path-scurry": "^1.10.2" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -13422,9 +12628,9 @@ "dev": true }, "node_modules/regenerate-unicode-properties": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz", - "integrity": "sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", + "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", "dev": true, "dependencies": { "regenerate": "^1.4.2" @@ -13454,6 +12660,18 @@ "integrity": "sha512-TVILVSz2jY5D47F4mA4MppkBrafEaiUWJO/TcZHEIuI13AqoZMkK1WMA4Om1YkYbTx+9Ki1/tSUXbceyr9saRg==", "dev": true }, + "node_modules/regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, "node_modules/regexpu-core": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", @@ -13492,6 +12710,11 @@ "jsesc": "bin/jsesc" } }, + "node_modules/reinterval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reinterval/-/reinterval-1.1.0.tgz", + "integrity": "sha512-QIRet3SYrGp0HUHO88jVskiG6seqUGC5iAG7AwI/BV4ypGcuqk9Du6YQBUOUqm9c8pw1eyLoIaONifRua1lsEQ==" + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -13540,12 +12763,12 @@ } }, "node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, "engines": { - "node": ">=8" + "node": ">=4" } }, "node_modules/resolve-url-loader": { @@ -13588,16 +12811,19 @@ } }, "node_modules/restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", "dev": true, "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/retry": { @@ -13620,15 +12846,15 @@ } }, "node_modules/rfdc": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.1.tgz", - "integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==", - "dev": true + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==" }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "dependencies": { "glob": "^7.1.3" @@ -13641,9 +12867,9 @@ } }, "node_modules/rollup": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.17.2.tgz", - "integrity": "sha512-/9ClTJPByC0U4zNLowV1tMBe8yMEAxewtR3cUNX5BoEpGH3dQEWpJLr6CLp0fPdYRF/fzVOgvDb1zXuakwF5kQ==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.4.tgz", + "integrity": "sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A==", "dev": true, "dependencies": { "@types/estree": "1.0.5" @@ -13656,32 +12882,35 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.17.2", - "@rollup/rollup-android-arm64": "4.17.2", - "@rollup/rollup-darwin-arm64": "4.17.2", - "@rollup/rollup-darwin-x64": "4.17.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.17.2", - "@rollup/rollup-linux-arm-musleabihf": "4.17.2", - "@rollup/rollup-linux-arm64-gnu": "4.17.2", - "@rollup/rollup-linux-arm64-musl": "4.17.2", - "@rollup/rollup-linux-powerpc64le-gnu": "4.17.2", - "@rollup/rollup-linux-riscv64-gnu": "4.17.2", - "@rollup/rollup-linux-s390x-gnu": "4.17.2", - "@rollup/rollup-linux-x64-gnu": "4.17.2", - "@rollup/rollup-linux-x64-musl": "4.17.2", - "@rollup/rollup-win32-arm64-msvc": "4.17.2", - "@rollup/rollup-win32-ia32-msvc": "4.17.2", - "@rollup/rollup-win32-x64-msvc": "4.17.2", + "@rollup/rollup-android-arm-eabi": "4.22.4", + "@rollup/rollup-android-arm64": "4.22.4", + "@rollup/rollup-darwin-arm64": "4.22.4", + "@rollup/rollup-darwin-x64": "4.22.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.22.4", + "@rollup/rollup-linux-arm-musleabihf": "4.22.4", + "@rollup/rollup-linux-arm64-gnu": "4.22.4", + "@rollup/rollup-linux-arm64-musl": "4.22.4", + "@rollup/rollup-linux-powerpc64le-gnu": "4.22.4", + "@rollup/rollup-linux-riscv64-gnu": "4.22.4", + "@rollup/rollup-linux-s390x-gnu": "4.22.4", + "@rollup/rollup-linux-x64-gnu": "4.22.4", + "@rollup/rollup-linux-x64-musl": "4.22.4", + "@rollup/rollup-win32-arm64-msvc": "4.22.4", + "@rollup/rollup-win32-ia32-msvc": "4.22.4", + "@rollup/rollup-win32-x64-msvc": "4.22.4", "fsevents": "~2.3.2" } }, - "node_modules/run-async": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", - "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==", + "node_modules/run-applescript": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", + "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", "dev": true, "engines": { - "node": ">=0.12.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/run-parallel": { @@ -13719,7 +12948,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, "funding": [ { "type": "github", @@ -13741,15 +12969,10 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, - "node_modules/safevalues": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/safevalues/-/safevalues-0.3.4.tgz", - "integrity": "sha512-LRneZZRXNgjzwG4bDQdOTSbze3fHm1EAKN/8bePxnlEZiBmkYEDggaHbuvHI9/hoqHbGfsEA7tWS9GhYHZBBsw==" - }, "node_modules/sass": { - "version": "1.71.1", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.71.1.tgz", - "integrity": "sha512-wovtnV2PxzteLlfNzbgm1tFXPLoZILYAMJtvoXXkD7/+1uP41eKkIt1ypWq5/q2uT94qHjXehEYfmjKOvjL9sg==", + "version": "1.77.6", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.6.tgz", + "integrity": "sha512-ByXE1oLD79GVq9Ht1PeHWCPMPB8XHpBuz1r85oByKHjZY6qV6rWnQovQzXJXuQ/XyE1Oj3iPk3lo28uzaRA2/Q==", "dev": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0", @@ -13764,9 +12987,9 @@ } }, "node_modules/sass-loader": { - "version": "14.1.1", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-14.1.1.tgz", - "integrity": "sha512-QX8AasDg75monlybel38BZ49JP5Z+uSKfKwF2rO7S74BywaRmGQMUBw9dtkS+ekyM/QnP+NOrRYq8ABMZ9G8jw==", + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.0.tgz", + "integrity": "sha512-n13Z+3rU9A177dk4888czcVFiC8CL9dii4qpXWUg3YIIgZEvi9TCFKjOQcbK0kJM7DJu9VucrZFddvNfYCPwtw==", "dev": true, "dependencies": { "neo-async": "^2.6.2" @@ -13804,9 +13027,9 @@ } }, "node_modules/sax": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz", - "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", "dev": true, "optional": true }, @@ -13829,6 +13052,23 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/schema-utils/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/select-hose": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", @@ -13849,13 +13089,10 @@ } }, "node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, "bin": { "semver": "bin/semver.js" }, @@ -13863,28 +13100,10 @@ "node": ">=10" } }, - "node_modules/semver/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/semver/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "dev": true, "dependencies": { "debug": "2.6.9", @@ -13932,12 +13151,6 @@ "node": ">=4" } }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, "node_modules/send/node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -14026,20 +13239,29 @@ "dev": true }, "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "dev": true, "dependencies": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" }, "engines": { "node": ">= 0.8.0" } }, + "node_modules/serve-static/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -14124,35 +13346,72 @@ } }, "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, "node_modules/sigstore": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-2.3.0.tgz", - "integrity": "sha512-q+o8L2ebiWD1AxD17eglf1pFrl9jtW7FHa0ygqY6EKvibK8JHyq9Z26v9MZXeDiw+RbfOJ9j2v70M10Hd6E06A==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-2.3.1.tgz", + "integrity": "sha512-8G+/XDU8wNsJOQS5ysDVO0Etg9/2uA5gR9l4ZwijjlwxBcrU6RPfwi2+jJmbP+Ap1Hlp/nVAaEO4Fj22/SL2gQ==", "dev": true, "dependencies": { - "@sigstore/bundle": "^2.3.1", + "@sigstore/bundle": "^2.3.2", "@sigstore/core": "^1.0.0", - "@sigstore/protobuf-specs": "^0.3.1", - "@sigstore/sign": "^2.3.0", - "@sigstore/tuf": "^2.3.1", - "@sigstore/verify": "^1.2.0" + "@sigstore/protobuf-specs": "^0.3.2", + "@sigstore/sign": "^2.3.2", + "@sigstore/tuf": "^2.3.4", + "@sigstore/verify": "^1.2.1" }, "engines": { "node": "^16.14.0 || >=18.0.0" } }, "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", "dev": true, "engines": { - "node": ">=8" + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/smart-buffer": { @@ -14166,16 +13425,16 @@ } }, "node_modules/socket.io": { - "version": "4.7.5", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.5.tgz", - "integrity": "sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA==", + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.0.tgz", + "integrity": "sha512-8U6BEgGjQOfGz3HHTYaC/L1GaxDCJ/KM0XTkJly0EhZ5U/du9uNEZy4ZgYzEzIqlx2CMm25CrCqr1ck899eLNA==", "dev": true, "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", "cors": "~2.8.5", "debug": "~4.3.2", - "engine.io": "~6.5.2", + "engine.io": "~6.6.0", "socket.io-adapter": "~2.5.2", "socket.io-parser": "~4.2.4" }, @@ -14193,6 +13452,27 @@ "ws": "~8.17.1" } }, + "node_modules/socket.io-adapter/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/socket.io-parser": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", @@ -14232,14 +13512,14 @@ } }, "node_modules/socks-proxy-agent": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.3.tgz", - "integrity": "sha512-VNegTZKhuGq5vSD6XNKlbqWhyt/40CgoEw8XxD6dhnm8Jq9IEa3nIa4HwnM8XOqU0CdB0BwWVXusqiFXfHB3+A==", + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz", + "integrity": "sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==", "dev": true, "dependencies": { "agent-base": "^7.1.1", "debug": "^4.3.4", - "socks": "^2.7.1" + "socks": "^2.8.3" }, "engines": { "node": ">= 14" @@ -14255,9 +13535,9 @@ } }, "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, "engines": { "node": ">=0.10.0" @@ -14341,9 +13621,9 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.17", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.17.tgz", - "integrity": "sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg==", + "version": "3.0.20", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.20.tgz", + "integrity": "sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==", "dev": true }, "node_modules/spdy": { @@ -14376,10 +13656,18 @@ "wbuf": "^1.7.3" } }, + "node_modules/split2": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", + "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", + "dependencies": { + "readable-stream": "^3.0.0" + } + }, "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", "dev": true }, "node_modules/ssri": { @@ -14403,6 +13691,11 @@ "node": ">= 0.6" } }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==" + }, "node_modules/streamroller": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz", @@ -14411,65 +13704,35 @@ "dependencies": { "date-format": "^4.0.14", "debug": "^4.3.4", - "fs-extra": "^8.1.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/streamroller/node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/streamroller/node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "dev": true, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/streamroller/node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true, + "fs-extra": "^8.1.0" + }, "engines": { - "node": ">= 4.0.0" + "node": ">=8.0" } }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, "dependencies": { "safe-buffer": "~5.2.0" } }, "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/string-width-cjs": { @@ -14487,6 +13750,48 @@ "node": ">=8" } }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -14512,13 +13817,22 @@ "node": ">=8" } }, - "node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "engines": { - "node": ">=4" + "node": ">=8" + } + }, + "node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" } }, "node_modules/strip-final-newline": { @@ -14542,23 +13856,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/strong-log-transformer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/strong-log-transformer/-/strong-log-transformer-2.1.0.tgz", - "integrity": "sha512-B3Hgul+z0L9a236FAUC9iZsL+nVHgoCJnqCbN588DjYxvGXaXaaFbfmQ/JhvKjZwsOukuR72XbHv71Qkug0HxA==", - "dev": true, - "dependencies": { - "duplexer": "^0.1.1", - "minimist": "^1.2.0", - "through": "^2.3.4" - }, - "bin": { - "sl-log-transformer": "bin/sl-log-transformer.js" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -14593,9 +13890,9 @@ } }, "node_modules/synckit": { - "version": "0.8.8", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz", - "integrity": "sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==", + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.1.tgz", + "integrity": "sha512-7gr8p9TQP6RAHusBOSLs46F4564ZrjV8xFmw5zCmgmhGUcw2hxsShhJ6CEiHQMgPDwAQ1fWHPM0ypc4RMAig4A==", "dev": true, "dependencies": { "@pkgr/core": "^0.1.0", @@ -14608,6 +13905,101 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/table": { + "version": "6.8.2", + "resolved": "https://registry.npmjs.org/table/-/table-6.8.2.tgz", + "integrity": "sha512-w2sfv80nrAh2VCbqR5AK27wswXhqcck2AhfnNW76beQXskGZ1V12GwS//yYVa3d3fcvAip2OUnbDAjW2k3v9fA==", + "dev": true, + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/table/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/table/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/table/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/table/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/table/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/table/node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/table/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", @@ -14634,22 +14026,6 @@ "node": ">=10" } }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "dev": true, - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/tar/node_modules/fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", @@ -14702,9 +14078,9 @@ "dev": true }, "node_modules/terser": { - "version": "5.29.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.29.1.tgz", - "integrity": "sha512-lZQ/fyaIGxsbGxApKmoPTODIzELy3++mXhS5hOqaAWZjQtpq/hFHAc+rm29NND1rYRxRWKcjuARNwULNXa5RtQ==", + "version": "5.31.6", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.6.tgz", + "integrity": "sha512-PQ4DAriWzKj+qgehQ7LK5bQqCFNMmlhjR2PFFLuqGCpuCAauxemVBWwWOxo3UIwWQx8+Pr61Df++r76wDmkQBg==", "dev": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", @@ -14802,53 +14178,23 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/test-exclude/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, - "node_modules/through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", - "dev": true + "node_modules/thingies": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/thingies/-/thingies-1.21.0.tgz", + "integrity": "sha512-hsqsJsFMsV+aD4s3CWKk85ep/3I9XzYV/IXaSouJMYIoDlgyi11cBhsqYe9/geRfB0YIikBQg6raRaM+nIMP9g==", + "dev": true, + "engines": { + "node": ">=10.18" + }, + "peerDependencies": { + "tslib": "^2" + } }, "node_modules/thunky": { "version": "1.1.0", @@ -14857,15 +14203,15 @@ "dev": true }, "node_modules/tmp": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", - "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", "dev": true, "dependencies": { - "rimraf": "^3.0.0" + "os-tmpdir": "~1.0.2" }, "engines": { - "node": ">=8.17.0" + "node": ">=0.6.0" } }, "node_modules/to-fast-properties": { @@ -14898,6 +14244,22 @@ "node": ">=0.6" } }, + "node_modules/tree-dump": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.0.2.tgz", + "integrity": "sha512-dpev9ABuLWdEubk+cIaI9cHwRNNDjkBBLXTwI4UCUFdQ5xXKqNXoK4FEciw/vxf+NQ7Cb7sGUyeUtORvHIdRXQ==", + "dev": true, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -14919,24 +14281,31 @@ "typescript": ">=4.2.0" } }, - "node_modules/tsconfig-paths": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", - "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", "dev": true, "dependencies": { - "json5": "^2.2.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" + "tslib": "^1.8.1" }, "engines": { - "node": ">=6" + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" } }, - "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true }, "node_modules/tuf-js": { "version": "2.2.1", @@ -14995,10 +14364,15 @@ "integrity": "sha512-KNNZtayBCtmnNmbo5mG47p1XsCyrx6iVqomjcZnec/1Y5GGARaxPs6r49RnSPeUP3YjNYiU9sQHAtY4BBvnZwg==", "dev": true }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, "node_modules/typescript": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", - "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -15009,9 +14383,9 @@ } }, "node_modules/ua-parser-js": { - "version": "0.7.37", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.37.tgz", - "integrity": "sha512-xV8kqRKM+jhMvcHWUKthV9fNebIzrNy//2O9ZwWcfiBFR5f25XVZPLlEajk/sf3Ra15V92isyQqnIEXRDaZWEA==", + "version": "0.7.39", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.39.tgz", + "integrity": "sha512-IZ6acm6RhQHNibSt7+c09hhvsKy9WUr4DVbeq9U8o71qxyYtJpQeDxQnMrVqnIFMLcQjHO0I9wgfO2vIahht4w==", "dev": true, "funding": [ { @@ -15027,29 +14401,23 @@ "url": "https://github.com/sponsors/faisalman" } ], + "bin": { + "ua-parser-js": "script/cli.js" + }, "engines": { "node": "*" } }, - "node_modules/undici": { - "version": "6.11.1", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.11.1.tgz", - "integrity": "sha512-KyhzaLJnV1qa3BSHdj4AZ2ndqI0QWPxYzaIOio0WzcEJB9gvuysprJSLtpvc2D9mhR9jPDUk7xlJlZbH2KR5iw==", - "dev": true, - "engines": { - "node": ">=18.0" - } - }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", "dev": true }, "node_modules/unicode-canonical-property-names-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", - "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", "dev": true, "engines": { "node": ">=4" @@ -15069,9 +14437,9 @@ } }, "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", - "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", + "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==", "dev": true, "engines": { "node": ">=4" @@ -15086,6 +14454,18 @@ "node": ">=4" } }, + "node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/unique-filename": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", @@ -15111,12 +14491,12 @@ } }, "node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true, "engines": { - "node": ">= 10.0.0" + "node": ">= 4.0.0" } }, "node_modules/unpipe": { @@ -15129,9 +14509,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.15", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.15.tgz", - "integrity": "sha512-K9HWH62x3/EalU1U6sjSZiylm9C8tgq2mSvshZpqc7QE69RaA2qjhkW2HlNA0tFpEbtyFz7HTqbSdN4MSwUodA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", "dev": true, "funding": [ { @@ -15148,8 +14528,8 @@ } ], "dependencies": { - "escalade": "^3.1.2", - "picocolors": "^1.0.0" + "escalade": "^3.2.0", + "picocolors": "^1.1.0" }, "bin": { "update-browserslist-db": "cli.js" @@ -15167,11 +14547,19 @@ "punycode": "^2.1.0" } }, + "node_modules/uri-js/node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/utils-merge": { "version": "1.0.1", @@ -15191,6 +14579,12 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/v8-compile-cache": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.4.0.tgz", + "integrity": "sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw==", + "dev": true + }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -15220,14 +14614,14 @@ } }, "node_modules/vite": { - "version": "5.1.7", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.7.tgz", - "integrity": "sha512-sgnEEFTZYMui/sTlH1/XEnVNHMujOahPLGMxn1+5sIT45Xjng1Ec1K78jRP15dSmVgg5WBin9yO81j3o9OxofA==", + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.6.tgz", + "integrity": "sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==", "dev": true, "dependencies": { - "esbuild": "^0.19.3", - "postcss": "^8.4.35", - "rollup": "^4.2.0" + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" }, "bin": { "vite": "bin/vite.js" @@ -15246,6 +14640,7 @@ "less": "*", "lightningcss": "^1.21.0", "sass": "*", + "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" @@ -15263,6 +14658,9 @@ "sass": { "optional": true }, + "sass-embedded": { + "optional": true + }, "stylus": { "optional": true }, @@ -15275,9 +14673,9 @@ } }, "node_modules/vite/node_modules/@esbuild/aix-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", - "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", "cpu": [ "ppc64" ], @@ -15291,9 +14689,9 @@ } }, "node_modules/vite/node_modules/@esbuild/android-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", - "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", "cpu": [ "arm" ], @@ -15307,9 +14705,9 @@ } }, "node_modules/vite/node_modules/@esbuild/android-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", - "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", "cpu": [ "arm64" ], @@ -15323,9 +14721,9 @@ } }, "node_modules/vite/node_modules/@esbuild/android-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", - "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", "cpu": [ "x64" ], @@ -15339,9 +14737,9 @@ } }, "node_modules/vite/node_modules/@esbuild/darwin-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", - "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", "cpu": [ "arm64" ], @@ -15355,9 +14753,9 @@ } }, "node_modules/vite/node_modules/@esbuild/darwin-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", - "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", "cpu": [ "x64" ], @@ -15371,9 +14769,9 @@ } }, "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", - "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", "cpu": [ "arm64" ], @@ -15387,9 +14785,9 @@ } }, "node_modules/vite/node_modules/@esbuild/freebsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", - "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", "cpu": [ "x64" ], @@ -15403,9 +14801,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", - "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", "cpu": [ "arm" ], @@ -15419,9 +14817,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", - "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", "cpu": [ "arm64" ], @@ -15435,9 +14833,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", - "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", "cpu": [ "ia32" ], @@ -15451,9 +14849,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-loong64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", - "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", "cpu": [ "loong64" ], @@ -15467,9 +14865,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-mips64el": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", - "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", "cpu": [ "mips64el" ], @@ -15483,9 +14881,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", - "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", "cpu": [ "ppc64" ], @@ -15499,9 +14897,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-riscv64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", - "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", "cpu": [ "riscv64" ], @@ -15515,9 +14913,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-s390x": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", - "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", "cpu": [ "s390x" ], @@ -15531,9 +14929,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", - "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", "cpu": [ "x64" ], @@ -15547,9 +14945,9 @@ } }, "node_modules/vite/node_modules/@esbuild/netbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", - "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", "cpu": [ "x64" ], @@ -15563,9 +14961,9 @@ } }, "node_modules/vite/node_modules/@esbuild/openbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", - "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", "cpu": [ "x64" ], @@ -15579,9 +14977,9 @@ } }, "node_modules/vite/node_modules/@esbuild/sunos-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", - "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", "cpu": [ "x64" ], @@ -15595,9 +14993,9 @@ } }, "node_modules/vite/node_modules/@esbuild/win32-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", - "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", "cpu": [ "arm64" ], @@ -15611,9 +15009,9 @@ } }, "node_modules/vite/node_modules/@esbuild/win32-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", - "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", "cpu": [ "ia32" ], @@ -15627,9 +15025,9 @@ } }, "node_modules/vite/node_modules/@esbuild/win32-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", - "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", "cpu": [ "x64" ], @@ -15643,9 +15041,9 @@ } }, "node_modules/vite/node_modules/esbuild": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", - "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "dev": true, "hasInstallScript": true, "bin": { @@ -15655,29 +15053,57 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.19.12", - "@esbuild/android-arm": "0.19.12", - "@esbuild/android-arm64": "0.19.12", - "@esbuild/android-x64": "0.19.12", - "@esbuild/darwin-arm64": "0.19.12", - "@esbuild/darwin-x64": "0.19.12", - "@esbuild/freebsd-arm64": "0.19.12", - "@esbuild/freebsd-x64": "0.19.12", - "@esbuild/linux-arm": "0.19.12", - "@esbuild/linux-arm64": "0.19.12", - "@esbuild/linux-ia32": "0.19.12", - "@esbuild/linux-loong64": "0.19.12", - "@esbuild/linux-mips64el": "0.19.12", - "@esbuild/linux-ppc64": "0.19.12", - "@esbuild/linux-riscv64": "0.19.12", - "@esbuild/linux-s390x": "0.19.12", - "@esbuild/linux-x64": "0.19.12", - "@esbuild/netbsd-x64": "0.19.12", - "@esbuild/openbsd-x64": "0.19.12", - "@esbuild/sunos-x64": "0.19.12", - "@esbuild/win32-arm64": "0.19.12", - "@esbuild/win32-ia32": "0.19.12", - "@esbuild/win32-x64": "0.19.12" + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vite/node_modules/postcss": { + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" } }, "node_modules/void-elements": { @@ -15690,49 +15116,89 @@ } }, "node_modules/vue-eslint-parser": { - "version": "9.4.2", - "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.2.tgz", - "integrity": "sha512-Ry9oiGmCAK91HrKMtCrKFWmSFWvYkpGglCeFAIqDdr9zdXmMMpJOmUJS7WWsW7fX81h6mwHmUZCQQ1E0PkSwYQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-7.1.1.tgz", + "integrity": "sha512-8FdXi0gieEwh1IprIBafpiJWcApwrU+l2FEj8c1HtHFdNXMd0+2jUSjBVmcQYohf/E72irwAXEXLga6TQcB3FA==", "dev": true, "dependencies": { - "debug": "^4.3.4", - "eslint-scope": "^7.1.1", - "eslint-visitor-keys": "^3.3.0", - "espree": "^9.3.1", - "esquery": "^1.4.0", - "lodash": "^4.17.21", - "semver": "^7.3.6" + "debug": "^4.1.1", + "eslint-scope": "^5.0.0", + "eslint-visitor-keys": "^1.1.0", + "espree": "^6.2.1", + "esquery": "^1.0.1", + "lodash": "^4.17.15" }, "engines": { - "node": "^14.17.0 || >=16.0.0" + "node": ">=8.10" }, "funding": { "url": "https://github.com/sponsors/mysticatea" }, "peerDependencies": { - "eslint": ">=6.0.0" + "eslint": ">=5.0.0" + } + }, + "node_modules/vue-eslint-parser/node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" } }, "node_modules/vue-eslint-parser/node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, "dependencies": { "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" + "estraverse": "^4.1.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=8.0.0" + } + }, + "node_modules/vue-eslint-parser/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/vue-eslint-parser/node_modules/espree": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", + "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", + "dev": true, + "dependencies": { + "acorn": "^7.1.1", + "acorn-jsx": "^5.2.0", + "eslint-visitor-keys": "^1.1.0" }, - "funding": { - "url": "https://opencollective.com/eslint" + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/vue-eslint-parser/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" } }, "node_modules/watchpack": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", - "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", + "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==", "dev": true, "dependencies": { "glob-to-regexp": "^0.4.1", @@ -15760,27 +15226,32 @@ "defaults": "^1.0.3" } }, + "node_modules/weak-lru-cache": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz", + "integrity": "sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==", + "dev": true + }, "node_modules/webpack": { - "version": "5.90.3", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.3.tgz", - "integrity": "sha512-h6uDYlWCctQRuXBs1oYpVe6sFcWedl0dpcVaTf/YF67J9bKvwJajFulMVSYKHrksMB3I/pIagRzDxwxkebuzKA==", + "version": "5.94.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz", + "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==", "dev": true, "dependencies": { - "@types/eslint-scope": "^3.7.3", "@types/estree": "^1.0.5", - "@webassemblyjs/ast": "^1.11.5", - "@webassemblyjs/wasm-edit": "^1.11.5", - "@webassemblyjs/wasm-parser": "^1.11.5", + "@webassemblyjs/ast": "^1.12.1", + "@webassemblyjs/wasm-edit": "^1.12.1", + "@webassemblyjs/wasm-parser": "^1.12.1", "acorn": "^8.7.1", - "acorn-import-assertions": "^1.9.0", + "acorn-import-attributes": "^1.9.5", "browserslist": "^4.21.10", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.15.0", + "enhanced-resolve": "^5.17.1", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.9", + "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", "loader-runner": "^4.2.0", "mime-types": "^2.1.27", @@ -15788,7 +15259,7 @@ "schema-utils": "^3.2.0", "tapable": "^2.1.1", "terser-webpack-plugin": "^5.3.10", - "watchpack": "^2.4.0", + "watchpack": "^2.4.1", "webpack-sources": "^3.2.3" }, "bin": { @@ -15808,19 +15279,20 @@ } }, "node_modules/webpack-dev-middleware": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-6.1.2.tgz", - "integrity": "sha512-Wu+EHmX326YPYUpQLKmKbTyZZJIB8/n6R09pTmB03kJmnMsVPTo9COzHZFr01txwaCAuZvfBJE4ZCHRcKs5JaQ==", + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.2.tgz", + "integrity": "sha512-xOO8n6eggxnwYpy1NlzUKpvrjfJTvae5/D6WOK0S2LSo7vjmo5gCM1DbLUmFqrMTJP+W/0YZNctm7jasWvLuBA==", "dev": true, "dependencies": { "colorette": "^2.0.10", - "memfs": "^3.4.12", + "memfs": "^4.6.0", "mime-types": "^2.1.31", + "on-finished": "^2.4.1", "range-parser": "^1.2.1", "schema-utils": "^4.0.0" }, "engines": { - "node": ">= 14.15.0" + "node": ">= 18.12.0" }, "funding": { "type": "opencollective", @@ -15836,54 +15308,54 @@ } }, "node_modules/webpack-dev-server": { - "version": "4.15.1", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.1.tgz", - "integrity": "sha512-5hbAst3h3C3L8w6W4P96L5vaV0PxSmJhxZvWKYIdgxOQm8pNZ5dEOmmSLBVpP85ReeyRt6AS1QJNyo/oFFPeVA==", - "dev": true, - "dependencies": { - "@types/bonjour": "^3.5.9", - "@types/connect-history-api-fallback": "^1.3.5", - "@types/express": "^4.17.13", - "@types/serve-index": "^1.9.1", - "@types/serve-static": "^1.13.10", - "@types/sockjs": "^0.3.33", - "@types/ws": "^8.5.5", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.0.4.tgz", + "integrity": "sha512-dljXhUgx3HqKP2d8J/fUMvhxGhzjeNVarDLcbO/EWMSgRizDkxHQDZQaLFL5VJY9tRBj2Gz+rvCEYYvhbqPHNA==", + "dev": true, + "dependencies": { + "@types/bonjour": "^3.5.13", + "@types/connect-history-api-fallback": "^1.5.4", + "@types/express": "^4.17.21", + "@types/serve-index": "^1.9.4", + "@types/serve-static": "^1.15.5", + "@types/sockjs": "^0.3.36", + "@types/ws": "^8.5.10", "ansi-html-community": "^0.0.8", - "bonjour-service": "^1.0.11", - "chokidar": "^3.5.3", + "bonjour-service": "^1.2.1", + "chokidar": "^3.6.0", "colorette": "^2.0.10", "compression": "^1.7.4", "connect-history-api-fallback": "^2.0.0", "default-gateway": "^6.0.3", "express": "^4.17.3", "graceful-fs": "^4.2.6", - "html-entities": "^2.3.2", + "html-entities": "^2.4.0", "http-proxy-middleware": "^2.0.3", - "ipaddr.js": "^2.0.1", - "launch-editor": "^2.6.0", - "open": "^8.0.9", - "p-retry": "^4.5.0", - "rimraf": "^3.0.2", - "schema-utils": "^4.0.0", - "selfsigned": "^2.1.1", + "ipaddr.js": "^2.1.0", + "launch-editor": "^2.6.1", + "open": "^10.0.3", + "p-retry": "^6.2.0", + "rimraf": "^5.0.5", + "schema-utils": "^4.2.0", + "selfsigned": "^2.4.1", "serve-index": "^1.9.1", "sockjs": "^0.3.24", "spdy": "^4.0.2", - "webpack-dev-middleware": "^5.3.1", - "ws": "^8.13.0" + "webpack-dev-middleware": "^7.1.0", + "ws": "^8.16.0" }, "bin": { "webpack-dev-server": "bin/webpack-dev-server.js" }, "engines": { - "node": ">= 12.13.0" + "node": ">= 18.12.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "webpack": "^4.37.0 || ^5.0.0" + "webpack": "^5.0.0" }, "peerDependenciesMeta": { "webpack": { @@ -15894,41 +15366,98 @@ } } }, - "node_modules/webpack-dev-server/node_modules/webpack-dev-middleware": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz", - "integrity": "sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==", + "node_modules/webpack-dev-server/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "dev": true, "dependencies": { - "colorette": "^2.0.10", - "memfs": "^3.4.3", - "mime-types": "^2.1.31", - "range-parser": "^1.2.1", - "schema-utils": "^4.0.0" + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/webpack-dev-server/node_modules/http-proxy-middleware": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", + "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", + "dev": true, + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" }, "engines": { - "node": ">= 12.13.0" + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "dev": true, + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/webpack-dev-server/node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "engines": { + "node": ">=10.0.0" }, "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } } }, "node_modules/webpack-merge": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", - "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", + "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", "dev": true, "dependencies": { "clone-deep": "^4.0.1", "flat": "^5.0.2", - "wildcard": "^2.0.0" + "wildcard": "^2.0.1" }, "engines": { - "node": ">=10.0.0" + "node": ">=18.0.0" } }, "node_modules/webpack-sources": { @@ -16156,6 +15685,35 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrap-ansi/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -16189,23 +15747,50 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", - "dev": true, + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "engines": { - "node": ">=10.0.0" + "node": ">=8.3.0" }, "peerDependencies": { "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" + "utf-8-validate": "^5.0.2" }, "peerDependenciesMeta": { "bufferutil": { @@ -16216,6 +15801,14 @@ } } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -16258,6 +15851,35 @@ "node": ">=12" } }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/yargs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -16270,13 +15892,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/zone.js": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.14.5.tgz", - "integrity": "sha512-9XYWZzY6PhHOSdkYryNcMm7L8EK7a4q+GbTvxbIA2a9lMdRUpGuyaYvLDcg8D6bdn+JomSsbPcilVKg6SmUx6w==", - "dependencies": { - "tslib": "^2.3.0" + "node_modules/yoctocolors-cjs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", + "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zone.js": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.14.10.tgz", + "integrity": "sha512-YGAhaO7J5ywOXW6InXNlLmfU194F8lVgu7bRntUF3TiG8Y3nBK0x1UJJuHUP/e8IyihkjCYqhCScpSwnlaSRkQ==" } } } diff --git a/modules/ui/package.json b/modules/ui/package.json index 7f83fc5f7..ae6d57f2d 100644 --- a/modules/ui/package.json +++ b/modules/ui/package.json @@ -9,6 +9,7 @@ "test": "ng test", "test:coverage": "ng test --code-coverage", "test-headless": "ng test --browsers=ChromeHeadless --watch=false", + "test-ci": "ng test --no-watch --no-progress --code-coverage --browsers=ChromeHeadless", "docker": "docker rm -f test-run-ui && docker rmi test-run-ui && docker build -t test-run-ui . && docker run -d -p 80:80 --name test-run-ui test-run-ui", "lint": "ng lint", "lint:fix": "ng lint --fix", @@ -17,36 +18,37 @@ }, "private": true, "dependencies": { - "@angular/animations": "^17.0.8", - "@angular/cdk": "^17.0.4", - "@angular/common": "^17.0.8", - "@angular/compiler": "^17.0.8", - "@angular/core": "^17.0.8", - "@angular/forms": "^17.3.1", - "@angular/material": "^17.3.1", - "@angular/platform-browser": "^17.0.8", - "@angular/platform-browser-dynamic": "^17.3.1", - "@angular/router": "^17.3.1", - "@ngrx/component-store": "^17.1.1", - "@ngrx/effects": "^17.1.1", - "@ngrx/store": "^17.0.1", + "@angular/animations": "^18.2.4", + "@angular/cdk": "^18.2.0", + "@angular/common": "^18.2.4", + "@angular/compiler": "^18.2.4", + "@angular/core": "^18.2.4", + "@angular/forms": "^18.2.4", + "@angular/material": "^18.2.0", + "@angular/platform-browser": "^18.2.4", + "@angular/platform-browser-dynamic": "^18.2.4", + "@angular/router": "^18.2.4", + "@ngrx/component-store": "^18.0.2", + "@ngrx/effects": "^18.0.2", + "@ngrx/store": "^18.0.2", "ngx-mask": "^16.4.2", + "ngx-mqtt": "^17.0.0", "rxjs": "~7.8.0", "tslib": "^2.6.2", - "zone.js": "^0.14.4" + "zone.js": "^0.14.10" }, "devDependencies": { - "@angular-devkit/build-angular": "^17.3.2", - "@angular-eslint/builder": "17.2.0", - "@angular-eslint/eslint-plugin": "17.2.0", - "@angular-eslint/eslint-plugin-template": "17.2.0", - "@angular-eslint/schematics": "17.2.0", - "@angular-eslint/template-parser": "17.2.0", - "@angular/cli": "~17.0.9", - "@angular/compiler-cli": "^17.3.1", + "@angular-devkit/build-angular": "^18.1.4", + "@angular-eslint/builder": "18.3.0", + "@angular-eslint/eslint-plugin": "^18.3.0", + "@angular-eslint/eslint-plugin-template": "^18.3.0", + "@angular-eslint/schematics": "^18.3.0", + "@angular-eslint/template-parser": "18.3.0", + "@angular/cli": "~18.2.4", + "@angular/compiler-cli": "^18.2.4", "@types/jasmine": "~4.3.6", - "@typescript-eslint/eslint-plugin": "^6.21.0", - "@typescript-eslint/parser": "^6.21.0", + "@typescript-eslint/eslint-plugin": "^8.2.0", + "@typescript-eslint/parser": "^8.2.0", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", @@ -57,7 +59,7 @@ "karma-jasmine": "~5.1.0", "karma-jasmine-html-reporter": "~2.1.0", "prettier": "^3.2.5", - "prettier-eslint": "^16.3.0", - "typescript": "~5.2.2" + "prettier-eslint": "^13.0.0", + "typescript": "~5.5.4" } } diff --git a/modules/ui/src/app/app.component.html b/modules/ui/src/app/app.component.html index 38c210251..cb22dd809 100644 --- a/modules/ui/src/app/app.component.html +++ b/modules/ui/src/app/app.component.html @@ -29,7 +29,7 @@ route: Routes.Testing, svgIcon: 'testrun_logo_small', label: 'Testing', - name: 'testrun' + name: 'testrun', } "> @@ -40,7 +40,7 @@ route: Routes.Devices, svgIcon: 'devices', label: 'Devices', - name: 'devices' + name: 'devices', } "> @@ -51,7 +51,7 @@ route: Routes.Reports, svgIcon: 'reports', label: 'Reports', - name: 'reports' + name: 'reports', } "> @@ -62,17 +62,13 @@ route: Routes.RiskAssessment, svgIcon: 'risk_assessment', label: 'Risk Assessment', - name: 'risk-assessment' + name: 'risk-assessment', } "> + (consentShownEvent)="consentShown()">
@@ -83,7 +79,7 @@ class="app-toolbar-button app-toolbar-button-menu" aria-label="Menu" (click)="toggleMenu($event)" - (keydown.tab)="skipToNavigation($event)"> + (keydown.tab)="skipToNavigation($event, vm.focusNavigation)"> menu Testrun "> tune + + +
- - - - No ports are detected. Please define a valid ones using +
+ + + + No ports detected. Please connect and configure network and + device connections in the + + + Selected port is missing! Please define a valid one using + + System settings + panel. + + + + Further information is required in your device configurations. + Please update your + Devices + to continue testing. - - Selected port is missing! Please define a valid one using - + + + Step 1: To perform a device test, please, select ports in Testrun > panel. - - - Step 1: To perform a device test, please, select ports in - System settings - panel. - - - Step 2: To perform a device test please - Create a Device - first. - - - Step 3: Once device is created, you are able to - start testing. - - - Congratulations, the device is under test now! Do not forget to fill - Risk Assessment questionnaire. It is required to complete verification process. - + + Step 2: To perform a device test please + Create a Device + first. + + + Step 3: Once device is created, you are able to + start testing. + + + The device is now being tested. Why not take the time to complete + the device + Risk Assessment questionnaire? + +
@@ -251,6 +300,10 @@

Testrun

+ Testrun mat-button routerLink="{{ route }}" routerLinkActive="app-sidebar-button-active" + [matTooltip]="label" (keydown.enter)="onNavigationClick()"> {{ icon }} diff --git a/modules/ui/src/app/app.component.scss b/modules/ui/src/app/app.component.scss index 20e81c53e..c8cd1e7ce 100644 --- a/modules/ui/src/app/app.component.scss +++ b/modules/ui/src/app/app.component.scss @@ -59,8 +59,8 @@ $nav-open-btn-width: 210px; } .app-sidebar-button-active { - border: 1px solid mat.get-color-from-palette($color-primary, 50); - background-color: mat.get-color-from-palette($color-primary, 50); + border: 1px solid mat.m2-get-color-from-palette($color-primary, 50); + background-color: mat.m2-get-color-from-palette($color-primary, 50); } .app-sidebar-button-active > .mat-icon, @@ -133,8 +133,8 @@ $nav-open-btn-width: 210px; } .app-sidebar-button-active { - border: 1px solid mat.get-color-from-palette($color-primary, 500); - background-color: mat.get-color-from-palette($color-primary, 500); + border: 1px solid mat.m2-get-color-from-palette($color-primary, 500); + background-color: mat.m2-get-color-from-palette($color-primary, 500); } .app-sidebar-button-active > .mat-icon { @@ -186,7 +186,7 @@ $nav-open-btn-width: 210px; .app-content-main { position: relative; display: grid; - grid-template-rows: 0 auto; + grid-template-rows: auto 0 1fr; overflow: hidden; } @@ -208,3 +208,9 @@ app-version { display: flex; justify-content: center; } + +.separator { + width: 1px; + height: 28px; + background-color: $light-grey; +} diff --git a/modules/ui/src/app/app.component.spec.ts b/modules/ui/src/app/app.component.spec.ts index 81e93b4b6..bddb6546a 100644 --- a/modules/ui/src/app/app.component.spec.ts +++ b/modules/ui/src/app/app.component.spec.ts @@ -42,24 +42,24 @@ import { import { Routes } from './model/routes'; import { MockStore, provideMockStore } from '@ngrx/store/testing'; import { State } from '@ngrx/store'; -import { appFeatureKey } from './store/reducers'; import { FocusManagerService } from './services/focus-manager.service'; import { AppState } from './store/state'; +import { setIsOpenAddDevice } from './store/actions'; import { - setIsOpenAddDevice, - toggleMenu, - updateFocusNavigation, -} from './store/actions'; -import { - selectError, selectHasConnectionSettings, selectHasDevices, + selectHasExpiredDevices, selectHasRiskProfiles, selectInterfaces, + selectInternetConnection, + selectIsAllDevicesOutdated, selectIsOpenStartTestrun, selectIsOpenWaitSnackBar, - selectMenuOpened, + selectIsTestingComplete, + selectReports, + selectRiskProfiles, selectStatus, + selectSystemConfig, selectSystemStatus, } from './store/selectors'; import { MatIconTestingModule } from '@angular/material/icon/testing'; @@ -67,6 +67,13 @@ import { CertificatesComponent } from './pages/certificates/certificates.compone import { of } from 'rxjs'; import { WINDOW } from './providers/window.provider'; import { LiveAnnouncer } from '@angular/cdk/a11y'; +import { HISTORY } from './mocks/reports.mock'; +import { TestRunMqttService } from './services/test-run-mqtt.service'; +import { MOCK_ADAPTERS } from './mocks/settings.mock'; +import { WifiComponent } from './components/wifi/wifi.component'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { Profile } from './model/profile'; +import { TestrunStatus } from './model/testrun-status'; const windowMock = { location: { @@ -81,9 +88,9 @@ describe('AppComponent', () => { let router: Router; let mockService: SpyObj; let store: MockStore; - let focusNavigation = true; let mockFocusManagerService: SpyObj; let mockLiveAnnouncer: SpyObj; + let mockMqttService: SpyObj; const enterKeyEvent = new KeyboardEvent('keydown', { key: 'Enter', @@ -109,6 +116,7 @@ describe('AppComponent', () => { 'testrunInProgress', 'fetchProfiles', 'fetchCertificates', + 'getHistory', ]); mockService.fetchCertificates.and.returnValue(of([])); @@ -116,6 +124,7 @@ describe('AppComponent', () => { 'focusFirstElementInContainer', ]); mockLiveAnnouncer = jasmine.createSpyObj('mockLiveAnnouncer', ['announce']); + mockMqttService = jasmine.createSpyObj(['getNetworkAdapters']); TestBed.configureTestingModule({ imports: [ @@ -131,34 +140,34 @@ describe('AppComponent', () => { CalloutComponent, MatIconTestingModule, CertificatesComponent, + WifiComponent, + MatTooltipModule, ], providers: [ { provide: TestRunService, useValue: mockService }, { provide: LiveAnnouncer, useValue: mockLiveAnnouncer }, + { provide: TestRunMqttService, useValue: mockMqttService }, { provide: State, - useValue: { - getValue: () => ({ - [appFeatureKey]: { - appComponent: { - focusNavigation: focusNavigation, - }, - }, - }), - }, + useValue: {}, }, provideMockStore({ selectors: [ { selector: selectInterfaces, value: {} }, { selector: selectHasConnectionSettings, value: true }, - { selector: selectError, value: null }, - { selector: selectMenuOpened, value: false }, + { selector: selectInternetConnection, value: true }, + { selector: selectSystemConfig, value: { network: {} } }, { selector: selectHasDevices, value: false }, + { selector: selectIsAllDevicesOutdated, value: false }, + { selector: selectHasExpiredDevices, value: false }, { selector: selectHasRiskProfiles, value: false }, { selector: selectStatus, value: null }, { selector: selectSystemStatus, value: null }, + { selector: selectIsTestingComplete, value: false }, + { selector: selectRiskProfiles, value: [] }, { selector: selectIsOpenStartTestrun, value: false }, { selector: selectIsOpenWaitSnackBar, value: false }, + { selector: selectReports, value: [] }, ], }), { provide: FocusManagerService, useValue: mockFocusManagerService }, @@ -170,15 +179,18 @@ describe('AppComponent', () => { FakeSpinnerComponent, FakeShutdownAppComponent, FakeVersionComponent, + FakeTestingCompleteComponent, ], }); + mockMqttService.getNetworkAdapters.and.returnValue(of(MOCK_ADAPTERS)); store = TestBed.inject(MockStore); fixture = TestBed.createComponent(AppComponent); component = fixture.componentInstance; router = TestBed.get(Router); compiled = fixture.nativeElement as HTMLElement; spyOn(store, 'dispatch').and.callFake(() => {}); + component.appStore.updateSettingMissedError(null); }); it('should create the app', () => { @@ -370,17 +382,21 @@ describe('AppComponent', () => { }); it('should dispatch toggleMenu action', () => { + spyOn(component.appStore, 'toggleMenu'); + const menuBtn = compiled.querySelector( '.app-toolbar-button-menu' ) as HTMLButtonElement; menuBtn.click(); - expect(store.dispatch).toHaveBeenCalledWith(toggleMenu()); + expect(component.appStore.toggleMenu).toHaveBeenCalled(); }); it('should focus navigation on tab press if menu button was clicked', () => { - focusNavigation = true; + component.appStore.updateFocusNavigation(true); + fixture.detectChanges(); + spyOn(component.appStore, 'updateFocusNavigation'); const menuBtn = compiled.querySelector( '.app-toolbar-button-menu' ) as HTMLButtonElement; @@ -388,8 +404,8 @@ describe('AppComponent', () => { menuBtn.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab' })); const navigation = compiled.querySelector('.app-sidebar'); - expect(store.dispatch).toHaveBeenCalledWith( - updateFocusNavigation({ focusNavigation: false }) + expect(component.appStore.updateFocusNavigation).toHaveBeenCalledWith( + false ); expect( mockFocusManagerService.focusFirstElementInContainer @@ -397,7 +413,8 @@ describe('AppComponent', () => { }); it('should not focus navigation button on tab press if menu button was not clicked', () => { - focusNavigation = false; + component.appStore.updateFocusNavigation(false); + fixture.detectChanges(); const menuBtn = compiled.querySelector( '.app-toolbar-button-menu' ) as HTMLButtonElement; @@ -429,6 +446,28 @@ describe('AppComponent', () => { expect(version).toBeTruthy(); }); + it('should internet icon', () => { + fixture.detectChanges(); + const internet = compiled.querySelector('app-wifi'); + + expect(internet).toBeTruthy(); + }); + + describe('Testing complete', () => { + beforeEach(() => { + store.overrideSelector(selectIsTestingComplete, true); + fixture.detectChanges(); + }); + + it('should have testing complete component', () => { + const testingCompleteComp = compiled.querySelector( + 'app-testing-complete' + ); + + expect(testingCompleteComp).toBeTruthy(); + }); + }); + describe('Callout component visibility', () => { describe('with no connection settings', () => { beforeEach(() => { @@ -486,6 +525,16 @@ describe('AppComponent', () => { expect(callout).toBeTruthy(); expect(calloutContent).toContain('Step 3'); }); + + it('should NOT have callout component with "Step 3" if has reports', () => { + store.overrideSelector(selectReports, [...HISTORY]); + store.refreshState(); + fixture.detectChanges(); + + const callout = compiled.querySelector('app-callout'); + + expect(callout).toBeFalsy(); + }); }); describe('with systemStatus data IN Progress and without riskProfiles', () => { @@ -500,12 +549,44 @@ describe('AppComponent', () => { fixture.detectChanges(); }); - it('should have callout component with "Congratulations" text', () => { + it('should have callout component with "The device is now being tested" text', () => { const callout = compiled.querySelector('app-callout'); const calloutContent = callout?.innerHTML.trim(); expect(callout).toBeTruthy(); - expect(calloutContent).toContain('Congratulations'); + expect(calloutContent).toContain('The device is now being tested'); + }); + + it('should have callout component with "Risk Assessment" link', () => { + const callout = compiled.querySelector('app-callout'); + const calloutLinkEl = compiled.querySelector( + '.message-link' + ) as HTMLAnchorElement; + const calloutLinkContent = calloutLinkEl.innerHTML.trim(); + + expect(callout).toBeTruthy(); + expect(calloutLinkContent).toContain('Risk Assessment'); + }); + }); + + describe('with systemStatus data IN Progress and without riskProfiles', () => { + beforeEach(() => { + store.overrideSelector(selectHasConnectionSettings, true); + store.overrideSelector(selectHasDevices, true); + store.overrideSelector(selectHasRiskProfiles, false); + store.overrideSelector( + selectStatus, + MOCK_PROGRESS_DATA_IN_PROGRESS.status + ); + fixture.detectChanges(); + }); + + it('should have callout component with "The device is now being tested" text', () => { + const callout = compiled.querySelector('app-callout'); + const calloutContent = callout?.innerHTML.trim(); + + expect(callout).toBeTruthy(); + expect(calloutContent).toContain('The device is now being tested'); }); it('should have callout component with "Risk Assessment" link', () => { @@ -621,7 +702,7 @@ describe('AppComponent', () => { }); }); - describe('with devices setted, without systemStatus data, but run the tests ', () => { + describe('with devices setted, without systemStatus data, but run the tests', () => { beforeEach(() => { store.overrideSelector(selectHasDevices, true); fixture.detectChanges(); @@ -654,7 +735,7 @@ describe('AppComponent', () => { describe('error', () => { describe('with settingMissedError with one port is missed', () => { beforeEach(() => { - store.overrideSelector(selectError, { + component.appStore.updateSettingMissedError({ isSettingMissed: true, devicePortMissed: true, internetPortMissed: false, @@ -673,7 +754,7 @@ describe('AppComponent', () => { describe('with settingMissedError with two ports are missed', () => { beforeEach(() => { - store.overrideSelector(selectError, { + component.appStore.updateSettingMissedError({ isSettingMissed: true, devicePortMissed: true, internetPortMissed: true, @@ -686,13 +767,13 @@ describe('AppComponent', () => { const calloutContent = callout?.innerHTML.trim(); expect(callout).toBeTruthy(); - expect(calloutContent).toContain('No ports are detected.'); + expect(calloutContent).toContain('No ports detected.'); }); }); describe('with no settingMissedError', () => { beforeEach(() => { - store.overrideSelector(selectError, null); + component.appStore.updateSettingMissedError(null); store.overrideSelector(selectHasDevices, true); fixture.detectChanges(); }); @@ -703,6 +784,31 @@ describe('AppComponent', () => { }); }); }); + + describe('with expired devices', () => { + beforeEach(() => { + store.overrideSelector(selectHasExpiredDevices, true); + fixture.detectChanges(); + }); + + it('should have callout component', () => { + const callouts = compiled.querySelectorAll('app-callout'); + let hasExpiredDeviceCallout = false; + callouts.forEach(callout => { + if ( + callout?.innerHTML + .trim() + .includes( + 'Further information is required in your device configurations.' + ) + ) { + hasExpiredDeviceCallout = true; + } + }); + + expect(hasExpiredDeviceCallout).toBeTrue(); + }); + }); }); it('should not call toggleSettingsBtn focus on closeSetting when device length is 0', async () => { @@ -738,6 +844,15 @@ describe('AppComponent', () => { expect(component.certDrawer.open).toHaveBeenCalledTimes(1); }); + + it('should set focus to first focusable elem when close callout', fakeAsync(() => { + component.calloutClosed('mockId'); + tick(100); + + expect( + mockFocusManagerService.focusFirstElementInContainer + ).toHaveBeenCalled(); + })); }); @Component({ @@ -771,7 +886,14 @@ class FakeShutdownAppComponent { }) class FakeVersionComponent { @Input() consentShown!: boolean; - @Input() hasRiskProfiles!: boolean; @Output() consentShownEvent = new EventEmitter(); - @Output() navigateToRiskAssessmentEvent = new EventEmitter(); +} + +@Component({ + selector: 'app-testing-complete', + template: '
', +}) +class FakeTestingCompleteComponent { + @Input() profiles: Profile[] = []; + @Input() data!: TestrunStatus | null; } diff --git a/modules/ui/src/app/app.component.ts b/modules/ui/src/app/app.component.ts index 341f6bab5..ba8d33831 100644 --- a/modules/ui/src/app/app.component.ts +++ b/modules/ui/src/app/app.component.ts @@ -13,7 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Component, ElementRef, ViewChild } from '@angular/core'; +import { + AfterViewInit, + Component, + ElementRef, + QueryList, + ViewChild, + ViewChildren, +} from '@angular/core'; import { MatIconRegistry } from '@angular/material/icon'; import { DomSanitizer } from '@angular/platform-browser'; import { MatDrawer } from '@angular/material/sidenav'; @@ -24,17 +31,13 @@ import { Routes } from './model/routes'; import { FocusManagerService } from './services/focus-manager.service'; import { State, Store } from '@ngrx/store'; import { AppState } from './store/state'; -import { - setIsOpenAddDevice, - toggleMenu, - updateFocusNavigation, -} from './store/actions'; -import { appFeatureKey } from './store/reducers'; +import { setIsOpenAddDevice } from './store/actions'; import { SettingsComponent } from './pages/settings/settings.component'; import { AppStore } from './app.store'; import { TestRunService } from './services/test-run.service'; import { LiveAnnouncer } from '@angular/cdk/a11y'; import { filter, take } from 'rxjs/operators'; +import { skip, timer } from 'rxjs'; const DEVICES_LOGO_URL = '/assets/icons/devices.svg'; const DEVICES_RUN_URL = '/assets/icons/device_run.svg'; @@ -44,6 +47,8 @@ const TESTRUN_LOGO_URL = '/assets/icons/testrun_logo_small.svg'; const TESTRUN_LOGO_COLOR_URL = '/assets/icons/testrun_logo_color.svg'; const CLOSE_URL = '/assets/icons/close.svg'; const DRAFT_URL = '/assets/icons/draft.svg'; +const PILOT_URL = '/assets/icons/pilot.svg'; +const QUALIFICATION_URL = '/assets/icons/qualification.svg'; @Component({ selector: 'app-root', @@ -51,7 +56,7 @@ const DRAFT_URL = '/assets/icons/draft.svg'; styleUrls: ['./app.component.scss'], providers: [AppStore], }) -export class AppComponent { +export class AppComponent implements AfterViewInit { public readonly CalloutType = CalloutType; public readonly StatusOfTestrun = StatusOfTestrun; public readonly Routes = Routes; @@ -64,6 +69,8 @@ export class AppComponent { public toggleCertificatesBtn!: HTMLButtonElement; @ViewChild('navigation') public navigation!: ElementRef; @ViewChild('settings') public settings!: SettingsComponent; + @ViewChildren('riskAssessmentLink') + riskAssessmentLink!: QueryList; viewModel$ = this.appStore.viewModel$; constructor( @@ -80,6 +87,9 @@ export class AppComponent { this.appStore.getDevices(); this.appStore.getRiskProfiles(); this.appStore.getSystemStatus(); + this.appStore.getReports(); + this.appStore.getTestModules(); + this.appStore.getNetworkAdapters(); this.matIconRegistry.addSvgIcon( 'devices', this.domSanitizer.bypassSecurityTrustResourceUrl(DEVICES_LOGO_URL) @@ -112,14 +122,49 @@ export class AppComponent { 'draft', this.domSanitizer.bypassSecurityTrustResourceUrl(DRAFT_URL) ); + this.matIconRegistry.addSvgIcon( + 'pilot', + this.domSanitizer.bypassSecurityTrustResourceUrl(PILOT_URL) + ); + this.matIconRegistry.addSvgIcon( + 'qualification', + this.domSanitizer.bypassSecurityTrustResourceUrl(QUALIFICATION_URL) + ); + } + + ngAfterViewInit() { + this.viewModel$ + .pipe( + filter(({ isStatusLoaded }) => isStatusLoaded === true), + take(1) + ) + .subscribe(({ systemStatus }) => { + let skipCount = 0; + if (systemStatus === StatusOfTestrun.InProgress) { + // link should not be focused after page is just loaded + skipCount = 1; + } + this.riskAssessmentLink.changes.pipe(skip(skipCount)).subscribe(() => { + if (this.riskAssessmentLink.length > 0) { + this.riskAssessmentLink.first.nativeElement.focus(); + } + }); + }); } get isRiskAssessmentRoute(): boolean { return this.route.url === Routes.RiskAssessment; } + get isDevicesRoute(): boolean { + return this.route.url === Routes.Devices; + } + navigateToDeviceRepository(): void { this.route.navigate([Routes.Devices]); + } + navigateToAddDevice(): void { + this.route.navigate([Routes.Devices]); this.store.dispatch(setIsOpenAddDevice({ isOpenAddDevice: true })); } @@ -129,7 +174,9 @@ export class AppComponent { } navigateToRiskAssessment(): void { - this.route.navigate([Routes.RiskAssessment]); + this.route.navigate([Routes.RiskAssessment]).then(() => { + this.appStore.setFocusOnPage(); + }); } async closeCertificates(): Promise { @@ -153,19 +200,19 @@ export class AppComponent { public toggleMenu(event: MouseEvent) { event.stopPropagation(); - this.store.dispatch(toggleMenu()); + this.appStore.toggleMenu(); } /** * When side menu is opened */ - skipToNavigation(event: Event) { - if (this.state.getValue()[appFeatureKey].appComponent.focusNavigation) { + skipToNavigation(event: Event, focusNavigation: boolean) { + if (focusNavigation) { event.preventDefault(); // if not prevented, second element will be focused this.focusManagerService.focusFirstElementInContainer( this.navigation.nativeElement ); - this.store.dispatch(updateFocusNavigation({ focusNavigation: false })); // user will be navigated according to normal flow on tab + this.appStore.updateFocusNavigation(false); // user will be navigated according to normal flow on tab } } @@ -204,4 +251,13 @@ export class AppComponent { this.appStore.setFocusOnPage(); }); } + + calloutClosed(id: string | null) { + if (id) { + this.appStore.setCloseCallout(id); + timer(100).subscribe(() => { + this.focusManagerService.focusFirstElementInContainer(); + }); + } + } } diff --git a/modules/ui/src/app/app.module.ts b/modules/ui/src/app/app.module.ts index 78621a464..4ab788cbc 100644 --- a/modules/ui/src/app/app.module.ts +++ b/modules/ui/src/app/app.module.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http'; -import { NgModule } from '@angular/core'; +import { importProvidersFrom, NgModule } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatButtonToggleModule } from '@angular/material/button-toggle'; import { MatIconModule } from '@angular/material/icon'; @@ -49,6 +49,16 @@ import { ShutdownAppComponent } from './components/shutdown-app/shutdown-app.com import { WindowProvider } from './providers/window.provider'; import { CertificatesComponent } from './pages/certificates/certificates.component'; import { LOADER_TIMEOUT_CONFIG_TOKEN } from './services/loaderConfig'; +import { WifiComponent } from './components/wifi/wifi.component'; +import { TestingCompleteComponent } from './components/testing-complete/testing-complete.component'; + +import { MqttModule, IMqttServiceOptions } from 'ngx-mqtt'; +import { MatNativeDateModule } from '@angular/material/core'; + +export const MQTT_SERVICE_OPTIONS: IMqttServiceOptions = { + hostname: window.location.hostname, + port: 9001, +}; @NgModule({ declarations: [AppComponent, SettingsComponent], @@ -79,6 +89,9 @@ import { LOADER_TIMEOUT_CONFIG_TOKEN } from './services/loaderConfig'; SettingsDropdownComponent, ShutdownAppComponent, CertificatesComponent, + MqttModule.forRoot(MQTT_SERVICE_OPTIONS), + WifiComponent, + TestingCompleteComponent, ], providers: [ WindowProvider, @@ -93,6 +106,7 @@ import { LOADER_TIMEOUT_CONFIG_TOKEN } from './services/loaderConfig'; multi: true, }, { provide: LOADER_TIMEOUT_CONFIG_TOKEN, useValue: 1000 }, + importProvidersFrom(MatNativeDateModule), ], bootstrap: [AppComponent], }) diff --git a/modules/ui/src/app/app.store.spec.ts b/modules/ui/src/app/app.store.spec.ts index 2bdf63195..300a250fd 100644 --- a/modules/ui/src/app/app.store.spec.ts +++ b/modules/ui/src/app/app.store.spec.ts @@ -15,31 +15,44 @@ */ import { fakeAsync, TestBed, tick } from '@angular/core/testing'; import { of, skip, take } from 'rxjs'; -import { AppStore, CONSENT_SHOWN_KEY } from './app.store'; +import { AppStore, CALLOUT_STATE_KEY, CONSENT_SHOWN_KEY } from './app.store'; import { MockStore, provideMockStore } from '@ngrx/store/testing'; import { AppState } from './store/state'; import { - selectError, selectHasConnectionSettings, selectHasDevices, + selectHasExpiredDevices, selectHasRiskProfiles, selectInterfaces, + selectInternetConnection, + selectIsAllDevicesOutdated, selectIsOpenWaitSnackBar, - selectMenuOpened, + selectIsTestingComplete, + selectReports, + selectRiskProfiles, selectStatus, + selectSystemConfig, + selectSystemStatus, + selectTestModules, } from './store/selectors'; import { TestRunService } from './services/test-run.service'; import SpyObj = jasmine.SpyObj; -import { device } from './mocks/device.mock'; +import { device, MOCK_MODULES, MOCK_TEST_MODULES } from './mocks/device.mock'; import { + fetchReports, fetchRiskProfiles, fetchSystemStatus, setDevices, + updateAdapters, + setTestModules, } from './store/actions'; import { MOCK_PROGRESS_DATA_IN_PROGRESS } from './mocks/testrun.mock'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { NotificationService } from './services/notification.service'; import { FocusManagerService } from './services/focus-manager.service'; +import { TestRunMqttService } from './services/test-run-mqtt.service'; +import { MOCK_ADAPTERS } from './mocks/settings.mock'; +import { TestingType } from './model/device'; const mock = (() => { let store: { [key: string]: string } = {}; @@ -50,6 +63,12 @@ const mock = (() => { setItem: (key: string, value: string) => { store[key] = value + ''; }, + getObject: (key: string) => { + return store[key] || null; + }, + setObject: (key: string, value: object) => { + store[key] = JSON.stringify(value); + }, clear: () => { store = {}; }, @@ -65,15 +84,22 @@ describe('AppStore', () => { let mockService: SpyObj; let mockNotificationService: SpyObj; let mockFocusManagerService: SpyObj; + let mockMqttService: SpyObj; beforeEach(() => { - mockService = jasmine.createSpyObj('mockService', ['fetchDevices']); + window.sessionStorage.clear(); + + mockService = jasmine.createSpyObj('mockService', [ + 'fetchDevices', + 'getTestModules', + ]); mockNotificationService = jasmine.createSpyObj('mockNotificationService', [ 'notify', ]); mockFocusManagerService = jasmine.createSpyObj([ 'focusFirstElementInContainer', ]); + mockMqttService = jasmine.createSpyObj(['getNetworkAdapters']); TestBed.configureTestingModule({ providers: [ @@ -82,11 +108,20 @@ describe('AppStore', () => { selectors: [ { selector: selectStatus, value: null }, { selector: selectIsOpenWaitSnackBar, value: false }, + { selector: selectTestModules, value: MOCK_TEST_MODULES }, + { selector: selectInternetConnection, value: false }, + { selector: selectIsAllDevicesOutdated, value: false }, + { selector: selectSystemStatus, value: null }, + { selector: selectIsTestingComplete, value: false }, + { selector: selectRiskProfiles, value: [] }, + { selector: selectSystemConfig, value: { network: {} } }, + { selector: selectInterfaces, value: {} }, ], }), { provide: TestRunService, useValue: mockService }, { provide: NotificationService, useValue: mockNotificationService }, { provide: FocusManagerService, useValue: mockFocusManagerService }, + { provide: TestRunMqttService, useValue: mockMqttService }, ], imports: [BrowserAnimationsModule], }); @@ -95,11 +130,11 @@ describe('AppStore', () => { appStore = TestBed.inject(AppStore); store.overrideSelector(selectHasDevices, true); + store.overrideSelector(selectHasExpiredDevices, true); store.overrideSelector(selectHasRiskProfiles, false); + store.overrideSelector(selectReports, []); store.overrideSelector(selectHasConnectionSettings, true); - store.overrideSelector(selectMenuOpened, true); store.overrideSelector(selectInterfaces, {}); - store.overrideSelector(selectError, null); store.overrideSelector(selectStatus, null); spyOn(store, 'dispatch').and.callFake(() => {}); @@ -139,13 +174,22 @@ describe('AppStore', () => { expect(store).toEqual({ consentShown: false, hasDevices: true, + hasExpiredDevices: true, + isAllDevicesOutdated: false, hasRiskProfiles: false, + reports: [], isStatusLoaded: false, systemStatus: null, + testrunStatus: null, + isTestingComplete: false, + riskProfiles: [], hasConnectionSettings: true, - isMenuOpen: true, + isMenuOpen: false, interfaces: {}, + focusNavigation: false, settingMissedError: null, + calloutState: new Map(), + hasInternetConnection: false, }); done(); }); @@ -226,5 +270,255 @@ describe('AppStore', () => { ).toHaveBeenCalled(); })); }); + + describe('getReports', () => { + it('should dispatch fetchReports', () => { + appStore.getReports(); + + expect(store.dispatch).toHaveBeenCalledWith(fetchReports()); + }); + }); + + describe('getTestModules', () => { + const modules = [...MOCK_MODULES]; + + beforeEach(() => { + mockService.getTestModules.and.returnValue(of(modules)); + }); + + it('should dispatch action setDevices', () => { + appStore.getTestModules(); + + expect(store.dispatch).toHaveBeenCalledWith( + setTestModules({ + testModules: [ + { + displayName: 'Connection', + name: 'connection', + enabled: true, + }, + { + displayName: 'Udmi', + name: 'udmi', + enabled: true, + }, + ], + }) + ); + }); + }); + + describe('getNetworkAdapters', () => { + const adapters = MOCK_ADAPTERS; + + beforeEach(() => { + mockMqttService.getNetworkAdapters.and.returnValue(of(adapters)); + }); + + it('should dispatch action setDevices', () => { + appStore.getNetworkAdapters(); + + expect(store.dispatch).toHaveBeenCalledWith( + updateAdapters({ adapters }) + ); + }); + + it('should notify about new adapters', () => { + appStore.getNetworkAdapters(); + + expect(mockNotificationService.notify).toHaveBeenCalledWith( + 'New network adapter(s) mockNewInternetKey has been detected. You can switch to using it in the System settings menu' + ); + }); + }); + + describe('setCloseCallout', () => { + it('should update store', done => { + appStore.viewModel$.pipe(skip(1), take(1)).subscribe(store => { + expect(store.calloutState.get('test')).toEqual(true); + done(); + }); + + appStore.setCloseCallout('test'); + }); + + it('should update storage', () => { + appStore.setCloseCallout('test'); + + expect(mock.getObject(CALLOUT_STATE_KEY)).toBeTruthy(); + }); + }); + + describe('checkInterfacesInConfig', () => { + it('should update settingMissedError with all false if all ports are present', done => { + appStore.viewModel$.pipe(skip(1), take(1)).subscribe(store => { + expect(store.settingMissedError).toEqual({ + isSettingMissed: false, + devicePortMissed: false, + internetPortMissed: false, + }); + done(); + }); + + store.overrideSelector(selectInterfaces, { + enx00e04c020fa8: '00:e0:4c:02:0f:a8', + enx207bd26205e9: '20:7b:d2:62:05:e9', + }); + store.overrideSelector(selectSystemConfig, { + network: { + device_intf: 'enx00e04c020fa8', + internet_intf: 'enx207bd26205e9', + }, + }); + store.refreshState(); + }); + + it('should update settingMissedError with all true if all ports are missing', done => { + appStore.viewModel$.pipe(skip(1), take(1)).subscribe(store => { + expect(store.settingMissedError).toEqual({ + isSettingMissed: true, + devicePortMissed: true, + internetPortMissed: true, + }); + done(); + }); + + store.overrideSelector(selectInterfaces, { + enx00e04c020fa9: '00:e0:4c:02:0f:a8', + enx207bd26205e8: '20:7b:d2:62:05:e9', + }); + store.overrideSelector(selectSystemConfig, { + network: { + device_intf: 'enx00e04c020fa8', + internet_intf: 'enx207bd26205e9', + }, + }); + store.refreshState(); + }); + + it('should update settingMissedError with devicePortMissed true if device port is missing', done => { + appStore.viewModel$.pipe(skip(1), take(1)).subscribe(store => { + expect(store.settingMissedError).toEqual({ + isSettingMissed: true, + devicePortMissed: true, + internetPortMissed: false, + }); + done(); + }); + + store.overrideSelector(selectInterfaces, { + enx00e04c020fa9: '00:e0:4c:02:0f:a8', + enx207bd26205e8: '20:7b:d2:62:05:e9', + }); + store.overrideSelector(selectSystemConfig, { + network: { + device_intf: 'enx00e04c020fa8', + internet_intf: 'enx207bd26205e8', + }, + }); + store.refreshState(); + }); + + it('should update settingMissedError with internetPortMissed true if device internet is missing', done => { + appStore.viewModel$.pipe(skip(1), take(1)).subscribe(store => { + expect(store.settingMissedError).toEqual({ + isSettingMissed: true, + devicePortMissed: false, + internetPortMissed: true, + }); + done(); + }); + + store.overrideSelector(selectInterfaces, { + enx00e04c020fa9: '00:e0:4c:02:0f:a8', + enx207bd26205e8: '20:7b:d2:62:05:e9', + }); + store.overrideSelector(selectSystemConfig, { + network: { + device_intf: 'enx00e04c020fa9', + internet_intf: 'enx207bd26205e9', + }, + }); + store.refreshState(); + }); + + it('should update settingMissedError with all false if interface are not empty and config is not set', done => { + appStore.viewModel$.pipe(skip(1), take(1)).subscribe(store => { + expect(store.settingMissedError).toEqual({ + isSettingMissed: false, + devicePortMissed: false, + internetPortMissed: false, + }); + done(); + }); + + store.overrideSelector(selectInterfaces, { + enx00e04c020fa8: '00:e0:4c:02:0f:a8', + enx207bd26205e9: '20:7b:d2:62:05:e9', + }); + store.overrideSelector(selectSystemConfig, { + network: { + device_intf: '', + internet_intf: '', + }, + }); + store.refreshState(); + }); + + it('should update settingMissedError with all false if interface are empty and config is not set', done => { + appStore.viewModel$.pipe(skip(1), take(1)).subscribe(store => { + expect(store.settingMissedError).toEqual({ + isSettingMissed: false, + devicePortMissed: false, + internetPortMissed: false, + }); + done(); + }); + + store.overrideSelector(selectInterfaces, {}); + store.overrideSelector(selectSystemConfig, { + network: { + device_intf: '', + internet_intf: '', + }, + }); + store.refreshState(); + }); + + it('should send GA event', done => { + // @ts-expect-error data layer should be defined + window.dataLayer = window.dataLayer || []; + + appStore.viewModel$.pipe(skip(1), take(1)).subscribe(() => { + expect( + // @ts-expect-error data layer should be defined + window.dataLayer.some(event => event.event === 'pilot_is_compliant') + ).toBeTrue(); + done(); + }); + + store.overrideSelector(selectIsTestingComplete, true); + store.overrideSelector(selectSystemStatus, { + status: 'Compliant', + mac_addr: '00:1e:42:35:73:c4', + device: { + manufacturer: 'Delta', + model: '03-DIN-CPU', + mac_addr: '00:1e:42:35:73:c4', + firmware: '1.2.2', + test_pack: TestingType.Pilot, + }, + started: '2023-06-22T09:20:00.123Z', + finished: '2023-06-22T09:26:00.123Z', + report: 'https://api.testrun.io/report.pdf', + tags: [], + tests: { + total: 3, + results: [], + }, + }); + store.refreshState(); + }); + }); }); }); diff --git a/modules/ui/src/app/app.store.ts b/modules/ui/src/app/app.store.ts index 9bd8dcff4..a12a536a3 100644 --- a/modules/ui/src/app/app.store.ts +++ b/modules/ui/src/app/app.store.ts @@ -16,63 +16,118 @@ import { Injectable } from '@angular/core'; import { ComponentStore } from '@ngrx/component-store'; -import { tap } from 'rxjs/operators'; +import { tap, withLatestFrom } from 'rxjs/operators'; import { - selectError, selectHasConnectionSettings, selectHasDevices, + selectHasExpiredDevices, selectHasRiskProfiles, selectInterfaces, - selectMenuOpened, + selectInternetConnection, + selectIsAllDevicesOutdated, + selectIsTestingComplete, + selectReports, + selectRiskProfiles, selectStatus, + selectSystemConfig, + selectSystemStatus, } from './store/selectors'; import { Store } from '@ngrx/store'; import { AppState } from './store/state'; import { TestRunService } from './services/test-run.service'; -import { delay, exhaustMap, Observable, skip } from 'rxjs'; -import { Device } from './model/device'; +import { + combineLatest, + delay, + exhaustMap, + filter, + Observable, + skip, +} from 'rxjs'; +import { Device, TestingType, TestModule } from './model/device'; import { setDevices, setIsOpenStartTestrun, fetchSystemStatus, fetchRiskProfiles, + fetchReports, + setTestModules, + updateAdapters, } from './store/actions'; -import { TestrunStatus } from './model/testrun-status'; -import { SettingMissedError, SystemInterfaces } from './model/setting'; +import { StatusOfTestrun, TestrunStatus } from './model/testrun-status'; +import { + Adapters, + SettingMissedError, + SystemConfig, + SystemInterfaces, +} from './model/setting'; import { FocusManagerService } from './services/focus-manager.service'; +import { TestRunMqttService } from './services/test-run-mqtt.service'; +import { NotificationService } from './services/notification.service'; +import { Profile } from './model/profile'; export const CONSENT_SHOWN_KEY = 'CONSENT_SHOWN'; +export const CALLOUT_STATE_KEY = 'CALLOUT_STATE'; export interface AppComponentState { consentShown: boolean; isStatusLoaded: boolean; systemStatus: TestrunStatus | null; + calloutState: Map; + isMenuOpen: boolean; + /** + * Indicates, if side menu should be focused on keyboard navigation after menu is opened + */ + focusNavigation: boolean; + settingMissedError: SettingMissedError | null; } @Injectable() export class AppStore extends ComponentStore { private consentShown$ = this.select(state => state.consentShown); + private calloutState$ = this.select(state => state.calloutState); private isStatusLoaded$ = this.select(state => state.isStatusLoaded); + private hasInternetConnection$ = this.store.select(selectInternetConnection); private hasDevices$ = this.store.select(selectHasDevices); + private isAllDevicesOutdated$ = this.store.select(selectIsAllDevicesOutdated); + private hasExpiredDevices$ = this.store.select(selectHasExpiredDevices); private hasRiskProfiles$ = this.store.select(selectHasRiskProfiles); + private reports$ = this.store.select(selectReports); private hasConnectionSetting$ = this.store.select( selectHasConnectionSettings ); - private isMenuOpen$ = this.store.select(selectMenuOpened); + private isMenuOpened$ = this.select(state => state.isMenuOpen); + private focusNavigation$ = this.select(state => state.focusNavigation); private interfaces$: Observable = this.store.select(selectInterfaces); + private systemConfig$: Observable = + this.store.select(selectSystemConfig); private settingMissedError$: Observable = - this.store.select(selectError); + this.select(state => state.settingMissedError); systemStatus$: Observable = this.store.select(selectStatus); + testrunStatus$: Observable = + this.store.select(selectSystemStatus); + isTestingComplete$: Observable = this.store.select( + selectIsTestingComplete + ); + riskProfiles$: Observable = this.store.select(selectRiskProfiles); viewModel$ = this.select({ consentShown: this.consentShown$, hasDevices: this.hasDevices$, + isAllDevicesOutdated: this.isAllDevicesOutdated$, + hasExpiredDevices: this.hasExpiredDevices$, hasRiskProfiles: this.hasRiskProfiles$, + reports: this.reports$, isStatusLoaded: this.isStatusLoaded$, systemStatus: this.systemStatus$, + testrunStatus: this.testrunStatus$, + isTestingComplete: this.isTestingComplete$, + riskProfiles: this.riskProfiles$, hasConnectionSettings: this.hasConnectionSetting$, - isMenuOpen: this.isMenuOpen$, + isMenuOpen: this.isMenuOpened$, interfaces: this.interfaces$, settingMissedError: this.settingMissedError$, + calloutState: this.calloutState$, + hasInternetConnection: this.hasInternetConnection$, + focusNavigation: this.focusNavigation$, }); updateConsent = this.updater((state, consentShown: boolean) => ({ @@ -80,11 +135,39 @@ export class AppStore extends ComponentStore { consentShown, })); + updateCalloutState = this.updater((state, callout: string) => { + const calloutState = state.calloutState; + calloutState.set(callout, true); + // @ts-expect-error property is defined in index.html + sessionStorage.setObject(CALLOUT_STATE_KEY, calloutState); + return { + ...state, + calloutState: new Map(calloutState), + }; + }); + updateIsStatusLoaded = this.updater((state, isStatusLoaded: boolean) => ({ ...state, isStatusLoaded, })); + updateFocusNavigation = this.updater((state, focusNavigation: boolean) => ({ + ...state, + focusNavigation, + })); + + updateIsMenuOpened = this.updater((state, isMenuOpen: boolean) => ({ + ...state, + isMenuOpen, + })); + + updateSettingMissedError = this.updater( + (state, settingMissedError: SettingMissedError | null) => ({ + ...state, + settingMissedError, + }) + ); + setContent = this.effect(trigger$ => { return trigger$.pipe( tap(() => { @@ -131,6 +214,27 @@ export class AppStore extends ComponentStore { ); }); + getNetworkAdapters = this.effect(trigger$ => { + return trigger$.pipe( + exhaustMap(() => { + return this.testRunMqttService.getNetworkAdapters().pipe( + tap((adapters: Adapters) => { + if (adapters.adapters_added) { + this.notifyAboutTheAdapters(adapters.adapters_added); + } + this.store.dispatch(updateAdapters({ adapters })); + }) + ); + }) + ); + }); + + private notifyAboutTheAdapters(adapters: SystemInterfaces) { + this.notificationService.notify( + `New network adapter(s) ${Object.keys(adapters).join(', ')} has been detected. You can switch to using it in the System settings menu` + ); + } + setIsOpenStartTestrun = this.effect(trigger$ => { return trigger$.pipe( tap(() => { @@ -150,15 +254,117 @@ export class AppStore extends ComponentStore { ); }); + getReports = this.effect(trigger$ => { + return trigger$.pipe( + tap(() => { + this.store.dispatch(fetchReports()); + }) + ); + }); + + getTestModules = this.effect(trigger$ => { + return trigger$.pipe( + exhaustMap(() => { + return this.testRunService.getTestModules().pipe( + tap((testModules: string[]) => { + this.store.dispatch( + setTestModules({ + testModules: testModules.map( + module => + ({ + displayName: module, + name: module.toLowerCase(), + enabled: true, + }) as TestModule + ), + }) + ); + }) + ); + }) + ); + }); + + setCloseCallout = this.effect(trigger$ => { + return trigger$.pipe( + tap((id: string) => { + this.updateCalloutState(id); + }) + ); + }); + + toggleMenu = this.effect(trigger$ => { + return trigger$.pipe( + withLatestFrom(this.isMenuOpened$), + tap(([, opened]) => { + this.updateIsMenuOpened(!opened); + if (!opened) { + this.updateFocusNavigation(true); + } + }) + ); + }); + + checkInterfacesInConfig = this.effect(() => { + return combineLatest([ + this.interfaces$.pipe(skip(1)), + this.systemConfig$.pipe(skip(1)), + ]).pipe( + filter(([, { network }]) => network !== null), + tap(([interfaces, { network, single_intf }]) => { + const deviceValid = + network?.device_intf == '' || + (!!network?.device_intf && !!interfaces[network.device_intf]); + const internetValid = single_intf + ? true + : network?.internet_intf == '' || + (!!network?.internet_intf && !!interfaces[network.internet_intf]); + this.updateSettingMissedError({ + isSettingMissed: !deviceValid || !internetValid, + devicePortMissed: !deviceValid, + internetPortMissed: !internetValid, + }); + }) + ); + }); + + sendGAEvent = this.effect(() => { + return combineLatest([this.isTestingComplete$, this.testrunStatus$]).pipe( + filter(([isTestingComplete]) => isTestingComplete === true), + filter( + ([, testrunStatus]) => + testrunStatus?.status === StatusOfTestrun.Compliant && + testrunStatus?.device.test_pack === TestingType.Pilot + ), + tap(() => { + // @ts-expect-error data layer is not null + window.dataLayer.push({ + event: 'pilot_is_compliant', + }); + }) + ); + }); + constructor( private store: Store, private testRunService: TestRunService, - private focusManagerService: FocusManagerService + private testRunMqttService: TestRunMqttService, + private focusManagerService: FocusManagerService, + private notificationService: NotificationService ) { + // @ts-expect-error get object is defined in index.html + const calloutState = sessionStorage.getObject(CALLOUT_STATE_KEY); + super({ consentShown: sessionStorage.getItem(CONSENT_SHOWN_KEY) !== null, isStatusLoaded: false, systemStatus: null, + calloutState: calloutState + ? new Map(Object.entries(calloutState)) + : new Map(), + isMenuOpen: false, + focusNavigation: false, + settingMissedError: null, }); } } diff --git a/modules/ui/src/app/components/callout/callout.component.html b/modules/ui/src/app/components/callout/callout.component.html index a3fb7930c..36f0c981f 100644 --- a/modules/ui/src/app/components/callout/callout.component.html +++ b/modules/ui/src/app/components/callout/callout.component.html @@ -15,12 +15,23 @@ -->
{{ type }} + +

+
diff --git a/modules/ui/src/app/components/callout/callout.component.scss b/modules/ui/src/app/components/callout/callout.component.scss index 8a9d77125..bd1008638 100644 --- a/modules/ui/src/app/components/callout/callout.component.scss +++ b/modules/ui/src/app/components/callout/callout.component.scss @@ -21,25 +21,13 @@ width: 100%; } -:host:has(.callout-container.info), -:host:has(.callout-container.error), -:host:has(.callout-container.check_circle) { - position: absolute; -} - -:host + ::ng-deep app-callout { - top: 60px; -} - -@media (width < 742px) { - :host + ::ng-deep app-callout { - top: 80px; - } -} +:host .info-pilot ::ng-deep app-program-type-icon { + padding-right: 1px; -@media (width < 490px) { - :host + ::ng-deep app-callout { - top: 100px; + .icon { + width: 18px; + height: 18px; + line-height: 18px; } } @@ -61,10 +49,16 @@ .callout-container.info { margin: 24px 32px; - background-color: mat.get-color-from-palette($color-primary, 50); + background-color: mat.m2-get-color-from-palette($color-primary, 50); .callout-icon { - color: mat.get-color-from-palette($color-primary, 700); + color: mat.m2-get-color-from-palette($color-primary, 700); + } + + .info-pilot { + width: 24px; + display: flex; + justify-content: center; } } @@ -117,3 +111,9 @@ line-height: 20px; letter-spacing: 0.2px; } + +.callout-close-button { + margin-left: auto; + margin-right: -20px; + color: $warn; +} diff --git a/modules/ui/src/app/components/callout/callout.component.spec.ts b/modules/ui/src/app/components/callout/callout.component.spec.ts index 28215ec7c..fab52c949 100644 --- a/modules/ui/src/app/components/callout/callout.component.spec.ts +++ b/modules/ui/src/app/components/callout/callout.component.spec.ts @@ -16,6 +16,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { CalloutComponent } from './callout.component'; +import { MatIconTestingModule } from '@angular/material/icon/testing'; describe('CalloutComponent', () => { let component: CalloutComponent; @@ -24,7 +25,7 @@ describe('CalloutComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [CalloutComponent], + imports: [CalloutComponent, MatIconTestingModule], }).compileComponents(); fixture = TestBed.createComponent(CalloutComponent); component = fixture.componentInstance; @@ -42,4 +43,27 @@ describe('CalloutComponent', () => { expect(calloutContainerdEl?.classList).toContain('mockValue'); }); + + describe('closeable', () => { + beforeEach(() => { + fixture.componentRef.setInput('closable', true); + fixture.detectChanges(); + }); + + it('should have close button', () => { + const closeButton = compiled.querySelector('.callout-close-button'); + + expect(closeButton).toBeTruthy(); + }); + + it('should emit event', () => { + const calloutClosedSpy = spyOn(component.calloutClosed, 'emit'); + const closeButton = compiled.querySelector( + '.callout-close-button' + ) as HTMLButtonElement; + closeButton?.click(); + + expect(calloutClosedSpy).toHaveBeenCalled(); + }); + }); }); diff --git a/modules/ui/src/app/components/callout/callout.component.ts b/modules/ui/src/app/components/callout/callout.component.ts index bfb6ea9bf..8fa46d9f0 100644 --- a/modules/ui/src/app/components/callout/callout.component.ts +++ b/modules/ui/src/app/components/callout/callout.component.ts @@ -13,18 +13,38 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + Input, + Output, +} from '@angular/core'; import { CommonModule } from '@angular/common'; import { MatIconModule } from '@angular/material/icon'; +import { MatButtonModule } from '@angular/material/button'; +import { CalloutType } from '../../model/callout-type'; +import { ProgramType } from '../../model/program-type'; +import { ProgramTypeIconComponent } from '../program-type-icon/program-type-icon.component'; @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + CommonModule, + MatIconModule, + MatButtonModule, + ProgramTypeIconComponent, + ], selector: 'app-callout', standalone: true, - imports: [CommonModule, MatIconModule], - templateUrl: './callout.component.html', styleUrls: ['./callout.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, + templateUrl: './callout.component.html', }) export class CalloutComponent { + readonly CalloutType = CalloutType; + readonly ProgramType = ProgramType; + @Input() id: string | null = null; @Input() type = ''; + @Input() closable = false; + @Output() calloutClosed = new EventEmitter(); } diff --git a/modules/ui/src/app/components/component-with-announcement.ts b/modules/ui/src/app/components/component-with-announcement.ts new file mode 100644 index 000000000..0f555e15a --- /dev/null +++ b/modules/ui/src/app/components/component-with-announcement.ts @@ -0,0 +1,35 @@ +import { LiveAnnouncer } from '@angular/cdk/a11y'; +import { FocusManagerService } from '../services/focus-manager.service'; +import { timer } from 'rxjs'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Constructor = new (...args: any[]) => T; + +export function ComponentWithAnnouncement(base: T) { + return class extends base { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + constructor(...args: any[]) { + const [dialogRef, title, liveAnnouncer, focusService] = args; + super(...args); + + this.focusService = focusService; + + dialogRef.afterOpened().subscribe(() => { + (liveAnnouncer as LiveAnnouncer).clear(); + (liveAnnouncer as LiveAnnouncer) + .announce(title, 'assertive') + .then(() => { + timer(200).subscribe(() => { + this.focusService.focusFirstElementInContainer(); + }); + }); + }); + + dialogRef.beforeClosed().subscribe(() => { + (liveAnnouncer as LiveAnnouncer).clear(); + }); + } + + focusService!: FocusManagerService; + }; +} diff --git a/modules/ui/src/app/components/device-item/device-item.component.html b/modules/ui/src/app/components/device-item/device-item.component.html index 8f85b6edf..f1586f9a0 100644 --- a/modules/ui/src/app/components/device-item/device-item.component.html +++ b/modules/ui/src/app/components/device-item/device-item.component.html @@ -14,26 +14,46 @@ limitations under the License. --> + +
+ +
+ + +

+ {{ device.test_pack }} + + + {{ device.manufacturer }} +

+

{{ device.model }} -

-
+

+

{{ device.mac_addr }} -

- +

+
- - + + + + +
+ + +
- - - - - - diff --git a/modules/ui/src/app/components/download-zip-modal/download-zip-modal.component.scss b/modules/ui/src/app/components/download-zip-modal/download-zip-modal.component.scss index 3524cb936..dd937047a 100644 --- a/modules/ui/src/app/components/download-zip-modal/download-zip-modal.component.scss +++ b/modules/ui/src/app/components/download-zip-modal/download-zip-modal.component.scss @@ -13,7 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@use '@angular/material' as mat; @import '../../../theming/colors'; +@import '../../../theming/variables'; :host { display: grid; @@ -31,6 +33,7 @@ } .risk-profile-select-form-content { + margin: 14px 0 6px; font-family: Roboto, sans-serif; font-size: 14px; line-height: 20px; @@ -49,12 +52,14 @@ } .risk-profile-select-form-actions { + justify-content: flex-end; min-height: 30px; padding: 16px 0 0; -} + gap: 8px; -.risk-profile-select-form-actions button:first-child { - margin-right: auto; + &:has(app-download-report) { + justify-content: space-between; + } } .profile-select { @@ -69,3 +74,100 @@ font-size: 12px; color: $grey-700; } + +.redirect-link { + cursor: pointer; + color: $primary; + display: inline-block; + width: fit-content; +} + +::ng-deep mat-select-trigger { + display: inline-flex; + width: 100%; + justify-content: space-between; +} + +::ng-deep mat-select-trigger .profile-item-risk { + vertical-align: middle; + align-self: center; + margin-right: 16px; +} +.testing-result-heading { + margin: 16px 0; +} +.testing-result-title { + margin: 0; + font-size: 32px; + line-height: 40px; + text-align: center; + color: $grey-900; +} + +.testing-result-subtitle { + margin: 0; + font-family: $font-secondary; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.2px; + text-align: center; + color: $grey-800; +} + +.testing-result { + display: flex; + height: auto; + min-height: 176px; + align-items: center; + gap: 8px; + margin: 6px 0 10px; + border-radius: 8px; +} + +.testing-result-status { + display: flex; + justify-content: center; + align-items: center; + flex: 1 0 0; + min-width: 208px; + width: fit-content; + height: 100%; + min-height: 176px; + box-sizing: border-box; + margin: 0; + padding: 16px; + border-radius: 8px; + color: $white; + font-size: 24px; + line-height: 32px; + background: red; +} + +.testing-result-description { + display: flex; + justify-content: center; + box-sizing: border-box; + margin: 0; + padding: 8px 24px; + color: $grey-800; + font-family: $font-secondary; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.2px; +} + +.failed-result { + background: $red-50; + + .testing-result-status { + background: $red-800; + } +} + +.success-result { + background: $green-50; + + .testing-result-status { + background: mat.m2-get-color-from-palette($color-accent, 700); + } +} diff --git a/modules/ui/src/app/components/download-zip-modal/download-zip-modal.component.spec.ts b/modules/ui/src/app/components/download-zip-modal/download-zip-modal.component.spec.ts index cdd4c665f..74d0990e1 100644 --- a/modules/ui/src/app/components/download-zip-modal/download-zip-modal.component.spec.ts +++ b/modules/ui/src/app/components/download-zip-modal/download-zip-modal.component.spec.ts @@ -1,6 +1,15 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { + ComponentFixture, + discardPeriodicTasks, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; -import { DownloadZipModalComponent } from './download-zip-modal.component'; +import { + DialogCloseAction, + DownloadZipModalComponent, +} from './download-zip-modal.component'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { PROFILE_MOCK, @@ -10,31 +19,56 @@ import { import { of } from 'rxjs'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { TestRunService } from '../../services/test-run.service'; +import { Routes } from '../../model/routes'; +import { Router } from '@angular/router'; +import { FocusManagerService } from '../../services/focus-manager.service'; +import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; +import { RouterTestingModule } from '@angular/router/testing'; +import { Component } from '@angular/core'; describe('DownloadZipModalComponent', () => { + // @ts-expect-error data layer should be defined + window.dataLayer = window.dataLayer || []; let component: DownloadZipModalComponent; let fixture: ComponentFixture; - - const testRunServiceMock = jasmine.createSpyObj(['getRiskClass']); + let router: Router; + const testRunServiceMock = jasmine.createSpyObj('testRunServiceMock', [ + 'getRiskClass', + 'downloadZip', + ]); + const focusServiceMock: jasmine.SpyObj = + jasmine.createSpyObj('focusServiceMock', ['focusFirstElementInContainer']); + const actionBehaviorSubject$ = new BehaviorSubject({ + action: DialogCloseAction.Close, + }); beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [DownloadZipModalComponent, NoopAnimationsModule], + imports: [ + RouterTestingModule.withRoutes([ + { path: 'risk-assessment', component: FakeRiskAssessmentComponent }, + ]), + DownloadZipModalComponent, + NoopAnimationsModule, + ], providers: [ { provide: MatDialogRef, useValue: { keydownEvents: () => of(new KeyboardEvent('keydown', { code: '' })), close: () => ({}), + beforeClosed: () => actionBehaviorSubject$.asObservable(), }, }, { provide: MAT_DIALOG_DATA, useValue: { profiles: [PROFILE_MOCK_2, PROFILE_MOCK], + url: 'localhost:8080', }, }, { provide: TestRunService, useValue: testRunServiceMock }, + { provide: FocusManagerService, useValue: focusServiceMock }, ], }); }); @@ -44,11 +78,14 @@ describe('DownloadZipModalComponent', () => { TestBed.overrideProvider(MAT_DIALOG_DATA, { useValue: { profiles: [PROFILE_MOCK_2, PROFILE_MOCK, PROFILE_MOCK_3], + url: 'localhost:8080', + isPilot: true, }, }); TestBed.compileComponents(); fixture = TestBed.createComponent(DownloadZipModalComponent); + router = TestBed.get(Router); component = fixture.componentInstance; fixture.detectChanges(); }); @@ -59,43 +96,66 @@ describe('DownloadZipModalComponent', () => { expect(select).toBeTruthy(); }); - it('should preselect first profile', async () => { - const select = fixture.nativeElement.querySelector( - 'mat-select' - ) as HTMLElement; - - expect(select.getAttribute('ng-reflect-value')).toEqual( - 'Primary profile' + it('should preselect "no profile" option', async () => { + expect(component.selectedProfile.name).toEqual( + 'No Risk Profile selected' ); }); - it('should close with null on redirect button click', async () => { - const closeSpy = spyOn(component.dialogRef, 'close'); - const redirectButton = fixture.nativeElement.querySelector( - '.redirect-button' - ) as HTMLButtonElement; + it('should close with Redirect action on redirect button click', fakeAsync(() => { + const result = { + action: DialogCloseAction.Redirect, + }; + actionBehaviorSubject$.next(result); + fixture.detectChanges(); - redirectButton.click(); + fixture.ngZone?.run(() => { + const closeSpy = spyOn(component.dialogRef, 'close'); + const redirectLink = fixture.nativeElement.querySelector( + '.redirect-link' + ) as HTMLAnchorElement; - expect(closeSpy).toHaveBeenCalledWith(null); + redirectLink.click(); - closeSpy.calls.reset(); - }); + tick(2000); + + expect(router.url).toBe(Routes.RiskAssessment); + expect( + focusServiceMock.focusFirstElementInContainer + ).toHaveBeenCalled(); + expect(closeSpy).toHaveBeenCalledWith(result); + + closeSpy.calls.reset(); + discardPeriodicTasks(); + }); + })); - it('should close with undefined on cancel button click', async () => { + it('should close with Close action on cancel button click', async () => { + const result = { + action: DialogCloseAction.Close, + }; const closeSpy = spyOn(component.dialogRef, 'close'); + actionBehaviorSubject$.next(result); + fixture.detectChanges(); + const cancelButton = fixture.nativeElement.querySelector( '.cancel-button' ) as HTMLButtonElement; cancelButton.click(); - expect(closeSpy).toHaveBeenCalledWith(undefined); + expect(closeSpy).toHaveBeenCalledWith(result); closeSpy.calls.reset(); }); - it('should close with profile on download button click', async () => { + it('should close with Download action and profile on download button click', async () => { + const result = { + action: DialogCloseAction.Download, + profile: '', + }; + actionBehaviorSubject$.next(result); + fixture.detectChanges(); const closeSpy = spyOn(component.dialogRef, 'close'); const downloadButton = fixture.nativeElement.querySelector( '.download-button' @@ -103,13 +163,41 @@ describe('DownloadZipModalComponent', () => { downloadButton.click(); - expect(closeSpy).toHaveBeenCalledWith('Primary profile'); + expect(closeSpy).toHaveBeenCalledWith(result); + expect(testRunServiceMock.downloadZip).toHaveBeenCalled(); + expect(router.url).not.toBe(Routes.RiskAssessment); + closeSpy.calls.reset(); + }); + it('should send GA event if report is for Pilot program', async () => { + const result = { + action: DialogCloseAction.Download, + profile: '', + }; + actionBehaviorSubject$.next(result); + fixture.detectChanges(); + const closeSpy = spyOn(component.dialogRef, 'close'); + const downloadButton = fixture.nativeElement.querySelector( + '.download-button' + ) as HTMLButtonElement; + + downloadButton.click(); + + expect( + // @ts-expect-error data layer should be defined + window.dataLayer.some( + (item: { event: string }) => item.event === 'pilot_download_zip' + ) + ).toBeTruthy(); closeSpy.calls.reset(); }); it('should have filtered and sorted profiles', async () => { - expect(component.profiles).toEqual([PROFILE_MOCK, PROFILE_MOCK_2]); + expect(component.profiles).toEqual([ + component.NO_PROFILE, + PROFILE_MOCK, + PROFILE_MOCK_2, + ]); }); it('#getRiskClass should call the service method getRiskClass"', () => { @@ -132,35 +220,52 @@ describe('DownloadZipModalComponent', () => { TestBed.overrideProvider(MAT_DIALOG_DATA, { useValue: { profiles: [], + url: 'localhost:8080', }, }); TestBed.compileComponents(); fixture = TestBed.createComponent(DownloadZipModalComponent); + router = TestBed.get(Router); component = fixture.componentInstance; fixture.detectChanges(); }); - it('should have no dropdown with profiles', async () => { + it('should have disabled dropdown', async () => { const select = fixture.nativeElement.querySelector('mat-select'); - expect(select).toEqual(null); + expect(select.classList.contains('mat-mdc-select-disabled')).toBeTruthy(); }); - it('should close with null on redirect button click', async () => { - const closeSpy = spyOn(component.dialogRef, 'close'); - const redirectButton = fixture.nativeElement.querySelector( - '.redirect-button' - ) as HTMLButtonElement; + it('should close with Redirect action on redirect button click', fakeAsync(() => { + const result = { + action: DialogCloseAction.Redirect, + }; + actionBehaviorSubject$.next(result); + fixture.detectChanges(); - redirectButton.click(); + fixture.ngZone?.run(() => { + const closeSpy = spyOn(component.dialogRef, 'close'); + const redirectLink = fixture.nativeElement.querySelector( + '.redirect-link' + ) as HTMLAnchorElement; - expect(closeSpy).toHaveBeenCalledWith(null); + redirectLink.click(); - closeSpy.calls.reset(); - }); + tick(2000); + + expect(router.url).toBe(Routes.RiskAssessment); + expect( + focusServiceMock.focusFirstElementInContainer + ).toHaveBeenCalled(); + expect(closeSpy).toHaveBeenCalledWith(result); - it('should close with undefined on cancel button click', async () => { + closeSpy.calls.reset(); + discardPeriodicTasks(); + }); + })); + + it('should close with Close action on cancel button click', async () => { const closeSpy = spyOn(component.dialogRef, 'close'); const cancelButton = fixture.nativeElement.querySelector( '.cancel-button' @@ -168,7 +273,9 @@ describe('DownloadZipModalComponent', () => { cancelButton.click(); - expect(closeSpy).toHaveBeenCalledWith(undefined); + expect(closeSpy).toHaveBeenCalledWith({ + action: DialogCloseAction.Close, + }); closeSpy.calls.reset(); }); @@ -181,9 +288,18 @@ describe('DownloadZipModalComponent', () => { downloadButton.click(); - expect(closeSpy).toHaveBeenCalledWith(''); + expect(closeSpy).toHaveBeenCalledWith({ + action: DialogCloseAction.Download, + profile: '', + }); closeSpy.calls.reset(); }); }); }); + +@Component({ + selector: 'app-fake-risk-assessment-component', + template: '', +}) +class FakeRiskAssessmentComponent {} diff --git a/modules/ui/src/app/components/download-zip-modal/download-zip-modal.component.ts b/modules/ui/src/app/components/download-zip-modal/download-zip-modal.component.ts index 395bcb480..199d89bf5 100644 --- a/modules/ui/src/app/components/download-zip-modal/download-zip-modal.component.ts +++ b/modules/ui/src/app/components/download-zip-modal/download-zip-modal.component.ts @@ -1,4 +1,10 @@ -import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + Inject, + OnDestroy, + OnInit, +} from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogActions, @@ -17,9 +23,30 @@ import { MatFormField } from '@angular/material/form-field'; import { MatSelectModule } from '@angular/material/select'; import { MatOptionModule } from '@angular/material/core'; import { TestRunService } from '../../services/test-run.service'; +import { Routes } from '../../model/routes'; +import { Router, RouterLink } from '@angular/router'; +import { TestrunStatus, StatusOfTestrun } from '../../model/testrun-status'; +import { DownloadReportComponent } from '../download-report/download-report.component'; +import { Subject, takeUntil, timer } from 'rxjs'; +import { FocusManagerService } from '../../services/focus-manager.service'; interface DialogData { profiles: Profile[]; + testrunStatus?: TestrunStatus; + isTestingComplete?: boolean; + url: string | null; + isPilot?: boolean; +} + +export enum DialogCloseAction { + Close, + Redirect, + Download, +} + +export interface DialogCloseResult { + action: DialogCloseAction; + profile: string | null | undefined; } @Component({ @@ -35,18 +62,32 @@ interface DialogData { MatFormField, MatSelectModule, MatOptionModule, + RouterLink, + DownloadReportComponent, ], templateUrl: './download-zip-modal.component.html', styleUrl: './download-zip-modal.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class DownloadZipModalComponent extends EscapableDialogComponent { +export class DownloadZipModalComponent + extends EscapableDialogComponent + implements OnDestroy, OnInit +{ + private destroy$: Subject = new Subject(); + readonly NO_PROFILE = { + name: 'No Risk Profile selected', + questions: [], + } as Profile; + public readonly Routes = Routes; + public readonly StatusOfTestrun = StatusOfTestrun; profiles: Profile[] = []; - selectedProfile: string = ''; + selectedProfile: Profile; constructor( private readonly testRunService: TestRunService, public override dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) public data: DialogData + @Inject(MAT_DIALOG_DATA) public data: DialogData, + private route: Router, + private focusManagerService: FocusManagerService ) { super(dialogRef); this.profiles = data.profiles.filter( @@ -56,15 +97,75 @@ export class DownloadZipModalComponent extends EscapableDialogComponent { this.profiles.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()) ); - this.selectedProfile = this.profiles[0].name; } + this.profiles.unshift(this.NO_PROFILE); + this.selectedProfile = this.profiles[0]; + } + + ngOnInit() { + this.dialogRef + ?.beforeClosed() + .pipe(takeUntil(this.destroy$)) + .subscribe((result: DialogCloseResult) => { + if (result.action === DialogCloseAction.Close) { + return; + } + if (result.action === DialogCloseAction.Redirect) { + this.route.navigate([Routes.RiskAssessment]).then(() => + timer(1000).subscribe(() => { + this.focusManagerService.focusFirstElementInContainer(); + }) + ); + return; + } + if (this.data.url != null && typeof result.profile === 'string') { + this.testRunService.downloadZip( + this.getZipLink(this.data.url), + result.profile + ); + if (this.data.isPilot) { + // @ts-expect-error data layer is not null + window.dataLayer.push({ + event: 'pilot_download_zip', + }); + } + } + }); } - cancel(profile?: string | null) { - this.dialogRef.close(profile); + ngOnDestroy() { + this.destroy$.next(true); + this.destroy$.unsubscribe(); + } + + cancel(profile?: Profile | null) { + if (profile === null) { + this.dialogRef.close({ + action: DialogCloseAction.Redirect, + } as DialogCloseResult); + return; + } + if (!profile) { + this.dialogRef.close({ + action: DialogCloseAction.Close, + } as DialogCloseResult); + return; + } + let value = profile.name; + if (value === this.NO_PROFILE.name) { + value = ''; + } + this.dialogRef.close({ + action: DialogCloseAction.Download, + profile: value, + } as DialogCloseResult); } public getRiskClass(riskResult: string): RiskResultClassName { return this.testRunService.getRiskClass(riskResult); } + + private getZipLink(reportURL: string): string { + return reportURL.replace('report', 'export'); + } } diff --git a/modules/ui/src/app/components/dynamic-form/dynamic-form.component.html b/modules/ui/src/app/components/dynamic-form/dynamic-form.component.html new file mode 100644 index 000000000..cdbf27695 --- /dev/null +++ b/modules/ui/src/app/components/dynamic-form/dynamic-form.component.html @@ -0,0 +1,271 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ description }} + + Please, check. “ and \ are not allowed. + + + The field is required + + + The field must be a maximum of + {{ getControl(formControlName).getError('maxlength').requiredLength }} + characters. + + + + + + + + {{ description }} + + Please, check. “ and \ are not allowed. + + + The field is required + + + The field must be a maximum of + {{ getControl(formControlName).getError('maxlength').requiredLength }} + characters. + + + + + + + + {{ description }} + + The field is required + + + Please, check the email address. Valid e-mail can contain only latin + letters, numbers, @ and . (dot). + + + The field must be a maximum of + {{ getControl(formControlName).getError('maxlength').requiredLength }} + characters. + + + + + +
+

+ + + +

+ {{ + description + }} +
+
+ + + + + + {{ getControl(formControlName).value }} + + + + + {{ + description + }} + + + The field is required + + + + diff --git a/modules/ui/src/app/components/dynamic-form/dynamic-form.component.scss b/modules/ui/src/app/components/dynamic-form/dynamic-form.component.scss new file mode 100644 index 000000000..7fda3a91a --- /dev/null +++ b/modules/ui/src/app/components/dynamic-form/dynamic-form.component.scss @@ -0,0 +1,67 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@use '@angular/material' as mat; +@import 'src/theming/colors'; +@import 'src/theming/variables'; + +.field-label { + margin: 0; + color: $grey-800; + font-size: 18px; + line-height: 24px; + padding-top: 24px; + padding-bottom: 16px; + display: inline-block; + &:has(+ .field-select-multiple.ng-invalid.ng-dirty) { + color: mat.m2-get-color-from-palette($color-warn, 700); + } +} +mat-form-field { + width: 100%; +} +.field-hint { + font-family: $font-secondary; + font-size: 12px; + font-weight: 400; + line-height: 16px; + text-align: left; + padding-top: 8px; +} + +.form-field { + width: 100%; +} + +.form-field ::ng-deep .mat-mdc-form-field-textarea-control { + display: inherit; +} + +.field-select-multiple { + .field-select-checkbox { + &:has(::ng-deep .mat-mdc-checkbox-checked) { + background: mat.m2-get-color-from-palette($color-primary, 50); + } + ::ng-deep .mdc-checkbox__ripple { + display: none; + } + &:first-of-type { + margin-top: 0; + } + &:last-of-type { + margin-bottom: 8px; + } + } +} diff --git a/modules/ui/src/app/components/dynamic-form/dynamic-form.component.spec.ts b/modules/ui/src/app/components/dynamic-form/dynamic-form.component.spec.ts new file mode 100644 index 000000000..ef63d45d7 --- /dev/null +++ b/modules/ui/src/app/components/dynamic-form/dynamic-form.component.spec.ts @@ -0,0 +1,235 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DynamicFormComponent } from './dynamic-form.component'; +import { Component, ViewChild, ViewEncapsulation } from '@angular/core'; +import { + FormBuilder, + FormGroup, + FormsModule, + ReactiveFormsModule, +} from '@angular/forms'; +import { PROFILE_FORM } from '../../mocks/profile.mock'; +import { FormControlType } from '../../model/question'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; + +@Component({ + template: + '
', +}) +class DummyComponent { + @ViewChild('dynamicForm') public dynamicForm!: DynamicFormComponent; + public testForm!: FormGroup; + public format = PROFILE_FORM; + constructor(private readonly fb: FormBuilder) { + this.testForm = this.fb.group({ + test: [''], + }); + } +} + +describe('DynamicFormComponent', () => { + let dummy: DummyComponent; + let fixture: ComponentFixture; + let compiled: HTMLElement; + let component: DynamicFormComponent; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [DummyComponent], + imports: [ + DynamicFormComponent, + ReactiveFormsModule, + FormsModule, + NoopAnimationsModule, + ], + }) + .overrideComponent(DummyComponent, { + set: { encapsulation: ViewEncapsulation.None }, + }) + .compileComponents(); + + fixture = TestBed.createComponent(DummyComponent); + + dummy = fixture.componentInstance; + compiled = fixture.nativeElement as HTMLElement; + fixture.detectChanges(); + component = dummy.dynamicForm; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + PROFILE_FORM.forEach((item, index) => { + it(`should have form field with specific type"`, () => { + const fields = compiled.querySelectorAll('.form-field'); + + if (item.type === FormControlType.SELECT) { + const select = fields[index].querySelector('mat-select'); + expect(select).toBeTruthy(); + } else if (item.type === FormControlType.SELECT_MULTIPLE) { + const select = fields[index].querySelector('mat-checkbox'); + expect(select).toBeTruthy(); + } else if (item.type === FormControlType.TEXTAREA) { + const input = fields[index]?.querySelector('textarea'); + expect(input).toBeTruthy(); + } else { + const input = fields[index]?.querySelector('input'); + expect(input).toBeTruthy(); + } + }); + + it('should have label', () => { + const labels = compiled.querySelectorAll('.field-label'); + + const label = item.question; + expect(labels[index].textContent?.trim()).toEqual(label); + }); + + it('should have hint', () => { + const fields = compiled.querySelectorAll('.form-field'); + const hint = fields[index].querySelector('mat-hint'); + + if (item.description) { + expect(hint?.textContent?.trim()).toEqual(item.description); + } else { + expect(hint).toBeNull(); + } + }); + + if (item.type === FormControlType.SELECT) { + describe('select', () => { + it(`should have default value if provided`, () => { + const fields = compiled.querySelectorAll('.form-field'); + const select = fields[index].querySelector('mat-select'); + expect(select?.textContent?.trim()).toEqual(item.default || ''); + }); + + it('should have "required" error when field is not filled', () => { + const fields = compiled.querySelectorAll('.form-field'); + + component.getControl(index).setValue(''); + component.getControl(index).markAsTouched(); + + fixture.detectChanges(); + + const error = fields[index].querySelector('mat-error')?.innerHTML; + + expect(error).toContain('The field is required'); + }); + }); + } + + if (item.type === FormControlType.SELECT_MULTIPLE) { + describe('select multiple', () => { + it(`should mark form group as dirty while tab navigation`, () => { + const fields = compiled.querySelectorAll('.form-field'); + const checkbox = fields[index].querySelector( + '.field-select-checkbox:last-of-type mat-checkbox' + ); + checkbox?.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab' })); + fixture.detectChanges(); + + expect(component.getControl(index).dirty).toBeTrue(); + }); + }); + } + + if ( + item.type === FormControlType.TEXT || + item.type === FormControlType.TEXTAREA || + item.type === FormControlType.EMAIL_MULTIPLE + ) { + describe('text or text-long or email-multiple', () => { + if (item.validation?.required) { + it('should have "required" error when field is not filled', () => { + const fields = compiled.querySelectorAll('.form-field'); + const input = fields[index].querySelector( + '.mat-mdc-input-element' + ) as HTMLInputElement; + ['', ' '].forEach(value => { + input.value = value; + input.dispatchEvent(new Event('input')); + component.getControl(index).markAsTouched(); + fixture.detectChanges(); + const errors = fields[index].querySelectorAll('mat-error'); + let hasError = false; + errors.forEach(error => { + if (error.textContent === 'The field is required') { + hasError = true; + } + }); + + expect(hasError).toBeTrue(); + }); + }); + } + + it('should have "invalid_format" error when field does not satisfy validation rules', () => { + const fields = compiled.querySelectorAll('.form-field'); + const input: HTMLInputElement = fields[index].querySelector( + '.mat-mdc-input-element' + ) as HTMLInputElement; + input.value = 'as\\\\\\\\\\""""""""'; + input.dispatchEvent(new Event('input')); + component.getControl(index).markAsTouched(); + fixture.detectChanges(); + const result = + item.type === FormControlType.EMAIL_MULTIPLE + ? 'Please, check the email address. Valid e-mail can contain only latin letters, numbers, @ and . (dot).' + : 'Please, check. “ and \\ are not allowed.'; + const errors = fields[index].querySelectorAll('mat-error'); + let hasError = false; + errors.forEach(error => { + if (error.textContent === result) { + hasError = true; + } + }); + + expect(hasError).toBeTrue(); + }); + + if (item.validation?.max) { + it('should have "maxlength" error when field is exceeding max length', () => { + const fields = compiled.querySelectorAll('.form-field'); + const input: HTMLInputElement = fields[index].querySelector( + '.mat-mdc-input-element' + ) as HTMLInputElement; + input.value = + 'very long value very long value very long value very long value very long value very long value very long value very long value very long value very long value'; + input.dispatchEvent(new Event('input')); + component.getControl(index).markAsTouched(); + fixture.detectChanges(); + + const errors = fields[index].querySelectorAll('mat-error'); + let hasError = false; + errors.forEach(error => { + if ( + error.textContent === + `The field must be a maximum of ${item.validation?.max} characters.` + ) { + hasError = true; + } + }); + expect(hasError).toBeTrue(); + }); + } + }); + } + }); +}); diff --git a/modules/ui/src/app/components/dynamic-form/dynamic-form.component.ts b/modules/ui/src/app/components/dynamic-form/dynamic-form.component.ts new file mode 100644 index 000000000..d3e1fa3da --- /dev/null +++ b/modules/ui/src/app/components/dynamic-form/dynamic-form.component.ts @@ -0,0 +1,186 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + Component, + inject, + Input, + OnInit, + ViewEncapsulation, +} from '@angular/core'; +import { + FormControlType, + OptionType, + QuestionFormat, + Validation, +} from '../../model/question'; +import { + AbstractControl, + ControlContainer, + FormBuilder, + FormControl, + FormGroup, + ReactiveFormsModule, + ValidatorFn, + Validators, +} from '@angular/forms'; +import { + MatError, + MatFormField, + MatOption, + MatSelectModule, +} from '@angular/material/select'; +import { MatButtonModule } from '@angular/material/button'; +import { CommonModule } from '@angular/common'; +import { MatInputModule } from '@angular/material/input'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { TextFieldModule } from '@angular/cdk/text-field'; +import { DeviceValidators } from '../../pages/devices/components/device-form/device.validators'; +import { ProfileValidators } from '../../pages/risk-assessment/profile-form/profile.validators'; +import { DomSanitizer } from '@angular/platform-browser'; +@Component({ + selector: 'app-dynamic-form', + standalone: true, + imports: [ + MatFormField, + MatOption, + MatButtonModule, + CommonModule, + ReactiveFormsModule, + MatInputModule, + MatError, + MatFormFieldModule, + MatSelectModule, + MatCheckboxModule, + TextFieldModule, + ], + viewProviders: [ + { + provide: ControlContainer, + useFactory: () => inject(ControlContainer, { skipSelf: true }), + }, + ], + templateUrl: './dynamic-form.component.html', + styleUrl: './dynamic-form.component.scss', + encapsulation: ViewEncapsulation.None, +}) +export class DynamicFormComponent implements OnInit { + public readonly FormControlType = FormControlType; + + @Input() format: QuestionFormat[] = []; + @Input() optionKey: string | undefined; + + parentContainer = inject(ControlContainer); + get formGroup() { + return this.parentContainer.control as FormGroup; + } + + constructor( + private fb: FormBuilder, + private deviceValidators: DeviceValidators, + private profileValidators: ProfileValidators, + private domSanitizer: DomSanitizer + ) {} + getControl(name: string | number) { + return this.formGroup.get(name.toString()) as AbstractControl; + } + + getFormGroup(name: string | number): FormGroup { + return this.formGroup?.controls[name] as FormGroup; + } + + public markSectionAsDirty( + optionIndex: number, + optionLength: number, + formControlName: string + ) { + if (optionIndex === optionLength - 1) { + this.getControl(formControlName).markAsDirty({ + onlySelf: true, + }); + } + } + + ngOnInit() { + this.createProfileForm(this.format); + } + + createProfileForm(questions: QuestionFormat[]) { + questions.forEach((question, index) => { + if (question.type === FormControlType.SELECT_MULTIPLE) { + this.formGroup.addControl( + index.toString(), + this.getMultiSelectGroup(question) + ); + } else { + const validators = this.getValidators( + question.type, + question.validation + ); + this.formGroup.addControl( + index.toString(), + new FormControl(question.default || '', validators) + ); + } + }); + } + + getValidators(type: FormControlType, validation?: Validation): ValidatorFn[] { + const validators: ValidatorFn[] = []; + if (validation) { + if (validation.required) { + validators.push(this.profileValidators.textRequired()); + } + if (validation.max) { + validators.push(Validators.maxLength(Number(validation.max))); + } + if (type === FormControlType.EMAIL_MULTIPLE) { + validators.push(this.profileValidators.emailStringFormat()); + } + if (type === FormControlType.TEXT || type === FormControlType.TEXTAREA) { + validators.push(this.profileValidators.textFormat()); + } + } + return validators; + } + + getMultiSelectGroup(question: QuestionFormat): FormGroup { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const group: any = {}; + question.options?.forEach((option, index) => { + group[index] = false; + }); + return this.fb.group(group, { + validators: question.validation?.required + ? [this.profileValidators.multiSelectRequired] + : [], + }); + } + + getOptionValue(option: OptionType) { + if (this.optionKey && typeof option === 'object') { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (option as any)[this.optionKey]; + } + return option; + } + + getSanitizedOptionValue(option: OptionType) { + return this.domSanitizer.bypassSecurityTrustHtml( + this.getOptionValue(option) + ); + } +} diff --git a/modules/ui/src/app/components/program-type-icon/program-type-con.component.spec.ts b/modules/ui/src/app/components/program-type-icon/program-type-con.component.spec.ts new file mode 100644 index 000000000..7a984eff0 --- /dev/null +++ b/modules/ui/src/app/components/program-type-icon/program-type-con.component.spec.ts @@ -0,0 +1,46 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ProgramTypeIconComponent } from './program-type-icon.component'; +import { MatIconTestingModule } from '@angular/material/icon/testing'; + +describe('ProgramTypeIconComponent', () => { + let component: ProgramTypeIconComponent; + let fixture: ComponentFixture; + let compiled: HTMLElement; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ProgramTypeIconComponent, MatIconTestingModule], + }).compileComponents(); + fixture = TestBed.createComponent(ProgramTypeIconComponent); + component = fixture.componentInstance; + component.type = 'pilot'; + compiled = fixture.nativeElement as HTMLElement; + fixture.detectChanges(); + }); + + it('should create component', () => { + expect(component).toBeTruthy(); + }); + + it('should have svgIcon provided from type', () => { + const iconEl = compiled.querySelector('.icon'); + + expect(iconEl?.getAttribute('ng-reflect-svg-icon')).toEqual('pilot'); + }); +}); diff --git a/modules/ui/src/app/components/program-type-icon/program-type-icon.component.ts b/modules/ui/src/app/components/program-type-icon/program-type-icon.component.ts new file mode 100644 index 000000000..45a34ddfc --- /dev/null +++ b/modules/ui/src/app/components/program-type-icon/program-type-icon.component.ts @@ -0,0 +1,40 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Component, Input } from '@angular/core'; +import { MatIcon } from '@angular/material/icon'; + +@Component({ + selector: 'app-program-type-icon', + standalone: true, + imports: [MatIcon], + template: ` `, + styles: ` + :host { + display: inline-flex; + align-items: center; + padding-right: 4px; + } + .icon { + display: flex; + width: 16px; + height: 16px; + line-height: 16px; + } + `, +}) +export class ProgramTypeIconComponent { + @Input() type = ''; +} diff --git a/modules/ui/src/app/components/report-action/report-action.component.spec.ts b/modules/ui/src/app/components/report-action/report-action.component.spec.ts index b4d69b149..7408de89d 100644 --- a/modules/ui/src/app/components/report-action/report-action.component.spec.ts +++ b/modules/ui/src/app/components/report-action/report-action.component.spec.ts @@ -2,6 +2,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReportActionComponent } from './report-action.component'; import { MOCK_PROGRESS_DATA_COMPLIANT } from '../../mocks/testrun.mock'; +import { TestrunStatus } from '../../model/testrun-status'; describe('ReportActionComponent', () => { let component: ReportActionComponent; @@ -21,12 +22,25 @@ describe('ReportActionComponent', () => { expect(component).toBeTruthy(); }); - it('#getTestRunId should return data for title of link', () => { - const expectedResult = 'Delta 03-DIN-CPU 1.2.2 22 Jun 2023 9:20'; + describe('#getTestRunId', () => { + it('should return data for title of link', () => { + const expectedResult = 'Delta 03-DIN-CPU 1.2.2 22 Jun 2023 9:20'; - const result = component.getTestRunId(MOCK_PROGRESS_DATA_COMPLIANT); + const result = component.getTestRunId(MOCK_PROGRESS_DATA_COMPLIANT); - expect(result).toEqual(expectedResult); + expect(result).toEqual(expectedResult); + }); + + it('should return title as empty string if no device data', () => { + const MOCK_DATA_WITHOUT_DEVICE = { + ...MOCK_PROGRESS_DATA_COMPLIANT, + device: undefined as unknown, + } as TestrunStatus; + + const result = component.getTestRunId(MOCK_DATA_WITHOUT_DEVICE); + + expect(result).toEqual(''); + }); }); it('#getFormattedDateString should return date as string in the format "d MMM y H:mm"', () => { diff --git a/modules/ui/src/app/components/report-action/report-action.component.ts b/modules/ui/src/app/components/report-action/report-action.component.ts index 62dc39d58..debf132fa 100644 --- a/modules/ui/src/app/components/report-action/report-action.component.ts +++ b/modules/ui/src/app/components/report-action/report-action.component.ts @@ -15,6 +15,9 @@ export class ReportActionComponent { constructor(private datePipe: DatePipe) {} getTestRunId(data: TestrunStatus) { + if (!data.device) { + return ''; + } return `${data.device.manufacturer} ${data.device.model} ${ data.device.firmware } ${this.getFormattedDateString(data.started)}`; diff --git a/modules/ui/src/app/components/simple-dialog/simple-dialog.component.html b/modules/ui/src/app/components/simple-dialog/simple-dialog.component.html index 6d5f768d6..975773924 100644 --- a/modules/ui/src/app/components/simple-dialog/simple-dialog.component.html +++ b/modules/ui/src/app/components/simple-dialog/simple-dialog.component.html @@ -20,6 +20,8 @@ +
+
+
+ + + diff --git a/modules/ui/src/app/components/stepper/stepper.component.scss b/modules/ui/src/app/components/stepper/stepper.component.scss new file mode 100644 index 000000000..d5be19c55 --- /dev/null +++ b/modules/ui/src/app/components/stepper/stepper.component.scss @@ -0,0 +1,91 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../../../theming/colors'; +@import '../../../theming/variables'; + +.form-container { + height: 100%; + display: flex; + flex-direction: column; +} + +.form-header { + padding: 24px; +} + +.form-content { + display: grid; + padding: 24px 8px; + gap: 10px; + overflow: hidden; + height: 100%; +} + +.form-footer { + margin-top: auto; + display: inline-block; + text-align: center; + height: 24px; + padding: 0 24px 24px 24px; +} + +.form-steps { + display: inline-flex; + text-align: center; + align-items: center; + height: 100%; +} + +.form-step { + border: 2px solid $lighter-grey; + width: 4px; + height: 4px; + display: inline-block; + border-radius: 100%; + margin: 0 8px; + &.step-active { + border-color: $secondary; + background: $secondary; + } +} + +.form-button-back { + float: left; +} + +.form-button-forward { + float: right; +} + +.form-button-back, +.form-button-forward { + height: $icon-size; + width: $icon-size; + min-width: $icon-size; + margin: 0; + padding: 0; + & mat-icon { + color: $secondary; + width: $icon-size; + height: $icon-size; + font-size: $icon-size; + margin: 0; + } + + &.hidden { + visibility: hidden; + } +} diff --git a/modules/ui/src/app/components/stepper/stepper.component.spec.ts b/modules/ui/src/app/components/stepper/stepper.component.spec.ts new file mode 100644 index 000000000..25b5f9740 --- /dev/null +++ b/modules/ui/src/app/components/stepper/stepper.component.spec.ts @@ -0,0 +1,101 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { StepperComponent } from './stepper.component'; +import { Component, ViewChild, ViewEncapsulation } from '@angular/core'; +import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; +import { CdkStep } from '@angular/cdk/stepper'; +import { MatFormField, MatFormFieldModule } from '@angular/material/form-field'; +import { CommonModule } from '@angular/common'; +import { MatInputModule } from '@angular/material/input'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; + +@Component({ + selector: 'app-stepper-bypass', + standalone: true, + imports: [ + CdkStep, + StepperComponent, + MatFormField, + CommonModule, + ReactiveFormsModule, + MatInputModule, + MatFormFieldModule, + ], + templateUrl: './stepper-test.component.html', +}) +class TestStepperComponent { + @ViewChild('stepper') public stepper!: StepperComponent; + testForm; + firstStep; + secondStep; + constructor(private fb: FormBuilder) { + this.firstStep = this.fb.group({ + firstControl: ['', [Validators.required]], + }); + this.secondStep = this.fb.group({ + secondControl: ['', [Validators.required]], + }); + this.testForm = this.fb.group({ + steps: this.fb.array([this.firstStep, this.secondStep]), + }); + } +} + +describe('StepperComponent', () => { + let component: TestStepperComponent; + let fixture: ComponentFixture; + let compiled: HTMLElement; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [StepperComponent, TestStepperComponent, NoopAnimationsModule], + }) + .overrideComponent(TestStepperComponent, { + set: { encapsulation: ViewEncapsulation.None }, + }) + .compileComponents(); + + fixture = TestBed.createComponent(TestStepperComponent); + component = fixture.componentInstance; + compiled = fixture.nativeElement as HTMLElement; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should have title', () => { + expect(fixture.nativeElement.querySelector('.form-header')).toBeTruthy(); + }); + + it('should not mark selected step touched if not interacted', () => { + component.stepper.nextClick(); + + expect(component.firstStep.touched).toBeFalse(); + }); + + it('should mark selected step touched if interacted', () => { + const button = compiled.querySelector( + '.form-button-forward' + ) as HTMLButtonElement; + button.click(); + + expect(component.firstStep.touched).toBeTrue(); + }); +}); diff --git a/modules/ui/src/app/components/stepper/stepper.component.ts b/modules/ui/src/app/components/stepper/stepper.component.ts new file mode 100644 index 000000000..ee7f1c5ab --- /dev/null +++ b/modules/ui/src/app/components/stepper/stepper.component.ts @@ -0,0 +1,72 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Component, Input, TemplateRef } from '@angular/core'; +import { CdkStepper, CdkStepperModule } from '@angular/cdk/stepper'; +import { NgForOf, NgIf, NgTemplateOutlet } from '@angular/common'; +import { MatIcon } from '@angular/material/icon'; +import { MatButton, MatIconButton } from '@angular/material/button'; +import { FormGroup } from '@angular/forms'; +import { MatTooltipModule } from '@angular/material/tooltip'; + +@Component({ + selector: 'app-stepper', + standalone: true, + imports: [ + NgForOf, + NgTemplateOutlet, + CdkStepperModule, + NgIf, + MatIcon, + MatIconButton, + MatButton, + MatTooltipModule, + ], + templateUrl: './stepper.component.html', + styleUrl: './stepper.component.scss', + providers: [{ provide: CdkStepper, useExisting: StepperComponent }], +}) +export class StepperComponent extends CdkStepper { + @Input() header: TemplateRef | undefined; + @Input() title = ''; + @Input() activeClass = 'active'; + + forwardButtonHidden() { + return this.selectedIndex === this.steps.length - 1; + } + + backButtonHidden() { + return this.selectedIndex === 0; + } + + nextClick() { + if ( + this.selected?.interacted && + !(this.selected?.stepControl as FormGroup)?.valid + ) { + this.selected?.stepControl?.markAllAsTouched(); + } + } + + getStepLabel(isActive: boolean) { + return isActive + ? `Step #${this.selectedIndex + 1} out of ${this.steps.length}` + : ''; + } + + getNavigationLabel(index: number) { + return `Go to step #${index} out of ${this.title}`; + } +} diff --git a/modules/ui/src/app/components/testing-complete/testing-complete.component.spec.ts b/modules/ui/src/app/components/testing-complete/testing-complete.component.spec.ts new file mode 100644 index 000000000..494b58823 --- /dev/null +++ b/modules/ui/src/app/components/testing-complete/testing-complete.component.spec.ts @@ -0,0 +1,92 @@ +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; + +import { TestingCompleteComponent } from './testing-complete.component'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { RouterTestingModule } from '@angular/router/testing'; +import { Component } from '@angular/core'; +import { MOCK_PROGRESS_DATA_COMPLIANT } from '../../mocks/testrun.mock'; +import { of } from 'rxjs'; +import { MatDialogRef } from '@angular/material/dialog'; +import { + DialogCloseAction, + DownloadZipModalComponent, +} from '../download-zip-modal/download-zip-modal.component'; +import { FocusManagerService } from '../../services/focus-manager.service'; + +describe('TestingCompleteComponent', () => { + let component: TestingCompleteComponent; + let fixture: ComponentFixture; + const mockFocusManagerService = jasmine.createSpyObj( + 'mockFocusManagerService', + ['focusFirstElementInContainer'] + ); + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + RouterTestingModule.withRoutes([ + { path: 'risk-assessment', component: FakeRiskAssessmentComponent }, + ]), + TestingCompleteComponent, + BrowserAnimationsModule, + ], + providers: [ + { provide: FocusManagerService, useValue: mockFocusManagerService }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(TestingCompleteComponent); + component = fixture.componentInstance; + component.data = MOCK_PROGRESS_DATA_COMPLIANT; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('#onInit', () => { + it('should focus first element in container when dialog closes with Close action', fakeAsync(() => { + const openSpy = spyOn(component.dialog, 'open').and.returnValue({ + afterClosed: () => of({ action: DialogCloseAction.Close }), + } as MatDialogRef); + + component.ngOnInit(); + + tick(1000); + + expect(openSpy).toHaveBeenCalledWith(DownloadZipModalComponent, { + ariaLabel: 'Testing complete', + data: { + profiles: [], + testrunStatus: MOCK_PROGRESS_DATA_COMPLIANT, + isTestingComplete: true, + url: 'https://api.testrun.io/report.pdf', + isPilot: false, + }, + autoFocus: 'first-tabbable', + ariaDescribedBy: 'testing-result-main-info', + hasBackdrop: true, + disableClose: true, + panelClass: 'initiate-test-run-dialog', + }); + + tick(1000); + + expect( + mockFocusManagerService.focusFirstElementInContainer + ).toHaveBeenCalled(); + openSpy.calls.reset(); + })); + }); +}); + +@Component({ + selector: 'app-fake-risk-assessment-component', + template: '', +}) +class FakeRiskAssessmentComponent {} diff --git a/modules/ui/src/app/components/testing-complete/testing-complete.component.ts b/modules/ui/src/app/components/testing-complete/testing-complete.component.ts new file mode 100644 index 000000000..79fc46c79 --- /dev/null +++ b/modules/ui/src/app/components/testing-complete/testing-complete.component.ts @@ -0,0 +1,84 @@ +import { + ChangeDetectionStrategy, + Component, + Input, + OnDestroy, + OnInit, +} from '@angular/core'; +import { Subject, takeUntil, timer } from 'rxjs'; +import { MatDialog } from '@angular/material/dialog'; +import { + DialogCloseAction, + DialogCloseResult, + DownloadZipModalComponent, +} from '../download-zip-modal/download-zip-modal.component'; +import { Profile } from '../../model/profile'; +import { TestrunStatus } from '../../model/testrun-status'; +import { FocusManagerService } from '../../services/focus-manager.service'; +import { TestingType } from '../../model/device'; + +@Component({ + selector: 'app-testing-complete', + standalone: true, + imports: [], + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class TestingCompleteComponent implements OnDestroy, OnInit { + @Input() profiles: Profile[] = []; + @Input() data!: TestrunStatus | null; + private destroy$: Subject = new Subject(); + + constructor( + public dialog: MatDialog, + private focusManagerService: FocusManagerService + ) {} + + ngOnInit() { + timer(1000) + .pipe(takeUntil(this.destroy$)) + .subscribe(() => { + this.openTestingCompleteModal(); + }); + } + ngOnDestroy() { + this.destroy$.next(true); + this.destroy$.unsubscribe(); + } + + private openTestingCompleteModal(): void { + const dialogRef = this.dialog.open(DownloadZipModalComponent, { + ariaLabel: 'Testing complete', + data: { + profiles: this.profiles, + testrunStatus: this.data, + isTestingComplete: true, + url: this.data?.report, + isPilot: this.data?.device.test_pack === TestingType.Pilot, + }, + autoFocus: 'first-tabbable', + ariaDescribedBy: 'testing-result-main-info', + hasBackdrop: true, + disableClose: true, + panelClass: 'initiate-test-run-dialog', + }); + + dialogRef + ?.afterClosed() + .pipe(takeUntil(this.destroy$)) + .subscribe((result: DialogCloseResult) => { + if (result.action === DialogCloseAction.Close) { + this.focusFirstElement(); + return; + } + }); + } + + private focusFirstElement() { + timer(1000) + .pipe(takeUntil(this.destroy$)) + .subscribe(() => { + this.focusManagerService.focusFirstElementInContainer(); + }); + } +} diff --git a/modules/ui/src/app/components/version/consent-dialog/consent-dialog.component.html b/modules/ui/src/app/components/version/consent-dialog/consent-dialog.component.html index 516e72b49..6867dd445 100644 --- a/modules/ui/src/app/components/version/consent-dialog/consent-dialog.component.html +++ b/modules/ui/src/app/components/version/consent-dialog/consent-dialog.component.html @@ -81,24 +81,12 @@

Welcome to Testrun!

-
- - Risk Assessment feature added! -

- Now you can answer a short questionnaire, create a security profile and - attach it to the Testrun result to complete a device verification. Also, - it will speed up the process a lot! -

-

- +

+ + Pilot Assessment +

+ Pilot project support is now offered through Testrun. Follow the + instructions set out to get your pilot recommendation.

diff --git a/modules/ui/src/app/components/version/consent-dialog/consent-dialog.component.scss b/modules/ui/src/app/components/version/consent-dialog/consent-dialog.component.scss index 9eb334496..c32f47a41 100644 --- a/modules/ui/src/app/components/version/consent-dialog/consent-dialog.component.scss +++ b/modules/ui/src/app/components/version/consent-dialog/consent-dialog.component.scss @@ -81,6 +81,17 @@ } } +.section-container-pilot { + .section-title { + font-weight: 500; + letter-spacing: 0.25px; + } + .section-content { + margin: 0; + padding-top: 9px; + } +} + .consent-actions { border-top: 1px solid $lighter-grey; margin: 0 -16px; diff --git a/modules/ui/src/app/components/version/consent-dialog/consent-dialog.component.spec.ts b/modules/ui/src/app/components/version/consent-dialog/consent-dialog.component.spec.ts index c538554f2..d6925c131 100644 --- a/modules/ui/src/app/components/version/consent-dialog/consent-dialog.component.spec.ts +++ b/modules/ui/src/app/components/version/consent-dialog/consent-dialog.component.spec.ts @@ -13,7 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; import { ConsentDialogComponent } from './consent-dialog.component'; import { @@ -24,15 +29,28 @@ import { import { MatButtonModule } from '@angular/material/button'; import { of } from 'rxjs'; import { NEW_VERSION, VERSION } from '../../../mocks/version.mock'; +import { MatIconTestingModule } from '@angular/material/icon/testing'; +import { FocusManagerService } from '../../../services/focus-manager.service'; +import SpyObj = jasmine.SpyObj; describe('ConsentDialogComponent', () => { let component: ConsentDialogComponent; let fixture: ComponentFixture; let compiled: HTMLElement; + let mockFocusManagerService: SpyObj; beforeEach(() => { + mockFocusManagerService = jasmine.createSpyObj('mockFocusManagerService', [ + 'focusFirstElementInContainer', + ]); + TestBed.configureTestingModule({ - imports: [ConsentDialogComponent, MatDialogModule, MatButtonModule], + imports: [ + ConsentDialogComponent, + MatDialogModule, + MatButtonModule, + MatIconTestingModule, + ], providers: [ { provide: MatDialogRef, @@ -42,11 +60,12 @@ describe('ConsentDialogComponent', () => { }, }, { provide: MAT_DIALOG_DATA, useValue: {} }, + { provide: FocusManagerService, useValue: mockFocusManagerService }, ], }); fixture = TestBed.createComponent(ConsentDialogComponent); component = fixture.componentInstance; - component.data = { version: NEW_VERSION, hasRiskProfiles: false }; + component.data = { version: NEW_VERSION }; component.optOut = false; fixture.detectChanges(); compiled = fixture.nativeElement as HTMLElement; @@ -61,7 +80,7 @@ describe('ConsentDialogComponent', () => { const confirmButton = compiled.querySelector( '.confirm-button' ) as HTMLButtonElement; - const dialogRes = { grant: true, isNavigateToRiskAssessment: undefined }; + const dialogRes = { grant: true }; confirmButton?.click(); @@ -77,7 +96,7 @@ describe('ConsentDialogComponent', () => { const confirmButton = compiled.querySelector( '.confirm-button' ) as HTMLButtonElement; - const dialogRes = { grant: false, isNavigateToRiskAssessment: undefined }; + const dialogRes = { grant: false }; confirmButton?.click(); @@ -97,9 +116,18 @@ describe('ConsentDialogComponent', () => { expect(closeSpy).toHaveBeenCalledTimes(0); }); + it('should set focus to first focusable elem when close dialog', fakeAsync(() => { + component.confirm(true); + tick(100); + + expect( + mockFocusManagerService.focusFirstElementInContainer + ).toHaveBeenCalled(); + })); + describe('with new version available', () => { beforeEach(() => { - component.data = { version: NEW_VERSION, hasRiskProfiles: false }; + component.data = { version: NEW_VERSION }; fixture.detectChanges(); }); @@ -122,7 +150,7 @@ describe('ConsentDialogComponent', () => { describe('with no new version available', () => { beforeEach(() => { - component.data = { version: VERSION, hasRiskProfiles: false }; + component.data = { version: VERSION }; fixture.detectChanges(); }); @@ -134,51 +162,4 @@ describe('ConsentDialogComponent', () => { expect(content).toBeNull(); }); }); - - describe('with no risk assessment profiles', () => { - beforeEach(() => { - component.data = { version: VERSION, hasRiskProfiles: false }; - fixture.detectChanges(); - }); - - it('should has risk-assessment content', () => { - const content = compiled.querySelector( - '.section-content.risk-assessment' - ) as HTMLElement; - - const innerContent = content.innerHTML.trim(); - expect(innerContent).toContain( - 'Now you can answer a short questionnaire' - ); - }); - - it('should close dialog with isNavigateToRiskAssessment as true when click "confirm"', () => { - const closeSpy = spyOn(component.dialogRef, 'close'); - const riskAssessmentBtn = compiled.querySelector( - '.risk-assessment-button' - ) as HTMLButtonElement; - const dialogRes = { grant: true, isNavigateToRiskAssessment: true }; - - riskAssessmentBtn?.click(); - - expect(closeSpy).toHaveBeenCalledWith(dialogRes); - - closeSpy.calls.reset(); - }); - }); - - describe('with risk assessment profiles', () => { - beforeEach(() => { - component.data = { version: VERSION, hasRiskProfiles: true }; - fixture.detectChanges(); - }); - - it('should not has risk-assessment content', () => { - const content = compiled.querySelector( - '.section-content.risk-assessment' - ) as HTMLElement; - - expect(content).toBeNull(); - }); - }); }); diff --git a/modules/ui/src/app/components/version/consent-dialog/consent-dialog.component.ts b/modules/ui/src/app/components/version/consent-dialog/consent-dialog.component.ts index 8b27a8961..3becacf89 100644 --- a/modules/ui/src/app/components/version/consent-dialog/consent-dialog.component.ts +++ b/modules/ui/src/app/components/version/consent-dialog/consent-dialog.component.ts @@ -26,10 +26,11 @@ import { CalloutType } from '../../../model/callout-type'; import { NgIf } from '@angular/common'; import { MatCheckbox } from '@angular/material/checkbox'; import { FormsModule } from '@angular/forms'; +import { FocusManagerService } from '../../../services/focus-manager.service'; +import { timer } from 'rxjs'; type DialogData = { version: Version; - hasRiskProfiles: boolean; }; @Component({ @@ -50,16 +51,19 @@ export class ConsentDialogComponent { public readonly CalloutType = CalloutType; optOut = false; constructor( + private readonly focusManagerService: FocusManagerService, public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: DialogData ) {} - confirm(optOut: boolean, isNavigateToRiskAssessment?: boolean) { + confirm(optOut: boolean) { // dialog should be closed with opposite value to grant or deny access to GA const dialogResult: ConsentDialogResult = { grant: !optOut, - isNavigateToRiskAssessment, }; this.dialogRef.close(dialogResult); + timer(100).subscribe(() => { + this.focusManagerService.focusFirstElementInContainer(); + }); } } diff --git a/modules/ui/src/app/components/version/version.component.html b/modules/ui/src/app/components/version/version.component.html index d45ea69da..2068f6043 100644 --- a/modules/ui/src/app/components/version/version.component.html +++ b/modules/ui/src/app/components/version/version.component.html @@ -18,7 +18,7 @@ mat-button class="version-content" [class.version-content-update]="version.update_available" - [attr.aria-label]="getVersionButtonLabel(version.installed_version)" + [attr.aria-label]="getVersionButtonLabel(version)" (click)="openConsentDialog(version)"> {{ version?.installed_version }} { let component: VersionComponent; @@ -43,7 +48,7 @@ describe('VersionComponent', () => { mockService = jasmine.createSpyObj(['getVersion', 'fetchVersion']); mockService.getVersion.and.returnValue(versionBehaviorSubject$); TestBed.configureTestingModule({ - imports: [VersionComponent], + imports: [VersionComponent, MatIconTestingModule], providers: [{ provide: TestRunService, useValue: mockService }], }); fixture = TestBed.createComponent(VersionComponent); @@ -56,31 +61,32 @@ describe('VersionComponent', () => { }); it('should get correct aria label for version button', () => { - const labelUnavailableVersion = component.getVersionButtonLabel( - UNAVAILABLE_VERSION.installed_version - ); + const labelUnavailableVersion = + component.getVersionButtonLabel(UNAVAILABLE_VERSION); - const labelAvailableVersion = component.getVersionButtonLabel( - VERSION.installed_version - ); + const labelAvailableVersion = component.getVersionButtonLabel(NEW_VERSION); + + const labelVersion = component.getVersionButtonLabel(VERSION); expect(labelUnavailableVersion).toContain( 'Version temporarily unavailable.' ); expect(labelAvailableVersion).toContain('New version is available.'); + expect(labelVersion).toEqual('v1. Click to open the Welcome modal'); }); - it('should open consent window on start', () => { + it('should open consent window on start', fakeAsync(() => { const openSpy = spyOn(component.dialog, 'open').and.returnValue({ - afterClosed: () => of(true), - } as MatDialogRef); + afterClosed: () => of({ grant: null }), + } as MatDialogRef); versionBehaviorSubject$.next(VERSION); mockService.getVersion.and.returnValue(versionBehaviorSubject$); fixture.detectChanges(); component.ngOnInit(); + tick(2000); expect(openSpy).toHaveBeenCalled(); - }); + })); it('should open consent window when button clicked', () => { const openSpy = spyOn(component.dialog, 'open').and.returnValue({ diff --git a/modules/ui/src/app/components/version/version.component.ts b/modules/ui/src/app/components/version/version.component.ts index 186c3bcf6..ad8fcafa0 100644 --- a/modules/ui/src/app/components/version/version.component.ts +++ b/modules/ui/src/app/components/version/version.component.ts @@ -34,10 +34,10 @@ import { tap } from 'rxjs/internal/operators/tap'; import { Observable } from 'rxjs/internal/Observable'; import { Subject } from 'rxjs/internal/Subject'; import { takeUntil } from 'rxjs/internal/operators/takeUntil'; -import { filter } from 'rxjs'; +import { filter, timer } from 'rxjs'; import { ConsentDialogComponent } from './consent-dialog/consent-dialog.component'; -// eslint-disable-next-line @typescript-eslint/ban-types +// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type declare const gtag: Function; @Component({ selector: 'app-version', @@ -48,9 +48,7 @@ declare const gtag: Function; }) export class VersionComponent implements OnInit, OnDestroy { @Input() consentShown!: boolean; - @Input() hasRiskProfiles!: boolean; @Output() consentShownEvent = new EventEmitter(); - @Output() navigateToRiskAssessmentEvent = new EventEmitter(); version$!: Observable; private destroy$: Subject = new Subject(); @@ -66,9 +64,10 @@ export class VersionComponent implements OnInit, OnDestroy { filter(version => version !== null), tap(version => { if (!this.consentShown) { - // @ts-expect-error null is filtered - this.openConsentDialog(version); - this.consentShownEvent.emit(); + timer(2000).subscribe(() => { + this.openConsentDialog(version); + this.consentShownEvent.emit(); + }); } // @ts-expect-error data layer is not null window.dataLayer.push({ @@ -79,14 +78,21 @@ export class VersionComponent implements OnInit, OnDestroy { ); } - getVersionButtonLabel(installedVersion: string): string { - return installedVersion === UNAVAILABLE_VERSION.installed_version - ? 'Version temporarily unavailable. Click to open the Welcome modal' - : `${installedVersion} New version is available. Click to update`; + getVersionButtonLabel(installedVersion: Version): string { + if ( + installedVersion.installed_version === + UNAVAILABLE_VERSION.installed_version + ) { + return 'Version temporarily unavailable. Click to open the Welcome modal'; + } + if (installedVersion.update_available) { + return `${installedVersion.installed_version} New version is available. Click to update`; + } + return `${installedVersion.installed_version}. Click to open the Welcome modal`; } openConsentDialog(version: Version) { - const dialogData = { version, hasRiskProfiles: this.hasRiskProfiles }; + const dialogData = { version }; const dialogRef = this.dialog.open(ConsentDialogComponent, { ariaLabel: 'Welcome to Testrun modal window', data: dialogData, @@ -106,10 +112,6 @@ export class VersionComponent implements OnInit, OnDestroy { gtag('consent', 'update', { analytics_storage: dialogResult.grant ? 'granted' : 'denied', }); - - if (dialogResult.isNavigateToRiskAssessment) { - this.navigateToRiskAssessmentEvent.emit(); - } }); } diff --git a/modules/ui/src/app/components/wifi/wifi.component.html b/modules/ui/src/app/components/wifi/wifi.component.html new file mode 100644 index 000000000..c93d05f7e --- /dev/null +++ b/modules/ui/src/app/components/wifi/wifi.component.html @@ -0,0 +1,25 @@ + + diff --git a/modules/ui/src/app/components/wifi/wifi.component.scss b/modules/ui/src/app/components/wifi/wifi.component.scss new file mode 100644 index 000000000..bc0ac542e --- /dev/null +++ b/modules/ui/src/app/components/wifi/wifi.component.scss @@ -0,0 +1,40 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../../../theming/colors'; + +$icon-size: 24px; + +.app-toolbar-button { + border-radius: 20px; + border: 1px solid transparent; + min-width: 48px; + padding: 0; + box-sizing: border-box; + height: 34px; + margin: 11px 0; + line-height: 50% !important; + &.disabled { + opacity: 0.6; + } +} + +.wifi-icon { + margin-right: 0; + width: $icon-size; + font-size: $icon-size; + color: $dark-grey; + height: $icon-size; +} diff --git a/modules/ui/src/app/components/wifi/wifi.component.spec.ts b/modules/ui/src/app/components/wifi/wifi.component.spec.ts new file mode 100644 index 000000000..55e85a6a7 --- /dev/null +++ b/modules/ui/src/app/components/wifi/wifi.component.spec.ts @@ -0,0 +1,100 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { WifiComponent } from './wifi.component'; + +describe('WifiComponent', () => { + let component: WifiComponent; + let fixture: ComponentFixture; + let compiled: HTMLElement; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [WifiComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(WifiComponent); + component = fixture.componentInstance; + compiled = fixture.nativeElement as HTMLElement; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('Class tests', () => { + describe('with internet connection', () => { + it('should return label', () => { + expect(component.getLabel(true)).toEqual( + 'Testrun detects a working internet connection for the device under test.' + ); + }); + }); + + describe('with no internet connection', () => { + it('should return label', () => { + expect(component.getLabel(false)).toEqual( + 'No internet connection detected for the device under test.' + ); + }); + }); + + describe('with N/A internet connection', () => { + it('should return label', () => { + expect(component.getLabel(false, true)).toEqual( + 'Internet connection is not being monitored.' + ); + }); + }); + }); + + describe('DOM tests', () => { + describe('with internet connection', () => { + it('should have wifi icon', () => { + component.on = true; + fixture.detectChanges(); + + const icon = compiled.querySelector('mat-icon')?.textContent?.trim(); + + expect(icon).toEqual('wifi'); + }); + }); + + describe('should have no wifi icon', () => { + it('should have no wifi icon', () => { + component.on = false; + fixture.detectChanges(); + + const icon = compiled.querySelector('mat-icon')?.textContent?.trim(); + + expect(icon).toEqual('wifi_off'); + }); + }); + + it('button should be disabled', () => { + component.disable = true; + fixture.detectChanges(); + + const shutdownButton = compiled.querySelector( + '.wifi-button' + ) as HTMLButtonElement; + + expect(shutdownButton?.classList.contains('disabled')).toBeTrue(); + }); + }); +}); diff --git a/modules/ui/src/app/components/wifi/wifi.component.ts b/modules/ui/src/app/components/wifi/wifi.component.ts new file mode 100644 index 000000000..e7e28e8f9 --- /dev/null +++ b/modules/ui/src/app/components/wifi/wifi.component.ts @@ -0,0 +1,40 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Component, Input } from '@angular/core'; +import { MatIcon } from '@angular/material/icon'; +import { MatTooltip } from '@angular/material/tooltip'; +import { MatButton, MatIconButton } from '@angular/material/button'; + +@Component({ + selector: 'app-wifi', + standalone: true, + imports: [MatIcon, MatTooltip, MatButton, MatIconButton], + templateUrl: './wifi.component.html', + styleUrl: './wifi.component.scss', +}) +export class WifiComponent { + @Input() on: boolean | null = null; + @Input() disable: boolean = false; + + getLabel(on: boolean | null, disable: boolean = false) { + if (disable) { + return 'Internet connection is not being monitored.'; + } + return on + ? 'Testrun detects a working internet connection for the device under test.' + : 'No internet connection detected for the device under test.'; + } +} diff --git a/modules/ui/src/app/guards/can-deactivate.guard.spec.ts b/modules/ui/src/app/guards/can-deactivate.guard.spec.ts new file mode 100644 index 000000000..5571654bc --- /dev/null +++ b/modules/ui/src/app/guards/can-deactivate.guard.spec.ts @@ -0,0 +1,31 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { TestBed } from '@angular/core/testing'; + +import { CanDeactivateGuard } from './can-deactivate.guard'; + +describe('CanDeactivateGuard', () => { + let guard: CanDeactivateGuard; + + beforeEach(() => { + TestBed.configureTestingModule({}); + guard = TestBed.inject(CanDeactivateGuard); + }); + + it('should be created', () => { + expect(guard).toBeTruthy(); + }); +}); diff --git a/modules/ui/src/app/guards/can-deactivate.guard.ts b/modules/ui/src/app/guards/can-deactivate.guard.ts new file mode 100644 index 000000000..68fb2147c --- /dev/null +++ b/modules/ui/src/app/guards/can-deactivate.guard.ts @@ -0,0 +1,38 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Injectable } from '@angular/core'; +import { CanDeactivate, UrlTree } from '@angular/router'; +import { Observable } from 'rxjs'; + +type CanDeactivateType = + | Observable + | Promise + | boolean + | UrlTree; + +export interface CanComponentDeactivate { + canDeactivate: () => CanDeactivateType; +} +@Injectable({ + providedIn: 'root', +}) +export class CanDeactivateGuard + implements CanDeactivate +{ + canDeactivate(component: CanComponentDeactivate): CanDeactivateType { + return component.canDeactivate ? component.canDeactivate() : true; + } +} diff --git a/modules/ui/src/app/interceptors/error.interceptor.spec.ts b/modules/ui/src/app/interceptors/error.interceptor.spec.ts index 9fff32863..7271223a2 100644 --- a/modules/ui/src/app/interceptors/error.interceptor.spec.ts +++ b/modules/ui/src/app/interceptors/error.interceptor.spec.ts @@ -42,11 +42,15 @@ describe('ErrorInterceptor', () => { interceptor = TestBed.inject(ErrorInterceptor); }); + afterEach(() => { + notificationServiceMock.notify.calls.reset(); + }); + it('should be created', () => { expect(interceptor).toBeTruthy(); }); - it('should notify about backend errors', done => { + it('should notify about backend errors with message if exist', done => { const next: HttpHandler = { handle: () => { return throwError( @@ -66,6 +70,26 @@ describe('ErrorInterceptor', () => { ); }); + it('should notify about backend errors with default message', done => { + const next: HttpHandler = { + handle: () => { + return throwError(new HttpErrorResponse({ status: 500 })); + }, + }; + + const requestMock = new HttpRequest('GET', '/test'); + + interceptor.intercept(requestMock, next).subscribe( + () => ({}), + () => { + expect(notificationServiceMock.notify).toHaveBeenCalledWith( + 'Something went wrong. Check the Terminal for details.' + ); + done(); + } + ); + }); + it('should notify about other errors', done => { const next: HttpHandler = { handle: () => { @@ -79,7 +103,7 @@ describe('ErrorInterceptor', () => { () => ({}), () => { expect(notificationServiceMock.notify).toHaveBeenCalledWith( - 'Back End is not responding. Please, try again later.' + 'Testrun is not responding. Please try again in a moment.' ); done(); } @@ -99,7 +123,7 @@ describe('ErrorInterceptor', () => { () => ({}), () => { expect(notificationServiceMock.notify).toHaveBeenCalledWith( - 'Back End is not responding. Please, try again later.' + 'Testrun is not responding. Please try again in a moment.' ); done(); } diff --git a/modules/ui/src/app/interceptors/error.interceptor.ts b/modules/ui/src/app/interceptors/error.interceptor.ts index 924cbde02..9e653895a 100644 --- a/modules/ui/src/app/interceptors/error.interceptor.ts +++ b/modules/ui/src/app/interceptors/error.interceptor.ts @@ -57,17 +57,19 @@ export class ErrorInterceptor implements HttpInterceptor { catchError((error: HttpErrorResponse | TimeoutError) => { if (error instanceof TimeoutError) { this.notificationService.notify( - 'Back End is not responding. Please, try again later.' + 'Testrun is not responding. Please try again in a moment.' ); } else { if (error.status === 0) { this.notificationService.notify( - 'Back End is not responding. Please, try again later.' + 'Testrun is not responding. Please try again in a moment.' ); } else { this.notificationService.notify( - error.error?.error || error.message + error.error?.error || + 'Something went wrong. Check the Terminal for details.' ); + console.error(error.error?.error || error.message); } } return throwError(error); diff --git a/modules/ui/src/app/mocks/device.mock.ts b/modules/ui/src/app/mocks/device.mock.ts index 6066593e6..86ef4ffd7 100644 --- a/modules/ui/src/app/mocks/device.mock.ts +++ b/modules/ui/src/app/mocks/device.mock.ts @@ -13,9 +13,28 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Device } from '../model/device'; +import { + Device, + DeviceStatus, + DeviceQuestionnaireSection, +} from '../model/device'; +import { ProfileRisk } from '../model/profile'; +import { FormControlType } from '../model/question'; export const device = { + status: DeviceStatus.VALID, + manufacturer: 'Delta', + model: 'O3-DIN-CPU', + mac_addr: '00:1e:42:35:73:c4', + test_modules: { + dns: { + enabled: true, + }, + }, +} as Device; + +export const expired_device = { + status: DeviceStatus.INVALID, manufacturer: 'Delta', model: 'O3-DIN-CPU', mac_addr: '00:1e:42:35:73:c4', @@ -26,6 +45,7 @@ export const device = { }, } as Device; export const updated_device = { + status: DeviceStatus.VALID, manufacturer: 'Alpha', model: 'O3-XYZ-CPU', mac_addr: '00:1e:42:35:73:11', @@ -43,8 +63,63 @@ export const MOCK_TEST_MODULES = [ enabled: true, }, { - displayName: 'Smart Ready', + displayName: 'Udmi', name: 'udmi', - enabled: false, + enabled: true, + }, +]; + +export const MOCK_MODULES = ['Connection', 'Udmi']; + +export const DEVICES_FORM: DeviceQuestionnaireSection[] = [ + { + step: 1, + title: 'Step 1 title', + description: 'Step 1 description', + questions: [ + { + id: 1, + question: 'What type of device is this?', + type: FormControlType.SELECT, + options: [ + { + text: 'Building Automation Gateway', + risk: ProfileRisk.HIGH, + id: 1, + }, + { + text: 'IoT Gateway', + risk: ProfileRisk.LIMITED, + id: 2, + }, + ], + }, + { + id: 2, + question: 'Does your device process any sensitive information? ', + type: FormControlType.SELECT, + options: [ + { + id: 1, + text: 'Yes', + risk: ProfileRisk.LIMITED, + }, + { + id: 2, + text: 'No', + risk: ProfileRisk.HIGH, + }, + ], + }, + { + id: 3, + question: 'Please select the technology this device falls into', + type: FormControlType.SELECT, + options: [ + { text: 'Hardware - Access Control' }, + { text: 'Hardware - Air quality' }, + ], + }, + ], }, ]; diff --git a/modules/ui/src/app/mocks/profile.mock.ts b/modules/ui/src/app/mocks/profile.mock.ts index 3715685cd..3082d39d2 100644 --- a/modules/ui/src/app/mocks/profile.mock.ts +++ b/modules/ui/src/app/mocks/profile.mock.ts @@ -14,12 +14,8 @@ * limitations under the License. */ -import { - FormControlType, - Profile, - ProfileFormat, - ProfileStatus, -} from '../model/profile'; +import { Profile, ProfileFormat, ProfileStatus } from '../model/profile'; +import { FormControlType } from '../model/question'; export const PROFILE_MOCK: Profile = { name: 'Primary profile', @@ -155,3 +151,57 @@ export const RENAME_PROFILE_MOCK = { name: 'Primary profile', rename: 'New profile', }; + +export const COPY_PROFILE_MOCK: Profile = { + name: 'Copy of Primary profile', + status: ProfileStatus.VALID, + questions: [ + { + question: 'What is the email of the device owner(s)?', + answer: 'boddey@google.com, cmeredith@google.com', + }, + { + question: 'What type of device do you need reviewed?', + answer: 'IoT Sensor', + }, + { + question: 'Are any of the following statements true about your device?', + answer: 'First', + }, + { + question: 'What features does the device have?', + answer: [0, 1, 2], + }, + { + question: 'Comments', + answer: 'Yes', + }, + ], +}; + +export const OUTDATED_DRAFT_PROFILE_MOCK: Profile = { + name: 'Outdated profile', + status: ProfileStatus.DRAFT, + questions: [ + { + question: 'Old question', + answer: 'qwerty', + }, + { + question: 'What is the email of the device owner(s)?', + answer: 'boddey@google.com, cmeredith@google.com', + }, + { + question: 'What type of device do you need reviewed?', + answer: 'IoT Sensor', + }, + { + question: 'Another old question', + answer: 'qwerty', + }, + ], +}; + +export const EXPIRED_PROFILE_MOCK: Profile = Object.assign({}, PROFILE_MOCK, { + status: ProfileStatus.EXPIRED, +}); diff --git a/modules/ui/src/app/mocks/reports.mock.ts b/modules/ui/src/app/mocks/reports.mock.ts index 0cfb39420..2889b0571 100644 --- a/modules/ui/src/app/mocks/reports.mock.ts +++ b/modules/ui/src/app/mocks/reports.mock.ts @@ -1,16 +1,20 @@ import { HistoryTestrun, TestrunStatus } from '../model/testrun-status'; import { MatTableDataSource } from '@angular/material/table'; +import { DeviceStatus, TestingType } from '../model/device'; export const HISTORY = [ { mac_addr: '01:02:03:04:05:06', status: 'compliant', device: { + status: DeviceStatus.VALID, manufacturer: 'Delta', model: '03-DIN-SRC', mac_addr: '01:02:03:04:05:06', firmware: '1.2.2', + test_pack: TestingType.Qualification, }, + tags: [], report: 'https://api.testrun.io/report.pdf', started: '2023-06-23T10:11:00.123Z', finished: '2023-06-23T10:17:10.123Z', @@ -19,15 +23,34 @@ export const HISTORY = [ status: 'compliant', mac_addr: '01:02:03:04:05:07', device: { + status: DeviceStatus.VALID, manufacturer: 'Delta', model: '03-DIN-SRC', mac_addr: '01:02:03:04:05:07', firmware: '1.2.3', + test_pack: TestingType.Qualification, }, + tags: [], report: 'https://api.testrun.io/report.pdf', started: '2023-07-23T10:11:00.123Z', finished: '2023-07-23T10:17:10.123Z', }, + { + mac_addr: null, + status: 'compliant', + device: { + status: DeviceStatus.VALID, + manufacturer: 'Delta', + model: '03-DIN-SRC', + mac_addr: '01:02:03:04:05:08', + firmware: '1.2.2', + test_pack: TestingType.Qualification, + }, + tags: [], + report: 'https://api.testrun.io/report.pdf', + started: '2023-06-23T10:11:00.123Z', + finished: '2023-06-23T10:17:10.123Z', + }, ] as TestrunStatus[]; export const HISTORY_AFTER_REMOVE = [ @@ -35,17 +58,33 @@ export const HISTORY_AFTER_REMOVE = [ mac_addr: '01:02:03:04:05:06', status: 'compliant', device: { + status: DeviceStatus.VALID, manufacturer: 'Delta', model: '03-DIN-SRC', mac_addr: '01:02:03:04:05:06', firmware: '1.2.2', + test_pack: TestingType.Qualification, }, + tags: [], + report: 'https://api.testrun.io/report.pdf', + started: '2023-06-23T10:11:00.123Z', + finished: '2023-06-23T10:17:10.123Z', + }, + { + mac_addr: null, + status: 'compliant', + device: { + status: DeviceStatus.VALID, + manufacturer: 'Delta', + model: '03-DIN-SRC', + mac_addr: '01:02:03:04:05:08', + firmware: '1.2.2', + test_pack: TestingType.Qualification, + }, + tags: [], report: 'https://api.testrun.io/report.pdf', started: '2023-06-23T10:11:00.123Z', finished: '2023-06-23T10:17:10.123Z', - deviceFirmware: '1.2.2', - deviceInfo: 'Delta 03-DIN-SRC', - duration: '06m 10s', }, ]; @@ -54,33 +93,61 @@ export const FORMATTED_HISTORY = [ status: 'compliant', mac_addr: '01:02:03:04:05:06', device: { + status: DeviceStatus.VALID, manufacturer: 'Delta', model: '03-DIN-SRC', mac_addr: '01:02:03:04:05:06', firmware: '1.2.2', + test_pack: TestingType.Qualification, }, + tags: [], report: 'https://api.testrun.io/report.pdf', started: '2023-06-23T10:11:00.123Z', finished: '2023-06-23T10:17:10.123Z', deviceFirmware: '1.2.2', deviceInfo: 'Delta 03-DIN-SRC', duration: '06m 10s', + program: 'Device Qualification', }, { status: 'compliant', mac_addr: '01:02:03:04:05:07', device: { + status: DeviceStatus.VALID, manufacturer: 'Delta', model: '03-DIN-SRC', mac_addr: '01:02:03:04:05:07', firmware: '1.2.3', + test_pack: TestingType.Qualification, }, + tags: [], report: 'https://api.testrun.io/report.pdf', started: '2023-07-23T10:11:00.123Z', finished: '2023-07-23T10:17:10.123Z', deviceFirmware: '1.2.3', deviceInfo: 'Delta 03-DIN-SRC', duration: '06m 10s', + program: 'Device Qualification', + }, + { + mac_addr: null, + status: 'compliant', + device: { + status: DeviceStatus.VALID, + manufacturer: 'Delta', + model: '03-DIN-SRC', + mac_addr: '01:02:03:04:05:08', + firmware: '1.2.2', + test_pack: TestingType.Qualification, + }, + tags: [], + report: 'https://api.testrun.io/report.pdf', + started: '2023-06-23T10:11:00.123Z', + finished: '2023-06-23T10:17:10.123Z', + deviceFirmware: '1.2.2', + deviceInfo: 'Delta 03-DIN-SRC', + duration: '06m 10s', + program: 'Device Qualification', }, ]; diff --git a/modules/ui/src/app/mocks/settings.mock.ts b/modules/ui/src/app/mocks/settings.mock.ts index baab9a2c0..53f092a0b 100644 --- a/modules/ui/src/app/mocks/settings.mock.ts +++ b/modules/ui/src/app/mocks/settings.mock.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { SystemConfig, SystemInterfaces } from '../model/setting'; +import { Adapters, SystemConfig, SystemInterfaces } from '../model/setting'; export const MOCK_SYSTEM_CONFIG_WITH_NO_DATA: SystemConfig = { network: { @@ -33,16 +33,21 @@ export const MOCK_SYSTEM_CONFIG_WITH_DATA: SystemConfig = { monitor_period: 600, }; -export const MOCK_INTERFACES: SystemInterfaces = { - mockDeviceKey: 'mockDeviceValue', - mockInternetKey: 'mockInternetValue', +export const MOCK_SYSTEM_CONFIG_WITH_SINGLE_PORT: SystemConfig = { + network: { + device_intf: 'mockDeviceKey', + internet_intf: '', + }, + log_level: 'DEBUG', + monitor_period: 600, + single_intf: true, }; -export const MOCK_INTERNET_OPTIONS: SystemInterfaces = { - '': 'Not specified', +export const MOCK_INTERFACES: SystemInterfaces = { mockDeviceKey: 'mockDeviceValue', mockInternetKey: 'mockInternetValue', }; + export const MOCK_DEVICE_VALUE: SystemInterfaces = { key: 'mockDeviceKey', value: 'mockDeviceValue', @@ -60,3 +65,8 @@ export const MOCK_PERIOD_VALUE: SystemInterfaces = { key: '600', value: 'Very slow device', }; + +export const MOCK_ADAPTERS: Adapters = { + adapters_added: { mockNewInternetKey: 'mockNewInternetValue' }, + adapters_removed: { mockInternetKey: 'mockInternetValue' }, +}; diff --git a/modules/ui/src/app/mocks/testrun.mock.ts b/modules/ui/src/app/mocks/testrun.mock.ts index 0572e79c0..c90927cd3 100644 --- a/modules/ui/src/app/mocks/testrun.mock.ts +++ b/modules/ui/src/app/mocks/testrun.mock.ts @@ -19,6 +19,7 @@ import { TestrunStatus, TestsData, } from '../model/testrun-status'; +import { DeviceStatus } from '../model/device'; export const TEST_DATA_RESULT: IResult[] = [ { @@ -32,6 +33,11 @@ export const TEST_DATA_RESULT: IResult[] = [ 'The device should use the DNS server provided by the DHCP server', result: 'Non-Compliant', }, + { + name: 'dns.mdns', + description: 'Does the device has MDNS (or any kind of IP multicast)', + result: 'Not Started', + }, ]; export const TEST_DATA_RESULT_WITH_RECOMMENDATIONS: IResult[] = [ @@ -47,9 +53,28 @@ export const TEST_DATA_RESULT_WITH_RECOMMENDATIONS: IResult[] = [ }, ]; +export const TEST_DATA_RESULT_WITH_ERROR: IResult[] = [ + { + name: 'dns.network.hostname_resolution', + description: 'The device should resolve hostnames', + result: 'Compliant', + }, + { + name: 'dns.network.from_dhcp', + description: + 'The device should use the DNS server provided by the DHCP server', + result: 'Error', + }, + { + name: 'dns.mdns', + description: 'Does the device has MDNS (or any kind of IP multicast)', + result: 'Not Started', + }, +]; + export const TEST_DATA_TABLE_RESULT: IResult[] = [ ...TEST_DATA_RESULT, - ...new Array(24).fill(null).map(() => ({}) as IResult), + ...new Array(23).fill(null).map(() => ({}) as IResult), ]; export const EMPTY_RESULT = new Array(100) @@ -65,12 +90,13 @@ const PROGRESS_DATA_RESPONSE = ( status: string, finished: string | null, tests: TestsData | IResult[], - report?: string + report: string = '' ) => { return { status, mac_addr: '01:02:03:04:05:06', device: { + status: DeviceStatus.VALID, manufacturer: 'Delta', model: '03-DIN-CPU', mac_addr: '01:02:03:04:05:06', @@ -80,6 +106,7 @@ const PROGRESS_DATA_RESPONSE = ( finished, tests, report, + tags: ['VSA', 'Other tag', 'And one more'], }; }; @@ -131,3 +158,10 @@ export const MOCK_PROGRESS_DATA_WAITING_FOR_DEVICE: TestrunStatus = { status: StatusOfTestrun.WaitingForDevice, started: null, }; + +export const MOCK_PROGRESS_DATA_WITH_ERROR: TestrunStatus = + PROGRESS_DATA_RESPONSE(StatusOfTestrun.InProgress, null, { + ...TEST_DATA, + total: 3, + results: TEST_DATA_RESULT_WITH_ERROR, + }); diff --git a/modules/ui/src/app/mocks/topic.mock.ts b/modules/ui/src/app/mocks/topic.mock.ts new file mode 100644 index 000000000..4309ae84f --- /dev/null +++ b/modules/ui/src/app/mocks/topic.mock.ts @@ -0,0 +1,5 @@ +import { InternetConnection } from '../model/topic'; + +export const MOCK_INTERNET: InternetConnection = { + connection: false, +}; diff --git a/modules/ui/src/app/model/callout-type.ts b/modules/ui/src/app/model/callout-type.ts index c784b46f6..c8f86d1c4 100644 --- a/modules/ui/src/app/model/callout-type.ts +++ b/modules/ui/src/app/model/callout-type.ts @@ -15,6 +15,7 @@ */ export enum CalloutType { Info = 'info', + InfoPilot = 'info pilot', Check = 'check_circle', Warning = 'warning_amber', Error = 'error', diff --git a/modules/ui/src/app/model/device.ts b/modules/ui/src/app/model/device.ts index ba526e661..55d383401 100644 --- a/modules/ui/src/app/model/device.ts +++ b/modules/ui/src/app/model/device.ts @@ -13,12 +13,25 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { QuestionFormat } from './question'; +import { Question } from './profile'; + export interface Device { manufacturer: string; model: string; mac_addr: string; test_modules?: TestModules; firmware?: string; + status?: DeviceStatus; + type?: string; + technology?: string; + test_pack?: TestingType; + additional_info?: Question[]; +} + +export enum DeviceStatus { + VALID = 'Valid', + INVALID = 'Invalid', } /** @@ -43,3 +56,19 @@ export enum DeviceView { Basic = 'basic', WithActions = 'with actions', } + +export interface DeviceQuestionnaireSection { + step: number; + title?: string; + description?: string; + questions: QuestionnaireFormat[]; +} + +export interface QuestionnaireFormat extends QuestionFormat { + id: number; +} + +export enum TestingType { + Pilot = 'Pilot Assessment', + Qualification = 'Device Qualification', +} diff --git a/modules/ui/src/app/model/profile.ts b/modules/ui/src/app/model/profile.ts index efdb779e6..37ee31b21 100644 --- a/modules/ui/src/app/model/profile.ts +++ b/modules/ui/src/app/model/profile.ts @@ -13,6 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +import { QuestionFormat } from './question'; export interface Profile { name: string; risk?: string; @@ -22,32 +24,7 @@ export interface Profile { created?: string; } -export interface Question { - question?: string; - answer?: string | number[]; -} - -export enum FormControlType { - SELECT = 'select', - TEXTAREA = 'text-long', - EMAIL_MULTIPLE = 'email-multiple', - SELECT_MULTIPLE = 'select-multiple', - TEXT = 'text', -} - -export interface Validation { - required?: boolean; - max?: string; -} - -export interface ProfileFormat { - question: string; - type: FormControlType; - description?: string; - options?: string[]; - default?: string; - validation?: Validation; -} +export type ProfileFormat = QuestionFormat; export interface Question { question?: string; @@ -62,6 +39,7 @@ export enum ProfileRisk { export enum ProfileStatus { VALID = 'Valid', DRAFT = 'Draft', + EXPIRED = 'Expired', } export interface RiskResultClassName { diff --git a/modules/ui/src/app/model/program-type.ts b/modules/ui/src/app/model/program-type.ts new file mode 100644 index 000000000..6c6d8b9f5 --- /dev/null +++ b/modules/ui/src/app/model/program-type.ts @@ -0,0 +1,19 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export enum ProgramType { + Pilot = 'pilot', + Qualification = 'qualification', +} diff --git a/modules/ui/src/app/model/question.ts b/modules/ui/src/app/model/question.ts new file mode 100644 index 000000000..284fcdd3d --- /dev/null +++ b/modules/ui/src/app/model/question.ts @@ -0,0 +1,38 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export interface Validation { + required: boolean | undefined; + max?: string; +} + +export enum FormControlType { + SELECT = 'select', + TEXTAREA = 'text-long', + EMAIL_MULTIPLE = 'email-multiple', + SELECT_MULTIPLE = 'select-multiple', + TEXT = 'text', +} + +export interface QuestionFormat { + question: string; + type: FormControlType; + description?: string; + options?: OptionType[]; + default?: string; + validation?: Validation; +} + +export type OptionType = string | object; diff --git a/modules/ui/src/app/model/setting.ts b/modules/ui/src/app/model/setting.ts index 5e71052f3..02c3ec0ce 100644 --- a/modules/ui/src/app/model/setting.ts +++ b/modules/ui/src/app/model/setting.ts @@ -20,6 +20,7 @@ export interface SystemConfig { } | null; log_level?: string; monitor_period?: number; + single_intf?: boolean; } export interface InterfacesValidation { @@ -48,3 +49,8 @@ export enum FormKey { export type SystemInterfaces = { [key: string]: string; }; + +export type Adapters = { + adapters_added?: SystemInterfaces; + adapters_removed?: SystemInterfaces; +}; diff --git a/modules/ui/src/app/model/testrun-status.ts b/modules/ui/src/app/model/testrun-status.ts index 2ac908185..faa707f92 100644 --- a/modules/ui/src/app/model/testrun-status.ts +++ b/modules/ui/src/app/model/testrun-status.ts @@ -16,18 +16,21 @@ import { Device } from './device'; export interface TestrunStatus { - mac_addr: string; + mac_addr: string | null; status: string; + description?: string; device: IDevice; started: string | null; finished: string | null; tests?: TestsResponse; - report?: string; + report: string; + tags: string[] | null; } export interface HistoryTestrun extends TestrunStatus { deviceFirmware: string; deviceInfo: string; + program: string; duration: string; } @@ -75,16 +78,33 @@ export enum StatusOfTestResult { NotStarted = 'Not Started', InProgress = 'In Progress', Error = 'Error', // test failed to run - Info = 'Informational', // nice to know information, not necessarily compliant/non-compliant + Info = 'Informational', // nice to know information, not necessarily compliant/non-compliant, + Skipped = 'Skipped', + Disabled = 'Disabled', } export interface StatusResultClassName { green: boolean; red: boolean; blue: boolean; + cyan: boolean; grey: boolean; } +export const IDLE_STATUS = { + status: StatusOfTestrun.Idle, + device: {} as IDevice, + started: null, + finished: null, + report: '', + mac_addr: '', + tests: { + total: 0, + results: [], + }, + tags: [], +} as TestrunStatus; + export type TestrunStatusKey = keyof typeof StatusOfTestrun; export type TestrunStatusValue = (typeof StatusOfTestrun)[TestrunStatusKey]; export type TestResultKey = keyof typeof StatusOfTestResult; diff --git a/modules/ui/src/app/model/topic.ts b/modules/ui/src/app/model/topic.ts new file mode 100644 index 000000000..d330dbb82 --- /dev/null +++ b/modules/ui/src/app/model/topic.ts @@ -0,0 +1,9 @@ +export enum Topic { + NetworkAdapters = 'events/adapter', + InternetConnection = 'events/internet', + Status = 'status', +} + +export interface InternetConnection { + connection: boolean | null; +} diff --git a/modules/ui/src/app/model/version.ts b/modules/ui/src/app/model/version.ts index 21ec6ee38..5eb946e42 100644 --- a/modules/ui/src/app/model/version.ts +++ b/modules/ui/src/app/model/version.ts @@ -23,5 +23,4 @@ export interface Version { export interface ConsentDialogResult { grant: boolean; - isNavigateToRiskAssessment?: boolean; } diff --git a/modules/ui/src/app/pages/certificates/certificate.validator.ts b/modules/ui/src/app/pages/certificates/certificate.validator.ts index 9143824e8..72e197af1 100644 --- a/modules/ui/src/app/pages/certificates/certificate.validator.ts +++ b/modules/ui/src/app/pages/certificates/certificate.validator.ts @@ -9,7 +9,6 @@ export const getValidationErrors = (file: File): string[] => { errors.push(validateExtension(file.name)); errors.push(validateFileNameLength(file.name)); errors.push(validateSize(file.size)); - // @ts-expect-error null values are filtered return errors.filter(error => error !== null); }; const validateFileName = (name: string): string | null => { diff --git a/modules/ui/src/app/pages/certificates/certificates.component.html b/modules/ui/src/app/pages/certificates/certificates.component.html index d75f115b4..bcc5173b6 100644 --- a/modules/ui/src/app/pages/certificates/certificates.component.html +++ b/modules/ui/src/app/pages/certificates/certificates.component.html @@ -39,14 +39,5 @@

Certificates

deleteCertificate($event) ">
-
diff --git a/modules/ui/src/app/pages/certificates/certificates.component.spec.ts b/modules/ui/src/app/pages/certificates/certificates.component.spec.ts index c8f0e91b6..1deda3d21 100644 --- a/modules/ui/src/app/pages/certificates/certificates.component.spec.ts +++ b/modules/ui/src/app/pages/certificates/certificates.component.spec.ts @@ -89,20 +89,6 @@ describe('CertificatesComponent', () => { expect(component.closeCertificatedEvent.emit).toHaveBeenCalled(); }); - it('should emit closeSettingEvent when close button clicked', () => { - const headerCloseButton = fixture.nativeElement.querySelector( - '.close-button' - ) as HTMLButtonElement; - spyOn(component.closeCertificatedEvent, 'emit'); - - headerCloseButton.click(); - - expect(mockLiveAnnouncer.announce).toHaveBeenCalledWith( - 'The certificates panel is closed.' - ); - expect(component.closeCertificatedEvent.emit).toHaveBeenCalled(); - }); - it('should have upload file button', () => { const uploadCertificatesButton = fixture.nativeElement.querySelector( '.browse-files-button' @@ -166,9 +152,9 @@ describe('CertificatesComponent', () => { flush(); })); - it('should focus navigation button if next active element does not exist', fakeAsync(() => { + it('should focus navigation close button if next active element does not exist', fakeAsync(() => { const nextButton = window.document.querySelector( - '.certificates-drawer-content .close-button' + '.certificates-drawer-header .certificates-drawer-header-button' ) as HTMLButtonElement; const buttonFocusSpy = spyOn(nextButton, 'focus'); diff --git a/modules/ui/src/app/pages/certificates/certificates.component.ts b/modules/ui/src/app/pages/certificates/certificates.component.ts index bf6cf19fa..3b8cccccc 100644 --- a/modules/ui/src/app/pages/certificates/certificates.component.ts +++ b/modules/ui/src/app/pages/certificates/certificates.component.ts @@ -104,7 +104,7 @@ export class CertificatesComponent implements OnDestroy { } else { // If next interactive element doest not exist, close button will be focused const menuButton = window.document.querySelector( - '.certificates-drawer-content .close-button' + '.certificates-drawer-header .certificates-drawer-header-button' ) as HTMLButtonElement; menuButton?.focus(); } diff --git a/modules/ui/src/app/pages/certificates/certificates.store.spec.ts b/modules/ui/src/app/pages/certificates/certificates.store.spec.ts index 06e3accf6..5e66104e6 100644 --- a/modules/ui/src/app/pages/certificates/certificates.store.spec.ts +++ b/modules/ui/src/app/pages/certificates/certificates.store.spec.ts @@ -42,6 +42,8 @@ describe('CertificatesStore', () => { 'uploadCertificate', 'deleteCertificate', ]); + // @ts-expect-error data layer should be defined + window.dataLayer = window.dataLayer || []; TestBed.configureTestingModule({ imports: [NoopAnimationsModule], @@ -152,6 +154,21 @@ describe('CertificatesStore', () => { container ); }); + + it('should send GA event "successful_saving_certificate"', () => { + const container = document.createElement('DIV'); + container.classList.add('certificates-drawer-content'); + document.querySelector('body')?.appendChild(container); + certificateStore.uploadCertificate(FILE); + + expect( + // @ts-expect-error data layer should be defined + window.dataLayer.some( + (item: { event: string }) => + item.event === 'successful_saving_certificate' + ) + ).toBeTruthy(); + }); }); describe('with invalid certificate file', () => { diff --git a/modules/ui/src/app/pages/certificates/certificates.store.ts b/modules/ui/src/app/pages/certificates/certificates.store.ts index 21f96eed0..610daeffb 100644 --- a/modules/ui/src/app/pages/certificates/certificates.store.ts +++ b/modules/ui/src/app/pages/certificates/certificates.store.ts @@ -97,6 +97,10 @@ export class CertificatesStore extends ComponentStore { !certificates.some(cert => cert.name === certificate.name) )[0]; this.updateCertificates(newCertificates); + // @ts-expect-error data layer is not null + window.dataLayer.push({ + event: 'successful_saving_certificate', + }); this.notify( `Certificate successfully added.\n${uploadedCertificate.name} by ${uploadedCertificate.organisation} valid until ${this.datePipe.transform(uploadedCertificate.expires, 'dd MMM yyyy')}` ); diff --git a/modules/ui/src/app/pages/devices/components/device-form/device-form.component.html b/modules/ui/src/app/pages/devices/components/device-form/device-form.component.html deleted file mode 100644 index 5978f6d22..000000000 --- a/modules/ui/src/app/pages/devices/components/device-form/device-form.component.html +++ /dev/null @@ -1,130 +0,0 @@ - -
- {{ data.title }} - - Device Manufacturer - - Please enter device manufacturer name - - Please, check. The manufacturer name must be a maximum of 28 - characters. Only letters, numbers, and accented letters are - permitted. - - - Device Manufacturer is required - - - - Device Model - - Please enter device name - - Please, check. The device model name must be a maximum of 28 - characters. Only letters, numbers, and accented letters are - permitted. - - - Device Model is required - - - - MAC address - - Please enter MAC address - - MAC address is required - - - Please, check. A MAC address consists of 12 hexadecimal digits (0 to 9, - a to f, or A to F). - - - This MAC address is already used for another device in the - repository. - - - - - - - {{ error$ | async }} - - - - - - - - - -
diff --git a/modules/ui/src/app/pages/devices/components/device-form/device-form.component.scss b/modules/ui/src/app/pages/devices/components/device-form/device-form.component.scss deleted file mode 100644 index 831f9907a..000000000 --- a/modules/ui/src/app/pages/devices/components/device-form/device-form.component.scss +++ /dev/null @@ -1,76 +0,0 @@ -/** - * Copyright 2023 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -@import 'src/theming/colors'; - -$device-form-max-width: 580px; -$device-form-min-width: 285px; - -:host { - display: grid; - grid-template-rows: 1fr; - overflow: auto; - grid-template-columns: minmax(285px, $device-form-max-width); -} - -.device-form { - display: grid; - padding: 24px; - max-width: $device-form-max-width; - min-width: $device-form-min-width; - gap: 10px; - overflow: auto; -} - -.manufacturer-field, -.model-field { - &::ng-deep.mat-mdc-form-field-subscript-wrapper:has(mat-error) { - height: 40px; - } -} - -.device-form-title { - color: $grey-800; - font-size: 22px; - line-height: 28px; - padding-bottom: 14px; -} - -.device-form-test-modules { - overflow: auto; - min-height: 78px; -} - -.device-form-actions { - padding: 0; - min-height: 30px; -} - -.close-button { - color: $primary; -} - -.device-form-mac-address-error { - white-space: nowrap; -} - -.delete-button { - color: $primary; - margin-right: auto; -} - -.hidden { - display: none; -} diff --git a/modules/ui/src/app/pages/devices/components/device-form/device-form.component.spec.ts b/modules/ui/src/app/pages/devices/components/device-form/device-form.component.spec.ts deleted file mode 100644 index 6ea72aa52..000000000 --- a/modules/ui/src/app/pages/devices/components/device-form/device-form.component.spec.ts +++ /dev/null @@ -1,459 +0,0 @@ -/** - * Copyright 2023 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { - ComponentFixture, - fakeAsync, - flush, - TestBed, -} from '@angular/core/testing'; - -import { DeviceFormComponent, FormAction } from './device-form.component'; -import { MatButtonModule } from '@angular/material/button'; -import { FormControl, ReactiveFormsModule } from '@angular/forms'; -import { MatCheckboxModule } from '@angular/material/checkbox'; -import { MatInputModule } from '@angular/material/input'; -import { - MAT_DIALOG_DATA, - MatDialogModule, - MatDialogRef, -} from '@angular/material/dialog'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { Device } from '../../../../model/device'; -import { of } from 'rxjs'; -import { DeviceTestsComponent } from '../../../../components/device-tests/device-tests.component'; -import { SpinnerComponent } from '../../../../components/spinner/spinner.component'; -import { NgxMaskDirective, NgxMaskPipe, provideNgxMask } from 'ngx-mask'; -import { DevicesStore } from '../../devices.store'; -import { device, MOCK_TEST_MODULES } from '../../../../mocks/device.mock'; -import SpyObj = jasmine.SpyObj; - -describe('DeviceFormComponent', () => { - let component: DeviceFormComponent; - let fixture: ComponentFixture; - let mockDevicesStore: SpyObj; - let compiled: HTMLElement; - - beforeEach(() => { - mockDevicesStore = jasmine.createSpyObj('DevicesStore', [ - 'editDevice', - 'saveDevice', - ]); - - TestBed.configureTestingModule({ - declarations: [DeviceFormComponent], - providers: [ - { provide: DevicesStore, useValue: mockDevicesStore }, - { - provide: MatDialogRef, - useValue: { - keydownEvents: () => of(new KeyboardEvent('keydown', { code: '' })), - close: () => ({}), - }, - }, - { provide: MAT_DIALOG_DATA, useValue: {} }, - provideNgxMask(), - ], - imports: [ - MatButtonModule, - ReactiveFormsModule, - MatCheckboxModule, - MatInputModule, - MatDialogModule, - BrowserAnimationsModule, - DeviceTestsComponent, - SpinnerComponent, - NgxMaskDirective, - NgxMaskPipe, - ], - }); - TestBed.overrideProvider(DevicesStore, { useValue: mockDevicesStore }); - - fixture = TestBed.createComponent(DeviceFormComponent); - component = fixture.componentInstance; - compiled = fixture.nativeElement as HTMLElement; - component.data = { - testModules: MOCK_TEST_MODULES, - devices: [], - }; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - it('should contain device form', () => { - const form = compiled.querySelector('.device-form'); - - expect(form).toBeTruthy(); - }); - - it('should close dialog on "cancel" click', () => { - const closeSpy = spyOn(component.dialogRef, 'close'); - const closeButton = compiled.querySelector( - '.close-button' - ) as HTMLButtonElement; - - closeButton?.click(); - - expect(closeSpy).toHaveBeenCalledWith(); - - closeSpy.calls.reset(); - }); - - it('should not save data when fields are empty', () => { - const saveButton = compiled.querySelector( - '.save-button' - ) as HTMLButtonElement; - const model: HTMLInputElement = compiled.querySelector( - '.device-form-model' - ) as HTMLInputElement; - const manufacturer: HTMLInputElement = compiled.querySelector( - '.device-form-manufacturer' - ) as HTMLInputElement; - const macAddress: HTMLInputElement = compiled.querySelector( - '.device-form-mac-address' - ) as HTMLInputElement; - - ['', ' '].forEach(value => { - model.value = value; - model.dispatchEvent(new Event('input')); - manufacturer.value = value; - manufacturer.dispatchEvent(new Event('input')); - macAddress.value = value; - macAddress.dispatchEvent(new Event('input')); - saveButton?.click(); - fixture.detectChanges(); - - const requiredErrors = compiled.querySelectorAll('mat-error'); - expect(requiredErrors?.length).toEqual(3); - - requiredErrors.forEach(error => { - expect(error?.innerHTML).toContain('required'); - }); - }); - }); - - it('should not save data if no test selected', fakeAsync(() => { - component.model.setValue('model'); - component.manufacturer.setValue('manufacturer'); - component.mac_addr.setValue('07:07:07:07:07:07'); - component.test_modules.setValue([false, false]); - - component.saveDevice(); - fixture.detectChanges(); - - const error = compiled.querySelector('mat-error'); - expect(error?.innerHTML).toContain( - 'At least one test has to be selected to save a Device.' - ); - - flush(); - })); - - it('should save data when form is valid', () => { - const device: Device = { - manufacturer: 'manufacturer', - model: 'model', - mac_addr: '07:07:07:07:07:07', - test_modules: { - connection: { - enabled: true, - }, - udmi: { - enabled: false, - }, - }, - }; - component.model.setValue('model'); - component.manufacturer.setValue('manufacturer'); - component.mac_addr.setValue('07:07:07:07:07:07'); - - component.saveDevice(); - - const args = mockDevicesStore.saveDevice.calls.argsFor(0); - // @ts-expect-error config is in object - expect(args[0].device).toEqual(device); - expect(mockDevicesStore.saveDevice).toHaveBeenCalled(); - }); - - describe('test modules', () => { - it('should be present', () => { - const test = compiled.querySelectorAll('mat-checkbox'); - - expect(test.length).toEqual(2); - }); - - it('should be enabled', () => { - const tests = compiled.querySelectorAll('.device-form-test-modules p'); - - expect(tests[0].classList.contains('disabled')).toEqual(false); - }); - }); - - describe('device model', () => { - it('should not contain errors when input is correct', () => { - const model: HTMLInputElement = compiled.querySelector( - '.device-form-model' - ) as HTMLInputElement; - ['model', 'Gebäude', 'jardín'].forEach(value => { - model.value = value; - model.dispatchEvent(new Event('input')); - - const errors = component.model.errors; - const uiValue = model.value; - const formValue = component.model.value; - - expect(uiValue).toEqual(formValue); - expect(errors).toBeNull(); - }); - }); - - it('should have "invalid_format" error when field does not satisfy validation rules', () => { - [ - 'very long value very long value very long value very long value very long value very long value very long value', - 'as&@3$', - ].forEach(value => { - const model: HTMLInputElement = compiled.querySelector( - '.device-form-model' - ) as HTMLInputElement; - model.value = value; - model.dispatchEvent(new Event('input')); - component.model.markAsTouched(); - fixture.detectChanges(); - - const modelError = compiled.querySelector('mat-error')?.innerHTML; - const error = component.model.hasError('invalid_format'); - - expect(error).toBeTruthy(); - expect(modelError).toContain( - 'The device model name must be a maximum of 28 characters. Only letters, numbers, and accented letters are permitted.' - ); - }); - }); - }); - - describe('device manufacturer', () => { - it('should not contain errors when input is correct', () => { - const manufacturer: HTMLInputElement = compiled.querySelector( - '.device-form-manufacturer' - ) as HTMLInputElement; - ['manufacturer', 'Gebäude', 'jardín'].forEach(value => { - manufacturer.value = value; - manufacturer.dispatchEvent(new Event('input')); - - const errors = component.manufacturer.errors; - const uiValue = manufacturer.value; - const formValue = component.manufacturer.value; - - expect(uiValue).toEqual(formValue); - expect(errors).toBeNull(); - }); - }); - - it('should have "invalid_format" error when field does not satisfy validation', () => { - [ - 'very long value very long value very long value very long value very long value very long value very long value', - 'as&@3$', - ].forEach(value => { - const manufacturer: HTMLInputElement = compiled.querySelector( - '.device-form-manufacturer' - ) as HTMLInputElement; - manufacturer.value = value; - manufacturer.dispatchEvent(new Event('input')); - component.manufacturer.markAsTouched(); - fixture.detectChanges(); - - const manufacturerError = - compiled.querySelector('mat-error')?.innerHTML; - const error = component.manufacturer.hasError('invalid_format'); - - expect(error).toBeTruthy(); - expect(manufacturerError).toContain( - 'The manufacturer name must be a maximum of 28 characters. Only letters, numbers, and accented letters are permitted.' - ); - }); - }); - }); - - describe('mac address', () => { - it('should not be disabled', () => { - expect(component.mac_addr.disabled).toBeFalse(); - }); - - it('should not contain errors when input is correct', () => { - const macAddress: HTMLInputElement = compiled.querySelector( - '.device-form-mac-address' - ) as HTMLInputElement; - ['07:07:07:07:07:07', ' 07:07:07:07:07:07 '].forEach(value => { - macAddress.value = value; - macAddress.dispatchEvent(new Event('input')); - - const errors = component.mac_addr.errors; - const formValue = component.mac_addr.value; - - expect(macAddress.value).toEqual(formValue); - expect(errors).toBeNull(); - }); - }); - - it('should have "pattern" error when field does not satisfy pattern', () => { - ['value', 'q01e423573c4'].forEach(value => { - const macAddress: HTMLInputElement = compiled.querySelector( - '.device-form-mac-address' - ) as HTMLInputElement; - macAddress.value = value; - macAddress.dispatchEvent(new Event('input')); - component.mac_addr.markAsTouched(); - fixture.detectChanges(); - - const macAddressError = compiled.querySelector('mat-error')?.innerHTML; - const error = component.mac_addr.hasError('pattern'); - - expect(error).toBeTruthy(); - expect(macAddressError).toContain( - 'Please, check. A MAC address consists of 12 hexadecimal digits (0 to 9, a to f, or A to F).' - ); - }); - }); - - it('should have "has_same_mac_address" error when MAC address is already used', () => { - component.data = { - testModules: MOCK_TEST_MODULES, - devices: [device], - }; - component.ngOnInit(); - fixture.detectChanges(); - - const macAddress: HTMLInputElement = compiled.querySelector( - '.device-form-mac-address' - ) as HTMLInputElement; - macAddress.value = '00:1e:42:35:73:c4'; - macAddress.dispatchEvent(new Event('input')); - component.mac_addr.markAsTouched(); - fixture.detectChanges(); - - const macAddressError = compiled.querySelector('mat-error')?.innerHTML; - const error = component.mac_addr.hasError('has_same_mac_address'); - - expect(error).toBeTruthy(); - expect(macAddressError).toContain( - 'This MAC address is already used for another device in the repository.' - ); - }); - }); - - it('should have hidden delete device button', () => { - const deleteButton = compiled.querySelector( - '.delete-button' - ) as HTMLButtonElement; - - expect(deleteButton.classList.contains('hidden')).toBeTrue(); - }); - - describe('when device is present', () => { - beforeEach(() => { - component.data = { - devices: [device], - testModules: MOCK_TEST_MODULES, - device: { - manufacturer: 'Delta', - model: 'O3-DIN-CPU', - mac_addr: '00:1e:42:35:73:c4', - test_modules: { - udmi: { - enabled: true, - }, - }, - }, - }; - component.ngOnInit(); - fixture.detectChanges(); - }); - - it('should fill form values with device values', () => { - const model: HTMLInputElement = compiled.querySelector( - '.device-form-model' - ) as HTMLInputElement; - const manufacturer: HTMLInputElement = compiled.querySelector( - '.device-form-manufacturer' - ) as HTMLInputElement; - - expect(model.value).toEqual('O3-DIN-CPU'); - expect(manufacturer.value).toEqual('Delta'); - }); - - it('should save data even mac address already exist', fakeAsync(() => { - // fill the test controls - component.test_modules.push(new FormControl(false)); - component.test_modules.push(new FormControl(true)); - component.saveDevice(); - fixture.detectChanges(); - - fixture.whenStable().then(() => { - const error = compiled.querySelector('mat-error'); - expect(error).toBeFalse(); - }); - - const args = mockDevicesStore.editDevice.calls.argsFor(0); - // @ts-expect-error config is in object - expect(args[0].device).toEqual({ - manufacturer: 'Delta', - model: 'O3-DIN-CPU', - mac_addr: '00:1e:42:35:73:c4', - test_modules: { - connection: { - enabled: false, - }, - udmi: { - enabled: true, - }, - }, - }); - expect(mockDevicesStore.editDevice).toHaveBeenCalled(); - - flush(); - })); - - it('should have delete device button', () => { - const deleteButton = compiled.querySelector( - '.delete-button' - ) as HTMLButtonElement; - - expect(deleteButton.classList.contains('hidden')).toBeFalse(); - expect(deleteButton).toBeTruthy(); - }); - - it('should close dialog with delete action on "delete" click', () => { - const closeSpy = spyOn(component.dialogRef, 'close'); - const closeButton = compiled.querySelector( - '.delete-button' - ) as HTMLButtonElement; - - closeButton?.click(); - - expect(closeSpy).toHaveBeenCalledWith({ action: FormAction.Delete }); - - closeSpy.calls.reset(); - }); - }); - - it('should has loader element', () => { - const spinner = compiled.querySelector('app-spinner'); - - expect(spinner).toBeTruthy(); - }); -}); diff --git a/modules/ui/src/app/pages/devices/components/device-form/device-form.component.ts b/modules/ui/src/app/pages/devices/components/device-form/device-form.component.ts deleted file mode 100644 index 38ab05ffa..000000000 --- a/modules/ui/src/app/pages/devices/components/device-form/device-form.component.ts +++ /dev/null @@ -1,211 +0,0 @@ -/** - * Copyright 2023 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; -import { - AbstractControl, - FormArray, - FormBuilder, - FormGroup, - Validators, -} from '@angular/forms'; -import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; - -import { Device, TestModule } from '../../../../model/device'; -import { DeviceValidators } from './device.validators'; -import { Subject } from 'rxjs'; -import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; -import { EscapableDialogComponent } from '../../../../components/escapable-dialog/escapable-dialog.component'; -import { DevicesStore } from '../../devices.store'; - -const MAC_ADDRESS_PATTERN = - '^[\\s]*[a-fA-F0-9]{2}(?:[:][a-fA-F0-9]{2}){5}[\\s]*$'; - -interface DialogData { - title?: string; - device?: Device; - devices: Device[]; - testModules: TestModule[]; -} - -export enum FormAction { - Delete = 'Delete', - Save = 'Save', -} - -export interface FormResponse { - device?: Device; - action: FormAction; -} - -@Component({ - selector: 'app-device-form', - templateUrl: './device-form.component.html', - styleUrls: ['./device-form.component.scss'], - providers: [DevicesStore], -}) -export class DeviceFormComponent - extends EscapableDialogComponent - implements OnInit, OnDestroy -{ - deviceForm!: FormGroup; - testModules: TestModule[] = []; - error$: BehaviorSubject = new BehaviorSubject( - null - ); - private destroy$: Subject = new Subject(); - - constructor( - public override dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) public data: DialogData, - private fb: FormBuilder, - private deviceValidators: DeviceValidators, - private devicesStore: DevicesStore - ) { - super(dialogRef); - } - - get model() { - return this.deviceForm.get('model') as AbstractControl; - } - - get manufacturer() { - return this.deviceForm.get('manufacturer') as AbstractControl; - } - - get mac_addr() { - return this.deviceForm.get('mac_addr') as AbstractControl; - } - - get test_modules() { - return this.deviceForm.controls['test_modules'] as FormArray; - } - - ngOnInit() { - this.createDeviceForm(); - this.testModules = this.data.testModules; - if (this.data.device) { - this.model.setValue(this.data.device.model); - this.manufacturer.setValue(this.data.device.manufacturer); - this.mac_addr.setValue(this.data.device.mac_addr); - } - } - - ngOnDestroy() { - this.destroy$.next(true); - this.destroy$.unsubscribe(); - } - - delete(): void { - this.dialogRef.close({ action: FormAction.Delete } as FormResponse); - } - - cancel(): void { - this.dialogRef.close(); - } - - saveDevice() { - this.checkMandatoryFields(); - if (this.deviceForm.invalid) { - this.deviceForm.markAllAsTouched(); - return; - } - - if (this.isAllTestsDisabled()) { - this.error$.next( - 'At least one test has to be selected to save a Device.' - ); - return; - } - - const device = this.createDeviceFromForm(); - - this.updateDevice(device, () => { - this.dialogRef.close({ - action: FormAction.Save, - device, - } as FormResponse); - }); - } - - private updateDevice(device: Device, callback: () => void) { - if (this.data.device) { - this.devicesStore.editDevice({ - device, - mac_addr: this.data.device.mac_addr, - onSuccess: callback, - }); - } else { - this.devicesStore.saveDevice({ device, onSuccess: callback }); - } - } - - private isAllTestsDisabled(): boolean { - return this.deviceForm.value.test_modules.every((enabled: boolean) => { - return !enabled; - }); - } - - private createDeviceFromForm(): Device { - const testModules: { [key: string]: { enabled: boolean } } = {}; - this.deviceForm.value.test_modules.forEach( - (enabled: boolean, i: number) => { - testModules[this.testModules[i]?.name] = { - enabled: enabled, - }; - } - ); - return { - model: this.model.value.trim(), - manufacturer: this.manufacturer.value.trim(), - mac_addr: this.mac_addr.value.trim(), - test_modules: testModules, - } as Device; - } - - /** - * Model, manufacturer, MAC address are mandatory. - * It should be checked on submit. Other validation happens on blur. - */ - private checkMandatoryFields() { - this.setRequiredErrorIfEmpty(this.model); - this.setRequiredErrorIfEmpty(this.manufacturer); - this.setRequiredErrorIfEmpty(this.mac_addr); - } - - private setRequiredErrorIfEmpty(control: AbstractControl) { - if (!control.value.trim()) { - control.setErrors({ required: true }); - } - } - - private createDeviceForm() { - this.deviceForm = this.fb.group({ - model: ['', [this.deviceValidators.deviceStringFormat()]], - manufacturer: ['', [this.deviceValidators.deviceStringFormat()]], - mac_addr: [ - '', - [ - Validators.pattern(MAC_ADDRESS_PATTERN), - this.deviceValidators.differentMACAddress( - this.data.devices, - this.data.device - ), - ], - ], - test_modules: new FormArray([]), - }); - } -} diff --git a/modules/ui/src/app/pages/devices/components/device-form/device.validators.ts b/modules/ui/src/app/pages/devices/components/device-form/device.validators.ts index 2b7b23ae1..3ffa45802 100644 --- a/modules/ui/src/app/pages/devices/components/device-form/device.validators.ts +++ b/modules/ui/src/app/pages/devices/components/device-form/device.validators.ts @@ -22,8 +22,11 @@ import { Device } from '../../../../model/device'; * Validator uses for Device Name and Device Manufacturer inputs */ export class DeviceValidators { + static readonly STRING_FORMAT_MAX_LENGTH = 28; readonly STRING_FORMAT_REGEXP = new RegExp( - "^([a-z0-9\\p{L}\\p{M}.',-_ ]{1,28})$", + "^([a-z0-9\\p{L}\\p{M}.',-_ ]{1," + + DeviceValidators.STRING_FORMAT_MAX_LENGTH + + '})$', 'u' ); @@ -51,6 +54,16 @@ export class DeviceValidators { }; } + public testModulesRequired(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const value = control.value; + if (value.every((module: boolean) => !module)) { + return { required: true }; + } + return null; + }; + } + public differentMACAddress(devices: Device[], device?: Device): ValidatorFn { return (control: AbstractControl): ValidationErrors | null => { const value = control.value?.trim(); diff --git a/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.html b/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.html new file mode 100644 index 000000000..4bf0e13a5 --- /dev/null +++ b/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.html @@ -0,0 +1,347 @@ + +
+ +
+ +

{{ data.title }}

+
+
+ + + +

+ {{ data.title }} dialogue step 1 +

+
+ + Device Manufacturer + + Please enter device manufacturer name + + Please, check. The manufacturer name must be a maximum of 28 + characters. Only letters, numbers, and accented letters are + permitted. + + + Device Manufacturer is required + + + + Device Model + + Please enter device name + + Please, check. The device model name must be a maximum of 28 + characters. Only letters, numbers, and accented letters are + permitted. + + + Device Model is required + + + + MAC address + + Please enter MAC address + + MAC address is required + + + Please, check. A MAC address consists of 12 hexadecimal digits (0 + to 9, a to f, or A to F). + + + This MAC address is already used for another device in the + repository. + + + + Please, select the testing journey for device + + + + + + Device Qualification + + + + + Pilot Assessment + + + + + + + At least one test has to be selected to save a Device. + +
+
+ + + +

+ {{ data.title }} dialogue step {{ step.step + 1 }} +

+
+

+ {{ step.title }} +

+

+ {{ step.description }} +

+ +
+
+
+ + +

+ {{ data.title }} dialogue last step +

+

+ {{ data.title }} dialogue step 4 +

+
+
+

Summary

+

+ + The device has been configured. Please check the setup. + + + No changes were made to the device configuration. + + The device cannot be configured +

+
+
+ +
+

+ Device type + {{ device?.type }} +

+

+ Technology + {{ device?.technology }} +

+
+
+
+ Select Save to create your new device. You will then be able to + carry on your device testing journey: +
    +
  • + Run Testrun against your device until you achieve a compliant + result +
  • +
  • Export the Testrun report and output files
  • +
  • Send the testing results to the lab for validation
  • +
+
+ +
+
+

+ + error + + Unable to create the device +

+
+
+

+ Validation error! +

+

+ Please go back and correct the errors on + + , + + + and + + Step {{ step + 1 }}. +

+ +

+ All existing fields must be filled in. +

+
+
+
+
+
+ + + +
+
+
+
+
diff --git a/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.scss b/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.scss new file mode 100644 index 000000000..c2088c67b --- /dev/null +++ b/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.scss @@ -0,0 +1,354 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@use '@angular/material' as mat; +@import 'src/theming/colors'; +@import 'src/theming/variables'; + +$form-min-width: 732px; + +:host { + container-type: size; + container-name: qualification-form; + + display: grid; + grid-template-rows: 1fr; + overflow: auto; + grid-template-columns: minmax(285px, $form-max-width); + height: 100vh; + max-height: 978px; +} + +.device-qualification-form { + overflow: hidden; +} + +::ng-deep .device-form-test-modules { + overflow: auto; + min-height: 78px; + display: grid; + grid-template-columns: repeat(2, 1fr); + grid-template-rows: repeat(4, 1fr); + grid-auto-flow: column; + padding-top: 16px; + p { + margin: 8px 0; + } +} + +.close-button { + color: $primary; +} + +.hidden { + display: none; +} + +.device-qualification-form-header { + position: relative; + padding-top: 24px; + &-title { + margin: 0; + font-size: 32px; + font-style: normal; + font-weight: 400; + line-height: 40px; + color: $grey-800; + text-align: center; + padding: 38px 0; + background-image: url(/assets/icons/create_device_header.svg); + } + + &-close-button { + position: absolute; + right: 0; + top: 0; + min-width: 24px; + width: 24px; + height: 24px; + box-sizing: content-box; + line-height: normal !important; + padding: 0; + margin: 0; + + .close-button-icon { + width: 24px; + height: 24px; + margin: 0; + } + + ::ng-deep * { + line-height: inherit !important; + } + } +} + +.device-qualification-form-journey-label { + font-family: $font-secondary; + font-style: normal; + font-weight: 400; + font-size: 16px; + line-height: 24px; + letter-spacing: 0.1px; + color: $grey-800; + margin: 24px 16px 0 16px; +} + +.device-qualification-form-journey-button { + padding: 0 18px 0 24px; +} + +.device-qualification-form-journey-button-info { + display: flex; +} + +.device-qualification-form-journey-button-label { + font-family: $font-secondary; + font-style: normal; + font-weight: 500; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.2px; + color: $grey-800; +} + +.device-qualification-form-test-modules-container { + padding: 0 24px; +} + +.device-qualification-form-step-title { + margin: 0; + font-style: normal; + font-weight: 500; + font-size: 22px; + line-height: 28px; + text-align: center; + color: $grey-900; + padding: 0 24px; + display: inline-block; + height: 28px; +} + +.device-qualification-form-step-description { + font-family: $font-secondary; + text-align: center; + color: $grey-800; + margin: 0; + padding: 8px 16px 0 16px; +} + +.step-link { + color: $primary; + text-decoration: underline; + cursor: pointer; +} + +.device-qualification-form-step-content { + padding: 0 16px; + overflow: scroll; +} + +.device-qualification-form-page { + padding-top: 10px; + margin-top: -10px; + display: grid; + gap: 8px; + height: 100%; + overflow: hidden; + align-content: start; + &:has(.device-qualification-form-summary-container) { + grid-template-rows: min-content min-content 1fr min-content; + } +} + +.device-qualification-form-summary-container { + display: grid; + align-items: center; + justify-content: center; + overflow: scroll; + ::ng-deep { + .device-item, + .device-qualification-form-summary-info { + width: $device-item-width; + } + } +} + +.device-qualification-form-summary { + border-radius: 12px; + background: mat.m2-get-color-from-palette($color-primary, 50); + padding: 24px; + width: max-content; + margin-left: auto; + margin-right: auto; + margin-top: 24px; + &-error { + background: $red-50; + } +} + +.device-qualification-form-actions { + width: $device-item-width; + text-align: center; + padding: 8px 24px; + justify-self: center; + align-self: end; + &:has(.delete-button) { + text-align: right; + } + .close-button, + .delete-button { + border: 1px solid $lighter-grey; + } + .delete-button { + color: $primary; + float: left; + } + .close-button { + padding: 0 16px; + } + .save-button { + margin-left: 16px; + } +} + +.device-qualification-form-summary-info { + margin-top: 16px; + border-radius: 12px; + padding: 16px 24px; + background: #fff; + box-sizing: border-box; + &-title, + &-title-error { + font-size: 18px; + font-weight: 400; + line-height: 24px; + text-align: center; + color: $grey-900; + } + &-description { + font-family: $font-secondary; + font-size: 16px; + font-weight: 400; + line-height: 24px; + text-align: center; + color: $grey-800; + } + .info-label { + display: block; + font-family: $font-secondary; + font-size: 14px; + font-weight: 400; + line-height: 20px; + text-align: left; + color: $secondary; + } + .info-value { + display: block; + color: $grey-800; + font-family: $font-secondary; + font-size: 16px; + font-weight: 400; + line-height: 24px; + text-align: left; + } +} + +.device-qualification-form-summary-info-title-error { + display: flex; + align-items: center; + height: 48px; + margin: auto; + justify-content: center; + color: $red-800; + gap: 14px; + ::ng-deep mat-icon { + color: $red-800; + } +} + +.device-qualification-form-instructions { + margin-top: auto; + padding-top: 8px; + color: $grey-800; + text-align: center; + font-family: $font-secondary; + font-size: 16px; + width: 510px; + ul { + margin-bottom: 0; + text-align: left; + padding-left: 26px; + } + li { + line-height: 24px; + } +} + +::ng-deep mat-error { + background: $white; +} + +:host mat-form-field { + &::ng-deep.mat-mdc-form-field-error-wrapper { + margin-top: -20px; + position: static; + } +} + +.device-qualification-form-test-modules-container-error + ::ng-deep + .device-tests-title { + color: $red-800; +} + +.device-qualification-form-test-modules-error { + padding: 0 24px; +} + +.form-content-summary { + display: grid; + grid-template-rows: 1fr auto; + grid-row-gap: 16px; + overflow: hidden; +} + +@container qualification-form (height < 870px) { + .device-qualification-form-page { + overflow: scroll; + ::ng-deep app-device-tests { + overflow: visible; + } + } +} + +@container qualification-form (height < 580px) { + .device-qualification-form-page { + overflow: scroll; + .device-qualification-form-step-content, + .device-qualification-form-summary-container { + overflow: visible; + } + } +} + +@container qualification-form (width < 360px) { + .manufacturer-field ::ng-deep mat-hint { + white-space: nowrap; + } + ::ng-deep .device-form-test-modules { + display: block; + } +} diff --git a/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.spec.ts b/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.spec.ts new file mode 100644 index 000000000..bcc34b35d --- /dev/null +++ b/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.spec.ts @@ -0,0 +1,884 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + ComponentFixture, + discardPeriodicTasks, + fakeAsync, + flush, + TestBed, + tick, +} from '@angular/core/testing'; + +import { DeviceQualificationFromComponent } from './device-qualification-from.component'; +import { + MAT_DIALOG_DATA, + MatDialogModule, + MatDialogRef, +} from '@angular/material/dialog'; +import { of } from 'rxjs'; +import { NgxMaskDirective, NgxMaskPipe, provideNgxMask } from 'ngx-mask'; +import { MatButtonModule } from '@angular/material/button'; +import { FormArray, ReactiveFormsModule } from '@angular/forms'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatInputModule } from '@angular/material/input'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { DeviceTestsComponent } from '../../../../components/device-tests/device-tests.component'; +import { SpinnerComponent } from '../../../../components/spinner/spinner.component'; +import { + device, + DEVICES_FORM, + MOCK_TEST_MODULES, +} from '../../../../mocks/device.mock'; +import { MatIconTestingModule } from '@angular/material/icon/testing'; +import { TestRunService } from '../../../../services/test-run.service'; +import { DevicesStore } from '../../devices.store'; +import { provideMockStore } from '@ngrx/store/testing'; +import { FormAction } from '../../devices.component'; +import { DeviceStatus, TestingType } from '../../../../model/device'; +import { Component, Input } from '@angular/core'; +import { QuestionFormat } from '../../../../model/question'; +import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; +import { selectDevices } from '../../../../store/selectors'; + +describe('DeviceQualificationFromComponent', () => { + let component: DeviceQualificationFromComponent; + let fixture: ComponentFixture; + let compiled: HTMLElement; + const testrunServiceMock: jasmine.SpyObj = + jasmine.createSpyObj('testrunServiceMock', [ + 'fetchQuestionnaireFormat', + 'saveDevice', + ]); + const keyboardEvent = new BehaviorSubject( + new KeyboardEvent('keydown', { code: '' }) + ); + + const MOCK_DEVICE = { + status: DeviceStatus.VALID, + manufacturer: '', + model: '', + mac_addr: '', + test_pack: TestingType.Qualification, + type: '', + technology: '', + test_modules: { + udmi: { + enabled: true, + }, + connection: { + enabled: true, + }, + }, + additional_info: [ + { question: 'What type of device is this?', answer: '' }, + { + question: 'Does your device process any sensitive information? ', + answer: '', + }, + { + question: 'Please select the technology this device falls into', + answer: '', + }, + ], + }; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [FakeDynamicFormComponent], + imports: [ + DeviceQualificationFromComponent, + MatButtonModule, + ReactiveFormsModule, + MatCheckboxModule, + MatInputModule, + MatDialogModule, + BrowserAnimationsModule, + DeviceTestsComponent, + SpinnerComponent, + NgxMaskDirective, + NgxMaskPipe, + MatIconTestingModule, + ], + providers: [ + DevicesStore, + { + provide: MatDialogRef, + useValue: { + keydownEvents: () => keyboardEvent.asObservable(), + close: () => ({}), + }, + }, + { provide: MAT_DIALOG_DATA, useValue: {} }, + { provide: TestRunService, useValue: testrunServiceMock }, + provideNgxMask(), + provideMockStore({ + selectors: [{ selector: selectDevices, value: [device, device] }], + }), + ], + }).compileComponents(); + + fixture = TestBed.createComponent(DeviceQualificationFromComponent); + component = fixture.componentInstance; + compiled = fixture.nativeElement as HTMLElement; + + component.data = { + testModules: MOCK_TEST_MODULES, + devices: [], + index: 0, + isCreate: true, + }; + testrunServiceMock.fetchQuestionnaireFormat.and.returnValue( + of(DEVICES_FORM) + ); + + testrunServiceMock.saveDevice.and.returnValue(of(true)); + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + it('should contain device form', () => { + fixture.detectChanges(); + const form = compiled.querySelector('.device-qualification-form'); + + expect(form).toBeTruthy(); + }); + + it('should fetch devices format', () => { + fixture.detectChanges(); + const getQuestionnaireFormatSpy = spyOn( + component.devicesStore, + 'getQuestionnaireFormat' + ); + component.ngOnInit(); + fixture.detectChanges(); + + expect(getQuestionnaireFormatSpy).toHaveBeenCalled(); + }); + + it('should close dialog on "cancel" click with do data if form has no changes', () => { + fixture.detectChanges(); + const closeSpy = spyOn(component.dialogRef, 'close'); + const closeButton = compiled.querySelector( + '.device-qualification-form-header-close-button' + ) as HTMLButtonElement; + + closeButton?.click(); + + expect(closeSpy).toHaveBeenCalledWith(); + + closeSpy.calls.reset(); + }); + + it('should close dialog on escape', fakeAsync(() => { + const closeSpy = spyOn(component.dialogRef, 'close'); + fixture.detectChanges(); + + keyboardEvent.next(new KeyboardEvent('keydown', { code: 'Escape' })); + + tick(); + + expect(closeSpy).toHaveBeenCalledWith(); + + closeSpy.calls.reset(); + })); + + it('should close dialog on submit with "Save" action', fakeAsync(() => { + component.device = MOCK_DEVICE; + const closeSpy = spyOn(component.dialogRef, 'close'); + fixture.detectChanges(); + + component.submit(); + tick(); + flush(); + + expect(closeSpy).toHaveBeenCalledWith({ + action: 'Save', + device: MOCK_DEVICE, + }); + + closeSpy.calls.reset(); + })); + + it('should close dialog on delete with "Delete" action', fakeAsync(() => { + const closeSpy = spyOn(component.dialogRef, 'close'); + fixture.detectChanges(); + + component.delete(); + tick(); + + expect(closeSpy).toHaveBeenCalledWith({ + action: 'Delete', + device: MOCK_DEVICE, + index: 0, + }); + + closeSpy.calls.reset(); + })); + + describe('#deviceHasNoChanges', () => { + const deviceProps = [ + { manufacturer: 'test' }, + { model: 'test' }, + { mac_addr: 'test' }, + { test_pack: TestingType.Pilot }, + { type: 'test' }, + { technology: 'test' }, + { + test_modules: { + udmi: { + enabled: false, + }, + }, + }, + { additional_info: undefined }, + { + additional_info: [ + { question: 'What type of device is this?', answer: 'test' }, + ], + }, + ]; + it('should return true if devices the same', () => { + const result = component.deviceHasNoChanges(MOCK_DEVICE, MOCK_DEVICE); + + expect(result).toBeTrue(); + }); + + deviceProps.forEach(item => { + it(`should return false if devices have different props`, () => { + const MOCK_DEVICE_1 = { ...MOCK_DEVICE, ...item }; + const result = component.deviceHasNoChanges(MOCK_DEVICE_1, MOCK_DEVICE); + + expect(result).toBeFalse(); + }); + }); + }); + + it('should trigger onResize method when window is resized ', () => { + fixture.detectChanges(); + const spyOnResize = spyOn(component, 'onResize'); + window.dispatchEvent(new Event('resize')); + fixture.detectChanges(); + expect(spyOnResize).toHaveBeenCalled(); + }); + + it('#goToStep should set selected index', () => { + fixture.detectChanges(); + component.goToStep(0); + + expect(component.stepper.selectedIndex).toBe(0); + }); + + it('should close dialog on "cancel" click', () => { + fixture.detectChanges(); + component.manufacturer.setValue('test'); + ( + component.deviceQualificationForm.get('steps') as FormArray + ).controls.forEach(control => control.markAsDirty()); + fixture.detectChanges(); + const closeSpy = spyOn(component.dialogRef, 'close'); + const closeButton = compiled.querySelector( + '.device-qualification-form-header-close-button' + ) as HTMLButtonElement; + + closeButton?.click(); + + expect(closeSpy).toHaveBeenCalledWith({ + action: FormAction.Close, + index: 0, + device: { + status: DeviceStatus.VALID, + manufacturer: 'test', + model: '', + mac_addr: '', + test_pack: 'Device Qualification', + type: '', + technology: '', + test_modules: { + udmi: { + enabled: true, + }, + connection: { + enabled: true, + }, + }, + additional_info: [ + { question: 'What type of device is this?', answer: '' }, + { + question: 'Does your device process any sensitive information? ', + answer: '', + }, + { + question: 'Please select the technology this device falls into', + answer: '', + }, + ], + }, + }); + + closeSpy.calls.reset(); + }); + + describe('test modules', () => { + beforeEach(() => { + fixture.detectChanges(); + }); + + it('should be present', () => { + const test = compiled.querySelectorAll('mat-checkbox'); + + expect(test.length).toEqual(2); + }); + + it('should be enabled', () => { + const tests = compiled.querySelectorAll('.device-form-test-modules p'); + + expect(tests[0].classList.contains('disabled')).toEqual(false); + }); + + it('should have error when no modules selected', () => { + component.test_modules.setValue([false, false]); + component.test_modules.markAsTouched(); + fixture.detectChanges(); + const modules = compiled.querySelector( + '.device-qualification-form-test-modules-container-error' + ); + const error = compiled.querySelector( + '.device-qualification-form-test-modules-error' + ); + + expect(modules).toBeTruthy(); + expect(error?.innerHTML.trim()).toEqual( + 'At least one test has to be selected to save a Device.' + ); + }); + }); + + describe('device model', () => { + beforeEach(() => { + fixture.detectChanges(); + }); + + it('should not contain errors when input is correct', () => { + const model: HTMLInputElement = compiled.querySelector( + '.device-qualification-form-model' + ) as HTMLInputElement; + ['model', 'Gebäude', 'jardín'].forEach(value => { + model.value = value; + model.dispatchEvent(new Event('input')); + + const errors = component.model.errors; + const uiValue = model.value; + const formValue = component.model.value; + + expect(uiValue).toEqual(formValue); + expect(errors).toBeNull(); + }); + }); + + it('should have "invalid_format" error when field does not satisfy validation rules', () => { + [ + 'very long value very long value very long value very long value very long value very long value very long value', + 'as&@3$', + ].forEach(value => { + const model: HTMLInputElement = compiled.querySelector( + '.device-qualification-form-model' + ) as HTMLInputElement; + model.value = value; + model.dispatchEvent(new Event('input')); + component.model.markAsTouched(); + fixture.detectChanges(); + + const modelError = compiled.querySelector('mat-error')?.innerHTML; + const error = component.model.hasError('invalid_format'); + + expect(error).toBeTruthy(); + expect(modelError).toContain( + 'The device model name must be a maximum of 28 characters. Only letters, numbers, and accented letters are permitted.' + ); + }); + }); + }); + + describe('device manufacturer', () => { + beforeEach(() => { + fixture.detectChanges(); + }); + + it('should not contain errors when input is correct', () => { + const manufacturer: HTMLInputElement = compiled.querySelector( + '.device-qualification-form-manufacturer' + ) as HTMLInputElement; + ['manufacturer', 'Gebäude', 'jardín'].forEach(value => { + manufacturer.value = value; + manufacturer.dispatchEvent(new Event('input')); + + const errors = component.manufacturer.errors; + const uiValue = manufacturer.value; + const formValue = component.manufacturer.value; + + expect(uiValue).toEqual(formValue); + expect(errors).toBeNull(); + }); + }); + + it('should have "invalid_format" error when field does not satisfy validation', () => { + [ + 'very long value very long value very long value very long value very long value very long value very long value', + 'as&@3$', + ].forEach(value => { + const manufacturer: HTMLInputElement = compiled.querySelector( + '.device-qualification-form-manufacturer' + ) as HTMLInputElement; + manufacturer.value = value; + manufacturer.dispatchEvent(new Event('input')); + component.manufacturer.markAsTouched(); + fixture.detectChanges(); + + const manufacturerError = + compiled.querySelector('mat-error')?.innerHTML; + const error = component.manufacturer.hasError('invalid_format'); + + expect(error).toBeTruthy(); + expect(manufacturerError).toContain( + 'The manufacturer name must be a maximum of 28 characters. Only letters, numbers, and accented letters are permitted.' + ); + }); + }); + }); + + describe('mac address', () => { + beforeEach(() => { + fixture.detectChanges(); + }); + + it('should not be disabled', () => { + expect(component.mac_addr.disabled).toBeFalse(); + }); + + it('should not contain errors when input is correct', () => { + const macAddress: HTMLInputElement = compiled.querySelector( + '.device-qualification-form-mac-address' + ) as HTMLInputElement; + ['07:07:07:07:07:07', ' 07:07:07:07:07:07 '].forEach(value => { + macAddress.value = value; + macAddress.dispatchEvent(new Event('input')); + + const errors = component.mac_addr.errors; + const formValue = component.mac_addr.value; + + expect(macAddress.value).toEqual(formValue); + expect(errors).toBeNull(); + }); + }); + + it('should have "pattern" error when field does not satisfy pattern', () => { + ['value', 'q01e423573c4'].forEach(value => { + const macAddress: HTMLInputElement = compiled.querySelector( + '.device-qualification-form-mac-address' + ) as HTMLInputElement; + macAddress.value = value; + macAddress.dispatchEvent(new Event('input')); + component.mac_addr.markAsTouched(); + fixture.detectChanges(); + + const macAddressError = compiled.querySelector('mat-error')?.innerHTML; + const error = component.mac_addr.hasError('pattern'); + + expect(error).toBeTruthy(); + expect(macAddressError).toContain( + 'Please, check. A MAC address consists of 12 hexadecimal digits (0 to 9, a to f, or A to F).' + ); + }); + }); + + it('should have "has_same_mac_address" error when MAC address is already used', () => { + component.data = { + testModules: MOCK_TEST_MODULES, + devices: [device], + index: 0, + isCreate: true, + }; + component.ngOnInit(); + fixture.detectChanges(); + + const macAddress: HTMLInputElement = compiled.querySelector( + '.device-qualification-form-mac-address' + ) as HTMLInputElement; + macAddress.value = '00:1e:42:35:73:c4'; + macAddress.dispatchEvent(new Event('input')); + component.mac_addr.markAsTouched(); + fixture.detectChanges(); + + const macAddressError = compiled.querySelector('mat-error')?.innerHTML; + const error = component.mac_addr.hasError('has_same_mac_address'); + + expect(error).toBeTruthy(); + expect(macAddressError).toContain( + 'This MAC address is already used for another device in the repository.' + ); + }); + }); + + describe('when device is present', () => { + beforeEach(() => { + component.data = { + devices: [device], + testModules: MOCK_TEST_MODULES, + device: { + status: DeviceStatus.VALID, + manufacturer: 'Delta', + model: 'O3-DIN-CPU', + mac_addr: '00:1e:42:35:73:c4', + test_modules: { + udmi: { + enabled: true, + }, + }, + }, + isCreate: false, + index: 0, + }; + }); + + it('should fill form values with device values', fakeAsync(() => { + fixture.detectChanges(); + + testrunServiceMock.fetchQuestionnaireFormat.and.returnValue( + of(DEVICES_FORM) + ); + + tick(1); + + const model: HTMLInputElement = compiled.querySelector( + '.device-qualification-form-model' + ) as HTMLInputElement; + const manufacturer: HTMLInputElement = compiled.querySelector( + '.device-qualification-form-manufacturer' + ) as HTMLInputElement; + + expect(model.value).toEqual('O3-DIN-CPU'); + expect(manufacturer.value).toEqual('Delta'); + + discardPeriodicTasks(); + })); + }); + + describe('steps', () => { + beforeEach(() => { + fixture.detectChanges(); + }); + + describe('with questionnaire', () => { + it('should have steps', () => { + expect( + (component.deviceQualificationForm.get('steps') as FormArray).controls + .length + ).toEqual(3); + }); + }); + + it('should not save data when fields are empty', () => { + const forwardButton = compiled.querySelector( + '.form-button-forward' + ) as HTMLButtonElement; + const model: HTMLInputElement = compiled.querySelector( + '.device-qualification-form-model' + ) as HTMLInputElement; + const manufacturer: HTMLInputElement = compiled.querySelector( + '.device-qualification-form-manufacturer' + ) as HTMLInputElement; + const macAddress: HTMLInputElement = compiled.querySelector( + '.device-qualification-form-mac-address' + ) as HTMLInputElement; + + ['', ' '].forEach(value => { + model.value = value; + model.dispatchEvent(new Event('input')); + manufacturer.value = value; + manufacturer.dispatchEvent(new Event('input')); + macAddress.value = value; + macAddress.dispatchEvent(new Event('input')); + forwardButton?.click(); + fixture.detectChanges(); + + const requiredErrors = compiled.querySelectorAll('mat-error'); + expect(requiredErrors?.length).toEqual(3); + + requiredErrors.forEach(error => { + expect(error?.innerHTML).toContain('required'); + }); + }); + }); + + describe('happy flow', () => { + beforeEach(() => { + component.model.setValue('model'); + component.manufacturer.setValue('manufacturer'); + component.mac_addr.setValue('07:07:07:07:07:07'); + component.test_modules.setValue([true, true]); + }); + + it('should save device when step is changed', () => { + const forwardButton = compiled.querySelector( + '.form-button-forward' + ) as HTMLButtonElement; + forwardButton.click(); + + expect(component.device).toEqual({ + status: DeviceStatus.VALID, + manufacturer: 'manufacturer', + model: 'model', + mac_addr: '07:07:07:07:07:07', + test_pack: TestingType.Qualification, + type: '', + technology: '', + test_modules: { + udmi: { + enabled: true, + }, + connection: { + enabled: true, + }, + }, + additional_info: [ + { question: 'What type of device is this?', answer: '' }, + { + question: 'Does your device process any sensitive information? ', + answer: '', + }, + { + question: 'Please select the technology this device falls into', + answer: '', + }, + ], + }); + }); + + describe('summary', () => { + beforeEach(() => { + const forwardButton = compiled.querySelector( + '.form-button-forward' + ) as HTMLButtonElement; + forwardButton.click(); // will redirect to 2 step + fixture.detectChanges(); + + const nextForwardButton = compiled.querySelector( + '.form-button-forward' + ) as HTMLButtonElement; + nextForwardButton.click(); //will redirect to summary + + fixture.detectChanges(); + }); + + it('should have device item', () => { + const item = compiled.querySelector('app-device-item'); + expect(item).toBeTruthy(); + }); + + it('should have instructions', () => { + const instructions = compiled.querySelector( + '.device-qualification-form-instructions' + ); + expect(instructions).toBeTruthy(); + }); + + it('should not have instructions when device is editing', () => { + component.data = { + devices: [device], + testModules: MOCK_TEST_MODULES, + device: { + status: DeviceStatus.VALID, + manufacturer: 'Delta', + model: 'O3-DIN-CPU', + mac_addr: '00:1e:42:35:73:c4', + test_modules: { + udmi: { + enabled: true, + }, + }, + }, + isCreate: false, + index: 0, + }; + fixture.detectChanges(); + + const instructions = compiled.querySelector( + '.device-qualification-form-instructions' + ); + expect(instructions).toBeNull(); + }); + + it('should save device', () => { + const saveSpy = spyOn(component.devicesStore, 'saveDevice'); + + component.submit(); + + const args = saveSpy.calls.argsFor(0); + // @ts-expect-error config is in object + expect(args[0].device).toEqual({ + status: DeviceStatus.VALID, + manufacturer: 'manufacturer', + model: 'model', + mac_addr: '07:07:07:07:07:07', + test_pack: 'Device Qualification', + type: '', + technology: '', + test_modules: { + connection: { + enabled: true, + }, + udmi: { + enabled: true, + }, + }, + additional_info: [ + { question: 'What type of device is this?', answer: '' }, + { + question: + 'Does your device process any sensitive information? ', + answer: '', + }, + { + question: 'Please select the technology this device falls into', + answer: '', + }, + ], + }); + expect(saveSpy).toHaveBeenCalled(); + }); + + it('should edit device', () => { + component.data = { + devices: [device], + testModules: MOCK_TEST_MODULES, + device: { + status: DeviceStatus.VALID, + manufacturer: 'Delta', + model: 'O3-DIN-CPU', + mac_addr: '00:1e:42:35:73:c4', + test_modules: { + udmi: { + enabled: true, + }, + }, + }, + isCreate: false, + index: 0, + }; + fixture.detectChanges(); + const editSpy = spyOn(component.devicesStore, 'editDevice'); + + component.submit(); + + const args = editSpy.calls.argsFor(0); + // @ts-expect-error config is in object + expect(args[0].device).toEqual({ + status: DeviceStatus.VALID, + manufacturer: 'manufacturer', + model: 'model', + mac_addr: '07:07:07:07:07:07', + test_pack: 'Device Qualification', + type: '', + technology: '', + test_modules: { + connection: { + enabled: true, + }, + udmi: { + enabled: true, + }, + }, + additional_info: [ + { question: 'What type of device is this?', answer: '' }, + { + question: + 'Does your device process any sensitive information? ', + answer: '', + }, + { + question: 'Please select the technology this device falls into', + answer: '', + }, + ], + }); + expect(editSpy).toHaveBeenCalled(); + }); + }); + }); + + describe('with errors', () => { + beforeEach(() => { + component.data = { + devices: [device], + testModules: MOCK_TEST_MODULES, + device: { + status: DeviceStatus.VALID, + manufacturer: 'Delta', + model: 'O3-DIN-CPU', + mac_addr: '00:1e:42:35:73:c4', + test_modules: { + udmi: { + enabled: true, + }, + }, + }, + isCreate: false, + index: 0, + }; + component.model.setValue(''); + + fixture.detectChanges(); + }); + + describe('summary', () => { + beforeEach(() => { + const forwardButton = compiled.querySelector( + '.form-button-forward' + ) as HTMLButtonElement; + forwardButton.click(); // will redirect to 2 step + fixture.detectChanges(); + + const nextForwardButton = compiled.querySelector( + '.form-button-forward' + ) as HTMLButtonElement; + nextForwardButton.click(); //will redirect to summary + fixture.detectChanges(); + }); + + it('should have error message', () => { + const error = compiled.querySelector( + '.device-qualification-form-summary-info-description' + ); + expect(error?.textContent?.trim()).toEqual( + 'Please go back and correct the errors on Step 1.' + ); + }); + }); + }); + }); +}); + +@Component({ + selector: 'app-dynamic-form', + template: '
', +}) +class FakeDynamicFormComponent { + @Input() format: QuestionFormat[] = []; + @Input() optionKey: string | undefined; +} diff --git a/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.ts b/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.ts new file mode 100644 index 000000000..09edf8cdc --- /dev/null +++ b/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.ts @@ -0,0 +1,574 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + AfterViewInit, + Component, + ElementRef, + HostListener, + Inject, + OnDestroy, + OnInit, + ViewChild, +} from '@angular/core'; +import { + AbstractControl, + FormArray, + FormBuilder, + FormGroup, + ReactiveFormsModule, + Validators, +} from '@angular/forms'; +import { DeviceValidators } from '../device-form/device.validators'; +import { + Device, + DeviceQuestionnaireSection, + DeviceStatus, + DeviceView, + TestingType, + TestModule, +} from '../../../../model/device'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { CommonModule } from '@angular/common'; +import { CdkStep, StepperSelectionEvent } from '@angular/cdk/stepper'; +import { StepperComponent } from '../../../../components/stepper/stepper.component'; +import { + MatError, + MatFormField, + MatFormFieldModule, +} from '@angular/material/form-field'; +import { DeviceTestsComponent } from '../../../../components/device-tests/device-tests.component'; +import { MatButtonModule } from '@angular/material/button'; +import { MatInputModule } from '@angular/material/input'; +import { MatSelectModule } from '@angular/material/select'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { TextFieldModule } from '@angular/cdk/text-field'; +import { NgxMaskDirective, NgxMaskPipe, provideNgxMask } from 'ngx-mask'; +import { MatIcon } from '@angular/material/icon'; +import { MatRadioButton, MatRadioGroup } from '@angular/material/radio'; +import { ProfileValidators } from '../../../risk-assessment/profile-form/profile.validators'; +import { DevicesStore } from '../../devices.store'; +import { DynamicFormComponent } from '../../../../components/dynamic-form/dynamic-form.component'; +import { filter, skip, Subject, takeUntil, timer } from 'rxjs'; +import { FormAction, FormResponse } from '../../devices.component'; +import { DeviceItemComponent } from '../../../../components/device-item/device-item.component'; +import { ProgramTypeIconComponent } from '../../../../components/program-type-icon/program-type-icon.component'; +import { Question } from '../../../../model/profile'; +import { FormControlType } from '../../../../model/question'; +import { ProgramType } from '../../../../model/program-type'; +import { FocusManagerService } from '../../../../services/focus-manager.service'; + +const MAC_ADDRESS_PATTERN = + '^[\\s]*[a-fA-F0-9]{2}(?:[:][a-fA-F0-9]{2}){5}[\\s]*$'; + +interface DialogData { + title?: string; + device?: Device; + initialDevice?: Device; + devices: Device[]; + testModules: TestModule[]; + index: number; + isCreate: boolean; +} + +@Component({ + selector: 'app-device-qualification-from', + standalone: true, + imports: [ + CdkStep, + StepperComponent, + MatFormField, + DeviceTestsComponent, + MatButtonModule, + CommonModule, + ReactiveFormsModule, + MatInputModule, + MatError, + MatFormFieldModule, + MatSelectModule, + MatCheckboxModule, + TextFieldModule, + NgxMaskDirective, + NgxMaskPipe, + MatIcon, + MatRadioGroup, + MatRadioButton, + DynamicFormComponent, + DeviceItemComponent, + ProgramTypeIconComponent, + ], + providers: [provideNgxMask(), DevicesStore], + templateUrl: './device-qualification-from.component.html', + styleUrl: './device-qualification-from.component.scss', +}) +export class DeviceQualificationFromComponent + implements OnInit, AfterViewInit, OnDestroy +{ + readonly FORM_HEIGHT = 993; + readonly TestingType = TestingType; + readonly DeviceView = DeviceView; + readonly ProgramType = ProgramType; + @ViewChild('stepper') public stepper!: StepperComponent; + testModules: TestModule[] = []; + deviceQualificationForm: FormGroup = this.fb.group({}); + device: Device | undefined; + format: DeviceQuestionnaireSection[] = []; + selectedIndex: number = 0; + typeStep = 1; + typeQuestion = 0; + technologyStep = 1; + technologyQuestion = 2; + + private destroy$: Subject = new Subject(); + + get model() { + return this.getStep(0).get('model') as AbstractControl; + } + + get manufacturer() { + return this.getStep(0).get('manufacturer') as AbstractControl; + } + + get mac_addr() { + return this.getStep(0).get('mac_addr') as AbstractControl; + } + + get test_pack() { + return this.getStep(0).get('test_pack') as AbstractControl; + } + + get type() { + return this.getStep(this.typeStep)?.get( + this.typeQuestion.toString() + ) as AbstractControl; + } + + get technology() { + return this.getStep(this.technologyStep)?.get( + this.technologyQuestion.toString() + ) as AbstractControl; + } + + get test_modules() { + return this.getStep(0).controls['test_modules'] as FormArray; + } + + get formValid() { + return ( + this.deviceQualificationForm.get('steps') as FormArray + ).controls.every(control => (control as FormGroup).valid); + } + + deviceHasNoChanges(device1: Device | undefined, device2: Device | undefined) { + return device1 && device2 && this.compareDevices(device1, device2); + } + + @HostListener('window:resize', ['$event']) + onResize() { + this.setDialogHeight(); + } + + constructor( + private fb: FormBuilder, + private deviceValidators: DeviceValidators, + private profileValidators: ProfileValidators, + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: DialogData, + public devicesStore: DevicesStore, + private element: ElementRef, + private focusService: FocusManagerService + ) { + this.device = data.device; + } + + ngOnInit(): void { + this.createBasicStep(); + this.testModules = this.data.testModules; + + this.devicesStore.questionnaireFormat$.pipe(skip(1)).subscribe(format => { + this.createDeviceForm(format); + this.format = format; + + format.forEach(step => { + step.questions.forEach((question, index) => { + // need to define the step and index of type and technology + if (question.question.toLowerCase().includes('type')) { + this.typeStep = step.step; + this.typeQuestion = index; + } else if (question.question.toLowerCase().includes('technology')) { + this.technologyStep = step.step; + this.technologyQuestion = index; + } + }); + }); + + timer(0) + .pipe(takeUntil(this.destroy$)) + .subscribe(() => { + if (this.data.device) { + this.fillDeviceForm(this.format, this.data.device!); + } + if (this.data.index) { + // previous steps should be marked as interacted + for (let i = 0; i <= this.data.index; i++) { + this.goToStep(i); + } + } + this.dialogRef + .keydownEvents() + .pipe(filter((e: KeyboardEvent) => e.code === 'Escape')) + .subscribe(() => { + this.closeForm(); + }); + }); + }); + + this.devicesStore.getQuestionnaireFormat(); + } + + ngAfterViewInit() { + //set static height for better UX + this.element.nativeElement.style.height = + this.element.nativeElement.offsetHeight + 'px'; + } + + ngOnDestroy() { + this.destroy$.next(true); + this.destroy$.unsubscribe(); + } + + submit(): void { + this.updateDevice(this.device!, () => { + this.dialogRef.close({ + action: FormAction.Save, + device: this.device, + } as FormResponse); + }); + } + + delete(): void { + this.dialogRef.close({ + action: FormAction.Delete, + device: this.createDeviceFromForm(), + index: this.stepper.selectedIndex, + } as FormResponse); + } + + closeForm(): void { + const device1 = this.data.initialDevice; + const device2 = this.createDeviceFromForm(); + if ( + (device1 && device2 && this.compareDevices(device1, device2)) || + (!device1 && this.deviceIsEmpty(device2)) + ) { + this.dialogRef.close(); + } else { + this.dialogRef.close({ + action: FormAction.Close, + device: this.createDeviceFromForm(), + index: this.stepper.selectedIndex, + } as FormResponse); + } + } + + getStep(step: number) { + return (this.deviceQualificationForm.get('steps') as FormArray).controls[ + step + ] as FormGroup; + } + + onStepChange(event: StepperSelectionEvent) { + this.focusService.focusFirstElementInContainer(); + if (event.previouslySelectedStep.completed) { + this.device = this.createDeviceFromForm(); + } + } + + getErrorSteps(): number[] { + const steps: number[] = []; + (this.deviceQualificationForm.get('steps') as FormArray).controls.forEach( + (control, index) => { + if (!control.valid) steps.push(index); + } + ); + return steps; + } + + goToStep(index: number, event?: Event) { + event?.preventDefault(); + this.stepper.selectedIndex = index; + } + + private fillDeviceForm( + format: DeviceQuestionnaireSection[], + device: Device + ): void { + format.forEach(step => { + step.questions.forEach((question, index) => { + const answer = device.additional_info?.find( + answers => answers.question === question.question + )?.answer; + if (answer !== undefined && answer !== null && answer !== '') { + if (question.type === FormControlType.SELECT_MULTIPLE) { + question.options?.forEach((item, idx) => { + if ((answer as number[])?.includes(idx)) { + ( + this.getStep(step.step).get(index.toString()) as FormGroup + ).controls[idx].setValue(true); + } else { + ( + this.getStep(step.step).get(index.toString()) as FormGroup + ).controls[idx].setValue(false); + } + }); + } else { + ( + this.getStep(step.step).get(index.toString()) as AbstractControl + ).setValue(answer || ''); + } + } else { + ( + this.getStep(step.step)?.get(index.toString()) as AbstractControl + )?.markAsTouched(); + } + }); + }); + this.model.setValue(device.model); + this.manufacturer.setValue(device.manufacturer); + this.mac_addr.setValue(device.mac_addr); + + if ( + device.test_pack && + (device.test_pack === TestingType.Qualification || + device.test_pack === TestingType.Pilot) + ) { + this.test_pack.setValue(device.test_pack); + } else { + this.test_pack.setValue(TestingType.Qualification); + } + + this.type?.setValue(device.type); + this.technology?.setValue(device.technology); + } + + private updateDevice(device: Device, callback: () => void) { + if (!this.data.isCreate && this.data.device) { + this.devicesStore.editDevice({ + device, + mac_addr: this.data.device.mac_addr, + onSuccess: callback, + }); + } else { + this.devicesStore.saveDevice({ device, onSuccess: callback }); + } + } + + private createDeviceFromForm(): Device { + const testModules: { [key: string]: { enabled: boolean } } = {}; + this.getStep(0).value.test_modules.forEach( + (enabled: boolean, i: number) => { + testModules[this.testModules[i]?.name] = { + enabled: enabled, + }; + } + ); + + const additionalInfo: Question[] = []; + + this.format.forEach(step => { + step.questions.forEach((question, index) => { + const response: Question = {}; + response.question = question.question; + + if (question.type === FormControlType.SELECT_MULTIPLE) { + const answer: number[] = []; + question.options?.forEach((_, idx) => { + const value = this.getStep(step.step).value[index][idx]; + if (value) { + answer.push(idx); + } + }); + response.answer = answer; + } else { + response.answer = this.getStep(step.step).value[index]?.trim(); + } + additionalInfo.push(response); + }); + }); + + return { + status: DeviceStatus.VALID, + model: this.model.value.trim(), + manufacturer: this.manufacturer.value.trim(), + mac_addr: this.mac_addr.value.trim(), + test_pack: this.test_pack.value, + test_modules: testModules, + type: this.type.value, + technology: this.technology.value, + additional_info: additionalInfo, + } as Device; + } + + private createBasicStep() { + const firstStep = this.fb.group({ + model: [ + '', + [ + this.profileValidators.textRequired(), + this.deviceValidators.deviceStringFormat(), + ], + ], + manufacturer: [ + '', + [ + this.profileValidators.textRequired(), + this.deviceValidators.deviceStringFormat(), + ], + ], + mac_addr: [ + '', + [ + this.profileValidators.textRequired(), + Validators.pattern(MAC_ADDRESS_PATTERN), + this.deviceValidators.differentMACAddress( + this.data.devices, + this.data.device + ), + ], + ], + test_modules: new FormArray( + [], + this.deviceValidators.testModulesRequired() + ), + test_pack: [TestingType.Qualification], + }); + + this.deviceQualificationForm = this.fb.group({ + steps: this.fb.array([firstStep]), + }); + } + + private createDeviceForm(format: DeviceQuestionnaireSection[]) { + format.forEach(() => { + (this.deviceQualificationForm.get('steps') as FormArray).controls.push( + this.createStep() + ); + }); + + // summary step + (this.deviceQualificationForm.get('steps') as FormArray).controls.push( + this.fb.group({}) + ); + } + + private createStep() { + return new FormGroup({}); + } + + private compareDevices(device1: Device, device2: Device) { + if (device1.manufacturer !== device2.manufacturer) { + return false; + } + if (device1.model !== device2.model) { + return false; + } + if (device1.mac_addr !== device2.mac_addr) { + return false; + } + if (device1.type !== device2.type) { + return false; + } + if (device1.technology !== device2.technology) { + return false; + } + if (device1.test_pack !== device2.test_pack) { + return false; + } + const keys1 = Object.keys(device1.test_modules!); + + for (const key of keys1) { + const val1 = device1.test_modules![key]; + const val2 = device2.test_modules![key]; + if (val1?.enabled !== val2?.enabled) { + return false; + } + } + + if (device1.additional_info) { + for (const question of device1.additional_info) { + if ( + question.answer !== + device2.additional_info?.find( + question2 => question2.question === question.question + )?.answer + ) { + return false; + } + } + } else { + return false; + } + return true; + } + + private deviceIsEmpty(device: Device) { + if (device.manufacturer !== '') { + return false; + } + if (device.model !== '') { + return false; + } + if (device.mac_addr !== '') { + return false; + } + if (device.type !== '') { + return false; + } + if (device.technology !== '') { + return false; + } + if (device.test_pack !== TestingType.Qualification) { + return false; + } + const keys1 = Object.keys(device.test_modules!); + + for (const key of keys1) { + const val1 = device.test_modules![key]; + if (!val1.enabled) { + return false; + } + } + + if (device.additional_info) { + for (const question of device.additional_info) { + if (question.answer !== '') { + return false; + } + } + } else { + return false; + } + return true; + } + + private setDialogHeight(): void { + const windowHeight = window.innerHeight; + if (windowHeight < this.FORM_HEIGHT) { + this.element.nativeElement.style.height = '100vh'; + } else { + this.element.nativeElement.style.height = this.FORM_HEIGHT + 'px'; + } + } +} diff --git a/modules/ui/src/app/pages/devices/devices-routing.module.ts b/modules/ui/src/app/pages/devices/devices-routing.module.ts index 19acb07c9..53ed9ef0a 100644 --- a/modules/ui/src/app/pages/devices/devices-routing.module.ts +++ b/modules/ui/src/app/pages/devices/devices-routing.module.ts @@ -16,8 +16,15 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { DevicesComponent } from './devices.component'; +import { CanDeactivateGuard } from '../../guards/can-deactivate.guard'; -const routes: Routes = [{ path: '', component: DevicesComponent }]; +const routes: Routes = [ + { + path: '', + component: DevicesComponent, + canDeactivate: [CanDeactivateGuard], + }, +]; @NgModule({ imports: [RouterModule.forChild(routes)], diff --git a/modules/ui/src/app/pages/devices/devices.component.html b/modules/ui/src/app/pages/devices/devices.component.html index c9f5d3aee..f6567b49e 100644 --- a/modules/ui/src/app/pages/devices/devices.component.html +++ b/modules/ui/src/app/pages/devices/devices.component.html @@ -20,12 +20,15 @@

Devices

- + Devices
@@ -216,7 +230,9 @@

Reports

mat-row #rowElement [class.report-selected]="rowElement === vm.selectedRow" - (click)="selectRow(rowElement)"> + (click)="selectRow(rowElement)" + (keydown.enter)="selectRow(rowElement)" + (keydown.space)="selectRow(rowElement)"> @@ -231,7 +247,7 @@

Reports

context: { header: 'Sorry, there are no reports yet!', message: - 'Reports will automatically generate following a test attempt completion.' + 'Reports will automatically generate following a test attempt completion.', } "> diff --git a/modules/ui/src/app/pages/reports/reports.component.scss b/modules/ui/src/app/pages/reports/reports.component.scss index 47d76d880..bea1c4b00 100644 --- a/modules/ui/src/app/pages/reports/reports.component.scss +++ b/modules/ui/src/app/pages/reports/reports.component.scss @@ -94,7 +94,7 @@ } .filter-button.active .mat-icon { - color: mat.get-color-from-palette($color-primary, 600); + color: mat.m2-get-color-from-palette($color-primary, 600); } } @@ -124,10 +124,12 @@ display: flex; align-items: center; justify-content: center; - grid-row: 1/3; } .results-content-empty { + position: absolute; + top: 0; + width: 100%; height: 100%; } diff --git a/modules/ui/src/app/pages/reports/reports.component.spec.ts b/modules/ui/src/app/pages/reports/reports.component.spec.ts index 19a0a827d..4bcf2ebf3 100644 --- a/modules/ui/src/app/pages/reports/reports.component.spec.ts +++ b/modules/ui/src/app/pages/reports/reports.component.spec.ts @@ -13,9 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; -import { ReportsComponent } from './reportscomponent'; +import { ReportsComponent } from './reports.component'; import { TestRunService } from '../../services/test-run.service'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { ReportsModule } from './reports.module'; @@ -87,6 +92,7 @@ describe('ReportsComponent', () => { 'setFilterOpened', 'updateSort', 'getHistory', + 'getReports', ]); mockLiveAnnouncer = jasmine.createSpyObj(['announce']); @@ -112,12 +118,6 @@ describe('ReportsComponent', () => { }); describe('ngOnInit', () => { - it('should set dataSource data', () => { - component.ngOnInit(); - - expect(mockReportsStore.getHistory).toHaveBeenCalled(); - }); - it('should update sort', fakeAsync(() => { const sort = new MatSort(); component.sort = sort; @@ -125,6 +125,12 @@ describe('ReportsComponent', () => { expect(mockReportsStore.updateSort).toHaveBeenCalledWith(sort); })); + + it('should get reports', fakeAsync(() => { + component.ngOnInit(); + + expect(mockReportsStore.getReports).toHaveBeenCalled(); + })); }); it('#sortData should call update sort', () => { @@ -246,7 +252,7 @@ describe('ReportsComponent', () => { fixture.detectChanges(); }); - it('should focus next active element if exist', () => { + it('should focus next active element if exist', fakeAsync(() => { const row = window.document.querySelector('tbody tr') as HTMLElement; row.classList.add('report-selected'); const nextButton = window.document.querySelector( @@ -256,10 +262,12 @@ describe('ReportsComponent', () => { component.focusNextButton(); + tick(50); + expect(buttonFocusSpy).toHaveBeenCalled(); - }); + })); - it('should focus navigation button if next active element does not exist', () => { + it('should focus navigation button if next active element does not exist', fakeAsync(() => { const button = document.createElement('BUTTON'); button.classList.add('app-sidebar-button-reports'); document.querySelector('body')?.appendChild(button); @@ -267,15 +275,18 @@ describe('ReportsComponent', () => { component.focusNextButton(); + tick(50); + expect(buttonFocusSpy).toHaveBeenCalled(); - }); + })); }); it('#removeDevice should call delete report', () => { const data = HISTORY[0]; component.removeDevice(data); expect(mockReportsStore.deleteReport).toHaveBeenCalledWith({ - mac_addr: data.device.mac_addr, + mac_addr: data.mac_addr, + deviceMacAddr: data.device.mac_addr, started: data.started, }); }); @@ -321,6 +332,7 @@ describe('ReportsComponent', () => { green: false, red: true, blue: false, + cyan: false, grey: false, }); component.ngOnInit(); diff --git a/modules/ui/src/app/pages/reports/reportscomponent.ts b/modules/ui/src/app/pages/reports/reports.component.ts similarity index 95% rename from modules/ui/src/app/pages/reports/reportscomponent.ts rename to modules/ui/src/app/pages/reports/reports.component.ts index e390f0e06..f2cba227d 100644 --- a/modules/ui/src/app/pages/reports/reportscomponent.ts +++ b/modules/ui/src/app/pages/reports/reports.component.ts @@ -28,7 +28,7 @@ import { } from '../../model/testrun-status'; import { DatePipe } from '@angular/common'; import { MatSort, Sort } from '@angular/material/sort'; -import { Subject, takeUntil } from 'rxjs'; +import { Subject, takeUntil, timer } from 'rxjs'; import { MatRow } from '@angular/material/table'; import { FilterDialogComponent } from './components/filter-dialog/filter-dialog.component'; import { MatDialog } from '@angular/material/dialog'; @@ -56,7 +56,7 @@ export class ReportsComponent implements OnInit, OnDestroy { ) {} ngOnInit() { - this.store.getHistory(); + this.store.getReports(); this.store.updateSort(this.sort); } @@ -152,19 +152,24 @@ export class ReportsComponent implements OnInit, OnDestroy { '.report-selected + tr a' ) as HTMLButtonElement; if (next) { - next.focus(); + timer(50).subscribe(() => { + next.focus(); + }); } else { // If next interactive element doest not exist, add menu reports button should be focused const menuButton = window.document.querySelector( '.app-sidebar-button-reports' ) as HTMLButtonElement; - menuButton?.focus(); + timer(50).subscribe(() => { + menuButton?.focus(); + }); } } removeDevice(data: TestrunStatus) { this.store.deleteReport({ mac_addr: data.mac_addr, + deviceMacAddr: data.device.mac_addr, started: data.started, }); this.focusNextButton(); diff --git a/modules/ui/src/app/pages/reports/reports.module.ts b/modules/ui/src/app/pages/reports/reports.module.ts index f218780c5..18db8a27f 100644 --- a/modules/ui/src/app/pages/reports/reports.module.ts +++ b/modules/ui/src/app/pages/reports/reports.module.ts @@ -15,7 +15,7 @@ */ import { NgModule } from '@angular/core'; import { CommonModule, DatePipe } from '@angular/common'; -import { ReportsComponent } from './reportscomponent'; +import { ReportsComponent } from './reports.component'; import { ReportsRoutingModule } from './reports-routing.module'; import { MatTableModule } from '@angular/material/table'; import { MatIconModule } from '@angular/material/icon'; diff --git a/modules/ui/src/app/pages/reports/reports.store.spec.ts b/modules/ui/src/app/pages/reports/reports.store.spec.ts index e78b40036..e6b7ec2f3 100644 --- a/modules/ui/src/app/pages/reports/reports.store.spec.ts +++ b/modules/ui/src/app/pages/reports/reports.store.spec.ts @@ -18,9 +18,8 @@ import { TestRunService } from '../../services/test-run.service'; import SpyObj = jasmine.SpyObj; import { TestBed } from '@angular/core/testing'; import { skip, take } from 'rxjs'; -import { throwError } from 'rxjs/internal/observable/throwError'; import { of } from 'rxjs/internal/observable/of'; -import { DATA_SOURCE_INITIAL_VALUE, ReportsStore } from './reports.store'; +import { ReportsStore } from './reports.store'; import { EMPTY_FILTERS, FILTERS, @@ -29,31 +28,39 @@ import { HISTORY_AFTER_REMOVE, } from '../../mocks/reports.mock'; import { DatePipe } from '@angular/common'; -import { HttpErrorResponse } from '@angular/common/http'; import { MatRow } from '@angular/material/table'; import { MatSort } from '@angular/material/sort'; -import { provideMockStore } from '@ngrx/store/testing'; -import { selectRiskProfiles } from '../../store/selectors'; +import { MockStore, provideMockStore } from '@ngrx/store/testing'; +import { selectReports, selectRiskProfiles } from '../../store/selectors'; +import { AppState } from '../../store/state'; +import { setReports } from '../../store/actions'; describe('ReportsStore', () => { let reportsStore: ReportsStore; let mockService: SpyObj; + let store: MockStore; beforeEach(() => { - mockService = jasmine.createSpyObj(['getHistory', 'deleteReport']); + mockService = jasmine.createSpyObj(['deleteReport']); TestBed.configureTestingModule({ providers: [ ReportsStore, { provide: TestRunService, useValue: mockService }, provideMockStore({ - selectors: [{ selector: selectRiskProfiles, value: [] }], + selectors: [ + { selector: selectRiskProfiles, value: [] }, + { selector: selectReports, value: [] }, + ], }), DatePipe, ], }); reportsStore = TestBed.inject(ReportsStore); + store = TestBed.inject(MockStore); + + spyOn(store, 'dispatch').and.callFake(() => {}); }); it('should be created', () => { @@ -110,7 +117,7 @@ describe('ReportsStore', () => { }); it('should update dataSource', (done: DoneFn) => { - reportsStore.viewModel$.pipe(skip(2), take(1)).subscribe(store => { + reportsStore.viewModel$.pipe(skip(1), take(1)).subscribe(store => { expect(store.dataSource.data).toEqual(FORMATTED_HISTORY); expect(store.dataLoaded).toEqual(true); done(); @@ -129,11 +136,12 @@ describe('ReportsStore', () => { 'duration', 'deviceInfo', 'deviceFirmware', + 'program', 'status', 'report', ], chips: ['chips'], - dataSource: DATA_SOURCE_INITIAL_VALUE, + dataSource: store.dataSource, filterOpened: false, activeFilter: '', filteredValues: { @@ -142,7 +150,7 @@ describe('ReportsStore', () => { results: [], dateRange: '', }, - dataLoaded: false, + dataLoaded: true, selectedRow: null, isFiltersEmpty: true, profiles: [], @@ -154,56 +162,59 @@ describe('ReportsStore', () => { describe('effects', () => { describe('getHistory', () => { - describe('should update store', () => { - it('with empty value if error happens', done => { - mockService.getHistory.and.returnValue( - throwError( - new HttpErrorResponse({ error: { error: 'error' }, status: 500 }) - ) - ); - - reportsStore.viewModel$.pipe(skip(1), take(1)).subscribe(store => { - expect(store.dataSource.data).toEqual([]); - done(); - }); + it('should update store', done => { + store.overrideSelector(selectReports, [...HISTORY]); + store.refreshState(); - reportsStore.getHistory(); + reportsStore.viewModel$.pipe(take(1)).subscribe(store => { + expect(store.dataSource.data).toEqual(FORMATTED_HISTORY); + done(); }); - it('with value if not null', done => { - mockService.getHistory.and.returnValue(of([...HISTORY])); - - reportsStore.viewModel$.pipe(skip(1), take(1)).subscribe(store => { - expect(store.dataSource.data).toEqual(FORMATTED_HISTORY); - done(); - }); - - reportsStore.getHistory(); - }); + reportsStore.getHistory(); }); }); describe('deleteReport', () => { it('should update store', done => { mockService.deleteReport.and.returnValue(of(true)); - reportsStore.setHistory([...HISTORY]); + store.overrideSelector(selectReports, [...HISTORY]); + store.refreshState(); - reportsStore.viewModel$.pipe(skip(1), take(1)).subscribe(store => { - expect(store.dataSource.data).toEqual(HISTORY_AFTER_REMOVE); - done(); + reportsStore.deleteReport({ + mac_addr: '01:02:03:04:05:07', + deviceMacAddr: '01:02:03:04:05:07', + started: '2023-07-23T10:11:00.123Z', }); + expect(store.dispatch).toHaveBeenCalledWith( + setReports({ reports: HISTORY_AFTER_REMOVE }) + ); + done(); + }); + + it('should update store after remove with null mac_addr', done => { + mockService.deleteReport.and.returnValue(of(true)); + store.overrideSelector(selectReports, [...HISTORY_AFTER_REMOVE]); + store.refreshState(); + reportsStore.deleteReport({ - mac_addr: '00:1e:42:35:73:c4', - started: '2023-06-22T10:11:00.123Z', + mac_addr: null, + deviceMacAddr: '01:02:03:04:05:08', + started: '2023-06-23T10:11:00.123Z', }); + + expect(store.dispatch).toHaveBeenCalledWith( + setReports({ reports: [HISTORY_AFTER_REMOVE[0]] }) + ); + done(); }); }); describe('updateSort', () => { it('should update store', done => { const sort = new MatSort(); - reportsStore.setHistory([...HISTORY]); + store.overrideSelector(selectReports, [...HISTORY]); reportsStore.updateSort(sort); @@ -217,7 +228,7 @@ describe('ReportsStore', () => { describe('setFilteredValuesResults', () => { it('should update store', done => { const updatedFilters = { ...FILTERS, ...{ results: ['test2'] } }; - reportsStore.setHistory([...HISTORY]); + store.overrideSelector(selectReports, [...HISTORY]); reportsStore.setFilteredValues({ ...FILTERS }); reportsStore.setFilteredValuesResults(['test2']); @@ -235,7 +246,7 @@ describe('ReportsStore', () => { describe('setFilteredValuesDeviceInfo', () => { it('should update store', done => { const updatedFilters = { ...FILTERS, ...{ deviceInfo: 'test2' } }; - reportsStore.setHistory([...HISTORY]); + store.overrideSelector(selectReports, [...HISTORY]); reportsStore.setFilteredValues({ ...FILTERS }); reportsStore.setFilteredValuesDeviceInfo('test2'); @@ -253,7 +264,7 @@ describe('ReportsStore', () => { describe('setFilteredValuesDeviceFirmware', () => { it('should update store', done => { const updatedFilters = { ...FILTERS, ...{ deviceFirmware: 'test2' } }; - reportsStore.setHistory([...HISTORY]); + store.overrideSelector(selectReports, [...HISTORY]); reportsStore.setFilteredValues({ ...FILTERS }); reportsStore.setFilteredValuesDeviceFirmware('test2'); @@ -271,7 +282,7 @@ describe('ReportsStore', () => { describe('setFilteredValuesDateRange', () => { it('should update store', done => { const updatedFilters = { ...FILTERS, ...{ dateRange: 'test2' } }; - reportsStore.setHistory([...HISTORY]); + store.overrideSelector(selectReports, [...HISTORY]); reportsStore.setFilteredValues({ ...FILTERS }); reportsStore.setFilteredValuesDateRange('test2'); @@ -289,7 +300,7 @@ describe('ReportsStore', () => { describe('setFilteredValues', () => { it('should update store', done => { const updatedFilters = { ...EMPTY_FILTERS }; - reportsStore.setHistory([...HISTORY]); + store.overrideSelector(selectReports, [...HISTORY]); reportsStore.setFilteredValues({ ...FILTERS }); reportsStore.setFilteredValues(updatedFilters); diff --git a/modules/ui/src/app/pages/reports/reports.store.ts b/modules/ui/src/app/pages/reports/reports.store.ts index ab93a1085..7b13bf5e9 100644 --- a/modules/ui/src/app/pages/reports/reports.store.ts +++ b/modules/ui/src/app/pages/reports/reports.store.ts @@ -4,13 +4,14 @@ import { MatRow, MatTableDataSource } from '@angular/material/table'; import { HistoryTestrun, TestrunStatus } from '../../model/testrun-status'; import { DateRange, Filters } from '../../model/filters'; import { TestRunService } from '../../services/test-run.service'; -import { catchError, EMPTY, exhaustMap } from 'rxjs'; +import { exhaustMap } from 'rxjs'; import { tap, withLatestFrom } from 'rxjs/operators'; import { DatePipe } from '@angular/common'; import { MatSort } from '@angular/material/sort'; -import { selectRiskProfiles } from '../../store/selectors'; +import { selectReports, selectRiskProfiles } from '../../store/selectors'; import { Store } from '@ngrx/store'; import { AppState } from '../../store/state'; +import { fetchReports, setReports } from '../../store/actions'; export interface ReportsComponentState { displayedColumns: string[]; @@ -28,7 +29,6 @@ export interface ReportsComponentState { export const DATA_SOURCE_INITIAL_VALUE = new MatTableDataSource( [] ); - @Injectable() export class ReportsStore extends ComponentStore { private displayedColumns$ = this.select(state => state.displayedColumns); @@ -40,7 +40,7 @@ export class ReportsStore extends ComponentStore { private dataLoaded$ = this.select(state => state.dataLoaded); private selectedRow$ = this.select(state => state.selectedRow); private isFiltersEmpty$ = this.select(state => state.isFiltersEmpty); - private history$ = this.select(state => state.history); + private history$ = this.store.select(selectReports); private profiles$ = this.store.select(selectRiskProfiles); viewModel$ = this.select({ displayedColumns: this.displayedColumns$, @@ -105,13 +105,6 @@ export class ReportsStore extends ComponentStore { }; }); - setHistory = this.updater((state, history: TestrunStatus[]) => { - return { - ...state, - history, - }; - }); - updateFilteredValues = this.updater((state, filteredValues: Filters) => { return { ...state, @@ -119,42 +112,23 @@ export class ReportsStore extends ComponentStore { }; }); - getHistory = this.effect(trigger$ => { - return trigger$.pipe( - exhaustMap(() => { - return this.testRunService.getHistory().pipe( - withLatestFrom(this.filteredValues$), - tap(([reports, filteredValues]) => { - if (reports) { - this.setDataSource(reports); - this.setFilteredValues(filteredValues); - this.setHistory(reports); - } - }), - catchError(() => { - this.setDataSource([]); - this.setHistory([]); - return EMPTY; - }) - ); - }) - ); - }); - deleteReport = this.effect<{ - mac_addr: string; + mac_addr: string | null; + deviceMacAddr: string; started: string | null; }>(trigger$ => { return trigger$.pipe( - exhaustMap(({ mac_addr, started }) => { - return this.testRunService.deleteReport(mac_addr, started || '').pipe( - withLatestFrom(this.history$), - tap(([remove, current]) => { - if (remove) { - this.removeReport(mac_addr, started, current); - } - }) - ); + exhaustMap(({ mac_addr, deviceMacAddr, started }) => { + return this.testRunService + .deleteReport(mac_addr || deviceMacAddr, started || '') + .pipe( + withLatestFrom(this.history$), + tap(([remove, current]) => { + if (remove) { + this.removeReport(mac_addr, deviceMacAddr, started, current); + } + }) + ); }) ); }); @@ -225,19 +199,40 @@ export class ReportsStore extends ComponentStore { ); }); + getHistory = this.effect(() => { + return this.history$.pipe( + withLatestFrom(this.filteredValues$), + tap(([reports, filteredValues]) => { + this.setDataSource([...reports]); + this?.setFilteredValues(filteredValues); + }) + ); + }); + + getReports = this.effect(trigger$ => { + return trigger$.pipe( + tap(() => { + this.store.dispatch(fetchReports()); + }) + ); + }); + private removeReport( - mac_addr: string, + mac_addr: string | null, + deviceMacAddr: string, started: string | null, current: TestrunStatus[] ) { - const history = current; + const history = [...current]; const idx = history.findIndex( - report => report.mac_addr === mac_addr && report.started === started + report => + report.mac_addr === mac_addr && + report.device.mac_addr === deviceMacAddr && + report.started === started ); if (typeof idx === 'number') { history.splice(idx, 1); - this.setHistory(history); - this.setDataSource(history); + this.store.dispatch(setReports({ reports: history })); } } @@ -258,6 +253,7 @@ export class ReportsStore extends ComponentStore { deviceFirmware: item.device.firmware, deviceInfo: item.device.manufacturer + ' ' + item.device.model, duration: this.getDuration(item.started, item.finished), + program: item.device.test_pack ?? '', }; }); } @@ -372,6 +368,7 @@ export class ReportsStore extends ComponentStore { 'duration', 'deviceInfo', 'deviceFirmware', + 'program', 'status', 'report', ], diff --git a/modules/ui/src/app/pages/risk-assessment/components/success-dialog/success-dialog.component.html b/modules/ui/src/app/pages/risk-assessment/components/success-dialog/success-dialog.component.html new file mode 100644 index 000000000..debcfa36c --- /dev/null +++ b/modules/ui/src/app/pages/risk-assessment/components/success-dialog/success-dialog.component.html @@ -0,0 +1,45 @@ + +Risk Assessment Profile Completed +

+ It has been saved as "{{ data.profile.name }}" and can now be attached to + reports. +

+

+ The preliminary risk estimation based on your answers is + + {{ data.profile.risk }} risk + + +
{{ getRiskExplanation(data.profile.risk) }} The full report can be found + in the zip file. Please share with the lab to validate this profile and + determine next steps. +

+ + + diff --git a/modules/ui/src/app/pages/risk-assessment/components/success-dialog/success-dialog.component.scss b/modules/ui/src/app/pages/risk-assessment/components/success-dialog/success-dialog.component.scss new file mode 100644 index 000000000..23badf7a4 --- /dev/null +++ b/modules/ui/src/app/pages/risk-assessment/components/success-dialog/success-dialog.component.scss @@ -0,0 +1,73 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../../../../../theming/colors'; +@import '../../../../../theming/variables'; + +:host { + display: grid; + overflow: hidden; + width: 570px; + padding: 24px 0 8px 0; + > * { + padding: 0 16px 0 24px; + } +} + +.simple-dialog-title { + font-family: $font-primary; + font-size: 18px; + font-weight: 400; + line-height: 24px; + text-align: left; +} + +.simple-dialog-title + .simple-dialog-content { + margin-top: 0; + padding-top: 0; + border-bottom: 1px solid $lighter-grey; +} + +.simple-dialog-content { + font-family: Roboto, sans-serif; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.2px; + color: $grey-800; + padding: 16px 16px 16px 24px; + margin: 0; +} + +.simple-dialog-actions { + padding: 0; + min-height: 30px; +} + +.simple-dialog-content-risk { + font-weight: bold; + display: inline-flex; + align-items: center; +} + +.profile-item-risk { + display: inline-flex; + align-items: center; + height: 20px; + margin-left: 2px; + font-family: $font-secondary; + font-size: 12px; + font-weight: 400; + letter-spacing: 0.3px; +} diff --git a/modules/ui/src/app/pages/risk-assessment/components/success-dialog/success-dialog.component.spec.ts b/modules/ui/src/app/pages/risk-assessment/components/success-dialog/success-dialog.component.spec.ts new file mode 100644 index 000000000..b3a40c1bc --- /dev/null +++ b/modules/ui/src/app/pages/risk-assessment/components/success-dialog/success-dialog.component.spec.ts @@ -0,0 +1,78 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SuccessDialogComponent } from './success-dialog.component'; +import { TestRunService } from '../../../../services/test-run.service'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { of } from 'rxjs'; +import { PROFILE_MOCK } from '../../../../mocks/profile.mock'; +import { ProfileRisk } from '../../../../model/profile'; + +describe('SuccessDialogComponent', () => { + let component: SuccessDialogComponent; + let fixture: ComponentFixture; + const testRunServiceMock = jasmine.createSpyObj(['getRiskClass']); + let compiled: HTMLElement; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SuccessDialogComponent], + providers: [ + { provide: TestRunService, useValue: testRunServiceMock }, + { + provide: MatDialogRef, + useValue: { + keydownEvents: () => of(new KeyboardEvent('keydown', { code: '' })), + close: () => ({}), + }, + }, + { provide: MAT_DIALOG_DATA, useValue: {} }, + ], + }).compileComponents(); + fixture = TestBed.createComponent(SuccessDialogComponent); + component = fixture.componentInstance; + component.data = { + profile: PROFILE_MOCK, + }; + compiled = fixture.nativeElement as HTMLElement; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should close dialog on "cancel" click', () => { + const closeSpy = spyOn(component.dialogRef, 'close'); + const confirmButton = compiled.querySelector( + '.confirm-button' + ) as HTMLButtonElement; + + confirmButton?.click(); + + expect(closeSpy).toHaveBeenCalled(); + + closeSpy.calls.reset(); + }); + + it('should return proper text for risk', () => { + expect(component.getRiskExplanation(ProfileRisk.LIMITED)).toEqual(''); + expect(component.getRiskExplanation(ProfileRisk.HIGH)).toEqual( + 'An additional assessment may be required.' + ); + }); +}); diff --git a/modules/ui/src/app/pages/risk-assessment/components/success-dialog/success-dialog.component.ts b/modules/ui/src/app/pages/risk-assessment/components/success-dialog/success-dialog.component.ts new file mode 100644 index 000000000..e7ce23ff3 --- /dev/null +++ b/modules/ui/src/app/pages/risk-assessment/components/success-dialog/success-dialog.component.ts @@ -0,0 +1,65 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Component, Inject } from '@angular/core'; +import { + MAT_DIALOG_DATA, + MatDialogModule, + MatDialogRef, +} from '@angular/material/dialog'; +import { MatButtonModule } from '@angular/material/button'; +import { EscapableDialogComponent } from '../../../../components/escapable-dialog/escapable-dialog.component'; +import { + Profile, + ProfileRisk, + RiskResultClassName, +} from '../../../../model/profile'; +import { TestRunService } from '../../../../services/test-run.service'; +import { CommonModule } from '@angular/common'; + +interface DialogData { + profile: Profile; +} + +@Component({ + selector: 'app-success-dialog', + templateUrl: './success-dialog.component.html', + styleUrls: ['./success-dialog.component.scss'], + standalone: true, + imports: [MatDialogModule, MatButtonModule, CommonModule], +}) +export class SuccessDialogComponent extends EscapableDialogComponent { + constructor( + private readonly testRunService: TestRunService, + public override dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: DialogData + ) { + super(dialogRef); + } + + confirm() { + this.dialogRef.close(); + } + + public getRiskClass(riskResult: string): RiskResultClassName { + return this.testRunService.getRiskClass(riskResult); + } + + getRiskExplanation(risk: string | undefined) { + return risk === ProfileRisk.HIGH + ? 'An additional assessment may be required.' + : ''; + } +} diff --git a/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.html b/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.html index e5d0ae0cb..8bde474ba 100644 --- a/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.html +++ b/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.html @@ -15,11 +15,11 @@ -->
-

Profile name *

+

Profile name *

+ class="profile-form-field"> Specify risk assessment profile name Required for saving a profile @@ -36,28 +36,12 @@ The Profile name is required - This Profile name is already used for another Risk Assessment - profile + This Profile name is already used for another profile - - - + +
@@ -76,240 +60,19 @@ (click)="onSaveClick(ProfileStatus.DRAFT)"> Save Draft + +
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {{ description }} - - Please, check. “ and \ are not allowed. - - - The field is required - - - The field must be a maximum of - {{ getControl(formControlName).getError('maxlength').requiredLength }} - characters. - - - - - - - - {{ description }} - - Please, check. “ and \ are not allowed. - - - The field is required - - - The field must be a maximum of - {{ getControl(formControlName).getError('maxlength').requiredLength }} - characters. - - - - - - - - {{ description }} - - The field is required - - - Please, check the email address. Valid e-mail can contain only latin - letters, numbers, @ and . (dot). - - - The field must be a maximum of - {{ getControl(formControlName).getError('maxlength').requiredLength }} - characters. - - - - - -
-

- - {{ option }} - -

- {{ - description - }} -
-
- - - - - - {{ option }} - - - {{ - description - }} - - The field is required - - - diff --git a/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.scss b/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.scss index d9e90a2c4..1e4ad721b 100644 --- a/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.scss +++ b/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.scss @@ -17,42 +17,32 @@ @import 'src/theming/colors'; @import 'src/theming/variables'; +:host { + height: 100%; + display: flex; + flex-direction: column; +} + .profile-form { + overflow: scroll; + + .name-field-label { + padding-top: 0; + } .field-container { display: flex; flex-direction: column; align-items: flex-start; padding: 8px 16px 8px 24px; } - .field-label { - margin: 0; - color: $grey-800; - font-size: 18px; - line-height: 24px; - padding-top: 24px; - padding-bottom: 16px; - &:first-child { - padding-top: 0; - } - &:has(+ .field-select-multiple.ng-invalid.ng-dirty) { - color: mat.get-color-from-palette($color-warn, 700); - } - } - mat-form-field { + + .profile-form-field { width: 100%; } - .field-hint { - font-family: $font-secondary; - font-size: 12px; - font-weight: 400; - line-height: 16px; - text-align: left; - padding-top: 8px; - } } -.profile-form-field { - width: 100%; +.profile-form-field ::ng-deep .mat-mdc-form-field-textarea-control { + display: inherit; } .form-actions { @@ -61,23 +51,14 @@ padding: 8px 24px 24px 24px; } -.save-draft-button:not(.mat-mdc-button-disabled) { +.save-draft-button:not(.mat-mdc-button-disabled), +.discard-button:not(.mat-mdc-button-disabled) { color: $primary; } -.field-select-multiple { - .field-select-checkbox { - &:has(::ng-deep .mat-mdc-checkbox-checked) { - background: mat.get-color-from-palette($color-primary, 50); - } - ::ng-deep .mdc-checkbox__ripple { - display: none; - } - &:first-of-type { - margin-top: 0; - } - &:last-of-type { - margin-bottom: 8px; - } - } +.save-profile-button:not(.mat-mdc-button-disabled), +.save-draft-button:not(.mat-mdc-button-disabled), +.discard-button:not(.mat-mdc-button-disabled) { + cursor: pointer; + pointer-events: auto; } diff --git a/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.spec.ts b/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.spec.ts index 7344dd92c..d279a1f14 100644 --- a/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.spec.ts +++ b/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.spec.ts @@ -18,15 +18,17 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ProfileFormComponent } from './profile-form.component'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { + COPY_PROFILE_MOCK, NEW_PROFILE_MOCK, NEW_PROFILE_MOCK_DRAFT, + OUTDATED_DRAFT_PROFILE_MOCK, PROFILE_FORM, PROFILE_MOCK, PROFILE_MOCK_2, PROFILE_MOCK_3, RENAME_PROFILE_MOCK, } from '../../../mocks/profile.mock'; -import { FormControlType, ProfileStatus } from '../../../model/profile'; +import { ProfileStatus } from '../../../model/profile'; describe('ProfileFormComponent', () => { let component: ProfileFormComponent; @@ -132,7 +134,6 @@ describe('ProfileFormComponent', () => { name.dispatchEvent(new Event('input')); component.nameControl.markAsTouched(); - fixture.detectChanges(); fixture.detectChanges(); const nameError = compiled.querySelector('mat-error')?.innerHTML; @@ -140,180 +141,11 @@ describe('ProfileFormComponent', () => { expect(error).toBeTruthy(); expect(nameError).toContain( - 'This Profile name is already used for another Risk Assessment profile' + 'This Profile name is already used for another profile' ); }); }); - PROFILE_FORM.forEach((item, index) => { - const uiIndex = index + 1; // as Profile name is at 0 position, the json items start from 1 i - - it(`should have form field with specific type"`, () => { - const fields = compiled.querySelectorAll('.profile-form-field'); - - if (item.type === FormControlType.SELECT) { - const select = fields[uiIndex].querySelector('mat-select'); - expect(select).toBeTruthy(); - } else if (item.type === FormControlType.SELECT_MULTIPLE) { - const select = fields[uiIndex].querySelector('mat-checkbox'); - expect(select).toBeTruthy(); - } else if (item.type === FormControlType.TEXTAREA) { - const input = fields[uiIndex]?.querySelector('textarea'); - expect(input).toBeTruthy(); - } else { - const input = fields[uiIndex]?.querySelector('input'); - expect(input).toBeTruthy(); - } - }); - - it('should have label', () => { - const labels = compiled.querySelectorAll('.field-label'); - const uiIndex = index + 1; // as Profile name is at 0 position, the json items start from 1 i - - const label = item?.validation?.required - ? item.question + ' *' - : item.question; - expect(labels[uiIndex].textContent?.trim()).toEqual(label); - }); - - it('should have hint', () => { - const fields = compiled.querySelectorAll('.profile-form-field'); - const uiIndex = index + 1; // as Profile name is at 0 position, the json items start from 1 i - const hint = fields[uiIndex].querySelector('mat-hint'); - - if (item.description) { - expect(hint?.textContent?.trim()).toEqual(item.description); - } else { - expect(hint).toBeNull(); - } - }); - - if (item.type === FormControlType.SELECT) { - describe('select', () => { - it(`should have default value if provided`, () => { - const fields = compiled.querySelectorAll('.profile-form-field'); - const select = fields[uiIndex].querySelector('mat-select'); - expect(select?.textContent?.trim()).toEqual(item.default || ''); - }); - - it('should have "required" error when field is not filled', () => { - const fields = compiled.querySelectorAll('.profile-form-field'); - - component.getControl(index).setValue(''); - component.getControl(index).markAsTouched(); - - fixture.detectChanges(); - - const error = fields[uiIndex].querySelector('mat-error')?.innerHTML; - - expect(error).toContain('The field is required'); - }); - }); - } - - if (item.type === FormControlType.SELECT_MULTIPLE) { - describe('select multiple', () => { - it(`should mark form group as dirty while tab navigation`, () => { - const fields = compiled.querySelectorAll('.profile-form-field'); - const checkbox = fields[uiIndex].querySelector( - '.field-select-checkbox:last-of-type mat-checkbox' - ); - checkbox?.dispatchEvent( - new KeyboardEvent('keydown', { key: 'Tab' }) - ); - fixture.detectChanges(); - - expect(component.getControl(index).dirty).toBeTrue(); - }); - }); - } - - if ( - item.type === FormControlType.TEXT || - item.type === FormControlType.TEXTAREA || - item.type === FormControlType.EMAIL_MULTIPLE - ) { - describe('text or text-long or email-multiple', () => { - if (item.validation?.required) { - it('should have "required" error when field is not filled', () => { - const fields = compiled.querySelectorAll('.profile-form-field'); - const uiIndex = index + 1; // as Profile name is at 0 position, the json items start from 1 i - const input = fields[uiIndex].querySelector( - '.mat-mdc-input-element' - ) as HTMLInputElement; - ['', ' '].forEach(value => { - input.value = value; - input.dispatchEvent(new Event('input')); - component.getControl(index).markAsTouched(); - fixture.detectChanges(); - const errors = fields[uiIndex].querySelectorAll('mat-error'); - let hasError = false; - errors.forEach(error => { - if (error.textContent === 'The field is required') { - hasError = true; - } - }); - - expect(hasError).toBeTrue(); - }); - }); - } - - it('should have "invalid_format" error when field does not satisfy validation rules', () => { - const fields = compiled.querySelectorAll('.profile-form-field'); - const uiIndex = index + 1; // as Profile name is at 0 position, the json items start from 1 i - const input: HTMLInputElement = fields[uiIndex].querySelector( - '.mat-mdc-input-element' - ) as HTMLInputElement; - input.value = 'as\\\\\\\\\\""""""""'; - input.dispatchEvent(new Event('input')); - component.getControl(index).markAsTouched(); - fixture.detectChanges(); - const result = - item.type === FormControlType.EMAIL_MULTIPLE - ? 'Please, check the email address. Valid e-mail can contain only latin letters, numbers, @ and . (dot).' - : 'Please, check. “ and \\ are not allowed.'; - const errors = fields[uiIndex].querySelectorAll('mat-error'); - let hasError = false; - errors.forEach(error => { - if (error.textContent === result) { - hasError = true; - } - }); - - expect(hasError).toBeTrue(); - }); - - if (item.validation?.max) { - it('should have "maxlength" error when field is exceeding max length', () => { - const fields = compiled.querySelectorAll('.profile-form-field'); - const uiIndex = index + 1; // as Profile name is at 0 position, the json items start from 1 i - const input: HTMLInputElement = fields[uiIndex].querySelector( - '.mat-mdc-input-element' - ) as HTMLInputElement; - input.value = - 'very long value very long value very long value very long value very long value very long value very long value very long value very long value very long value'; - input.dispatchEvent(new Event('input')); - component.getControl(index).markAsTouched(); - fixture.detectChanges(); - - const errors = fields[uiIndex].querySelectorAll('mat-error'); - let hasError = false; - errors.forEach(error => { - if ( - error.textContent === - `The field must be a maximum of ${item.validation?.max} characters.` - ) { - hasError = true; - } - }); - expect(hasError).toBeTrue(); - }); - } - }); - } - }); - describe('Draft button', () => { it('should be disabled when profile name is empty', () => { component.nameControl.setValue(''); @@ -388,9 +220,52 @@ describe('ProfileFormComponent', () => { }); }); }); + + describe('Discard button', () => { + beforeEach(() => { + fillForm(component); + fixture.detectChanges(); + }); + + it('should be enabled when form is filled', () => { + const discardButton = compiled.querySelector( + '.discard-button' + ) as HTMLButtonElement; + + expect(discardButton.disabled).toBeFalse(); + }); + + it('should emit discard', () => { + const emitSpy = spyOn(component.discard, 'emit'); + const discardButton = compiled.querySelector( + '.discard-button' + ) as HTMLButtonElement; + discardButton.click(); + + expect(emitSpy).toHaveBeenCalled(); + }); + }); }); describe('Class tests', () => { + describe('with outdated draft profile', () => { + beforeEach(() => { + component.selectedProfile = OUTDATED_DRAFT_PROFILE_MOCK; + fixture.detectChanges(); + }); + + it('should have an error when uses the name of copy profile', () => { + expect(component.profileForm.value).toEqual({ + 0: '', + 1: 'IoT Sensor', + 2: '', + 3: { 0: false, 1: false, 2: false }, + 4: '', + name: 'Outdated profile', + }); + }); + }); + describe('with profile', () => { beforeEach(() => { component.selectedProfile = PROFILE_MOCK; @@ -432,6 +307,15 @@ describe('ProfileFormComponent', () => { component.nameControl.hasError('has_same_profile_name') ).toBeTrue(); }); + + it('should have an error when uses the name of copy profile', () => { + component.selectedProfile = COPY_PROFILE_MOCK; + component.profiles = [PROFILE_MOCK, PROFILE_MOCK_2, COPY_PROFILE_MOCK]; + + expect( + component.nameControl.hasError('has_same_profile_name') + ).toBeTrue(); + }); }); describe('with no profile', () => { diff --git a/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.ts b/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.ts index a15867ae7..2656221cd 100644 --- a/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.ts +++ b/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.ts @@ -16,6 +16,7 @@ import { CdkTextareaAutosize, TextFieldModule } from '@angular/cdk/text-field'; import { afterNextRender, + AfterViewInit, ChangeDetectionStrategy, Component, EventEmitter, @@ -39,19 +40,19 @@ import { FormGroup, ReactiveFormsModule, ValidatorFn, - Validators, } from '@angular/forms'; import { MatInputModule } from '@angular/material/input'; import { DeviceValidators } from '../../devices/components/device-form/device.validators'; import { - FormControlType, Profile, ProfileFormat, ProfileStatus, Question, - Validation, } from '../../../model/profile'; +import { FormControlType } from '../../../model/question'; import { ProfileValidators } from './profile.validators'; +import { DynamicFormComponent } from '../../../components/dynamic-form/dynamic-form.component'; +import { CdkTrapFocus } from '@angular/cdk/a11y'; @Component({ selector: 'app-profile-form', @@ -66,22 +67,24 @@ import { ProfileValidators } from './profile.validators'; MatSelectModule, MatCheckboxModule, TextFieldModule, + DynamicFormComponent, ], templateUrl: './profile-form.component.html', styleUrl: './profile-form.component.scss', + hostDirectives: [CdkTrapFocus], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class ProfileFormComponent implements OnInit { +export class ProfileFormComponent implements OnInit, AfterViewInit { private profile: Profile | null = null; private profileList!: Profile[]; private injector = inject(Injector); private nameValidator!: ValidatorFn; - public readonly FormControlType = FormControlType; public readonly ProfileStatus = ProfileStatus; profileForm: FormGroup = this.fb.group({}); @ViewChildren(CdkTextareaAutosize) autosize!: QueryList; @Input() profileFormat!: ProfileFormat[]; + @Input() isCopyProfile!: boolean; @Input() set profiles(profiles: Profile[]) { this.profileList = profiles; @@ -105,15 +108,19 @@ export class ProfileFormComponent implements OnInit { } @Output() saveProfile = new EventEmitter(); + @Output() discard = new EventEmitter(); constructor( private deviceValidators: DeviceValidators, private profileValidators: ProfileValidators, private fb: FormBuilder ) {} ngOnInit() { - this.profileForm = this.createProfileForm(this.profileFormat); + this.profileForm = this.createProfileForm(); + } + + ngAfterViewInit(): void { if (this.selectedProfile) { - this.fillProfileForm(this.profileFormat, this.selectedProfile); + this.fillProfileForm(this.profileFormat, this.selectedProfile!); } } @@ -138,7 +145,7 @@ export class ProfileFormComponent implements OnInit { return this.profileForm.get(name.toString()) as AbstractControl; } - createProfileForm(questions: ProfileFormat[]): FormGroup { + createProfileForm(): FormGroup { // eslint-disable-next-line @typescript-eslint/no-explicit-any const group: any = {}; @@ -153,52 +160,9 @@ export class ProfileFormComponent implements OnInit { this.nameValidator, ]); - questions.forEach((question, index) => { - if (question.type === FormControlType.SELECT_MULTIPLE) { - group[index] = this.getMultiSelectGroup(question); - } else { - const validators = this.getValidators( - question.type, - question.validation - ); - group[index] = new FormControl(question.default || '', validators); - } - }); return new FormGroup(group); } - getValidators(type: FormControlType, validation?: Validation): ValidatorFn[] { - const validators: ValidatorFn[] = []; - if (validation) { - if (validation.required) { - validators.push(this.profileValidators.textRequired()); - } - if (validation.max) { - validators.push(Validators.maxLength(Number(validation.max))); - } - if (type === FormControlType.EMAIL_MULTIPLE) { - validators.push(this.profileValidators.emailStringFormat()); - } - if (type === FormControlType.TEXT || type === FormControlType.TEXTAREA) { - validators.push(this.profileValidators.textFormat()); - } - } - return validators; - } - - getMultiSelectGroup(question: ProfileFormat): FormGroup { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const group: any = {}; - question.options?.forEach((option, index) => { - group[index] = false; - }); - return this.fb.group(group, { - validators: question.validation?.required - ? [this.profileValidators.multiSelectRequired] - : [], - }); - } - getFormGroup(name: string | number): FormGroup { return this.profileForm?.controls[name] as FormGroup; } @@ -206,18 +170,22 @@ export class ProfileFormComponent implements OnInit { fillProfileForm(profileFormat: ProfileFormat[], profile: Profile): void { this.nameControl.setValue(profile.name); profileFormat.forEach((question, index) => { + const answer = profile.questions.find( + answers => answers.question === question.question + ); if (question.type === FormControlType.SELECT_MULTIPLE) { question.options?.forEach((item, idx) => { - if ((profile.questions[index].answer as number[])?.includes(idx)) { + if ((answer?.answer as number[])?.includes(idx)) { this.getFormGroup(index).controls[idx].setValue(true); } else { this.getFormGroup(index).controls[idx].setValue(false); } }); } else { - this.getControl(index).setValue(profile.questions[index].answer); + this.getControl(index).setValue(answer?.answer || ''); } }); + this.nameControl.markAsTouched(); this.triggerResize(); } @@ -231,14 +199,8 @@ export class ProfileFormComponent implements OnInit { this.saveProfile.emit(response); } - public markSectionAsDirty( - optionIndex: number, - optionLength: number, - formControlName: string - ) { - if (optionIndex === optionLength - 1) { - this.getControl(formControlName).markAsDirty(); - } + onDiscardClick() { + this.discard.emit(); } private buildResponseFromForm( @@ -251,7 +213,7 @@ export class ProfileFormComponent implements OnInit { const request: any = { questions: [], }; - if (profile) { + if (profile && !this.isCopyProfile) { request.name = profile.name; request.rename = this.nameControl.value?.trim(); } else { diff --git a/modules/ui/src/app/pages/risk-assessment/profile-form/profile.validators.ts b/modules/ui/src/app/pages/risk-assessment/profile-form/profile.validators.ts index dcad4b397..10280586d 100644 --- a/modules/ui/src/app/pages/risk-assessment/profile-form/profile.validators.ts +++ b/modules/ui/src/app/pages/risk-assessment/profile-form/profile.validators.ts @@ -36,8 +36,14 @@ export class ProfileValidators { profile: Profile | null ): ValidatorFn { return (control: AbstractControl): ValidationErrors | null => { - const value = control.value?.trim(); - if (value && profiles.length && (!profile || profile?.name !== value)) { + const value = control.value?.trim().toLowerCase(); + if ( + value && + profiles.length && + (!profile || + !profile.created || + (profile.created && profile?.name.toLowerCase() !== value)) + ) { const isSameProfileName = this.hasSameProfileName(value, profiles); return isSameProfileName ? { has_same_profile_name: true } : null; } @@ -85,7 +91,8 @@ export class ProfileValidators { profiles: Profile[] ): boolean { return ( - profiles.some(profile => profile.name === profileName?.trim()) || false + profiles.some(profile => profile.name.toLowerCase() === profileName) || + false ); } } diff --git a/modules/ui/src/app/pages/risk-assessment/profile-item/profile-item.component.html b/modules/ui/src/app/pages/risk-assessment/profile-item/profile-item.component.html index 35850f0ed..41f90de38 100644 --- a/modules/ui/src/app/pages/risk-assessment/profile-item/profile-item.component.html +++ b/modules/ui/src/app/pages/risk-assessment/profile-item/profile-item.component.html @@ -13,17 +13,32 @@ See the License for the specific language governing permissions and limitations under the License. --> -
+
+ (keydown.enter)="enterProfileItem(profile)" + (keydown.space)="enterProfileItem(profile)"> + [attr.aria-label]=" + profile.status === ProfileStatus.EXPIRED + ? EXPIRED_TOOLTIP + : profile.status + "> + +

- {{ profile.created | date: 'dd MMM yyyy' }} + + Outdated ({{ profile.created | date: 'dd MMM yyyy' }}) + + + {{ profile.created | date: 'dd MMM yyyy' }} +

+
diff --git a/modules/ui/src/app/pages/risk-assessment/profile-item/profile-item.component.scss b/modules/ui/src/app/pages/risk-assessment/profile-item/profile-item.component.scss index a9a22b9e4..739a7bd14 100644 --- a/modules/ui/src/app/pages/risk-assessment/profile-item/profile-item.component.scss +++ b/modules/ui/src/app/pages/risk-assessment/profile-item/profile-item.component.scss @@ -28,15 +28,28 @@ $profile-item-container-gap: 16px; .profile-item-container { display: grid; - grid-template-columns: minmax(160px, 1fr) $profile-icon-container-size; + grid-template-columns: minmax(160px, 1fr) repeat( + 2, + $profile-icon-container-size + ); gap: $profile-item-container-gap; box-sizing: border-box; padding: 12px 16px; border-bottom: 1px solid $lighter-grey; align-items: center; - height: 92px; + min-height: 92px; + &-expired { + grid-template-columns: minmax(160px, 1fr) $profile-icon-container-size; + } } +.profile-item-container-expired .profile-item-info { + .profile-item-icon, + .profile-item-name, + .profile-item-created { + color: $red-800; + } +} .profile-item-icon-container { grid-area: icon; display: inline-block; diff --git a/modules/ui/src/app/pages/risk-assessment/profile-item/profile-item.component.spec.ts b/modules/ui/src/app/pages/risk-assessment/profile-item/profile-item.component.spec.ts index ae48e64ec..ccdf0e211 100644 --- a/modules/ui/src/app/pages/risk-assessment/profile-item/profile-item.component.spec.ts +++ b/modules/ui/src/app/pages/risk-assessment/profile-item/profile-item.component.spec.ts @@ -13,11 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; import { ProfileItemComponent } from './profile-item.component'; -import { PROFILE_MOCK } from '../../../mocks/profile.mock'; +import { + EXPIRED_PROFILE_MOCK, + PROFILE_MOCK, +} from '../../../mocks/profile.mock'; import { TestRunService } from '../../../services/test-run.service'; +import { LiveAnnouncer } from '@angular/cdk/a11y'; describe('ProfileItemComponent', () => { let component: ProfileItemComponent; @@ -25,11 +34,16 @@ describe('ProfileItemComponent', () => { let compiled: HTMLElement; const testRunServiceMock = jasmine.createSpyObj(['getRiskClass']); - + const mockLiveAnnouncer = jasmine.createSpyObj('mockLiveAnnouncer', [ + 'announce', + ]); beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ProfileItemComponent], - providers: [{ provide: TestRunService, useValue: testRunServiceMock }], + providers: [ + { provide: TestRunService, useValue: testRunServiceMock }, + { provide: LiveAnnouncer, useValue: mockLiveAnnouncer }, + ], }).compileComponents(); fixture = TestBed.createComponent(ProfileItemComponent); @@ -59,8 +73,12 @@ describe('ProfileItemComponent', () => { const deleteButton = fixture.nativeElement.querySelector( '.profile-item-button.delete' ); + const copyButton = fixture.nativeElement.querySelector( + '.profile-item-button.copy' + ); expect(deleteButton?.ariaLabel?.trim()).toContain(PROFILE_MOCK.name); + expect(copyButton?.ariaLabel?.trim()).toContain(PROFILE_MOCK.name); }); it('should emit delete event on delete button clicked', () => { @@ -84,4 +102,48 @@ describe('ProfileItemComponent', () => { expect(profileClickedSpy).toHaveBeenCalledWith(PROFILE_MOCK); }); + + it('should change tooltip on focusout', fakeAsync(() => { + component.profile = EXPIRED_PROFILE_MOCK; + fixture.detectChanges(); + + fixture.nativeElement.dispatchEvent(new Event('focusout')); + tick(); + + expect(component.tooltip.message).toEqual( + 'Expired. Please, create a new Risk profile.' + ); + })); + + it('#getRiskClass should call getRiskClass on testRunService', () => { + const MOCK_RISK = 'mock value'; + component.getRiskClass(MOCK_RISK); + expect(testRunServiceMock.getRiskClass).toHaveBeenCalledWith(MOCK_RISK); + }); + + it('#enterProfileItem should emit profileClicked', () => { + const profileClickedSpy = spyOn(component.profileClicked, 'emit'); + + component.enterProfileItem(PROFILE_MOCK); + + expect(profileClickedSpy).toHaveBeenCalled(); + }); + + describe('with Expired profile', () => { + beforeEach(() => { + component.enterProfileItem(EXPIRED_PROFILE_MOCK); + }); + + it('should change tooltip on enterProfileItem', () => { + expect(component.tooltip.message).toEqual( + 'This risk profile is outdated. Please create a new risk profile.' + ); + }); + + it('should announce', () => { + expect(mockLiveAnnouncer.announce).toHaveBeenCalledWith( + 'This risk profile is outdated. Please create a new risk profile.' + ); + }); + }); }); diff --git a/modules/ui/src/app/pages/risk-assessment/profile-item/profile-item.component.ts b/modules/ui/src/app/pages/risk-assessment/profile-item/profile-item.component.ts index 79bd08833..6ddbe06e9 100644 --- a/modules/ui/src/app/pages/risk-assessment/profile-item/profile-item.component.ts +++ b/modules/ui/src/app/pages/risk-assessment/profile-item/profile-item.component.ts @@ -17,8 +17,10 @@ import { ChangeDetectionStrategy, Component, EventEmitter, + HostListener, Input, Output, + ViewChild, } from '@angular/core'; import { Profile, @@ -27,27 +29,67 @@ import { } from '../../../model/profile'; import { MatIcon } from '@angular/material/icon'; import { MatButtonModule } from '@angular/material/button'; -import { CommonModule } from '@angular/common'; +import { CommonModule, DatePipe } from '@angular/common'; import { TestRunService } from '../../../services/test-run.service'; -import { MatTooltipModule } from '@angular/material/tooltip'; +import { MatTooltip, MatTooltipModule } from '@angular/material/tooltip'; +import { LiveAnnouncer } from '@angular/cdk/a11y'; @Component({ selector: 'app-profile-item', standalone: true, imports: [MatIcon, MatButtonModule, CommonModule, MatTooltipModule], + providers: [MatTooltip, DatePipe], templateUrl: './profile-item.component.html', styleUrl: './profile-item.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) export class ProfileItemComponent { public readonly ProfileStatus = ProfileStatus; + public readonly EXPIRED_TOOLTIP = + 'Expired. Please, create a new Risk profile.'; @Input() profile!: Profile; @Output() deleteButtonClicked = new EventEmitter(); @Output() profileClicked = new EventEmitter(); + @Output() copyProfileClicked = new EventEmitter(); - constructor(private readonly testRunService: TestRunService) {} + @ViewChild('tooltip') tooltip!: MatTooltip; + + @HostListener('focusout', ['$event']) + outEvent(): void { + if (this.profile.status === ProfileStatus.EXPIRED) { + this.tooltip.message = this.EXPIRED_TOOLTIP; + } + } + + constructor( + private readonly testRunService: TestRunService, + private liveAnnouncer: LiveAnnouncer, + private datePipe: DatePipe + ) {} public getRiskClass(riskResult: string): RiskResultClassName { return this.testRunService.getRiskClass(riskResult); } + + public async enterProfileItem(profile: Profile) { + if (profile.status === ProfileStatus.EXPIRED) { + this.tooltip.message = + 'This risk profile is outdated. Please create a new risk profile.'; + this.tooltip.show(); + await this.liveAnnouncer.announce( + 'This risk profile is outdated. Please create a new risk profile.' + ); + } else { + this.profileClicked.emit(profile); + } + } + + getProfileItemLabel(profile: Profile) { + return `${profile.status} ${profile.risk} risk ${profile.name} ${this.datePipe.transform(profile.created, 'dd MMM yyyy')}`; + } + + delete(event: Event, name: string) { + event.preventDefault(); + this.deleteButtonClicked.emit(name); + } } diff --git a/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.html b/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.html index 2f11ea76b..620712a2f 100644 --- a/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.html +++ b/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.html @@ -23,11 +23,11 @@

Risk assessment

+ (saveProfile)="saveProfileClicked($event, vm.selectedProfile)" + (discard)="discard(vm.selectedProfile)">
@@ -43,16 +43,13 @@

Saved profiles

+ (profileClicked)="profileClicked($event)" + (copyProfileClicked)="copyProfileAndOpenForm($event)">
diff --git a/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.scss b/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.scss index c4ef49782..90f8a3a41 100644 --- a/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.scss +++ b/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.scss @@ -22,6 +22,8 @@ } .risk-assessment-content-empty { + position: absolute; + top: 0; height: 100%; width: calc(100%); display: flex; @@ -61,7 +63,7 @@ .main-content { padding: 16px 32px; - overflow: scroll; + overflow: hidden; width: calc(100% - $profiles-drawer-width); } diff --git a/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.spec.ts b/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.spec.ts index e2aa6332e..2be8723c9 100644 --- a/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.spec.ts +++ b/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.spec.ts @@ -26,7 +26,12 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { TestRunService } from '../../services/test-run.service'; import SpyObj = jasmine.SpyObj; import { MatSidenavModule } from '@angular/material/sidenav'; -import { NEW_PROFILE_MOCK, PROFILE_MOCK } from '../../mocks/profile.mock'; +import { + COPY_PROFILE_MOCK, + NEW_PROFILE_MOCK, + NEW_PROFILE_MOCK_DRAFT, + PROFILE_MOCK, +} from '../../mocks/profile.mock'; import { of } from 'rxjs'; import { Component, Input } from '@angular/core'; import { Profile, ProfileFormat } from '../../model/profile'; @@ -43,6 +48,7 @@ describe('RiskAssessmentComponent', () => { const mockLiveAnnouncer: SpyObj = jasmine.createSpyObj([ 'announce', + 'clear', ]); let compiled: HTMLElement; @@ -166,15 +172,13 @@ describe('RiskAssessmentComponent', () => { tick(); expect(openSpy).toHaveBeenCalledWith(SimpleDialogComponent, { - ariaLabel: 'Delete risk profile', data: { title: 'Delete risk profile?', content: `You are about to delete ${PROFILE_MOCK.name}. Are you sure?`, }, - autoFocus: true, + autoFocus: 'dialog', hasBackdrop: true, disableClose: true, - panelClass: 'simple-dialog', }); openSpy.calls.reset(); @@ -217,6 +221,31 @@ describe('RiskAssessmentComponent', () => { }); }); + describe('#getCopyOfProfile', () => { + it('should open the form with copy of profile', () => { + const copy = component.getCopyOfProfile(PROFILE_MOCK); + expect(copy).toEqual(COPY_PROFILE_MOCK); + }); + }); + + it('#profileClicked should call openForm with profile', fakeAsync(() => { + spyOn(component, 'openForm'); + + component.profileClicked(PROFILE_MOCK); + tick(); + + expect(component.openForm).toHaveBeenCalledWith(PROFILE_MOCK); + })); + + it('#copyProfileAndOpenForm should call openForm with copy of profile', fakeAsync(() => { + spyOn(component, 'openForm'); + + component.copyProfileAndOpenForm(PROFILE_MOCK); + tick(); + + expect(component.openForm).toHaveBeenCalledWith(COPY_PROFILE_MOCK); + })); + describe('#saveProfile', () => { describe('with no profile selected', () => { beforeEach(() => { @@ -224,10 +253,13 @@ describe('RiskAssessmentComponent', () => { }); it('should call store saveProfile when it is new profile', () => { - expect(mockRiskAssessmentStore.saveProfile).toHaveBeenCalledWith({ + const args = mockRiskAssessmentStore.saveProfile.calls.argsFor(0); + // @ts-expect-error config is in object + expect(args[0].profile).toEqual({ name: 'test', questions: [], }); + expect(mockRiskAssessmentStore.saveProfile).toHaveBeenCalled(); }); it('should close the form', () => { @@ -236,7 +268,7 @@ describe('RiskAssessmentComponent', () => { }); describe('with profile selected', () => { - it('should open save profile modal', fakeAsync(() => { + it('should open save profile modal for valid profile', fakeAsync(() => { const openSpy = spyOn(component.dialog, 'open').and.returnValue({ afterClosed: () => of(true), } as MatDialogRef); @@ -244,22 +276,40 @@ describe('RiskAssessmentComponent', () => { component.saveProfileClicked(NEW_PROFILE_MOCK, PROFILE_MOCK); expect(openSpy).toHaveBeenCalledWith(SimpleDialogComponent, { - ariaLabel: 'Save changes', data: { - title: 'Save changes', + title: 'Save profile', + content: `You are about to save changes in Primary profile. Are you sure?`, + }, + autoFocus: 'dialog', + hasBackdrop: true, + disableClose: true, + }); + + openSpy.calls.reset(); + })); + + it('should open save draft profile modal', fakeAsync(() => { + const openSpy = spyOn(component.dialog, 'open').and.returnValue({ + afterClosed: () => of(true), + } as MatDialogRef); + + component.saveProfileClicked(NEW_PROFILE_MOCK_DRAFT, PROFILE_MOCK); + + expect(openSpy).toHaveBeenCalledWith(SimpleDialogComponent, { + data: { + title: 'Save draft profile', content: `You are about to save changes in Primary profile. Are you sure?`, }, - autoFocus: true, + autoFocus: 'dialog', hasBackdrop: true, disableClose: true, - panelClass: 'simple-dialog', }); openSpy.calls.reset(); })); it('should call store saveProfile', fakeAsync(() => { - spyOn(component.dialog, 'open').and.returnValue({ + const openSpy = spyOn(component.dialog, 'open').and.returnValue({ afterClosed: () => of(true), } as MatDialogRef); @@ -267,9 +317,23 @@ describe('RiskAssessmentComponent', () => { tick(); - expect(mockRiskAssessmentStore.saveProfile).toHaveBeenCalledWith( - NEW_PROFILE_MOCK - ); + const args = mockRiskAssessmentStore.saveProfile.calls.argsFor(0); + // @ts-expect-error config is in object + expect(args[0].profile).toEqual(NEW_PROFILE_MOCK); + expect(mockRiskAssessmentStore.saveProfile).toHaveBeenCalled(); + openSpy.calls.reset(); + })); + + it('should call store saveProfile and should not open save draft profile modal when profile does not have changes', fakeAsync(() => { + const openSpy = spyOn(component.dialog, 'open').and.returnValue({ + afterClosed: () => of(true), + } as MatDialogRef); + + component.saveProfileClicked(PROFILE_MOCK, PROFILE_MOCK); + + expect(openSpy).not.toHaveBeenCalled(); + expect(mockRiskAssessmentStore.saveProfile).toHaveBeenCalled(); + openSpy.calls.reset(); })); it('should close the form', fakeAsync(() => { @@ -284,6 +348,43 @@ describe('RiskAssessmentComponent', () => { })); }); }); + + describe('#discard', () => { + describe('with no selected profile', () => { + beforeEach(() => { + component.discard(null); + }); + + it('should call setFocusOnCreateButton', () => { + expect( + mockRiskAssessmentStore.setFocusOnCreateButton + ).toHaveBeenCalled(); + }); + + it('should close the form', () => { + expect(component.isOpenProfileForm).toBeFalse(); + }); + }); + + describe('with selected profile', () => { + beforeEach(fakeAsync(() => { + component.discard(PROFILE_MOCK); + tick(100); + })); + + it('should call setFocusOnCreateButton', fakeAsync(() => { + expect( + mockRiskAssessmentStore.setFocusOnSelectedProfile + ).toHaveBeenCalled(); + })); + + it('should update selected profile', () => { + expect( + mockRiskAssessmentStore.updateSelectedProfile + ).toHaveBeenCalledWith(null); + }); + }); + }); }); }); @@ -301,6 +402,7 @@ class FakeProfileItemComponent { }) class FakeProfileFormComponent { @Input() profiles!: Profile[]; + @Input() isCopyProfile!: boolean; @Input() selectedProfile!: Profile; @Input() profileFormat!: ProfileFormat[]; } diff --git a/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.ts b/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.ts index 503d87a52..e30efe710 100644 --- a/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.ts +++ b/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.ts @@ -18,14 +18,17 @@ import { Component, OnDestroy, OnInit, + ViewContainerRef, } from '@angular/core'; import { RiskAssessmentStore } from './risk-assessment.store'; import { SimpleDialogComponent } from '../../components/simple-dialog/simple-dialog.component'; -import { Subject, takeUntil } from 'rxjs'; +import { Subject, takeUntil, timer } from 'rxjs'; import { MatDialog } from '@angular/material/dialog'; import { LiveAnnouncer } from '@angular/cdk/a11y'; -import { Profile } from '../../model/profile'; +import { Profile, ProfileStatus } from '../../model/profile'; import { Observable } from 'rxjs/internal/Observable'; +import { DeviceValidators } from '../devices/components/device-form/device.validators'; +import { SuccessDialogComponent } from './components/success-dialog/success-dialog.component'; @Component({ selector: 'app-risk-assessment', @@ -37,11 +40,13 @@ import { Observable } from 'rxjs/internal/Observable'; export class RiskAssessmentComponent implements OnInit, OnDestroy { viewModel$ = this.store.viewModel$; isOpenProfileForm = false; + isCopyProfile = false; private destroy$: Subject = new Subject(); constructor( private store: RiskAssessmentStore, public dialog: MatDialog, - private liveAnnouncer: LiveAnnouncer + private liveAnnouncer: LiveAnnouncer, + public element: ViewContainerRef ) {} ngOnInit() { @@ -53,6 +58,12 @@ export class RiskAssessmentComponent implements OnInit, OnDestroy { this.destroy$.unsubscribe(); } + async profileClicked(profile: Profile | null = null) { + if (profile === null || profile.status !== ProfileStatus.EXPIRED) { + await this.openForm(profile); + } + } + async openForm(profile: Profile | null = null) { this.isOpenProfileForm = true; this.store.updateSelectedProfile(profile); @@ -60,21 +71,41 @@ export class RiskAssessmentComponent implements OnInit, OnDestroy { this.store.setFocusOnProfileForm(); } + async copyProfileAndOpenForm(profile: Profile) { + this.isCopyProfile = true; + await this.openForm(this.getCopyOfProfile(profile)); + } + + getCopyOfProfile(profile: Profile): Profile { + const copyOfProfile = { ...profile }; + copyOfProfile.name = this.getCopiedProfileName(profile.name); + delete copyOfProfile.created; // new profile is not create yet + return copyOfProfile; + } + + private getCopiedProfileName(name: string): string { + name = `Copy of ${name}`; + if (name.length > DeviceValidators.STRING_FORMAT_MAX_LENGTH) { + name = + name.substring(0, DeviceValidators.STRING_FORMAT_MAX_LENGTH - 3) + + '...'; + } + return name; + } + deleteProfile( profileName: string, index: number, selectedProfile: Profile | null ): void { const dialogRef = this.dialog.open(SimpleDialogComponent, { - ariaLabel: 'Delete risk profile', data: { title: 'Delete risk profile?', content: `You are about to delete ${profileName}. Are you sure?`, }, - autoFocus: true, + autoFocus: 'dialog', hasBackdrop: true, disableClose: true, - panelClass: 'simple-dialog', }); dialogRef @@ -85,28 +116,95 @@ export class RiskAssessmentComponent implements OnInit, OnDestroy { this.store.deleteProfile(profileName); this.closeFormAfterDelete(profileName, selectedProfile); this.setFocus(index); + } else { + this.store.setFocusOnSelectedProfile(); } }); } saveProfileClicked(profile: Profile, selectedProfile: Profile | null): void { + this.liveAnnouncer.clear(); if (!selectedProfile) { - this.saveProfile(profile); - this.store.setFocusOnCreateButton(); + this.saveProfile(profile, this.store.setFocusOnCreateButton); + } else if ( + this.compareProfiles(profile, selectedProfile) || + this.isCopyProfile + ) { + this.saveProfile(profile, this.store.setFocusOnSelectedProfile); } else { - this.openSaveDialog(selectedProfile.name) + this.openSaveDialog( + selectedProfile.name, + profile.status === ProfileStatus.DRAFT + ) .pipe(takeUntil(this.destroy$)) .subscribe(saveProfile => { if (saveProfile) { - this.saveProfile(profile); - this.store.setFocusOnSelectedProfile(); + this.saveProfile(profile, this.store.setFocusOnSelectedProfile); } }); } } - trackByIndex = (index: number): number => { - return index; + private compareProfiles(profile1: Profile, profile2: Profile) { + if (profile1.name !== profile2.name) { + return false; + } + if ( + profile1.rename && + (profile1.rename !== profile1.name || profile1.rename !== profile2.name) + ) { + return false; + } + if (profile1.status !== profile2.status) { + return false; + } + + for (const question of profile1.questions) { + const answer1 = question.answer; + const answer2 = profile2.questions?.find( + question2 => question2.question === question.question + )?.answer; + if (answer1 !== undefined && answer2 !== undefined) { + if (typeof question.answer === 'string') { + if (answer1 !== answer2) { + return false; + } + } else { + //the type of answer is array + if (answer1?.length !== answer2?.length) { + return false; + } + if ( + (answer1 as number[]).some( + answer => !(answer2 as number[]).includes(answer) + ) + ) + return false; + } + } else { + return !!answer1 == !!answer2; + } + } + + return true; + } + + discard(selectedProfile: Profile | null) { + this.liveAnnouncer.clear(); + this.isOpenProfileForm = false; + this.isCopyProfile = false; + if (selectedProfile) { + timer(100).subscribe(() => { + this.store.setFocusOnSelectedProfile(); + this.store.updateSelectedProfile(null); + }); + } else { + this.store.setFocusOnCreateButton(); + } + } + + trackByName = (index: number, item: Profile): string => { + return item.name; }; private closeFormAfterDelete(name: string, selectedProfile: Profile | null) { @@ -116,9 +214,19 @@ export class RiskAssessmentComponent implements OnInit, OnDestroy { } } - private saveProfile(profile: Profile) { - this.store.saveProfile(profile); + private saveProfile(profile: Profile, focusElement: () => void) { + this.store.saveProfile({ + profile, + onSave: (profile: Profile) => { + if (profile.status === ProfileStatus.VALID) { + this.openSuccessDialog(profile, focusElement); + } else { + focusElement(); + } + }, + }); this.isOpenProfileForm = false; + this.isCopyProfile = false; } private setFocus(index: number): void { @@ -132,19 +240,40 @@ export class RiskAssessmentComponent implements OnInit, OnDestroy { this.store.setFocus({ nextItem, firstItem }); } - private openSaveDialog(profileName: string): Observable { + private openSaveDialog( + profileName: string, + draft: boolean = false + ): Observable { const dialogRef = this.dialog.open(SimpleDialogComponent, { - ariaLabel: 'Save changes', data: { - title: 'Save changes', + title: `Save ${draft ? 'draft profile' : 'profile'}`, content: `You are about to save changes in ${profileName}. Are you sure?`, }, + autoFocus: 'dialog', + hasBackdrop: true, + disableClose: true, + }); + + return dialogRef?.afterClosed(); + } + + private openSuccessDialog(profile: Profile, focusElement: () => void): void { + const dialogRef = this.dialog.open(SuccessDialogComponent, { + ariaLabel: 'Risk Assessment Profile Completed', + data: { + profile, + }, autoFocus: true, hasBackdrop: true, disableClose: true, panelClass: 'simple-dialog', }); - return dialogRef?.afterClosed(); + dialogRef + ?.afterClosed() + .pipe(takeUntil(this.destroy$)) + .subscribe(() => { + focusElement(); + }); } } diff --git a/modules/ui/src/app/pages/risk-assessment/risk-assessment.store.spec.ts b/modules/ui/src/app/pages/risk-assessment/risk-assessment.store.spec.ts index 3bc123929..5bd264641 100644 --- a/modules/ui/src/app/pages/risk-assessment/risk-assessment.store.spec.ts +++ b/modules/ui/src/app/pages/risk-assessment/risk-assessment.store.spec.ts @@ -29,7 +29,7 @@ import { import { FocusManagerService } from '../../services/focus-manager.service'; import { AppState } from '../../store/state'; import { selectRiskProfiles } from '../../store/selectors'; -import { fetchRiskProfiles, setRiskProfiles } from '../../store/actions'; +import { setRiskProfiles } from '../../store/actions'; describe('RiskAssessmentStore', () => { let riskAssessmentStore: RiskAssessmentStore; @@ -128,33 +128,35 @@ describe('RiskAssessmentStore', () => { const mockFirstItem = document.createElement('section') as HTMLElement; const mockNullEL = window.document.querySelector(`.mock`) as HTMLElement; - it('should set focus to the next profile item when available', () => { + it('should set focus to the next profile item when available', fakeAsync(() => { const mockData = { nextItem: mockNextItem, firstItem: mockFirstItem, }; riskAssessmentStore.setFocus(mockData); + tick(100); expect( mockFocusManagerService.focusFirstElementInContainer ).toHaveBeenCalledWith(mockNextItem); - }); + })); - it('should set focus to the first profile item when available and no next item', () => { + it('should set focus to the first profile item when available and no next item', fakeAsync(() => { const mockData = { nextItem: mockNullEL, firstItem: mockFirstItem, }; riskAssessmentStore.setFocus(mockData); + tick(100); expect( mockFocusManagerService.focusFirstElementInContainer ).toHaveBeenCalledWith(mockFirstItem); - }); + })); - it('should set focus to the first element in the main when no items', () => { + it('should set focus to the first element in the main when no items', fakeAsync(() => { const mockData = { nextItem: mockNullEL, firstItem: mockFirstItem, @@ -164,11 +166,12 @@ describe('RiskAssessmentStore', () => { store.refreshState(); riskAssessmentStore.setFocus(mockData); + tick(100); expect( mockFocusManagerService.focusFirstElementInContainer ).toHaveBeenCalledWith(); - }); + })); }); describe('setFocusOnCreateButton', () => { @@ -230,10 +233,18 @@ describe('RiskAssessmentStore', () => { }); describe('saveProfile', () => { - it('should dispatch fetchRiskProfiles', () => { - riskAssessmentStore.saveProfile(NEW_PROFILE_MOCK); + it('should dispatch setRiskProfiles', () => { + const onSave = jasmine.createSpy('onSave'); + mockService.fetchProfiles.and.returnValue(of([NEW_PROFILE_MOCK])); + riskAssessmentStore.saveProfile({ + profile: NEW_PROFILE_MOCK, + onSave, + }); - expect(store.dispatch).toHaveBeenCalledWith(fetchRiskProfiles()); + expect(store.dispatch).toHaveBeenCalledWith( + setRiskProfiles({ riskProfiles: [NEW_PROFILE_MOCK] }) + ); + expect(onSave).toHaveBeenCalled(); }); }); }); diff --git a/modules/ui/src/app/pages/risk-assessment/risk-assessment.store.ts b/modules/ui/src/app/pages/risk-assessment/risk-assessment.store.ts index 93e89b434..40a8c523a 100644 --- a/modules/ui/src/app/pages/risk-assessment/risk-assessment.store.ts +++ b/modules/ui/src/app/pages/risk-assessment/risk-assessment.store.ts @@ -17,14 +17,14 @@ import { Injectable } from '@angular/core'; import { ComponentStore } from '@ngrx/component-store'; import { tap, withLatestFrom } from 'rxjs/operators'; -import { delay, exhaustMap } from 'rxjs'; +import { catchError, delay, EMPTY, exhaustMap, throwError, timer } from 'rxjs'; import { TestRunService } from '../../services/test-run.service'; import { Profile, ProfileFormat } from '../../model/profile'; import { FocusManagerService } from '../../services/focus-manager.service'; import { Store } from '@ngrx/store'; import { AppState } from '../../store/state'; import { selectRiskProfiles } from '../../store/selectors'; -import { fetchRiskProfiles, setRiskProfiles } from '../../store/actions'; +import { setRiskProfiles } from '../../store/actions'; export interface AppComponentState { selectedProfile: Profile | null; @@ -76,13 +76,15 @@ export class RiskAssessmentStore extends ComponentStore { return trigger$.pipe( withLatestFrom(this.profiles$), tap(([{ nextItem, firstItem }, profiles]) => { - if (nextItem) { - this.focusManagerService.focusFirstElementInContainer(nextItem); - } else if (profiles.length > 1) { - this.focusManagerService.focusFirstElementInContainer(firstItem); - } else { - this.focusManagerService.focusFirstElementInContainer(); - } + timer(100).subscribe(() => { + if (nextItem) { + this.focusManagerService.focusFirstElementInContainer(nextItem); + } else if (profiles.length > 1) { + this.focusManagerService.focusFirstElementInContainer(firstItem); + } else { + this.focusManagerService.focusFirstElementInContainer(); + } + }); }) ); } @@ -131,14 +133,30 @@ export class RiskAssessmentStore extends ComponentStore { ); }); - saveProfile = this.effect(trigger$ => { + saveProfile = this.effect<{ + profile: Profile; + onSave: ((profile: Profile) => void) | undefined; + }>(trigger$ => { return trigger$.pipe( - exhaustMap((name: Profile) => { - return this.testRunService.saveProfile(name).pipe( - tap(saved => { + exhaustMap(({ profile, onSave }) => { + return this.testRunService.saveProfile(profile).pipe( + exhaustMap(saved => { if (saved) { - this.store.dispatch(fetchRiskProfiles()); + return this.testRunService.fetchProfiles(); + } + return throwError('Failed to upload profile'); + }), + tap(newProfiles => { + this.store.dispatch(setRiskProfiles({ riskProfiles: newProfiles })); + const uploadedProfile = newProfiles.find( + p => p.name === profile.name || p.name === profile.rename + ); + if (onSave && uploadedProfile) { + onSave(uploadedProfile); } + }), + catchError(() => { + return EMPTY; }) ); }) diff --git a/modules/ui/src/app/pages/settings/components/settings-dropdown/settings-dropdown.component.scss b/modules/ui/src/app/pages/settings/components/settings-dropdown/settings-dropdown.component.scss index 16093e336..38e82686c 100644 --- a/modules/ui/src/app/pages/settings/components/settings-dropdown/settings-dropdown.component.scss +++ b/modules/ui/src/app/pages/settings/components/settings-dropdown/settings-dropdown.component.scss @@ -48,6 +48,11 @@ .setting-field { width: 100%; + + &.mat-form-field-disabled { + opacity: 0.6; + } + ::ng-deep .mat-mdc-form-field-infix { min-height: 76px; display: flex; diff --git a/modules/ui/src/app/pages/settings/settings.component.html b/modules/ui/src/app/pages/settings/settings.component.html index 36849b42e..9a7671171 100644 --- a/modules/ui/src/app/pages/settings/settings.component.html +++ b/modules/ui/src/app/pages/settings/settings.component.html @@ -34,13 +34,6 @@

System settings

- - Warning! Testrun requires two ports to operate correctly. - - System settings - Warning! No ports is detected. + Warning! No ports detected. diff --git a/modules/ui/src/app/pages/settings/settings.component.scss b/modules/ui/src/app/pages/settings/settings.component.scss index 770d79a52..6596a3591 100644 --- a/modules/ui/src/app/pages/settings/settings.component.scss +++ b/modules/ui/src/app/pages/settings/settings.component.scss @@ -146,3 +146,10 @@ } } } + +.settings-drawer-header-button:not(.mat-mdc-button-disabled), +.close-button:not(.mat-mdc-button-disabled), +.save-button:not(.mat-mdc-button-disabled) { + cursor: pointer; + pointer-events: auto; +} diff --git a/modules/ui/src/app/pages/settings/settings.component.spec.ts b/modules/ui/src/app/pages/settings/settings.component.spec.ts index d11a30d54..01da4b9c3 100644 --- a/modules/ui/src/app/pages/settings/settings.component.spec.ts +++ b/modules/ui/src/app/pages/settings/settings.component.spec.ts @@ -33,7 +33,6 @@ import { LoaderService } from '../../services/loader.service'; import { SettingsStore } from './settings.store'; import { MOCK_INTERFACES, - MOCK_INTERNET_OPTIONS, MOCK_SYSTEM_CONFIG_WITH_DATA, } from '../../mocks/settings.mock'; import { SettingsDropdownComponent } from './components/settings-dropdown/settings-dropdown.component'; @@ -299,12 +298,6 @@ describe('GeneralSettingsComponent', () => { fixture.detectChanges(); }); - it('should have callout component', () => { - const callout = compiled.querySelector('app-callout'); - - expect(callout).toBeTruthy(); - }); - it('should have disabled "Save" button', () => { component.deviceControl.setValue( MOCK_SYSTEM_CONFIG_WITH_DATA?.network?.device_intf @@ -331,7 +324,7 @@ describe('GeneralSettingsComponent', () => { isLessThanOneInterface: false, interfaces: MOCK_INTERFACES, deviceOptions: MOCK_INTERFACES, - internetOptions: MOCK_INTERNET_OPTIONS, + internetOptions: MOCK_INTERFACES, logLevelOptions: {}, monitoringPeriodOptions: {}, }); diff --git a/modules/ui/src/app/pages/settings/settings.component.ts b/modules/ui/src/app/pages/settings/settings.component.ts index 1f9b4f62a..4c9d0dffe 100644 --- a/modules/ui/src/app/pages/settings/settings.component.ts +++ b/modules/ui/src/app/pages/settings/settings.component.ts @@ -52,7 +52,11 @@ export class SettingsComponent implements OnInit, OnDestroy { } @Input() set settingsDisable(value: boolean) { this.isSettingsDisable = value; - value ? this.disableSettings() : this.enableSettings(); + if (value) { + this.disableSettings(); + } else { + this.enableSettings(); + } } public readonly CalloutType = CalloutType; public readonly EventType = EventType; @@ -79,7 +83,14 @@ export class SettingsComponent implements OnInit, OnDestroy { } get isFormValues(): boolean { - return this.internetControl?.value.value && this.deviceControl?.value.value; + return ( + this.deviceControl?.value?.value && + (this.isInternetControlDisabled || this.internetControl?.value?.value) + ); + } + + get isInternetControlDisabled(): boolean { + return this.internetControl?.disabled; } get isFormError(): boolean { @@ -98,7 +109,7 @@ export class SettingsComponent implements OnInit, OnDestroy { this.createSettingForm(); this.cleanFormErrorMessage(); this.settingsStore.getInterfaces(); - this.settingsStore.getSystemConfig(); + this.getSystemConfig(); this.setDefaultFormValues(); } @@ -108,7 +119,7 @@ export class SettingsComponent implements OnInit, OnDestroy { } this.showLoading(); this.getSystemInterfaces(); - this.settingsStore.getSystemConfig(); + this.getSystemConfig(); this.setDefaultFormValues(); } closeSetting(message: string): void { @@ -173,7 +184,7 @@ export class SettingsComponent implements OnInit, OnDestroy { const data: SystemConfig = { network: { device_intf: device_intf.key, - internet_intf: internet_intf.key, + internet_intf: this.isInternetControlDisabled ? '' : internet_intf.key, }, log_level: log_level.key, monitor_period: Number(monitor_period.key), diff --git a/modules/ui/src/app/pages/settings/settings.store.spec.ts b/modules/ui/src/app/pages/settings/settings.store.spec.ts index 669faef98..969f5cb9f 100644 --- a/modules/ui/src/app/pages/settings/settings.store.spec.ts +++ b/modules/ui/src/app/pages/settings/settings.store.spec.ts @@ -13,33 +13,32 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { - DEFAULT_INTERNET_OPTION, - LOG_LEVELS, - MONITORING_PERIOD, - SettingsStore, -} from './settings.store'; +import { LOG_LEVELS, MONITORING_PERIOD, SettingsStore } from './settings.store'; import { TestRunService } from '../../services/test-run.service'; import SpyObj = jasmine.SpyObj; import { TestBed } from '@angular/core/testing'; import { MockStore, provideMockStore } from '@ngrx/store/testing'; import { AppState } from '../../store/state'; import { skip, take } from 'rxjs'; -import { selectHasConnectionSettings } from '../../store/selectors'; +import { + selectAdapters, + selectHasConnectionSettings, +} from '../../store/selectors'; import { of } from 'rxjs/internal/observable/of'; import { fetchSystemConfigSuccess } from '../../store/actions'; import { fetchInterfacesSuccess } from '../../store/actions'; import { FormBuilder, FormControl } from '@angular/forms'; import { FormKey, SystemConfig } from '../../model/setting'; import { + MOCK_ADAPTERS, MOCK_DEVICE_VALUE, MOCK_INTERFACE_VALUE, MOCK_INTERFACES, - MOCK_INTERNET_OPTIONS, MOCK_LOG_VALUE, MOCK_PERIOD_VALUE, MOCK_SYSTEM_CONFIG_WITH_DATA, MOCK_SYSTEM_CONFIG_WITH_NO_DATA, + MOCK_SYSTEM_CONFIG_WITH_SINGLE_PORT, } from '../../mocks/settings.mock'; describe('SettingsStore', () => { @@ -60,7 +59,10 @@ describe('SettingsStore', () => { SettingsStore, { provide: TestRunService, useValue: mockService }, provideMockStore({ - selectors: [{ selector: selectHasConnectionSettings, value: true }], + selectors: [ + { selector: selectHasConnectionSettings, value: true }, + { selector: selectAdapters, value: {} }, + ], }), FormBuilder, ], @@ -113,7 +115,6 @@ describe('SettingsStore', () => { expect(store.interfaces).toEqual(MOCK_INTERFACES); expect(store.deviceOptions).toEqual(MOCK_INTERFACES); expect(store.internetOptions).toEqual({ - '': 'Not specified', mockDeviceKey: 'mockDeviceValue', mockInternetKey: 'mockInternetValue', }); @@ -186,7 +187,7 @@ describe('SettingsStore', () => { settingsStore.viewModel$.pipe(skip(3), take(1)).subscribe(store => { expect(store.interfaces).toEqual(interfaces); expect(store.deviceOptions).toEqual(interfaces); - expect(store.internetOptions).toEqual(MOCK_INTERNET_OPTIONS); + expect(store.internetOptions).toEqual(interfaces); done(); }); @@ -272,6 +273,27 @@ describe('SettingsStore', () => { }); }); + describe('with single port mode', () => { + beforeEach(() => { + settingsStore.setSystemConfig(MOCK_SYSTEM_CONFIG_WITH_SINGLE_PORT); + settingsStore.setInterfaces(MOCK_INTERFACES); + }); + + it('should disable internet control', () => { + const form = fb.group({ + device_intf: ['value'], + internet_intf: [''], + log_level: [''], + monitor_period: ['value'], + }); + settingsStore.setDefaultFormValues(form); + + expect( + (form.get(FormKey.INTERNET) as FormControl).disabled + ).toBeTrue(); + }); + }); + describe('when values are empty', () => { beforeEach(() => { settingsStore.setSystemConfig(MOCK_SYSTEM_CONFIG_WITH_NO_DATA); @@ -293,7 +315,7 @@ describe('SettingsStore', () => { }); expect((form.get(FormKey.INTERNET) as FormControl).value).toEqual({ key: '', - value: DEFAULT_INTERNET_OPTION[''], + value: undefined, }); expect((form.get(FormKey.LOG_LEVEL) as FormControl).value).toEqual({ key: 'INFO', @@ -308,5 +330,38 @@ describe('SettingsStore', () => { }); }); }); + + describe('adaptersUpdate', () => { + const updateInterfaces = { + mockDeviceKey: 'mockDeviceValue', + mockNewInternetKey: 'mockNewInternetValue', + }; + const updateInternetOptions = { + mockDeviceKey: 'mockDeviceValue', + mockNewInternetKey: 'mockNewInternetValue', + }; + + beforeEach(() => { + settingsStore.setInterfaces(MOCK_INTERFACES); + }); + + it('should update store', done => { + settingsStore.viewModel$ + .pipe(skip(3), take(1)) + .subscribe(storeValue => { + expect(storeValue.interfaces).toEqual(updateInterfaces); + expect(storeValue.deviceOptions).toEqual(updateInterfaces); + expect(storeValue.internetOptions).toEqual(updateInternetOptions); + + expect(store.dispatch).toHaveBeenCalledWith( + fetchInterfacesSuccess({ interfaces: updateInterfaces }) + ); + done(); + }); + + store.overrideSelector(selectAdapters, MOCK_ADAPTERS); + store.refreshState(); + }); + }); }); }); diff --git a/modules/ui/src/app/pages/settings/settings.store.ts b/modules/ui/src/app/pages/settings/settings.store.ts index f489228a9..87071b222 100644 --- a/modules/ui/src/app/pages/settings/settings.store.ts +++ b/modules/ui/src/app/pages/settings/settings.store.ts @@ -23,12 +23,15 @@ import { SystemConfig, SystemInterfaces, } from '../../model/setting'; -import { exhaustMap, switchMap, Observable } from 'rxjs'; +import { exhaustMap, switchMap, Observable, skip } from 'rxjs'; import { tap, withLatestFrom } from 'rxjs/operators'; import * as AppActions from '../../store/actions'; import { Store } from '@ngrx/store'; import { AppState } from '../../store/state'; -import { selectHasConnectionSettings } from '../../store/selectors'; +import { + selectAdapters, + selectHasConnectionSettings, +} from '../../store/selectors'; import { FormControl, FormGroup } from '@angular/forms'; export interface SettingsComponentState { @@ -43,10 +46,6 @@ export interface SettingsComponentState { monitoringPeriodOptions: SystemInterfaces; } -export const DEFAULT_INTERNET_OPTION = { - '': 'Not specified', -}; - export const LOG_LEVELS = { DEBUG: 'Every event will be logged', INFO: 'Normal events and issues', @@ -75,6 +74,8 @@ export class SettingsStore extends ComponentStore { private hasConnectionSettings$ = this.store.select( selectHasConnectionSettings ); + + private adapters$ = this.store.select(selectAdapters); private isSubmitting$ = this.select(state => state.isSubmitting); private isLessThanOneInterfaces$ = this.select( state => state.isLessThanOneInterface @@ -108,26 +109,22 @@ export class SettingsStore extends ComponentStore { isSubmitting, })); - setInterfaces = this.updater((state, interfaces: SystemInterfaces) => ({ - ...state, - interfaces, - deviceOptions: interfaces, - internetOptions: { - ...DEFAULT_INTERNET_OPTION, - ...interfaces, - }, - isLessThanOneInterface: Object.keys(interfaces).length < 1, - })); + setInterfaces = this.updater((state, interfaces: SystemInterfaces) => { + return { + ...state, + interfaces, + deviceOptions: interfaces, + internetOptions: interfaces, + isLessThanOneInterface: Object.keys(interfaces).length < 1, + }; + }); getInterfaces = this.effect(trigger$ => { return trigger$.pipe( exhaustMap(() => { return this.testRunService.getSystemInterfaces().pipe( tap((interfaces: SystemInterfaces) => { - this.store.dispatch( - AppActions.fetchInterfacesSuccess({ interfaces }) - ); - this.setInterfaces(interfaces); + this.updateInterfaces(interfaces); }) ); }) @@ -176,16 +173,21 @@ export class SettingsStore extends ComponentStore { this.systemConfig$.pipe( withLatestFrom(this.deviceOptions$, this.internetOptions$), tap(([config, deviceOptions, internetOptions]) => { + if (config.single_intf) { + this.disableInternetInterface(formGroup); + } else { + this.setDefaultInternetInterfaceValue( + config.network?.internet_intf, + internetOptions, + formGroup + ); + } + this.setDefaultDeviceInterfaceValue( config.network?.device_intf, deviceOptions, formGroup ); - this.setDefaultInternetInterfaceValue( - config.network?.internet_intf, - internetOptions, - formGroup - ); this.setDefaultLogLevelValue( config.log_level, LOG_LEVELS, @@ -202,6 +204,48 @@ export class SettingsStore extends ComponentStore { ); }); + adaptersUpdate = this.effect(() => { + return this.adapters$.pipe( + skip(1), + withLatestFrom(this.interfaces$), + tap(([adapters, interfaces]) => { + const updatedInterfaces = { ...interfaces }; + if (adapters.adapters_added) { + this.addInterfaces(adapters.adapters_added, updatedInterfaces); + } + if (adapters.adapters_removed) { + this.removeInterfaces(adapters.adapters_removed, updatedInterfaces); + } + this.updateInterfaces(updatedInterfaces); + }) + ); + }); + + private updateInterfaces(interfaces: SystemInterfaces) { + this.store.dispatch( + AppActions.fetchInterfacesSuccess({ interfaces: interfaces }) + ); + this.setInterfaces(interfaces); + } + + private addInterfaces( + newInterfaces: SystemInterfaces, + interfaces: SystemInterfaces + ): void { + for (const [key, value] of Object.entries(newInterfaces)) { + interfaces[key] = value; + } + } + + private removeInterfaces( + interfacesToDelete: SystemInterfaces, + interfaces: SystemInterfaces + ): void { + for (const key of Object.keys(interfacesToDelete)) { + delete interfaces[key]; + } + } + private setDefaultDeviceInterfaceValue( value: string | undefined, options: { [key: string]: string }, @@ -254,6 +298,11 @@ export class SettingsStore extends ComponentStore { ); } + private disableInternetInterface(formGroup: FormGroup) { + const internetControl = formGroup.get(FormKey.INTERNET) as FormControl; + internetControl.disable(); + } + private setDefaultValue( value: string | undefined, defaultValue: string | undefined, diff --git a/modules/ui/src/app/pages/testrun/components/download-options/download-options.component.scss b/modules/ui/src/app/pages/testrun/components/download-options/download-options.component.scss index cf368929d..a08461b6d 100644 --- a/modules/ui/src/app/pages/testrun/components/download-options/download-options.component.scss +++ b/modules/ui/src/app/pages/testrun/components/download-options/download-options.component.scss @@ -22,7 +22,7 @@ $option-height: 36px; .download-options-field { width: $option-width; - background: mat.get-color-from-palette($color-primary, 50); + background: mat.m2-get-color-from-palette($color-primary, 50); ::ng-deep.mat-mdc-text-field-wrapper { padding: 0 16px; @@ -39,7 +39,7 @@ $option-height: 36px; } ::ng-deep.mat-mdc-select-placeholder { - color: mat.get-color-from-palette($color-primary, 700); + color: mat.m2-get-color-from-palette($color-primary, 700); font-size: 14px; font-style: normal; font-weight: 500; @@ -48,11 +48,11 @@ $option-height: 36px; } ::ng-deep.mat-mdc-select-arrow { - color: mat.get-color-from-palette($color-primary, 700); + color: mat.m2-get-color-from-palette($color-primary, 700); } ::ng-deep .mat-mdc-text-field-wrapper .mdc-notched-outline > * { - border-color: mat.get-color-from-palette($color-primary, 50); + border-color: mat.m2-get-color-from-palette($color-primary, 50); } ::ng-deep diff --git a/modules/ui/src/app/pages/testrun/components/download-options/download-options.component.ts b/modules/ui/src/app/pages/testrun/components/download-options/download-options.component.ts index fddd6822f..81b4a1fee 100644 --- a/modules/ui/src/app/pages/testrun/components/download-options/download-options.component.ts +++ b/modules/ui/src/app/pages/testrun/components/download-options/download-options.component.ts @@ -97,6 +97,9 @@ export class DownloadOptionsComponent { } getReportTitle(data: TestrunStatus) { + if (!data.device) { + return ''; + } return `${data.device.manufacturer} ${data.device.model} ${ data.device.firmware } ${data.status} ${this.getFormattedDateString(data.started)}` diff --git a/modules/ui/src/app/pages/testrun/components/testrun-initiate-form/testrun-initiate-form.component.html b/modules/ui/src/app/pages/testrun/components/testrun-initiate-form/testrun-initiate-form.component.html index c576d242e..6caab0423 100644 --- a/modules/ui/src/app/pages/testrun/components/testrun-initiate-form/testrun-initiate-form.component.html +++ b/modules/ui/src/app/pages/testrun/components/testrun-initiate-form/testrun-initiate-form.component.html @@ -22,6 +22,7 @@ { const testRunServiceMock = jasmine.createSpyObj([ 'getDevices', 'fetchDevices', - 'getTestModules', 'startTestrun', 'systemStatus$', 'getSystemStatus', 'fetchVersion', 'setIsOpenStartTestrun', ]); - testRunServiceMock.getTestModules.and.returnValue([ - { - displayName: 'Connection', - name: 'connection', - enabled: true, - }, - { - displayName: 'DNS', - name: 'dns', - enabled: false, - }, - ]); testRunServiceMock.getDevices.and.returnValue( new BehaviorSubject([device, device]) ); @@ -81,7 +73,10 @@ describe('ProgressInitiateFormComponent', () => { close: () => ({}), }, }, - { provide: MAT_DIALOG_DATA, useValue: {} }, + { + provide: MAT_DIALOG_DATA, + useValue: { testModules: MOCK_TEST_MODULES }, + }, provideMockStore({ selectors: [{ selector: selectDevices, value: [device, device] }], }), @@ -206,6 +201,7 @@ describe('ProgressInitiateFormComponent', () => { component.startTestRun(); expect(testRunServiceMock.startTestrun).toHaveBeenCalledWith({ + status: DeviceStatus.VALID, manufacturer: 'Delta', model: 'O3-DIN-CPU', mac_addr: '00:1e:42:35:73:c4', @@ -214,7 +210,7 @@ describe('ProgressInitiateFormComponent', () => { connection: { enabled: true, }, - dns: { + udmi: { enabled: true, }, }, @@ -240,7 +236,7 @@ describe('ProgressInitiateFormComponent', () => { expect(buttonSpy).toHaveBeenCalled(); }); - it('should focus firmware', () => { + it('should focus firmware', fakeAsync(() => { component.selectedDevice = device; component.setFirmwareFocus = true; const firmwareSpy = spyOn( @@ -249,9 +245,10 @@ describe('ProgressInitiateFormComponent', () => { ); component.ngAfterViewChecked(); fixture.detectChanges(); + tick(100); expect(firmwareSpy).toHaveBeenCalled(); - }); + })); }); it('should focus element on focusButton ', () => { @@ -313,12 +310,6 @@ describe('ProgressInitiateFormComponent', () => { expect(deviceItem).toBeTruthy(); }); - it('should have tabindex -1 for device item', () => { - const deviceItem = compiled.querySelector('app-device-item button'); - - expect((deviceItem as HTMLElement).tabIndex).toBe(-1); - }); - it('should display firmware if device selected', () => { const firmware = compiled.querySelector('input'); diff --git a/modules/ui/src/app/pages/testrun/components/testrun-initiate-form/testrun-initiate-form.component.ts b/modules/ui/src/app/pages/testrun/components/testrun-initiate-form/testrun-initiate-form.component.ts index c1a2afb92..cbec35a0a 100644 --- a/modules/ui/src/app/pages/testrun/components/testrun-initiate-form/testrun-initiate-form.component.ts +++ b/modules/ui/src/app/pages/testrun/components/testrun-initiate-form/testrun-initiate-form.component.ts @@ -24,7 +24,12 @@ import { } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { TestRunService } from '../../../../services/test-run.service'; -import { Device, TestModule, DeviceView } from '../../../../model/device'; +import { + Device, + TestModule, + DeviceStatus, + DeviceView, +} from '../../../../model/device'; import { AbstractControl, FormArray, @@ -33,7 +38,7 @@ import { } from '@angular/forms'; import { DeviceValidators } from '../../../devices/components/device-form/device.validators'; import { EscapableDialogComponent } from '../../../../components/escapable-dialog/escapable-dialog.component'; -import { take } from 'rxjs'; +import { take, timer } from 'rxjs'; import { Store } from '@ngrx/store'; import { AppState } from '../../../../store/state'; import { selectDevices } from '../../../../store/selectors'; @@ -42,6 +47,7 @@ import { TestrunStatus } from '../../../../model/testrun-status'; interface DialogData { device?: Device; + testModules: TestModule[]; } @Component({ @@ -60,6 +66,7 @@ export class TestrunInitiateFormComponent testModules: TestModule[] = []; prevDevice: Device | null = null; setFirmwareFocus = false; + readonly DeviceStatus = DeviceStatus; readonly DeviceView = DeviceView; error$: BehaviorSubject = new BehaviorSubject( null @@ -91,7 +98,7 @@ export class TestrunInitiateFormComponent ngOnInit() { this.createInitiateForm(); - this.testModules = this.testRunService.getTestModules(); + this.testModules = this.data?.testModules; if (this.data?.device) { this.deviceSelected(this.data.device); @@ -120,9 +127,11 @@ export class TestrunInitiateFormComponent this.changeDetectorRef.detectChanges(); } if (this.setFirmwareFocus) { - this.firmwareInput?.nativeElement.focus(); - this.setFirmwareFocus = false; - this.changeDetectorRef.detectChanges(); + timer(100).subscribe(() => { + this.firmwareInput?.nativeElement.focus(); + this.setFirmwareFocus = false; + this.changeDetectorRef.detectChanges(); + }); } } diff --git a/modules/ui/src/app/pages/testrun/components/testrun-status-card/testrun-status-card.component.html b/modules/ui/src/app/pages/testrun/components/testrun-status-card/testrun-status-card.component.html index b94c0d55a..be4d3b259 100644 --- a/modules/ui/src/app/pages/testrun/components/testrun-status-card/testrun-status-card.component.html +++ b/modules/ui/src/app/pages/testrun/components/testrun-status-card/testrun-status-card.component.html @@ -40,7 +40,7 @@ *ngTemplateOutlet=" InProgress; context: { - data: data + data: data, } "> @@ -53,7 +53,7 @@ *ngTemplateOutlet=" Finished; context: { - data: data + data: data, } "> @@ -64,7 +64,7 @@ *ngTemplateOutlet=" Finished; context: { - data: data + data: data, } "> @@ -73,7 +73,7 @@ *ngTemplateOutlet=" InProgress; context: { - data: data + data: data, } "> diff --git a/modules/ui/src/app/pages/testrun/components/testrun-status-card/testrun-status-card.component.scss b/modules/ui/src/app/pages/testrun/components/testrun-status-card/testrun-status-card.component.scss index ddf8a41b8..b7c86a91a 100644 --- a/modules/ui/src/app/pages/testrun/components/testrun-status-card/testrun-status-card.component.scss +++ b/modules/ui/src/app/pages/testrun/components/testrun-status-card/testrun-status-card.component.scss @@ -33,11 +33,11 @@ padding: 16px 32px; &.progress { - background-color: mat.get-color-from-palette($color-primary, 700); + background-color: mat.m2-get-color-from-palette($color-primary, 700); } &.completed-success { - background-color: mat.get-color-from-palette($color-accent, 700); + background-color: mat.m2-get-color-from-palette($color-accent, 700); } &.completed-failed { diff --git a/modules/ui/src/app/pages/testrun/components/testrun-status-card/testrun-status-card.component.spec.ts b/modules/ui/src/app/pages/testrun/components/testrun-status-card/testrun-status-card.component.spec.ts index 29fdd7731..2e35d0581 100644 --- a/modules/ui/src/app/pages/testrun/components/testrun-status-card/testrun-status-card.component.spec.ts +++ b/modules/ui/src/app/pages/testrun/components/testrun-status-card/testrun-status-card.component.spec.ts @@ -26,6 +26,7 @@ import { MOCK_PROGRESS_DATA_IN_PROGRESS, MOCK_PROGRESS_DATA_MONITORING, MOCK_PROGRESS_DATA_WAITING_FOR_DEVICE, + MOCK_PROGRESS_DATA_WITH_ERROR, } from '../../../../mocks/testrun.mock'; import { TestrunModule } from '../../testrun.module'; @@ -129,7 +130,7 @@ describe('ProgressStatusCardComponent', () => { }); it('should return correct test result if status "Compliant"', () => { - const expectedResult = '2/2'; + const expectedResult = '2/3'; const result = component.getTestsResult(MOCK_PROGRESS_DATA_COMPLIANT); @@ -144,6 +145,14 @@ describe('ProgressStatusCardComponent', () => { expect(result).toEqual(expectedResult); }); + it('should not include Error and Not Started status in completed test result', () => { + const expectedResult = '1/3'; + + const result = component.getTestsResult(MOCK_PROGRESS_DATA_WITH_ERROR); + + expect(result).toEqual(expectedResult); + }); + it('should return empty string if no data', () => { const expectedResult = ''; diff --git a/modules/ui/src/app/pages/testrun/components/testrun-status-card/testrun-status-card.component.ts b/modules/ui/src/app/pages/testrun/components/testrun-status-card/testrun-status-card.component.ts index 027588b24..d84b692e1 100644 --- a/modules/ui/src/app/pages/testrun/components/testrun-status-card/testrun-status-card.component.ts +++ b/modules/ui/src/app/pages/testrun/components/testrun-status-card/testrun-status-card.component.ts @@ -16,6 +16,7 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; import { IResult, + StatusOfTestResult, StatusOfTestrun, TestrunStatus, TestsData, @@ -65,11 +66,11 @@ export class TestrunStatusCardComponent { (data.tests as TestsData)?.results?.length && (data.tests as TestsData)?.total ) { - return `${(data.tests as TestsData)?.results?.length}/${ + return `${(data.tests as TestsData)?.results?.filter(result => result.result !== StatusOfTestResult.NotStarted && result.result !== StatusOfTestResult.Error).length}/${ (data.tests as TestsData)?.total }`; } else if ((data.tests as IResult[])?.length) { - return `${(data.tests as IResult[])?.length}/${ + return `${(data.tests as IResult[])?.filter(result => result.result !== StatusOfTestResult.NotStarted && result.result !== StatusOfTestResult.Error).length}/${ (data.tests as IResult[])?.length }`; } @@ -99,7 +100,13 @@ export class TestrunStatusCardComponent { const testData = data.tests as TestsData; if (testData && testData.total && testData.results?.length) { - return Math.round((testData.results.length / testData.total) * 100); + return Math.round( + (testData.results.filter( + result => result.result !== StatusOfTestResult.NotStarted + ).length / + testData.total) * + 100 + ); } return 0; } diff --git a/modules/ui/src/app/pages/testrun/components/testrun-table/testrun-table.component.scss b/modules/ui/src/app/pages/testrun/components/testrun-table/testrun-table.component.scss index 231df35cd..cc5bc118b 100644 --- a/modules/ui/src/app/pages/testrun/components/testrun-table/testrun-table.component.scss +++ b/modules/ui/src/app/pages/testrun/components/testrun-table/testrun-table.component.scss @@ -59,6 +59,7 @@ $expander-button-size: 28px; padding: 16px; letter-spacing: 0.2px; vertical-align: top; + font-weight: 400; } .tests-item-cell-result { diff --git a/modules/ui/src/app/pages/testrun/components/testrun-table/testrun-table.component.spec.ts b/modules/ui/src/app/pages/testrun/components/testrun-table/testrun-table.component.spec.ts index 60c325c3d..73b7196e8 100644 --- a/modules/ui/src/app/pages/testrun/components/testrun-table/testrun-table.component.spec.ts +++ b/modules/ui/src/app/pages/testrun/components/testrun-table/testrun-table.component.spec.ts @@ -55,6 +55,7 @@ describe('ProgressTableComponent', () => { green: false, red: true, blue: false, + cyan: false, grey: false, }; @@ -73,6 +74,14 @@ describe('ProgressTableComponent', () => { 'dns.network.hostname_resolutionCompliant' ); }); + + it('#getAriaLabel should return valid message', () => { + component.isAllCollapsed = true; + + const result = component.getAriaLabel(); + + expect(result).toEqual('Expand all rows'); + }); }); describe('DOM tests', () => { @@ -142,6 +151,12 @@ describe('ProgressTableComponent', () => { expect(button).not.toBeNull(); expect(button?.ariaLabel).toBe('Collapse row'); }); + + it('#checkAllCollapsed should return isAllCollapsed', () => { + component.checkAllCollapsed(); + + expect(component.isAllCollapsed).toBeFalse(); + }); }); }); }); diff --git a/modules/ui/src/app/pages/testrun/testrun.component.html b/modules/ui/src/app/pages/testrun/testrun.component.html index d485d1926..92f8cf958 100644 --- a/modules/ui/src/app/pages/testrun/testrun.component.html +++ b/modules/ui/src/app/pages/testrun/testrun.component.html @@ -30,16 +30,32 @@ startNewTestrunButton; context: { hasDevices: vm.hasDevices, - systemStatus: vm.systemStatus + systemStatus: vm.systemStatus, + isAllDevicesOutdated: vm.isAllDevicesOutdated, } ">
-

- {{ data.device.manufacturer }} {{ data.device.model }} v{{ - data.device.firmware - }} +

+ {{ getTestRunName(data) }}

+ + {{ tag }} +
@@ -89,6 +106,7 @@

diff --git a/modules/ui/src/app/pages/testrun/testrun.component.scss b/modules/ui/src/app/pages/testrun/testrun.component.scss index 41566da88..9e01bc725 100644 --- a/modules/ui/src/app/pages/testrun/testrun.component.scss +++ b/modules/ui/src/app/pages/testrun/testrun.component.scss @@ -15,6 +15,7 @@ */ @use 'node_modules/@angular/material/index' as mat; @import 'src/theming/colors'; +@import 'src/theming/variables'; :host { display: flex; @@ -24,6 +25,9 @@ } .progress-content-empty { + position: absolute; + top: 0; + width: 100%; height: 100%; display: flex; align-items: center; @@ -86,6 +90,32 @@ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + cursor: pointer; +} + +.toolbar-tag-container { + align-self: flex-start; + display: flex; + align-items: center; + gap: 6px; + margin-left: -10px; +} + +.toolbar-tag { + background-color: mat.m2-get-color-from-palette($color-primary, 800); + color: $white; + font-family: $font-primary; + font-size: 8px; + font-weight: 500; + height: 16px; + line-height: 16px; + letter-spacing: 0.6px; + text-align: center; + padding: 0 4px 0 4px; + border-radius: 2px; + &:hover { + cursor: pointer; + } } .report-button, @@ -96,8 +126,8 @@ } .report-button { - background-color: mat.get-color-from-palette($color-primary, 50); - color: mat.get-color-from-palette($color-primary, 700); + background-color: mat.m2-get-color-from-palette($color-primary, 50); + color: mat.m2-get-color-from-palette($color-primary, 700); & ::ng-deep .mat-mdc-button-touch-target { height: auto; } diff --git a/modules/ui/src/app/pages/testrun/testrun.component.spec.ts b/modules/ui/src/app/pages/testrun/testrun.component.spec.ts index 98d1b986f..197f51f8f 100644 --- a/modules/ui/src/app/pages/testrun/testrun.component.spec.ts +++ b/modules/ui/src/app/pages/testrun/testrun.component.spec.ts @@ -49,10 +49,12 @@ import { selectDevices, selectHasDevices, selectHasRiskProfiles, + selectIsAllDevicesOutdated, selectIsOpenStartTestrun, selectIsOpenWaitSnackBar, selectRiskProfiles, selectSystemStatus, + selectTestModules, } from '../../store/selectors'; import { TestrunStore } from './testrun.store'; import { @@ -62,6 +64,7 @@ import { import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { NotificationService } from '../../services/notification.service'; import { Profile } from '../../model/profile'; +import { MatTooltipModule } from '@angular/material/tooltip'; describe('TestrunComponent', () => { let component: TestrunComponent; @@ -119,10 +122,12 @@ describe('TestrunComponent', () => { provideMockStore({ selectors: [ { selector: selectHasDevices, value: false }, + { selector: selectIsAllDevicesOutdated, value: false }, { selector: selectIsOpenStartTestrun, value: false }, { selector: selectIsOpenWaitSnackBar, value: false }, { selector: selectHasRiskProfiles, value: false }, { selector: selectRiskProfiles, value: [] }, + { selector: selectTestModules, value: [] }, { selector: selectSystemStatus, value: MOCK_PROGRESS_DATA_IN_PROGRESS, @@ -137,6 +142,7 @@ describe('TestrunComponent', () => { MatDialogModule, SpinnerComponent, BrowserAnimationsModule, + MatTooltipModule, ], }) .overrideComponent(TestrunComponent, { @@ -234,9 +240,18 @@ describe('TestrunComponent', () => { }, provideMockStore({ selectors: [ - { selector: selectHasDevices, value: false }, { selector: selectDevices, value: [] }, + { selector: selectHasDevices, value: false }, + { selector: selectIsAllDevicesOutdated, value: false }, + { selector: selectIsOpenStartTestrun, value: false }, { selector: selectIsOpenWaitSnackBar, value: false }, + { selector: selectHasRiskProfiles, value: false }, + { selector: selectRiskProfiles, value: [] }, + { selector: selectTestModules, value: [] }, + { + selector: selectSystemStatus, + value: MOCK_PROGRESS_DATA_IN_PROGRESS, + }, ], }), ], @@ -247,6 +262,7 @@ describe('TestrunComponent', () => { MatDialogModule, SpinnerComponent, BrowserAnimationsModule, + MatTooltipModule, ], }) .overrideComponent(TestrunComponent, { @@ -284,6 +300,23 @@ describe('TestrunComponent', () => { }); }); + describe('with all devices outdated', () => { + beforeEach(() => { + store.overrideSelector(selectSystemStatus, null); + store.overrideSelector(selectHasDevices, true); + store.overrideSelector(selectIsAllDevicesOutdated, true); + fixture.detectChanges(); + }); + + it('should have disabled "Start" button', () => { + const startBtn = compiled.querySelector( + '.start-button' + ) as HTMLButtonElement; + + expect(startBtn.disabled).toBeTrue(); + }); + }); + describe('with not systemStatus$ data', () => { beforeEach(() => { store.overrideSelector(selectSystemStatus, null); @@ -324,13 +357,16 @@ describe('TestrunComponent', () => { hasBackdrop: true, disableClose: true, panelClass: 'initiate-test-run-dialog', + data: { + testModules: [], + }, }); expect(store.dispatch).toHaveBeenCalledWith( fetchSystemStatusSuccess({ systemStatus: MOCK_PROGRESS_DATA_IN_PROGRESS, }) ); - tick(10); + tick(1000); expect( stateServiceMock.focusFirstElementInContainer ).toHaveBeenCalled(); @@ -396,6 +432,14 @@ describe('TestrunComponent', () => { expect(downloadComp).toBeNull(); }); + + it('should have tags', () => { + const tags = fixture.nativeElement.querySelector( + '.toolbar-tag-container' + ); + + expect(tags).toBeTruthy(); + }); }); describe('with available systemStatus$ data, as Completed', () => { @@ -405,6 +449,7 @@ describe('TestrunComponent', () => { MOCK_PROGRESS_DATA_COMPLIANT ); store.overrideSelector(selectHasDevices, true); + store.refreshState(); fixture.detectChanges(); }); diff --git a/modules/ui/src/app/pages/testrun/testrun.component.ts b/modules/ui/src/app/pages/testrun/testrun.component.ts index b861ef953..447da1512 100644 --- a/modules/ui/src/app/pages/testrun/testrun.component.ts +++ b/modules/ui/src/app/pages/testrun/testrun.component.ts @@ -34,6 +34,8 @@ import { FocusManagerService } from '../../services/focus-manager.service'; import { TestrunStore } from './testrun.store'; import { TestRunService } from '../../services/test-run.service'; import { NotificationService } from '../../services/notification.service'; +import { TestModule } from '../../model/device'; +import { combineLatest } from 'rxjs/internal/observable/combineLatest'; @Component({ selector: 'app-progress', @@ -60,11 +62,14 @@ export class TestrunComponent implements OnInit, OnDestroy { ) {} ngOnInit(): void { - this.testrunStore.isOpenStartTestrun$ + combineLatest([ + this.testrunStore.isOpenStartTestrun$, + this.testrunStore.testModules$, + ]) .pipe(takeUntil(this.destroy$)) - .subscribe(isOpenStartTestrun => { + .subscribe(([isOpenStartTestrun, testModules]) => { if (isOpenStartTestrun) { - this.openTestRunModal(); + this.openTestRunModal(testModules); } }); } @@ -103,16 +108,19 @@ export class TestrunComponent implements OnInit, OnDestroy { this.sendCloseRequest(); } - private getTestRunName(systemStatus: TestrunStatus): string { + getTestRunName(systemStatus: TestrunStatus): string { if (systemStatus?.device) { const device = systemStatus.device; - return `${device.manufacturer} ${device.model} v${device.firmware}`; + return `${device.manufacturer} ${device.model} ${device.firmware}`; } else { return ''; } } private setCancellingStatus() { this.testrunStore.setCancellingStatus(); + timer(2000).subscribe(() => { + this.focusManagerService.focusFirstElementInContainer(); + }); } private showLoading() { this.testrunStore.showLoading(); @@ -126,13 +134,16 @@ export class TestrunComponent implements OnInit, OnDestroy { this.destroy$.unsubscribe(); } - openTestRunModal(): void { + openTestRunModal(testModules: TestModule[]): void { const dialogRef = this.dialog.open(TestrunInitiateFormComponent, { ariaLabel: 'Initiate testrun', autoFocus: true, hasBackdrop: true, disableClose: true, panelClass: 'initiate-test-run-dialog', + data: { + testModules, + }, }); dialogRef @@ -147,11 +158,9 @@ export class TestrunComponent implements OnInit, OnDestroy { this.testrunStore.setStatus(status); } this.testrunStore.setIsOpenStartTestrun(false); - timer(10) - .pipe(takeUntil(this.destroy$)) - .subscribe(() => { - this.focusManagerService.focusFirstElementInContainer(); - }); + timer(1000).subscribe(() => { + this.focusManagerService.focusFirstElementInContainer(); + }); }); } diff --git a/modules/ui/src/app/pages/testrun/testrun.module.ts b/modules/ui/src/app/pages/testrun/testrun.module.ts index d940c34bc..993ca8fff 100644 --- a/modules/ui/src/app/pages/testrun/testrun.module.ts +++ b/modules/ui/src/app/pages/testrun/testrun.module.ts @@ -35,6 +35,7 @@ import { DownloadReportComponent } from '../../components/download-report/downlo import { SpinnerComponent } from '../../components/spinner/spinner.component'; import { CalloutComponent } from '../../components/callout/callout.component'; import { DownloadOptionsComponent } from './components/download-options/download-options.component'; +import { MatTooltipModule } from '@angular/material/tooltip'; @NgModule({ declarations: [ @@ -60,6 +61,7 @@ import { DownloadOptionsComponent } from './components/download-options/download SpinnerComponent, CalloutComponent, DownloadOptionsComponent, + MatTooltipModule, ], }) export class TestrunModule {} diff --git a/modules/ui/src/app/pages/testrun/testrun.store.spec.ts b/modules/ui/src/app/pages/testrun/testrun.store.spec.ts index 03f7817af..449ffb408 100644 --- a/modules/ui/src/app/pages/testrun/testrun.store.spec.ts +++ b/modules/ui/src/app/pages/testrun/testrun.store.spec.ts @@ -20,9 +20,11 @@ import { skip, take } from 'rxjs'; import { selectHasConnectionSettings, selectHasDevices, + selectIsAllDevicesOutdated, selectIsOpenStartTestrun, selectRiskProfiles, selectSystemStatus, + selectTestModules, } from '../../store/selectors'; import { fetchSystemStatus, @@ -62,10 +64,12 @@ describe('TestrunStore', () => { provideMockStore({ selectors: [ { selector: selectHasDevices, value: false }, + { selector: selectIsAllDevicesOutdated, value: false }, { selector: selectSystemStatus, value: null }, { selector: selectHasConnectionSettings, value: true }, { selector: selectIsOpenStartTestrun, value: false }, { selector: selectRiskProfiles, value: [] }, + { selector: selectTestModules, value: [] }, ], }), ], @@ -85,10 +89,12 @@ describe('TestrunStore', () => { testrunStore.viewModel$.pipe(take(1)).subscribe(store => { expect(store).toEqual({ hasDevices: false, + isAllDevicesOutdated: false, systemStatus: null, dataSource: [], stepsToResolveCount: 0, profiles: [], + testModules: [], }); done(); }); diff --git a/modules/ui/src/app/pages/testrun/testrun.store.ts b/modules/ui/src/app/pages/testrun/testrun.store.ts index eacad9959..2dc0c19f1 100644 --- a/modules/ui/src/app/pages/testrun/testrun.store.ts +++ b/modules/ui/src/app/pages/testrun/testrun.store.ts @@ -21,9 +21,11 @@ import { AppState } from '../../store/state'; import { Store } from '@ngrx/store'; import { selectHasDevices, + selectIsAllDevicesOutdated, selectIsOpenStartTestrun, selectRiskProfiles, selectSystemStatus, + selectTestModules, } from '../../store/selectors'; import { fetchSystemStatus, @@ -41,12 +43,14 @@ import { } from '../../model/testrun-status'; import { FocusManagerService } from '../../services/focus-manager.service'; import { LoaderService } from '../../services/loader.service'; +import { TestModule } from '../../model/device'; const EMPTY_RESULT = new Array(100).fill(null).map(() => ({}) as IResult); export interface TestrunComponentState { dataSource: IResult[] | undefined; stepsToResolveCount: number; + testModules: TestModule[]; } @Injectable() @@ -56,15 +60,20 @@ export class TestrunStore extends ComponentStore { state => state.stepsToResolveCount ); private hasDevices$ = this.store.select(selectHasDevices); + private isAllDevicesOutdated$ = this.store.select(selectIsAllDevicesOutdated); private profiles$ = this.store.select(selectRiskProfiles); private systemStatus$ = this.store.select(selectSystemStatus); isOpenStartTestrun$ = this.store.select(selectIsOpenStartTestrun); + testModules$ = this.store.select(selectTestModules); + viewModel$ = this.select({ hasDevices: this.hasDevices$, + isAllDevicesOutdated: this.isAllDevicesOutdated$, systemStatus: this.systemStatus$, dataSource: this.dataSource$, stepsToResolveCount: this.stepsToResolveCount$, profiles: this.profiles$, + testModules: this.testModules$, }); setDataSource = this.updater((state, dataSource: IResult[] | undefined) => { @@ -215,6 +224,7 @@ export class TestrunStore extends ComponentStore { super({ dataSource: undefined, stepsToResolveCount: 0, + testModules: [], }); } } diff --git a/modules/ui/src/app/services/notification.service.spec.ts b/modules/ui/src/app/services/notification.service.spec.ts index c02c45f46..175ed661e 100644 --- a/modules/ui/src/app/services/notification.service.spec.ts +++ b/modules/ui/src/app/services/notification.service.spec.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { TestBed } from '@angular/core/testing'; +import { fakeAsync, TestBed, tick } from '@angular/core/testing'; import { NotificationService } from './notification.service'; import { @@ -25,6 +25,8 @@ import { of } from 'rxjs/internal/observable/of'; import { MockStore, provideMockStore } from '@ngrx/store/testing'; import { AppState } from '../store/state'; import { SnackBarComponent } from '../components/snack-bar/snack-bar.component'; +import { setIsOpenWaitSnackBar } from '../store/actions'; +import { FocusManagerService } from './focus-manager.service'; describe('NotificationService', () => { let service: NotificationService; @@ -36,10 +38,14 @@ describe('NotificationService', () => { openFromComponent: () => ({}), }; + const focusServiceMock: jasmine.SpyObj = + jasmine.createSpyObj('focusServiceMock', ['focusFirstElementInContainer']); + beforeEach(() => { TestBed.configureTestingModule({ providers: [ { provide: MatSnackBar, useValue: mockMatSnackBar }, + { provide: FocusManagerService, useValue: focusServiceMock }, provideMockStore({}), ], }); @@ -84,6 +90,22 @@ describe('NotificationService', () => { politeness: 'assertive', }); }); + + it('should open snackbar with addition panelClass', () => { + const openSpy = spyOn(service.snackBar, 'open').and.returnValues({ + afterOpened: () => of(void 0), + afterDismissed: () => of({ dismissedByAction: true }), + } as MatSnackBarRef); + + service.notify('something good happened', 1500, 'mock-class'); + + expect(openSpy).toHaveBeenCalledWith('something good happened', 'OK', { + horizontalPosition: 'center', + panelClass: ['test-run-notification', 'mock-class'], + duration: 1500, + politeness: 'assertive', + }); + }); }); describe('openSnackBar', () => { @@ -103,6 +125,18 @@ describe('NotificationService', () => { panelClass: 'snack-bar-info', }); }); + + it('should call focusFirstElementInContainer', fakeAsync(() => { + spyOn(service.snackBar, 'openFromComponent').and.returnValues({ + afterOpened: () => of(void 0), + afterDismissed: () => of({ dismissedByAction: true }), + } as MatSnackBarRef); + + service.openSnackBar(); + tick(8000); + + expect(focusServiceMock.focusFirstElementInContainer).toHaveBeenCalled(); + })); }); describe('dismiss', () => { @@ -114,4 +148,21 @@ describe('NotificationService', () => { expect(matSnackBarSpy).toHaveBeenCalled(); }); }); + + it('#dismissSnackBar should dispatch setIsOpenWaitSnackBar action', () => { + service.dismissSnackBar(); + + expect(store.dispatch).toHaveBeenCalledWith( + setIsOpenWaitSnackBar({ isOpenWaitSnackBar: false }) + ); + }); + + it('#dismissWithTimout should call dismissSnackBar after timer', fakeAsync(() => { + const dismissSnackBarSpy = spyOn(service, 'dismissSnackBar').and.stub(); + + service.dismissWithTimout(); + tick(5000); + + expect(dismissSnackBarSpy).toHaveBeenCalledWith(); + })); }); diff --git a/modules/ui/src/app/services/notification.service.ts b/modules/ui/src/app/services/notification.service.ts index 4b72105fc..413438d3d 100644 --- a/modules/ui/src/app/services/notification.service.ts +++ b/modules/ui/src/app/services/notification.service.ts @@ -53,6 +53,7 @@ export class NotificationService { timeout = TIMEOUT_MS, container?: Document | Element | null ) { + const previousActiveElement = document.activeElement; const panelClasses = ['test-run-notification']; if (panelClass) { panelClasses.push(panelClass); @@ -72,9 +73,13 @@ export class NotificationService { this.snackBarRef .afterDismissed() .pipe(take(1)) - .subscribe(() => - this.focusManagerService.focusFirstElementInContainer(container) - ); + .subscribe(() => { + if (previousActiveElement) { + (previousActiveElement as HTMLElement).focus(); + } else { + this.focusManagerService.focusFirstElementInContainer(container); + } + }); } dismiss() { this.snackBar.dismiss(); @@ -99,8 +104,10 @@ export class NotificationService { this.snackBarCompRef .afterDismissed() - .pipe(take(1)) - .subscribe(() => this.focusManagerService.focusFirstElementInContainer()); + .pipe(take(1), delay(1000)) + .subscribe(() => { + this.focusManagerService.focusFirstElementInContainer(); + }); } dismissSnackBar() { diff --git a/modules/ui/src/app/services/test-run-mqtt.service.spec.ts b/modules/ui/src/app/services/test-run-mqtt.service.spec.ts new file mode 100644 index 000000000..6f5c7e325 --- /dev/null +++ b/modules/ui/src/app/services/test-run-mqtt.service.spec.ts @@ -0,0 +1,109 @@ +import { TestBed } from '@angular/core/testing'; + +import { TestRunMqttService } from './test-run-mqtt.service'; +import { IMqttMessage, MqttModule, MqttService } from 'ngx-mqtt'; +import { MQTT_SERVICE_OPTIONS } from '../app.module'; +import SpyObj = jasmine.SpyObj; +import { of } from 'rxjs'; +import { MOCK_ADAPTERS } from '../mocks/settings.mock'; +import { Topic } from '../model/topic'; +import { MOCK_INTERNET } from '../mocks/topic.mock'; +import { MOCK_PROGRESS_DATA_IN_PROGRESS } from '../mocks/testrun.mock'; +import { TestRunService } from './test-run.service'; + +describe('TestRunMqttService', () => { + let service: TestRunMqttService; + let mockService: SpyObj; + let testRunServiceMock: SpyObj; + + beforeEach(() => { + mockService = jasmine.createSpyObj(['observe']); + testRunServiceMock = jasmine.createSpyObj(['changeReportURL']); + + TestBed.configureTestingModule({ + imports: [MqttModule.forRoot(MQTT_SERVICE_OPTIONS)], + providers: [ + { provide: MqttService, useValue: mockService }, + { provide: TestRunService, useValue: testRunServiceMock }, + ], + }); + service = TestBed.inject(TestRunMqttService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + describe('getNetworkAdapters', () => { + beforeEach(() => { + mockService.observe.and.returnValue(of(getResponse(MOCK_ADAPTERS))); + }); + + it('should subscribe the topic', done => { + service.getNetworkAdapters().subscribe(() => { + expect(mockService.observe).toHaveBeenCalledWith(Topic.NetworkAdapters); + done(); + }); + }); + + it('should return object of type', done => { + service.getNetworkAdapters().subscribe(res => { + expect(res).toEqual(MOCK_ADAPTERS); + done(); + }); + }); + }); + + describe('getInternetConnection', () => { + beforeEach(() => { + mockService.observe.and.returnValue(of(getResponse(MOCK_INTERNET))); + }); + + it('should subscribe the topic', done => { + service.getInternetConnection().subscribe(() => { + expect(mockService.observe).toHaveBeenCalledWith( + Topic.InternetConnection + ); + done(); + }); + }); + + it('should return object of type', done => { + service.getInternetConnection().subscribe(res => { + expect(res).toEqual(MOCK_INTERNET); + done(); + }); + }); + }); + + describe('getStatus', () => { + beforeEach(() => { + mockService.observe.and.returnValue( + of(getResponse(MOCK_PROGRESS_DATA_IN_PROGRESS)) + ); + testRunServiceMock.changeReportURL.and.returnValue(''); + }); + + it('should subscribe the topic', done => { + service.getStatus().subscribe(() => { + expect(mockService.observe).toHaveBeenCalledWith(Topic.Status); + done(); + }); + }); + + it('should return object of type', done => { + service.getStatus().subscribe(res => { + expect(res).toEqual(MOCK_PROGRESS_DATA_IN_PROGRESS); + done(); + }); + }); + }); + + function getResponse(response: Type): IMqttMessage { + const enc = new TextEncoder(); + const message = enc.encode(JSON.stringify(response)); + return { + payload: message, + } as IMqttMessage; + } +}); diff --git a/modules/ui/src/app/services/test-run-mqtt.service.ts b/modules/ui/src/app/services/test-run-mqtt.service.ts new file mode 100644 index 000000000..d53622d28 --- /dev/null +++ b/modules/ui/src/app/services/test-run-mqtt.service.ts @@ -0,0 +1,47 @@ +import { Injectable } from '@angular/core'; +import { IMqttMessage, MqttService } from 'ngx-mqtt'; +import { catchError, Observable, of } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { Adapters } from '../model/setting'; +import { TestrunStatus } from '../model/testrun-status'; +import { InternetConnection, Topic } from '../model/topic'; +import { TestRunService } from './test-run.service'; + +@Injectable({ + providedIn: 'root', +}) +export class TestRunMqttService { + constructor( + private mqttService: MqttService, + private testrunService: TestRunService + ) {} + + getNetworkAdapters(): Observable { + return this.topic(Topic.NetworkAdapters); + } + + getInternetConnection(): Observable { + return this.topic(Topic.InternetConnection); + } + + getStatus(): Observable { + return this.topic(Topic.Status).pipe( + map(result => { + result.report = this.testrunService.changeReportURL(result.report); + return result; + }) + ); + } + + private topic(topicName: string): Observable { + return this.mqttService.observe(topicName).pipe( + map( + (res: IMqttMessage) => + JSON.parse(new TextDecoder().decode(res.payload)) as Type + ), + catchError(() => { + return of({} as Type); + }) + ); + } +} diff --git a/modules/ui/src/app/services/test-run.service.spec.ts b/modules/ui/src/app/services/test-run.service.spec.ts index 069c94c0a..113f72c8f 100644 --- a/modules/ui/src/app/services/test-run.service.spec.ts +++ b/modules/ui/src/app/services/test-run.service.spec.ts @@ -18,7 +18,7 @@ import { HttpTestingController, } from '@angular/common/http/testing'; import { fakeAsync, getTestBed, TestBed, tick } from '@angular/core/testing'; -import { Device, TestModule } from '../model/device'; +import { Device } from '../model/device'; import { TestRunService, UNAVAILABLE_VERSION } from './test-run.service'; import { SystemConfig, SystemInterfaces } from '../model/setting'; @@ -28,7 +28,7 @@ import { StatusOfTestrun, TestrunStatus, } from '../model/testrun-status'; -import { device } from '../mocks/device.mock'; +import { device, DEVICES_FORM, MOCK_MODULES } from '../mocks/device.mock'; import { NEW_VERSION, VERSION } from '../mocks/version.mock'; import { MockStore, provideMockStore } from '@ngrx/store/testing'; import { AppState } from '../store/state'; @@ -74,39 +74,23 @@ describe('TestRunService', () => { expect(service).toBeTruthy(); }); - it('should have test modules', () => { - expect(service.getTestModules()).toEqual([ - { - displayName: 'Connection', - name: 'connection', - enabled: true, - }, - { - displayName: 'NTP', - name: 'ntp', - enabled: true, - }, - { - displayName: 'DNS', - name: 'dns', - enabled: true, - }, - { - displayName: 'Services', - name: 'services', - enabled: true, - }, - { - displayName: 'TLS', - name: 'tls', - enabled: true, - }, - { - displayName: 'Protocol', - name: 'protocol', - enabled: true, - }, - ] as TestModule[]); + it('getTestModules should return modules', () => { + let result: string[] = []; + const testModules = MOCK_MODULES; + + service.getTestModules().subscribe(res => { + expect(res).toEqual(result); + }); + + result = testModules; + service.getTestModules(); + const req = httpTestingController.expectOne( + 'http://localhost:8000/system/modules' + ); + + expect(req.request.method).toBe('GET'); + + req.flush(testModules); }); it('fetchDevices should return devices', () => { @@ -170,6 +154,8 @@ describe('TestRunService', () => { }); describe('fetchSystemStatus', () => { + const systemStatusUrl = 'http://localhost:8000/system/status'; + it('should get system status data with no changes', () => { const result = { ...MOCK_PROGRESS_DATA_IN_PROGRESS }; @@ -177,12 +163,22 @@ describe('TestRunService', () => { expect(res).toEqual(result); }); - const req = httpTestingController.expectOne( - 'http://localhost:8000/system/status' - ); + const req = httpTestingController.expectOne(systemStatusUrl); expect(req.request.method).toBe('GET'); req.flush(result); }); + + it('should get system status as empty object if error happens', () => { + const mockError = { error: 'someError' } as ErrorEvent; + + service.fetchSystemStatus().subscribe(res => { + expect(res).toEqual({} as TestrunStatus); + }); + + const req = httpTestingController.expectOne(systemStatusUrl); + + req.error(mockError); + }); }); it('stopTestrun should have necessary request data', () => { @@ -261,6 +257,7 @@ describe('TestRunService', () => { green: false, red: false, blue: false, + cyan: false, grey: false, }; @@ -272,10 +269,11 @@ describe('TestRunService', () => { const statusesForBlueRes = [ StatusOfTestResult.SmartReady, - StatusOfTestResult.Info, StatusOfTestResult.InProgress, ]; + const statusesForCyanRes = [StatusOfTestResult.Info]; + const statusesForRedRes = [ StatusOfTestResult.NonCompliant, StatusOfTestResult.Error, @@ -284,6 +282,8 @@ describe('TestRunService', () => { const statusesForGreyRes = [ StatusOfTestResult.NotDetected, StatusOfTestResult.NotStarted, + StatusOfTestResult.Skipped, + StatusOfTestResult.Disabled, ]; statusesForGreenRes.forEach(testCase => { @@ -306,6 +306,16 @@ describe('TestRunService', () => { }); }); + statusesForCyanRes.forEach(testCase => { + it(`should return class "cyan" if test result is "${testCase}"`, () => { + const expectedResult = { ...availableResultClasses, cyan: true }; + + const result = service.getResultClass(testCase); + + expect(result).toEqual(expectedResult); + }); + }); + statusesForRedRes.forEach(testCase => { it(`should return class "red" if test result is "${testCase}"`, () => { const expectedResult = { ...availableResultClasses, red: true }; @@ -675,4 +685,20 @@ describe('TestRunService', () => { req.flush(data, mockErrorResponse); }); }); + + describe('fetchQuestionnaireFormat', () => { + it('should get system status data with no changes', () => { + const result = { ...DEVICES_FORM }; + + service.fetchQuestionnaireFormat().subscribe(res => { + expect(res).toEqual(result); + }); + + const req = httpTestingController.expectOne( + 'http://localhost:8000/devices/format' + ); + expect(req.request.method).toBe('GET'); + req.flush(result); + }); + }); }); diff --git a/modules/ui/src/app/services/test-run.service.ts b/modules/ui/src/app/services/test-run.service.ts index 5620f9404..c517f7004 100644 --- a/modules/ui/src/app/services/test-run.service.ts +++ b/modules/ui/src/app/services/test-run.service.ts @@ -17,7 +17,7 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; import { Observable } from 'rxjs/internal/Observable'; -import { Device, TestModule } from '../model/device'; +import { Device, DeviceQuestionnaireSection } from '../model/device'; import { catchError, map, of, retry } from 'rxjs'; import { SystemConfig, SystemInterfaces } from '../model/setting'; import { @@ -49,43 +49,18 @@ export const UNAVAILABLE_VERSION = { providedIn: 'root', }) export class TestRunService { - private readonly testModules: TestModule[] = [ - { - displayName: 'Connection', - name: 'connection', - enabled: true, - }, - { - displayName: 'NTP', - name: 'ntp', - enabled: true, - }, - { - displayName: 'DNS', - name: 'dns', - enabled: true, - }, - { - displayName: 'Services', - name: 'services', - enabled: true, - }, - { - displayName: 'TLS', - name: 'tls', - enabled: true, - }, - { - displayName: 'Protocol', - name: 'protocol', - enabled: true, - }, - ]; - private version = new BehaviorSubject(null); constructor(private http: HttpClient) {} + changeReportURL(url: string): string { + if (!url) { + return ''; + } + // replace url part before '/report' from static to dynamic API URL + return url.replace(/^.*(?=\/report)/, `${API_URL}`); + } + fetchDevices(): Observable { return this.http.get(`${API_URL}/devices`); } @@ -105,7 +80,15 @@ export class TestRunService { } fetchSystemStatus() { - return this.http.get(`${API_URL}/system/status`); + return this.http.get(`${API_URL}/system/status`).pipe( + map(result => { + result.report = this.changeReportURL(result.report); + return result; + }), + catchError(() => { + return of({} as TestrunStatus); + }) + ); } stopTestrun(): Observable { @@ -123,8 +106,10 @@ export class TestRunService { .pipe(map(() => true)); } - getTestModules(): TestModule[] { - return this.testModules; + getTestModules(): Observable { + return this.http + .get(`${API_URL}/system/modules`) + .pipe(catchError(() => of([]))); } saveDevice(device: Device): Observable { @@ -165,7 +150,14 @@ export class TestRunService { } getHistory(): Observable { - return this.http.get(`${API_URL}/reports`); + return this.http.get(`${API_URL}/reports`).pipe( + map(result => { + result.forEach( + item => (item.report = this.changeReportURL(item.report)) + ); + return result; + }) + ); } public getResultClass(result: string): StatusResultClassName { @@ -179,11 +171,13 @@ export class TestRunService { result === StatusOfTestResult.Error, blue: result === StatusOfTestResult.SmartReady || - result === StatusOfTestResult.Info || result === StatusOfTestResult.InProgress, + cyan: result === StatusOfTestResult.Info, grey: result === StatusOfTestResult.NotDetected || - result === StatusOfTestResult.NotStarted, + result === StatusOfTestResult.NotStarted || + result === StatusOfTestResult.Skipped || + result === StatusOfTestResult.Disabled, }; } @@ -305,6 +299,12 @@ export class TestRunService { return this.http.get(`${API_URL}/profiles/format`); } + fetchQuestionnaireFormat(): Observable { + return this.http.get( + `${API_URL}/devices/format` + ); + } + saveProfile(profile: Profile): Observable { return this.http .post(`${API_URL}/profiles`, JSON.stringify(profile)) diff --git a/modules/ui/src/app/store/actions.ts b/modules/ui/src/app/store/actions.ts index 3ca38d16f..58153ad9a 100644 --- a/modules/ui/src/app/store/actions.ts +++ b/modules/ui/src/app/store/actions.ts @@ -15,41 +15,22 @@ */ import { createAction, props } from '@ngrx/store'; -import { - InterfacesValidation, - SettingMissedError, - SystemConfig, -} from '../model/setting'; +import { Adapters, InterfacesValidation, SystemConfig } from '../model/setting'; import { SystemInterfaces } from '../model/setting'; -import { Device } from '../model/device'; +import { Device, TestModule } from '../model/device'; import { TestrunStatus } from '../model/testrun-status'; import { Profile } from '../model/profile'; -// App component -export const toggleMenu = createAction('[App Component] Toggle Menu'); - -export const fetchInterfaces = createAction('[App Component] Fetch Interfaces'); - export const fetchInterfacesSuccess = createAction( '[App Component] Fetch interfaces Success', props<{ interfaces: SystemInterfaces }>() ); -export const updateFocusNavigation = createAction( - '[App Component] update focus navigation', - props<{ focusNavigation: boolean }>() -); - export const updateValidInterfaces = createAction( '[App Component] Update Valid Interfaces', props<{ validInterfaces: InterfacesValidation }>() ); -export const updateError = createAction( - '[App Component] Update Setting Missed Error', - props<{ settingMissedError: SettingMissedError }>() -); - // Settings export const fetchSystemConfigSuccess = createAction( '[Settings] Fetch System Config Success', @@ -79,6 +60,16 @@ export const setHasDevices = createAction( props<{ hasDevices: boolean }>() ); +export const setHasExpiredDevices = createAction( + '[Shared] Set Has Expired Devices', + props<{ hasExpiredDevices: boolean }>() +); + +export const setIsAllDevicesOutdated = createAction( + '[Shared] Set Is All Devices Outdated', + props<{ isAllDevicesOutdated: boolean }>() +); + export const setDevices = createAction( '[Shared] Set Devices', props<{ devices: Device[] }>() @@ -121,6 +112,33 @@ export const setStatus = createAction( props<{ status: string }>() ); +export const setIsTestingComplete = createAction( + '[Shared] Set Is Open Testing Complete', + props<{ isTestingComplete: boolean }>() +); + export const stopInterval = createAction('[Shared] Stop Interval'); export const fetchRiskProfiles = createAction('[Shared] Fetch risk profiles'); + +export const updateAdapters = createAction( + '[Shared] Update Adapters', + props<{ adapters: Adapters }>() +); + +export const fetchReports = createAction('[Shared] Fetch reports'); + +export const setReports = createAction( + '[Shared] Set Reports', + props<{ reports: TestrunStatus[] }>() +); + +export const setTestModules = createAction( + '[Shared] Set Test Modules', + props<{ testModules: TestModule[] }>() +); + +export const updateInternetConnection = createAction( + '[Shared] Fetch internet connection', + props<{ internetConnection: boolean | null }>() +); diff --git a/modules/ui/src/app/store/effects.spec.ts b/modules/ui/src/app/store/effects.spec.ts index 024782c63..c0a06352d 100644 --- a/modules/ui/src/app/store/effects.spec.ts +++ b/modules/ui/src/app/store/effects.spec.ts @@ -28,20 +28,28 @@ import { Action } from '@ngrx/store'; import * as actions from './actions'; import { MockStore, provideMockStore } from '@ngrx/store/testing'; import { AppState } from './state'; -import { - selectIsOpenWaitSnackBar, - selectMenuOpened, - selectSystemStatus, -} from './selectors'; -import { device } from '../mocks/device.mock'; +import { selectIsOpenWaitSnackBar, selectSystemStatus } from './selectors'; +import { device, expired_device } from '../mocks/device.mock'; import { MOCK_PROGRESS_DATA_CANCELLING, + MOCK_PROGRESS_DATA_COMPLIANT, MOCK_PROGRESS_DATA_IN_PROGRESS, MOCK_PROGRESS_DATA_WAITING_FOR_DEVICE, } from '../mocks/testrun.mock'; -import { fetchSystemStatus, setStatus, setTestrunStatus } from './actions'; +import { + fetchSystemStatus, + fetchSystemStatusSuccess, + setReports, + setStatus, + setTestrunStatus, +} from './actions'; import { NotificationService } from '../services/notification.service'; import { PROFILE_MOCK } from '../mocks/profile.mock'; +import { throwError } from 'rxjs/internal/observable/throwError'; +import { HttpErrorResponse } from '@angular/common/http'; +import { IDLE_STATUS } from '../model/testrun-status'; +import { HISTORY } from '../mocks/reports.mock'; +import { TestRunMqttService } from '../services/test-run-mqtt.service'; describe('Effects', () => { let actions$ = new Observable(); @@ -54,6 +62,11 @@ describe('Effects', () => { 'dismissWithTimout', 'openSnackBar', ]); + const mockMqttService: jasmine.SpyObj = + jasmine.createSpyObj('mockMqttService', [ + 'getStatus', + 'getInternetConnection', + ]); beforeEach(() => { testRunServiceMock = jasmine.createSpyObj('testRunServiceMock', [ @@ -64,6 +77,7 @@ describe('Effects', () => { 'testrunInProgress', 'stopTestrun', 'fetchProfiles', + 'getHistory', ]); testRunServiceMock.getSystemInterfaces.and.returnValue(of({})); testRunServiceMock.getSystemConfig.and.returnValue(of({ network: {} })); @@ -72,12 +86,21 @@ describe('Effects', () => { of(MOCK_PROGRESS_DATA_IN_PROGRESS) ); testRunServiceMock.fetchProfiles.and.returnValue(of([])); + testRunServiceMock.getHistory.and.returnValue(of([])); + mockMqttService.getInternetConnection.and.returnValue( + of({ connection: false }) + ); + + mockMqttService.getStatus.and.returnValue( + of(MOCK_PROGRESS_DATA_IN_PROGRESS) + ); TestBed.configureTestingModule({ providers: [ AppEffects, { provide: TestRunService, useValue: testRunServiceMock }, { provide: NotificationService, useValue: notificationServiceMock }, + { provide: TestRunMqttService, useValue: mockMqttService }, provideMockActions(() => actions$), provideMockStore({}), ], @@ -129,199 +152,38 @@ describe('Effects', () => { }); }); - it('onSetRiskProfiles$ should call setHasRiskProfiles', done => { - actions$ = of(actions.setRiskProfiles({ riskProfiles: [PROFILE_MOCK] })); + it('onSetExpiredDevices$ should call setHasExpiredDevices', done => { + actions$ = of(actions.setDevices({ devices: [device, expired_device] })); - effects.onSetRiskProfiles$.subscribe(action => { + effects.onSetExpiredDevices$.subscribe(action => { expect(action).toEqual( - actions.setHasRiskProfiles({ hasRiskProfiles: true }) + actions.setHasExpiredDevices({ hasExpiredDevices: true }) ); done(); }); }); - it('onMenuOpened$ should call updateFocusNavigation', done => { - actions$ = of(actions.toggleMenu()); - store.overrideSelector(selectMenuOpened, true); + it('onSetIsAllDevicesOutdated$ should call setIsAllDevicesOutdated', done => { + actions$ = of( + actions.setDevices({ devices: [expired_device, expired_device] }) + ); - effects.onMenuOpened$.subscribe(action => { + effects.onSetIsAllDevicesOutdated$.subscribe(action => { expect(action).toEqual( - actions.updateFocusNavigation({ focusNavigation: true }) + actions.setIsAllDevicesOutdated({ isAllDevicesOutdated: true }) ); done(); }); }); - describe('onValidateInterfaces$', () => { - it('should call updateError and set false if interfaces are not missed', done => { - actions$ = of( - actions.updateValidInterfaces({ - validInterfaces: { - deviceValid: true, - internetValid: true, - }, - }) - ); - - effects.onValidateInterfaces$.subscribe(action => { - expect(action).toEqual( - actions.updateError({ - settingMissedError: { - isSettingMissed: false, - devicePortMissed: false, - internetPortMissed: false, - }, - }) - ); - done(); - }); - }); - - it('should call updateError and set true if interfaces are missed', done => { - actions$ = of( - actions.updateValidInterfaces({ - validInterfaces: { - deviceValid: false, - internetValid: false, - }, - }) - ); - - effects.onValidateInterfaces$.subscribe(action => { - expect(action).toEqual( - actions.updateError({ - settingMissedError: { - isSettingMissed: true, - devicePortMissed: true, - internetPortMissed: true, - }, - }) - ); - done(); - }); - }); - }); - - describe('checkInterfacesInConfig$', () => { - it('should call updateValidInterfaces and set deviceValid as false if device interface is no longer available', done => { - actions$ = of( - actions.fetchInterfacesSuccess({ - interfaces: { - enx00e04c020fa8: '00:e0:4c:02:0f:a8', - enx207bd26205e9: '20:7b:d2:62:05:e9', - }, - }), - actions.fetchSystemConfigSuccess({ - systemConfig: { - network: { - device_intf: 'enx00e04c020fa2', - internet_intf: 'enx207bd26205e9', - }, - }, - }) - ); - - effects.checkInterfacesInConfig$.subscribe(action => { - expect(action).toEqual( - actions.updateValidInterfaces({ - validInterfaces: { - deviceValid: false, - internetValid: true, - }, - }) - ); - done(); - }); - }); - - it('should call updateValidInterfaces and set all true if interface is set and valid', done => { - actions$ = of( - actions.fetchInterfacesSuccess({ - interfaces: { - enx00e04c020fa8: '00:e0:4c:02:0f:a8', - enx207bd26205e9: '20:7b:d2:62:05:e9', - }, - }), - actions.fetchSystemConfigSuccess({ - systemConfig: { - network: { - device_intf: 'enx00e04c020fa8', - internet_intf: 'enx207bd26205e9', - }, - }, - }) - ); - - effects.checkInterfacesInConfig$.subscribe(action => { - expect(action).toEqual( - actions.updateValidInterfaces({ - validInterfaces: { - deviceValid: true, - internetValid: true, - }, - }) - ); - done(); - }); - }); - - it('should call updateValidInterfaces and set all true if interface are empty and config is not set', done => { - actions$ = of( - actions.fetchInterfacesSuccess({ - interfaces: {}, - }), - actions.fetchSystemConfigSuccess({ - systemConfig: { - network: { - device_intf: '', - internet_intf: '', - }, - }, - }) - ); - - effects.checkInterfacesInConfig$.subscribe(action => { - expect(action).toEqual( - actions.updateValidInterfaces({ - validInterfaces: { - deviceValid: true, - internetValid: true, - }, - }) - ); - done(); - }); - }); + it('onSetRiskProfiles$ should call setHasRiskProfiles', done => { + actions$ = of(actions.setRiskProfiles({ riskProfiles: [PROFILE_MOCK] })); - it('should call updateValidInterfaces and set all true if interface are not empty and config is not set', done => { - actions$ = of( - actions.fetchInterfacesSuccess({ - interfaces: { - enx00e04c020fa8: '00:e0:4c:02:0f:a8', - enx207bd26205e9: '20:7b:d2:62:05:e9', - }, - }), - actions.fetchSystemConfigSuccess({ - systemConfig: { - network: { - device_intf: '', - internet_intf: '', - }, - }, - }) + effects.onSetRiskProfiles$.subscribe(action => { + expect(action).toEqual( + actions.setHasRiskProfiles({ hasRiskProfiles: true }) ); - - effects.checkInterfacesInConfig$.subscribe(action => { - expect(action).toEqual( - actions.updateValidInterfaces({ - validInterfaces: { - deviceValid: true, - internetValid: true, - }, - }) - ); - done(); - }); + done(); }); }); @@ -387,14 +249,15 @@ describe('Effects', () => { ); }); - it('should call fetchSystemStatus for status "in progress"', fakeAsync(() => { + it('should call fetchSystemStatus for status "in progress"', () => { effects.onFetchSystemStatusSuccess$.subscribe(() => { - tick(5000); - - expect(dispatchSpy).toHaveBeenCalledWith(fetchSystemStatus()); - discardPeriodicTasks(); + expect(dispatchSpy).toHaveBeenCalledWith( + fetchSystemStatusSuccess({ + systemStatus: MOCK_PROGRESS_DATA_IN_PROGRESS, + }) + ); }); - })); + }); it('should dispatch status and systemStatus', done => { effects.onFetchSystemStatusSuccess$.subscribe(() => { @@ -423,6 +286,12 @@ describe('Effects', () => { done(); }); }); + + it('should call fetchInternetConnection for status "in progress"', () => { + effects.onFetchSystemStatusSuccess$.subscribe(() => { + expect(mockMqttService.getInternetConnection).toHaveBeenCalled(); + }); + }); }); describe('with status "waiting for device"', () => { @@ -439,14 +308,15 @@ describe('Effects', () => { ); }); - it('should call fetchSystemStatus for status "waiting for device"', fakeAsync(() => { + it('should call fetchSystemStatus for status "waiting for device"', () => { effects.onFetchSystemStatusSuccess$.subscribe(() => { - tick(5000); - - expect(dispatchSpy).toHaveBeenCalledWith(fetchSystemStatus()); - discardPeriodicTasks(); + expect(dispatchSpy).toHaveBeenCalledWith( + fetchSystemStatusSuccess({ + systemStatus: MOCK_PROGRESS_DATA_IN_PROGRESS, + }) + ); }); - })); + }); it('should open snackbar when waiting for device is too long', fakeAsync(() => { effects.onFetchSystemStatusSuccess$.subscribe(() => { @@ -487,4 +357,78 @@ describe('Effects', () => { done(); }); }); + + describe('onFetchReports$', () => { + it(' should call setReports on success', done => { + testRunServiceMock.getHistory.and.returnValue(of([])); + actions$ = of(actions.fetchReports()); + + effects.onFetchReports$.subscribe(action => { + expect(action).toEqual( + actions.setReports({ + reports: [], + }) + ); + done(); + }); + }); + + it('should call setReports with empty array if null is returned', done => { + testRunServiceMock.getHistory.and.returnValue(of(null)); + actions$ = of(actions.fetchReports()); + + effects.onFetchReports$.subscribe(action => { + expect(action).toEqual( + actions.setReports({ + reports: [], + }) + ); + done(); + }); + }); + + it('should call setReports with empty array if error happens', done => { + testRunServiceMock.getHistory.and.returnValue( + throwError( + new HttpErrorResponse({ error: { error: 'error' }, status: 500 }) + ) + ); + actions$ = of(actions.fetchReports()); + + effects.onFetchReports$.subscribe({ + complete: () => { + expect(dispatchSpy).toHaveBeenCalledWith( + setReports({ + reports: [], + }) + ); + done(); + }, + }); + }); + }); + + describe('checkStatusInReports$', () => { + it('should call setTestrunStatus if current test run is completed and not present in reports', done => { + store.overrideSelector( + selectSystemStatus, + Object.assign({}, MOCK_PROGRESS_DATA_COMPLIANT, { + mac_addr: '01:02:03:04:05:07', + report: 'http://localhost:8000/report/1234 1234/2024-07-17T15:33:40', + }) + ); + actions$ = of( + actions.setReports({ + reports: HISTORY, + }) + ); + + effects.checkStatusInReports$.subscribe(action => { + expect(action).toEqual( + actions.fetchSystemStatusSuccess({ systemStatus: IDLE_STATUS }) + ); + done(); + }); + }); + }); }); diff --git a/modules/ui/src/app/store/effects.ts b/modules/ui/src/app/store/effects.ts index b6cdfcc71..12140eb65 100644 --- a/modules/ui/src/app/store/effects.ts +++ b/modules/ui/src/app/store/effects.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Injectable, NgZone } from '@angular/core'; +import { Injectable } from '@angular/core'; import { Actions, createEffect, ofType } from '@ngrx/effects'; import { Store } from '@ngrx/store'; import { map, switchMap, tap, withLatestFrom } from 'rxjs/operators'; @@ -22,85 +22,53 @@ import { map, switchMap, tap, withLatestFrom } from 'rxjs/operators'; import * as AppActions from './actions'; import { AppState } from './state'; import { TestRunService } from '../services/test-run.service'; -import { filter, combineLatest, interval, Subject, timer, take } from 'rxjs'; import { - selectIsOpenWaitSnackBar, - selectMenuOpened, - selectSystemStatus, -} from './selectors'; -import { IResult, StatusOfTestrun, TestsData } from '../model/testrun-status'; + filter, + Subject, + timer, + take, + catchError, + EMPTY, + Subscription, +} from 'rxjs'; +import { selectIsOpenWaitSnackBar, selectSystemStatus } from './selectors'; +import { + IDLE_STATUS, + StatusOfTestrun, + TestrunStatus, +} from '../model/testrun-status'; import { fetchSystemStatus, + fetchSystemStatusSuccess, + setIsTestingComplete, + setReports, setStatus, setTestrunStatus, stopInterval, + updateInternetConnection, } from './actions'; import { takeUntil } from 'rxjs/internal/operators/takeUntil'; import { NotificationService } from '../services/notification.service'; import { Profile } from '../model/profile'; +import { DeviceStatus } from '../model/device'; +import { TestRunMqttService } from '../services/test-run-mqtt.service'; +import { InternetConnection } from '../model/topic'; const WAIT_TO_OPEN_SNACKBAR_MS = 60 * 1000; @Injectable() export class AppEffects { - private startInterval = false; - private destroyInterval$: Subject = new Subject(); + private isSinglePortMode: boolean | undefined = false; + private statusSubscription: Subscription | undefined; + private internetSubscription: Subscription | undefined; private destroyWaitDeviceInterval$: Subject = new Subject(); - checkInterfacesInConfig$ = createEffect(() => - combineLatest([ - this.actions$.pipe(ofType(AppActions.fetchInterfacesSuccess)), - this.actions$.pipe(ofType(AppActions.fetchSystemConfigSuccess)), - ]).pipe( - filter( - ([ - , - { - systemConfig: { network }, - }, - ]) => network !== null - ), - map( - ([ - { interfaces }, - { - systemConfig: { network }, - }, - ]) => - AppActions.updateValidInterfaces({ - validInterfaces: { - deviceValid: - network?.device_intf == '' || - (!!network?.device_intf && !!interfaces[network.device_intf]), - internetValid: - network?.internet_intf == '' || - (!!network?.internet_intf && - !!interfaces[network.internet_intf]), - }, - }) - ) - ) - ); - - onValidateInterfaces$ = createEffect(() => { - return this.actions$.pipe( - ofType(AppActions.updateValidInterfaces), - map(({ validInterfaces }) => - AppActions.updateError({ - settingMissedError: { - isSettingMissed: - !validInterfaces.deviceValid || !validInterfaces.internetValid, - devicePortMissed: !validInterfaces.deviceValid, - internetPortMissed: !validInterfaces.internetValid, - }, - }) - ) - ); - }); - onFetchSystemConfigSuccess$ = createEffect(() => { return this.actions$.pipe( ofType(AppActions.fetchSystemConfigSuccess), + tap( + ({ systemConfig }) => (this.isSinglePortMode = systemConfig.single_intf) + ), map(({ systemConfig }) => AppActions.setHasConnectionSettings({ hasConnectionSettings: @@ -110,20 +78,37 @@ export class AppEffects { ); }); - onMenuOpened$ = createEffect(() => { + onSetDevices$ = createEffect(() => { + return this.actions$.pipe( + ofType(AppActions.setDevices), + map(({ devices }) => + AppActions.setHasDevices({ hasDevices: devices.length > 0 }) + ) + ); + }); + + onSetExpiredDevices$ = createEffect(() => { return this.actions$.pipe( - ofType(AppActions.toggleMenu), - withLatestFrom(this.store.select(selectMenuOpened)), - filter(([, opened]) => opened === true), - map(() => AppActions.updateFocusNavigation({ focusNavigation: true })) // user will be navigated to side menu on tab + ofType(AppActions.setDevices), + map(({ devices }) => + AppActions.setHasExpiredDevices({ + hasExpiredDevices: devices.some( + device => device.status === DeviceStatus.INVALID + ), + }) + ) ); }); - onSetDevices$ = createEffect(() => { + onSetIsAllDevicesOutdated$ = createEffect(() => { return this.actions$.pipe( ofType(AppActions.setDevices), map(({ devices }) => - AppActions.setHasDevices({ hasDevices: devices.length > 0 }) + AppActions.setIsAllDevicesOutdated({ + isAllDevicesOutdated: devices.every( + device => device.status === DeviceStatus.INVALID + ), + }) ) ); }); @@ -190,8 +175,8 @@ export class AppEffects { return this.actions$.pipe( ofType(AppActions.stopInterval), tap(() => { - this.startInterval = false; - this.destroyInterval$.next(true); + this.statusSubscription?.unsubscribe(); + this.internetSubscription?.unsubscribe(); }) ); }, @@ -203,11 +188,14 @@ export class AppEffects { return this.actions$.pipe( ofType(AppActions.fetchSystemStatusSuccess), tap(({ systemStatus }) => { - if ( - this.testrunService.testrunInProgress(systemStatus.status) && - !this.startInterval - ) { + this.store.dispatch( + setIsTestingComplete({ + isTestingComplete: this.isTestrunFinished(systemStatus.status), + }) + ); + if (this.testrunService.testrunInProgress(systemStatus.status)) { this.pullingSystemStatusData(); + this.fetchInternetConnection(); } else if ( !this.testrunService.testrunInProgress(systemStatus.status) ) { @@ -225,35 +213,20 @@ export class AppEffects { ) { this.showSnackBar(); } - if ( - systemStatus?.status !== StatusOfTestrun.WaitingForDevice && - isOpenWaitSnackBar - ) { - this.notificationService.dismissWithTimout(); + if (systemStatus?.status !== StatusOfTestrun.WaitingForDevice) { + if (isOpenWaitSnackBar) { + this.notificationService.dismissWithTimout(); + } else { + this.destroyWaitDeviceInterval$.next(true); + } } }), tap(([{ systemStatus }, , status]) => { // for app - requires only status if (systemStatus.status !== status?.status) { - this.ngZone.run(() => { - this.store.dispatch(setStatus({ status: systemStatus.status })); - this.store.dispatch( - setTestrunStatus({ systemStatus: systemStatus }) - ); - }); - } else if ( - systemStatus.finished !== status?.finished || - (systemStatus.tests as TestsData)?.results?.length !== - (status?.tests as TestsData)?.results?.length || - (systemStatus.tests as IResult[])?.length !== - (status?.tests as IResult[])?.length - ) { - this.ngZone.run(() => { - this.store.dispatch( - setTestrunStatus({ systemStatus: systemStatus }) - ); - }); + this.store.dispatch(setStatus({ status: systemStatus.status })); } + this.store.dispatch(setTestrunStatus({ systemStatus: systemStatus })); }) ); }, @@ -273,6 +246,55 @@ export class AppEffects { ); }); + onFetchReports$ = createEffect(() => { + return this.actions$.pipe( + ofType(AppActions.fetchReports), + switchMap(() => + this.testrunService.getHistory().pipe( + map((reports: TestrunStatus[] | null) => { + if (reports !== null) { + return AppActions.setReports({ reports }); + } + return AppActions.setReports({ reports: [] }); + }), + catchError(() => { + this.store.dispatch(setReports({ reports: [] })); + return EMPTY; + }) + ) + ) + ); + }); + + checkStatusInReports$ = createEffect(() => { + return this.actions$.pipe( + ofType(AppActions.setReports), + withLatestFrom(this.store.select(selectSystemStatus)), + filter(([, systemStatus]) => { + return ( + systemStatus != null && this.isTestrunFinished(systemStatus.status) + ); + }), + filter(([{ reports }, systemStatus]) => { + return ( + !reports?.some(report => report.report === systemStatus!.report) || + false + ); + }), + map(() => + AppActions.fetchSystemStatusSuccess({ systemStatus: IDLE_STATUS }) + ) + ); + }); + + private isTestrunFinished(status: string) { + return ( + status === StatusOfTestrun.Compliant || + status === StatusOfTestrun.NonCompliant || + status === StatusOfTestrun.Error + ); + } + private showSnackBar() { timer(WAIT_TO_OPEN_SNACKBAR_MS) .pipe( @@ -290,22 +312,43 @@ export class AppEffects { } private pullingSystemStatusData(): void { - this.ngZone.runOutsideAngular(() => { - this.startInterval = true; - interval(5000) - .pipe( - takeUntil(this.destroyInterval$), - tap(() => this.store.dispatch(fetchSystemStatus())) - ) - .subscribe(); - }); + if ( + this.statusSubscription === undefined || + this.statusSubscription?.closed + ) { + this.statusSubscription = this.testrunMqttService + .getStatus() + .subscribe(systemStatus => { + this.store.dispatch(fetchSystemStatusSuccess({ systemStatus })); + }); + } + } + + private fetchInternetConnection() { + if (this.isSinglePortMode) { + return; + } + if ( + this.internetSubscription === undefined || + this.internetSubscription?.closed + ) { + this.internetSubscription = this.testrunMqttService + .getInternetConnection() + .subscribe((internetConnection: InternetConnection) => { + this.store.dispatch( + updateInternetConnection({ + internetConnection: internetConnection.connection, + }) + ); + }); + } } constructor( private actions$: Actions, private testrunService: TestRunService, + private testrunMqttService: TestRunMqttService, private store: Store, - private ngZone: NgZone, private notificationService: NotificationService ) {} } diff --git a/modules/ui/src/app/store/reducers.spec.ts b/modules/ui/src/app/store/reducers.spec.ts index ad611e9f9..3411137fe 100644 --- a/modules/ui/src/app/store/reducers.spec.ts +++ b/modules/ui/src/app/store/reducers.spec.ts @@ -14,36 +14,43 @@ * limitations under the License. */ import * as fromReducer from './reducers'; -import { initialAppComponentState, initialSharedState } from './state'; +import { initialState as initialAppState } from './state'; import { fetchInterfacesSuccess, + fetchSystemConfigSuccess, setDeviceInProgress, setDevices, setHasConnectionSettings, setHasDevices, + setHasExpiredDevices, setHasRiskProfiles, + setIsAllDevicesOutdated, setIsOpenAddDevice, setIsOpenStartTestrun, setIsOpenWaitSnackBar, + setIsTestingComplete, + setReports, setRiskProfiles, setStatus, + setTestModules, setTestrunStatus, - toggleMenu, - updateError, - updateFocusNavigation, + updateAdapters, + updateInternetConnection, } from './actions'; -import { device } from '../mocks/device.mock'; +import { device, MOCK_TEST_MODULES } from '../mocks/device.mock'; import { MOCK_PROGRESS_DATA_CANCELLING } from '../mocks/testrun.mock'; import { PROFILE_MOCK } from '../mocks/profile.mock'; +import { HISTORY } from '../mocks/reports.mock'; +import { MOCK_ADAPTERS } from '../mocks/settings.mock'; describe('Reducer', () => { describe('unknown action', () => { it('should return the default state', () => { - const initialState = initialAppComponentState; + const initialState = initialAppState; const action = { type: 'Unknown', }; - const state = fromReducer.appComponentReducer(initialState, action); + const state = fromReducer.rootReducer(initialState, action); expect(state).toBe(initialState); }); @@ -51,13 +58,13 @@ describe('Reducer', () => { describe('fetchInterfacesSuccess action', () => { it('should update state', () => { - const initialState = initialAppComponentState; + const initialState = initialAppState; const newInterfaces = { enx00e04c020fa8: '00:e0:4c:02:0f:a8', enx207bd26205e9: '20:7b:d2:62:05:e9', }; const action = fetchInterfacesSuccess({ interfaces: newInterfaces }); - const state = fromReducer.appComponentReducer(initialState, action); + const state = fromReducer.rootReducer(initialState, action); const newState = { ...initialState, ...{ interfaces: newInterfaces } }; expect(state).toEqual(newState); @@ -65,94 +72,72 @@ describe('Reducer', () => { }); }); - describe('updateFocusNavigation action', () => { - it('should update state', () => { - const initialState = initialAppComponentState; - const action = updateFocusNavigation({ focusNavigation: true }); - const state = fromReducer.appComponentReducer(initialState, action); - - const newState = { ...initialState, ...{ focusNavigation: true } }; - expect(state).toEqual(newState); - expect(state).not.toBe(initialState); - }); - }); - - describe('toggleMenu action', () => { + describe('setHasConnectionSettings action', () => { it('should update state', () => { - const initialState = initialAppComponentState; - const action = toggleMenu(); - const state = fromReducer.appComponentReducer(initialState, action); + const initialState = initialAppState; + const action = setHasConnectionSettings({ hasConnectionSettings: true }); + const state = fromReducer.sharedReducer(initialState, action); + const newState = { ...initialState, ...{ hasConnectionSettings: true } }; - const newState = { ...initialState, ...{ isMenuOpen: true } }; expect(state).toEqual(newState); expect(state).not.toBe(initialState); }); }); - describe('setHasConnectionSettings action', () => { + describe('setIsOpenAddDevice action', () => { it('should update state', () => { - const initialState = initialSharedState; - const action = setHasConnectionSettings({ hasConnectionSettings: true }); + const initialState = initialAppState; + const action = setIsOpenAddDevice({ isOpenAddDevice: true }); const state = fromReducer.sharedReducer(initialState, action); - const newState = { ...initialState, ...{ hasConnectionSettings: true } }; + const newState = { ...initialState, ...{ isOpenAddDevice: true } }; expect(state).toEqual(newState); expect(state).not.toBe(initialState); }); }); - describe('updateError action', () => { + describe('setIsOpenWaitSnackBar action', () => { it('should update state', () => { - const mockSettingMissedError = { - isSettingMissed: true, - devicePortMissed: true, - internetPortMissed: true, - }; - const initialState = initialAppComponentState; - const action = updateError({ - settingMissedError: mockSettingMissedError, - }); - const state = fromReducer.appComponentReducer(initialState, action); - const newState = { - ...initialState, - ...{ settingMissedError: mockSettingMissedError }, - }; + const initialState = initialAppState; + const action = setIsOpenWaitSnackBar({ isOpenWaitSnackBar: true }); + const state = fromReducer.sharedReducer(initialState, action); + const newState = { ...initialState, ...{ isOpenWaitSnackBar: true } }; expect(state).toEqual(newState); expect(state).not.toBe(initialState); }); }); - describe('setIsOpenAddDevice action', () => { + describe('setHasDevices action', () => { it('should update state', () => { - const initialState = initialSharedState; - const action = setIsOpenAddDevice({ isOpenAddDevice: true }); + const initialState = initialAppState; + const action = setHasDevices({ hasDevices: true }); const state = fromReducer.sharedReducer(initialState, action); - const newState = { ...initialState, ...{ isOpenAddDevice: true } }; + const newState = { ...initialState, ...{ hasDevices: true } }; expect(state).toEqual(newState); expect(state).not.toBe(initialState); }); }); - describe('setIsOpenWaitSnackBar action', () => { + describe('setHasExpiredDevices action', () => { it('should update state', () => { - const initialState = initialSharedState; - const action = setIsOpenWaitSnackBar({ isOpenWaitSnackBar: true }); + const initialState = initialAppState; + const action = setHasExpiredDevices({ hasExpiredDevices: true }); const state = fromReducer.sharedReducer(initialState, action); - const newState = { ...initialState, ...{ isOpenWaitSnackBar: true } }; + const newState = { ...initialState, ...{ hasExpiredDevices: true } }; expect(state).toEqual(newState); expect(state).not.toBe(initialState); }); }); - describe('setHasDevices action', () => { + describe('setIsAllDevicesOutdated action', () => { it('should update state', () => { - const initialState = initialSharedState; - const action = setHasDevices({ hasDevices: true }); + const initialState = initialAppState; + const action = setIsAllDevicesOutdated({ isAllDevicesOutdated: true }); const state = fromReducer.sharedReducer(initialState, action); - const newState = { ...initialState, ...{ hasDevices: true } }; + const newState = { ...initialState, ...{ isAllDevicesOutdated: true } }; expect(state).toEqual(newState); expect(state).not.toBe(initialState); @@ -161,7 +146,7 @@ describe('Reducer', () => { describe('setDevices action', () => { it('should update state', () => { - const initialState = initialSharedState; + const initialState = initialAppState; const devices = [device, device]; const action = setDevices({ devices }); const state = fromReducer.sharedReducer(initialState, action); @@ -174,7 +159,7 @@ describe('Reducer', () => { describe('setHasRiskProfiles action', () => { it('should update state', () => { - const initialState = initialSharedState; + const initialState = initialAppState; const action = setHasRiskProfiles({ hasRiskProfiles: true }); const state = fromReducer.sharedReducer(initialState, action); const newState = { ...initialState, ...{ hasRiskProfiles: true } }; @@ -186,7 +171,7 @@ describe('Reducer', () => { describe('setRiskProfiles action', () => { it('should update state', () => { - const initialState = initialSharedState; + const initialState = initialAppState; const riskProfiles = [PROFILE_MOCK]; const action = setRiskProfiles({ riskProfiles }); const state = fromReducer.sharedReducer(initialState, action); @@ -199,7 +184,7 @@ describe('Reducer', () => { describe('setDeviceInProgress action', () => { it('should update state', () => { - const initialState = initialSharedState; + const initialState = initialAppState; const deviceInProgress = device; const action = setDeviceInProgress({ device: deviceInProgress }); const state = fromReducer.sharedReducer(initialState, action); @@ -215,7 +200,7 @@ describe('Reducer', () => { describe('setTestrunStatus action', () => { it('should update state', () => { - const initialState = initialSharedState; + const initialState = initialAppState; const action = setTestrunStatus({ systemStatus: MOCK_PROGRESS_DATA_CANCELLING, }); @@ -230,9 +215,26 @@ describe('Reducer', () => { }); }); + describe('setIsTestingComplete action', () => { + it('should update state', () => { + const initialState = initialAppState; + const action = setIsTestingComplete({ + isTestingComplete: true, + }); + const state = fromReducer.sharedReducer(initialState, action); + const newState = { + ...initialState, + ...{ isTestingComplete: true }, + }; + + expect(state).toEqual(newState); + expect(state).not.toBe(initialState); + }); + }); + describe('setIsOpenStartTestrun action', () => { it('should update state', () => { - const initialState = initialSharedState; + const initialState = initialAppState; const action = setIsOpenStartTestrun({ isOpenStartTestrun: true }); const state = fromReducer.sharedReducer(initialState, action); const newState = { ...initialState, ...{ isOpenStartTestrun: true } }; @@ -244,7 +246,7 @@ describe('Reducer', () => { describe('setStatus action', () => { it('should update state', () => { - const initialState = initialSharedState; + const initialState = initialAppState; const action = setStatus({ status: MOCK_PROGRESS_DATA_CANCELLING.status, }); @@ -258,4 +260,85 @@ describe('Reducer', () => { expect(state).not.toBe(initialState); }); }); + + describe('setReports action', () => { + it('should update state', () => { + const initialState = initialAppState; + const action = setReports({ + reports: HISTORY, + }); + const state = fromReducer.sharedReducer(initialState, action); + const newState = { + ...initialState, + ...{ reports: HISTORY }, + }; + + expect(state).toEqual(newState); + expect(state).not.toBe(initialState); + }); + }); + + describe('setTestModules action', () => { + it('should update state', () => { + const initialState = initialAppState; + const action = setTestModules({ + testModules: MOCK_TEST_MODULES, + }); + const state = fromReducer.sharedReducer(initialState, action); + const newState = { + ...initialState, + ...{ testModules: MOCK_TEST_MODULES }, + }; + + expect(state).toEqual(newState); + expect(state).not.toBe(initialState); + }); + }); + + describe('updateAdapters action', () => { + it('should update state', () => { + const initialState = initialAppState; + const action = updateAdapters({ + adapters: MOCK_ADAPTERS, + }); + const state = fromReducer.sharedReducer(initialState, action); + const newState = { + ...initialState, + ...{ adapters: MOCK_ADAPTERS }, + }; + + expect(state).toEqual(newState); + expect(state).not.toBe(initialState); + }); + }); + + describe('updateInternetConnection action', () => { + it('should update state', () => { + const initialState = initialAppState; + const action = updateInternetConnection({ internetConnection: true }); + const state = fromReducer.sharedReducer(initialState, action); + const newState = { ...initialState, ...{ internetConnection: true } }; + + expect(state).toEqual(newState); + expect(state).not.toBe(initialState); + }); + }); + + describe('fetchSystemConfigSuccess action', () => { + it('should update state', () => { + const initialState = initialAppState; + + const action = fetchSystemConfigSuccess({ + systemConfig: { network: {} }, + }); + const state = fromReducer.rootReducer(initialState, action); + + const newState = { + ...initialState, + ...{ systemConfig: { network: {} } }, + }; + expect(state).toEqual(newState); + expect(state).not.toBe(initialState); + }); + }); }); diff --git a/modules/ui/src/app/store/reducers.ts b/modules/ui/src/app/store/reducers.ts index 501c231a5..e51fde1b7 100644 --- a/modules/ui/src/app/store/reducers.ts +++ b/modules/ui/src/app/store/reducers.ts @@ -13,34 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { combineReducers, createReducer, on } from '@ngrx/store'; +import { createReducer, on } from '@ngrx/store'; import * as Actions from './actions'; -import { initialAppComponentState, initialSharedState } from './state'; +import { initialState } from './state'; export const appFeatureKey = 'app'; -export const appComponentReducer = createReducer( - initialAppComponentState, - on(Actions.toggleMenu, state => ({ - ...state, - isMenuOpen: !state.isMenuOpen, - })), - on(Actions.fetchInterfacesSuccess, (state, { interfaces }) => ({ - ...state, - interfaces, - })), - on(Actions.updateFocusNavigation, (state, { focusNavigation }) => ({ - ...state, - focusNavigation, - })), - on(Actions.updateError, (state, { settingMissedError }) => ({ - ...state, - settingMissedError, - })) -); - export const sharedReducer = createReducer( - initialSharedState, + initialState, on(Actions.setHasConnectionSettings, (state, { hasConnectionSettings }) => { return { ...state, @@ -65,6 +45,18 @@ export const sharedReducer = createReducer( hasDevices, }; }), + on(Actions.setHasExpiredDevices, (state, { hasExpiredDevices }) => { + return { + ...state, + hasExpiredDevices, + }; + }), + on(Actions.setIsAllDevicesOutdated, (state, { isAllDevicesOutdated }) => { + return { + ...state, + isAllDevicesOutdated, + }; + }), on(Actions.setDevices, (state, { devices }) => { return { ...state, @@ -89,6 +81,12 @@ export const sharedReducer = createReducer( systemStatus, }; }), + on(Actions.setIsTestingComplete, (state, { isTestingComplete }) => { + return { + ...state, + isTestingComplete, + }; + }), on(Actions.setIsOpenStartTestrun, (state, { isOpenStartTestrun }) => { return { ...state, @@ -106,10 +104,39 @@ export const sharedReducer = createReducer( ...state, status, }; - }) + }), + on(Actions.setReports, (state, { reports }) => { + return { + ...state, + reports, + }; + }), + on(Actions.setTestModules, (state, { testModules }) => { + return { + ...state, + testModules, + }; + }), + on(Actions.updateAdapters, (state, { adapters }) => { + return { + ...state, + adapters, + }; + }), + on(Actions.updateInternetConnection, (state, { internetConnection }) => { + return { + ...state, + internetConnection, + }; + }), + on(Actions.fetchInterfacesSuccess, (state, { interfaces }) => ({ + ...state, + interfaces, + })), + on(Actions.fetchSystemConfigSuccess, (state, { systemConfig }) => ({ + ...state, + systemConfig, + })) ); -export const rootReducer = combineReducers({ - appComponent: appComponentReducer, - shared: sharedReducer, -}); +export const rootReducer = sharedReducer; diff --git a/modules/ui/src/app/store/selectors.spec.ts b/modules/ui/src/app/store/selectors.spec.ts index e8d31efc8..ba6a52315 100644 --- a/modules/ui/src/app/store/selectors.spec.ts +++ b/modules/ui/src/app/store/selectors.spec.ts @@ -16,53 +16,53 @@ import { AppState } from './state'; import { + selectAdapters, selectDeviceInProgress, selectDevices, - selectError, selectHasConnectionSettings, selectHasDevices, selectHasRiskProfiles, - selectInterfaces, selectIsOpenAddDevice, selectIsOpenStartTestrun, selectIsOpenWaitSnackBar, - selectMenuOpened, + selectReports, selectRiskProfiles, selectStatus, selectSystemStatus, + selectTestModules, + selectHasExpiredDevices, + selectInternetConnection, + selectIsAllDevicesOutdated, + selectIsTestingComplete, + selectInterfaces, + selectSystemConfig, } from './selectors'; describe('Selectors', () => { const initialState: AppState = { - appComponent: { - isMenuOpen: false, - interfaces: {}, - isStatusLoaded: false, - devicesLength: 0, - focusNavigation: false, - settingMissedError: null, - }, - shared: { - hasConnectionSettings: false, - devices: [], - hasDevices: false, - isOpenAddDevice: false, - riskProfiles: [], - hasRiskProfiles: false, - isStopTestrun: false, - isOpenWaitSnackBar: false, - isOpenStartTestrun: false, - systemStatus: null, - deviceInProgress: null, - status: null, - }, + hasConnectionSettings: false, + isAllDevicesOutdated: false, + devices: [], + hasDevices: false, + hasExpiredDevices: false, + isOpenAddDevice: false, + riskProfiles: [], + hasRiskProfiles: false, + isStopTestrun: false, + isOpenWaitSnackBar: false, + isOpenStartTestrun: false, + systemStatus: null, + deviceInProgress: null, + status: null, + isTestingComplete: false, + reports: [], + testModules: [], + adapters: {}, + internetConnection: null, + interfaces: {}, + systemConfig: { network: {} }, }; - it('should select the is menu opened', () => { - const result = selectMenuOpened.projector(initialState); - expect(result).toEqual(false); - }); - it('should select interfaces', () => { const result = selectInterfaces.projector(initialState); expect(result).toEqual({}); @@ -73,11 +73,6 @@ describe('Selectors', () => { expect(result).toEqual(false); }); - it('should select settingMissedError', () => { - const result = selectError.projector(initialState); - expect(result).toEqual(null); - }); - it('should select devices', () => { const result = selectDevices.projector(initialState); expect(result).toEqual([]); @@ -88,6 +83,16 @@ describe('Selectors', () => { expect(result).toEqual(false); }); + it('should select hasExpiredDevices', () => { + const result = selectHasExpiredDevices.projector(initialState); + expect(result).toEqual(false); + }); + + it('should select isAllDevicesOutdated', () => { + const result = selectIsAllDevicesOutdated.projector(initialState); + expect(result).toEqual(false); + }); + it('should select riskProfiles', () => { const result = selectRiskProfiles.projector(initialState); expect(result).toEqual([]); @@ -108,6 +113,11 @@ describe('Selectors', () => { expect(result).toEqual(null); }); + it('should select isTestingComplete', () => { + const result = selectIsTestingComplete.projector(initialState); + expect(result).toEqual(false); + }); + it('should select isOpenStartTestrun', () => { const result = selectIsOpenStartTestrun.projector(initialState); expect(result).toEqual(false); @@ -127,4 +137,29 @@ describe('Selectors', () => { const result = selectStatus.projector(initialState); expect(result).toEqual(null); }); + + it('should select status', () => { + const result = selectReports.projector(initialState); + expect(result).toEqual([]); + }); + + it('should select testModules', () => { + const result = selectTestModules.projector(initialState); + expect(result).toEqual([]); + }); + + it('should select adapters', () => { + const result = selectAdapters.projector(initialState); + expect(result).toEqual({}); + }); + + it('should select internetConnection', () => { + const result = selectInternetConnection.projector(initialState); + expect(result).toEqual(null); + }); + + it('should select systemConfig', () => { + const result = selectSystemConfig.projector(initialState); + expect(result).toEqual({ network: {} }); + }); }); diff --git a/modules/ui/src/app/store/selectors.ts b/modules/ui/src/app/store/selectors.ts index 2f42db3d6..2a3fb0ed9 100644 --- a/modules/ui/src/app/store/selectors.ts +++ b/modules/ui/src/app/store/selectors.ts @@ -22,74 +22,103 @@ export const selectAppState = createFeatureSelector( fromApp.appFeatureKey ); -export const selectMenuOpened = createSelector( - selectAppState, - (state: AppState) => state.appComponent.isMenuOpen -); - export const selectInterfaces = createSelector( selectAppState, - (state: AppState) => state.appComponent.interfaces + (state: AppState) => state.interfaces ); export const selectHasConnectionSettings = createSelector( selectAppState, - (state: AppState) => state.shared.hasConnectionSettings + (state: AppState) => state.hasConnectionSettings ); export const selectIsOpenAddDevice = createSelector( selectAppState, - (state: AppState) => state.shared.isOpenAddDevice + (state: AppState) => state.isOpenAddDevice ); export const selectHasDevices = createSelector( selectAppState, - (state: AppState) => state.shared.hasDevices + (state: AppState) => state.hasDevices +); + +export const selectHasExpiredDevices = createSelector( + selectAppState, + (state: AppState) => state.hasExpiredDevices +); +export const selectIsAllDevicesOutdated = createSelector( + selectAppState, + (state: AppState) => state.isAllDevicesOutdated ); export const selectDevices = createSelector( selectAppState, - (state: AppState) => state.shared.devices + (state: AppState) => state.devices ); export const selectDeviceInProgress = createSelector( selectAppState, - (state: AppState) => state.shared.deviceInProgress + (state: AppState) => state.deviceInProgress ); export const selectHasRiskProfiles = createSelector( selectAppState, - (state: AppState) => state.shared.hasRiskProfiles + (state: AppState) => state.hasRiskProfiles ); export const selectRiskProfiles = createSelector( selectAppState, - (state: AppState) => state.shared.riskProfiles -); - -export const selectError = createSelector( - selectAppState, - (state: AppState) => state.appComponent.settingMissedError + (state: AppState) => state.riskProfiles ); export const selectSystemStatus = createSelector( selectAppState, (state: AppState) => { - return state.shared.systemStatus; + return state.systemStatus; } ); +export const selectIsTestingComplete = createSelector( + selectAppState, + (state: AppState) => state.isTestingComplete +); + export const selectIsOpenWaitSnackBar = createSelector( selectAppState, - (state: AppState) => state.shared.isOpenWaitSnackBar + (state: AppState) => state.isOpenWaitSnackBar ); export const selectIsOpenStartTestrun = createSelector( selectAppState, - (state: AppState) => state.shared.isOpenStartTestrun + (state: AppState) => state.isOpenStartTestrun ); export const selectStatus = createSelector( selectAppState, - (state: AppState) => state.shared.status + (state: AppState) => state.status +); + +export const selectReports = createSelector( + selectAppState, + (state: AppState) => state.reports +); + +export const selectTestModules = createSelector( + selectAppState, + (state: AppState) => state.testModules +); + +export const selectAdapters = createSelector( + selectAppState, + (state: AppState) => state.adapters +); + +export const selectInternetConnection = createSelector( + selectAppState, + (state: AppState) => state.internetConnection +); + +export const selectSystemConfig = createSelector( + selectAppState, + (state: AppState) => state.systemConfig ); diff --git a/modules/ui/src/app/store/state.ts b/modules/ui/src/app/store/state.ts index e2528c5a0..2ce978b6f 100644 --- a/modules/ui/src/app/store/state.ts +++ b/modules/ui/src/app/store/state.ts @@ -14,37 +14,26 @@ * limitations under the License. */ import { TestrunStatus } from '../model/testrun-status'; -import { SettingMissedError, SystemInterfaces } from '../model/setting'; -import { Device } from '../model/device'; +import { Device, TestModule } from '../model/device'; +import { Adapters, SystemConfig, SystemInterfaces } from '../model/setting'; import { Profile } from '../model/profile'; export interface AppState { - appComponent: AppComponentState; - shared: SharedState; -} - -export interface AppComponentState { - isMenuOpen: boolean; + // app, settings interfaces: SystemInterfaces; - /** - * Indicates, if side menu should be focused on keyboard navigation after menu is opened - */ - focusNavigation: boolean; - settingMissedError: SettingMissedError | null; - isStatusLoaded: boolean; // TODO should be updated in effect when fetch status - devicesLength: number; // TODO should be renamed to focusToggleSettingsBtn (true when devices.length > 0) and updated in effect when fetch device -} - -export interface SharedState { + systemConfig: SystemConfig; devices: Device[]; //used in app, devices, testrun hasDevices: boolean; + hasExpiredDevices: boolean; + isAllDevicesOutdated: boolean; //app, risk-assessment, testrun, reports riskProfiles: Profile[]; hasRiskProfiles: boolean; //app, testrun status: string | null; systemStatus: TestrunStatus | null; + isTestingComplete: boolean; //app, settings hasConnectionSettings: boolean | null; // app, devices @@ -54,28 +43,32 @@ export interface SharedState { isStopTestrun: boolean; isOpenWaitSnackBar: boolean; deviceInProgress: Device | null; + reports: TestrunStatus[]; + testModules: TestModule[]; + adapters: Adapters; + internetConnection: boolean | null; } -export const initialAppComponentState: AppComponentState = { - isMenuOpen: false, - interfaces: {}, - focusNavigation: false, - isStatusLoaded: false, - devicesLength: 0, - settingMissedError: null, -}; - -export const initialSharedState: SharedState = { +export const initialState: AppState = { hasConnectionSettings: null, isOpenAddDevice: false, isStopTestrun: false, isOpenWaitSnackBar: false, hasDevices: false, + hasExpiredDevices: false, + isAllDevicesOutdated: false, devices: [], deviceInProgress: null, riskProfiles: [], hasRiskProfiles: false, isOpenStartTestrun: false, systemStatus: null, + isTestingComplete: false, status: null, + reports: [], + testModules: [], + adapters: {}, + internetConnection: null, + interfaces: {}, + systemConfig: { network: {} }, }; diff --git a/modules/ui/src/assets/icons/create_device_header.svg b/modules/ui/src/assets/icons/create_device_header.svg new file mode 100644 index 000000000..fd10cfa8f --- /dev/null +++ b/modules/ui/src/assets/icons/create_device_header.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/ui/src/assets/icons/pilot.svg b/modules/ui/src/assets/icons/pilot.svg new file mode 100644 index 000000000..0ce8298a3 --- /dev/null +++ b/modules/ui/src/assets/icons/pilot.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/modules/ui/src/assets/icons/qualification.svg b/modules/ui/src/assets/icons/qualification.svg new file mode 100644 index 000000000..732e417cb --- /dev/null +++ b/modules/ui/src/assets/icons/qualification.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/modules/ui/src/index.html b/modules/ui/src/index.html index 091591cac..1c79ceb31 100644 --- a/modules/ui/src/index.html +++ b/modules/ui/src/index.html @@ -28,6 +28,22 @@ j.src = 'https://www.googletagmanager.com/gtm.js?id=' + i + dl; f.parentNode.insertBefore(j, f); })(window, document, 'script', 'dataLayer', 'GTM-NDFZ7L89'); + + Storage.prototype.setObject = function (key, value) { + if (value instanceof Map) { + this.setItem( + key, + JSON.stringify(Object.fromEntries(value.entries())) + ); + } else { + this.setItem(key, JSON.stringify(value)); + } + }; + + Storage.prototype.getObject = function (key) { + var value = this.getItem(key); + return value && JSON.parse(value); + }; @@ -53,7 +69,7 @@ +
+ {# Badge #} +

+ {% if json_data['device']['test_pack'] == 'Device Qualification' %} + + Device Qualification + {% else %} + + Pilot Assessment + {% endif %} +

+

{{ title }}

+
+

+ {{ device['manufacturer'] }} + {{ device['model']}} +

+ {% else %} +
+
+ {# Badge #} +

+ {% if json_data['device']['test_pack'] == 'Device Qualification' %} + + Device Qualification + {% else %} + + Pilot Assessment + {% endif %} +

+ {{ title }} +
+ + {{ device['manufacturer'] }} + {{ device['model']}} + + {% endif %} + Testrun +
+{% endmacro %} \ No newline at end of file diff --git a/resources/report/pilot-icon.png b/resources/report/pilot-icon.png new file mode 100644 index 000000000..8f9f9d0f8 Binary files /dev/null and b/resources/report/pilot-icon.png differ diff --git a/resources/report/qualification-icon.png b/resources/report/qualification-icon.png new file mode 100644 index 000000000..7e7448ce3 Binary files /dev/null and b/resources/report/qualification-icon.png differ diff --git a/resources/report/risk_report_styles.css b/resources/report/risk_report_styles.css new file mode 100644 index 000000000..dfe44349c --- /dev/null +++ b/resources/report/risk_report_styles.css @@ -0,0 +1,211 @@ +/* Set some global variables */ + :root { + --header-height: .75in; + --header-width: 8.5in; + --header-pos-x: 0in; + --header-pos-y: 0in; + --page-width: 8.5in; + } + + @font-face { + font-family: 'Google Sans'; + font-style: normal; + src: url(https://fonts.gstatic.com/s/googlesans/v58/4Ua_rENHsxJlGDuGo1OIlJfC6l_24rlCK1Yo_Iqcsih3SAyH6cAwhX9RFD48TE63OOYKtrwEIJllpyk.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; + } + + @font-face { + font-family: 'Roboto Mono'; + font-style: normal; + src: url(https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; + } + + /* Define some common body formatting*/ + body { + font-family: 'Google Sans', sans-serif; + margin: 0px; + padding: 0px; + } + + /* Sets proper page size during print to pdf for weasyprint */ + @page { + size: Letter; + width: 8.5in; + height: 11in; + } + + .page { + position: relative; + margin: 0 20px; + width: 8.5in; + height: 11in; + } + + /* Define the header related css elements*/ + .header { + position: relative; + } + + h1 { + margin: 0 0 8px 0; + font-size: 20px; + font-weight: 400; + } + + h2 { + margin: 0px; + font-size: 48px; + font-weight: 700; + } + + h3 { + font-size: 24px; + margin-bottom: 10px; + margin-top: 15px; + } + + h4 { + font-size: 12px; + font-weight: 500; + color: #5F6368; + margin-bottom: 0; + margin-top: 0; + } + + /* CSS for the footer */ + .footer { + position: absolute; + height: 30px; + width: 8.5in; + bottom: 0in; + border-top: 1px solid #D3D3D3; + } + + .footer-label { + color: #3C4043; + position: absolute; + top: 5px; + font-size: 12px; + } + + @media print { + @page { + size: Letter; + width: 8.5in; + height: 11in; + } + } + + .risk-banner { + min-height: 120px; + padding: 5px 40px 0 40px; + margin-top: 30px; + } + + .risk-banner-limited { + background-color: #E4F7FB; + color: #007B83; + } + + .risk-banner-high { + background-color: #FCE8E6; + color: #C5221F; + } + + .risk-banner-title { + text-transform: uppercase; + font-weight: bold; + } + + .risk-table { + width: 100%; + margin-top: 20px; + text-align: left; + color: #3C4043; + font-size: 14px; + } + + .risk-table-head { + margin-bottom: 15px; + font-size: 14px; + margin-top: 40px; + text-align: left; + } + + .risk-table-head-question { + display: inline-block; + margin-left: 70px; + font-weight: bold; + } + + .risk-table-head-answer { + display: inline-block; + margin-left: 325px; + font-weight: bold; + } + + .risk-table-row { + margin-bottom: 8px; + background-color: #F8F9FA; + display: flex; + align-items: stretch; + overflow: hidden; + } + + .risk-question-no { + padding: 15px 20px; + width: 10px; + display: inline-block; + vertical-align: top; + position: relative; + } + + .risk-question { + padding: 15px 20px; + display: inline-block; + width: 350px; + vertical-align: top; + position: relative; + height: 100%; + } + + .risk-answer { + background-color: #E8F0FE; + padding: 15px 20px; + display: inline-block; + width: 340px; + position: relative; + height: 100%; + } + + ul { + margin-top: 0; + } + + .risk-label{ + position: absolute; + top: 0px; + right: 0px; + width: 52px; + height: 16px; + font-family: 'Google Sans', sans-serif; + font-size: 8px; + font-weight: 500; + line-height: 16px; + letter-spacing: 0.64px; + text-align: center; + font-weight: bold; + border-radius: 3px; + } + + .risk-label-high{ + background-color: #FCE8E6; + color: #C5221F; + } + + .risk-label-limited{ + width: 65px; + background-color:#E4F7FB; + color: #007B83; + } \ No newline at end of file diff --git a/resources/report/risk_report_template.html b/resources/report/risk_report_template.html new file mode 100644 index 000000000..f3a6c927f --- /dev/null +++ b/resources/report/risk_report_template.html @@ -0,0 +1,75 @@ + + + + + + + Risk Assessment + + + + + {% for page in pages %} +
+
+

Risk assessment

+

{{ manufacturer }} {{ model }}

+ Testrun +
+ {# Risk banner #} + + {% if loop.first and risk is not none %} +
+
+

{{ risk.lower() }} Risk

+
+
+ {% if risk == "High" %} + {{ high_risk_message }} + {% else %} + {{ limited_risk_message }} + {% endif %} +
+
+ {% endif %} + + + {# Risk table #} +
+
Question
+
Answer
+
+
+ {% for question in page %} +
+
{{ question['index'] }}.
+
{{ question['question'] }}
+
+ {% if question['answer'] is string %} + {{ question['answer'] }} + {% elif question['answer'] is sequence %} +
    + {% for answer in question['answer'] %} +
  • {{ answer }}
  • + {% endfor %} +
+ {% endif %} + {% if 'risk' in question %} +
{{ question['risk'].upper()}} RISK
+ {% endif %} +
+
+ + {% endfor %} + + +
+
+ {% endfor %} + + + \ No newline at end of file diff --git a/resources/report/test_report_styles.css b/resources/report/test_report_styles.css new file mode 100644 index 000000000..8168da3f2 --- /dev/null +++ b/resources/report/test_report_styles.css @@ -0,0 +1,635 @@ +/* Set some global variables */ + :root { + --header-height: .75in; + --header-width: 8.5in; + --header-pos-x: 0in; + --header-pos-y: 0in; + --page-width: 8.5in; + --summary-height: 2.8in; + --vertical-line-height: calc(var(--summary-height)-.2in); + --vertical-line-pos-x: 25%; + } + + @font-face { + font-family: 'Google Sans'; + font-style: normal; + src: url(https://fonts.gstatic.com/s/googlesans/v58/4Ua_rENHsxJlGDuGo1OIlJfC6l_24rlCK1Yo_Iqcsih3SAyH6cAwhX9RFD48TE63OOYKtrwEIJllpyk.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; + } + + @font-face { + font-family: 'Roboto Mono'; + font-style: normal; + src: url(https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; + } + + /* Define some common body formatting*/ + body { + font-family: 'Google Sans', sans-serif; + margin: 0; + padding: 0; + } + + /* Use this for various section breaks*/ + .gradient-line { + position: relative; + background-image: linear-gradient(to right, red, blue, green, yellow, orange); + height: 1px; + /* Adjust the height as needed */ + width: 100%; + /* To span the entire width */ + display: block; + /* Ensures it's a block-level element */ + } + + /* Sets proper page size during print to pdf for weasyprint */ + @page { + size: Letter; + width: 8.5in; + height: 11in; + } + + .page { + position: relative; + margin: 0 20px; + width: 8.5in; + height: 11in; + } + + /* Define the header related css elements*/ + .header { + position: relative; + border-bottom: 1px solid #DADCE0; + padding-bottom: 20px; + } + + .header.first-page { + border-bottom: none; + } + + .header-info { + max-width: 600px; + box-sizing: border-box; + display: flex; + align-items: center; + margin-bottom: 8px; + color: #202124; + font-size: 9px; + text-transform: uppercase; + } + + .first-page .header-info { + font-size: 18px; + } + + .header-info .header-info-badge { + margin-right: 8px; + } + + .first-page .header-info .header-info-badge { + margin-right: 16px; + } + + .header-info-badge { + align-items: center; + margin: 0; + padding: 5px 15px; + border: 1px solid #202124; + border-radius: 4px; + font-weight: 500; + letter-spacing: 0.64px; + box-sizing: border-box; + } + + .first-page .header-info-badge { + padding: 15px 30px; + letter-spacing: 1px; + } + + .header-info-badge img { + width: 9px; + height: 9px; + margin-right: 10px; + } + + .first-page .header-info-badge img { + width: 16px; + height: 16px; + } + + .header-info h1 { + margin: 0; + font-size: 9px; + font-weight: 700; + letter-spacing: 0.1px; + } + + .first-page .header-info h1 { + font-size: 18px; + letter-spacing: 1px; + } + + .header-info-device { + margin-top: 0; + max-width: 700px; + margin-bottom: 24px; + font-size: 24px; + font-weight: bold; + } + + .first-page .header-info-device { + margin: 0; + font-size: 48px; + font-weight: 700; + } + + h1 { + margin: 0 0 8px 0; + font-size: 20px; + font-weight: 400; + } + + h2 { + margin: 0; + font-size: 48px; + font-weight: 700; + } + + h3 { + font-size: 24px; + } + + h4 { + font-size: 12px; + font-weight: 500; + color: #5F6368; + margin-bottom: 0; + margin-top: 0; + } + + .module-summary { + background-color: #F8F9FA; + width: 100%; + margin-bottom: 25px; + } + + .module-summary thead tr th { + text-align: left; + padding-top: 15px; + padding-left: 15px; + font-weight: 500; + color: #5F6368; + font-size: 14px; + } + + .module-summary tbody tr td { + padding-bottom: 15px; + padding-left: 15px; + font-size: 24px; + } + + .module-data { + border: 1px solid #DADCE0; + border-radius: 3px; + border-spacing: 0; + } + + .module-data thead tr th { + text-align: left; + padding: 12px 25px; + color: #3C4043; + font-size: 14px; + font-weight: 700; + } + + .module-data tbody tr td { + text-align: left; + padding: 12px 25px; + color: #3C4043; + font-size: 14px; + font-weight: 400; + border-top: 1px solid #DADCE0; + font-family: 'Roboto Mono', monospace; + word-wrap: break-word; + word-break: break-word; + } + + div.steps-to-resolve { + background-color: #F8F9FA; + margin-bottom: 30px; + width: 756px; + padding: 20px 30px; + vertical-align: top; + } + + .steps-to-resolve-row { + vertical-align: top; + } + + .steps-to-resolve-row.content { + margin-left: 70px; + } + + .steps-to-resolve-test-name { + display: inline-block; + margin-left: 70px; + margin-right: 10px; + margin-bottom: 20px; + width: 250px; + vertical-align: top; + } + + .steps-to-resolve-description { + display: inline-block; + } + + .steps-to-resolve.subtitle { + text-align: left; + padding-top: 15px; + font-weight: 500; + color: #5F6368; + font-size: 14px; + } + + .steps-to-resolve-index { + font-size: 40px; + position: absolute; + margin-left: 10px; + } + + .callout-container.info { + background-color: #e8f0fe; + } + + .callout-container.info .icon { + width: 22px; + height: 22px; + margin-right: 10px; + background-size: contain; + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEwAAABOCAYAAACKX/AgAAAABHNCSVQICAgIfAhkiAAACYVJREFUeF7tXGtsVEUUPi0t0NIHli5Uni1I5KVYiCgPtQV8BcSIBkVUjFI0GiNGhR9KiIEfIqIkRlSqRlBQAVEREx9AqwIqClV5imILCBT6gHZLW2gLnm+xZHM5d2fm7t1tN9kv2R+dO3fmzHfncV7TmNKTZ89RFNoMxGrXjFb0MRAlzHAiRAmLEmbIgGH16AyLEmbIgGH16AyLEmbIgGH1OMP6rlVvZH1518E62nO4jkrKz9CBstNU4W2kU6fP8q/J10+Hdm34F0udkuOol6cdZXnaUr+uCTSwZwLFxca4JotJQzHh1PS9dU307Y4q2rjTS0XFp6j2zFkTWS/UTWwbS9m9O9CYgck09spUSm7fxlE7Tl4KC2F/H6un/PVlVLC7mhoa3bXE4uNiKHdACk0f66E+Xdo74cDonZASdryqgV7/5jit23aCQm2xtuElOn5IR3rsps7UOTXeiASTyiEhDEvv3cJyWrG5nM40uDujVINrFxdLk0d1oody0ik5wf2l6jphW/+uoZnLD1FV7fmNWzVA6Xnzfh7MrOzYIY7mT+lOw/okSV04LnOVsI+3VNDLX5QSTkAdJPEJOLJfCg3JSvAtI08y/1LjKC3p/OFdWdNIZVX88zYQlve24lrastdLNXyS6gAn6bMTMmjS8E461bXquEJYQ9M5mv/5Ufrk50plpyBjzKAUyuETbljvJIrjTdsEjXxobP2nhgp3eWnDzmoCqSrcdU0azbz9UopvY9aX1G7QhFXz0ntq2UHazmpCIECfmnpDOt1/fTq1j3fHwKhjteT978tpGf+gvwXCUFZDXrm/J6UkBrevBUUYZtaj+SUBycJXnchf+JExHrrk/6UWaGBOnmGWLdlQRp/8VBlwOwBpb0zLDGqmBUXYvDVHAi7DjI7xtGhqL7q8a+j1IxC990gdzXjvIB3j/c4OWJ7PTexq91hZ7nhtfLS5IiBZV2Um0vIn+oSNLIwUZhP6HMx922E177Mrf6ywe6wsd0TYz3/V0MJ1pbaNTxh6CeXnZV047WwrhuAB7M63uW/IYIcFa0tp6/4au8cBy40Jg1I6a8Uh270Cgr4wqZvx6RdQSsOHOHkhgx1pUHtmLf+XMBZTGBP2TkG5rVKKpTA7iP0Bpx4Mcv8fypwCstgtz5OnGn3WiCmMNn1sphMW7BPNHWzw2D+alU5TQVB/xOzdZCUogT0TW+YOcNKc7x24jKa8tl88CGBGrZ3Z18j2NJphi78+LpIF1QGnYTBkOWZE8SL2tEUP9hT9Z6cbz9Jidg6YQJuwv0rrad32E2Lbd16bFtbTUBQiQCFOT8goYd32k7Sf3U+60CYsnxVDyUSEBj99tEe3vxarN50VZ8hqRRMPagn76nRxcQvCmzhNCtn5JwHmTqg0eKk/p2XYLh5gs0wCHJveer0TU4uwr36vEj2lEAK2YaQAskr7LLzA6/+o0hqGVhCkYJc8u8ZekeqaIQ1pgzkNdUaLExeeklVsc1qxgb0fdwyT9zn/usoZBgO7iP1QEnIGJEvFrboMbiUJRf+cslXGjQjbeaiW6hsuVh7h/Luarf9IA3xwkN0KKMsI+6lw8ZuWNxA3lDCqf0qLmj+STDplMJtG9JNnGbwdKigJO1Amu0qyMxNUbbfa50OzZG9GcdkZpcxKwko4Ii0BplCkwi4Mh+i7CspTEraYBE+K+4SNnbeXai2u5kTeb9Y/308SwXEZgi0S7MbqX1dJWHOeg7WDdJtOrfVM/gZZVuPb5H3duohMSVDFBfCOcklK+Q+IG6YlBRdMkAQOVxmUVymXxW5y+MulJOycEGKMiQk+XBUuctzuR0mYncFaWaNne7ktsBvtIcokOxLUq0aDMLmRco5GRyoQTZcgTQ5rPSVhcMBJKKuOYMJsPrbdWP3HryQskzP/JByz+UpS3dZWhjwNCchyVEFJWC+PrLMUlcgGuarD1vB8e7FsAmWmt1WKpySsfzfZBNq0x0vwZEQakMyyea/srrIbq/8YlYRd0T1R9HnBQ7mNXSKRBmT+SOlSyJtFsrEKSsJg3SPsL6GAnW6RBqRJScjO6iBGlqx1lYThhdHspZSwnjOiJV+ZVLc1lEFW5JRJGD1IdvlY62oRdsvgVNH3BQXwgx/Mo8dWIcL1N3LJpAQ8ZGLfyO52HWgRhuTaHHYYSljK4XaE3Vs7TvDHXfqd/HGRtq6bQKxFGMhAHrxksGIDXbJRP67XUsS+xXFVyRuBMeXx2HShTVjfjPY0boicQrT6x0rad1Q/eqwrnFv1/jxST2ts8m/Hc7bRZQYXIrQJg/C4NID1bgX0sRnvHRD3B2vdcP+NPWvG0gOiztg2PoYe5zGZwCh7Bw0v+rKUlvLmKQHqBxLpTDOjm9uC89CqCuPzIJ7oBFBS8/KL6Tcbq+TBHA89eWsXo6aNJXko12ObiQzB5n56xEgA/8ogBgqk/88pWWh3Lufg2pGVytnUuC1iCmPCkLY9/94ehLs9Etb+eoLmrDpM+LotBfQ9Z+VhWst3nCTgwsNLU3pon4z+bRgThpev7ZtET4/LkGTxlYE0LAVJ57F9yaUH6HMa921HFrp55rYMGnaZsys1jghDp7gANTFALgKWwn2c+RfO0xOnIbINf7fZsyD3nZx2fvcI51dpjDd9/4mge7HhruFpvhwyXJgKBaCUQhfExYZAHpQhbC++mdeCFxsweN2rM8hnmMqb7H3XuXd1BrYhzB1o8JJS6v9xQNarD7Tw1ZlmgfBVX/zsKK3ZenEakXVGIcSFNKlczqLBVRbTC1PY0H9ht1Lhbi/B+NfZJ7EMZ7WWy1n+hHy4qYIWsp6GNEgd4K72qP7JlM36WxcOriKajgBxc8wTkSkEWxA/KD3ZQEUldbRpT7Xoz5L6w2n49PgMumek8z3L2m5Qe5i1Mfz9E98SwcUHLFWngMpyjgOimryL3UDPgvpzDZ/obsJ1wiAcyHq3oIxW8IVTty/FqwYPc2fyiHR6ODdCrjD7DwjLCHnwX3K6ejCzRUUSnkOPHs/Ogcdu7szLWw7c6LSjqhOSGWbtFDn+SO0u5P3HbQsAzoAc9mflcVo5PCqhRlgIax4E0teRkb2R3cRQbJ26t3GjN5uT4nIHphC8wbrOPzfIDCth/gJjpu34t9b3r2SQ5YjEvfP/SqbJp1Mh3wVGOP6dDCLSCCgjRopQ2KAeicbqiBtkoY0WI8ytAYS7Hce2ZLgFbS39RQkz/BJRwqKEGTJgWD06w6KEGTJgWD06w6KEGTJgWP0/nqir/+GPk3oAAAAASUVORK5CYII='); + } + + .callout-container { + display: flex; + box-sizing: border-box; + height: auto; + min-height: 48px; + padding: 6px 24px; + border-radius: 8px; + align-items: center; + color: #3c4043; + font-size: 14px; + } + + .device-information { + padding-top: 0.2in; + padding-left: 0.2in; + background-color: #F8F9FA; + width: 250px; + height: 100.4%; + } + + /* Define the summary related css elements*/ + .summary-content { + position: relative; + width: var(--page-width); + height: var(--summary-height); + margin-top: 19px; + margin-bottom: 19px; + background-color: #E8EAED; + padding-bottom: 20px; + } + + .summary-item-label { + position: relative; + } + + .summary-item-value { + position: relative; + font-size: 20px; + font-weight: 400; + color: #202124; + } + + .summary-item-space { + position: relative; + padding-bottom: 15px; + margin: 0; + } + + .summary-device-modules { + position: absolute; + left: 3.2in; + top: .2in; + } + + .summary-device-module-label { + font-size: 16px; + font-weight: 500; + color: #202124; + width: fit-content; + margin-bottom: 0.1in; + } + + .summary-vertical-line { + width: 1px; + height: var(--vertical-line-height); + background-color: #80868B; + position: absolute; + top: .3in; + bottom: .1in; + left: 3in; + } + + /* CSS for the color box */ + .summary-color-box { + position: absolute; + right: 0; + top: 0; + width: 2.6in; + height: 100%; + } + + .summary-box-compliant { + background-color: rgb(24, 128, 56); + } + + .summary-box-non-compliant { + background-color: #b31412; + } + + .summary-box-label { + font-size: 14px; + margin-top: 5px; + color: #DADCE0; + position: relative; + top: 10px; + left: 20px; + font-weight: 500; + } + + .summary-box-value { + font-size: 18px; + margin: 0 0 10px 0; + color: #ffffff; + position: relative; + top: 10px; + left: 20px; + } + + .result-list-title { + font-size: 24px; + color: black; + } + + .result-list { + position: relative; + margin-top: .2in; + font-size: 18px; + } + + .result-list h3, + .page-heading { + margin: 0.2in 0; + font-size: 30px; + font-weight: normal; + color: black; + } + + .result-line { + border: 1px solid #D3D3D3; + /* Light Gray border*/ + height: .4in; + width: 8.5in; + } + + .result-line-result { + border-top: 0; + } + + .result-list-header-label { + position: absolute; + font-size: 12px; + font-weight: bold; + height: 40px; + display: flex; + align-items: center; + } + + .result-test-label { + position: absolute; + font-size: 12px; + margin-top: 12px; + max-width: 300px; + font-weight: normal; + align-items: center; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + } + + .result-test-description { + max-width: 380px; + } + + .result-test-result-error { + background-color: #FCE8E6; + color: #C5221F; + } + + .result-test-result-feature-not-detected { + background-color: #e3e3e3; + } + + .result-test-result-informational { + background-color: #E0F7FA; + color: #006064; + } + + .result-test-result-non-compliant { + background-color: #FCE8E6; + color: #C5221F; + } + + .result-test-result { + position: absolute; + font-size: 12px; + width: fit-content; + height: 12px; + margin-top: 8px; + padding: 4px 4px 7px 5px; + border-radius: 2px; + left: 6.85in; + } + + .result-test-result-compliant { + background-color: #E6F4EA; + color: #137333; + } + + .result-test-result-skipped { + background-color: #e3e3e3; + color: #393939; + } + + /* CSS for the footer */ + .footer { + position: absolute; + height: 30px; + width: 8.5in; + bottom: 0; + border-top: 1px solid #D3D3D3; + } + + .footer-label { + color: #3C4043; + position: absolute; + top: 5px; + font-size: 12px; + } + + /*CSS for the markdown tables */ + .markdown-table { + border-collapse: collapse; + margin-left: 20px; + background-color: #F8F9FA; + } + + .markdown-table th, .markdown-table td { + border: none; + text-align: left; + padding: 8px; + } + + .markdown-header-h1 { + margin-top:20px; + margin-bottom:20px; + margin-right:0; + font-size: 2em; + } + + .markdown-header-h2 { + margin-top: 20px; + margin-bottom: 20px; + margin-right: 0; + font-size: 1.5em; + } + + .module-page-content { + /*Page height minus header(93px), footer(30px), + and a 20px bottom padding.*/ + height: calc(11in - 93px - 30px - 20px); + /* In case we mess something up in our calculations + we'll cut off the content of the page so + the header, footer and line break work + as expected + */ + overflow: hidden; + } + + .module-page-content h1, + .module-page-content h2 { + padding-left: 0.2in; + margin: 0.2in 0; + font-size: 30px; + font-weight: normal; + } + + /* CSS for Device profile */ + .device-profile-content { + width: 100%; + margin-top: 40px; + text-align: left; + color: #3C4043; + font-size: 14px; + } + + .device-profile-head { + margin-bottom: 15px; + } + + .device-profile-head-question { + display: inline-block; + margin-left: 70px; + font-weight: bold; + } + + .device-profile-head-answer { + display: inline-block; + margin-left: 325px; + font-weight: bold; + } + + .device-profile-row { + margin-bottom: 8px; + background-color: #F8F9FA; + display: flex; + align-items: stretch; + overflow: hidden; + } + + .device-profile-number { + padding: 15px 20px; + width: 10px; + display: inline-block; + vertical-align: top; + position: relative; + } + + .device-profile-question { + padding: 15px 20px; + display: inline-block; + width: 350px; + vertical-align: top; + position: relative; + height: 100%; + } + + .device-profile-answer { + background-color: #E8F0FE; + padding: 15px 20px; + display: inline-block; + width: 340px; + position: relative; + height: 100%; + } + + .device-profile-answer ul { + margin-top: 0; + padding-left: 20px; + } + + /* CSS for Steps to resolve to meet full device qualification in Pilot program */ + .steps-to-resolve-info { + display: flex; + flex-direction: column; + margin: 30px 0; + padding: 30px 45px; + background: #E8F0FE; + color: #174EA6; + } + + .steps-to-resolve-info-heading { + margin: 0 0 10px; + font-size: 24px; + font-weight: 500; + line-height: 16px; + letter-spacing: 0.64px; + text-transform: uppercase; + } + + .steps-to-resolve-info-content { + margin: 0; + font-size: 16px; + line-height: normal; + letter-spacing: 0.64px; + } + + @media print { + @page { + size: Letter; + width: 8.5in; + height: 11in; + } + } \ No newline at end of file diff --git a/resources/report/test_report_template.html b/resources/report/test_report_template.html new file mode 100644 index 000000000..fbd8d1c68 --- /dev/null +++ b/resources/report/test_report_template.html @@ -0,0 +1,241 @@ +{% import 'header_macros.jinja' as header_macros %} + + + + + + + Testrun Report + + + + + {% set page_index = namespace(value=0) %} + {# Test Results #} + {% for page in range(pages_num) %} + {% set page_index.value = page_index.value+1 %} +
+ {{ header_macros.header(loop.first, "Testrun report", json_data, device, logo, icon_qualification, icon_pilot)}} + {% if loop.first %} +
+
+
+

Manufacturer

+
{{ device['manufacturer']}}
+
+

Model

+
{{ device['model'] }}
+
+

Firmware

+
{{ device['firmware']}}
+
+

MAC Address

+
{{ device['mac_addr'] }}
+
+
+
+
+

Device Configuration

+
+ {% for module, enabled in modules.items() %} +
+ {% if enabled %} + + {% else %} + + {% endif %} + {{ module }} +
+ {% endfor %} +
+ {% if test_status == 'Compliant' %} +
+ {% else %} +
+ {% endif %} +
Test Status
+
Complete
+
Test Result
+
{{ test_status }}
+
Started
+
{{ json_data['started']}}
+
Duration
+
+ {% if duration.seconds//3600 > 0 %}{{ duration.seconds//3600 }}h {% endif %} + {% if duration.seconds//60 > 0 %}{{ duration.seconds//60 }}m {% endif %} + {{ duration.seconds%60 }}s +
+
+
+ {% endif %} + {% if loop.first %} + {% set results_from = 0 %} + {% set results_to = [tests_first_page, test_results|length]|min %} + {% else %} + {% set results_from = tests_first_page + (loop.index0 - 1) * tests_per_page %} + {% set results_to = [results_from + tests_per_page, test_results|length]|min %} + {% endif %} +
+

Results List ({{ successful_tests }}/{{ total_tests }})

+
+
Name
+
Description
+
Result
+
+ {% for i in range(results_from, results_to) %} +
+
{{ test_results[i]['name'] }}
+
{{ test_results[i]['description'] }}
+ {% if test_results[i]['result'] == 'Non-Compliant' %} +
+ {% elif test_results[i]['result'] == 'Compliant' %} +
+ {% elif test_results[i]['result'] == 'Error' %} +
+ {% elif test_results[i]['result'] == 'Feature Not Detected' %} +
+ {% elif test_results[i]['result'] == 'Informational' %} +
+ {% else %} +
+ {% endif %} + {{ test_results[i]['result'] }}
+
+ {% endfor %} +
+ +
+
+ {% endfor %} + {# Steps to resolve Device qualification #} + {% if steps_to_resolve|length > 0 and json_data['device']['test_pack'] == 'Device Qualification' %} + {% set page_index.value = page_index.value+1 %} +
+ {{ header_macros.header(False, "Testrun report", json_data, device, logo, icon_qualification, icon_pilot)}} +

Non-compliant tests and suggested steps to resolve

+ {% for step in steps_to_resolve %} +
+
+ {{ loop.index }}. +
+ Name
{{ step['name'] }} +
+
+ Description
{{ step["description"] }} +
+
+
+ Steps to resolve + {% for recommedtation in step['recommendations'] %} +
{{ loop.index }}. {{ recommedtation }} + {% endfor %} +
+
+ {% endfor %} + +
+ {% endif %} + {# Modules reports #} + {% for module in module_reports %} + {% set page_index.value = page_index.value+1 %} +
+ {{ header_macros.header(False, "Testrun report", json_data, device, logo, icon_qualification, icon_pilot)}} +
+ {{ module }} +
+ +
+
+ {% endfor %} + {# Device profile #} + {% set page_index.value = page_index.value+1 %} +
+ {{ header_macros.header(False, "Testrun report", json_data, device, logo, icon_qualification, icon_pilot)}} +

Device profile

+
+
+
Question
+
Answer
+
+ {% for question in json_data['device']['device_profile'] %} +
+
{{loop.index}}.
+
{{ question['question'] }}
+
+ {% if question['answer'] is string %} + {{ question['answer'] }} + {% elif question['answer'] is sequence %} +
    + {% for answer in question['answer'] %} +
  • {{ answer }}
  • + {% endfor %} +
+ {% endif %} +
+
+ {% endfor %} +
+ +
+
+ {# Pilot steps to resolve#} + {% if json_data['device']['test_pack'] == 'Pilot Assessment' and optional_steps_to_resolve|length > 0 %} + {% set page_index.value = page_index.value + 1 %} +
+ {{ header_macros.header(False, "Testrun report", json_data, device, logo, icon_qualification, icon_pilot)}} +

Recommendations for Device Qualification

+
+

Attention

+

+ The following recommendations are required solely for full device qualification. + They are optional for the pilot assessment. + But you may find it valuable to understand what will be required in the future + and our recommendations for your device. +

+
+ {% for step in optional_steps_to_resolve %} +
+
+ + {{ loop.index }}. + +
+ Name
+ {{ step['name'] }} +
+
+ Description
+ {{ step["description"] }} +
+
+
+ Steps to resolve + {% for recommedtation in step['optional_recommendations'] %} +
+ + {{ loop.index }}. {{ recommedtation }} + + {% endfor %} +
+
+ {% endfor %} + +
+ {% endif %} + + diff --git a/resources/risk_assessment.json b/resources/risk_assessment.json index b94c5b7b6..d4f2574fb 100644 --- a/resources/risk_assessment.json +++ b/resources/risk_assessment.json @@ -1,180 +1,7 @@ [ - { - "question": "What type of device is this?", - "type": "select", - "options": [ - { - "text": "Building Automation Gateway", - "risk": "High" - }, - { - "text": "IoT Gateway", - "risk": "High" - }, - { - "text": "Controller - AHU", - "risk": "High" - }, - { - "text": "Controller - Boiler", - "risk": "High" - }, - { - "text": "Controller - Chiller", - "risk": "High" - }, - { - "text": "Controller - FCU", - "risk": "Limited" - }, - { - "text": "Controller - Pump", - "risk": "Limited" - }, - { - "text": "Controller - CRAC", - "risk": "High" - }, - { - "text": "Controller - VAV", - "risk": "Limited" - }, - { - "text": "Controller - VRF", - "risk": "Limited" - }, - { - "text": "Controller - Multiple", - "risk": "High" - }, - { - "text": "Controller - Other", - "risk": "High" - }, - { - "text": "Controller - Lighting", - "risk": "Limited" - }, - { - "text": "Controller - Blinds/Facades", - "risk": "High" - }, - { - "text": "Controller - Lifts/Elevators", - "risk": "High" - }, - { - "text": "Controller - UPS", - "risk": "High" - }, - { - "text": "Sensor - Air Quality", - "risk": "Limited" - }, - { - "text": "Sensor - Vibration", - "risk": "Limited" - }, - { - "text": "Sensor - Humidity", - "risk": "Limited" - }, - { - "text": "Sensor - Water", - "risk": "Limited" - }, - { - "text": "Sensor - Occupancy", - "risk": "High" - }, - { - "text": "Sensor - Volume", - "risk": "Limited" - }, - { - "text": "Sensor - Weight", - "risk": "Limited" - }, - { - "text": "Sensor - Weather", - "risk": "Limited" - }, - { - "text": "Sensor - Steam", - "risk": "High" - }, - { - "text": "Sensor - Air Flow", - "risk": "Limited" - }, - { - "text": "Sensor - Lighting", - "risk": "Limited" - }, - { - "text": "Sensor - Other", - "risk": "High" - }, - { - "text": "Sensor - Air Quality", - "risk": "Limited" - }, - { - "text": "Monitoring - Fire System", - "risk": "Limited" - }, - { - "text": "Monitoring - Emergency Lighting", - "risk": "Limited" - }, - { - "text": "Monitoring - Other", - "risk": "High" - }, - { - "text": "Monitoring - UPS", - "risk": "Limited" - }, - { - "text": "Meter - Water", - "risk": "Limited" - }, - { - "text": "Meter - Gas", - "risk": "Limited" - }, - { - "text": "Meter - Electricity", - "risk": "Limited" - }, - { - "text": "Meter - Other", - "risk": "High" - }, - { - "text": "Other", - "risk": "High" - }, - { - "text": "Data - Storage", - "risk": "High" - }, - { - "text": "Data - Processing", - "risk": "High" - }, - { - "text": "Tablet", - "risk": "High" - } - ], - "validation": { - "required": true - } - }, { "question": "How will this device be used at Google?", - "description": "Desribe your use case. Add links to user journey diagrams and TDD if available.", + "description": "Describe your use case. Add links to user journey diagrams and TDD if available.", "type": "text-long", "validation": { "max": "512", @@ -218,33 +45,6 @@ "required": true } }, - { - "category": "Data Collection", - "question": "Are any of the following statements true about your device?", - "description": "This tells us about the data your device will collect", - "type": "select-multiple", - "options": [ - { - "text": "The device collects any Personal Identifiable Information (PII) or Personal Health Information (PHI)", - "risk": "High" - }, - { - "text": "The device collects intellectual property and trade secrets, sensitive business data, critical infrastructure data, identity assets", - "risk": "High" - }, - { - "text": "The device streams confidential business data in real-time (seconds)?", - "risk": "High" - }, - { - "text": "None of the above", - "risk": "Limited" - } - ], - "validation": { - "required": true - } - }, { "category": "Data Transmission", "question": "Which of the following statements are true about this device?", @@ -260,7 +60,7 @@ "risk": "High" }, { - "text": "A failure in data transmission would likely have a substantial negative impact (https://www.rra.rocks/docs/standard_levels#levels-definitions)", + "text": "A failure in data transmission would likely have a substantial negative impact (https://www.rra.rocks/docs/standard_levels#levels-definitions)", "risk": "High" }, { diff --git a/resources/test_packs/pilot.json b/resources/test_packs/pilot.json new file mode 100644 index 000000000..587d0a25a --- /dev/null +++ b/resources/test_packs/pilot.json @@ -0,0 +1,169 @@ +{ + "name": "Pilot Assessment", + "language": { + "compliant_description": "Your device has met the initial pilot assessment requirements. Please send your Testrun ZIP file to the qualification lab for verification. The lab will then contact you with further instructions.", + "non_compliant_description": "Your device didn't quite meet the initial pilot assessment requirements. The Testrun report will provide guidance on how to resolve any issues. If you require further support, please get in touch with the qualification lab." + }, + "tests": [ + { + "name": "connection.port_link", + "required_result": "Informational" + }, + { + "name": "connection.port_speed", + "required_result": "Informational" + }, + { + "name": "connection.port_duplex", + "required_result": "Informational" + }, + { + "name": "connection.switch.arp_inspection", + "required_result": "Informational" + }, + { + "name": "connection.switch.dhcp_snooping", + "required_result": "Informational" + }, + { + "name": "connection.dhcp_address", + "required_result": "Required" + }, + { + "name": "connection.mac_address", + "required_result": "Required" + }, + { + "name": "connection.mac_oui", + "required_result": "Informational" + }, + { + "name": "connection.private_address", + "required_result": "Informational" + }, + { + "name": "connection.shared_address", + "required_result": "Informational" + }, + { + "name": "connection.single_ip", + "required_result": "Informational" + }, + { + "name": "connection.target_ping", + "required_result": "Informational" + }, + { + "name": "connection.ipaddr.ip_change", + "required_result": "Informational" + }, + { + "name": "connection.ipaddr.dhcp_failover", + "required_result": "Informational" + }, + { + "name": "connection.ipv6_slaac", + "required_result": "Informational" + }, + { + "name": "connection.ipv6_ping", + "required_result": "Informational" + }, + { + "name": "dns.network.hostname_resolution", + "required_result": "Informational" + }, + { + "name": "dns.network.from_dhcp", + "required_result": "Informational" + }, + { + "name": "dns.mdns", + "required_result": "Informational" + }, + { + "name": "ntp.network.ntp_support", + "required_result": "Informational" + }, + { + "name": "ntp.network.ntp_dhcp", + "required_result": "Informational" + }, + { + "name": "protocol.valid_bacnet", + "required_result": "Informational" + }, + { + "name": "protocol.bacnet.version", + "required_result": "Informational" + }, + { + "name": "protocol.valid_modbus", + "required_result": "Informational" + }, + { + "name": "security.services.ftp", + "required_result": "Informational" + }, + { + "name": "security.ssh.version", + "required_result": "Informational" + }, + { + "name": "security.services.telnet", + "required_result": "Informational" + }, + { + "name": "security.services.smtp", + "required_result": "Informational" + }, + { + "name": "security.services.http", + "required_result": "Informational" + }, + { + "name": "security.services.pop", + "required_result": "Informational" + }, + { + "name": "security.services.imap", + "required_result": "Informational" + }, + { + "name": "security.services.snmpv3", + "required_result": "Informational" + }, + { + "name": "security.services.vnc", + "required_result": "Informational" + }, + { + "name": "security.services.tftp", + "required_result": "Informational" + }, + { + "name": "ntp.network.ntp_server", + "required_result": "Informational" + }, + { + "name": "security.tls.v1_0_client", + "required_result": "Required if Applicable" + }, + { + "name": "security.tls.v1_2_server", + "required_result": "Informational" + }, + { + "name": "security.tls.v1_2_client", + "required_result": "Informational" + }, + { + "name": "security.tls.v1_3_server", + "required_result": "Informational" + }, + { + "name": "security.tls.v1_3_client", + "required_result": "Informational" + } + ] + } \ No newline at end of file diff --git a/resources/test_packs/qualification.json b/resources/test_packs/qualification.json new file mode 100644 index 000000000..967370b4a --- /dev/null +++ b/resources/test_packs/qualification.json @@ -0,0 +1,177 @@ +{ + "name": "Device Qualification", + "language": { + "compliant_description": "Your device has met the initial device qualification requirements. Please send your Testrun ZIP file to the qualification lab for verification. The lab will then contact you with further instructions.", + "non_compliant_description": "Your device didn't quite meet the initial device qualification requirements. The Testrun report will provide guidance on how to resolve any issues. If you require further support, please get in touch with the qualification lab." + }, + "tests": [ + { + "name": "connection.port_link", + "required_result": "Required" + }, + { + "name": "connection.port_speed", + "required_result": "Required" + }, + { + "name": "connection.port_duplex", + "required_result": "Required" + }, + { + "name": "connection.switch.arp_inspection", + "required_result": "Required" + }, + { + "name": "connection.switch.dhcp_snooping", + "required_result": "Required" + }, + { + "name": "connection.dhcp_address", + "required_result": "Required" + }, + { + "name": "connection.mac_address", + "required_result": "Required" + }, + { + "name": "connection.mac_oui", + "required_result": "Required" + }, + { + "name": "connection.private_address", + "required_result": "Required" + }, + { + "name": "connection.shared_address", + "required_result": "Required" + }, + { + "name": "connection.dhcp_disconnect", + "required_result": "Required" + }, + { + "name": "connection.dhcp_disconnect_ip_change", + "required_result": "Required" + }, + { + "name": "connection.single_ip", + "required_result": "Required" + }, + { + "name": "connection.target_ping", + "required_result": "Required" + }, + { + "name": "connection.ipaddr.ip_change", + "required_result": "Required" + }, + { + "name": "connection.ipaddr.dhcp_failover", + "required_result": "Required" + }, + { + "name": "connection.ipv6_slaac", + "required_result": "Required" + }, + { + "name": "connection.ipv6_ping", + "required_result": "Required" + }, + { + "name": "dns.network.hostname_resolution", + "required_result": "Required" + }, + { + "name": "dns.network.from_dhcp", + "required_result": "Informational" + }, + { + "name": "dns.mdns", + "required_result": "Informational" + }, + { + "name": "ntp.network.ntp_support", + "required_result": "Required" + }, + { + "name": "ntp.network.ntp_dhcp", + "required_result": "Roadmap" + }, + { + "name": "protocol.valid_bacnet", + "required_result": "Recommended" + }, + { + "name": "protocol.bacnet.version", + "required_result": "Recommended" + }, + { + "name": "protocol.valid_modbus", + "required_result": "Recommended" + }, + { + "name": "security.services.ftp", + "required_result": "Required" + }, + { + "name": "security.ssh.version", + "required_result": "Required" + }, + { + "name": "security.services.telnet", + "required_result": "Required" + }, + { + "name": "security.services.smtp", + "required_result": "Required" + }, + { + "name": "security.services.http", + "required_result": "Required" + }, + { + "name": "security.services.pop", + "required_result": "Required" + }, + { + "name": "security.services.imap", + "required_result": "Required" + }, + { + "name": "security.services.snmpv3", + "required_result": "Required" + }, + { + "name": "security.services.vnc", + "required_result": "Required" + }, + { + "name": "security.services.tftp", + "required_result": "Required" + }, + { + "name": "ntp.network.ntp_server", + "required_result": "Required" + }, + { + "name": "security.tls.v1_0_client", + "required_result": "Informational" + }, + { + "name": "security.tls.v1_2_server", + "required_result": "Required if Applicable" + }, + { + "name": "security.tls.v1_2_client", + "required_result": "Required if Applicable" + }, + { + "name": "security.tls.v1_3_server", + "required_result": "Informational" + }, + { + "name": "security.tls.v1_3_client", + "required_result": "Informational" + } + ] +} \ No newline at end of file diff --git a/testing/api/certificates/WR2.pem b/testing/api/certificates/WR2.pem new file mode 100644 index 000000000..f82f4d12d --- /dev/null +++ b/testing/api/certificates/WR2.pem @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIFCzCCAvOgAwIBAgIQf/AFoHxM3tEArZ1mpRB7mDANBgkqhkiG9w0BAQsFADBH +MQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExM +QzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMjMxMjEzMDkwMDAwWhcNMjkwMjIw +MTQwMDAwWjA7MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVR29vZ2xlIFRydXN0IFNl +cnZpY2VzMQwwCgYDVQQDEwNXUjIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQCp/5x/RR5wqFOfytnlDd5GV1d9vI+aWqxG8YSau5HbyfsvAfuSCQAWXqAc ++MGr+XgvSszYhaLYWTwO0xj7sfUkDSbutltkdnwUxy96zqhMt/TZCPzfhyM1IKji +aeKMTj+xWfpgoh6zySBTGYLKNlNtYE3pAJH8do1cCA8Kwtzxc2vFE24KT3rC8gIc +LrRjg9ox9i11MLL7q8Ju26nADrn5Z9TDJVd06wW06Y613ijNzHoU5HEDy01hLmFX +xRmpC5iEGuh5KdmyjS//V2pm4M6rlagplmNwEmceOuHbsCFx13ye/aoXbv4r+zgX +FNFmp6+atXDMyGOBOozAKql2N87jAgMBAAGjgf4wgfswDgYDVR0PAQH/BAQDAgGG +MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjASBgNVHRMBAf8ECDAGAQH/ +AgEAMB0GA1UdDgQWBBTeGx7teRXUPjckwyG77DQ5bUKyMDAfBgNVHSMEGDAWgBTk +rysmcRorSCeFL1JmLO/wiRNxPjA0BggrBgEFBQcBAQQoMCYwJAYIKwYBBQUHMAKG +GGh0dHA6Ly9pLnBraS5nb29nL3IxLmNydDArBgNVHR8EJDAiMCCgHqAchhpodHRw +Oi8vYy5wa2kuZ29vZy9yL3IxLmNybDATBgNVHSAEDDAKMAgGBmeBDAECATANBgkq +hkiG9w0BAQsFAAOCAgEARXWL5R87RBOWGqtY8TXJbz3S0DNKhjO6V1FP7sQ02hYS +TL8Tnw3UVOlIecAwPJQl8hr0ujKUtjNyC4XuCRElNJThb0Lbgpt7fyqaqf9/qdLe +SiDLs/sDA7j4BwXaWZIvGEaYzq9yviQmsR4ATb0IrZNBRAq7x9UBhb+TV+PfdBJT +DhEl05vc3ssnbrPCuTNiOcLgNeFbpwkuGcuRKnZc8d/KI4RApW//mkHgte8y0YWu +ryUJ8GLFbsLIbjL9uNrizkqRSvOFVU6xddZIMy9vhNkSXJ/UcZhjJY1pXAprffJB +vei7j+Qi151lRehMCofa6WBmiA4fx+FOVsV2/7R6V2nyAiIJJkEd2nSi5SnzxJrl +Xdaqev3htytmOPvoKWa676ATL/hzfvDaQBEcXd2Ppvy+275W+DKcH0FBbX62xevG +iza3F4ydzxl6NJ8hk8R+dDXSqv1MbRT1ybB5W0k8878XSOjvmiYTDIfyc9acxVJr +Y/cykHipa+te1pOhv7wYPYtZ9orGBV5SGOJm4NrB3K1aJar0RfzxC3ikr7Dyc6Qw +qDTBU39CluVIQeuQRgwG3MuSxl7zRERDRilGoKb8uY45JzmxWuKxrfwT/478JuHU +/oTxUFqOl2stKnn7QGTq8z29W+GgBLCXSBxC9epaHM0myFH/FJlniXJfHeytWt0= +-----END CERTIFICATE----- diff --git a/testing/api/certificates/crt.pem b/testing/api/certificates/crt.pem new file mode 100644 index 000000000..410b1f104 --- /dev/null +++ b/testing/api/certificates/crt.pem @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFVzCCAz+gAwIBAgINAgPlk28xsBNJiGuiFzANBgkqhkiG9w0BAQwFADBHMQsw +CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU +MBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw +MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp +Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwggIiMA0GCSqGSIb3DQEBAQUA +A4ICDwAwggIKAoICAQC2EQKLHuOhd5s73L+UPreVp0A8of2C+X0yBoJx9vaMf/vo +27xqLpeXo4xL+Sv2sfnOhB2x+cWX3u+58qPpvBKJXqeqUqv4IyfLpLGcY9vXmX7w +Cl7raKb0xlpHDU0QM+NOsROjyBhsS+z8CZDfnWQpJSMHobTSPS5g4M/SCYe7zUjw +TcLCeoiKu7rPWRnWr4+wB7CeMfGCwcDfLqZtbBkOtdh+JhpFAz2weaSUKK0Pfybl +qAj+lug8aJRT7oM6iCsVlgmy4HqMLnXWnOunVmSPlk9orj2XwoSPwLxAwAtcvfaH +szVsrBhQf4TgTM2S0yDpM7xSma8ytSmzJSq0SPly4cpk9+aCEI3oncKKiPo4Zor8 +Y/kB+Xj9e1x3+naH+uzfsQ55lVe0vSbv1gHR6xYKu44LtcXFilWr06zqkUspzBmk +MiVOKvFlRNACzqrOSbTqn3yDsEB750Orp2yjj32JgfpMpf/VjsPOS+C12LOORc92 +wO1AK/1TD7Cn1TsNsYqiA94xrcx36m97PtbfkSIS5r762DL8EGMUUXLeXdYWk70p +aDPvOmbsB4om3xPXV2V4J95eSRQAogB/mqghtqmxlbCluQ0WEdrHbEg8QOB+DVrN +VjzRlwW5y0vtOUucxD/SVRNuJLDWcfr0wbrM7Rv1/oFB2ACYPTrIrnqYNxgFlQID +AQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E +FgQU5K8rJnEaK0gnhS9SZizv8IkTcT4wDQYJKoZIhvcNAQEMBQADggIBAJ+qQibb +C5u+/x6Wki4+omVKapi6Ist9wTrYggoGxval3sBOh2Z5ofmmWJyq+bXmYOfg6LEe +QkEzCzc9zolwFcq1JKjPa7XSQCGYzyI0zzvFIoTgxQ6KfF2I5DUkzps+GlQebtuy +h6f88/qBVRRiClmpIgUxPoLW7ttXNLwzldMXG+gnoot7TiYaelpkttGsN/H9oPM4 +7HLwEXWdyzRSjeZ2axfG34arJ45JK3VmgRAhpuo+9K4l/3wV3s6MJT/KYnAK9y8J +ZgfIPxz88NtFMN9iiMG1D53Dn0reWVlHxYciNuaCp+0KueIHoI17eko8cdLiA6Ef +MgfdG+RCzgwARWGAtQsgWSl4vflVy2PFPEz0tv/bal8xa5meLMFrUKTX5hgUvYU/ +Z6tGn6D/Qqc6f1zLXbBwHSs09dR2CQzreExZBfMzQsNhFRAbd03OIozUhfJFfbdT +6u9AWpQKXCBfTkBdYiJ23//OYb2MI3jSNwLgjt7RETeJ9r/tSQdirpLsQBqvFAnZ +0E6yove+7u7Y/9waLd64NnHi/Hm3lCXRSHNboTXns5lndcEZOitHTtNCjv0xyBZm +2tIMPNuzjsmhDYAPexZ3FL//2wmUspO8IFgV6dtxQ/PeEMMA3KgqlbbC1j+Qa3bb +bP6MvPJwNQzcmRk13NfIRmPVNnGuV/u3gm3c +-----END CERTIFICATE----- diff --git a/testing/api/certificates/invalid.pem b/testing/api/certificates/invalid.pem new file mode 100644 index 000000000..d3f5a12fa --- /dev/null +++ b/testing/api/certificates/invalid.pem @@ -0,0 +1 @@ + diff --git a/testing/api/certificates/invalidname1234567891234.pem b/testing/api/certificates/invalidname1234567891234.pem new file mode 100644 index 000000000..410b1f104 --- /dev/null +++ b/testing/api/certificates/invalidname1234567891234.pem @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFVzCCAz+gAwIBAgINAgPlk28xsBNJiGuiFzANBgkqhkiG9w0BAQwFADBHMQsw +CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU +MBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw +MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp +Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwggIiMA0GCSqGSIb3DQEBAQUA +A4ICDwAwggIKAoICAQC2EQKLHuOhd5s73L+UPreVp0A8of2C+X0yBoJx9vaMf/vo +27xqLpeXo4xL+Sv2sfnOhB2x+cWX3u+58qPpvBKJXqeqUqv4IyfLpLGcY9vXmX7w +Cl7raKb0xlpHDU0QM+NOsROjyBhsS+z8CZDfnWQpJSMHobTSPS5g4M/SCYe7zUjw +TcLCeoiKu7rPWRnWr4+wB7CeMfGCwcDfLqZtbBkOtdh+JhpFAz2weaSUKK0Pfybl +qAj+lug8aJRT7oM6iCsVlgmy4HqMLnXWnOunVmSPlk9orj2XwoSPwLxAwAtcvfaH +szVsrBhQf4TgTM2S0yDpM7xSma8ytSmzJSq0SPly4cpk9+aCEI3oncKKiPo4Zor8 +Y/kB+Xj9e1x3+naH+uzfsQ55lVe0vSbv1gHR6xYKu44LtcXFilWr06zqkUspzBmk +MiVOKvFlRNACzqrOSbTqn3yDsEB750Orp2yjj32JgfpMpf/VjsPOS+C12LOORc92 +wO1AK/1TD7Cn1TsNsYqiA94xrcx36m97PtbfkSIS5r762DL8EGMUUXLeXdYWk70p +aDPvOmbsB4om3xPXV2V4J95eSRQAogB/mqghtqmxlbCluQ0WEdrHbEg8QOB+DVrN +VjzRlwW5y0vtOUucxD/SVRNuJLDWcfr0wbrM7Rv1/oFB2ACYPTrIrnqYNxgFlQID +AQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E +FgQU5K8rJnEaK0gnhS9SZizv8IkTcT4wDQYJKoZIhvcNAQEMBQADggIBAJ+qQibb +C5u+/x6Wki4+omVKapi6Ist9wTrYggoGxval3sBOh2Z5ofmmWJyq+bXmYOfg6LEe +QkEzCzc9zolwFcq1JKjPa7XSQCGYzyI0zzvFIoTgxQ6KfF2I5DUkzps+GlQebtuy +h6f88/qBVRRiClmpIgUxPoLW7ttXNLwzldMXG+gnoot7TiYaelpkttGsN/H9oPM4 +7HLwEXWdyzRSjeZ2axfG34arJ45JK3VmgRAhpuo+9K4l/3wV3s6MJT/KYnAK9y8J +ZgfIPxz88NtFMN9iiMG1D53Dn0reWVlHxYciNuaCp+0KueIHoI17eko8cdLiA6Ef +MgfdG+RCzgwARWGAtQsgWSl4vflVy2PFPEz0tv/bal8xa5meLMFrUKTX5hgUvYU/ +Z6tGn6D/Qqc6f1zLXbBwHSs09dR2CQzreExZBfMzQsNhFRAbd03OIozUhfJFfbdT +6u9AWpQKXCBfTkBdYiJ23//OYb2MI3jSNwLgjt7RETeJ9r/tSQdirpLsQBqvFAnZ +0E6yove+7u7Y/9waLd64NnHi/Hm3lCXRSHNboTXns5lndcEZOitHTtNCjv0xyBZm +2tIMPNuzjsmhDYAPexZ3FL//2wmUspO8IFgV6dtxQ/PeEMMA3KgqlbbC1j+Qa3bb +bP6MvPJwNQzcmRk13NfIRmPVNnGuV/u3gm3c +-----END CERTIFICATE----- diff --git a/testing/api/devices/device_1/device_config.json b/testing/api/devices/device_1/device_config.json new file mode 100644 index 000000000..3be69a082 --- /dev/null +++ b/testing/api/devices/device_1/device_config.json @@ -0,0 +1,54 @@ +{ + "mac_addr": "00:1e:42:28:9e:4a", + "manufacturer": "Teltonika", + "model": "TRB140", + "type": "IoT Gateway", + "technology": "Hardware - Access Control", + "test_pack": "Device Qualification", + "additional_info": [ + { + "question": "What type of device is this?", + "answer": "IoT Gateway" + }, + { + "question": "Please select the technology this device falls into", + "answer": "Hardware - Access Control" + }, + { + "question": "Does your device process any sensitive information?", + "answer": "Yes" + }, + { + "question": "Can all non-essential services be disabled on your device?", + "answer": "Yes" + }, + { + "question": "Is there a second IP port on the device?", + "answer": "Yes" + }, + { + "question": "Can the second IP port on your device be disabled?", + "answer": "Yes" + } + ], + "test_modules": { + "protocol": { + "enabled": true + }, + "services": { + "enabled": false + }, + "ntp": { + "enabled": true + }, + "tls": { + "enabled": false + }, + "connection": { + "enabled": true + }, + "dns": { + "enabled": true + } + } +} diff --git a/testing/api/devices/device_2/device_config.json b/testing/api/devices/device_2/device_config.json new file mode 100644 index 000000000..177ee23e6 --- /dev/null +++ b/testing/api/devices/device_2/device_config.json @@ -0,0 +1,54 @@ +{ + "mac_addr": "00:1e:42:35:73:c6", + "manufacturer": "Google", + "model": "First", + "type": "IoT Gateway", + "technology": "Hardware - Access Control", + "test_pack": "Device Qualification", + "additional_info": [ + { + "question": "What type of device is this?", + "answer": "IoT Gateway" + }, + { + "question": "Please select the technology this device falls into", + "answer": "Hardware - Access Control" + }, + { + "question": "Does your device process any sensitive information?", + "answer": "Yes" + }, + { + "question": "Can all non-essential services be disabled on your device?", + "answer": "Yes" + }, + { + "question": "Is there a second IP port on the device?", + "answer": "Yes" + }, + { + "question": "Can the second IP port on your device be disabled?", + "answer": "Yes" + } + ], + "test_modules": { + "protocol": { + "enabled": true + }, + "services": { + "enabled": false + }, + "ntp": { + "enabled": true + }, + "tls": { + "enabled": false + }, + "connection": { + "enabled": true + }, + "dns": { + "enabled": true + } + } +} diff --git a/testing/api/profiles/draft_profile.json b/testing/api/profiles/draft_profile.json new file mode 100644 index 000000000..0f580fb98 --- /dev/null +++ b/testing/api/profiles/draft_profile.json @@ -0,0 +1,35 @@ +{ + "name": "draft_profile", + "version": "1.4", + "created": "2024-09-03", + "questions": [ + { + "question": "How will this device be used at Google?", + "answer": "Monitoring" + }, + { + "question": "Is this device going to be managed by Google or a third party?", + "answer": "Google" + }, + { + "question": "Will the third-party device administrator be able to grant access to authorized Google personnel upon request?", + "answer": "" + }, + { + "question": "Which of the following statements are true about this device?", + "answer": [] + }, + { + "question": "Does the network protocol assure server-to-client identity verification?", + "answer": "Yes" + }, + { + "question": "Click the statements that best describe the characteristics of this device.", + "answer": [] + }, + { + "question": "Are any of the following statements true about this device?", + "answer": [] + } + ] +} \ No newline at end of file diff --git a/testing/api/profiles/valid_profile.json b/testing/api/profiles/valid_profile.json new file mode 100644 index 000000000..207929f8d --- /dev/null +++ b/testing/api/profiles/valid_profile.json @@ -0,0 +1,39 @@ +{ + "name": "valid_profile", + "version": "1.4", + "created": "2024-09-03", + "questions": [ + { + "question": "How will this device be used at Google?", + "answer": "Monitoring" + }, + { + "question": "Is this device going to be managed by Google or a third party?", + "answer": "Google" + }, + { + "question": "Will the third-party device administrator be able to grant access to authorized Google personnel upon request?", + "answer": "N/A" + }, + { + "question": "Which of the following statements are true about this device?", + "answer": [0] + }, + { + "question": "Does the network protocol assure server-to-client identity verification?", + "answer": "Yes" + }, + { + "question": "Click the statements that best describe the characteristics of this device.", + "answer": [0] + }, + { + "question": "Are any of the following statements true about this device?", + "answer": [0] + }, + { + "question": "Comments", + "answer": "" + } + ] +} \ No newline at end of file diff --git a/testing/api/reports/report.json b/testing/api/reports/report.json new file mode 100644 index 000000000..bd697654d --- /dev/null +++ b/testing/api/reports/report.json @@ -0,0 +1,134 @@ +{ + "testrun": { + "version": "1.3.1" + }, + "mac_addr": null, + "device": { + "mac_addr": "00:1e:42:35:73:c4", + "manufacturer": "Teltonika", + "model": "TRB140", + "firmware": "1.2.3", + "test_modules": { + "protocol": { + "enabled": true + }, + "services": { + "enabled": false + }, + "connection": { + "enabled": false + }, + "tls": { + "enabled": true + }, + "ntp": { + "enabled": true + }, + "dns": { + "enabled": true + } + } + }, + "status": "Non-Compliant", + "started": "2024-08-05 13:37:53", + "finished": "2024-08-05 13:39:35", + "tests": { + "total": 12, + "results": [ + { + "name": "protocol.valid_bacnet", + "description": "BACnet device could not be discovered", + "expected_behavior": "BACnet traffic can be seen on the network and packets are valid and not malformed", + "required_result": "Recommended", + "result": "Feature Not Detected" + }, + { + "name": "protocol.bacnet.version", + "description": "Device did not respond to BACnet discovery", + "expected_behavior": "The BACnet client implements an up to date version of BACnet", + "required_result": "Recommended", + "result": "Feature Not Detected" + }, + { + "name": "protocol.valid_modbus", + "description": "Device did not respond to Modbus connection", + "expected_behavior": "Any Modbus functionality works as expected and valid Modbus traffic can be observed", + "required_result": "Recommended", + "result": "Feature Not Detected" + }, + { + "name": "security.tls.v1_2_server", + "description": "TLS 1.2 certificate is invalid", + "expected_behavior": "TLS 1.2 certificate is issued to the web browser client when accessed", + "required_result": "Required if Applicable", + "result": "Non-Compliant", + "recommendations": [ + "Enable TLS 1.2 support in the web server configuration", + "Disable TLS 1.0 and 1.1", + "Sign the certificate used by the web server" + ] + }, + { + "name": "security.tls.v1_2_client", + "description": "TLS 1.2 client connections valid", + "expected_behavior": "The packet indicates a TLS connection with at least TLS 1.2 and support for ECDH and ECDSA ciphers", + "required_result": "Required if Applicable", + "result": "Compliant" + }, + { + "name": "security.tls.v1_3_server", + "description": "TLS 1.3 certificate is invalid", + "expected_behavior": "TLS 1.3 certificate is issued to the web browser client when accessed", + "required_result": "Informational", + "result": "Informational" + }, + { + "name": "security.tls.v1_3_client", + "description": "TLS 1.3 client connections valid", + "expected_behavior": "The packet indicates a TLS connection with at least TLS 1.3", + "required_result": "Informational", + "result": "Informational" + }, + { + "name": "ntp.network.ntp_support", + "description": "Device sent NTPv3 packets. NTPv3 is not allowed", + "expected_behavior": "The device sends an NTPv4 request to the configured NTP server.", + "required_result": "Required", + "result": "Non-Compliant", + "recommendations": [ + "Set the NTP version to v4 in the NTP client", + "Install an NTP client that supports NTPv4" + ] + }, + { + "name": "ntp.network.ntp_dhcp", + "description": "Device sent NTP request to non-DHCP provided server", + "expected_behavior": "Device can accept NTP server address, provided by the DHCP server (DHCP OFFER PACKET)", + "required_result": "Roadmap", + "result": "Feature Not Detected" + }, + { + "name": "dns.network.hostname_resolution", + "description": "DNS traffic detected from device", + "expected_behavior": "The device sends DNS requests.", + "required_result": "Required", + "result": "Compliant" + }, + { + "name": "dns.network.from_dhcp", + "description": "DNS traffic detected only to DHCP provided server", + "expected_behavior": "The device sends DNS requests to the DNS server provided by the DHCP server", + "required_result": "Informational", + "result": "Informational" + }, + { + "name": "dns.mdns", + "description": "No MDNS traffic detected from the device", + "expected_behavior": "Device may send MDNS requests", + "required_result": "Informational", + "result": "Informational" + } + ] + }, + "report": "http://localhost:8000/report/Teltonika TRB140/2024-08-05T13:37:53" +} \ No newline at end of file diff --git a/testing/api/reports/report.pdf b/testing/api/reports/report.pdf new file mode 100644 index 000000000..0e449f196 Binary files /dev/null and b/testing/api/reports/report.pdf differ diff --git a/testing/api/system.json b/testing/api/sys_config/system.json similarity index 100% rename from testing/api/system.json rename to testing/api/sys_config/system.json diff --git a/testing/api/sys_config/updated_system.json b/testing/api/sys_config/updated_system.json new file mode 100644 index 000000000..95c10642c --- /dev/null +++ b/testing/api/sys_config/updated_system.json @@ -0,0 +1,7 @@ +{ + "network": { + "device_intf": "updated_endev0a", + "internet_intf": "updated_dummynet" + }, + "log_level": "DEBUG" +} \ No newline at end of file diff --git a/testing/api/test_api b/testing/api/test_api index 6751ae0ad..095123a3b 100755 --- a/testing/api/test_api +++ b/testing/api/test_api @@ -37,14 +37,15 @@ sudo docker build ./testing/docker/ci_test_device1 -t test-run/ci_device_1 -f . sudo chown -R $USER local # Copy configuration to testrun -sudo cp testing/api/system.json local/system.json +sudo cp testing/api/sys_config/system.json local/system.json # Needs to be sudo because this invokes bin/testrun sudo venv/bin/python3 -m pytest -v testing/api/test_api.py +return_code=$? # Clean up network interfaces after use sudo docker network rm endev0 sudo ip link del dev endev0a sudo ip link del dev dummynet -exit $? \ No newline at end of file +exit $return_code \ No newline at end of file diff --git a/testing/api/test_api.py b/testing/api/test_api.py index 75811e3bb..e67506a71 100644 --- a/testing/api/test_api.py +++ b/testing/api/test_api.py @@ -16,56 +16,81 @@ # pylint: disable=redefined-outer-name from collections.abc import Callable -import copy import json import os -from pathlib import Path import re import shutil import signal import subprocess import time -from typing import Iterator import pytest import requests +from cryptography import x509 +from cryptography.hazmat.backends import default_backend -ALL_DEVICES = "*" API = "http://127.0.0.1:8000" LOG_PATH = "/tmp/testrun.log" TEST_SITE_DIR = ".." DEVICES_DIRECTORY = "local/devices" -TESTING_DEVICES = "../device_configs" -SYSTEM_CONFIG_PATH = "local/system.json" +TESTING_DEVICES = "../devices" +PROFILES_DIRECTORY = "local/risk_profiles" +SYS_CONFIG_FILE = "local/system.json" +CERTS_DIRECTORY = "local/root_certs" + +SYS_CONFIG_PATH = "testing/api/sys_config" +CERTS_PATH = "testing/api/certificates" +PROFILES_PATH = "testing/api/profiles" +REPORTS_PATH = "testing/api/reports" +DEVICES_PATH = "testing/api/devices" +DEVICE_1_PATH = "testing/api/devices/device_1" +DEVICE_2_PATH = "testing/api/devices/device_2" + BASELINE_MAC_ADDR = "02:42:aa:00:01:01" ALL_MAC_ADDR = "02:42:aa:00:00:01" +DEVICE_PROFILE_QUESTIONS = "resources/devices/device_profile.json" + def pretty_print(dictionary: dict): """ Pretty print dictionary """ print(json.dumps(dictionary, indent=4)) +def query_system_status(): + """ Query system/status endpoint and returns 'status' value """ -def query_system_status() -> str: - """Query system status from API and returns this""" + # Send the get request r = requests.get(f"{API}/system/status", timeout=5) - response = json.loads(r.text) - return response["status"] + # Parse the json response + response = r.json() + + # Return the system status + return response["status"] def query_test_count() -> int: - """Queries status and returns number of test results""" + """ Queries status and returns number of test results """ r = requests.get(f"{API}/system/status", timeout=5) - response = json.loads(r.text) + response = r.json() return len(response["tests"]["results"]) +@pytest.fixture +def testing_devices(): + """ Use devices from the testing/devices directory """ + delete_all_devices() + shutil.copytree( + os.path.join(os.path.dirname(__file__), TESTING_DEVICES), + os.path.join(DEVICES_DIRECTORY), + dirs_exist_ok=True, + ) + return get_all_devices() def start_test_device( - device_name, mac_address, image_name="test-run/ci_device_1", args="" + device_name, mac_addr, image_name="test-run/ci_device_1", args="" ): """ Start test device container with given name """ cmd = subprocess.run( - f"docker run -d --network=endev0 --mac-address={mac_address}" + f"docker run -d --network=endev0 --mac-address={mac_addr}" f" --cap-add=NET_ADMIN -v /tmp:/out --privileged --name={device_name}" f" {image_name} {args}", shell=True, @@ -74,7 +99,6 @@ def start_test_device( ) print(cmd.stdout) - def stop_test_device(device_name): """ Stop docker container with given name """ cmd = subprocess.run( @@ -88,7 +112,6 @@ def stop_test_device(device_name): ) print(cmd.stdout) - def docker_logs(device_name): """ Print docker logs from given docker container name """ cmd = subprocess.run( @@ -97,28 +120,27 @@ def docker_logs(device_name): ) print(cmd.stdout) +def load_json(file_name, directory): + """ Utility method to load json files """ -@pytest.fixture -def empty_devices_dir(): - """ Use e,pty devices directory """ - local_delete_devices(ALL_DEVICES) + # Construct the base path relative to the main folder + base_path = os.path.abspath(os.path.join(__file__, "../../..")) + # Construct the full file path + file_path = os.path.join(base_path, directory, file_name) -@pytest.fixture -def testing_devices(): - """ Use devices from the testing/device_configs directory """ - local_delete_devices(ALL_DEVICES) - shutil.copytree( - os.path.join(os.path.dirname(__file__), TESTING_DEVICES), - os.path.join(DEVICES_DIRECTORY), - dirs_exist_ok=True, - ) - return local_get_devices() + # Open the file in read mode + with open(file_path, "r", encoding="utf-8") as file: + # Return the file content + return json.load(file) @pytest.fixture def testrun(request): # pylint: disable=W0613 - """ Start intstance of testrun """ + """ Start instance of testrun """ + + # Launch the Testrun in a new process group + # pylint: disable=W1509 with subprocess.Popen( "bin/testrun", stdout=subprocess.PIPE, @@ -127,44 +149,65 @@ def testrun(request): # pylint: disable=W0613 preexec_fn=os.setsid ) as proc: + # Wait until the API is ready to accept requests or timeout while True: + try: + + # Capture the process output outs = proc.communicate(timeout=1)[0] + except subprocess.TimeoutExpired as e: + + # If output is captured during timeout, decode and check if e.output is not None: output = e.output.decode("utf-8") + + # Check if the output contains the message indicating the API is ready if re.search("API waiting for requests", output): break + except Exception: - pytest.fail("testrun terminated") + # Fail if the Testrun process unexpectedly terminates + pytest.fail("Testrun terminated") + # Wait for two seconds before yielding time.sleep(2) yield + # Terminate the Testrun process group os.killpg(os.getpgid(proc.pid), signal.SIGTERM) try: + + # Wait up to 60 seconds for clean termination of the Testrun process outs = proc.communicate(timeout=60)[0] + + # If termination exceeds the timeout, force to kill the process except subprocess.TimeoutExpired as e: print(e.output) os.killpg(os.getpgid(proc.pid), signal.SIGKILL) pytest.exit( - "waited 60s but Testrun did not cleanly exit .. terminating all tests" + "Waited 60s but Testrun did not cleanly exit .. terminating all tests" ) print(outs) + # Stop any remaining Docker containers after the test cmd = subprocess.run( "docker stop $(docker ps -a -q)", shell=True, capture_output=True, check=False ) + print(cmd.stdout) + + # Remove the stopped Docker containers cmd = subprocess.run( "docker rm $(docker ps -a -q)", shell=True, capture_output=True, check=False ) - print(cmd.stdout) + print(cmd.stdout) def until_true(func: Callable, message: str, timeout: int): """ Blocks until given func returns True @@ -179,9 +222,8 @@ def until_true(func: Callable, message: str, timeout: int): time.sleep(1) raise TimeoutError(f"Timed out waiting {timeout}s for {message}") - -def dict_paths(thing: dict, stem: str = "") -> Iterator[str]: - """Returns json paths (in dot notation) from a given dictionary""" +def dict_paths(thing: dict, stem: str = ""): + """ Returns json paths (in dot notation) from a given dictionary """ for k, v in thing.items(): path = f"{stem}.{k}" if stem else k if isinstance(v, dict): @@ -189,770 +231,2820 @@ def dict_paths(thing: dict, stem: str = "") -> Iterator[str]: else: yield path +def get_network_interfaces() -> str: + """ Return list of network interfaces on machine -def get_network_interfaces(): - """return list of network interfaces on machine - - uses /sys/class/net rather than inetfaces as test-run uses the latter + Uses /sys/class/net rather than interfaces as testrun uses the latter """ + # Initialise empty list ifaces = [] - path = Path("/sys/class/net") - for i in path.iterdir(): - if not i.is_dir(): + + # Path to the directory containing network interfaces + path = "/sys/class/net" + + # Iterate over the items in the directory + for item in os.listdir(path): + + # Construct the full path + full_path = os.path.join(path, item) + + # Skip if the item is not a directory + if not os.path.isdir(full_path): continue - if i.stem.startswith("en") or i.stem.startswith("eth"): - ifaces.append(i.stem) + + # Check if the interface name starts with 'en' or 'eth' + if item.startswith("en") or item.startswith("eth"): + ifaces.append(item) + + # Return the list of network interfaces return ifaces +def test_invalid_api_path(testrun): # pylint: disable=W0613 + """ Test for invalid API path (404)""" -def local_delete_devices(path): - """ Deletes all local devices - """ - for thing in Path(DEVICES_DIRECTORY).glob(path): - if thing.is_file(): - thing.unlink() - else: - shutil.rmtree(thing) + # Send the get request to the invalid path + r = requests.get(f"{API}/non-existing", timeout=5) + + # Check that the response status code is 404 (Not Found) + assert r.status_code == 404 +# Tests for system endpoints -def local_get_devices(): - """ Returns path to device configs of devices in local/devices directory""" - return sorted( - Path(DEVICES_DIRECTORY).glob( - "*/device_config.json" - ) - ) +@pytest.fixture() +def restore_sys_config(): + """ Restore the original system configuration (system.json) after the test """ + + yield + + # Construct the full path for 'system.json' + sys_config = os.path.join(SYS_CONFIG_PATH, "system.json") + + # Restore system.json from 'testing/api/sys_config' after the test + if os.path.exists(sys_config): + + shutil.copy(sys_config, SYS_CONFIG_FILE) + +@pytest.fixture() +def update_sys_config(): + """ Update the system configuration (system.json) before the test """ + # Construct the full path for 'updated_system.json' + updated_sys_config = os.path.join(SYS_CONFIG_PATH, "updated_system.json") -def test_get_system_interfaces(testrun): # pylint: disable=W0613 - """Tests API system interfaces against actual local interfaces""" + # Restore system.json from 'testing/api/sys_config' after the test + if os.path.exists(updated_sys_config): + + shutil.copy(updated_sys_config, SYS_CONFIG_FILE) + +def test_get_sys_interfaces(testrun): # pylint: disable=W0613 + """ Tests API system interfaces against actual local interfaces (200) """ + + # Send a GET request to the API to retrieve system interfaces r = requests.get(f"{API}/system/interfaces", timeout=5) - response = json.loads(r.text) + + # Check if status code is 200 (OK) + assert r.status_code == 200 + + # Parse the JSON response + response = r.json() + + # Retrieve the actual network interfaces local_interfaces = get_network_interfaces() + + # Check if the key are in the response assert set(response.keys()) == set(local_interfaces) - # schema expects a flat list + # Ensure that all values in the response are strings assert all(isinstance(x, str) for x in response) +def test_update_sys_config(testrun, restore_sys_config): # pylint: disable=W0613 + """ Test update system configuration endpoint (200) """ -def test_status_idle(testrun): # pylint: disable=W0613 - until_true( - lambda: query_system_status().lower() == "idle", - "system status is `idle`", - 30, - ) + # Load the updated system configuration + updated_sys_config = load_json("updated_system.json", + directory=SYS_CONFIG_PATH) -# Currently not working due to blocking during monitoring period -@pytest.mark.skip() -def test_status_in_progress(testing_devices, testrun): # pylint: disable=W0613 + # Assign the values of 'device_intf' and 'internet_intf' from payload + updated_device_intf = updated_sys_config["network"]["device_intf"] + updated_internet_intf = updated_sys_config["network"]["internet_intf"] - payload = {"device": {"mac_addr": BASELINE_MAC_ADDR, "firmware": "asd"}} - r = requests.post(f"{API}/system/start", data=json.dumps(payload), timeout=10) + # Send the post request to update the system configuration + r = requests.post(f"{API}/system/config", + data=json.dumps(updated_sys_config), + timeout=5) + + # Check if status code is 200 (OK) assert r.status_code == 200 - until_true( - lambda: query_system_status().lower() == "waiting for device", - "system status is `waiting for device`", - 30, - ) + # Load 'system.json' from 'local' folder + local_sys_config = load_json("system.json", directory="local") - start_test_device("x123", BASELINE_MAC_ADDR) + # Assign 'device_intf' and 'internet_intf' values from 'local/system.json' + local_device_intf = local_sys_config["network"]["device_intf"] + local_internet_intf = local_sys_config["network"]["internet_intf"] - until_true( - lambda: query_system_status().lower() == "in progress", - "system status is `in progress`", - 600, - ) + # Check if 'device_intf' has been updated + assert updated_device_intf == local_device_intf + # Check if 'internet_intf' has been updated + assert updated_internet_intf == local_internet_intf -@pytest.mark.skip() -def test_status_non_compliant(testing_devices, testrun): # pylint: disable=W0613 +def test_update_sys_config_invalid_json(testrun): # pylint: disable=W0613 + """ Test invalid payload for update system configuration (400) """ - r = requests.get(f"{API}/devices", timeout=5) - all_devices = json.loads(r.text) - payload = { - "device": { - "mac_addr": all_devices[0]["mac_addr"], - "firmware": "asd" - } - } - r = requests.post(f"{API}/system/start", data=json.dumps(payload), - timeout=10) - assert r.status_code == 200 - print(r.text) + # Empty payload + updated_system_config = {} - until_true( - lambda: query_system_status().lower() == "waiting for device", - "system status is `waiting for device`", - 30, - ) + # Send the post request to update the system configuration + r = requests.post(f"{API}/system/config", + data=json.dumps(updated_system_config), + timeout=5) - start_test_device("x123", all_devices[0]["mac_addr"]) + # Check if status code is 400 (Invalid config) + assert r.status_code == 400 - until_true( - lambda: query_system_status().lower() == "non-compliant", - "system status is `complete", - 600, - ) +def test_get_sys_config(testrun): # pylint: disable=W0613 + """ Tests get system configuration endpoint (200) """ - stop_test_device("x123") + # Send a GET request to the API to retrieve system configuration + r = requests.get(f"{API}/system/config", timeout=5) -def test_create_get_devices(empty_devices_dir, testrun): # pylint: disable=W0613 - device_1 = { - "manufacturer": "Google", - "model": "First", - "mac_addr": "00:1e:42:35:73:c4", - "test_modules": { - "dns": {"enabled": True}, - "connection": {"enabled": True}, - "ntp": {"enabled": True}, - "baseline": {"enabled": True}, - "nmap": {"enabled": True}, - }, - } + # Check if status code is 200 (OK) + assert r.status_code == 200 - r = requests.post(f"{API}/device", data=json.dumps(device_1), - timeout=5) - print(r.text) - assert r.status_code == 201 - assert len(local_get_devices()) == 1 - - device_2 = { - "manufacturer": "Google", - "model": "Second", - "mac_addr": "00:1e:42:35:73:c6", - "test_modules": { - "dns": {"enabled": True}, - "connection": {"enabled": True}, - "ntp": {"enabled": True}, - "baseline": {"enabled": True}, - "nmap": {"enabled": True}, - }, + # Parse the JSON response + api_sys_config = r.json() + + # Assign the json response keys and expected types + expected_keys = { + "network": dict, + "log_level": str, + "startup_timeout": int, + "monitor_period": int, + "max_device_reports": int, + "api_url": str, + "api_port": int, + "org_name": str, } - r = requests.post(f"{API}/device", data=json.dumps(device_2), - timeout=5) - assert r.status_code == 201 - assert len(local_get_devices()) == 2 - # Test that returned devices API endpoint matches expected structure - r = requests.get(f"{API}/devices", timeout=5) - all_devices = json.loads(r.text) - pretty_print(all_devices) + # Iterate over the dict keys and values + for key, key_type in expected_keys.items(): - with open( - os.path.join(os.path.dirname(__file__), "mockito/get_devices.json"), - encoding="utf-8" - ) as f: - mockito = json.load(f) + # Check if the key is in the JSON response + assert key in api_sys_config - print(mockito) + # Check if the key has the expected data type + assert isinstance(api_sys_config[key], key_type) - # Validate structure - assert all(isinstance(x, dict) for x in all_devices) + # Load the local system configuration file 'local/system.json' + local_sys_config = load_json("system.json", directory="local") - # TOOO uncomment when is done - # assert set(dict_paths(mockito[0])) == set(dict_paths(all_devices[0])) + # Assign 'device_intf' and 'internet_intf' values from 'local/system.json' + local_device_intf = local_sys_config["network"]["device_intf"] + local_internet_intf = local_sys_config["network"]["internet_intf"] - # Validate contents of given keys matches - for key in ["mac_addr", "manufacturer", "model"]: - assert set([all_devices[0][key], all_devices[1][key]]) == set( - [device_1[key], device_2[key]] - ) + # Assign 'device_intf' and 'internet_intf' values from the api response + api_device_intf = api_sys_config["network"]["device_intf"] + api_internet_intf = api_sys_config["network"]["internet_intf"] + # Check if the device interface in the local config matches the API config + assert api_device_intf == local_device_intf -def test_delete_device_success(empty_devices_dir, testrun): # pylint: disable=W0613 - device_1 = { - "manufacturer": "Google", - "model": "First", - "mac_addr": "00:1e:42:35:73:c4", - "test_modules": { - "dns": {"enabled": True}, - "connection": {"enabled": True}, - "ntp": {"enabled": True}, - "baseline": {"enabled": True}, - "nmap": {"enabled": True}, - }, - } + # Check if the internet interface in the local config matches the API config + assert api_internet_intf == local_internet_intf - # Send create device request - r = requests.post(f"{API}/device", - data=json.dumps(device_1), - timeout=5) - print(r.text) +@pytest.fixture() +def start_test(): + """ Starts a testrun test using the API """ - # Check device has been created - assert r.status_code == 201 - assert len(local_get_devices()) == 1 - - device_2 = { - "manufacturer": "Google", - "model": "Second", - "mac_addr": "00:1e:42:35:73:c6", - "test_modules": { - "dns": {"enabled": True}, - "connection": {"enabled": True}, - "ntp": {"enabled": True}, - "baseline": {"enabled": True}, - "nmap": {"enabled": True}, - }, + # Load the device (payload) using load_json utility method + device = load_json("device_config.json", directory=DEVICE_1_PATH) + + # Assign the mac address + mac_addr = device["mac_addr"] + + # Assign the test modules + test_modules = device["test_modules"] + + # Payload with device details + payload = { + "device": { + "mac_addr": mac_addr, + "firmware": "test", + "test_modules": test_modules + } } - r = requests.post(f"{API}/device", - data=json.dumps(device_2), - timeout=5) - assert r.status_code == 201 - assert len(local_get_devices()) == 2 + # Send the post request (start test) + r = requests.post(f"{API}/system/start", + data=json.dumps(payload), + timeout=10) - # Test that device_1 deletes - r = requests.delete(f"{API}/device/", - data=json.dumps(device_1), - timeout=5) - assert r.status_code == 200 - assert len(local_get_devices()) == 1 + # Exception if status code is not 200 + if r.status_code != 200: + raise ValueError(f"API request failed with code: {r.status_code}") +@pytest.fixture() +def stop_test(): + """ Stops a testrun test using the API """ - # Test that returned devices API endpoint matches expected structure - r = requests.get(f"{API}/devices", timeout=5) - all_devices = json.loads(r.text) - pretty_print(all_devices) + # Send the post request to stop the test + r = requests.post(f"{API}/system/stop", timeout=10) - with open( - os.path.join(os.path.dirname(__file__), - "mockito/get_devices.json"), - encoding="utf-8" - ) as f: - mockito = json.load(f) + # Exception if status code is not 200 + if r.status_code != 200: + raise ValueError(f"API request failed with code: {r.status_code}") - print(mockito) + # Validate system status - # Validate structure - assert all(isinstance(x, dict) for x in all_devices) +@pytest.mark.parametrize("add_devices", [ + ["device_1"] +],indirect=True) +def test_start_testrun_success(empty_devices_dir, add_devices, testrun): # pylint: disable=W0613 + """ Test for testrun started successfully (200) """ - # TOOO uncomment when is done - # assert set(dict_paths(mockito[0])) == set(dict_paths(all_devices[0])) + # Load the device using load_json utility method + device = load_json("device_config.json", directory=DEVICE_1_PATH) - # Validate contents of given keys matches - for key in ["mac_addr", "manufacturer", "model"]: - assert set([all_devices[0][key]]) == set( - [device_2[key]] - ) + # Assign the device mac address + mac_addr = device["mac_addr"] + # Assign device modules + test_modules = device["test_modules"] -def test_delete_device_not_found(empty_devices_dir, testrun): # pylint: disable=W0613 - device_1 = { - "manufacturer": "Google", - "model": "First", - "mac_addr": "00:1e:42:35:73:c4", - "test_modules": { - "dns": {"enabled": True}, - "connection": {"enabled": True}, - "ntp": {"enabled": True}, - "baseline": {"enabled": True}, - "nmap": {"enabled": True}, - }, + # Payload with device details + payload = { + "device": { + "mac_addr": mac_addr, + "firmware": "test", + "test_modules": test_modules + } } - # Send create device request - r = requests.post(f"{API}/device", - data=json.dumps(device_1), - timeout=5) - print(r.text) - - # Check device has been created - assert r.status_code == 201 - assert len(local_get_devices()) == 1 + # Send the post request + r = requests.post(f"{API}/system/start", data=json.dumps(payload), timeout=10) - # Test that device_1 deletes - r = requests.delete(f"{API}/device/", - data=json.dumps(device_1), - timeout=5) + # Check if the response status code is 200 (OK) assert r.status_code == 200 - assert len(local_get_devices()) == 0 - # Test that device_1 is not found - r = requests.delete(f"{API}/device/", - data=json.dumps(device_1), - timeout=5) - assert r.status_code == 404 - assert len(local_get_devices()) == 0 + # Parse the json response + response = r.json() + # Check that device is in response + assert "device" in response -def test_delete_device_no_mac(empty_devices_dir, testrun): # pylint: disable=W0613 - device_1 = { - "manufacturer": "Google", - "model": "First", - "mac_addr": "00:1e:42:35:73:c4", - "test_modules": { - "dns": {"enabled": True}, - "connection": {"enabled": True}, - "ntp": {"enabled": True}, - "baseline": {"enabled": True}, - "nmap": {"enabled": True}, - }, + # Assign the json response keys and expected types + expected_keys = { + "mac_addr": str, + "firmware": str, + "test_modules": dict } - # Send create device request - r = requests.post(f"{API}/device", - data=json.dumps(device_1), - timeout=5) - print(r.text) + # Assign the device properties + device = response["device"] - # Check device has been created - assert r.status_code == 201 - assert len(local_get_devices()) == 1 + # Iterate over the 'expected_keys' dict keys and values + for key, key_type in expected_keys.items(): - device_1.pop("mac_addr") + # Check if the key is in the device + assert key in device - # Test that device_1 can't delete with no mac address - r = requests.delete(f"{API}/device/", - data=json.dumps(device_1), - timeout=5) - assert r.status_code == 400 - assert len(local_get_devices()) == 1 + # Check if the key has the expected data type + assert isinstance(device[key], key_type) +def test_start_testrun_invalid_json(testrun): # pylint: disable=W0613 + """ Test for invalid JSON payload when testrun is started (400) """ -# Currently not working due to blocking during monitoring period -@pytest.mark.skip() -def test_delete_device_testrun_running(testing_devices, testrun): # pylint: disable=W0613 + # Payload empty dict (no device) + payload = {} - payload = {"device": {"mac_addr": BASELINE_MAC_ADDR, "firmware": "asd"}} + # Send the post request r = requests.post(f"{API}/system/start", data=json.dumps(payload), timeout=10) - assert r.status_code == 200 - until_true( - lambda: query_system_status().lower() == "waiting for device", - "system status is `waiting for device`", - 30, - ) + # Check if the response status code is 400 (bad request) + assert r.status_code == 400 - start_test_device("x123", BASELINE_MAC_ADDR) + # Parse the json response + response = r.json() - until_true( - lambda: query_system_status().lower() == "in progress", - "system status is `in progress`", - 600, - ) + # Check if 'error' in response + assert "error" in response - device_1 = { - "manufacturer": "Google", - "model": "First", - "mac_addr": BASELINE_MAC_ADDR, - "test_modules": { - "dns": {"enabled": True}, - "connection": {"enabled": True}, - "ntp": {"enabled": True}, - "baseline": {"enabled": True}, - "nmap": {"enabled": True}, - }, - } - r = requests.delete(f"{API}/device/", - data=json.dumps(device_1), - timeout=5) - assert r.status_code == 403 +@pytest.mark.parametrize("add_devices", [ + ["device_1"] +],indirect=True) +def test_start_testrun_already_started(empty_devices_dir, add_devices, # pylint: disable=W0613 + testrun, start_test): # pylint: disable=W0613 + """ Test for testrun already started (409) """ + # Load the device using load_json utility method + device = load_json("device_config.json", directory=DEVICE_1_PATH) -def test_start_testrun_started_successfully( - testing_devices, # pylint: disable=W0613 - testrun): # pylint: disable=W0613 - payload = {"device": {"mac_addr": BASELINE_MAC_ADDR, "firmware": "asd"}} - r = requests.post(f"{API}/system/start", data=json.dumps(payload), timeout=10) - assert r.status_code == 200 + # Assign the device mac address + mac_addr = device["mac_addr"] + # Assign the test modules + test_modules = device["test_modules"] -# Currently not working due to blocking during monitoring period -@pytest.mark.skip() -def test_start_testrun_already_in_progress( - testing_devices, # pylint: disable=W0613 - testrun): # pylint: disable=W0613 - payload = {"device": {"mac_addr": BASELINE_MAC_ADDR, "firmware": "asd"}} + # Payload with device details + payload = { + "device": { + "mac_addr": mac_addr, + "firmware": "test", + "test_modules": test_modules + } + } + + # Send the post request (start test) r = requests.post(f"{API}/system/start", data=json.dumps(payload), timeout=10) - until_true( - lambda: query_system_status().lower() == "waiting for device", - "system status is `waiting for device`", - 30, - ) + # Parse the json response + response = r.json() - start_test_device("x123", BASELINE_MAC_ADDR) + # Check if the response status code is 409 (Conflict) + assert r.status_code == 409 - until_true( - lambda: query_system_status().lower() == "in progress", - "system status is `in progress`", - 600, - ) + # Check if 'error' in response + assert "error" in response + +def test_start_testrun_device_not_found(empty_devices_dir, testrun): # pylint: disable=W0613 + """ Test for start testrun when device is not found (404) """ + + # Payload with device details with no mac address assigned + payload = {"device": { + "mac_addr": "", + "firmware": "test", + "test_modules": {} + }} + + # Send the post request r = requests.post(f"{API}/system/start", data=json.dumps(payload), timeout=10) - assert r.status_code == 409 -def test_start_system_not_configured_correctly( - empty_devices_dir, # pylint: disable=W0613 - testrun): # pylint: disable=W0613 - device_1 = { - "manufacturer": "Google", - "model": "First", - "mac_addr": "00:1e:42:35:73:c4", - "test_modules": { - "dns": {"enabled": True}, - "connection": {"enabled": True}, - "ntp": {"enabled": True}, - "baseline": {"enabled": True}, - "nmap": {"enabled": True}, - }, - } + # Check if the response status code is 404 (not found) + assert r.status_code == 404 - # Send create device request - r = requests.post(f"{API}/device", - data=json.dumps(device_1), - timeout=5) - print(r.text) + # Parse the json response + response = r.json() - payload = {"device": {"mac_addr": None, "firmware": "asd"}} - r = requests.post(f"{API}/system/start", - data=json.dumps(payload), - timeout=10) - assert r.status_code == 500 + # Check if 'error' in response + assert "error" in response +@pytest.mark.parametrize("add_devices", [ + ["device_1"] +],indirect=True) +def test_start_testrun_error(empty_devices_dir, add_devices, # pylint: disable=W0613 + update_sys_config, testrun, restore_sys_config): # pylint: disable=W0613 + """ Test for start testrun internal server error (500) """ -def test_start_device_not_found(empty_devices_dir, # pylint: disable=W0613 - testrun): # pylint: disable=W0613 - device_1 = { - "manufacturer": "Google", - "model": "First", - "mac_addr": "00:1e:42:35:73:c4", - "test_modules": { - "dns": {"enabled": True}, - "connection": {"enabled": True}, - "ntp": {"enabled": True}, - "baseline": {"enabled": True}, - "nmap": {"enabled": True}, - }, - } + # Load the device using load_json utility method + device = load_json("device_config.json", directory=DEVICE_1_PATH) - # Send create device request - r = requests.post(f"{API}/device", - data=json.dumps(device_1), - timeout=5) - print(r.text) + # Assign the mac address + mac_addr = device["mac_addr"] - r = requests.delete(f"{API}/device/", - data=json.dumps(device_1), - timeout=5) + # Assign the test modules + test_modules = device["test_modules"] + + # Payload with device details + payload = { "device": + { + "mac_addr": mac_addr, + "firmware": "test", + "test_modules": test_modules + } + } + + # Send the post request + r = requests.post(f"{API}/system/start", data=json.dumps(payload), timeout=10) + + # Parse the json response + response = r.json() + + # Check if the response status code is 500 + assert r.status_code == 500 + + # Check if 'error' in response + assert "error" in response + +@pytest.mark.parametrize("add_devices", [ + ["device_1"] +],indirect=True) +def test_stop_running_testrun(empty_devices_dir, add_devices, # pylint: disable=W0613 + testrun, start_test): # pylint: disable=W0613 + """ Test for successfully stop testrun when test is running (200) """ + + # Send the post request to stop the test + r = requests.post(f"{API}/system/stop", timeout=10) + + # Parse the json response + response = r.json() + + # Check if status code is 200 (ok) assert r.status_code == 200 - payload = {"device": {"mac_addr": device_1["mac_addr"], "firmware": "asd"}} - r = requests.post(f"{API}/system/start", - data=json.dumps(payload), - timeout=10) + # Check if error in response + assert "success" in response + +def test_stop_testrun_not_running(testrun): # pylint: disable=W0613 + """ Test for stop testrun when is not running (404) """ + + # Send the post request to stop the test + r = requests.post(f"{API}/system/stop", timeout=10) + + # Parse the json response + response = r.json() + + # Check if status code is 404 (not found) assert r.status_code == 404 + # Check if error in response + assert "error" in response -def test_start_missing_device_information( - empty_devices_dir, # pylint: disable=W0613 - testrun): # pylint: disable=W0613 - device_1 = { - "manufacturer": "Google", - "model": "First", +def test_sys_shutdown(testrun): # pylint: disable=W0613 + """ Test for testrun shutdown endpoint (200) """ + + # Send a POST request to initiate the system shutdown + r = requests.post(f"{API}/system/shutdown", timeout=5) + + # Parse the json response + response = r.json() + + # Check if the response status code is 200 (OK) + assert r.status_code == 200 + + # Check if null in response + assert response is None + +@pytest.mark.parametrize("add_devices", [ + ["device_1"] +],indirect=True) +def test_sys_shutdown_in_progress(empty_devices_dir, add_devices, # pylint: disable=W0613 + testrun, start_test): # pylint: disable=W0613 + """ Test system shutdown during an in-progress test (400) """ + + # Attempt to shutdown while the test is running + r = requests.post(f"{API}/system/shutdown", timeout=5) + + # Check if the response status code is 400 (test in progress) + assert r.status_code == 400 + + # Parse the json response + response = r.json() + + # Check if 'error' in response + assert "error" in response + +def test_sys_status_idle(testrun): # pylint: disable=W0613 + """ Test for system status 'Idle' (200) """ + + # Send the get request + r = requests.get(f"{API}/system/status", timeout=5) + + # Check if the response status code is 200 (OK) + assert r.status_code == 200 + + # Parse the json response + response = r.json() + + # Check if system status is 'Idle' + assert response["status"] == "Idle" + +@pytest.mark.parametrize("add_devices", [ + ["device_1"] +],indirect=True) +def test_sys_status_cancelled(empty_devices_dir, add_devices, # pylint: disable=W0613 + testrun, start_test, stop_test): # pylint: disable=W0613 + """ Test for system status 'cancelled' (200) """ + + # Send the get request to retrieve system status + r = requests.get(f"{API}/system/status", timeout=5) + + # Parse the json response + response = r.json() + + # Check if status is 'Cancelled' + assert response["status"] == "Cancelled" + +@pytest.mark.parametrize("add_devices", [ + ["device_1"] +],indirect=True) +def test_sys_status_waiting(empty_devices_dir, add_devices, # pylint: disable=W0613 + testrun, start_test): # pylint: disable=W0613 + """ Test for system status 'Waiting for Device' (200) """ + + # Send the get request + r = requests.get(f"{API}/system/status", timeout=5) + + # Check if the response status code is 200 (OK) + assert r.status_code == 200 + + # Parse the json response + response = r.json() + + # Check if system status is 'Waiting for Device' + assert response["status"] == "Waiting for Device" + +def test_system_version(testrun): # pylint: disable=W0613 + """Test for testrun version endpoint""" + + # Send the get request to the API + r = requests.get(f"{API}/system/version", timeout=5) + + # Check if status code is 200 (ok) + assert r.status_code == 200 + + # Parse the response + response = r.json() + + # Assign the expected json response keys and expected types + expected_keys = { + "installed_version": str, + "update_available": bool, + "latest_version": str, + "latest_version_url": str + } + + # Iterate over the dict keys and values + for key, key_type in expected_keys.items(): + + # Check if the key is in the JSON response + assert key in response + + # Check if the key has the expected data type + assert isinstance(response[key], key_type) + +def test_get_test_modules(testrun): # pylint: disable=W0613 + """ Test the /system/modules endpoint to get the test modules (200) """ + + # Send a GET request to the API endpoint + r = requests.get(f"{API}/system/modules", timeout=5) + + # Check if status code is 200 (OK) + assert r.status_code == 200 + + # Parse the JSON response + response = r.json() + + # Check if the response is a list + assert isinstance(response, list) + +# Tests for reports endpoints + +def get_timestamp(formatted=False): + """ Returns timestamp value from 'started' field from the report + found at 'testing/api/reports/report.json' + By default it will return the raw time format or iso if formatted=True + """ + + # Load the report.json using load_json utility method + report_json = load_json("report.json", directory="testing/api/reports") + + # Assign the timestamp from report.json + timestamp = report_json["started"] + + # If formatted is changed to 'True' + if formatted: + + # Return the iso formatted timestamp + return timestamp.replace(" ", "T") + + # Else return the raw timestamp + return timestamp + +@pytest.fixture +def create_report_folder(): # pylint: disable=W0613 + """ Fixture to create the device reports folder in local/devices """ + + # Load the device using load_json utility method + device = load_json("device_config.json", directory=DEVICE_1_PATH) + + # Assign the device mac address + mac_addr = device["mac_addr"] + + # Assign the device name + device_name = f'{device["manufacturer"]} {device["model"]}' + + # Create the device folder path + main_folder = os.path.join(DEVICES_DIRECTORY, device_name) + + # Remove the ":" from mac address for the folder structure + mac_addr = mac_addr.replace(":", "") + + # Assign the timestamp from get_timestamp utility method + timestamp = get_timestamp(formatted=True) + + # Create the report folder path + report_folder = os.path.join(main_folder, "reports", timestamp, + "test", mac_addr) + + # Ensure the report folder exists + os.makedirs(report_folder, exist_ok=True) + + # Iterate over the files from 'testing/api/reports' folder + for file in os.listdir(REPORTS_PATH): + + # Construct full path of the file from 'testing/api/reports' folder + source_path = os.path.join(REPORTS_PATH, file) + + # Construct full path where the file will be copied + target_path = os.path.join(report_folder, file) + + # Copy the file + shutil.copy(source_path, target_path) + +def test_get_reports_no_reports(empty_devices_dir, testrun): # pylint: disable=W0613 + """Test get reports when no reports exist""" + + # Set the Origin headers to API address + headers = { + "Origin": API + } + + # Send a GET request to the /reports endpoint + r = requests.get(f"{API}/reports", headers=headers, timeout=5) + + # Check if the status code is 200 (OK) + assert r.status_code == 200 + + # Parse the JSON response + response = r.json() + + # Check if the response is a list + assert isinstance(response, list) + + # Check if the response is an empty list + assert response == [] + +@pytest.mark.parametrize("add_devices", [ + ["device_1"] +],indirect=True) +def test_get_reports(empty_devices_dir, add_devices, # pylint: disable=W0613 + create_report_folder, testrun): # pylint: disable=W0613 + """ Test for get reports when one report is available (200) """ + + # Set the Origin headers to API address + headers = { + "Origin": API + } + + # Get request to retrieve the generated reports + r = requests.get(f"{API}/reports", headers=headers, timeout=5) + + # Parse the json + response = r.json() + + # Check if status code is 200 (ok) + assert r.status_code == 200 + + # Check if response is a list + assert isinstance(response, list) + + # Check if there is one report + assert len(response) == 1 + + # Assign the report from the response list + report = response[0] + + # Assign the expected report properties + expected_keys = [ + "testrun", + "mac_addr", + "device", + "status", + "started", + "finished", + "tests", + "report" + ] + + # Iterate through the expected_keys + for key in expected_keys: + + # Check if the key exists in the report + assert key in report + +@pytest.mark.parametrize("add_devices", [ + ["device_1"] +],indirect=True) +def test_delete_report(empty_devices_dir, add_devices, # pylint: disable=W0613 + create_report_folder, testrun): # pylint: disable=W0613 + """ Test for succesfully delete a report (200) """ + + # Load the device using load_json utility method + device = load_json("device_config.json", directory=DEVICE_1_PATH) + + # Assign the device mac address + mac_addr = device["mac_addr"] + + # Assign the device name + device_name = f'{device["manufacturer"]} {device["model"]}' + + # Payload + delete_data = { + "mac_addr": mac_addr, + "timestamp": get_timestamp() + } + + # Send a DELETE request to remove the report + r = requests.delete(f"{API}/report", data=json.dumps(delete_data), timeout=5) + + # Check if status code is 200 (OK) + assert r.status_code == 200 + + # Parse the json response + response = r.json() + + # Check if "success" in response + assert "success" in response + + # Construct the 'reports' folder path + reports_folder = os.path.join(device_name, "reports") + + # Check if reports folder has been deleted + assert not os.path.exists(reports_folder) + +@pytest.mark.parametrize("add_devices", [ + ["device_1"] +],indirect=True) +def test_delete_report_no_payload(empty_devices_dir, add_devices, # pylint: disable=W0613 + create_report_folder, testrun): # pylint: disable=W0613 + """ Test delete report bad request when the payload is missing (400) """ + + # Send a DELETE request to remove the report without the payload + r = requests.delete(f"{API}/report", timeout=5) + + # Check if status code is 400 (bad request) + assert r.status_code == 400 + + # Parse the json response + response = r.json() + + # Check if "error" in response + assert "error" in response + + # Check if the correct error message returned + assert "Invalid request received, missing body" in response["error"] + +@pytest.mark.parametrize("add_devices", [ + ["device_1"] +],indirect=True) +def test_delete_report_invalid_payload(empty_devices_dir, add_devices, # pylint: disable=W0613 + create_report_folder, testrun): # pylint: disable=W0613 + """ Test delete report bad request missing mac addr or timestamp (400) """ + + # Empty payload + delete_data = {} + + # Send a DELETE request to remove the report + r = requests.delete(f"{API}/report", data=json.dumps(delete_data), timeout=5) + + # Check if status code is 400 (bad request) + assert r.status_code == 400 + + # Parse the json response + response = r.json() + + # Check if "error" in response + assert "error" in response + + # Check if the correct error message returned + assert "Missing mac address or timestamp" in response["error"] + +@pytest.mark.parametrize("add_devices", [ + ["device_1"] +],indirect=True) +def test_delete_report_invalid_timestamp(empty_devices_dir, add_devices, # pylint: disable=W0613 + create_report_folder, testrun): # pylint: disable=W0613 + """ Test delete report bad request if timestamp format is not valid (400) """ + + # Load the device using load_json utility method + device = load_json("device_config.json", directory=DEVICE_1_PATH) + + # Assign the device mac address + mac_addr = device["mac_addr"] + + # Assign the incorrect timestamp format + invalid_timestamp = "2024-01-01 invalid" + + # Payload + delete_data = { + "mac_addr": mac_addr, + "timestamp": invalid_timestamp + } + + # Send a DELETE request to remove the report + r = requests.delete(f"{API}/report", data=json.dumps(delete_data), timeout=5) + + # Check if status code is 400 (bad request) + assert r.status_code == 400 + + # Parse the json response + response = r.json() + + # Check if "error" in response + assert "error" in response + + # Check if the correct error message returned + assert "Incorrect timestamp format" in response["error"] + +def test_delete_report_no_device(empty_devices_dir, testrun): # pylint: disable=W0613 + """ Test delete report when device does not exist (404) """ + + # Payload to be deleted for a non existing device + delete_data = { "mac_addr": "00:1e:42:35:73:c4", - "test_modules": { - "dns": {"enabled": True}, - "connection": {"enabled": True}, - "ntp": {"enabled": True}, - "baseline": {"enabled": True}, - "nmap": {"enabled": True}, - }, + "timestamp": get_timestamp() + } + + # Send the delete request to the endpoint + r = requests.delete(f"{API}/report", data=json.dumps(delete_data), timeout=5) + + # Check if status is 404 (not found) + assert r.status_code == 404 + + # Parse the response json + response = r.json() + + # Check if "error" in response + assert "error" in response + + # Check if the correct error message returned + assert "Could not find device" in response["error"] + +@pytest.mark.parametrize("add_devices", [ + ["device_1"] +],indirect=True) +def test_delete_report_no_report(empty_devices_dir, add_devices, testrun): # pylint: disable=W0613 + """Test for delete report when report does not exist (404)""" + + # Load the device using load_json utility method + device = load_json("device_config.json", directory=DEVICE_1_PATH) + + # Assign the device mac address + mac_addr = device["mac_addr"] + + # Prepare the payload for the DELETE request + delete_data = { + "mac_addr": mac_addr, + "timestamp": get_timestamp() } - # Send create device request - r = requests.post(f"{API}/device", - data=json.dumps(device_1), + # Send the delete request to delete the report + r = requests.delete(f"{API}/report", + data=json.dumps(delete_data), + timeout=5) + + # Check if status code is 404 (not found) + assert r.status_code == 404 + + # Parse the JSON response + response = r.json() + + # Check if error is present in the response + assert "error" in response + + # Check if the correct error message is returned + assert "Report not found" in response["error"] + +@pytest.mark.parametrize("add_devices", [ + ["device_1"] +],indirect=True) +def test_get_report_success(empty_devices_dir, add_devices, # pylint: disable=W0613 + create_report_folder, testrun): # pylint: disable=W0613 + """Test for successfully get report when report exists (200)""" + + # Load the device using load_json utility method + device = load_json("device_config.json", directory=DEVICE_1_PATH) + + # Assign the device name + device_name = f'{device["manufacturer"]} {device["model"]}' + + # Assign the timestamp and change the format + timestamp = get_timestamp(formatted=True) + + # Send the get request + r = requests.get(f"{API}/report/{device_name}/{timestamp}", timeout=5) + + # Check if status code is 200 (ok) + assert r.status_code == 200 + + # Check if the response is a PDF + assert r.headers["Content-Type"] == "application/pdf" + +@pytest.mark.parametrize("add_devices", [ + ["device_1"] +],indirect=True) +def test_get_report_not_found(empty_devices_dir, add_devices, testrun): # pylint: disable=W0613 + """Test get report when report doesn't exist (404)""" + + # Load the device using load_json utility method + device = load_json("device_config.json", directory=DEVICE_1_PATH) + + # Assign the device name + device_name = f'{device["manufacturer"]} {device["model"]}' + + # Assign the timestamp + timestamp = get_timestamp() + + # Send the get request + r = requests.get(f"{API}/report/{device_name}/{timestamp}", timeout=5) + + # Check if status code is 404 (not found) + assert r.status_code == 404 + + # Parse the response json + response = r.json() + + # Check if "error" in response + assert "error" in response + + # Check if the correct error message returned + assert "Report could not be found" in response["error"] + +def test_get_report_device_not_found(empty_devices_dir, testrun): # pylint: disable=W0613 + """Test getting a report when the device is not found (404)""" + + # Assign device name + device_name = "nonexistent_device" + + # Assign the timestamp + timestamp = get_timestamp() + + # Send the get request + r = requests.get(f"{API}/report/{device_name}/{timestamp}", timeout=5) + + # Check if is 404 (not found) + assert r.status_code == 404 + + # Parse the json response + response = r.json() + + # Check if "error" in response + assert "error" in response + + # Check if the correct error message is returned + assert "Device not found" in response["error"] + +def test_export_report_device_not_found(empty_devices_dir, create_report_folder, # pylint: disable=W0613 + testrun): # pylint: disable=W0613 + """Test for export the report result when the device could not be found""" + + # Assign the non-existing device name + device_name = "non existing device" + + # Assign the timestamp + timestamp = get_timestamp() + + # Send the post request + r = requests.post(f"{API}/export/{device_name}/{timestamp}", timeout=5) + + # Check if is 404 (not found) + assert r.status_code == 404 + + # Parse the json response + response = r.json() + + # Check if "error" in response + assert "error" in response + + # Check if the correct error message returned + assert "A device with that name could not be found" in response["error"] + +@pytest.mark.parametrize("add_devices", [ + ["device_1"] +],indirect=True) +def test_export_report_profile_not_found(empty_devices_dir, add_devices, # pylint: disable=W0613 + create_report_folder, testrun): # pylint: disable=W0613 + """Test for export report result when the profile is not found""" + + # Load the device using load_json utility method + device = load_json("device_config.json", directory=DEVICE_1_PATH) + + # Assign the device name + device_name = f'{device["manufacturer"]} {device["model"]}' + + # Assign the timestamp + timestamp = get_timestamp() + + # Add a non existing profile into the payload + payload = {"profile": "non_existent_profile"} + + # Send the post request + r = requests.post(f"{API}/export/{device_name}/{timestamp}", + json=payload, timeout=5) - print(r.text) - payload = {} - r = requests.post(f"{API}/system/start", - data=json.dumps(payload), - timeout=10) - assert r.status_code == 400 + # Check if is 404 (not found) + assert r.status_code == 404 + + # Parse the json response + response = r.json() + + # Check if "error" in response + assert "error" in response + + # Check if the correct error message returned + assert "A profile with that name could not be found" in response["error"] + +@pytest.mark.parametrize("add_devices", [ + ["device_1"] +],indirect=True) +def test_export_report_not_found(empty_devices_dir, add_devices, testrun): # pylint: disable=W0613 + """Test for export the report result when the report could not be found""" + + # Load the device using load_json utility method + device = load_json("device_config.json", directory=DEVICE_1_PATH) + + # Assign the device name + device_name = f'{device["manufacturer"]} {device["model"]}' + + # Assign the timestamp + timestamp = get_timestamp() + + # Send the post request to trigger the zipping process + r = requests.post(f"{API}/export/{device_name}/{timestamp}", timeout=10) + + # Check if status code is 500 (Internal Server Error) + assert r.status_code == 404 + + # Parse the json response + response = r.json() + + # Check if "error" in response + assert "error" in response + + # Check if the correct error message is returned + assert "Report could not be found" in response["error"] + +@pytest.mark.parametrize("add_devices, add_profiles", [ + (["device_1"], ["valid_profile.json"]) +], indirect=True) +def test_export_report_with_profile(empty_devices_dir, add_devices, # pylint: disable=W0613 + empty_profiles_dir, add_profiles, # pylint: disable=W0613 + create_report_folder, testrun): # pylint: disable=W0613 + """Test export results with existing profile when report exists (200)""" + + # Load the profile using load_json utility method + profile = load_json("valid_profile.json", directory=PROFILES_PATH) + + # Load the device using load_json utility method + device = load_json("device_config.json", directory=DEVICE_1_PATH) + + # Assign the device name + device_name = f'{device["manufacturer"]} {device["model"]}' + + # Assign the timestamp and change the format + timestamp = get_timestamp(formatted=True) + + # Send the post request + r = requests.post(f"{API}/export/{device_name}/{timestamp}", + json=profile, + timeout=5) + + # Check if status code is 200 (OK) + assert r.status_code == 200 + + # Check if the response is a zip file + assert r.headers["Content-Type"] == "application/zip" + +@pytest.mark.parametrize("add_devices", [ + ["device_1"] +],indirect=True) +def test_export_results_with_no_profile(empty_devices_dir, add_devices, # pylint: disable=W0613 + create_report_folder, testrun): # pylint: disable=W0613 + """Test export results with no profile when report exists (200)""" + + # Load the device using load_json utility method + device = load_json("device_config.json", directory=DEVICE_1_PATH) + + # Assign the device name + device_name = f'{device["manufacturer"]} {device["model"]}' + + # Assign the timestamp and change the format + timestamp = get_timestamp(formatted=True) + + # Send the post request + r = requests.post(f"{API}/export/{device_name}/{timestamp}", timeout=5) + + # Check if status code is 200 (OK) + assert r.status_code == 200 + + # Check if the response is a zip file + assert r.headers["Content-Type"] == "application/zip" + +# Tests for device endpoints +@pytest.fixture() +def add_devices(request): + """ Upload specified device to local/devices """ + + # Access the parameter (devices list) provided to the fixture + devices = request.param + + # Iterate over the device names provided + for device in devices: + + # Construct the full path for the device_config.json + device_path = os.path.join(DEVICES_PATH, device) + + # Load the device configurations using load_json utility method + device = load_json("device_config.json", directory=device_path) + + # Assign the device name for the target directory + target_device_name = f'{device["manufacturer"]} {device["model"]}' + + # Construct the source path of the device config file + source_path = os.path.join(device_path, "device_config.json") + + # Construct the target path where the device config will be copied + target_path = os.path.join(DEVICES_DIRECTORY, target_device_name) + + # Create the target directory if it doesn't exist + os.makedirs(target_path, exist_ok=True) + + # Copy the device config from source to target + shutil.copy(source_path, target_path) + + # Return the list with devices names + return devices + +def delete_all_devices(): + """Utility method to delete all devices from local/devices""" + + try: + + # Check if the device_path (local/devices) exists and is a folder + if os.path.exists(DEVICES_DIRECTORY) and os.path.isdir(DEVICES_DIRECTORY): + + # Iterate over all devices from devices folder + for item in os.listdir(DEVICES_DIRECTORY): + + # Create the full path + item_path = os.path.join(DEVICES_DIRECTORY, item) + + # Check if item is a file + if os.path.isfile(item_path): + + # Remove file + os.unlink(item_path) + + else: + + # If item is a folder remove it + shutil.rmtree(item_path) + + except PermissionError: + + # Permission related issues + print(f"Permission Denied: {item}") + + except OSError as err: + + # System related issues + print(f"Error removing {item}: {err}") + +@pytest.fixture +def empty_devices_dir(): + """Delete all devices before and after test""" + + # Empty the directory before the test + delete_all_devices() + + yield + + # Empty the directory after the test + delete_all_devices() + +def get_all_devices(): + """ Returns list with paths to all devices from local/devices """ + + # List to store the paths of all 'device_config.json' files + devices = [] + + # Loop through each file/folder from 'local/devices'. + for device_folder in os.listdir(DEVICES_DIRECTORY): + + # Construct the full path for the file/folder + device_path = os.path.join(DEVICES_DIRECTORY, device_folder) + + # Check if the current path is a folder + if os.path.isdir(device_path): + + # Construct the full path to 'device_config.json' inside the folder. + config_path = os.path.join(device_path, "device_config.json") + + # Check if 'device_config.json' exists in the path. + if os.path.exists(config_path): + + # Append the file path to the list. + devices.append(config_path) + + # Return all the device_config.json paths + return devices + +def device_exists(device_mac): + """ Utility method to check if device exists """ + + # Send the get request + r = requests.get(f"{API}/devices", timeout=5) + + # Check if status code is not 200 (OK) + if r.status_code != 200: + raise ValueError(f"Api request failed with code: {r.status_code}") + + # Parse the JSON response to get the list of devices + devices = r.json() + + # Return if mac address is in the list of devices + return any(p["mac_addr"] == device_mac for p in devices) + +@pytest.mark.parametrize( + "add_devices", + [ + [], + ["device_1"], + ["device_1", "device_2"] + ], + indirect=True +) +def test_get_devices(empty_devices_dir, add_devices, testrun): # pylint: disable=W0613 + """ Test get devices when none, one or two devices are available (200) """ + + # Send the get request to retrieve all devices + r = requests.get(f"{API}/devices", timeout=5) + + # Check if status code is 200 (Ok) + assert r.status_code == 200 + + # Parse the json response + response = r.json() + + # Check if response is a list + assert isinstance(response, list) + + # Check if the number of devices matches the number of devices available + assert len(response) == len(add_devices) + + # Assign the expected device fields + expected_fields = [ + "status", + "mac_addr", + "manufacturer", + "model", + "type", + "technology", + "test_pack", + "test_modules", + ] + + # If devices are in the list + if len(add_devices) > 0: + + # Iterate over all expected_fields list + for field in expected_fields: + + # Check if devices have the expected fields + assert all(field in device for device in response) + +def test_create_device(empty_devices_dir, testrun): # pylint: disable=W0613 + """ Test for successfully create device endpoint (201) """ + + # Load the first device using load_json utility method + device_1 = load_json("device_config.json", directory=DEVICE_1_PATH) + + # Assign the mac address for the first device + mac_addr_1 = device_1["mac_addr"] + + # Send the post request to the '/device' endpoint + r = requests.post(f"{API}/device", data=json.dumps(device_1), timeout=5) + + # Check if status code is 201 (Created) + assert r.status_code == 201 + + # Check if there is one device in 'local/devices' + assert len(get_all_devices()) == 1 + + # Load the second device using load_json utility method + device_2 = load_json("device_config.json", directory=DEVICE_2_PATH) + + # Assign the mac address for the second device + mac_addr_2 = device_2["mac_addr"] + + # Send the post request to the '/device' endpoint + r = requests.post(f"{API}/device", data=json.dumps(device_2), timeout=5) + + # Check if status code is 201 (Created) + assert r.status_code == 201 + + # Check if there are two devices in 'local/devices' + assert len(get_all_devices()) == 2 + + # Send a get request to retrieve created devices + r = requests.get(f"{API}/devices", timeout=5) + + # Parse the json response (devices) + response = r.json() + + # Iterate through all the devices to find the device based on the "mac_addr" + created_devices = [ + d for d in response + if d["mac_addr"] in {mac_addr_1, mac_addr_2} + ] + + # Check if both devices have been found + assert len(created_devices) == 2 + +@pytest.mark.parametrize("add_devices", [ + ["device_1"] +],indirect=True) +def test_create_device_already_exists(empty_devices_dir, add_devices, # pylint: disable=W0613 + testrun): # pylint: disable=W0613 + """ Test for crete device when device already exists (409) """ + + # Error handling if there is not one devices in local/devices + if len(get_all_devices()) != 1: + raise Exception("Expected one device in local/devices") + + # Load the device (payload) using load_json utility method + device = load_json("device_config.json", directory=DEVICE_1_PATH) + + # Send the post request to create the device + r = requests.post(f"{API}/device", data=json.dumps(device), timeout=5) + + # Check if status code is 409 (conflict) + assert r.status_code == 409 + + # Parse the json response (devices) + response = r.json() + + # Check if 'error' in response + assert "error" in response + + # Check if 'local/device' has only one device + assert len(get_all_devices()) == 1 + +def test_create_device_invalid_json(empty_devices_dir, testrun): # pylint: disable=W0613 + """ Test for create device invalid json payload """ + + # Error handling if there are devices in local/devices + if len(get_all_devices()) != 0: + raise Exception("Expected no device in local/devices") + + # Empty payload + device = {} + + # Send the post request + r = requests.post(f"{API}/device", data=json.dumps(device), timeout=5) + + # Check if status code is 400 (bad request) + assert r.status_code == 400 + + # Parse the json response (devices) + response = r.json() + + # Check if 'error' in response + assert "error" in response + + # Check if 'local/device' has no devices + assert len(get_all_devices()) == 0 + +def test_create_device_invalid_request(empty_devices_dir, testrun): # pylint: disable=W0613 + """ Test for create device when no payload is added """ + + # Send the post request with no payload + r = requests.post(f"{API}/device", data=None, timeout=5) + + # Check if status code is 400 (bad request) + assert r.status_code == 400 + + # Parse the json response (devices) + response = r.json() + + # Check if 'error' in response + assert "error" in response + + # Check if 'local/device' has no devices + assert len(get_all_devices()) == 0 + +@pytest.mark.parametrize("add_devices", [ + ["device_1"] +],indirect=True) +def test_edit_device(empty_devices_dir, add_devices, # pylint: disable=W0613 + testrun): # pylint: disable=W0613 + """ Test for successfully edit device (200) """ + + # Error handling if there is not one devices in local/devices + if len(get_all_devices()) != 1: + raise Exception("Expected one device in local/devices") + + # Load the device (payload) using load_json utility method + device = load_json("device_config.json", directory=DEVICE_1_PATH) + + # Assign the mac address + mac_addr = device["mac_addr"] + + # Update the manufacturer and model values + device["manufacturer"] = "Updated Manufacturer" + device["model"] = "Updated Model" + + # Payload with the updated device name + updated_device = { + "mac_addr": mac_addr, + "device": device + } + + # Exception if the device is not found + if not device_exists(mac_addr): + raise ValueError(f"Device with mac address:{mac_addr} not found") + + # Send the post request to update the device + r = requests.post( + f"{API}/device/edit", + data=json.dumps(updated_device), + timeout=5) + + # Check if status code is 200 (OK) + assert r.status_code == 200 + + # Check if 'local/device' still has only one device + assert len(get_all_devices()) == 1 + + # Send a get request to verify device update + r = requests.get(f"{API}/devices", timeout=5) + + # Check if status code is 200 (OK) + assert r.status_code == 200 + + # Parse the response (devices list) + response = r.json() + + # Iterate through the devices to find the device based on "mac_addr" + updated_device = next( + (d for d in response if d["mac_addr"] == mac_addr), + None + ) + + # Error handling if the device is not being found + if updated_device is None: + raise Exception("The device could not be found") + + # Check if device "manufacturer" was updated + assert device["manufacturer"] == updated_device["manufacturer"] + + # Check if device "manufacturer" was updated + assert device["model"] == updated_device["model"] + +def test_edit_device_not_found(empty_devices_dir, testrun): # pylint: disable=W0613 + + """ Test for edit device when device is not found (404) """ + + # Error handling if there are devices in local/devices + if len(get_all_devices()) != 0: + raise Exception("Expected no device in local/devices") + + # Load the device (payload) using load_json utility method + device = load_json("device_config.json", directory=DEVICE_1_PATH) + + # Assign the mac address + mac_addr = device["mac_addr"] + + # Update the manufacturer and model values + device["manufacturer"] = "Updated manufacturer" + device["model"] = "Updated model" + + # Payload with the updated device name + updated_device = { + "mac_addr": mac_addr, + "device": device + } + + # Exception if the device is found + if device_exists(mac_addr): + raise ValueError(f"Device with mac address:{mac_addr} found") + + # Send the post request to update the device + r = requests.post( + f"{API}/device/edit", + data=json.dumps(updated_device), + timeout=5) + + # Check if status code is 404 (not found) + assert r.status_code == 404 + + # Parse the json response (devices) + response = r.json() + + # Check if 'error' in response + assert "error" in response + + # Check if 'local/device' still has no devices + assert len(get_all_devices()) == 0 + +def test_edit_device_invalid_json(empty_devices_dir, testrun): # pylint: disable=W0613 + """ Test for edit device invalid json (400) """ + + # Empty payload + payload = {} + + # Send the post request to update the device + r = requests.post(f"{API}/device/edit", + data=json.dumps(payload), + timeout=5) + + # Check if status code is 400 (bad request) + assert r.status_code == 400 + + # Parse the json response (devices) + response = r.json() + + # Check if 'error' in response + assert "error" in response + +@pytest.mark.parametrize( + "add_devices", + [ + ["device_1", "device_2"] + ], + indirect=True +) +def test_edit_device_mac_already_exists( empty_devices_dir, add_devices, # pylint: disable=W0613 + testrun): # pylint: disable=W0613 + """ Test for edit device when the mac address already exists (409) """ + + # Load the first device (payload) using load_json utility method + device_1 = load_json("device_config.json", directory=DEVICE_1_PATH) + + # Assign the device_1 initial mac address + mac_addr_1 = device_1["mac_addr"] + + # Load the second device using load_json utility method + device_2 = load_json("device_config.json", directory=DEVICE_2_PATH) + + # Update the device_1 mac address with device_2 mac address + device_1["mac_addr"] = device_2["mac_addr"] + + # Payload with the updated device mac address + updated_device = { + "mac_addr": mac_addr_1, + "device": device_1 + } + + # Exception if the device is not found + if not device_exists(mac_addr_1): + raise ValueError(f"Device with mac address:{mac_addr_1} not found") + + # Send the post request to update the device + r = requests.post(f"{API}/device/edit", + data=json.dumps(updated_device), + timeout=5) + + # Check if status code is 409 (conflict) + assert r.status_code == 409 + + # Parse the json response (devices) + response = r.json() + + # Check if 'error' in response + assert "error" in response + +@pytest.mark.parametrize("add_devices", [ + ["device_1"] +],indirect=True) +def test_edit_device_test_in_progress(empty_devices_dir, add_devices, # pylint: disable=W0613 + testrun, start_test): # pylint: disable=W0613 + """ Test for edit device when a test is in progress (403) """ + + # Load the device (payload) using load_json utility method + device = load_json("device_config.json", directory=DEVICE_1_PATH) + + # Assign the mac address + mac_addr = device["mac_addr"] + + # Update the manufacturer and model values + device["manufacturer"] = "Updated Manufacturer" + device["model"] = "Updated Model" + + # Payload with the updated device name + updated_device = { + "mac_addr": mac_addr, + "device": device + } + + # Exception if the device is not found + if not device_exists(mac_addr): + raise ValueError(f"Device with mac address:{mac_addr} not found") + + # Send the post request to update the device + r = requests.post( + f"{API}/device/edit", + data=json.dumps(updated_device), + timeout=5) + + # Check if status code is 403 (forbidden) + assert r.status_code == 403 + + # Send a get request to verify that device was not updated + r = requests.get(f"{API}/devices", timeout=5) + + # Exception if status code is not 200 + if r.status_code != 200: + raise ValueError(f"API request failed with code: {r.status_code}") + + # Parse the response (devices list) + response = r.json() + + # Iterate through the devices to find the device based on "mac_addr" + updated_device = next( + (d for d in response if d["mac_addr"] == mac_addr), + None + ) + + # Error handling if the device is not being found + if updated_device is None: + raise Exception("The device could not be found") + + # Check that device "manufacturer" was not updated + assert device["manufacturer"] != updated_device["manufacturer"] + + # Check that device "manufacturer" was not updated + assert device["model"] != updated_device["model"] + +@pytest.mark.parametrize("add_devices", [ + ["device_1"] +],indirect=True) +def test_edit_device_invalid_manufacturer(empty_devices_dir, add_devices, # pylint: disable=W0613 + testrun): # pylint: disable=W0613 + """ Test for edit device invalid chars in 'manufacturer' field (400) """ + + # Load the device + device = load_json("device_config.json", directory=DEVICE_1_PATH) + + # Modify the "manufacturer" field value with the invalid characters + device["manufacturer"] = "/';disallowed characters" + + # Send the post request to update the device + r = requests.post(f"{API}/device", data=json.dumps(device), + timeout=5) + + # Check if the status code is 400 (bad request) + assert r.status_code == 400 + + # Parse the json response + response = r.json() + + # Check if 'error' in response + assert "error" in response + +@pytest.mark.parametrize("add_devices", [ + ["device_1"] +],indirect=True) +def test_edit_device_invalid_model(empty_devices_dir, add_devices, testrun): # pylint: disable=W0613 + """ Test for edit device invalid chars in 'model' field (400) """ + + # Load the device + device = load_json("device_config.json", directory=DEVICE_1_PATH) + + # Modify the "model" field value with the invalid characters + device["model"] = "/';disallowed characters" + + # Send the post request to update the device + r = requests.post(f"{API}/device", data=json.dumps(device), + timeout=5) + + # Check if the status code is 400 (bad request) + assert r.status_code == 400 + + # Parse the json response + response = r.json() + + # Check if 'error' in response + assert "error" in response + +def test_edit_long_chars(empty_devices_dir, testrun): # pylint: disable=W0613 + """ Test for edit a device with model over 28 chars (400) """ + + # Load the device + device = load_json("device_config.json", directory=DEVICE_1_PATH) + + # Modify the "model" field value with 29 chars + device["model"] = "a" * 29 + + # Send the post request to edit the device + r = requests.post(f"{API}/device", data=json.dumps(device), + timeout=5) + + # Check if the status code is 400 (bad request) + assert r.status_code == 400 + + # Parse the json response + response = r.json() + + # Check if 'error' in response + assert "error" in response + +@pytest.mark.parametrize("add_devices", [ + ["device_1"] +],indirect=True) +def test_delete_device(empty_devices_dir, add_devices, testrun): # pylint: disable=W0613 + """ Test for succesfully delete device endpoint (200) """ + + # Load the device + device = load_json("device_config.json", directory=DEVICE_1_PATH) + + # Assign the mac address + mac_addr = device["mac_addr"] + + # Assign the payload with device to be deleted + payload = { "mac_addr": mac_addr } + + # Send the delete request + r = requests.delete(f"{API}/device/", + data=json.dumps(payload), + timeout=5) + + # Check if status code is 200 (OK) + assert r.status_code == 200 + + # Parse the JSON response + response = r.json() + + # Check if the response contains "success" key + assert "success" in response + + # Send the get request to check if the device has been deleted + r = requests.get(f"{API}/devices", timeout=5) + + # Exception if status code is not 200 + if r.status_code != 200: + raise ValueError(f"API request failed with code: {r.status_code}") + + # Parse the JSON response (device) + device = r.json() + + # Iterate through the devices to find the device based on the 'mac address' + deleted_device = next( + (d for d in device if d["mac_addr"] == mac_addr), + None + ) + + # Check if device was deleted + assert deleted_device is None + +def test_delete_device_not_found(empty_devices_dir, testrun): # pylint: disable=W0613 + """ Test for delete device when the device doesn't exist (404) """ + + # Assign the payload with non existing device mac address + payload = {"mac_addr" : "non-existing"} + + # Test that device_1 is not found + r = requests.delete(f"{API}/device/", + data=json.dumps(payload), + timeout=5) + + # Check if status code is 404 (not found) + assert r.status_code == 404 + + # Parse the JSON response + response = r.json() + + # Check if error in response + assert "error" in response + +@pytest.mark.parametrize("add_devices", [ + ["device_1"] +],indirect=True) +def test_delete_device_no_mac(empty_devices_dir, add_devices, testrun): # pylint: disable=W0613 + """ Test for delete device when no mac address in payload (400) """ + + # Assign an empty payload (no mac address) + payload = {} + + # Send the delete request + r = requests.delete(f"{API}/device/", + data=json.dumps(payload), + timeout=5) + + # Check if status code is 400 (bad request) + assert r.status_code == 400 + + # Parse the JSON response + response = r.json() + + # Check if 'error' in response + assert "error" in response + + # Check that device wasn't deleted from 'local/devices' + assert len(get_all_devices()) == 1 + +@pytest.mark.parametrize("add_devices", [ + ["device_1"] +],indirect=True) +def test_delete_device_testrun_in_progress(empty_devices_dir, add_devices, # pylint: disable=W0613 + testrun, start_test): # pylint: disable=W0613 + """ Test for delete device when testrun is in progress (403) """ + + # Load the device details + device = load_json("device_config.json", directory=DEVICE_1_PATH) + + # Assign the mac address + mac_addr = device["mac_addr"] + + # Assign the payload with device to be deleted mac address + payload = { "mac_addr": mac_addr } + + # Send the delete request + r = requests.delete(f"{API}/device/", + data=json.dumps(payload), + timeout=5) + + # Check if status code is 403 (forbidden) + assert r.status_code == 403 + + # Parse the JSON response + response = r.json() + + # Check if the response contains "success" key + assert "error" in response + +def test_create_invalid_manufacturer(empty_devices_dir, testrun): # pylint: disable=W0613 + """ Test for create device invalid chars in 'manufacturer' field (400) """ + + # Load the device + device = load_json("device_config.json", directory=DEVICE_1_PATH) + + # Modify the "manufacturer" field value with the invalid characters + device["manufacturer"] = "/';disallowed characters" + + # Send the post request to create the device + r = requests.post(f"{API}/device", data=json.dumps(device), + timeout=5) + + # Check if the status code is 400 (bad request) + assert r.status_code == 400 + + # Parse the json response + response = r.json() + + # Check if 'error' in response + assert "error" in response + +def test_create_invalid_model(empty_devices_dir, testrun): # pylint: disable=W0613 + """ Test for create device invalid chars in 'model' field (400) """ + + # Load the device + device = load_json("device_config.json", directory=DEVICE_1_PATH) + + # Modify the "model" field value with the invalid characters + device["model"] = "/';disallowed characters" + + # Send the post request to create the device + r = requests.post(f"{API}/device", data=json.dumps(device), + timeout=5) + + # Check if the status code is 400 (bad request) + assert r.status_code == 400 + + # Parse the json response + response = r.json() + + # Check if 'error' in response + assert "error" in response + +def test_create_long_chars(empty_devices_dir, testrun): # pylint: disable=W0613 + """ Test for create a device with model over 28 chars (400) """ + + # Load the device + device = load_json("device_config.json", directory=DEVICE_1_PATH) + + # Modify the "model" field value with 29 chars + device["model"] = "a" * 29 + + # Send the post request to create the device + r = requests.post(f"{API}/device", data=json.dumps(device), + timeout=5) + + # Check if the status code is 400 (bad request) + assert r.status_code == 400 + + # Parse the json response + response = r.json() + + # Check if 'error' in response + assert "error" in response + +def test_get_devices_format(testrun): # pylint: disable=W0613 + """ Test for get devices format (200) """ + + # Send the get request + r = requests.get(f"{API}/devices/format", timeout=5) + + # Check if status code is 200 (OK) + assert r.status_code == 200 + + # Parse the JSON response + response = r.json() + + # Check if the response is a list + assert isinstance(response, list) + + # Store the expected main keys and types + response_keys = { + "step": int, + "title": str, + "questions": list + } + + # Store the 'questions' field expected keys and types + questions_keys = { + "id": int, + "question": str, + "type": str, + "options": list + } + + # Iterate over the response items + for item in response: + + # Iterate over the 'response_keys' dict keys and values + for key, key_type in response_keys.items(): + + # Check if the key is in the response item + assert key in item + + # Check if the key has the expected data type + assert isinstance(item[key], key_type) + + # Iterate over the 'questions' field + for questions in item["questions"]: + + # Iterate over the 'questions_keys' dict keys and values + for key, key_type in questions_keys.items(): + + # Check if the key is in 'questions' field + assert key in questions + + # Check if the key has the expected data type + assert isinstance(questions[key], key_type) + +def test_sys_testpacks(testrun): # pylint: disable=W0613 + """ Test for system testpack endpoint (200) """ + + # Send the get request to the API + r = requests.get(f"{API}/system/testpacks", timeout=5) + + # Check if status code is 200 (ok) + assert r.status_code == 200 + + # Parse the response + response = r.json() + + # Check if the response is a list + assert isinstance(response, list) + +# Tests for certificates endpoints + +def delete_all_certs(): + """ Delete all certificates from root_certs folder """ + + try: + + # Check if the profile_path (local/root_certs) exists and is a folder + if os.path.exists(CERTS_DIRECTORY) and os.path.isdir(CERTS_DIRECTORY): + + # Iterate over all certificates from root_certs folder + for item in os.listdir(CERTS_DIRECTORY): + + # Combine the directory path with the item name to create the full path + item_path = os.path.join(CERTS_DIRECTORY, item) + + # Check if item is a file + if os.path.isfile(item_path): + + #If True remove file + os.unlink(item_path) + + else: + + # If item is a folder remove it + shutil.rmtree(item_path) + + except PermissionError: + + # Permission related issues + print(f"Permission Denied: {item}") + + except OSError as err: + + # System related issues + print(f"Error removing {item}: {err}") + +def load_cert_file(cert_filename): + """ Utility method to load a certificate file in binary read mode """ + + # Construct the full file path + cert_path = os.path.join(CERTS_PATH, cert_filename) + + # Open the certificate file in binary read mode + with open(cert_path, "rb") as cert_file: + + # Return the certificate file + return cert_file.read() + +def extract_name(cert_data): + """ Utility method to extract the Common Name (CN) from cert data """ + + # Load the cert using the cryptography library + cert = x509.load_pem_x509_certificate(cert_data, default_backend()) + + # Extract and return the common name value + return cert.subject.get_attributes_for_oid(x509.NameOID.COMMON_NAME)[0].value + +@pytest.fixture() +def add_certs(request): + """ Upload specified certificates to local/root_certs """ + + # Access the parameter (certs list) provided to the fixture + certs = request.param + + # Iterate over the certificate names provided + for cert in certs: + + # Construct the full path for cert from 'testing/api/certificates' + source_path = os.path.join(CERTS_PATH, cert) + + # Copy the cert from 'testing/api/certificates' to 'local/root_certs' + shutil.copy(source_path, CERTS_DIRECTORY) + + # Return the list with certs name + return certs + +@pytest.fixture() +def reset_certs(): + """ Delete the certificates before and after each test """ + + # Delete before the test + delete_all_certs() + + yield + + # Delete after the test + delete_all_certs() + +# Use parametrize to create a test suite for 3 scenarios +@pytest.mark.parametrize("add_certs", [ + [], + ["crt.pem"], + ["crt.pem", "WR2.pem"], +], indirect=True) +def test_get_certs(reset_certs, add_certs, testrun): # pylint: disable=W0613 + """ Test for get certs when none, one or two certs are available (200) """ + + # Send the GET request to "/system/config/certs" endpoint + r = requests.get(f"{API}/system/config/certs", timeout=5) + + # Check if the status code is 200 (OK) + assert r.status_code == 200 + + # Parse the response (certificates) + response = r.json() + + # Check if response is a list + assert isinstance(response, list) + + # Check if the number of certs matches the number of certs available + assert len(response) == len(add_certs) + +def test_upload_cert(reset_certs, testrun): # pylint: disable=W0613 + """ Test for upload certificate successfully (200) """ + + # Load the first certificate file content using the utility method + cert_file = load_cert_file("crt.pem") + + # Send a POST request to the API endpoint to upload the certificate + r = requests.post( + f"{API}/system/config/certs", + files={"file": ("crt.pem", cert_file, "application/x-x509-ca-cert")}, + timeout=5 + ) + + # Check if status code is 201 (Created) + assert r.status_code == 201 + + # Parse the response + response = r.json() + + # Check if 'filename' field is in the response + assert "filename" in response + + # Check if the certificate filename is 'crt.pem' + assert response["filename"] == "crt.pem" + + # Load the second certificate file using the utility method + cert_file = load_cert_file("WR2.pem") + + # Send a POST request to the API endpoint to upload the second certificate + r = requests.post( + f"{API}/system/config/certs", + files={"file": ("WR2.pem", cert_file, "application/x-x509-ca-cert")}, + timeout=5 + ) + + # Check if status code is 201 (Created) + assert r.status_code == 201 + + # Parse the response + response = r.json() + + # Check if 'filename' field is in the response + assert "filename" in response + + # Check if the certificate filename is 'WR2.pem' + assert response["filename"] == "WR2.pem" + + # Send get request to check that the certificates are listed + r = requests.get(f"{API}/system/config/certs", timeout=5) + + # Parse the response + response = r.json() + + # Check if "crt.pem" exists + assert any(cert["filename"] == "crt.pem" for cert in response) + + # Check if "WR2.pem" exists + assert any(cert["filename"] == "WR2.pem" for cert in response) + +def test_upload_invalid_cert_format(reset_certs, testrun): # pylint: disable=W0613 + """ Test for upload an invalid certificate format (400) """ + + # Load the first certificate file content using the utility method + cert_file = load_cert_file("invalid.pem") + + # Send a POST request to the API endpoint to upload the certificate + r = requests.post( + f"{API}/system/config/certs", + files={"file": ("invalid.pem", cert_file, "application/x-x509-ca-cert")}, + timeout=5 + ) + + # Check if status code is 400 (bad request) + assert r.status_code == 400 + + # Parse the response + response = r.json() + + # Check if "error" key is in response + assert "error" in response + +def test_upload_invalid_cert_name(reset_certs, testrun): # pylint: disable=W0613 + """ Test for upload a valid certificate with invalid filename (400) """ + + # Assign the invalid certificate name to a variable + cert_name = "invalidname1234567891234.pem" + + # Load the first certificate file content using the utility method + cert_file = load_cert_file(cert_name) + + # Send a POST request to the API endpoint to upload the certificate + r = requests.post( + f"{API}/system/config/certs", + files={"file": (cert_name, cert_file, "application/x-x509-ca-cert")}, + timeout=5 + ) + + # Check if status code is 400 (bad request) + assert r.status_code == 400 + + # Parse the response + response = r.json() + + # Check if "error" key is in response + assert "error" in response + +@pytest.mark.parametrize("add_certs", [["crt.pem"]], indirect=True) +def test_upload_existing_cert(reset_certs, add_certs, testrun): # pylint: disable=W0613 + """ Test for upload an existing certificate (409) """ + + # Load the cert file content using the utility method + cert_file = load_cert_file("crt.pem") + + # Send a POST request to the API endpoint to upload the second certificate + r = requests.post( + f"{API}/system/config/certs", + files={"file": ("crt.pem", cert_file, "application/x-x509-ca-cert")}, + timeout=5 + ) + + # Check if status code is 409 (conflict) + assert r.status_code == 409 + + # Parse the json response + response = r.json() + + # Check if "error" key is in response + assert "error" in response + +@pytest.mark.parametrize("add_certs", [["crt.pem", "WR2.pem"]], indirect=True) +def test_delete_cert_success(reset_certs, add_certs, testrun): # pylint: disable=W0613 + """ Test for successfully deleting an existing certificate (200) """ + + # Load the first cert details to extract the 'name' value + uploaded_cert = load_cert_file("crt.pem") + + # Assign the 'name' value from certificate + cert_name = extract_name(uploaded_cert) + + # Assign the payload + delete_payload = {"name": cert_name} + + # Send delete certificate request + r = requests.delete(f"{API}/system/config/certs", + data=json.dumps(delete_payload), + timeout=5) + + # Check if status code is 200 (OK) + assert r.status_code == 200 + + # Send the get request to display all certificates + r = requests.get(f"{API}/system/config/certs", timeout=5) + + # Parse the json response + response = r.json() + + # Check that the certificate is no longer listed + assert not any(cert["filename"] == "crt.pem" for cert in response) + + # Load the second cert details to extract the 'name' value + uploaded_cert = load_cert_file("WR2.pem") + + # Assign the 'name' value from certificate + cert_name = extract_name(uploaded_cert) + + # Assign the payload + delete_payload = {"name": cert_name} + + # Send delete certificate request + r = requests.delete(f"{API}/system/config/certs", + data=json.dumps(delete_payload), + timeout=5) + + # Check if status code is 200 (OK) + assert r.status_code == 200 + + # Send the get request to display all certificates + r = requests.get(f"{API}/system/config/certs", timeout=5) + + # Parse the json response + response = r.json() + + # Check that the certificate is no longer listed + assert not any(cert["filename"] == "WR2.pem" for cert in response) + +@pytest.mark.parametrize("add_certs", [["crt.pem"]], indirect=True) +def test_delete_cert_bad_request(reset_certs, add_certs, testrun): # pylint: disable=W0613 + """ Test for delete a certificate without providing the name (400)""" + + # Empty payload + delete_payload = {} + + # Send the delete request + r = requests.delete(f"{API}/system/config/certs", + data=json.dumps(delete_payload), + timeout=5) + + # Check if status code is 400 (Bad Request) + assert r.status_code == 400 + + # parse the json response + response = r.json() + + # Check if error in response + assert "error" in response + +def test_delete_cert_not_found(reset_certs, testrun): # pylint: disable=W0613 + """ Test for delete certificate when does not exist (404) """ + + # Attempt to delete a certificate with a name that doesn't exist + delete_payload = {"name": "non existing"} + + # Send the delete request + r = requests.delete(f"{API}/system/config/certs", + data=json.dumps(delete_payload), + timeout=5) + + # Check if status code is 404 (Not Found) + assert r.status_code == 404 + + # parse the json response + response = r.json() + + # Check if error in response + assert "error" in response + +# Tests for profile endpoints + +@pytest.fixture() +def add_profiles(request): + """ Upload specified profile to local/risk_profiles """ + + # Access the parameter (profiles list) provided to the fixture + profiles = request.param + + # Iterate over the profile names provided + for profile in profiles: + + # Construct full path of the file from 'testing/api/profiles' folder + source_path = os.path.join(PROFILES_PATH, profile) + + # Copy the file_name from 'testing/api/profiles' to 'local/risk_profiles' + shutil.copy(source_path, PROFILES_DIRECTORY) + + # Return the list with profiles name + return profiles + +def delete_all_profiles(): + """Utility method to delete all profiles from local/risk_profiles""" + + try: + + # Check if the profile_path (local/risk_profiles) exists and is a folder + if os.path.exists(PROFILES_DIRECTORY) and os.path.isdir(PROFILES_DIRECTORY): + + # Iterate over all profiles from risk_profiles folder + for item in os.listdir(PROFILES_DIRECTORY): + + # Create the full path + item_path = os.path.join(PROFILES_DIRECTORY, item) + + # Check if item is a file + if os.path.isfile(item_path): + + # Remove file + os.unlink(item_path) + + else: + + # If item is a folder remove it + shutil.rmtree(item_path) + + except PermissionError: + + # Permission related issues + print(f"Permission Denied: {item}") + + except OSError as err: + + # System related issues + print(f"Error removing {item}: {err}") + +@pytest.fixture() +def empty_profiles_dir(): + """ Delete all the profiles before and after test """ + + # Delete before the test + delete_all_profiles() + + yield + + # Delete after the test + delete_all_profiles() + +def profile_exists(profile_name): + """ Utility method to check if profile exists """ + + # Send the get request + r = requests.get(f"{API}/profiles", timeout=5) + + # Check if status code is not 200 (OK) + if r.status_code != 200: + raise ValueError(f"Api request failed with code: {r.status_code}") + + # Parse the JSON response to get the list of profiles + profiles = r.json() + + # Return if name is in the list of profiles + return any(p["name"] == profile_name for p in profiles) + +@pytest.fixture() +def remove_risk_assessment(): + """ Fixture to remove and restore risk_assessment.json """ + + # Path to the risk_assessment.json file + risk_assessment_path = os.path.join("resources", "risk_assessment.json") + + # Backup path for the risk_assessment.json file + backup_path = os.path.join("resources", "risk_assessment_backup.json") + + # Create a backup of the risk_assessment.json file + if os.path.exists(risk_assessment_path): + shutil.copy(risk_assessment_path, backup_path) + + # Delete the risk_assessment.json file + if os.path.exists(risk_assessment_path): + os.remove(risk_assessment_path) + + # Run the test + yield + + # Restore the risk assessment file after the test + if os.path.exists(backup_path): + shutil.copy(backup_path, risk_assessment_path) + os.remove(backup_path) + +def test_get_profiles_format(testrun): # pylint: disable=W0613 + """ Test for profiles format (200) """ + + # Send the get request + r = requests.get(f"{API}/profiles/format", timeout=5) + + # Check if status code is 200 (OK) + assert r.status_code == 200 + + # Parse the response + response = r.json() + + # Check if the response is a list + assert isinstance(response, list) + + # Check that each item in the response has keys "questions" and "type" + for item in response: + assert "question" in item + assert "type" in item + +# Use parametrize to create a test suite for 3 scenarios +@pytest.mark.parametrize("add_profiles", [ + [], + ["valid_profile.json"], + ["valid_profile.json", "draft_profile.json"], +], indirect=True) +def test_get_profiles(empty_profiles_dir, add_profiles, testrun): # pylint: disable=W0613 + """ Test get profiles when none, one or two profiles are available (200) """ + + # Send get request to the "/profiles" endpoint + r = requests.get(f"{API}/profiles", timeout=5) + + # Check if the status code is 200 (OK) + assert r.status_code == 200 + + # Parse the response (profiles) + response = r.json() + + # Check if response is a list + assert isinstance(response, list) + + # Check if the number of profiles matches the number of profiles available + assert len(response) == len(add_profiles) + + # Assign the expected profile fields + expected_fields = [ + "name", "status", "created", "version", "questions", "risk" + ] + + # Check if profile exist + if len(add_profiles) > 0: + + # Iterate through profiles + for profile in response: + + # Iterate through expected_fields list + for field in expected_fields: + + # Check if the field is in profile + assert field in profile + + # Assign profile["questions"] + profile_questions = profile["questions"] + + # Check if "questions" value is a list + assert isinstance(profile_questions, list) + + # Check that "questions" value has the expected fields + for element in profile_questions: + + # Check if each element is dict + assert isinstance(element, dict) + + # Check if "question" key is in dict element + assert "question" in element + + # Check if "asnswer" key is in dict element + assert "answer" in element + +def test_create_profile(testrun): # pylint: disable=W0613 + """ Test for create profile when profile does not exist (201) """ + + # Load the profile + new_profile = load_json("valid_profile.json", directory=PROFILES_PATH) + + # Assign the profile name to profile_name + profile_name = new_profile["name"] + + # Check if the profile already exists + if profile_exists(profile_name): + raise ValueError(f"Profile: {profile_name} exists") + + # Send the post request + r = requests.post(f"{API}/profiles", data=json.dumps(new_profile), timeout=5) + + # Check if status code is 201 (Created) + assert r.status_code == 201 + + # Parse the response + response = r.json() + + # Check if "success" key in response + assert "success" in response + + # Verify profile creation + r = requests.get(f"{API}/profiles", timeout=5) + + # Check if status code is 200 (OK) + assert r.status_code == 200 + + # Parse the response + profiles = r.json() + + # Iterate through all the profiles to find the profile based on the "name" + created_profile = next( + (p for p in profiles if p["name"] == profile_name), None + ) + + # Check if profile was created + assert created_profile is not None + +@pytest.mark.parametrize("add_profiles", [ + ["valid_profile.json"] +], indirect=True) +def test_update_profile(empty_profiles_dir, add_profiles, testrun): # pylint: disable=W0613 + """ Test for update profile when profile already exists (200) """ + + # Load the profile using load_json utility method + new_profile = load_json("valid_profile.json", directory=PROFILES_PATH) + + # Assign the new_profile name + profile_name = new_profile["name"] + + # Assign the profile questions + profile_questions = new_profile["questions"] + + # Assign the updated_profile name + updated_profile_name = "updated_valid_profile" + + # Payload with the updated device name + updated_profile = { + "name": profile_name, + "rename" : updated_profile_name, + "questions": profile_questions + } + + # Exception if the profile does not exists + if not profile_exists(profile_name): + raise ValueError(f"Profile: {profile_name} does not exists") + + # Send the post request to update the profile + r = requests.post( + f"{API}/profiles", + data=json.dumps(updated_profile), + timeout=5) + + # Check if status code is 200 (OK) + assert r.status_code == 200 + + # Parse the response + response = r.json() + + # Check if "success" key in response + assert "success" in response + + # Get request to verify profile update + r = requests.get(f"{API}/profiles", timeout=5) + + # Check if status code is 200 (OK) + assert r.status_code == 200 + # Parse the response + profiles = r.json() -def test_create_device_already_exists( - empty_devices_dir, # pylint: disable=W0613 - testrun): # pylint: disable=W0613 - device_1 = { - "manufacturer": "Google", - "model": "First", - "mac_addr": "00:1e:42:35:73:c4", - "test_modules": { - "dns": {"enabled": True}, - "connection": {"enabled": True}, - "ntp": {"enabled": True}, - "baseline": {"enabled": True}, - "nmap": {"enabled": True}, - }, - } + # Iterate through the profiles to find the profile based on the updated "name" + updated_profile_check = next( + (p for p in profiles if p["name"] == updated_profile_name), + None + ) + # Check if profile was updated + assert updated_profile_check is not None - r = requests.post(f"{API}/device", - data=json.dumps(device_1), - timeout=5) - print(r.text) - assert r.status_code == 201 - assert len(local_get_devices()) == 1 +def test_update_profile_no_profiles_format(empty_profiles_dir, # pylint: disable=W0613 + remove_risk_assessment, testrun): # pylint: disable=W0613 + """Test for profile update when profiles format is not available (501)""" - r = requests.post(f"{API}/device", - data=json.dumps(device_1), + # Prepare a valid profile update request + profile_update = load_json("valid_profile.json", directory=PROFILES_PATH) + + # Send a POST request to update the profile + r = requests.post(f"{API}/profiles", + data=json.dumps(profile_update), timeout=5) - print(r.text) - assert r.status_code == 409 + # Check if the response status code is 501 (Not Implemented) + assert r.status_code == 501 -def test_create_device_invalid_json( - empty_devices_dir, # pylint: disable=W0613 - testrun): # pylint: disable=W0613 - device_1 = { - } + # Parse the response + response = r.json() - r = requests.post(f"{API}/device", - data=json.dumps(device_1), - timeout=5) - print(r.text) - assert r.status_code == 400 + # Check if "error" key is present in the response + assert "error" in response + # Check if the error message matches the expected response + assert response["error"] == "Risk profiles are not available right now" -def test_create_device_invalid_request( - empty_devices_dir, # pylint: disable=W0613 - testrun): # pylint: disable=W0613 +@pytest.mark.parametrize("add_profiles", [ + ["valid_profile.json"] +], indirect=True) +def test_update_profile_invalid_json(empty_profiles_dir, add_profiles, # pylint: disable=W0613 + testrun): # pylint: disable=W0613 + """ Test for update profile invalid JSON payload (400) """ - r = requests.post(f"{API}/device", - data=None, - timeout=5) - print(r.text) + # Invalid JSON + updated_profile = {} + + # Send the post request to update the profile + r = requests.post( + f"{API}/profiles", + data=json.dumps(updated_profile), + timeout=5) + + # Parse the response + response = r.json() + + # Check if status code is 400 (Bad request) assert r.status_code == 400 + # Check if "error" key in response + assert "error" in response -def test_device_edit_device( - testing_devices, # pylint: disable=W0613 - testrun): # pylint: disable=W0613 - with open( - testing_devices[1], encoding="utf-8" - ) as f: - local_device = json.load(f) +def test_create_profile_invalid_json(empty_profiles_dir, testrun): # pylint: disable=W0613 + """ Test for create profile invalid JSON payload (400) """ - mac_addr = local_device["mac_addr"] - new_model = "Alphabet" + # Invalid JSON + new_profile = {} - r = requests.get(f"{API}/devices", timeout=5) - all_devices = json.loads(r.text) + # Send the post request to update the profile + r = requests.post( + f"{API}/profiles", + data=json.dumps(new_profile), + timeout=5) - api_device = next(x for x in all_devices if x["mac_addr"] == mac_addr) + # Parse the response + response = r.json() - updated_device = copy.deepcopy(api_device) - updated_device["model"] = new_model + # Check if status code is 400 (Bad request) + assert r.status_code == 400 - new_test_modules = { - k: {"enabled": not v["enabled"]} - for k, v in updated_device["test_modules"].items() - } - updated_device["test_modules"] = new_test_modules + # Check if "error" key in response + assert "error" in response - updated_device_payload = {} - updated_device_payload["device"] = updated_device - updated_device_payload["mac_addr"] = mac_addr +@pytest.mark.parametrize("add_profiles", [ + ["valid_profile.json"] +], indirect=True) +def test_delete_profile(empty_profiles_dir, add_profiles, testrun): # pylint: disable=W0613 + """ Test for successfully delete profile (200) """ - print("updated_device") - pretty_print(updated_device) - print("api_device") - pretty_print(api_device) + # Load the profile using load_json utility method + profile_to_delete = load_json("valid_profile.json", directory=PROFILES_PATH) - # update device - r = requests.post(f"{API}/device/edit", - data=json.dumps(updated_device_payload), - timeout=5) + # Assign the profile name + profile_name = profile_to_delete["name"] + # Send the delete request + r = requests.delete( + f"{API}/profiles", + data=json.dumps(profile_to_delete), + timeout=5) + + # Check if status code is 200 (OK) assert r.status_code == 200 - r = requests.get(f"{API}/devices", timeout=5) - all_devices = json.loads(r.text) - updated_device_api = next(x for x in all_devices if x["mac_addr"] == mac_addr) + # Parse the JSON response + response = r.json() - assert updated_device_api["model"] == new_model - assert updated_device_api["test_modules"] == new_test_modules + # Check if the response contains "success" key + assert "success" in response + # Check if the profile has been deleted + r = requests.get(f"{API}/profiles", timeout=5) -def test_device_edit_device_not_found( - empty_devices_dir, # pylint: disable=W0613 - testrun): # pylint: disable=W0613 - device_1 = { - "manufacturer": "Google", - "model": "First", - "mac_addr": "00:1e:42:35:73:c4", - "test_modules": { - "dns": {"enabled": True}, - "connection": {"enabled": True}, - "ntp": {"enabled": True}, - "baseline": {"enabled": True}, - "nmap": {"enabled": True}, - }, - } + # Check if status code is 200 (OK) + assert r.status_code == 200 - r = requests.post(f"{API}/device", - data=json.dumps(device_1), - timeout=5) - print(r.text) - assert r.status_code == 201 - assert len(local_get_devices()) == 1 + # Parse the JSON response + profiles = r.json() - updated_device = copy.deepcopy(device_1) + # Iterate through the profiles to find the profile based on the "name" + deleted_profile = next( + (p for p in profiles if p["name"] == profile_name), + None + ) - updated_device_payload = {} - updated_device_payload["device"] = updated_device - updated_device_payload["mac_addr"] = "00:1e:42:35:73:c6" - updated_device_payload["model"] = "Alphabet" + # Check if profile was deleted + assert deleted_profile is None +def test_delete_profile_no_profile(empty_profiles_dir, testrun): # pylint: disable=W0613 + """ Test delete profile if the profile does not exists (404) """ - r = requests.post(f"{API}/device/edit", - data=json.dumps(updated_device_payload), - timeout=5) + # Assign the profile to delete + profile_to_delete = {"name": "non existing"} - assert r.status_code == 404 + # Delete the profile + r = requests.delete( + f"{API}/profiles", + data=json.dumps(profile_to_delete), + timeout=5) + # Check if status code is 404 (Profile does not exist) + assert r.status_code == 404 -def test_device_edit_device_incorrect_json_format( - empty_devices_dir, # pylint: disable=W0613 - testrun): # pylint: disable=W0613 - device_1 = { - "manufacturer": "Google", - "model": "First", - "mac_addr": "00:1e:42:35:73:c4", - "test_modules": { - "dns": {"enabled": True}, - "connection": {"enabled": True}, - "ntp": {"enabled": True}, - "baseline": {"enabled": True}, - "nmap": {"enabled": True}, - }, - } + # Parse the response + response = r.json() - r = requests.post(f"{API}/device", - data=json.dumps(device_1), - timeout=5) - print(r.text) - assert r.status_code == 201 - assert len(local_get_devices()) == 1 + # Check if "error" key in response + assert "error" in response - updated_device_payload = {} +def test_delete_profile_invalid_json(empty_profiles_dir, testrun): # pylint: disable=W0613 + """ Test for delete profile invalid JSON payload (400) """ + # Invalid payload + profile_to_delete = {} - r = requests.post(f"{API}/device/edit", - data=json.dumps(updated_device_payload), - timeout=5) + # Delete the profile + r = requests.delete( + f"{API}/profiles", + data=json.dumps(profile_to_delete), + timeout=5) + # Check if status code is 400 (bad request) assert r.status_code == 400 + # Parse the response + response = r.json() -def test_device_edit_device_with_mac_already_exists( - empty_devices_dir, # pylint: disable=W0613 - testrun): # pylint: disable=W0613 - device_1 = { - "manufacturer": "Google", - "model": "First", - "mac_addr": "00:1e:42:35:73:c4", - "test_modules": { - "dns": {"enabled": True}, - "connection": {"enabled": True}, - "ntp": {"enabled": True}, - "baseline": {"enabled": True}, - "nmap": {"enabled": True}, - }, - } - - r = requests.post(f"{API}/device", - data=json.dumps(device_1), - timeout=5) - print(r.text) - assert r.status_code == 201 - assert len(local_get_devices()) == 1 - - device_2 = { - "manufacturer": "Google", - "model": "Second", - "mac_addr": "00:1e:42:35:73:c6", - "test_modules": { - "dns": {"enabled": True}, - "connection": {"enabled": True}, - "ntp": {"enabled": True}, - "baseline": {"enabled": True}, - "nmap": {"enabled": True}, - }, - } - r = requests.post(f"{API}/device", - data=json.dumps(device_2), - timeout=5) - assert r.status_code == 201 - assert len(local_get_devices()) == 2 - - updated_device = copy.deepcopy(device_1) + # Check if "error" key in response + assert "error" in response - updated_device_payload = {} - updated_device_payload = {} - updated_device_payload["device"] = updated_device - updated_device_payload["mac_addr"] = "00:1e:42:35:73:c6" - updated_device_payload["model"] = "Alphabet" + # Invalid payload + profile_to_delete_2 = {"status": "Draft"} + # Delete the profile + r = requests.delete( + f"{API}/profiles", + data=json.dumps(profile_to_delete_2), + timeout=5) - r = requests.post(f"{API}/device/edit", - data=json.dumps(updated_device_payload), - timeout=5) + # Check if status code is 400 (bad request) + assert r.status_code == 400 - assert r.status_code == 409 + # Parse the response + response = r.json() + # Check if "error" key in response + assert "error" in response -def test_system_latest_version(testrun): # pylint: disable=W0613 - r = requests.get(f"{API}/system/version", timeout=5) - assert r.status_code == 200 - updated_system_version = json.loads(r.text)["update_available"] - assert updated_system_version is False +@pytest.mark.parametrize("add_profiles", [ + ["valid_profile.json"] +], indirect=True) +def test_delete_profile_server_error(empty_profiles_dir, add_profiles, # pylint: disable=W0613 + testrun): # pylint: disable=W0613 + """ Test for delete profile causing internal server error (500) """ -def test_get_system_config(testrun): # pylint: disable=W0613 - r = requests.get(f"{API}/system/config", timeout=5) + # Assign the profile from the fixture + profile_to_delete = load_json("valid_profile.json", directory=PROFILES_PATH) - with open( - SYSTEM_CONFIG_PATH, - encoding="utf-8" - ) as f: - local_config = json.load(f) + # Assign the profile name to profile_name + profile_name = profile_to_delete["name"] - api_config = json.loads(r.text) + # Construct the path to the profile JSON file in local/risk_profiles + risk_profile_path = os.path.join(PROFILES_DIRECTORY, f"{profile_name}.json") - # validate structure - assert set(dict_paths(api_config)) | set(dict_paths(local_config)) == set( - dict_paths(api_config) - ) + # Delete the profile JSON file before making the DELETE request + if os.path.exists(risk_profile_path): + os.remove(risk_profile_path) - assert ( - local_config["network"]["device_intf"] - == api_config["network"]["device_intf"] - ) - assert ( - local_config["network"]["internet_intf"] - == api_config["network"]["internet_intf"] - ) + # Send the DELETE request to delete the profile + r = requests.delete(f"{API}/profiles", + json={"name": profile_to_delete["name"]}, + timeout=5) + # Check if status code is 500 (Internal Server Error) + assert r.status_code == 500 -def test_invalid_path_get(testrun): # pylint: disable=W0613 - r = requests.get(f"{API}/blah/blah", timeout=5) - response = json.loads(r.text) - assert r.status_code == 404 - with open( - os.path.join(os.path.dirname(__file__), "mockito/invalid_request.json"), - encoding="utf-8" - ) as f: - mockito = json.load(f) + # Parse the json response + response = r.json() - # validate structure - assert set(dict_paths(mockito)) == set(dict_paths(response)) + # Check if error in response + assert "error" in response +# Skipped tests currently not working due to blocking during monitoring period @pytest.mark.skip() -def test_trigger_run(testing_devices, testrun): # pylint: disable=W0613 +def test_delete_device_testrun_running(testing_devices, testrun): # pylint: disable=W0613 + payload = {"device": {"mac_addr": BASELINE_MAC_ADDR, "firmware": "asd"}} r = requests.post(f"{API}/system/start", data=json.dumps(payload), timeout=10) assert r.status_code == 200 @@ -966,49 +3058,54 @@ def test_trigger_run(testing_devices, testrun): # pylint: disable=W0613 start_test_device("x123", BASELINE_MAC_ADDR) until_true( - lambda: query_system_status().lower() == "compliant", - "system status is `complete`", + lambda: query_system_status().lower() == "in progress", + "system status is `in progress`", 600, ) - stop_test_device("x123") - - # Validate response - r = requests.get(f"{API}/system/status", timeout=5) - response = json.loads(r.text) - pretty_print(response) - - # Validate results - results = {x["name"]: x for x in response["tests"]["results"]} - print(results) - # there are only 3 baseline tests - assert len(results) == 3 + device_1 = { + "manufacturer": "Google", + "model": "First", + "mac_addr": BASELINE_MAC_ADDR, + "test_modules": { + "dns": {"enabled": True}, + "connection": {"enabled": True}, + "ntp": {"enabled": True}, + "baseline": {"enabled": True}, + "nmap": {"enabled": True}, + }, + } + r = requests.delete(f"{API}/device/", + data=json.dumps(device_1), + timeout=5) + assert r.status_code == 403 - # Validate structure - with open( - os.path.join( - os.path.dirname(__file__), "mockito/running_system_status.json" - ), encoding="utf-8" - ) as f: - mockito = json.load(f) +@pytest.mark.skip() +@pytest.mark.parametrize("add_devices", [ + ["device_1"] +],indirect=True) +def test_stop_running_test(empty_devices_dir, add_devices, testrun): # pylint: disable=W0613 + """ Test for successfully stop testrun when test is running (200) """ - # validate structure - assert set(dict_paths(mockito)).issubset(set(dict_paths(response))) + # Load the device and mac address using add_device utility method + device = load_json("device_config.json", directory=DEVICE_1_PATH) - # Validate results structure - assert set(dict_paths(mockito["tests"]["results"][0])).issubset( - set(dict_paths(response["tests"]["results"][0])) - ) + mac_addr = device["mac_addr"] - # Validate a result - assert results["baseline.compliant"]["result"] == "Compliant" + test_modules = device["test_modules"] + # Payload with device details + payload = { + "device": { + "mac_addr": mac_addr, + "firmware": "test", + "test_modules": test_modules + } + } -@pytest.mark.skip() -def test_stop_running_test(testing_devices, testrun): # pylint: disable=W0613 - payload = {"device": {"mac_addr": ALL_MAC_ADDR, "firmware": "asd"}} r = requests.post(f"{API}/system/start", data=json.dumps(payload), timeout=10) + assert r.status_code == 200 until_true( @@ -1029,28 +3126,113 @@ def test_stop_running_test(testing_devices, testrun): # pylint: disable=W0613 # Validate response r = requests.post(f"{API}/system/stop", timeout=5) - response = json.loads(r.text) + response = r.json() pretty_print(response) assert response == {"success": "Testrun stopped"} time.sleep(1) # Validate response r = requests.get(f"{API}/system/status", timeout=5) - response = json.loads(r.text) + response = r.json() pretty_print(response) assert response["status"] == "Cancelled" +@pytest.mark.skip() +def test_status_in_progress(testing_devices, testrun): # pylint: disable=W0613 + + payload = {"device": {"mac_addr": BASELINE_MAC_ADDR, "firmware": "asd"}} + r = requests.post(f"{API}/system/start", data=json.dumps(payload), timeout=10) + assert r.status_code == 200 + + until_true( + lambda: query_system_status().lower() == "waiting for device", + "system status is `waiting for device`", + 30, + ) + + start_test_device("x123", BASELINE_MAC_ADDR) + + until_true( + lambda: query_system_status().lower() == "in progress", + "system status is `in progress`", + 600, + ) + +@pytest.mark.skip() +def test_start_testrun_already_in_progress( + testing_devices, # pylint: disable=W0613 + testrun): # pylint: disable=W0613 + payload = {"device": {"mac_addr": BASELINE_MAC_ADDR, "firmware": "asd"}} + r = requests.post(f"{API}/system/start", data=json.dumps(payload), timeout=10) + + until_true( + lambda: query_system_status().lower() == "waiting for device", + "system status is `waiting for device`", + 30, + ) + + start_test_device("x123", BASELINE_MAC_ADDR) + + until_true( + lambda: query_system_status().lower() == "in progress", + "system status is `in progress`", + 600, + ) + r = requests.post(f"{API}/system/start", data=json.dumps(payload), timeout=10) + assert r.status_code == 409 + +@pytest.mark.skip() +def test_trigger_run(testing_devices, testrun): # pylint: disable=W0613 + payload = {"device": {"mac_addr": BASELINE_MAC_ADDR, "firmware": "asd"}} + r = requests.post(f"{API}/system/start", data=json.dumps(payload), timeout=10) + assert r.status_code == 200 + + until_true( + lambda: query_system_status().lower() == "waiting for device", + "system status is `waiting for device`", + 30, + ) + + start_test_device("x123", BASELINE_MAC_ADDR) + + until_true( + lambda: query_system_status().lower() == "compliant", + "system status is `complete`", + 600, + ) + + stop_test_device("x123") -def test_stop_running_not_running(testrun): # pylint: disable=W0613 # Validate response - r = requests.post(f"{API}/system/stop", - timeout=10) - response = json.loads(r.text) + r = requests.get(f"{API}/system/status", timeout=5) + response = r.json() pretty_print(response) - assert r.status_code == 404 - assert response["error"] == "Testrun is not currently running" + # Validate results + results = {x["name"]: x for x in response["tests"]["results"]} + print(results) + # there are only 3 baseline tests + assert len(results) == 3 + + # Validate structure + with open( + os.path.join( + os.path.dirname(__file__), "mockito/running_system_status.json" + ), encoding="utf-8" + ) as f: + mockito = json.load(f) + + # validate structure + assert set(dict_paths(mockito)).issubset(set(dict_paths(response))) + + # Validate results structure + assert set(dict_paths(mockito["tests"]["results"][0])).issubset( + set(dict_paths(response["tests"]["results"][0])) + ) + + # Validate a result + assert results["baseline.compliant"]["result"] == "Compliant" @pytest.mark.skip() def test_multiple_runs(testing_devices, testrun): # pylint: disable=W0613 @@ -1078,7 +3260,7 @@ def test_multiple_runs(testing_devices, testrun): # pylint: disable=W0613 # Validate response r = requests.get(f"{API}/system/status", timeout=5) - response = json.loads(r.text) + response = r.json() pretty_print(response) # Validate results @@ -1110,28 +3292,34 @@ def test_multiple_runs(testing_devices, testrun): # pylint: disable=W0613 stop_test_device("x123") +@pytest.mark.skip() +def test_status_non_compliant(testing_devices, testrun): # pylint: disable=W0613 -def test_create_invalid_chars(empty_devices_dir, testrun): # pylint: disable=W0613 - # local_delete_devices(ALL_DEVICES) - # We must start test run with no devices in local/devices for this test - # to function as expected - assert len(local_get_devices()) == 0 - - # Test adding device - device_1 = { - "manufacturer": "/'disallowed characters///", - "model": "First", - "mac_addr": BASELINE_MAC_ADDR, - "test_modules": { - "dns": {"enabled": False}, - "connection": {"enabled": True}, - "ntp": {"enabled": True}, - "baseline": {"enabled": True}, - "nmap": {"enabled": True}, - }, + r = requests.get(f"{API}/devices", timeout=5) + all_devices = r.json() + payload = { + "device": { + "mac_addr": all_devices[0]["mac_addr"], + "firmware": "asd" + } } - - r = requests.post(f"{API}/device", data=json.dumps(device_1), - timeout=5) + r = requests.post(f"{API}/system/start", data=json.dumps(payload), + timeout=10) + assert r.status_code == 200 print(r.text) - print(r.status_code) + + until_true( + lambda: query_system_status().lower() == "waiting for device", + "system status is `waiting for device`", + 30, + ) + + start_test_device("x123", all_devices[0]["mac_addr"]) + + until_true( + lambda: query_system_status().lower() == "non-compliant", + "system status is `complete", + 600, + ) + + stop_test_device("x123") diff --git a/testing/baseline/test_baseline b/testing/baseline/test_baseline index dab23620d..44a17d348 100755 --- a/testing/baseline/test_baseline +++ b/testing/baseline/test_baseline @@ -22,8 +22,6 @@ ifconfig sudo apt-get update sudo apt-get install openvswitch-common openvswitch-switch tcpdump jq moreutils coreutils isc-dhcp-client -pip3 install pytest==7.4.4 - # Setup device network sudo ip link add dev endev0a type veth peer name endev0b sudo ip link set dev endev0a up @@ -42,7 +40,7 @@ sudo cp testing/baseline/system.json local/system.json # Copy device configs to testrun sudo cp -r testing/device_configs/* local/devices -sudo bin/testrun --single-intf --no-ui --validate > $TESTRUN_OUT 2>&1 & +sudo bin/testrun --single-intf --no-ui --target 02:42:aa:00:01:01 -fw 1.0 --validate > $TESTRUN_OUT 2>&1 & TPID=$! # Time to wait for testrun to be ready @@ -74,6 +72,11 @@ echo "Done baseline test" more $TESTRUN_OUT -pytest testing/baseline/test_baseline.py +# Needs to be sudo because this invokes bin/testrun +sudo venv/bin/python3 -m pytest -v testing/baseline/test_baseline.py + +# Clean the device network +sudo ip link del dev endev0a +sudo docker network rm endev0 exit $? \ No newline at end of file diff --git a/testing/docker/ci_baseline/Dockerfile b/testing/docker/ci_baseline/Dockerfile index 93ad905f9..2af5ed46a 100644 --- a/testing/docker/ci_baseline/Dockerfile +++ b/testing/docker/ci_baseline/Dockerfile @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -FROM ubuntu@sha256:e6173d4dc55e76b87c4af8db8821b1feae4146dd47341e4d431118c7dd060a74 +FROM ubuntu@sha256:77d57fd89366f7d16615794a5b53e124d742404e20f035c22032233f1826bd6a # Update and get all additional requirements not contained in the base image RUN apt-get update && apt-get -y upgrade diff --git a/testing/pylint/test_pylint b/testing/pylint/test_pylint index 1f71482e5..7e102c7f8 100755 --- a/testing/pylint/test_pylint +++ b/testing/pylint/test_pylint @@ -14,27 +14,35 @@ # See the License for the specific language governing permissions and # limitations under the License. -ERROR_LIMIT=25 - -sudo cmd/install +# Install python venv +python3 -m venv venv +# Activate the venv source venv/bin/activate -sudo pip3 install pylint==3.0.3 +# Install pylint +pip install pylint==3.2.6 + +# Declare the applicable files files=$(find . -path ./venv -prune -o -name '*.py' -print) +# Define the pylint output file OUT=pylint.out -rm -f $OUT && touch $OUT +# Remove it if it already exists +rm -f $OUT +# Run pylint against the target files +# Change the evaluation to total the number of errors +# Output to the specified output file pylint $files -ry --extension-pkg-allow-list=docker --evaluation="error + warning + refactor + convention" 2>/dev/null | tee -a $OUT -new_errors=$(cat $OUT | grep -oP "(?!=^Your code has been rated at)([0-9]+)(?=\.00/10[ \(]?)" ) +# Obtain the total number of errors from the pylint out file +errors=$(cat $OUT | grep -oP "(?!=^Your code has been rated at)([0-9]+)(?=\.00/10[ \(]?)" ) -echo "$new_errors > $ERROR_LIMIT?" -if (( $new_errors > $ERROR_LIMIT)); then - echo new errors $new_errors > error limit $ERROR_LIMIT - echo failing .. +# Check if any errors exist +if (( $errors > 0 )); then + echo "$errors pylint issues have been identified. These must be resolved before merging." exit 1 fi diff --git a/testing/tests/test_tests.py b/testing/tests/test_tests.py index aaae1a09d..21be6b7de 100644 --- a/testing/tests/test_tests.py +++ b/testing/tests/test_tests.py @@ -96,7 +96,7 @@ def test_list_tests(capsys, results, test_matrix): print('============') print('============') print('tests seen:') - print('\n'.join(set([x.name for x in all_tests]))) + print('\n'.join(set(x.name for x in all_tests))) print('\ntesting for pass:') print('\n'.join(ci_pass)) print('\ntesting for fail:') diff --git a/testing/unit/conn/captures/monitor.pcap b/testing/unit/conn/captures/monitor.pcap new file mode 100644 index 000000000..0dfb85ff4 Binary files /dev/null and b/testing/unit/conn/captures/monitor.pcap differ diff --git a/testing/unit/conn/captures/startup.pcap b/testing/unit/conn/captures/startup.pcap new file mode 100644 index 000000000..dadd2edbc Binary files /dev/null and b/testing/unit/conn/captures/startup.pcap differ diff --git a/testing/unit/conn/conn_module_test.py b/testing/unit/conn/conn_module_test.py index d31a8051f..1e5798df1 100644 --- a/testing/unit/conn/conn_module_test.py +++ b/testing/unit/conn/conn_module_test.py @@ -13,13 +13,18 @@ # limitations under the License. """Module run all the Connection module related unit tests""" from port_stats_util import PortStatsUtil +from connection_module import ConnectionModule import os +import sys import unittest from common import logger MODULE = 'conn' -# Define the file paths -TEST_FILES_DIR = 'testing/unit/' + MODULE +# Define the directories +TEST_FILES_DIR = '/testing/unit/' + MODULE +OUTPUT_DIR = os.path.join(TEST_FILES_DIR, 'output/') +CAPTURES_DIR = os.path.join(TEST_FILES_DIR, 'captures/') + ETHTOOL_RESULTS_COMPLIANT_FILE = os.path.join(TEST_FILES_DIR, 'ethtool', 'ethtool_results_compliant.txt') ETHTOOL_RESULTS_NONCOMPLIANT_FILE = os.path.join( @@ -34,6 +39,11 @@ ETHTOOL_PORT_STATS_POST_NONCOMPLIANT_FILE = os.path.join( TEST_FILES_DIR, 'ethtool', 'ethtool_port_stats_post_monitor_noncompliant.txt') + +# Define the capture files to be used for the test +STARTUP_CAPTURE_FILE = os.path.join(CAPTURES_DIR, 'startup.pcap') +MONITOR_CAPTURE_FILE = os.path.join(CAPTURES_DIR, 'monitor.pcap') + LOGGER = None @@ -46,6 +56,9 @@ def setUpClass(cls): global LOGGER LOGGER = logger.get_logger('unit_test_' + MODULE) + # Set the MAC address for device in capture files + os.environ['DEVICE_MAC'] = '98:f0:7b:d1:87:06' + # Test the port link status def connection_port_link_compliant_test(self): LOGGER.info('connection_port_link_compliant_test') @@ -117,6 +130,45 @@ def connection_port_speed_autonegotiation_fail_test(self): LOGGER.info(result) self.assertEqual(result[0], False) + # Test proper filtering for ICMP protocol in DHCP packets + def connection_switch_dhcp_snooping_icmp_test(self): + LOGGER.info('connection_switch_dhcp_snooping_icmp_test') + conn_module = ConnectionModule(module=MODULE, + results_dir=OUTPUT_DIR, + startup_capture_file=STARTUP_CAPTURE_FILE, + monitor_capture_file=MONITOR_CAPTURE_FILE) + result = conn_module._connection_switch_dhcp_snooping() # pylint: disable=W0212 + LOGGER.info(result) + self.assertEqual(result[0], True) + + def communication_network_type_test(self): + LOGGER.info('communication_network_type_test') + conn_module = ConnectionModule(module=MODULE, + results_dir=OUTPUT_DIR, + startup_capture_file=STARTUP_CAPTURE_FILE, + monitor_capture_file=MONITOR_CAPTURE_FILE) + result = conn_module._communication_network_type() # pylint: disable=W0212 + details_expected = { + 'mac_address': '98:f0:7b:d1:87:06', + 'multicast': { + 'from': 11, + 'to': 0 + }, + 'broadcast': { + 'from': 13, + 'to': 0 + }, + 'unicast': { + 'from': 0, + 'to': 0 + } + } + LOGGER.info(result) + self.assertEqual(result[0], 'Informational') + self.assertEqual(result[1], 'Packet types detected: Multicast, Broadcast') + self.assertEqual(result[2], details_expected) + #self.assertEqual(result[0], True) + if __name__ == '__main__': suite = unittest.TestSuite() @@ -136,5 +188,17 @@ def connection_port_speed_autonegotiation_fail_test(self): suite.addTest( ConnectionModuleTest('connection_port_speed_autonegotiation_fail_test')) + # DHCP Snooping related tests + suite.addTest( + ConnectionModuleTest('connection_switch_dhcp_snooping_icmp_test')) + + # DHCP Snooping related tests + suite.addTest(ConnectionModuleTest('communication_network_type_test')) + runner = unittest.TextTestRunner() - runner.run(suite) + test_result = runner.run(suite) + + # Check if the tests failed and exit with the appropriate code + if not test_result.wasSuccessful(): + sys.exit(1) # Return a non-zero exit code for failures + sys.exit(0) # Return zero for success diff --git a/testing/unit/dns/dns_module_test.py b/testing/unit/dns/dns_module_test.py index 6c3dec74d..d530498dd 100644 --- a/testing/unit/dns/dns_module_test.py +++ b/testing/unit/dns/dns_module_test.py @@ -16,7 +16,7 @@ import unittest from scapy.all import rdpcap, DNS, wrpcap import os -from testreport import TestReport +import sys MODULE = 'dns' @@ -28,7 +28,6 @@ LOCAL_REPORT = os.path.join(REPORTS_DIR, 'dns_report_local.html') LOCAL_REPORT_NO_DNS = os.path.join(REPORTS_DIR, 'dns_report_local_no_dns.html') -CONF_FILE = 'modules/test/' + MODULE + '/conf/module_config.json' # Define the capture files to be used for the test DNS_SERVER_CAPTURE_FILE = os.path.join(CAPTURES_DIR, 'dns.pcap') @@ -44,11 +43,12 @@ def setUpClass(cls): # Create the output directories and ignore errors if it already exists os.makedirs(OUTPUT_DIR, exist_ok=True) + # Set the MAC address for device in capture files + os.environ['DEVICE_MAC'] = '38:d1:35:01:17:fe' + # Test the module report generation def dns_module_report_test(self): dns_module = DNSModule(module=MODULE, - log_dir=OUTPUT_DIR, - conf_file=CONF_FILE, results_dir=OUTPUT_DIR, dns_server_capture_file=DNS_SERVER_CAPTURE_FILE, startup_capture_file=STARTUP_CAPTURE_FILE, @@ -59,12 +59,6 @@ def dns_module_report_test(self): # Read the generated report with open(report_out_path, 'r', encoding='utf-8') as file: report_out = file.read() - formatted_report = self.add_formatting(report_out) - - # Write back the new formatted_report value - out_report_path = os.path.join(OUTPUT_DIR, 'dns_report_with_dns.html') - with open(out_report_path, 'w', encoding='utf-8') as file: - file.write(formatted_report) # Read the local good report with open(LOCAL_REPORT, 'r', encoding='utf-8') as file: @@ -103,8 +97,6 @@ def dns_module_report_no_dns_test(self): wrpcap(monitor_cap_file, packets_monitor) dns_module = DNSModule(module='dns', - log_dir=OUTPUT_DIR, - conf_file=CONF_FILE, results_dir=OUTPUT_DIR, dns_server_capture_file=dns_server_cap_file, startup_capture_file=startup_cap_file, @@ -115,12 +107,6 @@ def dns_module_report_no_dns_test(self): # Read the generated report with open(report_out_path, 'r', encoding='utf-8') as file: report_out = file.read() - formatted_report = self.add_formatting(report_out) - - # Write back the new formatted_report value - out_report_path = os.path.join(OUTPUT_DIR, 'dns_report_no_dns.html') - with open(out_report_path, 'w', encoding='utf-8') as file: - file.write(formatted_report) # Read the local good report with open(LOCAL_REPORT_NO_DNS, 'r', encoding='utf-8') as file: @@ -128,17 +114,6 @@ def dns_module_report_no_dns_test(self): self.assertEqual(report_out, report_local) - def add_formatting(self, body): - return f''' - - - {TestReport().generate_head()} - - {body} - - DNS Module

Requests to local DNS server Requests to external DNS servers Total DNS requests Total DNS responses
71 6 77 91
Source Destination Type URL
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.4 8.8.8.8 Query mqtt.googleapis.com
10.10.10.4 8.8.8.8 Query mqtt.googleapis.com
8.8.8.8 10.10.10.4 Response mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
8.8.8.8 10.10.10.4 Response mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query pool.ntp.org
10.10.10.14 10.10.10.4 Query pool.ntp.org
10.10.10.4 8.8.8.8 Query pool.ntp.org
10.10.10.4 8.8.8.8 Query pool.ntp.org
8.8.8.8 10.10.10.4 Response pool.ntp.org
10.10.10.4 10.10.10.14 Response pool.ntp.org
8.8.8.8 10.10.10.4 Response pool.ntp.org
10.10.10.4 10.10.10.14 Response pool.ntp.org
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query pool.ntp.org
10.10.10.14 10.10.10.4 Query pool.ntp.org
10.10.10.4 8.8.8.8 Query pool.ntp.org
10.10.10.4 10.10.10.14 Response pool.ntp.org
10.10.10.4 10.10.10.14 Response pool.ntp.org
8.8.8.8 10.10.10.4 Response pool.ntp.org
10.10.10.4 8.8.8.8 Response pool.ntp.org
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.4 8.8.8.8 Query mqtt.googleapis.com
8.8.8.8 10.10.10.4 Response mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query pool.ntp.org
10.10.10.14 10.10.10.4 Query pool.ntp.org
10.10.10.4 10.10.10.14 Response pool.ntp.org
10.10.10.4 10.10.10.14 Response pool.ntp.org
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query pool.ntp.org
10.10.10.4 10.10.10.14 Response pool.ntp.org
10.10.10.4 10.10.10.14 Response pool.ntp.org
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.14 10.10.10.4 Query mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
10.10.10.4 10.10.10.14 Response mqtt.googleapis.com
\ No newline at end of file +

DNS Module

+ + + + + + + + + + + + + + + + +
Requests to local DNS serverRequests to external DNS serversTotal DNS requestsTotal DNS responses
7107184
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SourceDestinationResolved IPTypeURLCount
10.10.10.1410.10.10.4N/AQuerymqtt.googleapis.com64
10.10.10.410.10.10.14173.194.195.206Responsemqtt.googleapis.com38
10.10.10.410.10.10.142607:f8b0:4001:c11::ceResponsemqtt.googleapis.com32
10.10.10.1410.10.10.4N/AQuerypool.ntp.org7
10.10.10.410.10.10.14N/AResponsepool.ntp.org4
10.10.10.410.10.10.145.78.89.3Responsepool.ntp.org2
10.10.10.410.10.10.14199.68.201.234Responsepool.ntp.org2
10.10.10.410.10.10.142607:f8b0:4001:c08::ceResponsemqtt.googleapis.com6
\ No newline at end of file diff --git a/testing/unit/dns/reports/dns_report_local_no_dns.html b/testing/unit/dns/reports/dns_report_local_no_dns.html index 20f3f7511..f144655f5 100644 --- a/testing/unit/dns/reports/dns_report_local_no_dns.html +++ b/testing/unit/dns/reports/dns_report_local_no_dns.html @@ -1,4 +1,4 @@ -

DNS Module

+

DNS Module

diff --git a/testing/unit/framework/session_test.py b/testing/unit/framework/session_test.py new file mode 100644 index 000000000..8c48c6046 --- /dev/null +++ b/testing/unit/framework/session_test.py @@ -0,0 +1,57 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Session methods tests""" + +from unittest.mock import patch +from core import session + + +class MockUtil: + """mock util functions""" + + @staticmethod + def get_sys_interfaces(): + return {"eth0": "00:1A:2B:3C:4D:5E", "eth1": "66:77:88:99:AA:BB"} + + @staticmethod + def diff_dicts(d1, d2): # pylint: disable=W0613 + return { + "items_added": {"eth1": "66:77:88:99:AA:BB"}, + "items_removed": {"eth2": "00:1B:2C:3D:4E:5F"}, + } + + +class TestrunSessionMock(session.TestrunSession): + def __init__(self): # pylint: disable=W0231 + self._ifaces = {"eth0": "00:1A:2B:3C:4D:5E", "eth2": "66:77:88:99:AA:BB"} + + +util = MockUtil() + + +@patch("common.util.get_sys_interfaces", side_effect=util.get_sys_interfaces) +@patch("common.util.diff_dicts", side_effect=util.diff_dicts) +def test_detect_network_adapters_change( + mock_get_sys_interfaces, # pylint: disable=W0613 + mock_diff_dicts, # pylint: disable=W0613 +): + testrun_session = TestrunSessionMock() + + # Test added and removed + result = testrun_session.detect_network_adapters_change() + assert result == { + "adapters_added": {"eth1": "66:77:88:99:AA:BB"}, + "adapters_removed": {"eth2": "00:1B:2C:3D:4E:5F"}, + } diff --git a/testing/unit/framework/util_test.py b/testing/unit/framework/util_test.py new file mode 100644 index 000000000..ec8fd48fc --- /dev/null +++ b/testing/unit/framework/util_test.py @@ -0,0 +1,61 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Util tests""" + +from collections import namedtuple +from unittest.mock import patch +from common import util +from net_orc import ip_control + +Snicaddr = namedtuple('snicaddr', + ['family', 'address']) + +mock_addrs = { + 'eth0': [Snicaddr(17, '00:1A:2B:3C:4D:5E')], + 'wlan0': [Snicaddr(17, '66:77:88:99:AA:BB')], + 'enp0s3': [Snicaddr(17, '11:22:33:44:55:66')] +} + +@patch('psutil.net_if_addrs') +def test_get_sys_interfaces(mock_net_if_addrs): + mock_net_if_addrs.return_value = mock_addrs + # Expected result + expected = { + 'eth0': '00:1A:2B:3C:4D:5E', + 'enp0s3': '11:22:33:44:55:66' + } + + result = ip_control.IPControl.get_sys_interfaces() + # Assert the result + assert result == expected + + +def test_diff_dicts(): + d1 = {'a': 1, 'b': 2} + d2 = {'a': 1, 'b': 2} + #Assert equal dicts + assert not util.diff_dicts(d1, d2) + d2 = {'a': 1, 'c': 3} + expected = {'items_removed': {'b': 2},'items_added': {'c': 3}} + #Assert items added adn removed + assert util.diff_dicts(d1, d2) == expected + d1 = {'a': 1} + d2 = {'b': 2} + expected = { + 'items_removed': {'a': 1}, + 'items_added': {'b': 2} + } + #Assert completely different dicts + assert util.diff_dicts(d1, d2) == expected diff --git a/testing/unit/ntp/ntp_module_test.py b/testing/unit/ntp/ntp_module_test.py index 20dd88ef1..ed5934048 100644 --- a/testing/unit/ntp/ntp_module_test.py +++ b/testing/unit/ntp/ntp_module_test.py @@ -11,12 +11,12 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -"""Module run all the DNS related unit tests""" +"""Module run all the NTP related unit tests""" from ntp_module import NTPModule import unittest from scapy.all import rdpcap, NTP, wrpcap import os -from testreport import TestReport +import sys MODULE = 'ntp' @@ -28,7 +28,6 @@ LOCAL_REPORT = os.path.join(REPORTS_DIR,'ntp_report_local.html') LOCAL_REPORT_NO_NTP = os.path.join(REPORTS_DIR,'ntp_report_local_no_ntp.html') -CONF_FILE = 'modules/test/' + MODULE + '/conf/module_config.json' # Define the capture files to be used for the test NTP_SERVER_CAPTURE_FILE = os.path.join(CAPTURES_DIR,'ntp.pcap') @@ -48,8 +47,6 @@ def setUpClass(cls): # Test the module report generation def ntp_module_report_test(self): ntp_module = NTPModule(module=MODULE, - log_dir=OUTPUT_DIR, - conf_file=CONF_FILE, results_dir=OUTPUT_DIR, ntp_server_capture_file=NTP_SERVER_CAPTURE_FILE, startup_capture_file=STARTUP_CAPTURE_FILE, @@ -60,12 +57,6 @@ def ntp_module_report_test(self): # Read the generated report with open(report_out_path, 'r', encoding='utf-8') as file: report_out = file.read() - formatted_report = self.add_formatting(report_out) - - # Write back the new formatted_report value - out_report_path = os.path.join(OUTPUT_DIR, 'ntp_report_with_ntp.html') - with open(out_report_path, 'w', encoding='utf-8') as file: - file.write(formatted_report) # Read the local good report with open(LOCAL_REPORT, 'r', encoding='utf-8') as file: @@ -104,8 +95,6 @@ def ntp_module_report_no_ntp_test(self): wrpcap(monitor_cap_file, packets_monitor) ntp_module = NTPModule(module='dns', - log_dir=OUTPUT_DIR, - conf_file=CONF_FILE, results_dir=OUTPUT_DIR, ntp_server_capture_file=ntp_server_cap_file, startup_capture_file=startup_cap_file, @@ -116,12 +105,6 @@ def ntp_module_report_no_ntp_test(self): # Read the generated report with open(report_out_path, 'r', encoding='utf-8') as file: report_out = file.read() - formatted_report = self.add_formatting(report_out) - - # Write back the new formatted_report value - out_report_path = os.path.join(OUTPUT_DIR,'ntp_report_no_ntp.html') - with open(out_report_path, 'w', encoding='utf-8') as file: - file.write(formatted_report) # Read the local good report with open(LOCAL_REPORT_NO_NTP, 'r', encoding='utf-8') as file: @@ -129,16 +112,6 @@ def ntp_module_report_no_ntp_test(self): self.assertEqual(report_out, report_local) - def add_formatting(self,body): - return f''' - - - {TestReport().generate_head()} - - {body} - - NTP Module +

NTP Module

@@ -15,6 +15,7 @@

NTP Module

+
101 104
@@ -24,1444 +25,90 @@

NTP Module

- + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - + + - + + - + + - - - - - - - - - - - - - - - + + - + + - + +
Destination Type VersionTimestampCountSync Request Average
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:12:29.447
10.10.10.510.10.10.15Server4Feb 15, 2024 22:12:29.448
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:12:31.577
10.10.10.510.10.10.15Server4Feb 15, 2024 22:12:31.577
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:12:33.694
10.10.10.510.10.10.15Server4Feb 15, 2024 22:12:33.694
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:12:35.785
10.10.10.510.10.10.15Server4Feb 15, 2024 22:12:35.786
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:12:37.806
10.10.10.510.10.10.15Server4Feb 15, 2024 22:12:37.806
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:12:39.856
10.10.10.510.10.10.15Server4Feb 15, 2024 22:12:39.856
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:12:41.931
10.10.10.510.10.10.15Server4Feb 15, 2024 22:12:41.932
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:12:43.954
10.10.10.510.10.10.15Server4Feb 15, 2024 22:12:43.956
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:13:06.439
10.10.10.510.10.10.15Server4Feb 15, 2024 22:13:06.439
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:13:08.492
10.10.10.510.10.10.15Server4Feb 15, 2024 22:13:08.494
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:13:40.536
10.10.10.510.10.10.15Server4Feb 15, 2024 22:13:40.541
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:13:48.274
10.10.10.510.10.10.15Server4Feb 15, 2024 22:13:48.277
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:14:12.619
10.10.10.510.10.10.15Server4Feb 15, 2024 22:14:12.624
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:14:44.702
10.10.10.510.10.10.15Server4Feb 15, 2024 22:14:44.703
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:14:53.026
10.10.10.510.10.10.15Server4Feb 15, 2024 22:14:53.029
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:15:16.786
10.10.10.510.10.10.15Server4Feb 15, 2024 22:15:16.791
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:15:48.884
10.10.10.510.10.10.15Server4Feb 15, 2024 22:15:48.887
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:15:57.829
10.10.10.510.10.10.15Server4Feb 15, 2024 22:15:57.829
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:16:20.970
10.10.10.510.10.10.15Server4Feb 15, 2024 22:16:20.970
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:16:54.054
10.10.10.510.10.10.15Server4Feb 15, 2024 22:16:54.054
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:17:02.738
10.10.10.510.10.10.15Server4Feb 15, 2024 22:17:02.740
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:17:26.136
10.10.10.510.10.10.15Server4Feb 15, 2024 22:17:26.139
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:17:59.293
10.10.10.510.10.10.15Server4Feb 15, 2024 22:17:59.293
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:18:07.242
10.10.10.510.10.10.15Server4Feb 15, 2024 22:18:07.242
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:18:32.379
10.10.10.510.10.10.15Server4Feb 15, 2024 22:18:32.379
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:20:06.908
10.10.10.510.10.10.15Server4Feb 15, 2024 22:20:06.908
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:20:08.936
10.10.10.510.10.10.15Server4Feb 15, 2024 22:20:08.937
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:20:10.974
10.10.10.510.10.10.15Server4Feb 15, 2024 22:20:10.974
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:20:12.998
10.10.10.510.10.10.15Server4Feb 15, 2024 22:20:12.999
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:20:59.581
10.10.10.510.10.10.15Server4Feb 15, 2024 22:20:59.582
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:21:34.063
10.10.10.510.10.10.15Server4Feb 15, 2024 22:21:34.063
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:21:36.121
10.10.10.510.10.10.15Server4Feb 15, 2024 22:21:36.121
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:21:38.176
10.10.10.510.10.10.15Server4Feb 15, 2024 22:21:38.176
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:21:40.277
10.10.10.510.10.10.15Server4Feb 15, 2024 22:21:40.277
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:22:05.704
10.10.10.510.10.10.15Server4Feb 15, 2024 22:22:05.706
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:22:45.469
10.10.10.510.10.10.15Server4Feb 15, 2024 22:22:45.470
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:23:09.826
10.10.10.510.10.10.15Server4Feb 15, 2024 22:23:09.828
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:23:50.337
10.10.10.510.10.10.15Server4Feb 15, 2024 22:23:50.343
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:24:13.945
10.10.10.510.10.10.15Server4Feb 15, 2024 22:24:13.946
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:24:54.876
10.10.10.510.10.10.15Server4Feb 15, 2024 22:24:54.877
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:25:59.000
10.10.10.510.10.10.15Server4Feb 15, 2024 22:25:59.001
10.10.10.15216.239.35.12Client4Feb 15, 2024 22:12:28.681
216.239.35.1210.10.10.15Server4Feb 15, 2024 22:12:28.728
10.10.10.15216.239.35.4Client4Feb 15, 2024 22:12:28.842
216.239.35.410.10.10.15Server4Feb 15, 2024 22:12:28.888
10.10.10.15216.239.35.8Client4Feb 15, 2024 22:12:29.042
216.239.35.810.10.10.15Server4Feb 15, 2024 22:12:29.089
10.10.10.15216.239.35.0Client4Feb 15, 2024 22:12:29.243
216.239.35.010.10.10.15Server4Feb 15, 2024 22:12:29.290
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:12:29.447
10.10.10.510.10.10.15Server4Feb 15, 2024 22:12:29.448
10.10.10.15216.239.35.12Client4Feb 15, 2024 22:12:30.802
216.239.35.1210.10.10.15Server4Feb 15, 2024 22:12:30.850
10.10.10.15216.239.35.4Client4Feb 15, 2024 22:12:30.973
216.239.35.410.10.10.15Server4Feb 15, 2024 22:12:31.032
10.10.10.15216.239.35.8Client4Feb 15, 2024 22:12:31.173
216.239.35.810.10.10.15Server4Feb 15, 2024 22:12:31.220
10.10.10.15216.239.35.0Client4Feb 15, 2024 22:12:31.376
216.239.35.010.10.10.15Server4Feb 15, 2024 22:12:31.423
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:12:31.577
10.10.10.510.10.10.15Server4Feb 15, 2024 22:12:31.577
10.10.10.15216.239.35.12Client4Feb 15, 2024 22:12:32.867
216.239.35.1210.10.10.15Server4Feb 15, 2024 22:12:32.914
10.10.10.15216.239.35.4Client4Feb 15, 2024 22:12:33.112
216.239.35.410.10.10.15Server4Feb 15, 2024 22:12:33.159
10.10.10.15216.239.35.8Client4Feb 15, 2024 22:12:33.271
216.239.35.810.10.10.15Server4Feb 15, 2024 22:12:33.318
10.10.10.15216.239.35.0Client4Feb 15, 2024 22:12:33.475
216.239.35.010.10.10.15Server4Feb 15, 2024 22:12:33.522
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:12:33.694
10.10.10.510.10.10.15Server4Feb 15, 2024 22:12:33.694
10.10.10.15216.239.35.12Client4Feb 15, 2024 22:12:34.956
216.239.35.1210.10.10.15Server4Feb 15, 2024 22:12:35.002
10.10.10.15216.239.35.4Client4Feb 15, 2024 22:12:35.182
216.239.35.410.10.10.15Server4Feb 15, 2024 22:12:35.228
10.10.10.15216.239.35.8Client4Feb 15, 2024 22:12:35.398
216.239.35.810.10.10.15Server4Feb 15, 2024 22:12:35.445
10.10.10.15216.239.35.0Client4Feb 15, 2024 22:12:35.625
216.239.35.010.10.10.15Server4Feb 15, 2024 22:12:35.673
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:12:35.785
10.10.10.510.10.10.15Server4Feb 15, 2024 22:12:35.786
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:12:37.806
10.10.10.510.10.10.15Server4Feb 15, 2024 22:12:37.806
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:12:39.856
10.10.10.510.10.10.15Server4Feb 15, 2024 22:12:39.856
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:12:41.931
10.10.10.510.10.10.15Server4Feb 15, 2024 22:12:41.932
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:12:43.954
10.10.10.510.10.10.15Server4Feb 15, 2024 22:12:43.956
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:13:06.439
10.10.10.15216.239.35.0Client4Feb 15, 2024 22:13:06.439
10.10.10.510.10.10.15Server4Feb 15, 2024 22:13:06.439
216.239.35.010.10.10.15Server4Feb 15, 2024 22:13:06.489
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:13:08.492
10.10.10.510.10.10.15Server4Feb 15, 2024 22:13:08.494
216.239.35.010.10.10.15Server4Feb 15, 2024 22:13:08.543
10.10.10.15216.239.35.12Client4Feb 15, 2024 22:13:40.310
216.239.35.1210.10.10.15Server4Feb 15, 2024 22:13:40.357
10.10.10.15216.239.35.4Client4Feb 15, 2024 22:13:40.512
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:13:40.536
10.10.10.510.10.10.15Server4Feb 15, 2024 22:13:40.542
216.239.35.410.10.10.15Server4Feb 15, 2024 22:13:40.574
216.239.35.010.10.10.15Server4Feb 15, 2024 22:13:40.583
10.10.10.15216.239.35.8Client4Feb 15, 2024 22:13:40.714
216.239.35.810.10.10.15Server4Feb 15, 2024 22:13:40.764
10.10.10.15216.239.35.0Client4Feb 15, 2024 22:13:40.917
216.239.35.010.10.10.15Server4Feb 15, 2024 22:13:40.965
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:13:48.274
10.10.10.510.10.10.15Server4Feb 15, 2024 22:13:48.277
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:14:12.619
10.10.10.510.10.10.15Server4Feb 15, 2024 22:14:12.624
216.239.35.010.10.10.15Server4Feb 15, 2024 22:14:12.668
10.10.10.15216.239.35.12Client4Feb 15, 2024 22:14:44.515
216.239.35.1210.10.10.15Server4Feb 15, 2024 22:14:44.562
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:14:44.702
10.10.10.510.10.10.15Server4Feb 15, 2024 22:14:44.704
10.10.10.15216.239.35.4Client4Feb 15, 2024 22:14:45.158
216.239.35.410.10.10.15Server4Feb 15, 2024 22:14:45.219
10.10.10.15216.239.35.0Client4Feb 15, 2024 22:14:45.359
216.239.35.010.10.10.15Server4Feb 15, 2024 22:14:45.406
10.10.10.15216.239.35.0Client4Feb 15, 2024 22:14:45.707
216.239.35.010.10.10.15Server4Feb 15, 2024 22:14:45.755
10.10.10.15216.239.35.8Client4Feb 15, 2024 22:14:45.980
216.239.35.810.10.10.15Server4Feb 15, 2024 22:14:46.027
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:14:53.026
10.10.10.510.10.10.15Server4Feb 15, 2024 22:14:53.029
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:15:16.786
10.10.10.510.10.10.15Server4Feb 15, 2024 22:15:16.791
10.10.10.15216.239.35.0Client4Feb 15, 2024 22:15:18.794
216.239.35.010.10.10.15Server4Feb 15, 2024 22:15:18.843
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:15:48.884
10.10.10.510.10.10.15Server4Feb 15, 2024 22:15:48.887
10.10.10.15 216.239.35.12 Client 4Feb 15, 2024 22:15:49.063837.942 seconds
216.239.35.12 10.10.10.15 Server 4Feb 15, 2024 22:15:49.110
10.10.10.15216.239.35.4Client4Feb 15, 2024 22:15:49.462
216.239.35.410.10.10.15Server4Feb 15, 2024 22:15:49.509
10.10.10.15216.239.35.0Client4Feb 15, 2024 22:15:50.127
216.239.35.010.10.10.15Server4Feb 15, 2024 22:15:50.175
10.10.10.15216.239.35.8Client4Feb 15, 2024 22:15:51.107
216.239.35.810.10.10.15Server4Feb 15, 2024 22:15:51.154
10.10.10.15216.239.35.0Client4Feb 15, 2024 22:15:51.890
216.239.35.010.10.10.15Server4Feb 15, 2024 22:15:51.938
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:15:57.829
10.10.10.510.10.10.15Server4Feb 15, 2024 22:15:57.829
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:16:20.970
10.10.10.510.10.10.15Server4Feb 15, 2024 22:16:20.971
10.10.10.15216.239.35.0Client4Feb 15, 2024 22:16:24.975
216.239.35.010.10.10.15Server4Feb 15, 2024 22:16:25.0238N/A
10.10.10.15 216.239.35.4 Client 4Feb 15, 2024 22:16:53.677837.834 seconds
216.239.35.4 10.10.10.15 Server 4Feb 15, 2024 22:16:53.739
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:16:54.054
10.10.10.510.10.10.15Server4Feb 15, 2024 22:16:54.054
10.10.10.15216.239.35.12Client4Feb 15, 2024 22:16:54.276
216.239.35.1210.10.10.15Server4Feb 15, 2024 22:16:54.322
10.10.10.15216.239.35.0Client4Feb 15, 2024 22:16:54.593
216.239.35.010.10.10.15Server4Feb 15, 2024 22:16:54.6488N/A
10.10.10.15 216.239.35.8 Client 4Feb 15, 2024 22:16:55.435838.056 seconds
216.239.35.8 10.10.10.15 Server 4Feb 15, 2024 22:16:55.4818N/A
10.10.10.15 216.239.35.0 Client 4Feb 15, 2024 22:16:57.0591420.601 seconds
216.239.35.0 10.10.10.15 Server 4Feb 15, 2024 22:16:57.107
10.10.10.1510.10.10.5Client4Feb 15, 2024 22:17:02.738
10.10.10.510.10.10.15Server4Feb 15, 2024 22:17:02.74017N/A
10.10.10.15 10.10.10.5 Client 4Feb 15, 2024 22:17:26.1366313.057 seconds
10.10.10.5 10.10.10.15 Server 4Feb 15, 2024 22:17:26.13963N/A
diff --git a/testing/unit/ntp/reports/ntp_report_local_no_ntp.html b/testing/unit/ntp/reports/ntp_report_local_no_ntp.html index 7df0fbd87..c93ab885f 100644 --- a/testing/unit/ntp/reports/ntp_report_local_no_ntp.html +++ b/testing/unit/ntp/reports/ntp_report_local_no_ntp.html @@ -1,4 +1,4 @@ -

NTP Module

+

NTP Module

@@ -15,6 +15,7 @@

NTP Module

+
0 0
diff --git a/testing/unit/protocol/protocol_module_test.py b/testing/unit/protocol/protocol_module_test.py index 32a0021cd..9d474ab91 100644 --- a/testing/unit/protocol/protocol_module_test.py +++ b/testing/unit/protocol/protocol_module_test.py @@ -15,6 +15,7 @@ from protocol_bacnet import BACnet import unittest import os +import sys from common import logger import inspect @@ -46,7 +47,6 @@ def setUpClass(cls): BACNET = BACnet(log=LOGGER, captures_dir=CAPTURES_DIR, capture_file='bacnet.pcap', - bin_dir='modules/test/protocol/bin', device_hw_addr=HW_ADDR) # Test the BACNet traffic for a matching Object ID and HW address @@ -103,4 +103,9 @@ def bacnet_protocol_validate_device_fail_test(self): suite.addTest(ProtocolModuleTest('bacnet_protocol_validate_device_fail_test')) runner = unittest.TextTestRunner() - runner.run(suite) + test_result = runner.run(suite) + + # Check if the tests failed and exit with the appropriate code + if not test_result.wasSuccessful(): + sys.exit(1) # Return a non-zero exit code for failures + sys.exit(0) # Return zero for success diff --git a/testing/unit/report/report_compliant.json b/testing/unit/report/report_compliant.json index 17e994d20..08ff585ad 100644 --- a/testing/unit/report/report_compliant.json +++ b/testing/unit/report/report_compliant.json @@ -68,7 +68,7 @@ }, { "name": "connection.switch.arp_inspection", - "description": "Device uses ARP", + "description": "Device uses ARP correctly", "expected_behavior": "Device continues to operate correctly when ARP inspection is enabled on the switch. No functionality is lost with ARP inspection enabled.", "required_result": "Required", "result": "Compliant" diff --git a/testing/unit/report/report_noncompliant.json b/testing/unit/report/report_noncompliant.json index 98fbeb284..b3ba74c0d 100644 --- a/testing/unit/report/report_noncompliant.json +++ b/testing/unit/report/report_noncompliant.json @@ -77,7 +77,7 @@ }, { "name": "connection.switch.arp_inspection", - "description": "Device uses ARP", + "description": "Device uses ARP correctly", "expected_behavior": "Device continues to operate correctly when ARP inspection is enabled on the switch. No functionality is lost with ARP inspection enabled.", "required_result": "Required", "result": "Compliant" diff --git a/testing/unit/report/report_test.py b/testing/unit/report/report_test.py index f92666b2c..e5c8b61a5 100644 --- a/testing/unit/report/report_test.py +++ b/testing/unit/report/report_test.py @@ -16,6 +16,9 @@ from testreport import TestReport import os import json +import shutil +from jinja2 import Template +import re MODULE = 'report' @@ -24,57 +27,171 @@ TEST_FILES_DIR = os.path.join('testing/unit', MODULE) OUTPUT_DIR = os.path.join(TEST_FILES_DIR, 'output/') +REPORT_RESOURCES_DIR = 'resources/report' + +CSS_PATH = os.path.join(REPORT_RESOURCES_DIR, 'test_report_styles.css') +HTML_PATH = os.path.join(REPORT_RESOURCES_DIR, 'test_report_template.html') class ReportTest(unittest.TestCase): """Contains and runs all the unit tests concerning DNS behaviors""" @classmethod def setUpClass(cls): + """Class-level setup to prepare for tests""" + + # Delete old files from output dir + if os.path.exists(OUTPUT_DIR) and os.path.isdir(OUTPUT_DIR): + shutil.rmtree(OUTPUT_DIR) + # Create the output directories and ignore errors if it already exists os.makedirs(OUTPUT_DIR, exist_ok=True) def create_report(self, results_file_path): + """Create the HTML report from the JSON file""" + + # Create the TestReport object report = TestReport() + # Load the json report data with open(results_file_path, 'r', encoding='utf-8') as file: report_json = json.loads(file.read()) + + # Populate the report with JSON data report.from_json(report_json) - # Load all module html reports + + # Load each module html report reports_md = [] - #reports_md.append(self.get_module_html_report('tls')) reports_md.append(self.get_module_html_report('dns')) reports_md.append(self.get_module_html_report('services')) reports_md.append(self.get_module_html_report('ntp')) + + # Add all the module reports to the full report report.add_module_reports(reports_md) - # Save report to file + # Create the HTML filename based on the JSON name file_name = os.path.splitext(os.path.basename(results_file_path))[0] report_out_file = os.path.join(OUTPUT_DIR, file_name + '.html') + + # Save report as HTML file with open(report_out_file, 'w', encoding='utf-8') as file: file.write(report.to_html()) def report_compliant_test(self): + """Generate a report for the compliant test""" + + # Generate a compliant report based on the 'report_compliant.json' file self.create_report(os.path.join(TEST_FILES_DIR, 'report_compliant.json')) def report_noncompliant_test(self): + """Generate a report for the non-compliant test""" + + # Generate non-compliant report based on the 'report_noncompliant.json' file self.create_report(os.path.join(TEST_FILES_DIR, 'report_noncompliant.json')) + # Generate formatted reports for each report generated from + # the test containers. + # Not a unit test but can't run from within the test module container and must + # be done through the venv. Useful for doing visual inspections + # of report formatting changes without having to re-run a new device test. + def report_formatting(self): + """Apply formatting and generate HTML reports for visual inspection""" + + # List of modules for which to generate formatted reports + test_modules = ['conn','dns','ntp','protocol','services','tls'] + + # List all items from UNIT_TEST_DIR + unit_tests = os.listdir(UNIT_TEST_DIR) + + # Loop through each items from UNIT_TEST_DIR + for test in unit_tests: + + # If the module name inside the test_modules list + if test in test_modules: + + # Construct the module path of outpit dir for the module + output_dir = os.path.join(UNIT_TEST_DIR,test,'output') + + # Check if output dir exists + if os.path.isdir(output_dir): + # List all files fro output dir + output_files = os.listdir(output_dir) + + # Loop through each file + for file in output_files: + + # Chck if is an html file + if file.endswith('.html'): + + # Construct teh full path of html file + report_out_path = os.path.join(output_dir,file) + + # Open the html file in read mode + with open(report_out_path, 'r', encoding='utf-8') as f: + report_out = f.read() + # Add the formatting + formatted_report = self.add_html_formatting(report_out) + + # Write back the new formatted_report value + out_report_dir = os.path.join(OUTPUT_DIR, test) + os.makedirs(out_report_dir, exist_ok=True) + + with open(os.path.join( + out_report_dir,file), 'w', + encoding='utf-8') as f: + f.write(formatted_report) + + def add_html_formatting(self, body): + """Wrap the raw report inside a complete HTML structure with styles""" + + # Load the css file + with open(CSS_PATH, 'r', encoding='UTF-8') as css_file: + styles = css_file.read() + + # Load the html file + with open(HTML_PATH, 'r', encoding='UTF-8') as html_file: + html_content = html_file.read() + + # Search for head content using regex + head = re.search(r'.*?', html_content, re.DOTALL).group(0) + # Define the html template + html_template = f''' + + + {head} + + {body} + + + ''' + # Create a Jinja2 template from the string + template = Template(html_template) + + # Render the template with css styles + return template.render(styles=styles, body=body) + def get_module_html_report(self, module): - # Combine the path components using os.path.join + """Load the HTML report for a specific module""" + + # Define the path to the module's HTML report file report_file = os.path.join( UNIT_TEST_DIR, os.path.join(module, os.path.join('reports', module + '_report_local.html'))) + # Read and return the content of the report file with open(report_file, 'r', encoding='utf-8') as file: report = file.read() return report if __name__ == '__main__': + suite = unittest.TestSuite() suite.addTest(ReportTest('report_compliant_test')) suite.addTest(ReportTest('report_noncompliant_test')) + # Create html test reports for each module in 'output' dir + suite.addTest(ReportTest('report_formatting')) + runner = unittest.TextTestRunner() runner.run(suite) diff --git a/testing/unit/risk_profile/profiles/risk_profile_valid_high.json b/testing/unit/risk_profile/profiles/risk_profile_valid_high.json index 338c91abb..c4887535a 100644 --- a/testing/unit/risk_profile/profiles/risk_profile_valid_high.json +++ b/testing/unit/risk_profile/profiles/risk_profile_valid_high.json @@ -1,52 +1,62 @@ { - "name": "Primary profile", - "version": "1.3-alpha", - "created": "2024-07-01", + "name": "Primary Profile High Risk", + "version": "1.4-a", + "created": "2024-10-01", "status": "Valid", + "risk": "High", "questions": [ { "question": "What type of device is this?", - "answer": "IoT Gateway" + "answer": "IoT Gateway", + "risk": "High" }, { "question": "How will this device be used at Google?", - "answer": "sakjdhaskjdh" + "answer": "Controlling things" }, { "question": "Is this device going to be managed by Google or a third party?", - "answer": "Google" + "answer": "Google", + "risk": "Limited" }, { "question": "Will the third-party device administrator be able to grant access to authorized Google personnel upon request?", - "answer": "N/A" + "answer": "Yes", + "risk": "Limited" }, { "question": "Are any of the following statements true about your device?", "answer": [ - 3 - ] + 2 + ], + "risk": "High" }, { "question": "Which of the following statements are true about this device?", "answer": [ - 5 - ] + 0, + 1 + ], + "risk": "High" }, { "question": "Does the network protocol assure server-to-client identity verification?", - "answer": "Yes" + "answer": "No", + "risk": "High" }, { "question": "Click the statements that best describe the characteristics of this device.", "answer": [ - 5 - ] + 2 + ], + "risk": "High" }, { "question": "Are any of the following statements true about this device?", "answer": [ - 6 - ] + 0 + ], + "risk": "High" }, { "question": "Comments", diff --git a/testing/unit/risk_profile/profiles/risk_profile_valid_limited.json b/testing/unit/risk_profile/profiles/risk_profile_valid_limited.json index fba02d4ba..09905fe1d 100644 --- a/testing/unit/risk_profile/profiles/risk_profile_valid_limited.json +++ b/testing/unit/risk_profile/profiles/risk_profile_valid_limited.json @@ -1,52 +1,61 @@ { - "name": "Primary profile", - "version": "1.3-alpha", - "created": "2024-07-01", + "name": "Primary Profile Limited Risk", + "version": "1.4-a", + "created": "2024-10-01", "status": "Valid", + "risk": "Limited", "questions": [ { "question": "What type of device is this?", - "answer": "Sensor - Lighting" + "answer": "Controller - Lighting", + "risk": "Limited" }, { "question": "How will this device be used at Google?", - "answer": "sakjdhaskjdh" + "answer": "Controlling Lights" }, { "question": "Is this device going to be managed by Google or a third party?", - "answer": "Google" + "answer": "Google", + "risk": "Limited" }, { "question": "Will the third-party device administrator be able to grant access to authorized Google personnel upon request?", - "answer": "N/A" + "answer": "N/A", + "risk": "Limited" }, { "question": "Are any of the following statements true about your device?", "answer": [ 3 - ] + ], + "risk": "Limited" }, { "question": "Which of the following statements are true about this device?", "answer": [ 5 - ] + ], + "risk": "Limited" }, { "question": "Does the network protocol assure server-to-client identity verification?", - "answer": "Yes" + "answer": "Yes", + "risk": "Limited" }, { "question": "Click the statements that best describe the characteristics of this device.", "answer": [ 5 - ] + ], + "risk": "Limited" }, { "question": "Are any of the following statements true about this device?", "answer": [ 6 - ] + ], + "risk": "Limited" }, { "question": "Comments", diff --git a/testing/unit/risk_profile/risk_profile_test.py b/testing/unit/risk_profile/risk_profile_test.py index 23cce43d3..ab5f8f1f2 100644 --- a/testing/unit/risk_profile/risk_profile_test.py +++ b/testing/unit/risk_profile/risk_profile_test.py @@ -15,6 +15,7 @@ import unittest import os import json +import sys from risk_profile import RiskProfile SECONDS_IN_YEAR = 31536000 @@ -35,9 +36,9 @@ class RiskProfileTest(unittest.TestCase): def setUpClass(cls): # Create the output directories and ignore errors if it already exists os.makedirs(OUTPUT_DIR, exist_ok=True) - with open(os.path.join('resources', - 'risk_assessment.json'), - 'r', encoding='utf-8') as file: + with open(os.path.join('resources', 'risk_assessment.json'), + 'r', + encoding='utf-8') as file: cls.profile_format = json.loads(file.read()) def risk_profile_high_test(self): @@ -81,7 +82,6 @@ def risk_profile_rename_test(self): with open(risk_profile_path, 'r', encoding='utf-8') as file: risk_profile_json = json.loads(file.read()) - # Create the RiskProfile object from the json file risk_profile = RiskProfile(risk_profile_json, self.profile_format) @@ -158,6 +158,7 @@ def risk_profile_update_risk_test(self): # Risk should now be limited after update self.assertEqual(risk_profile.risk, 'Limited') + if __name__ == '__main__': suite = unittest.TestSuite() @@ -169,4 +170,9 @@ def risk_profile_update_risk_test(self): suite.addTest(RiskProfileTest('risk_profile_update_risk_test')) runner = unittest.TextTestRunner() - runner.run(suite) + test_result = runner.run(suite) + + # Check if the tests failed and exit with the appropriate code + if not test_result.wasSuccessful(): + sys.exit(1) # Return a non-zero exit code for failures + sys.exit(0) # Return zero for success diff --git a/testing/unit/run.sh b/testing/unit/run.sh old mode 100644 new mode 100755 index 72ca9dcb0..90f51ac52 --- a/testing/unit/run.sh +++ b/testing/unit/run.sh @@ -14,4 +14,59 @@ # See the License for the specific language governing permissions and # limitations under the License. -sudo docker run --rm -it --name unit-test testrun/unit-test /bin/bash ./run_tests.sh \ No newline at end of file +# Must be run from the root directory of Testrun +run_test() { + local MODULE_NAME=$1 + shift + local DIRS=("$@") + + # Define the locations of the unit test files + local UNIT_TEST_DIR_SRC="$PWD/testing/unit/$MODULE_NAME" + local UNIT_TEST_FILE_SRC="$UNIT_TEST_DIR_SRC/${MODULE_NAME}_module_test.py" + + # Define the location in the container to + # load the unit test files + local UNIT_TEST_DIR_DST="/testing/unit/$MODULE_NAME" + local UNIT_TEST_FILE_DST="/testrun/python/src/module_test.py" + + # Build the docker run command + local DOCKER_CMD="sudo docker run --rm -it --name ${MODULE_NAME}-unit-test" + + + # Add volume mounts for the main test file + DOCKER_CMD="$DOCKER_CMD -v $UNIT_TEST_FILE_SRC:$UNIT_TEST_FILE_DST" + + # Add volume mounts for additional directories + for DIR in "${DIRS[@]}"; do + DOCKER_CMD="$DOCKER_CMD -v $UNIT_TEST_DIR_SRC/$DIR:$UNIT_TEST_DIR_DST/$DIR" + done + + # Add the container image and entry point + DOCKER_CMD="$DOCKER_CMD testrun/${MODULE_NAME}-test $UNIT_TEST_FILE_DST" + + # Execute the docker command + eval $DOCKER_CMD +} + +# Run all test module tests from within their containers +run_test "conn" "captures" "ethtool" "output" +run_test "dns" "captures" "reports" "output" +run_test "ntp" "captures" "reports" "output" +run_test "protocol" "captures" "output" +run_test "services" "reports" "results" "output" +run_test "tls" "captures" "CertAuth" "certs" "reports" "root_certs" "output" + +# Activate Python virtual environment +source venv/bin/activate + +# Add the framework sources +PYTHONPATH="$PWD/framework/python/src:$PWD/framework/python/src/common" + +# Set the python path with all sources +export PYTHONPATH + +# Run all host level unit tests from within the venv +python3 testing/unit/risk_profile/risk_profile_test.py +python3 testing/unit/report/report_test.py + +deactivate \ No newline at end of file diff --git a/testing/unit/run_report_test.sh b/testing/unit/run_report_test.sh new file mode 100644 index 000000000..49f4ca6c2 --- /dev/null +++ b/testing/unit/run_report_test.sh @@ -0,0 +1,67 @@ +#!/bin/bash -e + +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Must be run from the root directory of Testrun +run_test(){ + + local REPORT_TEST_FILE=$1 + + # Activate Python virtual environment + source venv/bin/activate + + # Add the framework sources + PYTHONPATH="$PWD/framework/python/src:$PWD/framework/python/src/common" + + # Set the python path with all sources + export PYTHONPATH + + # Temporarily disable 'set -e' to capture exit code + set +e + + # Run all host level unit tests from within the venv + python3 $REPORT_TEST_FILE + + # Capture the exit code + local exit_code=$? + + deactivate + + # Return the captured exit code to the caller + return $exit_code +} + + +# Check if the script received any arguments +if [[ $# -lt 1 ]]; then + echo "Usage: $0 " + exit 1 +fi + +# Call the run_test function with the provided arguments +run_test "$@" + +# Capture the exit code from the run_test function +exit_code=$? + +# If the exit code is not zero, print an error message +if [ $exit_code -ne 0 ]; then + echo "Tests failed with exit code $exit_code" +else + echo "All tests passed successfully." +fi + +# Exit with the captured exit code +exit $exit_code \ No newline at end of file diff --git a/testing/unit/run_test_module.sh b/testing/unit/run_test_module.sh new file mode 100644 index 000000000..8e31e6860 --- /dev/null +++ b/testing/unit/run_test_module.sh @@ -0,0 +1,82 @@ +#!/bin/bash -e + +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Must be run from the root directory of Testrun + +# Read the JSON file into a variable +DEVICE_TEST_PACK=$( [directories...]" + exit 1 +fi + +# Call the run_test function with the provided arguments +run_test "$@" + +# Capture the exit code from the run_test function +exit_code=$? + +# If the exit code is not zero, print an error message +if [ $exit_code -ne 0 ]; then + echo "Tests failed with exit code $exit_code" +else + echo "All tests passed successfully." +fi + +# Exit with the captured exit code +exit $exit_code diff --git a/testing/unit/run_tests.sh b/testing/unit/run_tests.sh deleted file mode 100644 index 48c667934..000000000 --- a/testing/unit/run_tests.sh +++ /dev/null @@ -1,69 +0,0 @@ -#!/bin/bash -e - -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# This script should be run from within the unit_test directory. If -# it is run outside this directory, paths will not be resolved correctly. - -# Move into the root directory of test-run -pushd ../../ >/dev/null 2>&1 - -echo "Root dir: $PWD" - -# Add the framework sources -PYTHONPATH="$PWD/framework/python/src:$PWD/framework/python/src/common" - -# Add the test module sources -PYTHONPATH="$PYTHONPATH:$PWD/modules/test/base/python/src" -PYTHONPATH="$PYTHONPATH:$PWD/modules/test/conn/python/src" -PYTHONPATH="$PYTHONPATH:$PWD/modules/test/tls/python/src" -PYTHONPATH="$PYTHONPATH:$PWD/modules/test/dns/python/src" -PYTHONPATH="$PYTHONPATH:$PWD/modules/test/services/python/src" -PYTHONPATH="$PYTHONPATH:$PWD/modules/test/ntp/python/src" -PYTHONPATH="$PYTHONPATH:$PWD/modules/test/protocol/python/src" - - -# Set the python path with all sources -export PYTHONPATH - -# Run the DHCP Unit tests -python3 -u $PWD/modules/network/dhcp-1/python/src/grpc_server/dhcp_config_test.py -python3 -u $PWD/modules/network/dhcp-2/python/src/grpc_server/dhcp_config_test.py - -# Run the Conn Module Unit Tests -python3 -u $PWD/testing/unit/conn/conn_module_test.py - -# Run the TLS Module Unit Tests -python3 -u $PWD/testing/unit/tls/tls_module_test.py - -# Run the DNS Module Unit Tests -python3 -u $PWD/testing/unit/dns/dns_module_test.py - -# Run the NMAP Module Unit Tests -python3 -u $PWD/testing/unit/services/services_module_test.py - -# Run the NTP Module Unit Tests -python3 -u $PWD/testing/unit/ntp/ntp_module_test.py - -# Run the Report Unit Tests -python3 -u $PWD/testing/unit/report/report_test.py - -# Run the RiskProfile Unit Tests -python3 -u $PWD/testing/unit/risk_profile/risk_profile_test.py - -# Run the RiskProfile Unit Tests -python3 -u $PWD/testing/unit/protocol/protocol_module_test.py - -popd >/dev/null 2>&1 diff --git a/testing/unit/services/output/services.log b/testing/unit/services/output/services.log deleted file mode 100644 index 7df3f745b..000000000 --- a/testing/unit/services/output/services.log +++ /dev/null @@ -1,6 +0,0 @@ -Jun 17 09:23:01 test_services INFO Module report generated at: testing/unit/services/output/services_report.html -Jun 17 09:23:01 test_services INFO Module report generated at: testing/unit/services/output/services_report.html -Jun 17 09:23:01 test_services INFO Module report generated at: testing/unit/services/output/services_report.html -Jun 17 09:32:48 test_services INFO Module report generated at: testing/unit/services/output/services_report.html -Jun 17 09:32:48 test_services INFO Module report generated at: testing/unit/services/output/services_report.html -Jun 17 09:32:48 test_services INFO Module report generated at: testing/unit/services/output/services_report.html diff --git a/testing/unit/services/reports/services_report_all_closed_local.html b/testing/unit/services/reports/services_report_all_closed_local.html index a726762d4..356a82d35 100644 --- a/testing/unit/services/reports/services_report_all_closed_local.html +++ b/testing/unit/services/reports/services_report_all_closed_local.html @@ -1,4 +1,4 @@ -

Services Module

+

Services Module

diff --git a/testing/unit/services/reports/services_report_local.html b/testing/unit/services/reports/services_report_local.html index c27973a2c..ce601cfc0 100644 --- a/testing/unit/services/reports/services_report_local.html +++ b/testing/unit/services/reports/services_report_local.html @@ -1,4 +1,4 @@ -

Services Module

+

Services Module

diff --git a/testing/unit/services/services_module_test.py b/testing/unit/services/services_module_test.py index 30c4928bf..ccd4b730e 100644 --- a/testing/unit/services/services_module_test.py +++ b/testing/unit/services/services_module_test.py @@ -15,8 +15,9 @@ from services_module import ServicesModule import unittest import os +import sys import shutil -from testreport import TestReport +# from testreport import TestReport MODULE = 'services' @@ -29,8 +30,6 @@ LOCAL_REPORT = os.path.join(REPORTS_DIR, 'services_report_local.html') LOCAL_REPORT_ALL_CLOSED = os.path.join(REPORTS_DIR, 'services_report_all_closed_local.html') -CONF_FILE = 'modules/test/' + MODULE + '/conf/module_config.json' - class ServicesTest(unittest.TestCase): """Contains and runs all the unit tests concerning DNS behaviors""" @@ -50,8 +49,6 @@ def services_module_ports_open_report_test(self): shutil.copy(src_scan_results_path, dst_scan_results_path) services_module = ServicesModule(module=MODULE, - log_dir=OUTPUT_DIR, - conf_file=CONF_FILE, results_dir=OUTPUT_DIR, run=False, nmap_scan_results_path=OUTPUT_DIR) @@ -61,13 +58,6 @@ def services_module_ports_open_report_test(self): # Read the generated report with open(report_out_path, 'r', encoding='utf-8') as file: report_out = file.read() - formatted_report = self.add_formatting(report_out) - - # Write back the new formatted_report value - out_report_path = os.path.join( - OUTPUT_DIR, 'services_report_ports_open.html') - with open(out_report_path, 'w', encoding='utf-8') as file: - file.write(formatted_report) # Read the local good report with open(LOCAL_REPORT, 'r', encoding='utf-8') as file: @@ -84,8 +74,6 @@ def services_module_report_all_closed_test(self): shutil.copy(src_scan_results_path, dst_scan_results_path) services_module = ServicesModule(module=MODULE, - log_dir=OUTPUT_DIR, - conf_file=CONF_FILE, results_dir=OUTPUT_DIR, run=False, nmap_scan_results_path=OUTPUT_DIR) @@ -95,13 +83,6 @@ def services_module_report_all_closed_test(self): # Read the generated report with open(report_out_path, 'r', encoding='utf-8') as file: report_out = file.read() - formatted_report = self.add_formatting(report_out) - - # Write back the new formatted_report value - out_report_path = os.path.join( - OUTPUT_DIR, 'services_report_all_closed.html') - with open(out_report_path, 'w', encoding='utf-8') as file: - file.write(formatted_report) # Read the local good report with open(LOCAL_REPORT_ALL_CLOSED, 'r', encoding='utf-8') as file: @@ -109,17 +90,6 @@ def services_module_report_all_closed_test(self): self.assertEqual(report_out, report_local) - def add_formatting(self, body): - return f''' - - - {TestReport().generate_head()} - - {body} - -